From 9ba375c06344835d783fe60bf33f857f9bc208a4 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 18 Nov 2019 18:18:28 -0800 Subject: [PATCH] Initial commit --- .appveyor.yml | 19 + .ci/node10/Dockerfile.linux | 17 + .ci/node12/Dockerfile.linux | 17 + .ci/node8/Dockerfile.linux | 17 + .cirrus.yml | 47 + .editorconfig | 9 + .eslintignore | 12 + .eslintrc.js | 107 + .gitignore | 81 +- .npmignore | 44 + .travis.yml | 48 + DeviceDescriptors.js | 23 + Errors.js | 17 + browser_patches/README.md | 31 + browser_patches/check_cdn.sh | 58 + browser_patches/do_checkout.sh | 136 + browser_patches/export.sh | 115 + browser_patches/firefox/.gitignore | 1 + browser_patches/firefox/BASE_REVISION | 1 + browser_patches/firefox/BUILD_NUMBER | 2 + browser_patches/firefox/README.md | 22 + browser_patches/firefox/archive.sh | 53 + browser_patches/firefox/build.sh | 45 + .../patches/0001-chore-bootstrap.patch | 4895 ++++++++++++++ browser_patches/upload.sh | 68 + browser_patches/webkit/.gitignore | 1 + browser_patches/webkit/BASE_REVISION | 1 + browser_patches/webkit/BUILD_NUMBER | 2 + browser_patches/webkit/archive.sh | 85 + browser_patches/webkit/build.sh | 29 + browser_patches/webkit/concat_protocol.js | 6 + .../webkit/patches/0001-chore-bootstrap.patch | 5890 +++++++++++++++++ browser_patches/webkit/pw_run.sh | 40 + chromium.js | 30 + docs/api.md | 4107 ++++++++++++ examples/block-images.js | 36 + examples/custom-event.js | 48 + examples/detect-sniff.js | 44 + examples/pdf.js | 33 + examples/proxy.js | 35 + examples/screenshot-fullpage.js | 29 + examples/screenshot.js | 27 + examples/search.js | 55 + firefox.js | 29 + install.js | 137 + misc/00-playwright-prefs.js | 3 + misc/install-preferences.js | 59 + misc/playwright.cfg | 212 + package.json | 75 + src/.eslintrc.js | 16 + src/ConnectionTransport.ts | 6 + src/DeviceDescriptors.ts | 871 +++ src/Errors.ts | 26 + src/Events.ts | 55 + src/TimeoutSettings.ts | 45 + src/USKeyboardLayout.ts | 285 + src/api.ts | 88 + src/chromium/Accessibility.ts | 373 ++ src/chromium/Browser.ts | 220 + src/chromium/BrowserContext.ts | 103 + src/chromium/BrowserFetcher.ts | 261 + src/chromium/Connection.ts | 219 + src/chromium/Coverage.ts | 298 + src/chromium/DOMWorld.ts | 580 ++ src/chromium/Dialog.ts | 70 + src/chromium/EmulationManager.ts | 51 + src/chromium/ExecutionContext.ts | 171 + src/chromium/Frame.ts | 264 + src/chromium/FrameManager.ts | 316 + src/chromium/Input.ts | 337 + src/chromium/JSHandle.ts | 578 ++ src/chromium/Launcher.ts | 404 ++ src/chromium/LifecycleWatcher.ts | 176 + src/chromium/Multimap.ts | 93 + src/chromium/NetworkManager.ts | 657 ++ src/chromium/Page.ts | 1073 +++ src/chromium/PipeTransport.ts | 73 + src/chromium/Playwright.ts | 66 + src/chromium/Target.ts | 134 + src/chromium/TaskQueue.ts | 30 + src/chromium/Tracing.ts | 66 + src/chromium/WebSocketTransport.ts | 58 + src/chromium/Worker.ts | 63 + src/chromium/protocolHelper.ts | 97 + src/firefox/Accessibility.ts | 273 + src/firefox/Browser.ts | 334 + src/firefox/BrowserFetcher.ts | 283 + src/firefox/Connection.ts | 203 + src/firefox/DOMWorld.ts | 503 ++ src/firefox/Dialog.ts | 51 + src/firefox/ExecutionContext.ts | 100 + src/firefox/FrameManager.ts | 394 ++ src/firefox/Input.ts | 292 + src/firefox/JSHandle.ts | 360 + src/firefox/Launcher.ts | 268 + src/firefox/NavigationWatchdog.ts | 125 + src/firefox/NetworkManager.ts | 365 + src/firefox/Page.ts | 640 ++ src/firefox/Playwright.ts | 66 + src/firefox/WebSocketTransport.ts | 89 + src/helper.ts | 162 + src/webkit/Browser.ts | 244 + src/webkit/BrowserFetcher.ts | 253 + src/webkit/Connection.ts | 247 + src/webkit/ExecutionContext.ts | 303 + src/webkit/FrameManager.ts | 944 +++ src/webkit/Input.ts | 305 + src/webkit/JSHandle.ts | 377 ++ src/webkit/Launcher.ts | 166 + src/webkit/Multimap.ts | 93 + src/webkit/NetworkManager.ts | 312 + src/webkit/Page.ts | 568 ++ src/webkit/PipeTransport.ts | 73 + src/webkit/Playwright.ts | 54 + src/webkit/Target.ts | 95 + src/webkit/TaskQueue.ts | 30 + src/webkit/protocolHelper.ts | 55 + test/CDPSession.spec.js | 83 + test/accessibility.spec.js | 383 ++ test/assets/beforeunload.html | 10 + 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 | 11 + 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/file-to-upload.txt | 1 + test/assets/frames/frame.html | 8 + test/assets/frames/frameset.html | 8 + test/assets/frames/nested-frames.html | 25 + 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/injectedfile.js | 2 + test/assets/injectedstyle.css | 3 + test/assets/input/button.html | 22 + test/assets/input/checkbox.html | 42 + test/assets/input/fileupload.html | 9 + test/assets/input/keyboard.html | 42 + test/assets/input/mouse-helper.js | 62 + test/assets/input/rotatedButton.html | 21 + test/assets/input/scrollable.html | 23 + test/assets/input/select.html | 69 + test/assets/input/textarea.html | 19 + test/assets/input/touches.html | 35 + 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 | 36 + 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 | 3 + 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.js | 73 + test/browsercontext.spec.js | 156 + test/chromiumonly.spec.js | 144 + test/click.spec.js | 310 + test/cookies.spec.js | 395 ++ test/coverage.spec.js | 216 + test/defaultbrowsercontext.spec.js | 96 + test/dialog.spec.js | 50 + test/diffstyle.css | 13 + test/elementhandle.spec.js | 215 + test/emulation.spec.js | 187 + test/evaluation.spec.js | 305 + test/fixtures.spec.js | 79 + test/fixtures/closeme.js | 5 + test/fixtures/dumpio.js | 8 + test/frame.spec.js | 211 + 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 -> 326 bytes test/golden-chromium/screenshot-sanity.png | Bin 0 -> 36252 bytes test/golden-chromium/transparent.png | Bin 0 -> 119 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 -> 326 bytes test/golden-firefox/screenshot-sanity.png | Bin 0 -> 26146 bytes test/golden-utils.js | 149 + test/golden-webkit/grid-cell-0.png | Bin 0 -> 478 bytes test/golden-webkit/grid-cell-1.png | Bin 0 -> 304 bytes .../screenshot-clip-odd-size.png | Bin 0 -> 97 bytes test/golden-webkit/screenshot-clip-rect.png | Bin 0 -> 1988 bytes .../screenshot-element-bounding-box.png | Bin 0 -> 477 bytes .../screenshot-element-fractional-offset.png | Bin 0 -> 135 bytes .../screenshot-element-fractional.png | Bin 0 -> 136 bytes ...creenshot-element-larger-than-viewport.png | Bin 0 -> 2819 bytes .../screenshot-element-padding-border.png | Bin 0 -> 184 bytes .../screenshot-element-scrolled-into-view.png | Bin 0 -> 184 bytes .../screenshot-grid-fullpage.png | Bin 0 -> 75083 bytes .../screenshot-offscreen-clip.png | Bin 0 -> 1540 bytes test/golden-webkit/screenshot-sanity.png | Bin 0 -> 36299 bytes test/golden-webkit/transparent.png | Bin 0 -> 329 bytes test/headful.spec.js | 152 + test/ignorehttpserrors.spec.js | 97 + test/input.spec.js | 223 + test/jshandle.spec.js | 199 + test/keyboard.spec.js | 255 + test/launcher.spec.js | 427 ++ test/mouse.spec.js | 162 + test/navigation.spec.js | 574 ++ test/network.spec.js | 445 ++ test/oopif.spec.js | 61 + test/page.spec.js | 1265 ++++ test/playwright.spec.js | 180 + test/queryselector.spec.js | 205 + test/requestinterception.spec.js | 585 ++ test/run_static_server.js | 33 + test/screenshot.spec.js | 241 + test/target.spec.js | 184 + test/test.js | 123 + test/touchscreen.spec.js | 37 + test/tracing.spec.js | 93 + test/utils.js | 261 + test/waittask.spec.js | 445 ++ test/worker.spec.js | 66 + tsconfig.json | 15 + utils/ESTreeWalker.js | 136 + utils/apply_next_version.js | 25 + utils/bisect.js | 204 + utils/browser/README.md | 37 + utils/browser/WebSocket.js | 1 + utils/browser/test.js | 98 + utils/check_availability.js | 174 + utils/doclint/.gitignore | 1 + utils/doclint/Message.js | 44 + utils/doclint/README.md | 30 + utils/doclint/Source.js | 125 + .../doclint/check_public_api/Documentation.js | 137 + utils/doclint/check_public_api/JSBuilder.js | 232 + utils/doclint/check_public_api/MDBuilder.js | 302 + utils/doclint/check_public_api/index.js | 339 + .../doclint/check_public_api/test/.gitignore | 2 + .../test/check-duplicates/doc.md | 15 + .../test/check-duplicates/foo.js | 13 + .../test/check-duplicates/result.txt | 3 + .../test/check-returns/doc.md | 14 + .../test/check-returns/foo.js | 20 + .../test/check-returns/result.txt | 4 + .../test/check-sorting/Events.js | 8 + .../test/check-sorting/doc.md | 15 + .../test/check-sorting/foo.js | 12 + .../test/check-sorting/result.txt | 4 + .../test/diff-arguments/doc.md | 14 + .../test/diff-arguments/foo.js | 19 + .../test/diff-arguments/result.txt | 4 + .../check_public_api/test/diff-classes/doc.md | 5 + .../check_public_api/test/diff-classes/foo.js | 2 + .../test/diff-classes/other.js | 2 + .../test/diff-classes/result.txt | 3 + .../test/diff-events/Events.js | 8 + .../check_public_api/test/diff-events/doc.md | 5 + .../check_public_api/test/diff-events/foo.js | 3 + .../test/diff-events/result.txt | 2 + .../check_public_api/test/diff-methods/doc.md | 10 + .../check_public_api/test/diff-methods/foo.js | 16 + .../test/diff-methods/result.txt | 3 + .../test/diff-properties/doc.md | 5 + .../test/diff-properties/foo.js | 7 + .../test/diff-properties/result.txt | 2 + .../test/js-builder-common/Events.js | 6 + .../test/js-builder-common/foo.js | 13 + .../test/js-builder-common/result.txt | 50 + .../test/js-builder-inheritance/Events.js | 8 + .../test/js-builder-inheritance/foo.js | 15 + .../test/js-builder-inheritance/result.txt | 61 + .../test/md-builder-common/doc.md | 24 + .../test/md-builder-common/result.txt | 39 + utils/doclint/check_public_api/test/test.js | 127 + utils/doclint/cli.js | 94 + utils/doclint/generate_types/index.js | 221 + utils/doclint/generate_types/test/test.ts | 283 + .../doclint/generate_types/test/tsconfig.json | 12 + utils/doclint/preprocessor/index.js | 131 + utils/doclint/preprocessor/test.js | 215 + utils/fetch_devices.js | 241 + .../flakiness-dashboard/FlakinessDashboard.js | 218 + utils/flakiness-dashboard/index.js | 3 + utils/prepare_playwright_core.js | 25 + utils/protocol-types-generator/index.js | 117 + utils/testrunner/.npmignore | 13 + utils/testrunner/LICENSE | 202 + utils/testrunner/Matchers.js | 130 + utils/testrunner/Multimap.js | 95 + utils/testrunner/README.md | 61 + utils/testrunner/Reporter.js | 245 + utils/testrunner/TestRunner.js | 474 ++ utils/testrunner/examples/fail.js | 54 + utils/testrunner/examples/hookfail.js | 35 + utils/testrunner/examples/hooktimeout.js | 35 + utils/testrunner/examples/timeout.js | 32 + .../examples/unhandledpromiserejection.js | 35 + utils/testrunner/index.js | 21 + utils/testrunner/package.json | 18 + utils/testrunner/test/test.js | 15 + utils/testrunner/test/testrunner.spec.js | 610 ++ utils/testserver/LICENSE | 202 + utils/testserver/README.md | 18 + utils/testserver/cert.pem | 28 + utils/testserver/index.js | 272 + utils/testserver/key.pem | 52 + utils/testserver/package.json | 15 + webkit.js | 29 + 388 files changed, 50843 insertions(+), 61 deletions(-) create mode 100644 .appveyor.yml create mode 100644 .ci/node10/Dockerfile.linux create mode 100644 .ci/node12/Dockerfile.linux create mode 100644 .ci/node8/Dockerfile.linux create mode 100644 .cirrus.yml create mode 100644 .editorconfig create mode 100644 .eslintignore create mode 100644 .eslintrc.js create mode 100644 .npmignore create mode 100644 .travis.yml create mode 100644 DeviceDescriptors.js create mode 100644 Errors.js create mode 100644 browser_patches/README.md create mode 100755 browser_patches/check_cdn.sh create mode 100755 browser_patches/do_checkout.sh create mode 100755 browser_patches/export.sh create mode 100644 browser_patches/firefox/.gitignore create mode 100644 browser_patches/firefox/BASE_REVISION create mode 100644 browser_patches/firefox/BUILD_NUMBER create mode 100644 browser_patches/firefox/README.md create mode 100755 browser_patches/firefox/archive.sh create mode 100755 browser_patches/firefox/build.sh create mode 100644 browser_patches/firefox/patches/0001-chore-bootstrap.patch create mode 100755 browser_patches/upload.sh create mode 100644 browser_patches/webkit/.gitignore create mode 100644 browser_patches/webkit/BASE_REVISION create mode 100644 browser_patches/webkit/BUILD_NUMBER create mode 100755 browser_patches/webkit/archive.sh create mode 100755 browser_patches/webkit/build.sh create mode 100644 browser_patches/webkit/concat_protocol.js create mode 100644 browser_patches/webkit/patches/0001-chore-bootstrap.patch create mode 100755 browser_patches/webkit/pw_run.sh create mode 100644 chromium.js create mode 100644 docs/api.md create mode 100644 examples/block-images.js create mode 100644 examples/custom-event.js create mode 100644 examples/detect-sniff.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 firefox.js create mode 100644 install.js create mode 100644 misc/00-playwright-prefs.js create mode 100644 misc/install-preferences.js create mode 100644 misc/playwright.cfg create mode 100644 package.json create mode 100644 src/.eslintrc.js create mode 100644 src/ConnectionTransport.ts create mode 100644 src/DeviceDescriptors.ts create mode 100644 src/Errors.ts create mode 100644 src/Events.ts create mode 100644 src/TimeoutSettings.ts create mode 100644 src/USKeyboardLayout.ts create mode 100644 src/api.ts create mode 100644 src/chromium/Accessibility.ts create mode 100644 src/chromium/Browser.ts create mode 100644 src/chromium/BrowserContext.ts create mode 100644 src/chromium/BrowserFetcher.ts create mode 100644 src/chromium/Connection.ts create mode 100644 src/chromium/Coverage.ts create mode 100644 src/chromium/DOMWorld.ts create mode 100644 src/chromium/Dialog.ts create mode 100644 src/chromium/EmulationManager.ts create mode 100644 src/chromium/ExecutionContext.ts create mode 100644 src/chromium/Frame.ts create mode 100644 src/chromium/FrameManager.ts create mode 100644 src/chromium/Input.ts create mode 100644 src/chromium/JSHandle.ts create mode 100644 src/chromium/Launcher.ts create mode 100644 src/chromium/LifecycleWatcher.ts create mode 100644 src/chromium/Multimap.ts create mode 100644 src/chromium/NetworkManager.ts create mode 100644 src/chromium/Page.ts create mode 100644 src/chromium/PipeTransport.ts create mode 100644 src/chromium/Playwright.ts create mode 100644 src/chromium/Target.ts create mode 100644 src/chromium/TaskQueue.ts create mode 100644 src/chromium/Tracing.ts create mode 100644 src/chromium/WebSocketTransport.ts create mode 100644 src/chromium/Worker.ts create mode 100644 src/chromium/protocolHelper.ts create mode 100644 src/firefox/Accessibility.ts create mode 100644 src/firefox/Browser.ts create mode 100644 src/firefox/BrowserFetcher.ts create mode 100644 src/firefox/Connection.ts create mode 100644 src/firefox/DOMWorld.ts create mode 100644 src/firefox/Dialog.ts create mode 100644 src/firefox/ExecutionContext.ts create mode 100644 src/firefox/FrameManager.ts create mode 100644 src/firefox/Input.ts create mode 100644 src/firefox/JSHandle.ts create mode 100644 src/firefox/Launcher.ts create mode 100644 src/firefox/NavigationWatchdog.ts create mode 100644 src/firefox/NetworkManager.ts create mode 100644 src/firefox/Page.ts create mode 100644 src/firefox/Playwright.ts create mode 100644 src/firefox/WebSocketTransport.ts create mode 100644 src/helper.ts create mode 100644 src/webkit/Browser.ts create mode 100644 src/webkit/BrowserFetcher.ts create mode 100644 src/webkit/Connection.ts create mode 100644 src/webkit/ExecutionContext.ts create mode 100644 src/webkit/FrameManager.ts create mode 100644 src/webkit/Input.ts create mode 100644 src/webkit/JSHandle.ts create mode 100644 src/webkit/Launcher.ts create mode 100644 src/webkit/Multimap.ts create mode 100644 src/webkit/NetworkManager.ts create mode 100644 src/webkit/Page.ts create mode 100644 src/webkit/PipeTransport.ts create mode 100644 src/webkit/Playwright.ts create mode 100644 src/webkit/Target.ts create mode 100644 src/webkit/TaskQueue.ts create mode 100644 src/webkit/protocolHelper.ts create mode 100644 test/CDPSession.spec.js create mode 100644 test/accessibility.spec.js create mode 100644 test/assets/beforeunload.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/file-to-upload.txt 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.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/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/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.js create mode 100644 test/browsercontext.spec.js create mode 100644 test/chromiumonly.spec.js create mode 100644 test/click.spec.js create mode 100644 test/cookies.spec.js create mode 100644 test/coverage.spec.js create mode 100644 test/defaultbrowsercontext.spec.js create mode 100644 test/dialog.spec.js create mode 100644 test/diffstyle.css create mode 100644 test/elementhandle.spec.js create mode 100644 test/emulation.spec.js create mode 100644 test/evaluation.spec.js create mode 100644 test/fixtures.spec.js create mode 100644 test/fixtures/closeme.js create mode 100644 test/fixtures/dumpio.js create mode 100644 test/frame.spec.js 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/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/golden-webkit/grid-cell-0.png create mode 100644 test/golden-webkit/grid-cell-1.png create mode 100644 test/golden-webkit/screenshot-clip-odd-size.png create mode 100644 test/golden-webkit/screenshot-clip-rect.png create mode 100644 test/golden-webkit/screenshot-element-bounding-box.png create mode 100644 test/golden-webkit/screenshot-element-fractional-offset.png create mode 100644 test/golden-webkit/screenshot-element-fractional.png create mode 100644 test/golden-webkit/screenshot-element-larger-than-viewport.png create mode 100644 test/golden-webkit/screenshot-element-padding-border.png create mode 100644 test/golden-webkit/screenshot-element-scrolled-into-view.png create mode 100644 test/golden-webkit/screenshot-grid-fullpage.png create mode 100644 test/golden-webkit/screenshot-offscreen-clip.png create mode 100644 test/golden-webkit/screenshot-sanity.png create mode 100644 test/golden-webkit/transparent.png create mode 100644 test/headful.spec.js create mode 100644 test/ignorehttpserrors.spec.js create mode 100644 test/input.spec.js create mode 100644 test/jshandle.spec.js create mode 100644 test/keyboard.spec.js create mode 100644 test/launcher.spec.js create mode 100644 test/mouse.spec.js create mode 100644 test/navigation.spec.js create mode 100644 test/network.spec.js create mode 100644 test/oopif.spec.js create mode 100644 test/page.spec.js create mode 100644 test/playwright.spec.js create mode 100644 test/queryselector.spec.js create mode 100644 test/requestinterception.spec.js create mode 100755 test/run_static_server.js create mode 100644 test/screenshot.spec.js create mode 100644 test/target.spec.js create mode 100644 test/test.js create mode 100644 test/touchscreen.spec.js create mode 100644 test/tracing.spec.js create mode 100644 test/utils.js create mode 100644 test/waittask.spec.js create mode 100644 test/worker.spec.js create mode 100644 tsconfig.json create mode 100644 utils/ESTreeWalker.js create mode 100644 utils/apply_next_version.js create mode 100755 utils/bisect.js create mode 100644 utils/browser/README.md create mode 100644 utils/browser/WebSocket.js create mode 100644 utils/browser/test.js create mode 100755 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/check_public_api/test/.gitignore create mode 100644 utils/doclint/check_public_api/test/check-duplicates/doc.md create mode 100644 utils/doclint/check_public_api/test/check-duplicates/foo.js create mode 100644 utils/doclint/check_public_api/test/check-duplicates/result.txt create mode 100644 utils/doclint/check_public_api/test/check-returns/doc.md create mode 100644 utils/doclint/check_public_api/test/check-returns/foo.js create mode 100644 utils/doclint/check_public_api/test/check-returns/result.txt create mode 100644 utils/doclint/check_public_api/test/check-sorting/Events.js create mode 100644 utils/doclint/check_public_api/test/check-sorting/doc.md create mode 100644 utils/doclint/check_public_api/test/check-sorting/foo.js create mode 100644 utils/doclint/check_public_api/test/check-sorting/result.txt create mode 100644 utils/doclint/check_public_api/test/diff-arguments/doc.md create mode 100644 utils/doclint/check_public_api/test/diff-arguments/foo.js create mode 100644 utils/doclint/check_public_api/test/diff-arguments/result.txt create mode 100644 utils/doclint/check_public_api/test/diff-classes/doc.md create mode 100644 utils/doclint/check_public_api/test/diff-classes/foo.js create mode 100644 utils/doclint/check_public_api/test/diff-classes/other.js create mode 100644 utils/doclint/check_public_api/test/diff-classes/result.txt create mode 100644 utils/doclint/check_public_api/test/diff-events/Events.js create mode 100644 utils/doclint/check_public_api/test/diff-events/doc.md create mode 100644 utils/doclint/check_public_api/test/diff-events/foo.js create mode 100644 utils/doclint/check_public_api/test/diff-events/result.txt create mode 100644 utils/doclint/check_public_api/test/diff-methods/doc.md create mode 100644 utils/doclint/check_public_api/test/diff-methods/foo.js create mode 100644 utils/doclint/check_public_api/test/diff-methods/result.txt create mode 100644 utils/doclint/check_public_api/test/diff-properties/doc.md create mode 100644 utils/doclint/check_public_api/test/diff-properties/foo.js create mode 100644 utils/doclint/check_public_api/test/diff-properties/result.txt create mode 100644 utils/doclint/check_public_api/test/js-builder-common/Events.js create mode 100644 utils/doclint/check_public_api/test/js-builder-common/foo.js create mode 100644 utils/doclint/check_public_api/test/js-builder-common/result.txt create mode 100644 utils/doclint/check_public_api/test/js-builder-inheritance/Events.js create mode 100644 utils/doclint/check_public_api/test/js-builder-inheritance/foo.js create mode 100644 utils/doclint/check_public_api/test/js-builder-inheritance/result.txt create mode 100644 utils/doclint/check_public_api/test/md-builder-common/doc.md create mode 100644 utils/doclint/check_public_api/test/md-builder-common/result.txt create mode 100644 utils/doclint/check_public_api/test/test.js create mode 100755 utils/doclint/cli.js create mode 100644 utils/doclint/generate_types/index.js create mode 100644 utils/doclint/generate_types/test/test.ts create mode 100644 utils/doclint/generate_types/test/tsconfig.json create mode 100644 utils/doclint/preprocessor/index.js create mode 100644 utils/doclint/preprocessor/test.js create mode 100755 utils/fetch_devices.js create mode 100644 utils/flakiness-dashboard/FlakinessDashboard.js create mode 100644 utils/flakiness-dashboard/index.js create mode 100644 utils/prepare_playwright_core.js create mode 100644 utils/protocol-types-generator/index.js create mode 100644 utils/testrunner/.npmignore create mode 100644 utils/testrunner/LICENSE create mode 100644 utils/testrunner/Matchers.js create mode 100644 utils/testrunner/Multimap.js create mode 100644 utils/testrunner/README.md create mode 100644 utils/testrunner/Reporter.js create mode 100644 utils/testrunner/TestRunner.js create mode 100644 utils/testrunner/examples/fail.js create mode 100644 utils/testrunner/examples/hookfail.js create mode 100644 utils/testrunner/examples/hooktimeout.js create mode 100644 utils/testrunner/examples/timeout.js create mode 100644 utils/testrunner/examples/unhandledpromiserejection.js create mode 100644 utils/testrunner/index.js create mode 100644 utils/testrunner/package.json create mode 100644 utils/testrunner/test/test.js create mode 100644 utils/testrunner/test/testrunner.spec.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 webkit.js diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 0000000000..b7db8e7aad --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,19 @@ +environment: + matrix: + - nodejs_version: "8.16.0" + FLAKINESS_DASHBOARD_NAME: Appveyor Chromium (Win + node8) + FLAKINESS_DASHBOARD_PASSWORD: + secure: g66jP+j6C+hkXLutBV9fdxB5fRJgcQQzy93SgQzXUmcCl/RjkJwnzyHvX0xfCVnv + +build: off + +install: + - ps: $env:FLAKINESS_DASHBOARD_BUILD_URL="https://ci.appveyor.com/project/aslushnikov/playwright/builds/$env:APPVEYOR_BUILD_ID/job/$env:APPVEYOR_JOB_ID" + - ps: Install-Product node $env:nodejs_version + - npm install + - if "%nodejs_version%" == "8.16.0" ( + npm run lint && + npm run coverage && + npm run test-doclint && + npm run test-types + ) diff --git a/.ci/node10/Dockerfile.linux b/.ci/node10/Dockerfile.linux new file mode 100644 index 0000000000..a7cf643b8d --- /dev/null +++ b/.ci/node10/Dockerfile.linux @@ -0,0 +1,17 @@ +FROM node:10 + +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/.ci/node12/Dockerfile.linux b/.ci/node12/Dockerfile.linux new file mode 100644 index 0000000000..79f9426157 --- /dev/null +++ b/.ci/node12/Dockerfile.linux @@ -0,0 +1,17 @@ +FROM node:12 + +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/.ci/node8/Dockerfile.linux b/.ci/node8/Dockerfile.linux new file mode 100644 index 0000000000..26f1197984 --- /dev/null +++ b/.ci/node8/Dockerfile.linux @@ -0,0 +1,17 @@ +FROM node:8.11.3 + +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/.cirrus.yml b/.cirrus.yml new file mode 100644 index 0000000000..41ac3430a9 --- /dev/null +++ b/.cirrus.yml @@ -0,0 +1,47 @@ +env: + DISPLAY: :99.0 + FLAKINESS_DASHBOARD_PASSWORD: ENCRYPTED[b3e207db5d153b543f219d3c3b9123d8321834b783b9e45ac7d380e026ab3a56398bde51b521ac5859e7e45cb95d0992] + FLAKINESS_DASHBOARD_NAME: Cirrus ${CIRRUS_TASK_NAME} + FLAKINESS_DASHBOARD_BUILD_URL: https://cirrus-ci.com/task/${CIRRUS_TASK_ID} + +task: + matrix: + - name: Chromium (node8 + linux) + container: + dockerfile: .ci/node8/Dockerfile.linux + - name: Chromium (node10 + linux) + container: + dockerfile: .ci/node10/Dockerfile.linux + - name: Chromium (node12 + linux) + container: + dockerfile: .ci/node12/Dockerfile.linux + xvfb_start_background_script: Xvfb :99 -ac -screen 0 1024x768x24 + install_script: npm install --unsafe-perm + lint_script: npm run lint + coverage_script: npm run coverage + test_doclint_script: npm run test-doclint + test_types_script: npm run test-types + +task: + matrix: + - name: Firefox (node8 + linux) + container: + dockerfile: .ci/node8/Dockerfile.linux + xvfb_start_background_script: Xvfb :99 -ac -screen 0 1024x768x24 + install_script: npm install --unsafe-perm + test_script: npm run funit + +task: + osx_instance: + image: high-sierra-base + name: Chromium (node8 + macOS) + env: + HOMEBREW_NO_AUTO_UPDATE: 1 + node_install_script: + - brew install node@8 + - brew link --force node@8 + install_script: npm install --unsafe-perm + lint_script: npm run lint + coverage_script: npm run coverage + test_doclint_script: npm run test-doclint + test_types_script: npm run test-types diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..c6c8b36219 --- /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 0000000000..28f6fcb632 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,12 @@ +test/assets/modernizr.js +third_party/* +utils/browser/playwright-web.js +utils/doclint/check_public_api/test/ +utils/testrunner/examples/ +node6/* +node6-test/* +node6-testrunner/* +lib/ +*.js +src/chromium/protocol.d.ts +src/webkit/protocol.d.ts \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000000..ab34834e10 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,107 @@ +module.exports = { + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + parserOptions: { + project: './tsconfig.json', + ecmaVersion: 9, + sourceType: 'module', + }, + + /** + * ESLint rules + * + * All available rules: http://eslint.org/docs/rules/ + * + * Rules take the following form: + * "rule-name", [severity, { opts }] + * Severity: 2 == error, 1 == warning, 0 == off. + */ + "rules": { + '@typescript-eslint/no-unused-vars': [2, {args: 'none'}], + /** + * Enforced rules + */ + // syntax preferences + "quotes": [2, "single", { + "avoidEscape": true, + "allowTemplateLiterals": true + }], + "semi": 2, + "no-extra-semi": 2, + "comma-style": [2, "last"], + "wrap-iife": [2, "inside"], + "spaced-comment": [2, "always", { + "markers": ["*"] + }], + "eqeqeq": [2], + "arrow-body-style": [2, "as-needed"], + "accessor-pairs": [2, { + "getWithoutSet": false, + "setWithoutGet": false + }], + "brace-style": [2, "1tbs", {"allowSingleLine": true}], + "curly": [2, "multi-or-nest", "consistent"], + "new-parens": 2, + "func-call-spacing": 2, + "arrow-parens": [2, "as-needed"], + "prefer-const": 2, + "quote-props": [2, "consistent"], + + // 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-implicit-globals": [2], + + // es2015 features + "require-yield": 2, + "template-curly-spacing": [2, "never"], + + // spacing details + "space-infix-ops": 2, + "space-in-parens": [2, "never"], + "space-before-function-paren": [2, "never"], + "no-whitespace-before-property": 2, + "keyword-spacing": [2, { + "overrides": { + "if": {"after": true}, + "else": {"after": true}, + "for": {"after": true}, + "while": {"after": true}, + "do": {"after": true}, + "switch": {"after": true}, + "return": {"after": true} + } + }], + "arrow-spacing": [2, { + "after": true, + "before": true + }], + + // file whitespace + "no-multiple-empty-lines": [2, {"max": 2}], + "no-mixed-spaces-and-tabs": 2, + "no-trailing-spaces": 2, + "linebreak-style": [ process.platform === "win32" ? 0 : 2, "unix" ], + "indent": [2, 2, { "SwitchCase": 1, "CallExpression": {"arguments": 2}, "MemberExpression": 2 }], + "key-spacing": [2, { + "beforeColon": false + }] + } +}; diff --git a/.gitignore b/.gitignore index ad46b30886..8d2eb40ef6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,61 +1,20 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env - -# next.js build output -.next +/node_modules/ +/test/output-chromium +/test/output-firefox +/test/test-user-data-dir* +/.local-chromium/ +/.local-browser/ +/.local-webkit/ +/.dev_profile* +.DS_Store +*.swp +*.pyc +.vscode +package-lock.json +yarn.lock +/node6 +/src/chromium/protocol.d.ts +/src/webkit/protocol.d.ts +/utils/browser/playwright-web.js +/index.d.ts +lib/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000000..71a68fec8e --- /dev/null +++ b/.npmignore @@ -0,0 +1,44 @@ +.appveyor.yml +.gitattributes + +# no longer generated, but old checkouts might still have it +node6 + +# exclude all tests +test +utils/node6-transform + +# exclude source files +src + +# 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 +.travis.yml +README.md +tsconfig.json + +# exclude types, see https://github.com/GoogleChrome/puppeteer/issues/3878 +/index.d.ts diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..3cd30ce746 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,48 @@ +language: node_js +dist: trusty +addons: + apt: + packages: + # This is required to run new chrome on old trusty + - libnss3 +notifications: + email: false +cache: + directories: + - node_modules +# allow headful tests +before_install: + - "sysctl kernel.unprivileged_userns_clone=1" + - "export DISPLAY=:99.0" + - "sh -e /etc/init.d/xvfb start" +script: + - 'if [ "$NODE8" = "true" ]; then npm run lint; fi' + - 'if [ "$NODE8" = "true" ]; then npm run coverage; fi' + - 'if [ "$FIREFOX" = "true" ]; then npm run funit; fi' + - 'if [ "$NODE8" = "true" ]; then npm run test-doclint; fi' + - 'if [ "$NODE8" = "true" ]; then npm run test-types; fi' + - 'if [ "$NODE8" = "true" ]; then npm run bundle; fi' + - 'if [ "$NODE8" = "true" ]; then npm run unit-bundle; fi' +jobs: + include: + - node_js: "8.16.0" + env: + - NODE8=true + - FLAKINESS_DASHBOARD_NAME="Travis Chromium (node8 + linux)" + - FLAKINESS_DASHBOARD_BUILD_URL="${TRAVIS_JOB_WEB_URL}" + - node_js: "8.16.0" + env: + - FIREFOX=true + - FLAKINESS_DASHBOARD_NAME="Travis Firefox (node8 + linux)" + - FLAKINESS_DASHBOARD_BUILD_URL="${TRAVIS_JOB_WEB_URL}" +before_deploy: "npm run apply-next-version" +deploy: + provider: npm + email: aslushnikov@gmail.com + api_key: + secure: Ng8o2KwJf90XCBNgUKK3jRZnwtdBSJatjYNmZBERJEqBWFTadFAp1NdhxZaqjnuG8aFYaH5bRJdL+EQBYUksVCbrv/gcaXeEFkwsfPfVX1QXGqu7NnZmtme2hbxppLQ7dEJ8hz2Z9K4vehqVOxmLabxvoupOumxEQMLCphVHh2FOmsm/S5JrRZqZ4V9k76eIc0/PiyfXNMdx5WTZjHbIRDIHRy9nqOXjFp2Rx3PMa3uU2fS8mTshYEYs151TA6e6VdHjqmBwEQC/M5tXbDlLCMNUr4JBtLTcL4OipNYjzkwD1N2xYlbSRqtvqqF4ifdvFhoI65a31GinlMC7Z/SH1Zy+d+/z3Mo7D63eYcsJVnsg9OYxTFy2piUntr0JqTBHtQoe/CvGxJmkcVt+H6YSkcBibSG9s9tG3qpAD5wBCFqqOYnfClX+YZziEd+Hngd9inxAf87qdvgVIZ5tPD2dygtE+te2/qoEHtvccv/HuS8MxNj5iKwlP7JaBPM6uAkazYqZP2R99I2ph9gNOEVuQLtk+3+OIdb8HWrEKUrJBgKhdKY1dvcKYElI+D8NRlyzrr6BnZfudACuAt2EtfKpfJ3mL+iRMFdBJ3ntLt93xBrB+j4z3pD0iWZcg1g3I742PFzQEHzyd/DDTP1yRTUoJeQWwoQRJyNO1m6Qk4wx77c= + on: + branch: master + condition: "$NODE8 = true" + skip_cleanup: true + tag: next diff --git a/DeviceDescriptors.js b/DeviceDescriptors.js new file mode 100644 index 0000000000..b5f92b43ea --- /dev/null +++ b/DeviceDescriptors.js @@ -0,0 +1,23 @@ +/** + * Copyright 2019 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. + * + * 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 {DeviceDescriptors} = require('./lib/DeviceDescriptors'); + +const descriptors = DeviceDescriptors.slice(); +module.exports = descriptors; +for (const device of descriptors) + module.exports[device.name] = device; diff --git a/Errors.js b/Errors.js new file mode 100644 index 0000000000..4779e1d850 --- /dev/null +++ b/Errors.js @@ -0,0 +1,17 @@ +/** + * 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. + */ + +module.exports = require('./lib/Errors'); diff --git a/browser_patches/README.md b/browser_patches/README.md new file mode 100644 index 0000000000..51c7554d68 --- /dev/null +++ b/browser_patches/README.md @@ -0,0 +1,31 @@ +# Compiling and Uploading Builds + +### 1. Getting code + +```sh +$ ./checkout.sh firefox/ # or ./checkout.sh webkit/ +``` + +This command will create a `./firefox/checkout` folder that contains firefox GIT checkout. +Checkout current branch will be set to `pwdev` and it will have all additional changes +applied to the browser atop of the `./firefox/BASE_REVISION` version. + +### 2. Compiling + +> **NOTE** You might need to prepare your host environment according to browser build instructions: +> - [firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Build_Instructions) +> - [webkit](https://webkit.org/building-webkit/) + +```sh +$ ./firefox/build.sh # or ./webkit/build.sh +``` + +### 3. Uploading builds to Azure CDN + +> **NOTE** You should have `$AZ_ACCOUNT_KEY` and `$AZ_ACCOUNT_NAME` variables set in your environment. + +```sh +$ ./upload.sh firefox/ # or ./upload.sh webkit/ +``` + +This will package archives and upload builds to Azure CDN. diff --git a/browser_patches/check_cdn.sh b/browser_patches/check_cdn.sh new file mode 100755 index 0000000000..9b9407589c --- /dev/null +++ b/browser_patches/check_cdn.sh @@ -0,0 +1,58 @@ +#!/bin/bash +set -e +set +x + +HOST="https://playwrightaccount.blob.core.windows.net/builds" +ARCHIVES=( + "$HOST/firefox/%s/firefox-mac.zip" + "$HOST/firefox/%s/firefox-linux.zip" + "$HOST/firefox/%s/firefox-win.zip" + "$HOST/webkit/%s/minibrowser-linux.zip" + "$HOST/webkit/%s/minibrowser-mac10.14.zip" + "$HOST/webkit/%s/minibrowser-mac10.15.zip" +) + +ALIASES=( + "FF-MAC" + "FF-LINUX" + "FF-WIN" + "WK-MAC-10.14" + "WK-MAC-10.15" + "WK-LINUX" +) +COLUMN="%-15s" + +# COLORS +RED=$'\e[1;31m' +GRN=$'\e[1;32m' +YEL=$'\e[1;33m' +END=$'\e[0m' + +# Read start revision if there's any. +REVISION=$(git rev-parse HEAD) +if [[ $# == 1 ]]; then + if ! git rev-parse $1; then + echo "ERROR: there is no $REVISION in this repo - pull from upstream?" + exit 1 + fi + REVISION=$(git rev-parse $1) +fi + +printf "%12s" "" +for i in "${ALIASES[@]}"; do + printf $COLUMN $i +done +printf "\n" +while true; do + printf "%-12s" ${REVISION:0:10} + for i in "${ARCHIVES[@]}"; do + URL=$(printf $i $REVISION) + if [[ $(curl -s -L -I $URL | head -1 | cut -f2 -d' ') == 200 ]]; then + printf ${GRN}$COLUMN${END} "YES" + else + printf ${RED}$COLUMN${END} "NO" + fi + done; + echo + REVISION=$(git rev-parse $REVISION^) +done; diff --git a/browser_patches/do_checkout.sh b/browser_patches/do_checkout.sh new file mode 100755 index 0000000000..b83bd9b11d --- /dev/null +++ b/browser_patches/do_checkout.sh @@ -0,0 +1,136 @@ +#!/bin/bash +set -e +set +x + +function cleanup() { + cd $OLD_DIR +} + +OLD_DIR=$(pwd -P) +cd "$(dirname "$0")" +trap cleanup EXIT + +if [[ ($1 == '--help') || ($1 == '-h') ]]; then + echo "usage: do_something.sh [firefox|webkit]" + echo + echo "Produces a browser checkout ready to be built." + echo + exit 0 +fi + +if [[ $# == 0 ]]; then + echo "missing browser: 'firefox' or 'webkit'" + echo "try './do_something.sh --help' for more information" + exit 1 +fi + +# FRIENDLY_CHECKOUT_PATH is used only for logging. +FRIENDLY_CHECKOUT_PATH=""; +CHECKOUT_PATH="" +# Export path is where we put the patches and BASE_REVISION +REMOTE_URL="" +BASE_BRANCH="" +if [[ ("$1" == "firefox") || ("$1" == "firefox/") ]]; then + # we always apply our patches atop of beta since it seems to get better + # reliability guarantees. + BASE_BRANCH="beta" + FRIENDLY_CHECKOUT_PATH="//browser_patches/firefox/checkout"; + CHECKOUT_PATH="$PWD/firefox/checkout" + REMOTE_URL="https://github.com/mozilla/gecko-dev" +elif [[ ("$1" == "webkit") || ("$1" == "webkit/") ]]; then + # webkit has only a master branch. + BASE_BRANCH="master" + FRIENDLY_CHECKOUT_PATH="//browser_patches/webkit/checkout"; + CHECKOUT_PATH="$PWD/webkit/checkout" + REMOTE_URL="" + REMOTE_URL="https://github.com/webkit/webkit" +else + echo ERROR: unknown browser to export - "$1" + exit 1 +fi + +# if there's no checkout folder - checkout one. +if ! [[ -d $CHECKOUT_PATH ]]; then + echo "-- $FRIENDLY_CHECKOUT_PATH is missing - checking out.." + git clone --single-branch --branch $BASE_BRANCH $REMOTE_URL $CHECKOUT_PATH +else + echo "-- checking $FRIENDLY_CHECKOUT_PATH folder - OK" +fi + +# if folder exists but not a git repository - bail out. +if ! [[ -d $CHECKOUT_PATH/.git ]]; then + echo "ERROR: $FRIENDLY_CHECKOUT_PATH is not a git repository! Remove it and re-run the script." + exit 1 +else + echo "-- checking $FRIENDLY_CHECKOUT_PATH is a git repo - OK" +fi + +# Switch to git repository. +cd $CHECKOUT_PATH + +# Check if git repo is dirty. +if [[ -n $(git status -s) ]]; then + echo "ERROR: $FRIENDLY_CHECKOUT_PATH has dirty GIT state - commit everything and re-run the script." + exit 1 +fi + +if [[ $(git config --get remote.origin.url) == "$REMOTE_URL" ]]; then + echo "-- checking git origin url to point to $REMOTE_URL - OK"; +else + echo "ERROR: git origin url DOES NOT point to $REMOTE_URL. Remove $FRIENDLY_CHECKOUT_PATH and re-run the script."; + exit 1 +fi + +# if there's no "BASE_BRANCH" branch - bail out. +if ! git show-ref --verify --quiet refs/heads/$BASE_BRANCH; then + echo "ERROR: $FRIENDLY_CHECKOUT_PATH/ does not have '$BASE_BRANCH' branch! Remove checkout/ and re-run the script." + exit 1 +else + echo "-- checking $FRIENDLY_CHECKOUT_PATH has 'beta' branch - OK" +fi + +if ! [[ -z $(git log --oneline origin/$BASE_BRANCH..$BASE_BRANCH) ]]; then + echo "ERROR: branch '$BASE_BRANCH' and branch 'origin/$BASE_BRANCH' have diverged - bailing out. Remove checkout/ and re-run the script." + exit 1; +else + echo "-- checking that $BASE_BRANCH and origin/$BASE_BRANCH are not diverged - OK" +fi + +git checkout $BASE_BRANCH +git pull origin $BASE_BRANCH + +PINNED_COMMIT=$(cat ../BASE_REVISION) +if ! git cat-file -e $PINNED_COMMIT^{commit}; then + echo "ERROR: $FRIENDLY_CHECKOUT_PATH/ does not include the BASE_REVISION (@$PINNED_COMMIT). Remove checkout/ and re-run the script." + exit 1 +else + echo "-- checking $FRIENDLY_CHECKOUT_PATH repo has BASE_REVISION (@$PINNED_COMMIT) commit - OK" +fi + +# If there's already a PWDEV branch than we should check if it's fine to reset all changes +# to it. +if git show-ref --verify --quiet refs/heads/pwdev; then + read -p "Do you want to reset 'PWDEV' branch? (ALL CHANGES WILL BE LOST) Y/n " -n 1 -r + echo + # if it's not fine to reset branch - bail out. + if ! [[ $REPLY =~ ^[Yy]$ ]]; then + echo "If you want to keep the branch, than I can't do much! Bailing out!" + exit 1 + else + git checkout pwdev + git reset --hard $PINNED_COMMIT + echo "-- PWDEV now points to BASE_REVISION (@$PINNED_COMMIT)" + fi +else + # Otherwise just create a new branch. + git checkout -b pwdev + git reset --hard $PINNED_COMMIT + echo "-- created 'pwdev' branch that points to BASE_REVISION (@$PINNED_COMMIT)." +fi + +echo "-- applying all patches" +git am ../patches/* + +echo +echo +echo "DONE. Browser is ready to be built." diff --git a/browser_patches/export.sh b/browser_patches/export.sh new file mode 100755 index 0000000000..b6384e79f1 --- /dev/null +++ b/browser_patches/export.sh @@ -0,0 +1,115 @@ +#!/bin/bash +set -e +set +x + +cleanup() { + cd $OLD_DIR +} + +OLD_DIR=$(pwd -P) +cd "$(dirname "$0")" +trap cleanup EXIT + +if [[ ($1 == '--help') || ($1 == '-h') ]]; then + echo "usage: export.sh [firefox|webkit] [custom_checkout_path]" + echo + echo "Exports BASE_REVISION and patch from the checkout to browser folder." + echo + echo "You can optionally specify custom_checkout_path if you have browser checkout somewhere else" + echo "and wish to export patches from it." + echo + exit 0 +fi + +if [[ $# == 0 ]]; then + echo "missing browser: 'firefox' or 'webkit'" + echo "try './export.sh --help' for more information" + exit 1 +fi + +# FRIENDLY_CHECKOUT_PATH is used only for logging. +FRIENDLY_CHECKOUT_PATH=""; +CHECKOUT_PATH="" +# Export path is where we put the patches and BASE_REVISION +EXPORT_PATH="" +BASE_BRANCH="" +if [[ ("$1" == "firefox") || ("$1" == "firefox/") ]]; then + # we always apply our patches atop of beta since it seems to get better + # reliability guarantees. + BASE_BRANCH="origin/beta" + FRIENDLY_CHECKOUT_PATH="//browser_patches/firefox/checkout"; + CHECKOUT_PATH="$PWD/firefox/checkout" + EXPORT_PATH="$PWD/firefox/" +elif [[ ("$1" == "webkit") || ("$1" == "webkit/") ]]; then + # webkit has only a master branch. + BASE_BRANCH="origin/master" + FRIENDLY_CHECKOUT_PATH="//browser_patches/webkit/checkout"; + CHECKOUT_PATH="$PWD/webkit/checkout" + EXPORT_PATH="$PWD/webkit/" +else + echo ERROR: unknown browser to export - "$1" + exit 1 +fi + +# we will use this just for beauty. +if [[ $# == 2 ]]; then + echo "WARNING: using custom checkout path $CHECKOUT_PATH" + CHECKOUT_PATH=$2 + FRIENDLY_CHECKOUT_PATH="" +fi + +# if there's no checkout folder - bail out. +if ! [[ -d $CHECKOUT_PATH ]]; then + echo "ERROR: $FRIENDLY_CHECKOUT_PATH is missing - nothing to export." + exit 1; +else + echo "-- checking $FRIENDLY_CHECKOUT_PATH exists - OK" +fi + +# if folder exists but not a git repository - bail out. +if ! [[ -d $CHECKOUT_PATH/.git ]]; then + echo "ERROR: $FRIENDLY_CHECKOUT_PATH is not a git repository! Nothing to export." + exit 1 +else + echo "-- checking $FRIENDLY_CHECKOUT_PATH is a git repo - OK" +fi + +# Switch to git repository. +cd $CHECKOUT_PATH + +# Check if git repo is dirty. +if [[ -n $(git status -s) ]]; then + echo "ERROR: $FRIENDLY_CHECKOUT_PATH has dirty GIT state - aborting export." + exit 1 +else + echo "-- checking $FRIENDLY_CHECKOUT_PATH is clean - OK" +fi + +CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) +MERGE_BASE=$(git merge-base $BASE_BRANCH $CURRENT_BRANCH) +echo "==============================================================" +echo " Repository: $FRIENDLY_CHECKOUT_PATH" +echo " Changes between branches: $BASE_BRANCH..$CURRENT_BRANCH" +echo " BASE_REVISION: $MERGE_BASE" +echo +read -p "Export? Y/n " -n 1 -r +echo +# if it's not fine to reset branch - bail out. +if ! [[ $REPLY =~ ^[Yy]$ ]]; then + echo "Exiting." + exit 1 +fi + +echo $MERGE_BASE > $EXPORT_PATH/BASE_REVISION +git checkout -b tmpsquash_export_script $MERGE_BASE +git merge --squash $CURRENT_BRANCH +git commit -am "chore: bootstrap" +PATCH_NAME=$(git format-patch -1 HEAD) +mv $PATCH_NAME $EXPORT_PATH/patches/ +git checkout $CURRENT_BRANCH +git branch -D tmpsquash_export_script + +# Increment BUILD_NUMBER +BUILD_NUMBER=$(cat $EXPORT_PATH/BUILD_NUMBER) +BUILD_NUMBER=$((BUILD_NUMBER+1)) +echo $BUILD_NUMBER > $EXPORT_PATH/BUILD_NUMBER diff --git a/browser_patches/firefox/.gitignore b/browser_patches/firefox/.gitignore new file mode 100644 index 0000000000..5e660dc18e --- /dev/null +++ b/browser_patches/firefox/.gitignore @@ -0,0 +1 @@ +/checkout diff --git a/browser_patches/firefox/BASE_REVISION b/browser_patches/firefox/BASE_REVISION new file mode 100644 index 0000000000..37eb8e8658 --- /dev/null +++ b/browser_patches/firefox/BASE_REVISION @@ -0,0 +1 @@ +46ca28eadfe840021e2ea496fa6b26f924fa135b diff --git a/browser_patches/firefox/BUILD_NUMBER b/browser_patches/firefox/BUILD_NUMBER new file mode 100644 index 0000000000..d474e1b4d6 --- /dev/null +++ b/browser_patches/firefox/BUILD_NUMBER @@ -0,0 +1,2 @@ +1 + diff --git a/browser_patches/firefox/README.md b/browser_patches/firefox/README.md new file mode 100644 index 0000000000..e3368007ed --- /dev/null +++ b/browser_patches/firefox/README.md @@ -0,0 +1,22 @@ +# Building Juggler (Linux & Mac) + +1. Run `./do_checkout.sh` script. This will create a "checkout" folder with gecko-dev mirror from +GitHub and apply the PlayWright-specific patches. +2. Run `./do_build.sh` script to compile browser. Note: you'll need to follow [build instructions](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Build_Instructions) to setup host environment first. + +# Updating `FIREFOX_REVISION` and `//patches/*` + +The `./export.sh` script will export a patch that describes all the differences between the current branch in `./checkout` +and the `beta` branch in `./checkout`. + +# Uploading to Azure CDN + +Uploading requires having both `AZ_ACCOUNT_KEY` and `AZ_ACCOUNT_NAME` env variables to be defined. + +The following sequence of steps will checkout, build and upload build to Azure CDN on both Linux and Mac: + +```sh +$ ./do_checkout.sh +$ ./build.sh +$ ./upload.sh +``` diff --git a/browser_patches/firefox/archive.sh b/browser_patches/firefox/archive.sh new file mode 100755 index 0000000000..42e75711da --- /dev/null +++ b/browser_patches/firefox/archive.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +if [[ ("$1" == "-h") || ("$1" == "--help") ]]; then + echo "usage: $0" + echo + echo "Generate distributable .zip archive from ./checkout folder that was previously built." + echo + exit 0 +fi + +set -e +set -x + +createZIPForLinuxOrMac() { + cd checkout + local zipname=$1 + local OBJ_FOLDER=$(ls -1 | grep obj-) + if [[ $OBJ_FOLDER == "" ]]; then + echo "ERROR: cannot find obj-* folder in the checkout/. Did you build?" + exit 1; + fi + if ! [[ -d $OBJ_FOLDER/dist/firefox ]]; then + echo "ERROR: cannot find $OBJ_FOLDER/dist/firefox folder in the checkout/. Did you build?" + exit 1; + fi + # Copy the libstdc++ version we linked against. + # TODO(aslushnikov): this won't be needed with official builds. + if [[ "$(uname)" == "Linux" ]]; then + cp /usr/lib/x86_64-linux-gnu/libstdc++.so.6 $OBJ_FOLDER/dist/firefox/libstdc++.so.6 + fi + + # tar resulting directory and cleanup TMP. + cd $OBJ_FOLDER/dist + zip -r ../../../$zipname firefox + cd - +} + +cleanup() { + cd $OLD_DIR +} + +OLD_DIR=$(pwd -P) +cd "$(dirname "$0")" +trap cleanup EXIT + +if [[ "$(uname)" == "Darwin" ]]; then + createZIPForLinuxOrMac "firefox-mac.zip" +elif [[ "$(uname)" == "Linux" ]]; then + createZIPForLinuxOrMac "firefox-linux.zip" +else + echo "ERROR: cannot upload on this platform!" 1>&2 + exit 1; +fi diff --git a/browser_patches/firefox/build.sh b/browser_patches/firefox/build.sh new file mode 100755 index 0000000000..1870546b1b --- /dev/null +++ b/browser_patches/firefox/build.sh @@ -0,0 +1,45 @@ +#!/bin/bash +set -e +set +x + +function cleanup() { + cd $OLD_DIR +} + +OLD_DIR=$(pwd -P) +cd "$(dirname "$0")" +trap cleanup EXIT + +cd checkout + +if ! [[ $(git rev-parse --abbrev-ref HEAD) == "pwdev" ]]; then + echo "ERROR: Cannot build any branch other than PWDEV" + exit 1; +else + echo "-- checking git branch is PWDEV - OK" +fi + +if [[ "$(uname)" == "Darwin" ]]; then + # Firefox currently does not build on 10.15 out of the box - it requires SDK for 10.14. + # Make sure the SDK is out there. + if [[ $(sw_vers -productVersion) == "10.15" ]]; then + if ! [[ -d $HOME/SDK-archive/MacOSX10.14.sdk ]]; then + echo "As of Nov 2019, Firefox does not build on Mac 10.15 without 10.14 SDK." + echo "Check out instructions on getting 10.14 sdk at https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Build_Instructions/Mac_OS_X_Prerequisites" + echo "and make sure to put SDK to $HOME/SDK-archive/MacOSX10.14.sdk/" + exit 1 + else + echo "-- configuting .mozconfig with 10.14 SDK path" + echo "ac_add_options --with-macos-sdk=$HOME/SDK-archive/MacOSX10.14.sdk/" > .mozconfig + fi + fi + echo "-- building on Mac" +elif [[ "$(uname)" == "Linux" ]]; then + echo "-- building on Linux" +else + echo "ERROR: cannot upload on this platform!" 1>&2 + exit 1; +fi + +./mach build +./mach package diff --git a/browser_patches/firefox/patches/0001-chore-bootstrap.patch b/browser_patches/firefox/patches/0001-chore-bootstrap.patch new file mode 100644 index 0000000000..2e1f1298b4 --- /dev/null +++ b/browser_patches/firefox/patches/0001-chore-bootstrap.patch @@ -0,0 +1,4895 @@ +From db891a78ac18605d1fb4c6c4a260c1e16340789c Mon Sep 17 00:00:00 2001 +From: Andrey Lushnikov +Date: Fri, 15 Nov 2019 18:10:56 -0800 +Subject: [PATCH] chore: bootstrap + +--- + browser/installer/allowed-dupes.mn | 5 + + browser/installer/package-manifest.in | 5 + + docshell/base/nsDocShell.cpp | 1 + + dom/ipc/BrowserChild.cpp | 7 + + .../permissions/nsPermissionManager.cpp | 8 +- + .../manager/ssl/nsCertOverrideService.cpp | 2 +- + testing/juggler/BrowserContextManager.js | 194 +++++ + testing/juggler/Helper.js | 101 +++ + testing/juggler/NetworkObserver.js | 450 ++++++++++++ + testing/juggler/TargetRegistry.js | 187 +++++ + testing/juggler/components/juggler.js | 112 +++ + testing/juggler/components/juggler.manifest | 3 + + testing/juggler/components/moz.build | 9 + + testing/juggler/content/ContentSession.js | 63 ++ + testing/juggler/content/FrameTree.js | 232 ++++++ + testing/juggler/content/NetworkMonitor.js | 62 ++ + testing/juggler/content/PageAgent.js | 621 ++++++++++++++++ + testing/juggler/content/RuntimeAgent.js | 460 ++++++++++++ + testing/juggler/content/ScrollbarManager.js | 85 +++ + .../juggler/content/floating-scrollbars.css | 47 ++ + testing/juggler/content/hidden-scrollbars.css | 13 + + testing/juggler/content/main.js | 39 ++ + testing/juggler/jar.mn | 29 + + testing/juggler/moz.build | 15 + + .../juggler/protocol/AccessibilityHandler.js | 15 + + testing/juggler/protocol/BrowserHandler.js | 66 ++ + testing/juggler/protocol/Dispatcher.js | 255 +++++++ + testing/juggler/protocol/NetworkHandler.js | 154 ++++ + testing/juggler/protocol/PageHandler.js | 269 +++++++ + testing/juggler/protocol/PrimitiveTypes.js | 143 ++++ + testing/juggler/protocol/Protocol.js | 660 ++++++++++++++++++ + testing/juggler/protocol/RuntimeHandler.js | 41 ++ + testing/juggler/protocol/TargetHandler.js | 75 ++ + .../statusfilter/nsBrowserStatusFilter.cpp | 12 +- + toolkit/toolkit.mozbuild | 1 + + uriloader/base/nsDocLoader.cpp | 18 + + uriloader/base/nsDocLoader.h | 5 + + uriloader/base/nsIWebProgress.idl | 7 +- + uriloader/base/nsIWebProgressListener2.idl | 23 + + 39 files changed, 4487 insertions(+), 7 deletions(-) + create mode 100644 testing/juggler/BrowserContextManager.js + create mode 100644 testing/juggler/Helper.js + create mode 100644 testing/juggler/NetworkObserver.js + create mode 100644 testing/juggler/TargetRegistry.js + create mode 100644 testing/juggler/components/juggler.js + create mode 100644 testing/juggler/components/juggler.manifest + create mode 100644 testing/juggler/components/moz.build + create mode 100644 testing/juggler/content/ContentSession.js + create mode 100644 testing/juggler/content/FrameTree.js + create mode 100644 testing/juggler/content/NetworkMonitor.js + create mode 100644 testing/juggler/content/PageAgent.js + create mode 100644 testing/juggler/content/RuntimeAgent.js + create mode 100644 testing/juggler/content/ScrollbarManager.js + create mode 100644 testing/juggler/content/floating-scrollbars.css + create mode 100644 testing/juggler/content/hidden-scrollbars.css + create mode 100644 testing/juggler/content/main.js + create mode 100644 testing/juggler/jar.mn + create mode 100644 testing/juggler/moz.build + create mode 100644 testing/juggler/protocol/AccessibilityHandler.js + create mode 100644 testing/juggler/protocol/BrowserHandler.js + create mode 100644 testing/juggler/protocol/Dispatcher.js + create mode 100644 testing/juggler/protocol/NetworkHandler.js + create mode 100644 testing/juggler/protocol/PageHandler.js + create mode 100644 testing/juggler/protocol/PrimitiveTypes.js + create mode 100644 testing/juggler/protocol/Protocol.js + create mode 100644 testing/juggler/protocol/RuntimeHandler.js + create mode 100644 testing/juggler/protocol/TargetHandler.js + +diff --git a/browser/installer/allowed-dupes.mn b/browser/installer/allowed-dupes.mn +index 1ffaa0997927..c1bb33c8e63c 100644 +--- a/browser/installer/allowed-dupes.mn ++++ b/browser/installer/allowed-dupes.mn +@@ -141,6 +141,11 @@ browser/chrome/browser/res/payments/formautofill/autofillEditForms.js + # Bug 1451050 - Remote settings empty dumps (will be populated with data eventually) + browser/defaults/settings/pinning/pins.json + browser/defaults/settings/main/example.json ++# Juggler/marionette files ++chrome/juggler/content/content/floating-scrollbars.css ++browser/chrome/devtools/skin/floating-scrollbars-responsive-design.css ++chrome/juggler/content/server/stream-utils.js ++chrome/marionette/content/stream-utils.js + #ifdef MOZ_EME_WIN32_ARTIFACT + gmp-clearkey/0.1/manifest.json + i686/gmp-clearkey/0.1/manifest.json +diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in +index 0efb8c4210bf..6695fa1deb70 100644 +--- a/browser/installer/package-manifest.in ++++ b/browser/installer/package-manifest.in +@@ -208,6 +208,11 @@ + @RESPATH@/components/marionette.js + #endif + ++@RESPATH@/chrome/juggler@JAREXT@ ++@RESPATH@/chrome/juggler.manifest ++@RESPATH@/components/juggler.manifest ++@RESPATH@/components/juggler.js ++ + #if defined(ENABLE_TESTS) && defined(MOZ_DEBUG) + @RESPATH@/components/TestInterfaceJS.js + @RESPATH@/components/TestInterfaceJS.manifest +diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp +index b56ce1764dbb..1f4e7cb24d6f 100644 +--- a/docshell/base/nsDocShell.cpp ++++ b/docshell/base/nsDocShell.cpp +@@ -1241,6 +1241,7 @@ bool nsDocShell::SetCurrentURI(nsIURI* aURI, nsIRequest* aRequest, + isSubFrame = mLSHE->GetIsSubFrame(); + } + ++ FireOnFrameLocationChange(this, aRequest, aURI, aLocationFlags); + if (!isSubFrame && !isRoot) { + /* + * We don't want to send OnLocationChange notifications when +diff --git a/dom/ipc/BrowserChild.cpp b/dom/ipc/BrowserChild.cpp +index d033474bec84..e97ab5373f13 100644 +--- a/dom/ipc/BrowserChild.cpp ++++ b/dom/ipc/BrowserChild.cpp +@@ -3576,6 +3576,13 @@ NS_IMETHODIMP BrowserChild::OnStateChange(nsIWebProgress* aWebProgress, + return NS_OK; + } + ++NS_IMETHODIMP BrowserChild::OnFrameLocationChange(nsIWebProgress *aWebProgress, ++ nsIRequest *aRequest, ++ nsIURI *aLocation, ++ uint32_t aFlags) { ++ return NS_OK; ++} ++ + NS_IMETHODIMP BrowserChild::OnProgressChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + int32_t aCurSelfProgress, +diff --git a/extensions/permissions/nsPermissionManager.cpp b/extensions/permissions/nsPermissionManager.cpp +index ce3d5e64bb4e..64b86791e582 100644 +--- a/extensions/permissions/nsPermissionManager.cpp ++++ b/extensions/permissions/nsPermissionManager.cpp +@@ -189,6 +189,8 @@ nsresult GetOriginFromPrincipal(nsIPrincipal* aPrincipal, nsACString& aOrigin) { + + OriginAppendOASuffix(attrs, aOrigin); + ++ // Disable userContext for permissions. ++ // attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID); + return NS_OK; + } + +@@ -220,7 +222,7 @@ nsresult GetPrincipalFromOrigin(const nsACString& aOrigin, + attrs.mPrivateBrowsingId = 0; + + // Disable userContext for permissions. +- attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID); ++ // attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID); + + nsCOMPtr uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), originNoSuffix); +@@ -312,7 +314,7 @@ already_AddRefed GetNextSubDomainPrincipal( + mozilla::OriginAttributes attrs = aPrincipal->OriginAttributesRef(); + + // Disable userContext for permissions. +- attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID); ++ // attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID); + + nsCOMPtr principal = + mozilla::BasePrincipal::CreateContentPrincipal(newURI, attrs); +@@ -3220,7 +3222,7 @@ void nsPermissionManager::GetKeyForOrigin(const nsACString& aOrigin, + attrs.mPrivateBrowsingId = 0; + + // Disable userContext for permissions. +- attrs.StripAttributes(OriginAttributes::STRIP_USER_CONTEXT_ID); ++ // attrs.StripAttributes(OriginAttributes::STRIP_USER_CONTEXT_ID); + + #ifdef DEBUG + // Parse the origin string into a principal, and extract some useful +diff --git a/security/manager/ssl/nsCertOverrideService.cpp b/security/manager/ssl/nsCertOverrideService.cpp +index 31737688948a..255e5ae967b4 100644 +--- a/security/manager/ssl/nsCertOverrideService.cpp ++++ b/security/manager/ssl/nsCertOverrideService.cpp +@@ -611,7 +611,7 @@ nsCertOverrideService::IsCertUsedForOverrides(nsIX509Cert* aCert, + NS_IMETHODIMP + nsCertOverrideService:: + SetDisableAllSecurityChecksAndLetAttackersInterceptMyData(bool aDisable) { +- if (!(PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR") || ++ if (false /* juggler hacks */ && !(PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR") || + PR_GetEnv("MOZ_MARIONETTE"))) { + return NS_ERROR_NOT_AVAILABLE; + } +diff --git a/testing/juggler/BrowserContextManager.js b/testing/juggler/BrowserContextManager.js +new file mode 100644 +index 000000000000..751fac95177c +--- /dev/null ++++ b/testing/juggler/BrowserContextManager.js +@@ -0,0 +1,194 @@ ++"use strict"; ++ ++const {ContextualIdentityService} = ChromeUtils.import("resource://gre/modules/ContextualIdentityService.jsm"); ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm'); ++ ++const IDENTITY_NAME = 'JUGGLER '; ++const HUNDRED_YEARS = 60 * 60 * 24 * 365 * 100; ++ ++const ALL_PERMISSIONS = [ ++ 'geo', ++ 'microphone', ++ 'camera', ++ 'desktop-notifications', ++]; ++ ++class BrowserContextManager { ++ static instance() { ++ return BrowserContextManager._instance || null; ++ } ++ ++ static initialize() { ++ if (BrowserContextManager._instance) ++ return; ++ BrowserContextManager._instance = new BrowserContextManager(); ++ } ++ ++ constructor() { ++ this._id = 0; ++ this._browserContextIdToUserContextId = new Map(); ++ this._userContextIdToBrowserContextId = new Map(); ++ this._principalsForBrowserContextId = new Map(); ++ ++ // Cleanup containers from previous runs (if any) ++ for (const identity of ContextualIdentityService.getPublicIdentities()) { ++ if (identity.name && identity.name.startsWith(IDENTITY_NAME)) { ++ ContextualIdentityService.remove(identity.userContextId); ++ ContextualIdentityService.closeContainerTabs(identity.userContextId); ++ } ++ } ++ } ++ ++ grantPermissions(browserContextId, origin, permissions) { ++ const attrs = browserContextId ? {userContextId: this.userContextId(browserContextId)} : {}; ++ const principal = Services.scriptSecurityManager.createContentPrincipal(NetUtil.newURI(origin), attrs); ++ if (!this._principalsForBrowserContextId.has(browserContextId)) ++ this._principalsForBrowserContextId.set(browserContextId, []); ++ this._principalsForBrowserContextId.get(browserContextId).push(principal); ++ for (const permission of ALL_PERMISSIONS) { ++ const action = permissions.includes(permission) ? Ci.nsIPermissionManager.ALLOW_ACTION : Ci.nsIPermissionManager.DENY_ACTION; ++ Services.perms.addFromPrincipal(principal, permission, action); ++ } ++ } ++ ++ resetPermissions(browserContextId) { ++ if (!this._principalsForBrowserContextId.has(browserContextId)) ++ return; ++ const principals = this._principalsForBrowserContextId.get(browserContextId); ++ for (const principal of principals) { ++ for (const permission of ALL_PERMISSIONS) ++ Services.perms.removeFromPrincipal(principal, permission); ++ } ++ this._principalsForBrowserContextId.delete(browserContextId); ++ } ++ ++ createBrowserContext() { ++ const browserContextId = (++this._id) + ''; ++ const identity = ContextualIdentityService.create(IDENTITY_NAME + browserContextId); ++ this._browserContextIdToUserContextId.set(browserContextId, identity.userContextId); ++ this._userContextIdToBrowserContextId.set(identity.userContextId, browserContextId); ++ return browserContextId; ++ } ++ ++ browserContextId(userContextId) { ++ return this._userContextIdToBrowserContextId.get(userContextId); ++ } ++ ++ userContextId(browserContextId) { ++ return this._browserContextIdToUserContextId.get(browserContextId); ++ } ++ ++ removeBrowserContext(browserContextId) { ++ const userContextId = this._browserContextIdToUserContextId.get(browserContextId); ++ ContextualIdentityService.remove(userContextId); ++ ContextualIdentityService.closeContainerTabs(userContextId); ++ this._browserContextIdToUserContextId.delete(browserContextId); ++ this._userContextIdToBrowserContextId.delete(userContextId); ++ } ++ ++ getBrowserContexts() { ++ return Array.from(this._browserContextIdToUserContextId.keys()); ++ } ++ ++ setCookies(browserContextId, cookies) { ++ const protocolToSameSite = { ++ [undefined]: Ci.nsICookie.SAMESITE_NONE, ++ 'Lax': Ci.nsICookie.SAMESITE_LAX, ++ 'Strict': Ci.nsICookie.SAMESITE_STRICT, ++ }; ++ const userContextId = browserContextId ? this._browserContextIdToUserContextId.get(browserContextId) : undefined; ++ for (const cookie of cookies) { ++ const uri = cookie.url ? NetUtil.newURI(cookie.url) : null; ++ let domain = cookie.domain; ++ if (!domain) { ++ if (!uri) ++ throw new Error('At least one of the url and domain needs to be specified'); ++ domain = uri.host; ++ } ++ let path = cookie.path; ++ if (!path) ++ path = uri ? dirPath(uri.filePath) : '/'; ++ let secure = false; ++ if (cookie.secure !== undefined) ++ secure = cookie.secure; ++ else if (uri.scheme === 'https') ++ secure = true; ++ Services.cookies.add( ++ domain, ++ path, ++ cookie.name, ++ cookie.value, ++ secure, ++ cookie.httpOnly || false, ++ cookie.expires === undefined || cookie.expires === -1 /* isSession */, ++ cookie.expires === undefined ? Date.now() + HUNDRED_YEARS : cookie.expires, ++ { userContextId } /* originAttributes */, ++ protocolToSameSite[cookie.sameSite], ++ ); ++ } ++ } ++ ++ deleteCookies(browserContextId, cookies) { ++ const userContextId = browserContextId ? this._browserContextIdToUserContextId.get(browserContextId) : undefined; ++ for (const cookie of cookies) { ++ let defaultDomain = ''; ++ let defaultPath = '/'; ++ if (cookie.url) { ++ const uri = NetUtil.newURI(cookie.url); ++ defaultDomain = uri.host; ++ defaultPath = dirPath(uri.filePath); ++ } ++ Services.cookies.remove( ++ cookie.domain || defaultDomain, ++ cookie.name, ++ cookie.path || defaultPath, ++ { userContextId } /* originAttributes */, ++ ); ++ } ++ } ++ ++ getCookies(browserContextId, urls) { ++ const userContextId = browserContextId ? this._browserContextIdToUserContextId.get(browserContextId) : 0; ++ const result = []; ++ const sameSiteToProtocol = { ++ [Ci.nsICookie.SAMESITE_NONE]: undefined, ++ [Ci.nsICookie.SAMESITE_LAX]: 'Lax', ++ [Ci.nsICookie.SAMESITE_STRICT]: 'Strict', ++ }; ++ const uris = urls.map(url => NetUtil.newURI(url)); ++ for (let cookie of Services.cookies.enumerator) { ++ if (cookie.originAttributes.userContextId !== userContextId) ++ continue; ++ if (!uris.some(uri => cookieMatchesURI(cookie, uri))) ++ continue; ++ result.push({ ++ name: cookie.name, ++ value: cookie.value, ++ domain: cookie.host, ++ path: cookie.path, ++ expires: cookie.isSession ? -1 : cookie.expiry, ++ size: cookie.name.length + cookie.value.length, ++ httpOnly: cookie.isHttpOnly, ++ secure: cookie.isSecure, ++ session: cookie.isSession, ++ sameSite: sameSiteToProtocol[cookie.sameSite], ++ }); ++ } ++ return result; ++ } ++} ++ ++function cookieMatchesURI(cookie, uri) { ++ const hostMatches = cookie.host === uri.host || cookie.host === '.' + uri.host; ++ const pathMatches = uri.filePath.startsWith(cookie.path); ++ return hostMatches && pathMatches; ++} ++ ++function dirPath(path) { ++ return path.substring(0, path.lastIndexOf('/') + 1); ++} ++ ++var EXPORTED_SYMBOLS = ['BrowserContextManager']; ++this.BrowserContextManager = BrowserContextManager; ++ +diff --git a/testing/juggler/Helper.js b/testing/juggler/Helper.js +new file mode 100644 +index 000000000000..673e93b0278a +--- /dev/null ++++ b/testing/juggler/Helper.js +@@ -0,0 +1,101 @@ ++const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++ ++class Helper { ++ addObserver(handler, topic) { ++ Services.obs.addObserver(handler, topic); ++ return () => Services.obs.removeObserver(handler, topic); ++ } ++ ++ addMessageListener(receiver, eventName, handler) { ++ receiver.addMessageListener(eventName, handler); ++ return () => receiver.removeMessageListener(eventName, handler); ++ } ++ ++ addEventListener(receiver, eventName, handler) { ++ receiver.addEventListener(eventName, handler); ++ return () => receiver.removeEventListener(eventName, handler); ++ } ++ ++ on(receiver, eventName, handler) { ++ // The toolkit/modules/EventEmitter.jsm dispatches event name as a first argument. ++ // Fire event listeners without it for convenience. ++ const handlerWrapper = (_, ...args) => handler(...args); ++ receiver.on(eventName, handlerWrapper); ++ return () => receiver.off(eventName, handlerWrapper); ++ } ++ ++ addProgressListener(progress, listener, flags) { ++ progress.addProgressListener(listener, flags); ++ return () => progress.removeProgressListener(listener); ++ } ++ ++ removeListeners(listeners) { ++ for (const tearDown of listeners) ++ tearDown.call(null); ++ listeners.splice(0, listeners.length); ++ } ++ ++ generateId() { ++ return uuidGen.generateUUID().toString(); ++ } ++ ++ getNetworkErrorStatusText(status) { ++ if (!status) ++ return null; ++ for (const key of Object.keys(Cr)) { ++ if (Cr[key] === status) ++ return key; ++ } ++ // Security module. The following is taken from ++ // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/How_to_check_the_secruity_state_of_an_XMLHTTPRequest_over_SSL ++ if ((status & 0xff0000) === 0x5a0000) { ++ // NSS_SEC errors (happen below the base value because of negative vals) ++ if ((status & 0xffff) < Math.abs(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE)) { ++ // The bases are actually negative, so in our positive numeric space, we ++ // need to subtract the base off our value. ++ const nssErr = Math.abs(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE) - (status & 0xffff); ++ switch (nssErr) { ++ case 11: ++ return 'SEC_ERROR_EXPIRED_CERTIFICATE'; ++ case 12: ++ return 'SEC_ERROR_REVOKED_CERTIFICATE'; ++ case 13: ++ return 'SEC_ERROR_UNKNOWN_ISSUER'; ++ case 20: ++ return 'SEC_ERROR_UNTRUSTED_ISSUER'; ++ case 21: ++ return 'SEC_ERROR_UNTRUSTED_CERT'; ++ case 36: ++ return 'SEC_ERROR_CA_CERT_INVALID'; ++ case 90: ++ return 'SEC_ERROR_INADEQUATE_KEY_USAGE'; ++ case 176: ++ return 'SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED'; ++ default: ++ return 'SEC_ERROR_UNKNOWN'; ++ } ++ } ++ const sslErr = Math.abs(Ci.nsINSSErrorsService.NSS_SSL_ERROR_BASE) - (status & 0xffff); ++ switch (sslErr) { ++ case 3: ++ return 'SSL_ERROR_NO_CERTIFICATE'; ++ case 4: ++ return 'SSL_ERROR_BAD_CERTIFICATE'; ++ case 8: ++ return 'SSL_ERROR_UNSUPPORTED_CERTIFICATE_TYPE'; ++ case 9: ++ return 'SSL_ERROR_UNSUPPORTED_VERSION'; ++ case 12: ++ return 'SSL_ERROR_BAD_CERT_DOMAIN'; ++ default: ++ return 'SSL_ERROR_UNKNOWN'; ++ } ++ } ++ return ''; ++ } ++} ++ ++var EXPORTED_SYMBOLS = [ "Helper" ]; ++this.Helper = Helper; ++ +diff --git a/testing/juggler/NetworkObserver.js b/testing/juggler/NetworkObserver.js +new file mode 100644 +index 000000000000..cc8cb8fe9d83 +--- /dev/null ++++ b/testing/juggler/NetworkObserver.js +@@ -0,0 +1,450 @@ ++"use strict"; ++ ++const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm'); ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm'); ++const {CommonUtils} = ChromeUtils.import("resource://services-common/utils.js"); ++ ++ ++const Cc = Components.classes; ++const Ci = Components.interfaces; ++const Cu = Components.utils; ++const Cr = Components.results; ++const Cm = Components.manager; ++const CC = Components.Constructor; ++const helper = new Helper(); ++ ++const BinaryInputStream = CC('@mozilla.org/binaryinputstream;1', 'nsIBinaryInputStream', 'setInputStream'); ++const BinaryOutputStream = CC('@mozilla.org/binaryoutputstream;1', 'nsIBinaryOutputStream', 'setOutputStream'); ++const StorageStream = CC('@mozilla.org/storagestream;1', 'nsIStorageStream', 'init'); ++ ++// Cap response storage with 100Mb per tracked tab. ++const MAX_RESPONSE_STORAGE_SIZE = 100 * 1024 * 1024; ++ ++/** ++ * This is a nsIChannelEventSink implementation that monitors channel redirects. ++ */ ++const SINK_CLASS_DESCRIPTION = "Juggler NetworkMonitor Channel Event Sink"; ++const SINK_CLASS_ID = Components.ID("{c2b4c83e-607a-405a-beab-0ef5dbfb7617}"); ++const SINK_CONTRACT_ID = "@mozilla.org/network/monitor/channeleventsink;1"; ++const SINK_CATEGORY_NAME = "net-channel-event-sinks"; ++ ++class NetworkObserver { ++ static instance() { ++ return NetworkObserver._instance || null; ++ } ++ ++ static initialize() { ++ if (NetworkObserver._instance) ++ return; ++ NetworkObserver._instance = new NetworkObserver(); ++ } ++ ++ constructor() { ++ EventEmitter.decorate(this); ++ this._browserSessionCount = new Map(); ++ this._activityDistributor = Cc["@mozilla.org/network/http-activity-distributor;1"].getService(Ci.nsIHttpActivityDistributor); ++ this._activityDistributor.addObserver(this); ++ ++ this._redirectMap = new Map(); ++ this._channelSink = { ++ QueryInterface: ChromeUtils.generateQI([Ci.nsIChannelEventSink]), ++ asyncOnChannelRedirect: (oldChannel, newChannel, flags, callback) => { ++ this._onRedirect(oldChannel, newChannel); ++ callback.onRedirectVerifyCallback(Cr.NS_OK); ++ }, ++ }; ++ this._channelSinkFactory = { ++ QueryInterface: ChromeUtils.generateQI([Ci.nsIFactory]), ++ createInstance: (aOuter, aIID) => this._channelSink.QueryInterface(aIID), ++ }; ++ // Register self as ChannelEventSink to track redirects. ++ const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); ++ registrar.registerFactory(SINK_CLASS_ID, SINK_CLASS_DESCRIPTION, SINK_CONTRACT_ID, this._channelSinkFactory); ++ Services.catMan.addCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID, SINK_CONTRACT_ID, false, true); ++ ++ // Request interception state. ++ this._browserSuspendedChannels = new Map(); ++ this._extraHTTPHeaders = new Map(); ++ this._browserResponseStorages = new Map(); ++ ++ this._eventListeners = [ ++ helper.addObserver(this._onRequest.bind(this), 'http-on-modify-request'), ++ helper.addObserver(this._onResponse.bind(this, false /* fromCache */), 'http-on-examine-response'), ++ helper.addObserver(this._onResponse.bind(this, true /* fromCache */), 'http-on-examine-cached-response'), ++ helper.addObserver(this._onResponse.bind(this, true /* fromCache */), 'http-on-examine-merged-response'), ++ ]; ++ } ++ ++ setExtraHTTPHeaders(browser, headers) { ++ if (!headers) ++ this._extraHTTPHeaders.delete(browser); ++ else ++ this._extraHTTPHeaders.set(browser, headers); ++ } ++ ++ enableRequestInterception(browser) { ++ if (!this._browserSuspendedChannels.has(browser)) ++ this._browserSuspendedChannels.set(browser, new Map()); ++ } ++ ++ disableRequestInterception(browser) { ++ const suspendedChannels = this._browserSuspendedChannels.get(browser); ++ if (!suspendedChannels) ++ return; ++ this._browserSuspendedChannels.delete(browser); ++ for (const channel of suspendedChannels.values()) ++ channel.resume(); ++ } ++ ++ resumeSuspendedRequest(browser, requestId, headers) { ++ const suspendedChannels = this._browserSuspendedChannels.get(browser); ++ if (!suspendedChannels) ++ throw new Error(`Request interception is not enabled`); ++ const httpChannel = suspendedChannels.get(requestId); ++ if (!httpChannel) ++ throw new Error(`Cannot find request "${requestId}"`); ++ if (headers) { ++ // 1. Clear all previous headers. ++ for (const header of requestHeaders(httpChannel)) ++ httpChannel.setRequestHeader(header.name, '', false /* merge */); ++ // 2. Set new headers. ++ for (const header of headers) ++ httpChannel.setRequestHeader(header.name, header.value, false /* merge */); ++ } ++ suspendedChannels.delete(requestId); ++ httpChannel.resume(); ++ } ++ ++ getResponseBody(browser, requestId) { ++ const responseStorage = this._browserResponseStorages.get(browser); ++ if (!responseStorage) ++ throw new Error('Responses are not tracked for the given browser'); ++ return responseStorage.getBase64EncodedResponse(requestId); ++ } ++ ++ abortSuspendedRequest(browser, aRequestId) { ++ const suspendedChannels = this._browserSuspendedChannels.get(browser); ++ if (!suspendedChannels) ++ throw new Error(`Request interception is not enabled`); ++ const httpChannel = suspendedChannels.get(aRequestId); ++ if (!httpChannel) ++ throw new Error(`Cannot find request "${aRequestId}"`); ++ suspendedChannels.delete(aRequestId); ++ httpChannel.cancel(Cr.NS_ERROR_FAILURE); ++ httpChannel.resume(); ++ this.emit('requestfailed', httpChannel, { ++ requestId: requestId(httpChannel), ++ errorCode: helper.getNetworkErrorStatusText(httpChannel.status), ++ }); ++ } ++ ++ _onRedirect(oldChannel, newChannel) { ++ if (!(oldChannel instanceof Ci.nsIHttpChannel)) ++ return; ++ const httpChannel = oldChannel.QueryInterface(Ci.nsIHttpChannel); ++ const loadContext = getLoadContext(httpChannel); ++ if (!loadContext || !this._browserSessionCount.has(loadContext.topFrameElement)) ++ return; ++ this._redirectMap.set(newChannel, oldChannel); ++ } ++ ++ observeActivity(channel, activityType, activitySubtype, timestamp, extraSizeData, extraStringData) { ++ if (activityType !== Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION) ++ return; ++ if (!(channel instanceof Ci.nsIHttpChannel)) ++ return; ++ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); ++ const loadContext = getLoadContext(httpChannel); ++ if (!loadContext || !this._browserSessionCount.has(loadContext.topFrameElement)) ++ return; ++ if (activitySubtype !== Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE) ++ return; ++ this.emit('requestfinished', httpChannel, { ++ requestId: requestId(httpChannel), ++ }); ++ } ++ ++ _onRequest(channel, topic) { ++ if (!(channel instanceof Ci.nsIHttpChannel)) ++ return; ++ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); ++ const loadContext = getLoadContext(httpChannel); ++ if (!loadContext || !this._browserSessionCount.has(loadContext.topFrameElement)) ++ return; ++ const extraHeaders = this._extraHTTPHeaders.get(loadContext.topFrameElement); ++ if (extraHeaders) { ++ for (const header of extraHeaders) ++ httpChannel.setRequestHeader(header.name, header.value, false /* merge */); ++ } ++ const causeType = httpChannel.loadInfo ? httpChannel.loadInfo.externalContentPolicyType : Ci.nsIContentPolicy.TYPE_OTHER; ++ const suspendedChannels = this._browserSuspendedChannels.get(loadContext.topFrameElement); ++ if (suspendedChannels) { ++ httpChannel.suspend(); ++ suspendedChannels.set(requestId(httpChannel), httpChannel); ++ } ++ const oldChannel = this._redirectMap.get(httpChannel); ++ this._redirectMap.delete(httpChannel); ++ ++ // Install response body hooks. ++ new ResponseBodyListener(this, loadContext.topFrameElement, httpChannel); ++ ++ this.emit('request', httpChannel, { ++ url: httpChannel.URI.spec, ++ suspended: suspendedChannels ? true : undefined, ++ requestId: requestId(httpChannel), ++ redirectedFrom: oldChannel ? requestId(oldChannel) : undefined, ++ postData: readRequestPostData(httpChannel), ++ headers: requestHeaders(httpChannel), ++ method: httpChannel.requestMethod, ++ isNavigationRequest: httpChannel.isMainDocumentChannel, ++ cause: causeTypeToString(causeType), ++ }); ++ } ++ ++ _onResponse(fromCache, httpChannel, topic) { ++ const loadContext = getLoadContext(httpChannel); ++ if (!loadContext || !this._browserSessionCount.has(loadContext.topFrameElement)) ++ return; ++ httpChannel.QueryInterface(Ci.nsIHttpChannelInternal); ++ const headers = []; ++ httpChannel.visitResponseHeaders({ ++ visitHeader: (name, value) => headers.push({name, value}), ++ }); ++ ++ let remoteIPAddress = undefined; ++ let remotePort = undefined; ++ try { ++ remoteIPAddress = httpChannel.remoteAddress; ++ remotePort = httpChannel.remotePort; ++ } catch (e) { ++ // remoteAddress is not defined for cached requests. ++ } ++ this.emit('response', httpChannel, { ++ requestId: requestId(httpChannel), ++ securityDetails: getSecurityDetails(httpChannel), ++ fromCache, ++ headers, ++ remoteIPAddress, ++ remotePort, ++ status: httpChannel.responseStatus, ++ statusText: httpChannel.responseStatusText, ++ }); ++ } ++ ++ _onResponseFinished(browser, httpChannel, body) { ++ const responseStorage = this._browserResponseStorages.get(browser); ++ if (!responseStorage) ++ return; ++ responseStorage.addResponseBody(httpChannel, body); ++ this.emit('requestfinished', httpChannel, { ++ requestId: requestId(httpChannel), ++ }); ++ } ++ ++ startTrackingBrowserNetwork(browser) { ++ const value = this._browserSessionCount.get(browser) || 0; ++ this._browserSessionCount.set(browser, value + 1); ++ if (value === 0) ++ this._browserResponseStorages.set(browser, new ResponseStorage(MAX_RESPONSE_STORAGE_SIZE, MAX_RESPONSE_STORAGE_SIZE / 10)); ++ return () => this.stopTrackingBrowserNetwork(browser); ++ } ++ ++ stopTrackingBrowserNetwork(browser) { ++ const value = this._browserSessionCount.get(browser); ++ if (value) { ++ this._browserSessionCount.set(browser, value - 1); ++ } else { ++ this._browserSessionCount.delete(browser); ++ this._browserResponseStorages.delete(browser); ++ } ++ } ++ ++ dispose() { ++ this._activityDistributor.removeObserver(this); ++ const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); ++ registrar.unregisterFactory(SINK_CLASS_ID, this._channelSinkFactory); ++ Services.catMan.deleteCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID, false); ++ helper.removeListeners(this._eventListeners); ++ } ++} ++ ++const protocolVersionNames = { ++ [Ci.nsITransportSecurityInfo.TLS_VERSION_1]: 'TLS 1', ++ [Ci.nsITransportSecurityInfo.TLS_VERSION_1_1]: 'TLS 1.1', ++ [Ci.nsITransportSecurityInfo.TLS_VERSION_1_2]: 'TLS 1.2', ++ [Ci.nsITransportSecurityInfo.TLS_VERSION_1_3]: 'TLS 1.3', ++}; ++ ++function getSecurityDetails(httpChannel) { ++ const securityInfo = httpChannel.securityInfo; ++ if (!securityInfo) ++ return null; ++ securityInfo.QueryInterface(Ci.nsITransportSecurityInfo); ++ if (!securityInfo.serverCert) ++ return null; ++ return { ++ protocol: protocolVersionNames[securityInfo.protocolVersion] || '', ++ subjectName: securityInfo.serverCert.commonName, ++ issuer: securityInfo.serverCert.issuerCommonName, ++ // Convert to seconds. ++ validFrom: securityInfo.serverCert.validity.notBefore / 1000 / 1000, ++ validTo: securityInfo.serverCert.validity.notAfter / 1000 / 1000, ++ }; ++} ++ ++function readRequestPostData(httpChannel) { ++ if (!(httpChannel instanceof Ci.nsIUploadChannel)) ++ return undefined; ++ const iStream = httpChannel.uploadStream; ++ if (!iStream) ++ return undefined; ++ const isSeekableStream = iStream instanceof Ci.nsISeekableStream; ++ ++ let prevOffset; ++ if (isSeekableStream) { ++ prevOffset = iStream.tell(); ++ iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); ++ } ++ ++ // Read data from the stream. ++ let text = undefined; ++ try { ++ text = NetUtil.readInputStreamToString(iStream, iStream.available()); ++ const converter = Cc['@mozilla.org/intl/scriptableunicodeconverter'] ++ .createInstance(Ci.nsIScriptableUnicodeConverter); ++ converter.charset = 'UTF-8'; ++ text = converter.ConvertToUnicode(text); ++ } catch (err) { ++ text = undefined; ++ } ++ ++ // Seek locks the file, so seek to the beginning only if necko hasn't ++ // read it yet, since necko doesn't seek to 0 before reading (at lest ++ // not till 459384 is fixed). ++ if (isSeekableStream && prevOffset == 0) ++ iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); ++ return text; ++} ++ ++function getLoadContext(httpChannel) { ++ let loadContext = null; ++ try { ++ if (httpChannel.notificationCallbacks) ++ loadContext = httpChannel.notificationCallbacks.getInterface(Ci.nsILoadContext); ++ } catch (e) {} ++ try { ++ if (!loadContext && httpChannel.loadGroup) ++ loadContext = httpChannel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext); ++ } catch (e) { } ++ return loadContext; ++} ++ ++function requestId(httpChannel) { ++ return httpChannel.channelId + ''; ++} ++ ++function requestHeaders(httpChannel) { ++ const headers = []; ++ httpChannel.visitRequestHeaders({ ++ visitHeader: (name, value) => headers.push({name, value}), ++ }); ++ return headers; ++} ++ ++function causeTypeToString(causeType) { ++ for (let key in Ci.nsIContentPolicy) { ++ if (Ci.nsIContentPolicy[key] === causeType) ++ return key; ++ } ++ return 'TYPE_OTHER'; ++} ++ ++class ResponseStorage { ++ constructor(maxTotalSize, maxResponseSize) { ++ this._totalSize = 0; ++ this._maxResponseSize = maxResponseSize; ++ this._maxTotalSize = maxTotalSize; ++ this._responses = new Map(); ++ } ++ ++ addResponseBody(httpChannel, body) { ++ if (body.length > this._maxResponseSize) { ++ this._responses.set(requestId, { ++ evicted: true, ++ body: '', ++ }); ++ return; ++ } ++ let encodings = []; ++ if ((httpChannel instanceof Ci.nsIEncodedChannel) && httpChannel.contentEncodings && !httpChannel.applyConversion) { ++ const encodingHeader = httpChannel.getResponseHeader("Content-Encoding"); ++ encodings = encodingHeader.split(/\s*\t*,\s*\t*/); ++ } ++ this._responses.set(requestId(httpChannel), {body, encodings}); ++ this._totalSize += body.length; ++ if (this._totalSize > this._maxTotalSize) { ++ for (let [requestId, response] of this._responses) { ++ this._totalSize -= response.body.length; ++ response.body = ''; ++ response.evicted = true; ++ if (this._totalSize < this._maxTotalSize) ++ break; ++ } ++ } ++ } ++ ++ getBase64EncodedResponse(requestId) { ++ const response = this._responses.get(requestId); ++ if (!response) ++ throw new Error(`Request "${requestId}" is not found`); ++ if (response.evicted) ++ return {base64body: '', evicted: true}; ++ let result = response.body; ++ if (response.encodings && response.encodings.length) { ++ for (const encoding of response.encodings) ++ result = CommonUtils.convertString(result, encoding, 'uncompressed'); ++ } ++ return {base64body: btoa(result)}; ++ } ++} ++ ++class ResponseBodyListener { ++ constructor(networkObserver, browser, httpChannel) { ++ this._networkObserver = networkObserver; ++ this._browser = browser; ++ this._httpChannel = httpChannel; ++ this._chunks = []; ++ this.QueryInterface = ChromeUtils.generateQI([Ci.nsIStreamListener]); ++ httpChannel.QueryInterface(Ci.nsITraceableChannel); ++ this.originalListener = httpChannel.setNewListener(this); ++ } ++ ++ onDataAvailable(aRequest, aInputStream, aOffset, aCount) { ++ const iStream = new BinaryInputStream(aInputStream); ++ const sStream = new StorageStream(8192, aCount, null); ++ const oStream = new BinaryOutputStream(sStream.getOutputStream(0)); ++ ++ // Copy received data as they come. ++ const data = iStream.readBytes(aCount); ++ this._chunks.push(data); ++ ++ oStream.writeBytes(data, aCount); ++ this.originalListener.onDataAvailable(aRequest, sStream.newInputStream(0), aOffset, aCount); ++ } ++ ++ onStartRequest(aRequest) { ++ this.originalListener.onStartRequest(aRequest); ++ } ++ ++ onStopRequest(aRequest, aStatusCode) { ++ this.originalListener.onStopRequest(aRequest, aStatusCode); ++ const body = this._chunks.join(''); ++ delete this._chunks; ++ this._networkObserver._onResponseFinished(this._browser, this._httpChannel, body); ++ } ++} ++ ++var EXPORTED_SYMBOLS = ['NetworkObserver']; ++this.NetworkObserver = NetworkObserver; +diff --git a/testing/juggler/TargetRegistry.js b/testing/juggler/TargetRegistry.js +new file mode 100644 +index 000000000000..da5e4ee371d0 +--- /dev/null ++++ b/testing/juggler/TargetRegistry.js +@@ -0,0 +1,187 @@ ++const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm'); ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++ ++const Cc = Components.classes; ++const Ci = Components.interfaces; ++const Cu = Components.utils; ++ ++const helper = new Helper(); ++ ++class TargetRegistry { ++ static instance() { ++ return TargetRegistry._instance || null; ++ } ++ ++ static initialize(mainWindow, contextManager) { ++ if (TargetRegistry._instance) ++ return; ++ TargetRegistry._instance = new TargetRegistry(mainWindow, contextManager); ++ } ++ ++ constructor(mainWindow, contextManager) { ++ EventEmitter.decorate(this); ++ ++ this._mainWindow = mainWindow; ++ this._contextManager = contextManager; ++ this._targets = new Map(); ++ ++ this._browserTarget = new BrowserTarget(); ++ this._targets.set(this._browserTarget.id(), this._browserTarget); ++ this._tabToTarget = new Map(); ++ ++ for (const tab of this._mainWindow.gBrowser.tabs) ++ this._ensureTargetForTab(tab); ++ this._mainWindow.gBrowser.tabContainer.addEventListener('TabOpen', event => { ++ this._ensureTargetForTab(event.target); ++ }); ++ this._mainWindow.gBrowser.tabContainer.addEventListener('TabClose', event => { ++ const tab = event.target; ++ const target = this._tabToTarget.get(tab); ++ if (!target) ++ return; ++ this._targets.delete(target.id()); ++ this._tabToTarget.delete(tab); ++ target.dispose(); ++ this.emit(TargetRegistry.Events.TargetDestroyed, target.info()); ++ }); ++ } ++ ++ async newPage({browserContextId}) { ++ const tab = this._mainWindow.gBrowser.addTab('about:blank', { ++ userContextId: this._contextManager.userContextId(browserContextId), ++ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), ++ }); ++ this._mainWindow.gBrowser.selectedTab = tab; ++ // Await navigation to about:blank ++ await new Promise(resolve => { ++ const wpl = { ++ onLocationChange: function(aWebProgress, aRequest, aLocation) { ++ tab.linkedBrowser.removeProgressListener(wpl); ++ resolve(); ++ }, ++ QueryInterface: ChromeUtils.generateQI([ ++ Ci.nsIWebProgressListener, ++ Ci.nsISupportsWeakReference, ++ ]), ++ }; ++ tab.linkedBrowser.addProgressListener(wpl); ++ }); ++ const target = this._ensureTargetForTab(tab); ++ return target.id(); ++ } ++ ++ async closePage(targetId, runBeforeUnload = false) { ++ const tab = this.tabForTarget(targetId); ++ await this._mainWindow.gBrowser.removeTab(tab, { ++ skipPermitUnload: !runBeforeUnload, ++ }); ++ } ++ ++ targetInfos() { ++ return Array.from(this._targets.values()).map(target => target.info()); ++ } ++ ++ targetInfo(targetId) { ++ const target = this._targets.get(targetId); ++ return target ? target.info() : null; ++ } ++ ++ browserTargetInfo() { ++ return this._browserTarget.info(); ++ } ++ ++ tabForTarget(targetId) { ++ const target = this._targets.get(targetId); ++ if (!target) ++ throw new Error(`Target "${targetId}" does not exist!`); ++ if (!(target instanceof PageTarget)) ++ throw new Error(`Target "${targetId}" is not a page!`); ++ return target._tab; ++ } ++ ++ _ensureTargetForTab(tab) { ++ if (this._tabToTarget.has(tab)) ++ return this._tabToTarget.get(tab); ++ const openerTarget = tab.openerTab ? this._ensureTargetForTab(tab.openerTab) : null; ++ const target = new PageTarget(this, tab, this._contextManager.browserContextId(tab.userContextId), openerTarget); ++ ++ this._targets.set(target.id(), target); ++ this._tabToTarget.set(tab, target); ++ this.emit(TargetRegistry.Events.TargetCreated, target.info()); ++ } ++} ++ ++let lastTabTargetId = 0; ++ ++class PageTarget { ++ constructor(registry, tab, browserContextId, opener) { ++ this._targetId = 'target-page-' + (++lastTabTargetId); ++ this._registry = registry; ++ this._tab = tab; ++ this._browserContextId = browserContextId; ++ this._openerId = opener ? opener.id() : undefined; ++ this._url = tab.linkedBrowser.currentURI.spec; ++ ++ // First navigation always happens to about:blank - do not report it. ++ this._skipNextNavigation = true; ++ ++ const navigationListener = { ++ QueryInterface: ChromeUtils.generateQI([ Ci.nsIWebProgressListener]), ++ onLocationChange: (aWebProgress, aRequest, aLocation) => this._onNavigated(aLocation), ++ }; ++ this._eventListeners = [ ++ helper.addProgressListener(tab.linkedBrowser, navigationListener, Ci.nsIWebProgress.NOTIFY_LOCATION), ++ ]; ++ } ++ ++ id() { ++ return this._targetId; ++ } ++ ++ info() { ++ return { ++ targetId: this.id(), ++ type: 'page', ++ url: this._url, ++ browserContextId: this._browserContextId, ++ openerId: this._openerId, ++ }; ++ } ++ ++ _onNavigated(aLocation) { ++ if (this._skipNextNavigation) { ++ this._skipNextNavigation = false; ++ return; ++ } ++ this._url = aLocation.spec; ++ this._registry.emit(TargetRegistry.Events.TargetChanged, this.info()); ++ } ++ ++ dispose() { ++ helper.removeListeners(this._eventListeners); ++ } ++} ++ ++class BrowserTarget { ++ id() { ++ return 'target-browser'; ++ } ++ ++ info() { ++ return { ++ targetId: this.id(), ++ type: 'browser', ++ url: '', ++ } ++ } ++} ++ ++TargetRegistry.Events = { ++ TargetCreated: Symbol('TargetRegistry.Events.TargetCreated'), ++ TargetDestroyed: Symbol('TargetRegistry.Events.TargetDestroyed'), ++ TargetChanged: Symbol('TargetRegistry.Events.TargetChanged'), ++}; ++ ++var EXPORTED_SYMBOLS = ['TargetRegistry']; ++this.TargetRegistry = TargetRegistry; +diff --git a/testing/juggler/components/juggler.js b/testing/juggler/components/juggler.js +new file mode 100644 +index 000000000000..9654aeeb257d +--- /dev/null ++++ b/testing/juggler/components/juggler.js +@@ -0,0 +1,112 @@ ++const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++const {Dispatcher} = ChromeUtils.import("chrome://juggler/content/protocol/Dispatcher.js"); ++const {BrowserContextManager} = ChromeUtils.import("chrome://juggler/content/BrowserContextManager.js"); ++const {NetworkObserver} = ChromeUtils.import("chrome://juggler/content/NetworkObserver.js"); ++const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js"); ++ ++const Cc = Components.classes; ++const Ci = Components.interfaces; ++const Cu = Components.utils; ++ ++const FRAME_SCRIPT = "chrome://juggler/content/content/main.js"; ++ ++// Command Line Handler ++function CommandLineHandler() { ++ this._port = -1; ++}; ++ ++CommandLineHandler.prototype = { ++ classDescription: "Sample command-line handler", ++ classID: Components.ID('{f7a74a33-e2ab-422d-b022-4fb213dd2639}'), ++ contractID: "@mozilla.org/remote/juggler;1", ++ _xpcom_categories: [{ ++ category: "command-line-handler", ++ entry: "m-juggler" ++ }], ++ ++ /* nsICommandLineHandler */ ++ handle: async function(cmdLine) { ++ const jugglerFlag = cmdLine.handleFlagWithParam("juggler", false); ++ if (!jugglerFlag || isNaN(jugglerFlag)) ++ return; ++ this._port = parseInt(jugglerFlag, 10); ++ Services.obs.addObserver(this, 'sessionstore-windows-restored'); ++ }, ++ ++ observe: async function(subject, topic) { ++ Services.obs.removeObserver(this, 'sessionstore-windows-restored'); ++ ++ const win = await waitForBrowserWindow(); ++ BrowserContextManager.initialize(); ++ NetworkObserver.initialize(); ++ TargetRegistry.initialize(win, BrowserContextManager.instance()); ++ ++ const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm"); ++ const WebSocketServer = require('devtools/server/socket/websocket-server'); ++ this._server = Cc["@mozilla.org/network/server-socket;1"].createInstance(Ci.nsIServerSocket); ++ this._server.initSpecialConnection(this._port, Ci.nsIServerSocket.KeepWhenOffline | Ci.nsIServerSocket.LoopbackOnly, 4); ++ this._server.asyncListen({ ++ onSocketAccepted: async(socket, transport) => { ++ const input = transport.openInputStream(0, 0, 0); ++ const output = transport.openOutputStream(0, 0, 0); ++ const webSocket = await WebSocketServer.accept(transport, input, output); ++ new Dispatcher(webSocket); ++ } ++ }); ++ ++ Services.mm.loadFrameScript(FRAME_SCRIPT, true /* aAllowDelayedLoad */); ++ dump(`Juggler listening on ws://127.0.0.1:${this._server.port}\n`); ++ }, ++ ++ QueryInterface: ChromeUtils.generateQI([ Ci.nsICommandLineHandler ]), ++ ++ // CHANGEME: change the help info as appropriate, but ++ // follow the guidelines in nsICommandLineHandler.idl ++ // specifically, flag descriptions should start at ++ // character 24, and lines should be wrapped at ++ // 72 characters with embedded newlines, ++ // and finally, the string should end with a newline ++ helpInfo : " --juggler Enable Juggler automation\n" ++}; ++ ++var NSGetFactory = XPCOMUtils.generateNSGetFactory([CommandLineHandler]); ++ ++/** ++ * @return {!Promise} ++ */ ++async function waitForBrowserWindow() { ++ const windowsIt = Services.wm.getEnumerator('navigator:browser'); ++ if (windowsIt.hasMoreElements()) ++ return waitForWindowLoaded(windowsIt.getNext()); ++ ++ let fulfill; ++ let promise = new Promise(x => fulfill = x); ++ ++ const listener = { ++ onOpenWindow: window => { ++ if (window instanceof Ci.nsIDOMChromeWindow) { ++ Services.wm.removeListener(listener); ++ fulfill(waitForWindowLoaded(window)); ++ } ++ }, ++ onCloseWindow: () => {} ++ }; ++ Services.wm.addListener(listener); ++ return promise; ++ ++ /** ++ * @param {!Ci.nsIDOMChromeWindow} window ++ * @return {!Promise} ++ */ ++ function waitForWindowLoaded(window) { ++ if (window.document.readyState === 'complete') ++ return window; ++ return new Promise(fulfill => { ++ window.addEventListener('load', function listener() { ++ window.removeEventListener('load', listener); ++ fulfill(window); ++ }); ++ }); ++ } ++} +diff --git a/testing/juggler/components/juggler.manifest b/testing/juggler/components/juggler.manifest +new file mode 100644 +index 000000000000..50f893020756 +--- /dev/null ++++ b/testing/juggler/components/juggler.manifest +@@ -0,0 +1,3 @@ ++component {f7a74a33-e2ab-422d-b022-4fb213dd2639} juggler.js ++contract @mozilla.org/remote/juggler;1 {f7a74a33-e2ab-422d-b022-4fb213dd2639} ++category command-line-handler m-juggler @mozilla.org/remote/juggler;1 +diff --git a/testing/juggler/components/moz.build b/testing/juggler/components/moz.build +new file mode 100644 +index 000000000000..268fbc361d80 +--- /dev/null ++++ b/testing/juggler/components/moz.build +@@ -0,0 +1,9 @@ ++# This Source Code Form is subject to the terms of the Mozilla Public ++# License, v. 2.0. If a copy of the MPL was not distributed with this ++# file, You can obtain one at http://mozilla.org/MPL/2.0/. ++ ++EXTRA_COMPONENTS += [ ++ "juggler.js", ++ "juggler.manifest", ++] ++ +diff --git a/testing/juggler/content/ContentSession.js b/testing/juggler/content/ContentSession.js +new file mode 100644 +index 000000000000..f68780d529e7 +--- /dev/null ++++ b/testing/juggler/content/ContentSession.js +@@ -0,0 +1,63 @@ ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const {RuntimeAgent} = ChromeUtils.import('chrome://juggler/content/content/RuntimeAgent.js'); ++const {PageAgent} = ChromeUtils.import('chrome://juggler/content/content/PageAgent.js'); ++ ++const helper = new Helper(); ++ ++class ContentSession { ++ /** ++ * @param {string} sessionId ++ * @param {!ContentFrameMessageManager} messageManager ++ * @param {!FrameTree} frameTree ++ * @param {!ScrollbarManager} scrollbarManager ++ * @param {!NetworkMonitor} networkMonitor ++ */ ++ constructor(sessionId, messageManager, frameTree, scrollbarManager, networkMonitor) { ++ this._sessionId = sessionId; ++ this._messageManager = messageManager; ++ const runtimeAgent = new RuntimeAgent(this); ++ const pageAgent = new PageAgent(this, runtimeAgent, frameTree, scrollbarManager, networkMonitor); ++ this._agents = { ++ Page: pageAgent, ++ Runtime: runtimeAgent, ++ }; ++ this._eventListeners = [ ++ helper.addMessageListener(messageManager, this._sessionId, this._onMessage.bind(this)), ++ ]; ++ } ++ ++ emitEvent(eventName, params) { ++ this._messageManager.sendAsyncMessage(this._sessionId, {eventName, params}); ++ } ++ ++ mm() { ++ return this._messageManager; ++ } ++ ++ async _onMessage(msg) { ++ const id = msg.data.id; ++ try { ++ const [domainName, methodName] = msg.data.methodName.split('.'); ++ const agent = this._agents[domainName]; ++ if (!agent) ++ throw new Error(`unknown domain: ${domainName}`); ++ const handler = agent[methodName]; ++ if (!handler) ++ throw new Error(`unknown method: ${domainName}.${methodName}`); ++ const result = await handler.call(agent, msg.data.params); ++ this._messageManager.sendAsyncMessage(this._sessionId, {id, result}); ++ } catch (e) { ++ this._messageManager.sendAsyncMessage(this._sessionId, {id, error: e.message + '\n' + e.stack}); ++ } ++ } ++ ++ dispose() { ++ helper.removeListeners(this._eventListeners); ++ for (const agent of Object.values(this._agents)) ++ agent.dispose(); ++ } ++} ++ ++var EXPORTED_SYMBOLS = ['ContentSession']; ++this.ContentSession = ContentSession; ++ +diff --git a/testing/juggler/content/FrameTree.js b/testing/juggler/content/FrameTree.js +new file mode 100644 +index 000000000000..2931c75e60d2 +--- /dev/null ++++ b/testing/juggler/content/FrameTree.js +@@ -0,0 +1,232 @@ ++"use strict"; ++const Ci = Components.interfaces; ++const Cr = Components.results; ++const Cu = Components.utils; ++ ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm'); ++ ++const helper = new Helper(); ++ ++class FrameTree { ++ constructor(rootDocShell) { ++ EventEmitter.decorate(this); ++ this._docShellToFrame = new Map(); ++ this._frameIdToFrame = new Map(); ++ this._mainFrame = this._createFrame(rootDocShell); ++ const webProgress = rootDocShell.QueryInterface(Ci.nsIInterfaceRequestor) ++ .getInterface(Ci.nsIWebProgress); ++ this.QueryInterface = ChromeUtils.generateQI([ ++ Ci.nsIWebProgressListener, ++ Ci.nsIWebProgressListener2, ++ Ci.nsISupportsWeakReference, ++ ]); ++ ++ const flags = Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT | ++ Ci.nsIWebProgress.NOTIFY_FRAME_LOCATION; ++ this._eventListeners = [ ++ helper.addObserver(subject => this._onDocShellCreated(subject.QueryInterface(Ci.nsIDocShell)), 'webnavigation-create'), ++ helper.addObserver(subject => this._onDocShellDestroyed(subject.QueryInterface(Ci.nsIDocShell)), 'webnavigation-destroy'), ++ helper.addProgressListener(webProgress, this, flags), ++ ]; ++ } ++ ++ frameForDocShell(docShell) { ++ return this._docShellToFrame.get(docShell) || null; ++ } ++ ++ frame(frameId) { ++ return this._frameIdToFrame.get(frameId) || null; ++ } ++ ++ frames() { ++ let result = []; ++ collect(this._mainFrame); ++ return result; ++ ++ function collect(frame) { ++ result.push(frame); ++ for (const subframe of frame._children) ++ collect(subframe); ++ } ++ } ++ ++ mainFrame() { ++ return this._mainFrame; ++ } ++ ++ dispose() { ++ helper.removeListeners(this._eventListeners); ++ } ++ ++ onStateChange(progress, request, flag, status) { ++ if (!(request instanceof Ci.nsIChannel)) ++ return; ++ const channel = request.QueryInterface(Ci.nsIChannel); ++ const docShell = progress.DOMWindow.docShell; ++ const frame = this._docShellToFrame.get(docShell); ++ if (!frame) { ++ dump(`ERROR: got a state changed event for un-tracked docshell!\n`); ++ return; ++ } ++ ++ const isStart = flag & Ci.nsIWebProgressListener.STATE_START; ++ const isTransferring = flag & Ci.nsIWebProgressListener.STATE_TRANSFERRING; ++ const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP; ++ ++ if (isStart) { ++ // Starting a new navigation. ++ frame._pendingNavigationId = helper.generateId(); ++ frame._pendingNavigationURL = channel.URI.spec; ++ this.emit(FrameTree.Events.NavigationStarted, frame); ++ } else if (isTransferring || (isStop && frame._pendingNavigationId && !status)) { ++ // Navigation is committed. ++ for (const subframe of frame._children) ++ this._detachFrame(subframe); ++ const navigationId = frame._pendingNavigationId; ++ frame._pendingNavigationId = null; ++ frame._pendingNavigationURL = null; ++ frame._lastCommittedNavigationId = navigationId; ++ frame._url = channel.URI.spec; ++ this.emit(FrameTree.Events.NavigationCommitted, frame); ++ } else if (isStop && frame._pendingNavigationId && status) { ++ // Navigation is aborted. ++ const navigationId = frame._pendingNavigationId; ++ frame._pendingNavigationId = null; ++ frame._pendingNavigationURL = null; ++ this.emit(FrameTree.Events.NavigationAborted, frame, navigationId, helper.getNetworkErrorStatusText(status)); ++ } ++ } ++ ++ onFrameLocationChange(progress, request, location, flags) { ++ const docShell = progress.DOMWindow.docShell; ++ const frame = this._docShellToFrame.get(docShell); ++ const sameDocumentNavigation = !!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT); ++ if (frame && sameDocumentNavigation) { ++ frame._url = location.spec; ++ this.emit(FrameTree.Events.SameDocumentNavigation, frame); ++ } ++ } ++ ++ _onDocShellCreated(docShell) { ++ // Bug 1142752: sometimes, the docshell appears to be immediately ++ // destroyed, bailout early to prevent random exceptions. ++ if (docShell.isBeingDestroyed()) ++ return; ++ // If this docShell doesn't belong to our frame tree - do nothing. ++ let root = docShell; ++ while (root.parent) ++ root = root.parent; ++ if (root === this._mainFrame._docShell) ++ this._createFrame(docShell); ++ } ++ ++ _createFrame(docShell) { ++ const parentFrame = this._docShellToFrame.get(docShell.parent) || null; ++ const frame = new Frame(this, docShell, parentFrame); ++ this._docShellToFrame.set(docShell, frame); ++ this._frameIdToFrame.set(frame.id(), frame); ++ this.emit(FrameTree.Events.FrameAttached, frame); ++ return frame; ++ } ++ ++ _onDocShellDestroyed(docShell) { ++ const frame = this._docShellToFrame.get(docShell); ++ if (frame) ++ this._detachFrame(frame); ++ } ++ ++ _detachFrame(frame) { ++ // Detach all children first ++ for (const subframe of frame._children) ++ this._detachFrame(subframe); ++ this._docShellToFrame.delete(frame._docShell); ++ this._frameIdToFrame.delete(frame.id()); ++ if (frame._parentFrame) ++ frame._parentFrame._children.delete(frame); ++ frame._parentFrame = null; ++ this.emit(FrameTree.Events.FrameDetached, frame); ++ } ++} ++ ++FrameTree.Events = { ++ FrameAttached: 'frameattached', ++ FrameDetached: 'framedetached', ++ NavigationStarted: 'navigationstarted', ++ NavigationCommitted: 'navigationcommitted', ++ NavigationAborted: 'navigationaborted', ++ SameDocumentNavigation: 'samedocumentnavigation', ++}; ++ ++class Frame { ++ constructor(frameTree, docShell, parentFrame) { ++ this._frameTree = frameTree; ++ this._docShell = docShell; ++ this._children = new Set(); ++ this._frameId = helper.generateId(); ++ this._parentFrame = null; ++ this._url = ''; ++ if (parentFrame) { ++ this._parentFrame = parentFrame; ++ parentFrame._children.add(this); ++ } ++ ++ this._lastCommittedNavigationId = null; ++ this._pendingNavigationId = null; ++ this._pendingNavigationURL = null; ++ ++ this._textInputProcessor = null; ++ } ++ ++ textInputProcessor() { ++ if (!this._textInputProcessor) { ++ this._textInputProcessor = Cc["@mozilla.org/text-input-processor;1"].createInstance(Ci.nsITextInputProcessor); ++ this._textInputProcessor.beginInputTransactionForTests(this._docShell.DOMWindow); ++ } ++ return this._textInputProcessor; ++ } ++ ++ pendingNavigationId() { ++ return this._pendingNavigationId; ++ } ++ ++ pendingNavigationURL() { ++ return this._pendingNavigationURL; ++ } ++ ++ lastCommittedNavigationId() { ++ return this._lastCommittedNavigationId; ++ } ++ ++ docShell() { ++ return this._docShell; ++ } ++ ++ domWindow() { ++ return this._docShell.domWindow; ++ } ++ ++ name() { ++ const frameElement = this._docShell.domWindow.frameElement; ++ let name = ''; ++ if (frameElement) ++ name = frameElement.getAttribute('name') || frameElement.getAttribute('id') || ''; ++ return name; ++ } ++ ++ parentFrame() { ++ return this._parentFrame; ++ } ++ ++ id() { ++ return this._frameId; ++ } ++ ++ url() { ++ return this._url; ++ } ++} ++ ++var EXPORTED_SYMBOLS = ['FrameTree']; ++this.FrameTree = FrameTree; ++ +diff --git a/testing/juggler/content/NetworkMonitor.js b/testing/juggler/content/NetworkMonitor.js +new file mode 100644 +index 000000000000..2508cce41565 +--- /dev/null ++++ b/testing/juggler/content/NetworkMonitor.js +@@ -0,0 +1,62 @@ ++"use strict"; ++const Ci = Components.interfaces; ++const Cr = Components.results; ++const Cu = Components.utils; ++ ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++ ++const helper = new Helper(); ++ ++class NetworkMonitor { ++ constructor(rootDocShell, frameTree) { ++ this._frameTree = frameTree; ++ this._requestDetails = new Map(); ++ ++ this._eventListeners = [ ++ helper.addObserver(this._onRequest.bind(this), 'http-on-opening-request'), ++ ]; ++ } ++ ++ _onRequest(channel) { ++ if (!(channel instanceof Ci.nsIHttpChannel)) ++ return; ++ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); ++ const loadContext = getLoadContext(httpChannel); ++ if (!loadContext) ++ return; ++ const window = loadContext.associatedWindow; ++ const frame = this._frameTree.frameForDocShell(window.docShell) ++ if (!frame) ++ return; ++ this._requestDetails.set(httpChannel.channelId, { ++ frameId: frame.id(), ++ }); ++ } ++ ++ requestDetails(channelId) { ++ return this._requestDetails.get(channelId) || null; ++ } ++ ++ dispose() { ++ this._requestDetails.clear(); ++ helper.removeListeners(this._eventListeners); ++ } ++} ++ ++function getLoadContext(httpChannel) { ++ let loadContext = null; ++ try { ++ if (httpChannel.notificationCallbacks) ++ loadContext = httpChannel.notificationCallbacks.getInterface(Ci.nsILoadContext); ++ } catch (e) {} ++ try { ++ if (!loadContext && httpChannel.loadGroup) ++ loadContext = httpChannel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext); ++ } catch (e) { } ++ return loadContext; ++} ++ ++ ++var EXPORTED_SYMBOLS = ['NetworkMonitor']; ++this.NetworkMonitor = NetworkMonitor; ++ +diff --git a/testing/juggler/content/PageAgent.js b/testing/juggler/content/PageAgent.js +new file mode 100644 +index 000000000000..e8db4031620e +--- /dev/null ++++ b/testing/juggler/content/PageAgent.js +@@ -0,0 +1,621 @@ ++"use strict"; ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++const Ci = Components.interfaces; ++const Cr = Components.results; ++const Cu = Components.utils; ++ ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm'); ++ ++const helper = new Helper(); ++ ++class PageAgent { ++ constructor(session, runtimeAgent, frameTree, scrollbarManager, networkMonitor) { ++ this._session = session; ++ this._runtime = runtimeAgent; ++ this._frameTree = frameTree; ++ this._networkMonitor = networkMonitor; ++ this._scrollbarManager = scrollbarManager; ++ ++ this._frameToExecutionContext = new Map(); ++ this._scriptsToEvaluateOnNewDocument = new Map(); ++ this._bindingsToAdd = new Set(); ++ ++ this._eventListeners = []; ++ this._enabled = false; ++ ++ const docShell = frameTree.mainFrame().docShell(); ++ this._initialDPPX = docShell.contentViewer.overrideDPPX; ++ this._customScrollbars = null; ++ } ++ ++ async awaitViewportDimensions({width, height}) { ++ const win = this._frameTree.mainFrame().domWindow(); ++ if (win.innerWidth === width && win.innerHeight === height) ++ return; ++ await new Promise(resolve => { ++ const listener = helper.addEventListener(win, 'resize', () => { ++ if (win.innerWidth === width && win.innerHeight === height) { ++ helper.removeListeners([listener]); ++ resolve(); ++ } ++ }); ++ }); ++ } ++ ++ requestDetails({channelId}) { ++ return this._networkMonitor.requestDetails(channelId); ++ } ++ ++ async setViewport({deviceScaleFactor, isMobile, hasTouch}) { ++ const docShell = this._frameTree.mainFrame().docShell(); ++ docShell.contentViewer.overrideDPPX = deviceScaleFactor || this._initialDPPX; ++ docShell.deviceSizeIsPageSize = isMobile; ++ docShell.touchEventsOverride = hasTouch ? Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED : Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_NONE; ++ this._scrollbarManager.setFloatingScrollbars(isMobile); ++ } ++ ++ async setEmulatedMedia({media}) { ++ const docShell = this._frameTree.mainFrame().docShell(); ++ if (media) ++ docShell.contentViewer.emulateMedium(media); ++ else ++ docShell.contentViewer.stopEmulatingMedium(); ++ } ++ ++ async setUserAgent({userAgent}) { ++ const docShell = this._frameTree.mainFrame().docShell(); ++ docShell.customUserAgent = userAgent; ++ } ++ ++ addScriptToEvaluateOnNewDocument({script}) { ++ const scriptId = helper.generateId(); ++ this._scriptsToEvaluateOnNewDocument.set(scriptId, script); ++ return {scriptId}; ++ } ++ ++ removeScriptToEvaluateOnNewDocument({scriptId}) { ++ this._scriptsToEvaluateOnNewDocument.delete(scriptId); ++ } ++ ++ setCacheDisabled({cacheDisabled}) { ++ const enable = Ci.nsIRequest.LOAD_NORMAL; ++ const disable = Ci.nsIRequest.LOAD_BYPASS_CACHE | ++ Ci.nsIRequest.INHIBIT_CACHING; ++ ++ const docShell = this._frameTree.mainFrame().docShell(); ++ docShell.defaultLoadFlags = cacheDisabled ? disable : enable; ++ } ++ ++ setJavascriptEnabled({enabled}) { ++ const docShell = this._frameTree.mainFrame().docShell(); ++ docShell.allowJavascript = enabled; ++ } ++ ++ enable() { ++ if (this._enabled) ++ return; ++ ++ this._enabled = true; ++ // Dispatch frameAttached events for all initial frames ++ for (const frame of this._frameTree.frames()) { ++ this._onFrameAttached(frame); ++ if (frame.url()) ++ this._onNavigationCommitted(frame); ++ if (frame.pendingNavigationId()) ++ this._onNavigationStarted(frame); ++ } ++ this._eventListeners = [ ++ helper.addObserver(this._onDOMWindowCreated.bind(this), 'content-document-global-created'), ++ helper.addEventListener(this._session.mm(), 'DOMContentLoaded', this._onDOMContentLoaded.bind(this)), ++ helper.addEventListener(this._session.mm(), 'pageshow', this._onLoad.bind(this)), ++ helper.addEventListener(this._session.mm(), 'error', this._onError.bind(this)), ++ helper.on(this._frameTree, 'frameattached', this._onFrameAttached.bind(this)), ++ helper.on(this._frameTree, 'framedetached', this._onFrameDetached.bind(this)), ++ helper.on(this._frameTree, 'navigationstarted', this._onNavigationStarted.bind(this)), ++ helper.on(this._frameTree, 'navigationcommitted', this._onNavigationCommitted.bind(this)), ++ helper.on(this._frameTree, 'navigationaborted', this._onNavigationAborted.bind(this)), ++ helper.on(this._frameTree, 'samedocumentnavigation', this._onSameDocumentNavigation.bind(this)), ++ ]; ++ } ++ ++ _onDOMContentLoaded(event) { ++ const docShell = event.target.ownerGlobal.docShell; ++ const frame = this._frameTree.frameForDocShell(docShell); ++ if (!frame) ++ return; ++ this._session.emitEvent('Page.eventFired', { ++ frameId: frame.id(), ++ name: 'DOMContentLoaded', ++ }); ++ } ++ ++ _onError(errorEvent) { ++ const docShell = errorEvent.target.ownerGlobal.docShell; ++ const frame = this._frameTree.frameForDocShell(docShell); ++ if (!frame) ++ return; ++ this._session.emitEvent('Page.uncaughtError', { ++ frameId: frame.id(), ++ message: errorEvent.message, ++ stack: errorEvent.error.stack ++ }); ++ } ++ ++ _onLoad(event) { ++ const docShell = event.target.ownerGlobal.docShell; ++ const frame = this._frameTree.frameForDocShell(docShell); ++ if (!frame) ++ return; ++ this._session.emitEvent('Page.eventFired', { ++ frameId: frame.id(), ++ name: 'load' ++ }); ++ } ++ ++ _onNavigationStarted(frame) { ++ this._session.emitEvent('Page.navigationStarted', { ++ frameId: frame.id(), ++ navigationId: frame.pendingNavigationId(), ++ url: frame.pendingNavigationURL(), ++ }); ++ } ++ ++ _onNavigationAborted(frame, navigationId, errorText) { ++ this._session.emitEvent('Page.navigationAborted', { ++ frameId: frame.id(), ++ navigationId, ++ errorText, ++ }); ++ } ++ ++ _onSameDocumentNavigation(frame) { ++ this._session.emitEvent('Page.sameDocumentNavigation', { ++ frameId: frame.id(), ++ url: frame.url(), ++ }); ++ } ++ ++ _onNavigationCommitted(frame) { ++ this._session.emitEvent('Page.navigationCommitted', { ++ frameId: frame.id(), ++ navigationId: frame.lastCommittedNavigationId(), ++ url: frame.url(), ++ name: frame.name(), ++ }); ++ } ++ ++ _onDOMWindowCreated(window) { ++ const docShell = window.docShell; ++ const frame = this._frameTree.frameForDocShell(docShell); ++ if (!frame) ++ return; ++ ++ if (this._frameToExecutionContext.has(frame)) { ++ this._runtime.destroyExecutionContext(this._frameToExecutionContext.get(frame)); ++ this._frameToExecutionContext.delete(frame); ++ } ++ const executionContext = this._ensureExecutionContext(frame); ++ ++ if (!this._scriptsToEvaluateOnNewDocument.size && !this._bindingsToAdd.size) ++ return; ++ for (const bindingName of this._bindingsToAdd.values()) ++ this._exposeFunction(frame, bindingName); ++ for (const script of this._scriptsToEvaluateOnNewDocument.values()) { ++ try { ++ let result = executionContext.evaluateScript(script); ++ if (result && result.objectId) ++ executionContext.disposeObject(result.objectId); ++ } catch (e) { ++ } ++ } ++ } ++ ++ _onFrameAttached(frame) { ++ this._session.emitEvent('Page.frameAttached', { ++ frameId: frame.id(), ++ parentFrameId: frame.parentFrame() ? frame.parentFrame().id() : undefined, ++ }); ++ this._ensureExecutionContext(frame); ++ } ++ ++ _onFrameDetached(frame) { ++ this._session.emitEvent('Page.frameDetached', { ++ frameId: frame.id(), ++ }); ++ } ++ ++ _ensureExecutionContext(frame) { ++ let executionContext = this._frameToExecutionContext.get(frame); ++ if (!executionContext) { ++ executionContext = this._runtime.createExecutionContext(frame.domWindow(), { ++ frameId: frame.id(), ++ }); ++ this._frameToExecutionContext.set(frame, executionContext); ++ } ++ return executionContext; ++ } ++ ++ dispose() { ++ helper.removeListeners(this._eventListeners); ++ } ++ ++ async navigate({frameId, url, referer}) { ++ try { ++ const uri = NetUtil.newURI(url); ++ } catch (e) { ++ throw new Error(`Invalid url: "${url}"`); ++ } ++ let referrerURI = null; ++ let referrerInfo = null; ++ if (referer) { ++ try { ++ referrerURI = NetUtil.newURI(referer); ++ const ReferrerInfo = Components.Constructor( ++ '@mozilla.org/referrer-info;1', ++ 'nsIReferrerInfo', ++ 'init' ++ ); ++ referrerInfo = new ReferrerInfo(Ci.nsIHttpChannel.REFERRER_POLICY_UNSET, true, referrerURI); ++ } catch (e) { ++ throw new Error(`Invalid referer: "${referer}"`); ++ } ++ } ++ const frame = this._frameTree.frame(frameId); ++ const docShell = frame.docShell().QueryInterface(Ci.nsIWebNavigation); ++ docShell.loadURI(url, { ++ flags: Ci.nsIWebNavigation.LOAD_FLAGS_NONE, ++ referrerInfo, ++ postData: null, ++ headers: null, ++ }); ++ return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()}; ++ } ++ ++ async reload({frameId, url}) { ++ const frame = this._frameTree.frame(frameId); ++ const docShell = frame.docShell().QueryInterface(Ci.nsIWebNavigation); ++ docShell.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE); ++ return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()}; ++ } ++ ++ async goBack({frameId, url}) { ++ const frame = this._frameTree.frame(frameId); ++ const docShell = frame.docShell(); ++ if (!docShell.canGoBack) ++ return {navigationId: null, navigationURL: null}; ++ docShell.goBack(); ++ return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()}; ++ } ++ ++ async goForward({frameId, url}) { ++ const frame = this._frameTree.frame(frameId); ++ const docShell = frame.docShell(); ++ if (!docShell.canGoForward) ++ return {navigationId: null, navigationURL: null}; ++ docShell.goForward(); ++ return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()}; ++ } ++ ++ addBinding({name}) { ++ if (this._bindingsToAdd.has(name)) ++ throw new Error(`Binding with name ${name} already exists`); ++ this._bindingsToAdd.add(name); ++ for (const frame of this._frameTree.frames()) ++ this._exposeFunction(frame, name); ++ } ++ ++ _exposeFunction(frame, name) { ++ Cu.exportFunction((...args) => { ++ const executionContext = this._ensureExecutionContext(frame); ++ this._session.emitEvent('Page.bindingCalled', { ++ executionContextId: executionContext.id(), ++ name, ++ payload: args[0] ++ }); ++ }, frame.domWindow(), { ++ defineAs: name, ++ }); ++ } ++ ++ async setFileInputFiles({objectId, frameId, files}) { ++ const frame = this._frameTree.frame(frameId); ++ if (!frame) ++ throw new Error('Failed to find frame with id = ' + frameId); ++ const executionContext = this._ensureExecutionContext(frame); ++ const unsafeObject = executionContext.unsafeObject(objectId); ++ if (!unsafeObject) ++ throw new Error('Object is not input!'); ++ const nsFiles = await Promise.all(files.map(filePath => File.createFromFileName(filePath))); ++ unsafeObject.mozSetFileArray(nsFiles); ++ } ++ ++ getContentQuads({objectId, frameId}) { ++ const frame = this._frameTree.frame(frameId); ++ if (!frame) ++ throw new Error('Failed to find frame with id = ' + frameId); ++ const executionContext = this._ensureExecutionContext(frame); ++ const unsafeObject = executionContext.unsafeObject(objectId); ++ if (!unsafeObject.getBoxQuads) ++ throw new Error('RemoteObject is not a node'); ++ const quads = unsafeObject.getBoxQuads({relativeTo: this._frameTree.mainFrame().domWindow().document}).map(quad => { ++ return { ++ p1: {x: quad.p1.x, y: quad.p1.y}, ++ p2: {x: quad.p2.x, y: quad.p2.y}, ++ p3: {x: quad.p3.x, y: quad.p3.y}, ++ p4: {x: quad.p4.x, y: quad.p4.y}, ++ }; ++ }); ++ return {quads}; ++ } ++ ++ contentFrame({objectId, frameId}) { ++ const frame = this._frameTree.frame(frameId); ++ if (!frame) ++ throw new Error('Failed to find frame with id = ' + frameId); ++ const executionContext = this._ensureExecutionContext(frame); ++ const unsafeObject = executionContext.unsafeObject(objectId); ++ if (!unsafeObject.contentWindow) ++ return null; ++ const contentFrame = this._frameTree.frameForDocShell(unsafeObject.contentWindow.docShell); ++ return {frameId: contentFrame.id()}; ++ } ++ ++ async getBoundingBox({frameId, objectId}) { ++ const frame = this._frameTree.frame(frameId); ++ if (!frame) ++ throw new Error('Failed to find frame with id = ' + frameId); ++ const executionContext = this._ensureExecutionContext(frame); ++ const unsafeObject = executionContext.unsafeObject(objectId); ++ if (!unsafeObject.getBoxQuads) ++ throw new Error('RemoteObject is not a node'); ++ const quads = unsafeObject.getBoxQuads({relativeTo: this._frameTree.mainFrame().domWindow().document}); ++ if (!quads.length) ++ return null; ++ let x1 = Infinity; ++ let y1 = Infinity; ++ let x2 = -Infinity; ++ let y2 = -Infinity; ++ for (const quad of quads) { ++ const boundingBox = quad.getBounds(); ++ x1 = Math.min(boundingBox.x, x1); ++ y1 = Math.min(boundingBox.y, y1); ++ x2 = Math.max(boundingBox.x + boundingBox.width, x2); ++ y2 = Math.max(boundingBox.y + boundingBox.height, y2); ++ } ++ return {x: x1 + frame.domWindow().scrollX, y: y1 + frame.domWindow().scrollY, width: x2 - x1, height: y2 - y1}; ++ } ++ ++ async screenshot({mimeType, fullPage, clip}) { ++ const content = this._session.mm().content; ++ if (clip) { ++ const data = takeScreenshot(content, clip.x, clip.y, clip.width, clip.height, mimeType); ++ return {data}; ++ } ++ if (fullPage) { ++ const rect = content.document.documentElement.getBoundingClientRect(); ++ const width = content.innerWidth + content.scrollMaxX - content.scrollMinX; ++ const height = content.innerHeight + content.scrollMaxY - content.scrollMinY; ++ const data = takeScreenshot(content, 0, 0, width, height, mimeType); ++ return {data}; ++ } ++ const data = takeScreenshot(content, content.scrollX, content.scrollY, content.innerWidth, content.innerHeight, mimeType); ++ return {data}; ++ } ++ ++ async dispatchKeyEvent({type, keyCode, code, key, repeat, location}) { ++ const frame = this._frameTree.mainFrame(); ++ const tip = frame.textInputProcessor(); ++ if (key === 'Meta' && Services.appinfo.OS !== 'Darwin') ++ key = 'OS'; ++ else if (key === 'OS' && Services.appinfo.OS === 'Darwin') ++ key = 'Meta'; ++ let keyEvent = new (frame.domWindow().KeyboardEvent)("", { ++ key, ++ code, ++ location, ++ repeat, ++ keyCode ++ }); ++ const flags = 0; ++ if (type === 'keydown') ++ tip.keydown(keyEvent, flags); ++ else if (type === 'keyup') ++ tip.keyup(keyEvent, flags); ++ else ++ throw new Error(`Unknown type ${type}`); ++ } ++ ++ async dispatchTouchEvent({type, touchPoints, modifiers}) { ++ const frame = this._frameTree.mainFrame(); ++ const defaultPrevented = frame.domWindow().windowUtils.sendTouchEvent( ++ type.toLowerCase(), ++ touchPoints.map((point, id) => id), ++ touchPoints.map(point => point.x), ++ touchPoints.map(point => point.y), ++ touchPoints.map(point => point.radiusX === undefined ? 1.0 : point.radiusX), ++ touchPoints.map(point => point.radiusY === undefined ? 1.0 : point.radiusY), ++ touchPoints.map(point => point.rotationAngle === undefined ? 0.0 : point.rotationAngle), ++ touchPoints.map(point => point.force === undefined ? 1.0 : point.force), ++ touchPoints.length, ++ modifiers); ++ return {defaultPrevented}; ++ } ++ ++ async dispatchMouseEvent({type, x, y, button, clickCount, modifiers, buttons}) { ++ const frame = this._frameTree.mainFrame(); ++ frame.domWindow().windowUtils.sendMouseEvent( ++ type, ++ x, ++ y, ++ button, ++ clickCount, ++ modifiers, ++ false /*aIgnoreRootScrollFrame*/, ++ undefined /*pressure*/, ++ undefined /*inputSource*/, ++ undefined /*isDOMEventSynthesized*/, ++ undefined /*isWidgetEventSynthesized*/, ++ buttons); ++ if (type === 'mousedown' && button === 2) { ++ frame.domWindow().windowUtils.sendMouseEvent( ++ 'contextmenu', ++ x, ++ y, ++ button, ++ clickCount, ++ modifiers, ++ false /*aIgnoreRootScrollFrame*/, ++ undefined /*pressure*/, ++ undefined /*inputSource*/, ++ undefined /*isDOMEventSynthesized*/, ++ undefined /*isWidgetEventSynthesized*/, ++ buttons); ++ } ++ } ++ ++ async insertText({text}) { ++ const frame = this._frameTree.mainFrame(); ++ frame.textInputProcessor().commitCompositionWith(text); ++ } ++ ++ async getFullAXTree() { ++ const service = Cc["@mozilla.org/accessibilityService;1"] ++ .getService(Ci.nsIAccessibilityService); ++ const document = this._frameTree.mainFrame().domWindow().document; ++ const docAcc = service.getAccessibleFor(document); ++ ++ async function waitForQuiet() { ++ let state = {}; ++ docAcc.getState(state, {}); ++ if ((state.value & Ci.nsIAccessibleStates.STATE_BUSY) == 0) ++ return; ++ let resolve, reject; ++ const promise = new Promise((x, y) => {resolve = x, reject = y}); ++ let eventObserver = { ++ observe(subject, topic) { ++ if (topic !== "accessible-event") { ++ return; ++ } ++ ++ // If event type does not match expected type, skip the event. ++ let event = subject.QueryInterface(Ci.nsIAccessibleEvent); ++ if (event.eventType !== Ci.nsIAccessibleEvent.EVENT_STATE_CHANGE) { ++ return; ++ } ++ ++ // If event's accessible does not match expected accessible, ++ // skip the event. ++ if (event.accessible !== docAcc) { ++ return; ++ } ++ ++ Services.obs.removeObserver(this, "accessible-event"); ++ resolve(); ++ }, ++ }; ++ Services.obs.addObserver(eventObserver, "accessible-event"); ++ return promise; ++ } ++ function buildNode(accElement) { ++ let a = {}, b = {}; ++ accElement.getState(a, b); ++ const tree = { ++ role: service.getStringRole(accElement.role), ++ name: accElement.name || '', ++ }; ++ for (const userStringProperty of [ ++ 'value', ++ 'description' ++ ]) { ++ tree[userStringProperty] = accElement[userStringProperty] || undefined; ++ } ++ ++ const states = {}; ++ for (const name of service.getStringStates(a.value, b.value)) ++ states[name] = true; ++ for (const name of ['selected', ++ 'focused', ++ 'pressed', ++ 'focusable', ++ 'haspopup', ++ 'required', ++ 'invalid', ++ 'modal', ++ 'editable', ++ 'busy', ++ 'checked', ++ 'multiselectable']) { ++ if (states[name]) ++ tree[name] = true; ++ } ++ ++ if (states['multi line']) ++ tree['multiline'] = true; ++ if (states['editable'] && states['readonly']) ++ tree['readonly'] = true; ++ if (states['checked']) ++ tree['checked'] = true; ++ if (states['mixed']) ++ tree['checked'] = 'mixed'; ++ if (states['expanded']) ++ tree['expanded'] = true; ++ else if (states['collapsed']) ++ tree['expanded'] = false ++ if (!states['enabled']) ++ tree['disabled'] = true; ++ ++ const attributes = {}; ++ if (accElement.attributes) { ++ for (const { key, value } of accElement.attributes.enumerate()) { ++ attributes[key] = value; ++ } ++ } ++ for (const numericalProperty of ['level']) { ++ if (numericalProperty in attributes) ++ tree[numericalProperty] = parseFloat(attributes[numericalProperty]); ++ } ++ for (const stringProperty of ['tag', 'roledescription', 'valuetext', 'orientation', 'autocomplete', 'keyshortcuts']) { ++ if (stringProperty in attributes) ++ tree[stringProperty] = attributes[stringProperty]; ++ } ++ const children = []; ++ ++ for (let child = accElement.firstChild; child; child = child.nextSibling) { ++ children.push(buildNode(child)); ++ } ++ if (children.length) ++ tree.children = children; ++ return tree; ++ } ++ await waitForQuiet(); ++ return { ++ tree: buildNode(docAcc) ++ }; ++ } ++} ++ ++function takeScreenshot(win, left, top, width, height, mimeType) { ++ const MAX_SKIA_DIMENSIONS = 32767; ++ ++ const scale = win.devicePixelRatio; ++ const canvasWidth = width * scale; ++ const canvasHeight = height * scale; ++ ++ if (canvasWidth > MAX_SKIA_DIMENSIONS || canvasHeight > MAX_SKIA_DIMENSIONS) ++ throw new Error('Cannot take screenshot larger than ' + MAX_SKIA_DIMENSIONS); ++ ++ const canvas = win.document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas'); ++ canvas.width = canvasWidth; ++ canvas.height = canvasHeight; ++ ++ let ctx = canvas.getContext('2d'); ++ ctx.scale(scale, scale); ++ ctx.drawWindow(win, left, top, width, height, 'rgb(255,255,255)', ctx.DRAWWINDOW_DRAW_CARET); ++ const dataURL = canvas.toDataURL(mimeType); ++ return dataURL.substring(dataURL.indexOf(',') + 1); ++}; ++ ++var EXPORTED_SYMBOLS = ['PageAgent']; ++this.PageAgent = PageAgent; ++ +diff --git a/testing/juggler/content/RuntimeAgent.js b/testing/juggler/content/RuntimeAgent.js +new file mode 100644 +index 000000000000..2c474230071b +--- /dev/null ++++ b/testing/juggler/content/RuntimeAgent.js +@@ -0,0 +1,460 @@ ++"use strict"; ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const {addDebuggerToGlobal} = ChromeUtils.import("resource://gre/modules/jsdebugger.jsm", {}); ++ ++const Ci = Components.interfaces; ++const Cr = Components.results; ++const Cu = Components.utils; ++addDebuggerToGlobal(Cu.getGlobalForObject(this)); ++const helper = new Helper(); ++ ++const consoleLevelToProtocolType = { ++ 'dir': 'dir', ++ 'log': 'log', ++ 'debug': 'debug', ++ 'info': 'info', ++ 'error': 'error', ++ 'warn': 'warning', ++ 'dirxml': 'dirxml', ++ 'table': 'table', ++ 'trace': 'trace', ++ 'clear': 'clear', ++ 'group': 'startGroup', ++ 'groupCollapsed': 'startGroupCollapsed', ++ 'groupEnd': 'endGroup', ++ 'assert': 'assert', ++ 'profile': 'profile', ++ 'profileEnd': 'profileEnd', ++ 'count': 'count', ++ 'countReset': 'countReset', ++ 'time': null, ++ 'timeLog': 'timeLog', ++ 'timeEnd': 'timeEnd', ++ 'timeStamp': 'timeStamp', ++}; ++ ++const disallowedMessageCategories = new Set([ ++ 'XPConnect JavaScript', ++ 'component javascript', ++ 'chrome javascript', ++ 'chrome registration', ++ 'XBL', ++ 'XBL Prototype Handler', ++ 'XBL Content Sink', ++ 'xbl javascript', ++]); ++ ++class RuntimeAgent { ++ constructor(session) { ++ this._debugger = new Debugger(); ++ this._pendingPromises = new Map(); ++ this._session = session; ++ this._executionContexts = new Map(); ++ this._windowToExecutionContext = new Map(); ++ this._consoleServiceListener = { ++ QueryInterface: ChromeUtils.generateQI([Ci.nsIConsoleListener]), ++ ++ observe: message => { ++ if (!(message instanceof Ci.nsIScriptError) || !message.outerWindowID || ++ !message.category || disallowedMessageCategories.has(message.category)) { ++ return; ++ } ++ const errorWindow = Services.wm.getOuterWindowWithId(message.outerWindowID); ++ const executionContext = this._windowToExecutionContext.get(errorWindow); ++ if (!executionContext) ++ return; ++ const typeNames = { ++ [Ci.nsIConsoleMessage.debug]: 'debug', ++ [Ci.nsIConsoleMessage.info]: 'info', ++ [Ci.nsIConsoleMessage.warn]: 'warn', ++ [Ci.nsIConsoleMessage.error]: 'error', ++ }; ++ this._session.emitEvent('Runtime.console', { ++ args: [{ ++ value: message.message, ++ }], ++ type: typeNames[message.logLevel], ++ executionContextId: executionContext.id(), ++ location: { ++ lineNumber: message.lineNumber, ++ columnNumber: message.columnNumber, ++ url: message.sourceName, ++ }, ++ }); ++ }, ++ }; ++ ++ this._eventListeners = []; ++ this._enabled = false; ++ } ++ ++ _consoleAPICalled({wrappedJSObject}, topic, data) { ++ const type = consoleLevelToProtocolType[wrappedJSObject.level]; ++ if (!type) ++ return; ++ const executionContext = Array.from(this._executionContexts.values()).find(context => { ++ const domWindow = context._domWindow; ++ return domWindow && domWindow.windowUtils.currentInnerWindowID === wrappedJSObject.innerID; ++ }); ++ if (!executionContext) ++ return; ++ const args = wrappedJSObject.arguments.map(arg => executionContext.rawValueToRemoteObject(arg)); ++ this._session.emitEvent('Runtime.console', { ++ args, ++ type, ++ executionContextId: executionContext.id(), ++ location: { ++ lineNumber: wrappedJSObject.lineNumber - 1, ++ columnNumber: wrappedJSObject.columnNumber - 1, ++ url: wrappedJSObject.filename, ++ }, ++ }); ++ } ++ ++ enable() { ++ if (this._enabled) ++ return; ++ this._enabled = true; ++ for (const executionContext of this._executionContexts.values()) ++ this._notifyExecutionContextCreated(executionContext); ++ Services.console.registerListener(this._consoleServiceListener); ++ this._eventListeners = [ ++ () => Services.console.unregisterListener(this._consoleServiceListener), ++ helper.addObserver(this._consoleAPICalled.bind(this), "console-api-log-event"), ++ ]; ++ } ++ ++ _notifyExecutionContextCreated(executionContext) { ++ if (!this._enabled) ++ return; ++ this._session.emitEvent('Runtime.executionContextCreated', { ++ executionContextId: executionContext._id, ++ auxData: executionContext._auxData, ++ }); ++ } ++ ++ _notifyExecutionContextDestroyed(executionContext) { ++ if (!this._enabled) ++ return; ++ this._session.emitEvent('Runtime.executionContextDestroyed', { ++ executionContextId: executionContext._id, ++ }); ++ } ++ ++ dispose() { ++ helper.removeListeners(this._eventListeners); ++ } ++ ++ async _awaitPromise(executionContext, obj, exceptionDetails = {}) { ++ if (obj.promiseState === 'fulfilled') ++ return {success: true, obj: obj.promiseValue}; ++ if (obj.promiseState === 'rejected') { ++ const global = executionContext._global; ++ exceptionDetails.text = global.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}).return; ++ exceptionDetails.stack = global.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}).return; ++ return {success: false, obj: null}; ++ } ++ let resolve, reject; ++ const promise = new Promise((a, b) => { ++ resolve = a; ++ reject = b; ++ }); ++ this._pendingPromises.set(obj.promiseID, {resolve, reject, executionContext, exceptionDetails}); ++ if (this._pendingPromises.size === 1) ++ this._debugger.onPromiseSettled = this._onPromiseSettled.bind(this); ++ return await promise; ++ } ++ ++ _onPromiseSettled(obj) { ++ const pendingPromise = this._pendingPromises.get(obj.promiseID); ++ if (!pendingPromise) ++ return; ++ this._pendingPromises.delete(obj.promiseID); ++ if (!this._pendingPromises.size) ++ this._debugger.onPromiseSettled = undefined; ++ ++ if (obj.promiseState === 'fulfilled') { ++ pendingPromise.resolve({success: true, obj: obj.promiseValue}); ++ return; ++ }; ++ const global = pendingPromise.executionContext._global; ++ pendingPromise.exceptionDetails.text = global.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}).return; ++ pendingPromise.exceptionDetails.stack = global.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}).return; ++ pendingPromise.resolve({success: false, obj: null}); ++ } ++ ++ createExecutionContext(domWindow, auxData) { ++ const context = new ExecutionContext(this, domWindow, this._debugger.addDebuggee(domWindow), auxData); ++ this._executionContexts.set(context._id, context); ++ this._windowToExecutionContext.set(domWindow, context); ++ this._notifyExecutionContextCreated(context); ++ return context; ++ } ++ ++ destroyExecutionContext(destroyedContext) { ++ for (const [promiseID, {reject, executionContext}] of this._pendingPromises) { ++ if (executionContext === destroyedContext) { ++ reject(new Error('Execution context was destroyed!')); ++ this._pendingPromises.delete(promiseID); ++ } ++ } ++ if (!this._pendingPromises.size) ++ this._debugger.onPromiseSettled = undefined; ++ this._debugger.removeDebuggee(destroyedContext._domWindow); ++ this._executionContexts.delete(destroyedContext._id); ++ this._windowToExecutionContext.delete(destroyedContext._domWindow); ++ this._notifyExecutionContextDestroyed(destroyedContext); ++ } ++ ++ async evaluate({executionContextId, expression, returnByValue}) { ++ const executionContext = this._executionContexts.get(executionContextId); ++ if (!executionContext) ++ throw new Error('Failed to find execution context with id = ' + executionContextId); ++ const exceptionDetails = {}; ++ let result = await executionContext.evaluateScript(expression, exceptionDetails); ++ if (!result) ++ return {exceptionDetails}; ++ let isNode = undefined; ++ if (returnByValue) ++ result = executionContext.ensureSerializedToValue(result); ++ return {result}; ++ } ++ ++ async callFunction({executionContextId, functionDeclaration, args, returnByValue}) { ++ const executionContext = this._executionContexts.get(executionContextId); ++ if (!executionContext) ++ throw new Error('Failed to find execution context with id = ' + executionContextId); ++ const exceptionDetails = {}; ++ let result = await executionContext.evaluateFunction(functionDeclaration, args, exceptionDetails); ++ if (!result) ++ return {exceptionDetails}; ++ let isNode = undefined; ++ if (returnByValue) ++ result = executionContext.ensureSerializedToValue(result); ++ return {result}; ++ } ++ ++ async getObjectProperties({executionContextId, objectId}) { ++ const executionContext = this._executionContexts.get(executionContextId); ++ if (!executionContext) ++ throw new Error('Failed to find execution context with id = ' + executionContextId); ++ return {properties: executionContext.getObjectProperties(objectId)}; ++ } ++ ++ async disposeObject({executionContextId, objectId}) { ++ const executionContext = this._executionContexts.get(executionContextId); ++ if (!executionContext) ++ throw new Error('Failed to find execution context with id = ' + executionContextId); ++ return executionContext.disposeObject(objectId); ++ } ++} ++ ++class ExecutionContext { ++ constructor(runtime, domWindow, global, auxData) { ++ this._runtime = runtime; ++ this._domWindow = domWindow; ++ this._global = global; ++ this._remoteObjects = new Map(); ++ this._id = helper.generateId(); ++ this._auxData = auxData; ++ } ++ ++ id() { ++ return this._id; ++ } ++ ++ async evaluateScript(script, exceptionDetails = {}) { ++ const userInputHelper = this._domWindow.windowUtils.setHandlingUserInput(true); ++ let {success, obj} = this._getResult(this._global.executeInGlobal(script), exceptionDetails); ++ userInputHelper.destruct(); ++ if (!success) ++ return null; ++ if (obj && obj.isPromise) { ++ const awaitResult = await this._runtime._awaitPromise(this, obj, exceptionDetails); ++ if (!awaitResult.success) ++ return null; ++ obj = awaitResult.obj; ++ } ++ return this._createRemoteObject(obj); ++ } ++ ++ async evaluateFunction(functionText, args, exceptionDetails = {}) { ++ const funEvaluation = this._getResult(this._global.executeInGlobal('(' + functionText + ')'), exceptionDetails); ++ if (!funEvaluation.success) ++ return null; ++ if (!funEvaluation.obj.callable) ++ throw new Error('functionText does not evaluate to a function!'); ++ args = args.map(arg => { ++ if (arg.objectId) { ++ if (!this._remoteObjects.has(arg.objectId)) ++ throw new Error('Cannot find object with id = ' + arg.objectId); ++ return this._remoteObjects.get(arg.objectId); ++ } ++ switch (arg.unserializableValue) { ++ case 'Infinity': return Infinity; ++ case '-Infinity': return -Infinity; ++ case '-0': return -0; ++ case 'NaN': return NaN; ++ default: return this._toDebugger(arg.value); ++ } ++ }); ++ const userInputHelper = this._domWindow.windowUtils.setHandlingUserInput(true); ++ let {success, obj} = this._getResult(funEvaluation.obj.apply(null, args), exceptionDetails); ++ userInputHelper.destruct(); ++ if (!success) ++ return null; ++ if (obj && obj.isPromise) { ++ const awaitResult = await this._runtime._awaitPromise(this, obj, exceptionDetails); ++ if (!awaitResult.success) ++ return null; ++ obj = awaitResult.obj; ++ } ++ return this._createRemoteObject(obj); ++ } ++ ++ unsafeObject(objectId) { ++ if (!this._remoteObjects.has(objectId)) ++ throw new Error('Cannot find object with id = ' + objectId); ++ return this._remoteObjects.get(objectId).unsafeDereference(); ++ } ++ ++ rawValueToRemoteObject(rawValue) { ++ const debuggerObj = this._global.makeDebuggeeValue(rawValue); ++ return this._createRemoteObject(debuggerObj); ++ } ++ ++ _createRemoteObject(debuggerObj) { ++ if (debuggerObj instanceof Debugger.Object) { ++ const objectId = helper.generateId(); ++ this._remoteObjects.set(objectId, debuggerObj); ++ const rawObj = debuggerObj.unsafeDereference(); ++ const type = typeof rawObj; ++ let subtype = undefined; ++ if (debuggerObj.isProxy) ++ subtype = 'proxy'; ++ else if (Array.isArray(rawObj)) ++ subtype = 'array'; ++ else if (Object.is(rawObj, null)) ++ subtype = 'null'; ++ else if (rawObj instanceof this._domWindow.Node) ++ subtype = 'node'; ++ else if (rawObj instanceof this._domWindow.RegExp) ++ subtype = 'regexp'; ++ else if (rawObj instanceof this._domWindow.Date) ++ subtype = 'date'; ++ else if (rawObj instanceof this._domWindow.Map) ++ subtype = 'map'; ++ else if (rawObj instanceof this._domWindow.Set) ++ subtype = 'set'; ++ else if (rawObj instanceof this._domWindow.WeakMap) ++ subtype = 'weakmap'; ++ else if (rawObj instanceof this._domWindow.WeakSet) ++ subtype = 'weakset'; ++ else if (rawObj instanceof this._domWindow.Error) ++ subtype = 'error'; ++ else if (rawObj instanceof this._domWindow.Promise) ++ subtype = 'promise'; ++ else if ((rawObj instanceof this._domWindow.Int8Array) || (rawObj instanceof this._domWindow.Uint8Array) || ++ (rawObj instanceof this._domWindow.Uint8ClampedArray) || (rawObj instanceof this._domWindow.Int16Array) || ++ (rawObj instanceof this._domWindow.Uint16Array) || (rawObj instanceof this._domWindow.Int32Array) || ++ (rawObj instanceof this._domWindow.Uint32Array) || (rawObj instanceof this._domWindow.Float32Array) || ++ (rawObj instanceof this._domWindow.Float64Array)) { ++ subtype = 'typedarray'; ++ } ++ const isNode = debuggerObj.unsafeDereference() instanceof this._domWindow.Node; ++ return {objectId, type, subtype}; ++ } ++ if (typeof debuggerObj === 'symbol') { ++ const objectId = helper.generateId(); ++ this._remoteObjects.set(objectId, debuggerObj); ++ return {objectId, type: 'symbol'}; ++ } ++ ++ let unserializableValue = undefined; ++ if (Object.is(debuggerObj, NaN)) ++ unserializableValue = 'NaN'; ++ else if (Object.is(debuggerObj, -0)) ++ unserializableValue = '-0'; ++ else if (Object.is(debuggerObj, Infinity)) ++ unserializableValue = 'Infinity'; ++ else if (Object.is(debuggerObj, -Infinity)) ++ unserializableValue = '-Infinity'; ++ return unserializableValue ? {unserializableValue} : {value: debuggerObj}; ++ } ++ ++ ensureSerializedToValue(protocolObject) { ++ if (!protocolObject.objectId) ++ return protocolObject; ++ const obj = this._remoteObjects.get(protocolObject.objectId); ++ this._remoteObjects.delete(protocolObject.objectId); ++ return {value: this._serialize(obj)}; ++ } ++ ++ _toDebugger(obj) { ++ if (typeof obj !== 'object') ++ return obj; ++ const properties = {}; ++ for (let [key, value] of Object.entries(obj)) { ++ properties[key] = { ++ writable: true, ++ enumerable: true, ++ value: this._toDebugger(value), ++ }; ++ } ++ const baseObject = Array.isArray(obj) ? '([])' : '({})'; ++ const debuggerObj = this._global.executeInGlobal(baseObject).return; ++ debuggerObj.defineProperties(properties); ++ return debuggerObj; ++ } ++ ++ _serialize(obj) { ++ const result = this._global.executeInGlobalWithBindings('JSON.stringify(e)', {e: obj}); ++ if (result.throw) ++ throw new Error('Object is not serializable'); ++ return JSON.parse(result.return); ++ } ++ ++ disposeObject(objectId) { ++ this._remoteObjects.delete(objectId); ++ } ++ ++ getObjectProperties(objectId) { ++ if (!this._remoteObjects.has(objectId)) ++ throw new Error('Cannot find object with id = ' + arg.objectId); ++ const result = []; ++ for (let obj = this._remoteObjects.get(objectId); obj; obj = obj.proto) { ++ for (const propertyName of obj.getOwnPropertyNames()) { ++ const descriptor = obj.getOwnPropertyDescriptor(propertyName); ++ if (!descriptor.enumerable) ++ continue; ++ result.push({ ++ name: propertyName, ++ value: this._createRemoteObject(descriptor.value), ++ }); ++ } ++ } ++ return result; ++ } ++ ++ _getResult(completionValue, exceptionDetails = {}) { ++ if (!completionValue) { ++ exceptionDetails.text = 'Evaluation terminated!'; ++ exceptionDetails.stack = ''; ++ return {success: false, obj: null}; ++ } ++ if (completionValue.throw) { ++ if (this._global.executeInGlobalWithBindings('e instanceof Error', {e: completionValue.throw}).return) { ++ exceptionDetails.text = this._global.executeInGlobalWithBindings('e.message', {e: completionValue.throw}).return; ++ exceptionDetails.stack = this._global.executeInGlobalWithBindings('e.stack', {e: completionValue.throw}).return; ++ } else { ++ exceptionDetails.value = this._serialize(completionValue.throw); ++ } ++ return {success: false, obj: null}; ++ } ++ return {success: true, obj: completionValue.return}; ++ } ++} ++ ++var EXPORTED_SYMBOLS = ['RuntimeAgent']; ++this.RuntimeAgent = RuntimeAgent; +diff --git a/testing/juggler/content/ScrollbarManager.js b/testing/juggler/content/ScrollbarManager.js +new file mode 100644 +index 000000000000..caee4df323d0 +--- /dev/null ++++ b/testing/juggler/content/ScrollbarManager.js +@@ -0,0 +1,85 @@ ++const Ci = Components.interfaces; ++const Cr = Components.results; ++const Cu = Components.utils; ++const Cc = Components.classes; ++ ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++ ++const HIDDEN_SCROLLBARS = Services.io.newURI('chrome://juggler/content/content/hidden-scrollbars.css'); ++const FLOATING_SCROLLBARS = Services.io.newURI('chrome://juggler/content/content/floating-scrollbars.css'); ++ ++const isHeadless = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless; ++const helper = new Helper(); ++ ++class ScrollbarManager { ++ constructor(docShell) { ++ this._docShell = docShell; ++ this._customScrollbars = null; ++ this._contentViewerScrollBars = new Map(); ++ ++ if (isHeadless) ++ this._setCustomScrollbars(HIDDEN_SCROLLBARS); ++ ++ const webProgress = this._docShell.QueryInterface(Ci.nsIInterfaceRequestor) ++ .getInterface(Ci.nsIWebProgress); ++ ++ this.QueryInterface = ChromeUtils.generateQI(['nsIWebProgressListener', 'nsISupportsWeakReference']); ++ this._eventListeners = [ ++ helper.addProgressListener(webProgress, this, Ci.nsIWebProgress.NOTIFY_ALL), ++ ]; ++ } ++ ++ onLocationChange(webProgress, request, URI, flags) { ++ if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) ++ return; ++ this._updateAllDocShells(); ++ } ++ ++ setFloatingScrollbars(enabled) { ++ if (this._customScrollbars === HIDDEN_SCROLLBARS) ++ return; ++ this._setCustomScrollbars(enabled ? FLOATING_SCROLLBARS : null); ++ } ++ ++ _setCustomScrollbars(customScrollbars) { ++ if (this._customScrollbars === customScrollbars) ++ return; ++ this._customScrollbars = customScrollbars; ++ this._updateAllDocShells(); ++ } ++ ++ _updateAllDocShells() { ++ const allDocShells = [this._docShell]; ++ for (let i = 0; i < this._docShell.childCount; i++) ++ allDocShells.push(this._docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell)); ++ // At this point, a content viewer might not be loaded for certain docShells. ++ // Scrollbars will be updated in onLocationChange. ++ const contentViewers = allDocShells.map(docShell => docShell.contentViewer).filter(contentViewer => !!contentViewer); ++ ++ // Update scrollbar stylesheets. ++ for (const contentViewer of contentViewers) { ++ const oldScrollbars = this._contentViewerScrollBars.get(contentViewer); ++ if (oldScrollbars === this._customScrollbars) ++ continue; ++ const winUtils = contentViewer.DOMDocument.defaultView.windowUtils; ++ if (oldScrollbars) ++ winUtils.removeSheet(oldScrollbars, winUtils.AGENT_SHEET); ++ if (this._customScrollbars) ++ winUtils.loadSheet(this._customScrollbars, winUtils.AGENT_SHEET); ++ } ++ // Update state for all *existing* docShells. ++ this._contentViewerScrollBars.clear(); ++ for (const contentViewer of contentViewers) ++ this._contentViewerScrollBars.set(contentViewer, this._customScrollbars); ++ } ++ ++ dispose() { ++ this._setCustomScrollbars(null); ++ helper.removeListeners(this._eventListeners); ++ } ++} ++ ++var EXPORTED_SYMBOLS = ['ScrollbarManager']; ++this.ScrollbarManager = ScrollbarManager; ++ +diff --git a/testing/juggler/content/floating-scrollbars.css b/testing/juggler/content/floating-scrollbars.css +new file mode 100644 +index 000000000000..7709bdd34c65 +--- /dev/null ++++ b/testing/juggler/content/floating-scrollbars.css +@@ -0,0 +1,47 @@ ++@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); ++@namespace html url("http://www.w3.org/1999/xhtml"); ++ ++/* Restrict all styles to `*|*:not(html|select) > scrollbar` so that scrollbars ++ inside a . */ ++*|*:not(html|select) > scrollbar { ++ -moz-appearance: none !important; ++ position: relative; ++ background-color: transparent; ++ background-image: none; ++ z-index: 2147483647; ++ padding: 2px; ++ border: none; ++} ++ ++/* Scrollbar code will reset the margin to the correct side depending on ++ where layout actually puts the scrollbar */ ++*|*:not(html|select) > scrollbar[orient="vertical"] { ++ margin-left: -10px; ++ min-width: 10px; ++ max-width: 10px; ++} ++ ++*|*:not(html|select) > scrollbar[orient="horizontal"] { ++ margin-top: -10px; ++ min-height: 10px; ++ max-height: 10px; ++} ++ ++*|*:not(html|select) > scrollbar slider { ++ -moz-appearance: none !important; ++} ++ ++*|*:not(html|select) > scrollbar thumb { ++ -moz-appearance: none !important; ++ background-color: rgba(0,0,0,0.2); ++ border-width: 0px !important; ++ border-radius: 3px !important; ++} ++ ++*|*:not(html|select) > scrollbar scrollbarbutton, ++*|*:not(html|select) > scrollbar gripper { ++ display: none; ++} +diff --git a/testing/juggler/content/hidden-scrollbars.css b/testing/juggler/content/hidden-scrollbars.css +new file mode 100644 +index 000000000000..3a386425d379 +--- /dev/null ++++ b/testing/juggler/content/hidden-scrollbars.css +@@ -0,0 +1,13 @@ ++@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); ++@namespace html url("http://www.w3.org/1999/xhtml"); ++ ++/* Restrict all styles to `*|*:not(html|select) > scrollbar` so that scrollbars ++ inside a . */ ++*|*:not(html|select) > scrollbar { ++ -moz-appearance: none !important; ++ display: none; ++} ++ +diff --git a/testing/juggler/content/main.js b/testing/juggler/content/main.js +new file mode 100644 +index 000000000000..8585092e04e7 +--- /dev/null ++++ b/testing/juggler/content/main.js +@@ -0,0 +1,39 @@ ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const {ContentSession} = ChromeUtils.import('chrome://juggler/content/content/ContentSession.js'); ++const {FrameTree} = ChromeUtils.import('chrome://juggler/content/content/FrameTree.js'); ++const {NetworkMonitor} = ChromeUtils.import('chrome://juggler/content/content/NetworkMonitor.js'); ++const {ScrollbarManager} = ChromeUtils.import('chrome://juggler/content/content/ScrollbarManager.js'); ++ ++const sessions = new Map(); ++const frameTree = new FrameTree(docShell); ++const networkMonitor = new NetworkMonitor(docShell, frameTree); ++const scrollbarManager = new ScrollbarManager(docShell); ++ ++const helper = new Helper(); ++ ++const gListeners = [ ++ helper.addMessageListener(this, 'juggler:create-content-session', msg => { ++ const sessionId = msg.data; ++ sessions.set(sessionId, new ContentSession(sessionId, this, frameTree, scrollbarManager, networkMonitor)); ++ }), ++ ++ helper.addMessageListener(this, 'juggler:dispose-content-session', msg => { ++ const sessionId = msg.data; ++ const session = sessions.get(sessionId); ++ if (!session) ++ return; ++ sessions.delete(sessionId); ++ session.dispose(); ++ }), ++ ++ helper.addEventListener(this, 'unload', msg => { ++ helper.removeListeners(gListeners); ++ for (const session of sessions.values()) ++ session.dispose(); ++ sessions.clear(); ++ scrollbarManager.dispose(); ++ networkMonitor.dispose(); ++ frameTree.dispose(); ++ }), ++]; ++ +diff --git a/testing/juggler/jar.mn b/testing/juggler/jar.mn +new file mode 100644 +index 000000000000..27f5a15fd7f1 +--- /dev/null ++++ b/testing/juggler/jar.mn +@@ -0,0 +1,29 @@ ++# This Source Code Form is subject to the terms of the Mozilla Public ++# License, v. 2.0. If a copy of the MPL was not distributed with this ++# file, You can obtain one at http://mozilla.org/MPL/2.0/. ++ ++juggler.jar: ++% content juggler %content/ ++ content/Helper.js (Helper.js) ++ content/NetworkObserver.js (NetworkObserver.js) ++ content/BrowserContextManager.js (BrowserContextManager.js) ++ content/TargetRegistry.js (TargetRegistry.js) ++ content/protocol/PrimitiveTypes.js (protocol/PrimitiveTypes.js) ++ content/protocol/Protocol.js (protocol/Protocol.js) ++ content/protocol/Dispatcher.js (protocol/Dispatcher.js) ++ content/protocol/PageHandler.js (protocol/PageHandler.js) ++ content/protocol/RuntimeHandler.js (protocol/RuntimeHandler.js) ++ content/protocol/NetworkHandler.js (protocol/NetworkHandler.js) ++ content/protocol/BrowserHandler.js (protocol/BrowserHandler.js) ++ content/protocol/TargetHandler.js (protocol/TargetHandler.js) ++ content/protocol/AccessibilityHandler.js (protocol/AccessibilityHandler.js) ++ content/content/main.js (content/main.js) ++ content/content/ContentSession.js (content/ContentSession.js) ++ content/content/FrameTree.js (content/FrameTree.js) ++ content/content/NetworkMonitor.js (content/NetworkMonitor.js) ++ content/content/PageAgent.js (content/PageAgent.js) ++ content/content/RuntimeAgent.js (content/RuntimeAgent.js) ++ content/content/ScrollbarManager.js (content/ScrollbarManager.js) ++ content/content/floating-scrollbars.css (content/floating-scrollbars.css) ++ content/content/hidden-scrollbars.css (content/hidden-scrollbars.css) ++ +diff --git a/testing/juggler/moz.build b/testing/juggler/moz.build +new file mode 100644 +index 000000000000..1a0a3130bf95 +--- /dev/null ++++ b/testing/juggler/moz.build +@@ -0,0 +1,15 @@ ++# This Source Code Form is subject to the terms of the Mozilla Public ++# License, v. 2.0. If a copy of the MPL was not distributed with this ++# file, You can obtain one at http://mozilla.org/MPL/2.0/. ++ ++DIRS += ["components"] ++ ++JAR_MANIFESTS += ["jar.mn"] ++#JS_PREFERENCE_FILES += ["prefs/marionette.js"] ++ ++#MARIONETTE_UNIT_MANIFESTS += ["harness/marionette_harness/tests/unit/unit-tests.ini"] ++#XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"] ++ ++with Files("**"): ++ BUG_COMPONENT = ("Testing", "Juggler") ++ +diff --git a/testing/juggler/protocol/AccessibilityHandler.js b/testing/juggler/protocol/AccessibilityHandler.js +new file mode 100644 +index 000000000000..fc8a7397e50a +--- /dev/null ++++ b/testing/juggler/protocol/AccessibilityHandler.js +@@ -0,0 +1,15 @@ ++class AccessibilityHandler { ++ constructor(chromeSession, contentSession) { ++ this._chromeSession = chromeSession; ++ this._contentSession = contentSession; ++ } ++ ++ async getFullAXTree() { ++ return await this._contentSession.send('Page.getFullAXTree'); ++ } ++ ++ dispose() { } ++} ++ ++var EXPORTED_SYMBOLS = ['AccessibilityHandler']; ++this.AccessibilityHandler = AccessibilityHandler; +diff --git a/testing/juggler/protocol/BrowserHandler.js b/testing/juggler/protocol/BrowserHandler.js +new file mode 100644 +index 000000000000..e16d1c5c5798 +--- /dev/null ++++ b/testing/juggler/protocol/BrowserHandler.js +@@ -0,0 +1,66 @@ ++"use strict"; ++ ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++const { allowAllCerts } = ChromeUtils.import( ++ "chrome://marionette/content/cert.js" ++); ++const {BrowserContextManager} = ChromeUtils.import("chrome://juggler/content/BrowserContextManager.js"); ++ ++class BrowserHandler { ++ /** ++ * @param {ChromeSession} session ++ */ ++ constructor() { ++ this._sweepingOverride = null; ++ this._contextManager = BrowserContextManager.instance(); ++ } ++ ++ async close() { ++ Services.startup.quit(Ci.nsIAppStartup.eForceQuit); ++ } ++ ++ async setIgnoreHTTPSErrors({enabled}) { ++ if (!enabled) { ++ allowAllCerts.disable() ++ Services.prefs.setBoolPref('security.mixed_content.block_active_content', true); ++ } else { ++ allowAllCerts.enable() ++ Services.prefs.setBoolPref('security.mixed_content.block_active_content', false); ++ } ++ } ++ ++ grantPermissions({browserContextId, origin, permissions}) { ++ this._contextManager.grantPermissions(browserContextId, origin, permissions); ++ } ++ ++ resetPermissions({browserContextId}) { ++ this._contextManager.resetPermissions(browserContextId); ++ } ++ ++ setCookies({browserContextId, cookies}) { ++ this._contextManager.setCookies(browserContextId, cookies); ++ } ++ ++ deleteCookies({browserContextId, cookies}) { ++ this._contextManager.deleteCookies(browserContextId, cookies); ++ } ++ ++ getCookies({browserContextId, urls}) { ++ return {cookies: this._contextManager.getCookies(browserContextId, urls)}; ++ } ++ ++ async getInfo() { ++ const version = Components.classes["@mozilla.org/xre/app-info;1"] ++ .getService(Components.interfaces.nsIXULAppInfo) ++ .version; ++ const userAgent = Components.classes["@mozilla.org/network/protocol;1?name=http"] ++ .getService(Components.interfaces.nsIHttpProtocolHandler) ++ .userAgent; ++ return {version: 'Firefox/' + version, userAgent}; ++ } ++ ++ dispose() { } ++} ++ ++var EXPORTED_SYMBOLS = ['BrowserHandler']; ++this.BrowserHandler = BrowserHandler; +diff --git a/testing/juggler/protocol/Dispatcher.js b/testing/juggler/protocol/Dispatcher.js +new file mode 100644 +index 000000000000..7b3a6fa4fe7a +--- /dev/null ++++ b/testing/juggler/protocol/Dispatcher.js +@@ -0,0 +1,255 @@ ++const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js"); ++const {protocol, checkScheme} = ChromeUtils.import("chrome://juggler/content/protocol/Protocol.js"); ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const helper = new Helper(); ++ ++const PROTOCOL_HANDLERS = { ++ Page: ChromeUtils.import("chrome://juggler/content/protocol/PageHandler.js").PageHandler, ++ Network: ChromeUtils.import("chrome://juggler/content/protocol/NetworkHandler.js").NetworkHandler, ++ Browser: ChromeUtils.import("chrome://juggler/content/protocol/BrowserHandler.js").BrowserHandler, ++ Target: ChromeUtils.import("chrome://juggler/content/protocol/TargetHandler.js").TargetHandler, ++ Runtime: ChromeUtils.import("chrome://juggler/content/protocol/RuntimeHandler.js").RuntimeHandler, ++ Accessibility: ChromeUtils.import("chrome://juggler/content/protocol/AccessibilityHandler.js").AccessibilityHandler, ++}; ++ ++class Dispatcher { ++ /** ++ * @param {Connection} connection ++ */ ++ constructor(connection) { ++ this._connection = connection; ++ this._connection.onmessage = this._dispatch.bind(this); ++ this._connection.onclose = this._dispose.bind(this); ++ ++ this._targetSessions = new Map(); ++ this._sessions = new Map(); ++ this._rootSession = new ChromeSession(this, undefined, null /* contentSession */, TargetRegistry.instance().browserTargetInfo()); ++ ++ this._eventListeners = [ ++ helper.on(TargetRegistry.instance(), TargetRegistry.Events.TargetDestroyed, this._onTargetDestroyed.bind(this)), ++ ]; ++ } ++ ++ async createSession(targetId) { ++ const targetInfo = TargetRegistry.instance().targetInfo(targetId); ++ if (!targetInfo) ++ throw new Error(`Target "${targetId}" is not found`); ++ let targetSessions = this._targetSessions.get(targetId); ++ if (!targetSessions) { ++ targetSessions = new Map(); ++ this._targetSessions.set(targetId, targetSessions); ++ } ++ ++ const sessionId = helper.generateId(); ++ const contentSession = targetInfo.type === 'page' ? new ContentSession(this, sessionId, targetInfo) : null; ++ const chromeSession = new ChromeSession(this, sessionId, contentSession, targetInfo); ++ targetSessions.set(sessionId, chromeSession); ++ this._sessions.set(sessionId, chromeSession); ++ this._emitEvent(this._rootSession._sessionId, 'Target.attachedToTarget', { ++ sessionId: sessionId, ++ targetInfo ++ }); ++ return sessionId; ++ } ++ ++ _dispose() { ++ helper.removeListeners(this._eventListeners); ++ this._connection.onmessage = null; ++ this._connection.onclose = null; ++ this._rootSession.dispose(); ++ this._rootSession = null; ++ for (const session of this._sessions.values()) ++ session.dispose(); ++ this._sessions.clear(); ++ this._targetSessions.clear(); ++ } ++ ++ _onTargetDestroyed({targetId}) { ++ const sessions = this._targetSessions.get(targetId); ++ if (!sessions) ++ return; ++ this._targetSessions.delete(targetId); ++ for (const [sessionId, session] of sessions) { ++ session.dispose(); ++ this._sessions.delete(sessionId); ++ } ++ } ++ ++ async _dispatch(event) { ++ const data = JSON.parse(event.data); ++ const id = data.id; ++ const sessionId = data.sessionId; ++ delete data.sessionId; ++ try { ++ const session = sessionId ? this._sessions.get(sessionId) : this._rootSession; ++ if (!session) ++ throw new Error(`ERROR: cannot find session with id "${sessionId}"`); ++ const method = data.method; ++ const params = data.params || {}; ++ if (!id) ++ throw new Error(`ERROR: every message must have an 'id' parameter`); ++ if (!method) ++ throw new Error(`ERROR: every message must have a 'method' parameter`); ++ ++ const [domain, methodName] = method.split('.'); ++ const descriptor = protocol.domains[domain] ? protocol.domains[domain].methods[methodName] : null; ++ if (!descriptor) ++ throw new Error(`ERROR: method '${method}' is not supported`); ++ let details = {}; ++ if (!checkScheme(descriptor.params || {}, params, details)) ++ throw new Error(`ERROR: failed to call method '${method}' with parameters ${JSON.stringify(params, null, 2)}\n${details.error}`); ++ ++ const result = await session.dispatch(method, params); ++ ++ details = {}; ++ if ((descriptor.returns || result) && !checkScheme(descriptor.returns, result, details)) ++ throw new Error(`ERROR: failed to dispatch method '${method}' result ${JSON.stringify(result, null, 2)}\n${details.error}`); ++ ++ this._connection.send(JSON.stringify({id, sessionId, result})); ++ } catch (e) { ++ this._connection.send(JSON.stringify({id, sessionId, error: { ++ message: e.message, ++ data: e.stack ++ }})); ++ } ++ } ++ ++ _emitEvent(sessionId, eventName, params) { ++ const [domain, eName] = eventName.split('.'); ++ const scheme = protocol.domains[domain] ? protocol.domains[domain].events[eName] : null; ++ if (!scheme) ++ throw new Error(`ERROR: event '${eventName}' is not supported`); ++ const details = {}; ++ if (!checkScheme(scheme, params || {}, details)) ++ throw new Error(`ERROR: failed to emit event '${eventName}' ${JSON.stringify(params, null, 2)}\n${details.error}`); ++ this._connection.send(JSON.stringify({method: eventName, params, sessionId})); ++ } ++} ++ ++class ChromeSession { ++ /** ++ * @param {Connection} connection ++ */ ++ constructor(dispatcher, sessionId, contentSession, targetInfo) { ++ this._dispatcher = dispatcher; ++ this._sessionId = sessionId; ++ this._contentSession = contentSession; ++ this._targetInfo = targetInfo; ++ ++ this._handlers = {}; ++ for (const [domainName, handlerFactory] of Object.entries(PROTOCOL_HANDLERS)) { ++ if (protocol.domains[domainName].targets.includes(targetInfo.type)) ++ this._handlers[domainName] = new handlerFactory(this, contentSession); ++ } ++ } ++ ++ dispatcher() { ++ return this._dispatcher; ++ } ++ ++ targetId() { ++ return this._targetInfo.targetId; ++ } ++ ++ dispose() { ++ if (this._contentSession) ++ this._contentSession.dispose(); ++ this._contentSession = null; ++ for (const [domainName, handler] of Object.entries(this._handlers)) { ++ if (!handler.dispose) ++ throw new Error(`Handler for "${domainName}" domain does not define |dispose| method!`); ++ handler.dispose(); ++ delete this._handlers[domainName]; ++ } ++ // Root session don't have sessionId and don't emit detachedFromTarget. ++ if (this._sessionId) { ++ this._dispatcher._emitEvent(this._sessionId, 'Target.detachedFromTarget', { ++ sessionId: this._sessionId, ++ }); ++ } ++ } ++ ++ emitEvent(eventName, params) { ++ this._dispatcher._emitEvent(this._sessionId, eventName, params); ++ } ++ ++ async dispatch(method, params) { ++ const [domainName, methodName] = method.split('.'); ++ if (!this._handlers[domainName]) ++ throw new Error(`Domain "${domainName}" does not exist`); ++ if (!this._handlers[domainName][methodName]) ++ throw new Error(`Handler for domain "${domainName}" does not implement method "${methodName}"`); ++ return await this._handlers[domainName][methodName](params); ++ } ++} ++ ++class ContentSession { ++ constructor(dispatcher, sessionId, targetInfo) { ++ this._dispatcher = dispatcher; ++ const tab = TargetRegistry.instance().tabForTarget(targetInfo.targetId); ++ this._browser = tab.linkedBrowser; ++ this._messageId = 0; ++ this._pendingMessages = new Map(); ++ this._sessionId = sessionId; ++ this._browser.messageManager.sendAsyncMessage('juggler:create-content-session', this._sessionId); ++ this._disposed = false; ++ this._eventListeners = [ ++ helper.addMessageListener(this._browser.messageManager, this._sessionId, { ++ receiveMessage: message => this._onMessage(message) ++ }), ++ ]; ++ } ++ ++ isDisposed() { ++ return this._disposed; ++ } ++ ++ dispose() { ++ if (this._disposed) ++ return; ++ this._disposed = true; ++ helper.removeListeners(this._eventListeners); ++ for (const {resolve, reject, methodName} of this._pendingMessages.values()) ++ reject(new Error(`Failed "${methodName}": Page closed.`)); ++ this._pendingMessages.clear(); ++ if (this._browser.messageManager) ++ this._browser.messageManager.sendAsyncMessage('juggler:dispose-content-session', this._sessionId); ++ } ++ ++ /** ++ * @param {string} methodName ++ * @param {*} params ++ * @return {!Promise<*>} ++ */ ++ send(methodName, params) { ++ const id = ++this._messageId; ++ const promise = new Promise((resolve, reject) => { ++ this._pendingMessages.set(id, {resolve, reject, methodName}); ++ }); ++ this._browser.messageManager.sendAsyncMessage(this._sessionId, {id, methodName, params}); ++ return promise; ++ } ++ ++ _onMessage({data}) { ++ if (data.id) { ++ let id = data.id; ++ const {resolve, reject} = this._pendingMessages.get(data.id); ++ this._pendingMessages.delete(data.id); ++ if (data.error) ++ reject(new Error(data.error)); ++ else ++ resolve(data.result); ++ } else { ++ const { ++ eventName, ++ params = {} ++ } = data; ++ this._dispatcher._emitEvent(this._sessionId, eventName, params); ++ } ++ } ++} ++ ++ ++this.EXPORTED_SYMBOLS = ['Dispatcher']; ++this.Dispatcher = Dispatcher; ++ +diff --git a/testing/juggler/protocol/NetworkHandler.js b/testing/juggler/protocol/NetworkHandler.js +new file mode 100644 +index 000000000000..f5e7e919594b +--- /dev/null ++++ b/testing/juggler/protocol/NetworkHandler.js +@@ -0,0 +1,154 @@ ++"use strict"; ++ ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const {NetworkObserver} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.js'); ++const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js"); ++ ++const Cc = Components.classes; ++const Ci = Components.interfaces; ++const Cu = Components.utils; ++const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; ++const FRAME_SCRIPT = "chrome://juggler/content/content/ContentSession.js"; ++const helper = new Helper(); ++ ++class NetworkHandler { ++ constructor(chromeSession, contentSession) { ++ this._chromeSession = chromeSession; ++ this._contentSession = contentSession; ++ this._networkObserver = NetworkObserver.instance(); ++ this._httpActivity = new Map(); ++ this._enabled = false; ++ this._browser = TargetRegistry.instance().tabForTarget(this._chromeSession.targetId()).linkedBrowser; ++ this._requestInterception = false; ++ this._eventListeners = []; ++ this._pendingRequstWillBeSentEvents = new Set(); ++ } ++ ++ async enable() { ++ if (this._enabled) ++ return; ++ this._enabled = true; ++ this._eventListeners = [ ++ helper.on(this._networkObserver, 'request', this._onRequest.bind(this)), ++ helper.on(this._networkObserver, 'response', this._onResponse.bind(this)), ++ helper.on(this._networkObserver, 'requestfinished', this._onRequestFinished.bind(this)), ++ helper.on(this._networkObserver, 'requestfailed', this._onRequestFailed.bind(this)), ++ this._networkObserver.startTrackingBrowserNetwork(this._browser), ++ ]; ++ } ++ ++ async getResponseBody({requestId}) { ++ return this._networkObserver.getResponseBody(this._browser, requestId); ++ } ++ ++ async setExtraHTTPHeaders({headers}) { ++ this._networkObserver.setExtraHTTPHeaders(this._browser, headers); ++ } ++ ++ async setRequestInterception({enabled}) { ++ if (enabled) ++ this._networkObserver.enableRequestInterception(this._browser); ++ else ++ this._networkObserver.disableRequestInterception(this._browser); ++ // Right after we enable/disable request interception we need to await all pending ++ // requestWillBeSent events before successfully returning from the method. ++ await Promise.all(Array.from(this._pendingRequstWillBeSentEvents)); ++ } ++ ++ async resumeSuspendedRequest({requestId, headers}) { ++ this._networkObserver.resumeSuspendedRequest(this._browser, requestId, headers); ++ } ++ ++ async abortSuspendedRequest({requestId}) { ++ this._networkObserver.abortSuspendedRequest(this._browser, requestId); ++ } ++ ++ dispose() { ++ helper.removeListeners(this._eventListeners); ++ } ++ ++ _ensureHTTPActivity(requestId) { ++ let activity = this._httpActivity.get(requestId); ++ if (!activity) { ++ activity = { ++ _id: requestId, ++ _lastSentEvent: null, ++ request: null, ++ response: null, ++ complete: null, ++ failed: null, ++ }; ++ this._httpActivity.set(requestId, activity); ++ } ++ return activity; ++ } ++ ++ _reportHTTPAcitivityEvents(activity) { ++ // State machine - sending network events. ++ if (!activity._lastSentEvent && activity.request) { ++ this._chromeSession.emitEvent('Network.requestWillBeSent', activity.request); ++ activity._lastSentEvent = 'requestWillBeSent'; ++ } ++ if (activity._lastSentEvent === 'requestWillBeSent' && activity.response) { ++ this._chromeSession.emitEvent('Network.responseReceived', activity.response); ++ activity._lastSentEvent = 'responseReceived'; ++ } ++ if (activity._lastSentEvent === 'responseReceived' && activity.complete) { ++ this._chromeSession.emitEvent('Network.requestFinished', activity.complete); ++ activity._lastSentEvent = 'requestFinished'; ++ } ++ if (activity._lastSentEvent && activity.failed) { ++ this._chromeSession.emitEvent('Network.requestFailed', activity.failed); ++ activity._lastSentEvent = 'requestFailed'; ++ } ++ ++ // Clean up if request lifecycle is over. ++ if (activity._lastSentEvent === 'requestFinished' || activity._lastSentEvent === 'requestFailed') ++ this._httpActivity.delete(activity._id); ++ } ++ ++ async _onRequest(httpChannel, eventDetails) { ++ let pendingRequestCallback; ++ let pendingRequestPromise = new Promise(x => pendingRequestCallback = x); ++ this._pendingRequstWillBeSentEvents.add(pendingRequestPromise); ++ let details = null; ++ try { ++ details = await this._contentSession.send('Page.requestDetails', {channelId: httpChannel.channelId}); ++ } catch (e) { ++ if (this._contentSession.isDisposed()) { ++ pendingRequestCallback(); ++ this._pendingRequstWillBeSentEvents.delete(pendingRequestPromise); ++ return; ++ } ++ } ++ const activity = this._ensureHTTPActivity(eventDetails.requestId); ++ activity.request = { ++ frameId: details ? details.frameId : undefined, ++ ...eventDetails, ++ }; ++ this._reportHTTPAcitivityEvents(activity); ++ pendingRequestCallback(); ++ this._pendingRequstWillBeSentEvents.delete(pendingRequestPromise); ++ } ++ ++ async _onResponse(httpChannel, eventDetails) { ++ const activity = this._ensureHTTPActivity(eventDetails.requestId); ++ activity.response = eventDetails; ++ this._reportHTTPAcitivityEvents(activity); ++ } ++ ++ async _onRequestFinished(httpChannel, eventDetails) { ++ const activity = this._ensureHTTPActivity(eventDetails.requestId); ++ activity.complete = eventDetails; ++ this._reportHTTPAcitivityEvents(activity); ++ } ++ ++ async _onRequestFailed(httpChannel, eventDetails) { ++ const activity = this._ensureHTTPActivity(eventDetails.requestId); ++ activity.failed = eventDetails; ++ this._reportHTTPAcitivityEvents(activity); ++ } ++} ++ ++var EXPORTED_SYMBOLS = ['NetworkHandler']; ++this.NetworkHandler = NetworkHandler; +diff --git a/testing/juggler/protocol/PageHandler.js b/testing/juggler/protocol/PageHandler.js +new file mode 100644 +index 000000000000..32fb7e9d928a +--- /dev/null ++++ b/testing/juggler/protocol/PageHandler.js +@@ -0,0 +1,269 @@ ++"use strict"; ++ ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js"); ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++ ++const Cc = Components.classes; ++const Ci = Components.interfaces; ++const Cu = Components.utils; ++const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; ++const FRAME_SCRIPT = "chrome://juggler/content/content/ContentSession.js"; ++const helper = new Helper(); ++ ++class PageHandler { ++ constructor(chromeSession, contentSession) { ++ this._chromeSession = chromeSession; ++ this._contentSession = contentSession; ++ this._browser = TargetRegistry.instance().tabForTarget(chromeSession.targetId()).linkedBrowser; ++ this._dialogs = new Map(); ++ ++ this._eventListeners = []; ++ this._enabled = false; ++ } ++ ++ async close({runBeforeUnload}) { ++ // Postpone target close to deliver response in session. ++ Services.tm.dispatchToMainThread(() => { ++ TargetRegistry.instance().closePage(this._chromeSession.targetId(), runBeforeUnload); ++ }); ++ } ++ ++ async enable() { ++ if (this._enabled) ++ return; ++ this._enabled = true; ++ this._updateModalDialogs(); ++ ++ this._eventListeners = [ ++ helper.addEventListener(this._browser, 'DOMWillOpenModalDialog', async (event) => { ++ // wait for the dialog to be actually added to DOM. ++ await Promise.resolve(); ++ this._updateModalDialogs(); ++ }), ++ helper.addEventListener(this._browser, 'DOMModalDialogClosed', event => this._updateModalDialogs()), ++ ]; ++ await this._contentSession.send('Page.enable'); ++ } ++ ++ dispose() { ++ helper.removeListeners(this._eventListeners); ++ } ++ ++ async setViewport({viewport}) { ++ if (viewport) { ++ const {width, height} = viewport; ++ this._browser.style.setProperty('min-width', width + 'px'); ++ this._browser.style.setProperty('min-height', height + 'px'); ++ this._browser.style.setProperty('max-width', width + 'px'); ++ this._browser.style.setProperty('max-height', height + 'px'); ++ } else { ++ this._browser.style.removeProperty('min-width'); ++ this._browser.style.removeProperty('min-height'); ++ this._browser.style.removeProperty('max-width'); ++ this._browser.style.removeProperty('max-height'); ++ } ++ const dimensions = this._browser.getBoundingClientRect(); ++ await Promise.all([ ++ this._contentSession.send('Page.setViewport', { ++ deviceScaleFactor: viewport ? viewport.deviceScaleFactor : 0, ++ isMobile: viewport && viewport.isMobile, ++ hasTouch: viewport && viewport.hasTouch, ++ }), ++ this._contentSession.send('Page.awaitViewportDimensions', { ++ width: dimensions.width, ++ height: dimensions.height ++ }), ++ ]); ++ } ++ ++ _updateModalDialogs() { ++ const prompts = new Set(this._browser.tabModalPromptBox ? this._browser.tabModalPromptBox.listPrompts() : []); ++ for (const dialog of this._dialogs.values()) { ++ if (!prompts.has(dialog.prompt())) { ++ this._dialogs.delete(dialog.id()); ++ this._chromeSession.emitEvent('Page.dialogClosed', { ++ dialogId: dialog.id(), ++ }); ++ } else { ++ prompts.delete(dialog.prompt()); ++ } ++ } ++ for (const prompt of prompts) { ++ const dialog = Dialog.createIfSupported(prompt); ++ if (!dialog) ++ continue; ++ this._dialogs.set(dialog.id(), dialog); ++ this._chromeSession.emitEvent('Page.dialogOpened', { ++ dialogId: dialog.id(), ++ type: dialog.type(), ++ message: dialog.message(), ++ defaultValue: dialog.defaultValue(), ++ }); ++ } ++ } ++ ++ async setUserAgent(options) { ++ return await this._contentSession.send('Page.setUserAgent', options); ++ } ++ ++ async setFileInputFiles(options) { ++ return await this._contentSession.send('Page.setFileInputFiles', options); ++ } ++ ++ async setEmulatedMedia(options) { ++ return await this._contentSession.send('Page.setEmulatedMedia', options); ++ } ++ ++ async setJavascriptEnabled(options) { ++ return await this._contentSession.send('Page.setJavascriptEnabled', options); ++ } ++ ++ async setCacheDisabled(options) { ++ return await this._contentSession.send('Page.setCacheDisabled', options); ++ } ++ ++ async addBinding(options) { ++ return await this._contentSession.send('Page.addBinding', options); ++ } ++ ++ async screenshot(options) { ++ return await this._contentSession.send('Page.screenshot', options); ++ } ++ ++ async getBoundingBox(options) { ++ return await this._contentSession.send('Page.getBoundingBox', options); ++ } ++ ++ async getContentQuads(options) { ++ return await this._contentSession.send('Page.getContentQuads', options); ++ } ++ ++ /** ++ * @param {{frameId: string, url: string}} options ++ */ ++ async navigate(options) { ++ return await this._contentSession.send('Page.navigate', options); ++ } ++ ++ /** ++ * @param {{frameId: string, url: string}} options ++ */ ++ async goBack(options) { ++ return await this._contentSession.send('Page.goBack', options); ++ } ++ ++ /** ++ * @param {{frameId: string, url: string}} options ++ */ ++ async goForward(options) { ++ return await this._contentSession.send('Page.goForward', options); ++ } ++ ++ /** ++ * @param {{frameId: string, url: string}} options ++ */ ++ async reload(options) { ++ return await this._contentSession.send('Page.reload', options); ++ } ++ ++ /** ++ * @param {{frameId: String, objectId: String}} options ++ * @return {!Promise<*>} ++ */ ++ async contentFrame(options) { ++ return await this._contentSession.send('Page.contentFrame', options); ++ } ++ ++ async addScriptToEvaluateOnNewDocument(options) { ++ return await this._contentSession.send('Page.addScriptToEvaluateOnNewDocument', options); ++ } ++ ++ async removeScriptToEvaluateOnNewDocument(options) { ++ return await this._contentSession.send('Page.removeScriptToEvaluateOnNewDocument', options); ++ } ++ ++ async dispatchKeyEvent(options) { ++ return await this._contentSession.send('Page.dispatchKeyEvent', options); ++ } ++ ++ async dispatchTouchEvent(options) { ++ return await this._contentSession.send('Page.dispatchTouchEvent', options); ++ } ++ ++ async dispatchMouseEvent(options) { ++ return await this._contentSession.send('Page.dispatchMouseEvent', options); ++ } ++ ++ async insertText(options) { ++ return await this._contentSession.send('Page.insertText', options); ++ } ++ ++ async handleDialog({dialogId, accept, promptText}) { ++ const dialog = this._dialogs.get(dialogId); ++ if (!dialog) ++ throw new Error('Failed to find dialog with id = ' + dialogId); ++ if (accept) ++ dialog.accept(promptText); ++ else ++ dialog.dismiss(); ++ } ++} ++ ++class Dialog { ++ static createIfSupported(prompt) { ++ const type = prompt.args.promptType; ++ switch (type) { ++ case 'alert': ++ case 'prompt': ++ case 'confirm': ++ return new Dialog(prompt, type); ++ case 'confirmEx': ++ return new Dialog(prompt, 'beforeunload'); ++ default: ++ return null; ++ }; ++ } ++ ++ constructor(prompt, type) { ++ this._id = helper.generateId(); ++ this._type = type; ++ this._prompt = prompt; ++ } ++ ++ id() { ++ return this._id; ++ } ++ ++ message() { ++ return this._prompt.ui.infoBody.textContent; ++ } ++ ++ type() { ++ return this._type; ++ } ++ ++ prompt() { ++ return this._prompt; ++ } ++ ++ dismiss() { ++ if (this._prompt.ui.button1) ++ this._prompt.ui.button1.click(); ++ else ++ this._prompt.ui.button0.click(); ++ } ++ ++ defaultValue() { ++ return this._prompt.ui.loginTextbox.value; ++ } ++ ++ accept(promptValue) { ++ if (typeof promptValue === 'string' && this._type === 'prompt') ++ this._prompt.ui.loginTextbox.value = promptValue; ++ this._prompt.ui.button0.click(); ++ } ++} ++ ++var EXPORTED_SYMBOLS = ['PageHandler']; ++this.PageHandler = PageHandler; +diff --git a/testing/juggler/protocol/PrimitiveTypes.js b/testing/juggler/protocol/PrimitiveTypes.js +new file mode 100644 +index 000000000000..78b6601b91d0 +--- /dev/null ++++ b/testing/juggler/protocol/PrimitiveTypes.js +@@ -0,0 +1,143 @@ ++const t = {}; ++ ++t.String = function(x, details = {}, path = ['']) { ++ if (typeof x === 'string' || typeof x === 'String') ++ return true; ++ details.error = `Expected "${path.join('.')}" to be |string|; found |${typeof x}| \`${JSON.stringify(x)}\` instead.`; ++ return false; ++} ++ ++t.Number = function(x, details = {}, path = ['']) { ++ if (typeof x === 'number') ++ return true; ++ details.error = `Expected "${path.join('.')}" to be |number|; found |${typeof x}| \`${JSON.stringify(x)}\` instead.`; ++ return false; ++} ++ ++t.Boolean = function(x, details = {}, path = ['']) { ++ if (typeof x === 'boolean') ++ return true; ++ details.error = `Expected "${path.join('.')}" to be |boolean|; found |${typeof x}| \`${JSON.stringify(x)}\` instead.`; ++ return false; ++} ++ ++t.Null = function(x, details = {}, path = ['']) { ++ if (Object.is(x, null)) ++ return true; ++ details.error = `Expected "${path.join('.')}" to be \`null\`; found \`${JSON.stringify(x)}\` instead.`; ++ return false; ++} ++ ++t.Undefined = function(x, details = {}, path = ['']) { ++ if (Object.is(x, undefined)) ++ return true; ++ details.error = `Expected "${path.join('.')}" to be \`undefined\`; found \`${JSON.stringify(x)}\` instead.`; ++ return false; ++} ++ ++t.Any = x => true, ++ ++t.Enum = function(values) { ++ return function(x, details = {}, path = ['']) { ++ if (values.indexOf(x) !== -1) ++ return true; ++ details.error = `Expected "${path.join('.')}" to be one of [${values.join(', ')}]; found \`${JSON.stringify(x)}\` (${typeof x}) instead.`; ++ return false; ++ } ++} ++ ++t.Nullable = function(scheme) { ++ return function(x, details = {}, path = ['']) { ++ if (Object.is(x, null)) ++ return true; ++ return checkScheme(scheme, x, details, path); ++ } ++} ++ ++t.Optional = function(scheme) { ++ return function(x, details = {}, path = ['']) { ++ if (Object.is(x, undefined)) ++ return true; ++ return checkScheme(scheme, x, details, path); ++ } ++} ++ ++t.Array = function(scheme) { ++ return function(x, details = {}, path = ['']) { ++ if (!Array.isArray(x)) { ++ details.error = `Expected "${path.join('.')}" to be an array; found \`${JSON.stringify(x)}\` (${typeof x}) instead.`; ++ return false; ++ } ++ const lastPathElement = path[path.length - 1]; ++ for (let i = 0; i < x.length; ++i) { ++ path[path.length - 1] = lastPathElement + `[${i}]`; ++ if (!checkScheme(scheme, x[i], details, path)) ++ return false; ++ } ++ path[path.length - 1] = lastPathElement; ++ return true; ++ } ++} ++ ++t.Recursive = function(types, schemeName) { ++ return function(x, details = {}, path = ['']) { ++ const scheme = types[schemeName]; ++ return checkScheme(scheme, x, details, path); ++ } ++} ++ ++function beauty(path, obj) { ++ if (path.length === 1) ++ return `object ${JSON.stringify(obj, null, 2)}`; ++ return `property "${path.join('.')}" - ${JSON.stringify(obj, null, 2)}`; ++} ++ ++function checkScheme(scheme, x, details = {}, path = ['']) { ++ if (!scheme) ++ throw new Error(`ILLDEFINED SCHEME: ${path.join('.')}`); ++ if (typeof scheme === 'object') { ++ if (!x) { ++ details.error = `Object "${path.join('.')}" is undefined, but has some scheme`; ++ return false; ++ } ++ for (const [propertyName, aScheme] of Object.entries(scheme)) { ++ path.push(propertyName); ++ const result = checkScheme(aScheme, x[propertyName], details, path); ++ path.pop(); ++ if (!result) ++ return false; ++ } ++ for (const propertyName of Object.keys(x)) { ++ if (!scheme[propertyName]) { ++ path.push(propertyName); ++ details.error = `Found ${beauty(path, x[propertyName])} which is not described in this scheme`; ++ return false; ++ } ++ } ++ return true; ++ } ++ return scheme(x, details, path); ++} ++ ++/* ++ ++function test(scheme, obj) { ++ const details = {}; ++ if (!checkScheme(scheme, obj, details)) { ++ dump(`FAILED: ${JSON.stringify(obj)} ++ details.error: ${details.error} ++ `); ++ } else { ++ dump(`SUCCESS: ${JSON.stringify(obj)} ++`); ++ } ++} ++ ++test(t.Array(t.String), ['a', 'b', 2, 'c']); ++test(t.Either(t.String, t.Number), {}); ++ ++*/ ++ ++this.t = t; ++this.checkScheme = checkScheme; ++this.EXPORTED_SYMBOLS = ['t', 'checkScheme']; +diff --git a/testing/juggler/protocol/Protocol.js b/testing/juggler/protocol/Protocol.js +new file mode 100644 +index 000000000000..63186502775d +--- /dev/null ++++ b/testing/juggler/protocol/Protocol.js +@@ -0,0 +1,660 @@ ++const {t, checkScheme} = ChromeUtils.import('chrome://juggler/content/protocol/PrimitiveTypes.js'); ++ ++// Protocol-specific types. ++const types = {}; ++ ++types.TargetInfo = { ++ type: t.Enum(['page', 'browser']), ++ targetId: t.String, ++ browserContextId: t.Optional(t.String), ++ url: t.String, ++ // PageId of parent tab, if any. ++ openerId: t.Optional(t.String), ++}; ++ ++types.DOMPoint = { ++ x: t.Number, ++ y: t.Number, ++}; ++ ++types.DOMQuad = { ++ p1: types.DOMPoint, ++ p2: types.DOMPoint, ++ p3: types.DOMPoint, ++ p4: types.DOMPoint, ++}; ++ ++types.TouchPoint = { ++ x: t.Number, ++ y: t.Number, ++ radiusX: t.Optional(t.Number), ++ radiusY: t.Optional(t.Number), ++ rotationAngle: t.Optional(t.Number), ++ force: t.Optional(t.Number), ++}; ++ ++types.RemoteObject = { ++ type: t.Optional(t.Enum(['object', 'function', 'undefined', 'string', 'number', 'boolean', 'symbol', 'bigint'])), ++ subtype: t.Optional(t.Enum(['array', 'null', 'node', 'regexp', 'date', 'map', 'set', 'weakmap', 'weakset', 'error', 'proxy', 'promise', 'typedarray'])), ++ objectId: t.Optional(t.String), ++ unserializableValue: t.Optional(t.Enum(['Infinity', '-Infinity', '-0', 'NaN'])), ++ value: t.Any ++}; ++ ++types.AXTree = { ++ role: t.String, ++ name: t.String, ++ children: t.Optional(t.Array(t.Recursive(types, 'AXTree'))), ++ ++ selected: t.Optional(t.Boolean), ++ focused: t.Optional(t.Boolean), ++ pressed: t.Optional(t.Boolean), ++ focusable: t.Optional(t.Boolean), ++ haspopup: t.Optional(t.Boolean), ++ required: t.Optional(t.Boolean), ++ invalid: t.Optional(t.Boolean), ++ modal: t.Optional(t.Boolean), ++ editable: t.Optional(t.Boolean), ++ busy: t.Optional(t.Boolean), ++ multiline: t.Optional(t.Boolean), ++ readonly: t.Optional(t.Boolean), ++ checked: t.Optional(t.Enum(['mixed', true])), ++ expanded: t.Optional(t.Boolean), ++ disabled: t.Optional(t.Boolean), ++ multiselectable: t.Optional(t.Boolean), ++ ++ value: t.Optional(t.String), ++ description: t.Optional(t.String), ++ ++ value: t.Optional(t.String), ++ roledescription: t.Optional(t.String), ++ valuetext: t.Optional(t.String), ++ orientation: t.Optional(t.String), ++ autocomplete: t.Optional(t.String), ++ keyshortcuts: t.Optional(t.String), ++ ++ level: t.Optional(t.Number), ++ ++ tag: t.Optional(t.String), ++} ++ ++const Browser = { ++ targets: ['browser'], ++ ++ events: {}, ++ ++ methods: { ++ 'close': {}, ++ 'getInfo': { ++ returns: { ++ userAgent: t.String, ++ version: t.String, ++ }, ++ }, ++ 'setIgnoreHTTPSErrors': { ++ params: { ++ enabled: t.Boolean, ++ }, ++ }, ++ 'grantPermissions': { ++ params: { ++ origin: t.String, ++ browserContextId: t.Optional(t.String), ++ permissions: t.Array(t.Enum([ ++ 'geo', 'microphone', 'camera', 'desktop-notifications' ++ ])), ++ }, ++ }, ++ 'resetPermissions': { ++ params: { ++ browserContextId: t.Optional(t.String), ++ } ++ }, ++ 'setCookies': { ++ params: { ++ browserContextId: t.Optional(t.String), ++ cookies: t.Array({ ++ name: t.String, ++ value: t.String, ++ url: t.Optional(t.String), ++ domain: t.Optional(t.String), ++ path: t.Optional(t.String), ++ secure: t.Optional(t.Boolean), ++ httpOnly: t.Optional(t.Boolean), ++ sameSite: t.Optional(t.Enum(['Strict', 'Lax'])), ++ expires: t.Optional(t.Number), ++ }), ++ } ++ }, ++ 'deleteCookies': { ++ params: { ++ browserContextId: t.Optional(t.String), ++ cookies: t.Array({ ++ name: t.String, ++ domain: t.Optional(t.String), ++ path: t.Optional(t.String), ++ url: t.Optional(t.String), ++ }), ++ } ++ }, ++ 'getCookies': { ++ params: { ++ browserContextId: t.Optional(t.String), ++ urls: t.Array(t.String), ++ }, ++ returns: { ++ cookies: t.Array({ ++ name: t.String, ++ domain: t.String, ++ path: t.String, ++ value: t.String, ++ expires: t.Number, ++ size: t.Number, ++ httpOnly: t.Boolean, ++ secure: t.Boolean, ++ session: t.Boolean, ++ sameSite: t.Optional(t.Enum(['Strict', 'Lax'])), ++ }), ++ }, ++ }, ++ }, ++}; ++ ++const Target = { ++ targets: ['browser'], ++ ++ events: { ++ 'attachedToTarget': { ++ sessionId: t.String, ++ targetInfo: types.TargetInfo, ++ }, ++ 'detachedFromTarget': { ++ sessionId: t.String, ++ }, ++ 'targetCreated': types.TargetInfo, ++ 'targetDestroyed': types.TargetInfo, ++ 'targetInfoChanged': types.TargetInfo, ++ }, ++ ++ methods: { ++ // Start emitting tagOpened/tabClosed events ++ 'enable': {}, ++ 'attachToTarget': { ++ params: { ++ targetId: t.String, ++ }, ++ returns: { ++ sessionId: t.String, ++ }, ++ }, ++ 'newPage': { ++ params: { ++ browserContextId: t.Optional(t.String), ++ }, ++ returns: { ++ targetId: t.String, ++ } ++ }, ++ 'createBrowserContext': { ++ returns: { ++ browserContextId: t.String, ++ }, ++ }, ++ 'removeBrowserContext': { ++ params: { ++ browserContextId: t.String, ++ }, ++ }, ++ 'getBrowserContexts': { ++ returns: { ++ browserContextIds: t.Array(t.String), ++ }, ++ }, ++ }, ++}; ++ ++const Network = { ++ targets: ['page'], ++ events: { ++ 'requestWillBeSent': { ++ // frameId may be absent for redirected requests. ++ frameId: t.Optional(t.String), ++ requestId: t.String, ++ // RequestID of redirected request. ++ redirectedFrom: t.Optional(t.String), ++ postData: t.Optional(t.String), ++ headers: t.Array({ ++ name: t.String, ++ value: t.String, ++ }), ++ suspended: t.Optional(t.Boolean), ++ url: t.String, ++ method: t.String, ++ isNavigationRequest: t.Boolean, ++ cause: t.String, ++ }, ++ 'responseReceived': { ++ securityDetails: t.Nullable({ ++ protocol: t.String, ++ subjectName: t.String, ++ issuer: t.String, ++ validFrom: t.Number, ++ validTo: t.Number, ++ }), ++ requestId: t.String, ++ fromCache: t.Boolean, ++ remoteIPAddress: t.Optional(t.String), ++ remotePort: t.Optional(t.Number), ++ status: t.Number, ++ statusText: t.String, ++ headers: t.Array({ ++ name: t.String, ++ value: t.String, ++ }), ++ }, ++ 'requestFinished': { ++ requestId: t.String, ++ }, ++ 'requestFailed': { ++ requestId: t.String, ++ errorCode: t.String, ++ }, ++ }, ++ methods: { ++ 'enable': {}, ++ 'setRequestInterception': { ++ params: { ++ enabled: t.Boolean, ++ }, ++ }, ++ 'setExtraHTTPHeaders': { ++ params: { ++ headers: t.Array({ ++ name: t.String, ++ value: t.String, ++ }), ++ }, ++ }, ++ 'abortSuspendedRequest': { ++ params: { ++ requestId: t.String, ++ }, ++ }, ++ 'resumeSuspendedRequest': { ++ params: { ++ requestId: t.String, ++ headers: t.Optional(t.Array({ ++ name: t.String, ++ value: t.String, ++ })), ++ }, ++ }, ++ 'getResponseBody': { ++ params: { ++ requestId: t.String, ++ }, ++ returns: { ++ base64body: t.String, ++ evicted: t.Optional(t.Boolean), ++ }, ++ }, ++ }, ++}; ++ ++const Runtime = { ++ targets: ['page'], ++ events: { ++ 'executionContextCreated': { ++ executionContextId: t.String, ++ auxData: t.Any, ++ }, ++ 'executionContextDestroyed': { ++ executionContextId: t.String, ++ }, ++ 'console': { ++ executionContextId: t.String, ++ args: t.Array(types.RemoteObject), ++ type: t.String, ++ location: { ++ columnNumber: t.Number, ++ lineNumber: t.Number, ++ url: t.String, ++ }, ++ }, ++ }, ++ methods: { ++ 'enable': { ++ params: {}, ++ }, ++ 'evaluate': { ++ params: { ++ // Pass frameId here. ++ executionContextId: t.String, ++ expression: t.String, ++ returnByValue: t.Optional(t.Boolean), ++ }, ++ ++ returns: { ++ result: t.Optional(types.RemoteObject), ++ exceptionDetails: t.Optional({ ++ text: t.Optional(t.String), ++ stack: t.Optional(t.String), ++ value: t.Optional(t.Any), ++ }), ++ } ++ }, ++ 'callFunction': { ++ params: { ++ // Pass frameId here. ++ executionContextId: t.String, ++ functionDeclaration: t.String, ++ returnByValue: t.Optional(t.Boolean), ++ args: t.Array({ ++ objectId: t.Optional(t.String), ++ unserializableValue: t.Optional(t.Enum(['Infinity', '-Infinity', '-0', 'NaN'])), ++ value: t.Any, ++ }), ++ }, ++ ++ returns: { ++ result: t.Optional(types.RemoteObject), ++ exceptionDetails: t.Optional({ ++ text: t.Optional(t.String), ++ stack: t.Optional(t.String), ++ value: t.Optional(t.Any), ++ }), ++ } ++ }, ++ 'disposeObject': { ++ params: { ++ executionContextId: t.String, ++ objectId: t.String, ++ }, ++ }, ++ ++ 'getObjectProperties': { ++ params: { ++ executionContextId: t.String, ++ objectId: t.String, ++ }, ++ ++ returns: { ++ properties: t.Array({ ++ name: t.String, ++ value: types.RemoteObject, ++ }), ++ } ++ }, ++ }, ++}; ++ ++const Page = { ++ targets: ['page'], ++ events: { ++ 'eventFired': { ++ frameId: t.String, ++ name: t.Enum(['load', 'DOMContentLoaded']), ++ }, ++ 'uncaughtError': { ++ frameId: t.String, ++ message: t.String, ++ stack: t.String, ++ }, ++ 'frameAttached': { ++ frameId: t.String, ++ parentFrameId: t.Optional(t.String), ++ }, ++ 'frameDetached': { ++ frameId: t.String, ++ }, ++ 'navigationStarted': { ++ frameId: t.String, ++ navigationId: t.String, ++ url: t.String, ++ }, ++ 'navigationCommitted': { ++ frameId: t.String, ++ navigationId: t.String, ++ url: t.String, ++ // frame.id or frame.name ++ name: t.String, ++ }, ++ 'navigationAborted': { ++ frameId: t.String, ++ navigationId: t.String, ++ errorText: t.String, ++ }, ++ 'sameDocumentNavigation': { ++ frameId: t.String, ++ url: t.String, ++ }, ++ 'dialogOpened': { ++ dialogId: t.String, ++ type: t.Enum(['prompt', 'alert', 'confirm', 'beforeunload']), ++ message: t.String, ++ defaultValue: t.Optional(t.String), ++ }, ++ 'dialogClosed': { ++ dialogId: t.String, ++ }, ++ 'bindingCalled': { ++ executionContextId: t.String, ++ name: t.String, ++ payload: t.Any, ++ }, ++ }, ++ ++ methods: { ++ 'enable': { ++ params: {}, ++ }, ++ 'close': { ++ params: { ++ runBeforeUnload: t.Optional(t.Boolean), ++ }, ++ }, ++ 'setFileInputFiles': { ++ params: { ++ frameId: t.String, ++ objectId: t.String, ++ files: t.Array(t.String), ++ }, ++ }, ++ 'addBinding': { ++ params: { ++ name: t.String, ++ }, ++ }, ++ 'setViewport': { ++ params: { ++ viewport: t.Nullable({ ++ width: t.Number, ++ height: t.Number, ++ deviceScaleFactor: t.Number, ++ isMobile: t.Boolean, ++ hasTouch: t.Boolean, ++ isLandscape: t.Boolean, ++ }), ++ }, ++ }, ++ 'setUserAgent': { ++ params: { ++ userAgent: t.Nullable(t.String), ++ }, ++ }, ++ 'setEmulatedMedia': { ++ params: { ++ media: t.Enum(['screen', 'print', '']), ++ }, ++ }, ++ 'setCacheDisabled': { ++ params: { ++ cacheDisabled: t.Boolean, ++ }, ++ }, ++ 'setJavascriptEnabled': { ++ params: { ++ enabled: t.Boolean, ++ }, ++ }, ++ 'contentFrame': { ++ params: { ++ frameId: t.String, ++ objectId: t.String, ++ }, ++ returns: { ++ frameId: t.Nullable(t.String), ++ }, ++ }, ++ 'addScriptToEvaluateOnNewDocument': { ++ params: { ++ script: t.String, ++ }, ++ returns: { ++ scriptId: t.String, ++ } ++ }, ++ 'removeScriptToEvaluateOnNewDocument': { ++ params: { ++ scriptId: t.String, ++ }, ++ }, ++ 'navigate': { ++ params: { ++ frameId: t.String, ++ url: t.String, ++ referer: t.Optional(t.String), ++ }, ++ returns: { ++ navigationId: t.Nullable(t.String), ++ navigationURL: t.Nullable(t.String), ++ } ++ }, ++ 'goBack': { ++ params: { ++ frameId: t.String, ++ }, ++ returns: { ++ navigationId: t.Nullable(t.String), ++ navigationURL: t.Nullable(t.String), ++ } ++ }, ++ 'goForward': { ++ params: { ++ frameId: t.String, ++ }, ++ returns: { ++ navigationId: t.Nullable(t.String), ++ navigationURL: t.Nullable(t.String), ++ } ++ }, ++ 'reload': { ++ params: { ++ frameId: t.String, ++ }, ++ returns: { ++ navigationId: t.String, ++ navigationURL: t.String, ++ } ++ }, ++ 'getBoundingBox': { ++ params: { ++ frameId: t.String, ++ objectId: t.String, ++ }, ++ returns: t.Nullable({ ++ x: t.Number, ++ y: t.Number, ++ width: t.Number, ++ height: t.Number, ++ }), ++ }, ++ 'screenshot': { ++ params: { ++ mimeType: t.Enum(['image/png', 'image/jpeg']), ++ fullPage: t.Optional(t.Boolean), ++ clip: t.Optional({ ++ x: t.Number, ++ y: t.Number, ++ width: t.Number, ++ height: t.Number, ++ }) ++ }, ++ returns: { ++ data: t.String, ++ } ++ }, ++ 'getContentQuads': { ++ params: { ++ frameId: t.String, ++ objectId: t.String, ++ }, ++ returns: { ++ quads: t.Array(types.DOMQuad), ++ }, ++ }, ++ 'dispatchKeyEvent': { ++ params: { ++ type: t.String, ++ key: t.String, ++ keyCode: t.Number, ++ location: t.Number, ++ code: t.String, ++ repeat: t.Boolean, ++ } ++ }, ++ 'dispatchTouchEvent': { ++ params: { ++ type: t.Enum(['touchStart', 'touchEnd', 'touchMove', 'touchCancel']), ++ touchPoints: t.Array(types.TouchPoint), ++ modifiers: t.Number, ++ }, ++ returns: { ++ defaultPrevented: t.Boolean, ++ } ++ }, ++ 'dispatchMouseEvent': { ++ params: { ++ type: t.String, ++ button: t.Number, ++ x: t.Number, ++ y: t.Number, ++ modifiers: t.Number, ++ clickCount: t.Optional(t.Number), ++ buttons: t.Number, ++ } ++ }, ++ 'insertText': { ++ params: { ++ text: t.String, ++ } ++ }, ++ 'handleDialog': { ++ params: { ++ dialogId: t.String, ++ accept: t.Boolean, ++ promptText: t.Optional(t.String), ++ }, ++ }, ++ }, ++}; ++ ++ ++const Accessibility = { ++ targets: ['page'], ++ events: {}, ++ methods: { ++ 'getFullAXTree': { ++ params: {}, ++ returns: { ++ tree:types.AXTree ++ }, ++ } ++ } ++} ++ ++this.protocol = { ++ domains: {Browser, Target, Page, Runtime, Network, Accessibility}, ++}; ++this.checkScheme = checkScheme; ++this.EXPORTED_SYMBOLS = ['protocol', 'checkScheme']; +diff --git a/testing/juggler/protocol/RuntimeHandler.js b/testing/juggler/protocol/RuntimeHandler.js +new file mode 100644 +index 000000000000..0026e8ff58ef +--- /dev/null ++++ b/testing/juggler/protocol/RuntimeHandler.js +@@ -0,0 +1,41 @@ ++"use strict"; ++ ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++ ++const Cc = Components.classes; ++const Ci = Components.interfaces; ++const Cu = Components.utils; ++const helper = new Helper(); ++ ++class RuntimeHandler { ++ constructor(chromeSession, contentSession) { ++ this._chromeSession = chromeSession; ++ this._contentSession = contentSession; ++ } ++ ++ async enable(options) { ++ return await this._contentSession.send('Runtime.enable', options); ++ } ++ ++ async evaluate(options) { ++ return await this._contentSession.send('Runtime.evaluate', options); ++ } ++ ++ async callFunction(options) { ++ return await this._contentSession.send('Runtime.callFunction', options); ++ } ++ ++ async getObjectProperties(options) { ++ return await this._contentSession.send('Runtime.getObjectProperties', options); ++ } ++ ++ async disposeObject(options) { ++ return await this._contentSession.send('Runtime.disposeObject', options); ++ } ++ ++ dispose() {} ++} ++ ++var EXPORTED_SYMBOLS = ['RuntimeHandler']; ++this.RuntimeHandler = RuntimeHandler; +diff --git a/testing/juggler/protocol/TargetHandler.js b/testing/juggler/protocol/TargetHandler.js +new file mode 100644 +index 000000000000..4ea36eeba758 +--- /dev/null ++++ b/testing/juggler/protocol/TargetHandler.js +@@ -0,0 +1,75 @@ ++"use strict"; ++ ++const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); ++const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js"); ++const {BrowserContextManager} = ChromeUtils.import("chrome://juggler/content/BrowserContextManager.js"); ++const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); ++const helper = new Helper(); ++ ++class TargetHandler { ++ /** ++ * @param {ChromeSession} session ++ */ ++ constructor(session) { ++ this._session = session; ++ this._contextManager = BrowserContextManager.instance(); ++ this._targetRegistry = TargetRegistry.instance(); ++ this._enabled = false; ++ this._eventListeners = []; ++ } ++ ++ async attachToTarget({targetId}) { ++ const sessionId = await this._session.dispatcher().createSession(targetId); ++ return {sessionId}; ++ } ++ ++ async createBrowserContext() { ++ return {browserContextId: this._contextManager.createBrowserContext()}; ++ } ++ ++ async removeBrowserContext({browserContextId}) { ++ this._contextManager.removeBrowserContext(browserContextId); ++ } ++ ++ async getBrowserContexts() { ++ return {browserContextIds: this._contextManager.getBrowserContexts()}; ++ } ++ ++ async enable() { ++ if (this._enabled) ++ return; ++ this._enabled = true; ++ for (const targetInfo of this._targetRegistry.targetInfos()) ++ this._onTargetCreated(targetInfo); ++ ++ this._eventListeners = [ ++ helper.on(this._targetRegistry, TargetRegistry.Events.TargetCreated, this._onTargetCreated.bind(this)), ++ helper.on(this._targetRegistry, TargetRegistry.Events.TargetChanged, this._onTargetChanged.bind(this)), ++ helper.on(this._targetRegistry, TargetRegistry.Events.TargetDestroyed, this._onTargetDestroyed.bind(this)), ++ ]; ++ } ++ ++ dispose() { ++ helper.removeListeners(this._eventListeners); ++ } ++ ++ _onTargetCreated(targetInfo) { ++ this._session.emitEvent('Target.targetCreated', targetInfo); ++ } ++ ++ _onTargetChanged(targetInfo) { ++ this._session.emitEvent('Target.targetInfoChanged', targetInfo); ++ } ++ ++ _onTargetDestroyed(targetInfo) { ++ this._session.emitEvent('Target.targetDestroyed', targetInfo); ++ } ++ ++ async newPage({browserContextId}) { ++ const targetId = await this._targetRegistry.newPage({browserContextId}); ++ return {targetId}; ++ } ++} ++ ++var EXPORTED_SYMBOLS = ['TargetHandler']; ++this.TargetHandler = TargetHandler; +diff --git a/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp +index 9aea55ddf773..188a0f28b8e1 100644 +--- a/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp ++++ b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp +@@ -179,8 +179,16 @@ nsBrowserStatusFilter::OnStateChange(nsIWebProgress* aWebProgress, + } + + NS_IMETHODIMP +-nsBrowserStatusFilter::OnProgressChange(nsIWebProgress* aWebProgress, +- nsIRequest* aRequest, ++nsBrowserStatusFilter::OnFrameLocationChange(nsIWebProgress *aWebProgress, ++ nsIRequest *aRequest, ++ nsIURI *aLocation, ++ uint32_t aFlags) { ++ return NS_OK; ++} ++ ++NS_IMETHODIMP ++nsBrowserStatusFilter::OnProgressChange(nsIWebProgress *aWebProgress, ++ nsIRequest *aRequest, + int32_t aCurSelfProgress, + int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, +diff --git a/toolkit/toolkit.mozbuild b/toolkit/toolkit.mozbuild +index 79d6eeed7247..0362763ead99 100644 +--- a/toolkit/toolkit.mozbuild ++++ b/toolkit/toolkit.mozbuild +@@ -168,6 +168,7 @@ if CONFIG['ENABLE_MARIONETTE']: + DIRS += [ + '/testing/firefox-ui', + '/testing/marionette', ++ '/testing/juggler', + '/toolkit/components/telemetry/tests/marionette', + ] + +diff --git a/uriloader/base/nsDocLoader.cpp b/uriloader/base/nsDocLoader.cpp +index 92cb5f3cf6da..7918f127c801 100644 +--- a/uriloader/base/nsDocLoader.cpp ++++ b/uriloader/base/nsDocLoader.cpp +@@ -1370,6 +1370,24 @@ void nsDocLoader::FireOnLocationChange(nsIWebProgress* aWebProgress, + } + } + ++void nsDocLoader::FireOnFrameLocationChange(nsIWebProgress* aWebProgress, ++ nsIRequest* aRequest, ++ nsIURI *aUri, ++ uint32_t aFlags) { ++ NOTIFY_LISTENERS(nsIWebProgress::NOTIFY_FRAME_LOCATION, ++ nsCOMPtr listener2 = ++ do_QueryReferent(info.mWeakListener); ++ if (!listener2) ++ continue; ++ listener2->OnFrameLocationChange(aWebProgress, aRequest, aUri, aFlags); ++ ); ++ ++ // Pass the notification up to the parent... ++ if (mParent) { ++ mParent->FireOnFrameLocationChange(aWebProgress, aRequest, aUri, aFlags); ++ } ++} ++ + void nsDocLoader::FireOnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsresult aStatus, + const char16_t* aMessage) { +diff --git a/uriloader/base/nsDocLoader.h b/uriloader/base/nsDocLoader.h +index 4b551ff8e5c1..abc59361f2d6 100644 +--- a/uriloader/base/nsDocLoader.h ++++ b/uriloader/base/nsDocLoader.h +@@ -210,6 +210,11 @@ class nsDocLoader : public nsIDocumentLoader, + void FireOnLocationChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, + nsIURI* aUri, uint32_t aFlags); + ++ void FireOnFrameLocationChange(nsIWebProgress* aWebProgress, ++ nsIRequest* aRequest, ++ nsIURI *aUri, ++ uint32_t aFlags); ++ + MOZ_MUST_USE bool RefreshAttempted(nsIWebProgress* aWebProgress, nsIURI* aURI, + int32_t aDelay, bool aSameURI); + +diff --git a/uriloader/base/nsIWebProgress.idl b/uriloader/base/nsIWebProgress.idl +index b0cde5026dc7..09ebb0ef6799 100644 +--- a/uriloader/base/nsIWebProgress.idl ++++ b/uriloader/base/nsIWebProgress.idl +@@ -87,6 +87,10 @@ interface nsIWebProgress : nsISupports + * NOTIFY_REFRESH + * Receive onRefreshAttempted events. + * This is defined on nsIWebProgressListener2. ++ * ++ * NOTIFY_FRAME_LOCATION ++ * Receive onFrameLocationChange events. ++ * This is defined on nsIWebProgressListener2. + */ + const unsigned long NOTIFY_PROGRESS = 0x00000010; + const unsigned long NOTIFY_STATUS = 0x00000020; +@@ -94,11 +98,12 @@ interface nsIWebProgress : nsISupports + const unsigned long NOTIFY_LOCATION = 0x00000080; + const unsigned long NOTIFY_REFRESH = 0x00000100; + const unsigned long NOTIFY_CONTENT_BLOCKING = 0x00000200; ++ const unsigned long NOTIFY_FRAME_LOCATION = 0x00000400; + + /** + * This flag enables all notifications. + */ +- const unsigned long NOTIFY_ALL = 0x000003ff; ++ const unsigned long NOTIFY_ALL = 0x000007ff; + + /** + * Registers a listener to receive web progress events. +diff --git a/uriloader/base/nsIWebProgressListener2.idl b/uriloader/base/nsIWebProgressListener2.idl +index 87701f8d2cfe..ae1aa85c019c 100644 +--- a/uriloader/base/nsIWebProgressListener2.idl ++++ b/uriloader/base/nsIWebProgressListener2.idl +@@ -66,4 +66,27 @@ interface nsIWebProgressListener2 : nsIWebProgressListener { + in nsIURI aRefreshURI, + in long aMillis, + in boolean aSameURI); ++ ++ /** ++ * Called when the location of the window or its subframes changes. This is not ++ * when a load is requested, but rather once it is verified that the load is ++ * going to occur in the given window. For instance, a load that starts in a ++ * window might send progress and status messages for the new site, but it ++ * will not send the onLocationChange until we are sure that we are loading ++ * this new page here. ++ * ++ * @param aWebProgress ++ * The nsIWebProgress instance that fired the notification. ++ * @param aRequest ++ * The associated nsIRequest. This may be null in some cases. ++ * @param aLocation ++ * The URI of the location that is being loaded. ++ * @param aFlags ++ * This is a value which explains the situation or the reason why ++ * the location has changed. ++ */ ++ void onFrameLocationChange(in nsIWebProgress aWebProgress, ++ in nsIRequest aRequest, ++ in nsIURI aLocation, ++ [optional] in unsigned long aFlags); + }; +-- +2.17.1 + diff --git a/browser_patches/upload.sh b/browser_patches/upload.sh new file mode 100755 index 0000000000..0266f1ef8c --- /dev/null +++ b/browser_patches/upload.sh @@ -0,0 +1,68 @@ +#!/bin/bash +set -e +set +x + +cleanup() { + cd $OLD_DIR +} + +OLD_DIR=$(pwd -P) +cd "$(dirname "$0")" +trap cleanup EXIT + +if [[ ($1 == '--help') || ($1 == '-h') ]]; then + echo "usage: $0 [firefox|webkit]" + echo + echo "Archive and upload a browser" + echo + echo "NOTE: \$AZ_ACCOUNT_KEY (azure account name) and \$AZ_ACCOUNT_NAME (azure account name)" + echo "env variables are required to upload builds to CDN." + exit 0 +fi + +if [[ $# == 0 ]]; then + echo "missing browser: 'firefox' or 'webkit'" + echo "try '$0 --help' for more information" + exit 1 +fi + +if [[ (-z $AZ_ACCOUNT_KEY) || (-z $AZ_ACCOUNT_NAME) ]]; then + echo "ERROR: Either \$AZ_ACCOUNT_KEY or \$AZ_ACCOUNT_NAME environment variable is missing." + echo " 'Azure Account Name' and 'Azure Account Key' secrets that are required" + echo " to upload builds ot Azure CDN." + exit 1 +fi + +ARCHIVE_SCRIPT="" +BROWSER_NAME="" +BUILD_NUMBER="" +if [[ ("$1" == "firefox") || ("$1" == "firefox/") ]]; then + # we always apply our patches atop of beta since it seems to get better + # reliability guarantees. + ARCHIVE_FOLDER="$PWD/firefox" + BUILD_NUMBER=$(cat "$PWD/firefox/BUILD_NUMBER") + ARCHIVE_SCRIPT="$PWD/firefox/archive.sh" + BROWSER_NAME="firefox" +elif [[ ("$1" == "webkit") || ("$1" == "webkit/") ]]; then + ARCHIVE_FOLDER="$PWD/webkit" + BUILD_NUMBER=$(cat "$PWD/webkit/BUILD_NUMBER") + ARCHIVE_SCRIPT="$PWD/webkit/archive.sh" + BROWSER_NAME="webkit" +else + echo ERROR: unknown browser to export - "$1" + exit 1 +fi + +if ! [[ -z $(ls $ARCHIVE_FOLDER | grep '.zip') ]]; then + echo ERROR: .zip file already exists in $ARCHIVE_FOLDER! + echo Remove manually all zip files and re-run the script. + exit 1 +fi + +$ARCHIVE_SCRIPT +ZIP_NAME=$(ls $ARCHIVE_FOLDER | grep '.zip') +ZIP_PATH=$ARCHIVE_FOLDER/$ZIP_NAME +BLOB_NAME="$BROWSER_NAME/$BUILD_NUMBER/$ZIP_NAME" +az storage blob upload -c builds --account-key $AZ_ACCOUNT_KEY --account-name $AZ_ACCOUNT_NAME -f $ZIP_PATH -n "$BLOB_NAME" +echo "Uploaded $(du -h "$ZIP_PATH" | awk '{print $1}') as $BLOB_NAME" +rm $ZIP_PATH diff --git a/browser_patches/webkit/.gitignore b/browser_patches/webkit/.gitignore new file mode 100644 index 0000000000..5e660dc18e --- /dev/null +++ b/browser_patches/webkit/.gitignore @@ -0,0 +1 @@ +/checkout diff --git a/browser_patches/webkit/BASE_REVISION b/browser_patches/webkit/BASE_REVISION new file mode 100644 index 0000000000..0fc3234513 --- /dev/null +++ b/browser_patches/webkit/BASE_REVISION @@ -0,0 +1 @@ +cadee71e3e832cc0b78184a714ade07d9a6d3173 diff --git a/browser_patches/webkit/BUILD_NUMBER b/browser_patches/webkit/BUILD_NUMBER new file mode 100644 index 0000000000..d474e1b4d6 --- /dev/null +++ b/browser_patches/webkit/BUILD_NUMBER @@ -0,0 +1,2 @@ +1 + diff --git a/browser_patches/webkit/archive.sh b/browser_patches/webkit/archive.sh new file mode 100755 index 0000000000..57ebdcb0ad --- /dev/null +++ b/browser_patches/webkit/archive.sh @@ -0,0 +1,85 @@ +#!/bin/bash + +if [[ ("$1" == "-h") || ("$1" == "--help") ]]; then + echo "usage: $0" + echo + echo "Generate distributable .zip archive from ./checkout folder that was previously built." + echo + exit 0 +fi + +set -e +set -x + +main() { + cd checkout + + if [[ "$(uname)" == "Darwin" ]]; then + createZipForMac + elif [[ "$(uname)" == "Linux" ]]; then + createZipForLinux + else + echo "ERROR: cannot upload on this platform!" 1>&2 + exit 1; + fi +} + +createZipForLinux() { + # create a TMP directory to copy all necessary files + local tmpdir=$(mktemp -d -t webkit-deploy-XXXXXXXXXX) + mkdir -p $tmpdir + + # copy all relevant binaries + cp -t $tmpdir ./WebKitBuild/Release/bin/MiniBrowser ./WebKitBuild/Release/bin/WebKit*Process + # copy runner + cp -t $tmpdir ../pw_run.sh + # copy protocol + node ../concat_protocol.js > $tmpdir/protocol.json + # copy all relevant shared objects + LD_LIBRARY_PATH="$PWD/WebKitBuild/DependenciesGTK/Root/lib" ldd WebKitBuild/Release/bin/MiniBrowser | grep -o '[^ ]*WebKitBuild/[^ ]*' | xargs cp -t $tmpdir + + # we failed to nicely build libgdk_pixbuf - expect it in the env + rm $tmpdir/libgdk_pixbuf* + + # tar resulting directory and cleanup TMP. + local zipname="minibrowser-linux.zip" + zip -jr ../$zipname $tmpdir + rm -rf $tmpdir +} + +createZipForMac() { + # create a TMP directory to copy all necessary files + local tmpdir=$(mktemp -d) + + # copy all relevant files + ditto {./WebKitBuild/Release,$tmpdir}/com.apple.WebKit.Networking.xpc + ditto {./WebKitBuild/Release,$tmpdir}/com.apple.WebKit.Plugin.64.xpc + ditto {./WebKitBuild/Release,$tmpdir}/com.apple.WebKit.WebContent.xpc + ditto {./WebKitBuild/Release,$tmpdir}/JavaScriptCore.framework + ditto {./WebKitBuild/Release,$tmpdir}/libwebrtc.dylib + ditto {./WebKitBuild/Release,$tmpdir}/MiniBrowser.app + ditto {./WebKitBuild/Release,$tmpdir}/PluginProcessShim.dylib + ditto {./WebKitBuild/Release,$tmpdir}/SecItemShim.dylib + ditto {./WebKitBuild/Release,$tmpdir}/WebCore.framework + ditto {./WebKitBuild/Release,$tmpdir}/WebInspectorUI.framework + ditto {./WebKitBuild/Release,$tmpdir}/WebKit.framework + ditto {./WebKitBuild/Release,$tmpdir}/WebKitLegacy.framework + ditto {..,$tmpdir}/pw_run.sh + # copy protocol + node ../concat_protocol.js > $tmpdir/protocol.json + + # zip resulting directory and cleanup TMP. + local MAC_MAJOR_MINOR_VERSION=$(sw_vers -productVersion | grep -o '^\d\+.\d\+') + local zipname="minibrowser-mac-$MAC_MAJOR_MINOR_VERSION.zip" + ditto -c -k $tmpdir ../$zipname + rm -rf $tmpdir +} + +cleanup() { + cd $OLD_DIR +} + +OLD_DIR=$(pwd -P) +cd "$(dirname "$0")" +trap cleanup EXIT +main "$@" diff --git a/browser_patches/webkit/build.sh b/browser_patches/webkit/build.sh new file mode 100755 index 0000000000..ee2c12c28c --- /dev/null +++ b/browser_patches/webkit/build.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -e +set +x + +function cleanup() { + cd $OLD_DIR +} + +OLD_DIR=$(pwd -P) +cd "$(dirname "$0")" +trap cleanup EXIT + +cd checkout + +if ! [[ $(git rev-parse --abbrev-ref HEAD) == "pwdev" ]]; then + echo "ERROR: Cannot build any branch other than PWDEV" + exit 1; +else + echo "-- checking git branch is PWDEV - OK" +fi + +if [[ "$(uname)" == "Darwin" ]]; then + ./Tools/Scripts/build-webkit --release +elif [[ "$(uname)" == "Linux" ]]; then + ./Tools/Scripts/build-webkit --gtk --release MiniBrowser +else + echo "ERROR: cannot upload on this platform!" 1>&2 + exit 1; +fi diff --git a/browser_patches/webkit/concat_protocol.js b/browser_patches/webkit/concat_protocol.js new file mode 100644 index 0000000000..bf3d84bcc4 --- /dev/null +++ b/browser_patches/webkit/concat_protocol.js @@ -0,0 +1,6 @@ +const fs = require('fs'); +const path = require('path'); +const protocolDir = path.join(__dirname, './checkout/Source/JavaScriptCore/inspector/protocol'); +const files = fs.readdirSync(protocolDir).filter(f => f.endsWith('.json')).map(f => path.join(protocolDir, f)); +const json = files.map(file => JSON.parse(fs.readFileSync(file))); +console.log(JSON.stringify(json)); diff --git a/browser_patches/webkit/patches/0001-chore-bootstrap.patch b/browser_patches/webkit/patches/0001-chore-bootstrap.patch new file mode 100644 index 0000000000..1e2b20bc94 --- /dev/null +++ b/browser_patches/webkit/patches/0001-chore-bootstrap.patch @@ -0,0 +1,5890 @@ +From 23d352fbff5f65f02eec92327a5c839b9f6c6fca Mon Sep 17 00:00:00 2001 +From: Andrey Lushnikov +Date: Fri, 15 Nov 2019 18:07:34 -0800 +Subject: [PATCH xserver] chore: bootstrap + +--- + Source/JavaScriptCore/CMakeLists.txt | 3 + + Source/JavaScriptCore/DerivedSources.make | 3 + + .../inspector/InspectorBackendDispatcher.cpp | 21 +- + .../inspector/InspectorBackendDispatcher.h | 5 +- + .../inspector/InspectorTarget.h | 3 + + .../inspector/agents/InspectorTargetAgent.cpp | 46 +++- + .../inspector/agents/InspectorTargetAgent.h | 6 +- + .../inspector/protocol/Browser.json | 40 +++ + .../inspector/protocol/DOM.json | 21 ++ + .../inspector/protocol/Emulation.json | 14 + + .../inspector/protocol/Input.json | 160 +++++++++++ + .../inspector/protocol/Page.json | 18 +- + .../inspector/protocol/Target.json | 18 +- + .../inspector/InspectorInstrumentation.cpp | 6 + + .../inspector/InspectorInstrumentation.h | 9 + + .../inspector/agents/InspectorDOMAgent.cpp | 57 ++++ + .../inspector/agents/InspectorDOMAgent.h | 1 + + .../inspector/agents/InspectorPageAgent.cpp | 18 +- + .../inspector/agents/InspectorPageAgent.h | 3 +- + Source/WebCore/loader/FrameLoader.cpp | 1 + + Source/WebCore/page/History.cpp | 1 + + .../WebCore/platform/PlatformKeyboardEvent.h | 2 + + .../platform/gtk/PlatformKeyboardEventGtk.cpp | 242 +++++++++++++++++ + .../libwpe/PlatformKeyboardEventLibWPE.cpp | 240 ++++++++++++++++ + Source/WebKit/Shared/API/c/wpe/WebKit.h | 1 + + Source/WebKit/Shared/NativeWebKeyboardEvent.h | 5 + + Source/WebKit/Shared/NativeWebMouseEvent.h | 4 + + Source/WebKit/Shared/WebEvent.h | 6 +- + Source/WebKit/Shared/WebKeyboardEvent.cpp | 22 ++ + .../Shared/gtk/NativeWebKeyboardEventGtk.cpp | 2 +- + .../Shared/gtk/NativeWebMouseEventGtk.cpp | 4 +- + Source/WebKit/Sources.txt | 8 + + Source/WebKit/SourcesCocoa.txt | 1 + + Source/WebKit/SourcesGTK.txt | 5 + + Source/WebKit/SourcesWPE.txt | 6 +- + Source/WebKit/UIProcess/API/APIAttachment.cpp | 1 + + Source/WebKit/UIProcess/API/C/WKPage.cpp | 2 + + .../UIProcess/API/Cocoa/_WKBrowserInspector.h | 50 ++++ + .../API/Cocoa/_WKBrowserInspector.mm | 52 ++++ + .../API/glib/WebKitBrowserInspector.cpp | 141 ++++++++++ + .../API/glib/WebKitBrowserInspectorPrivate.h | 36 +++ + .../UIProcess/API/glib/WebKitWebContext.cpp | 5 + + .../UIProcess/API/gtk/PageClientImpl.cpp | 2 + + .../API/gtk/WebKitBrowserInspector.h | 84 ++++++ + Source/WebKit/UIProcess/API/gtk/webkit2.h | 1 + + .../API/wpe/WebKitBrowserInspector.h | 81 ++++++ + Source/WebKit/UIProcess/API/wpe/webkit.h | 1 + + .../UIProcess/BrowserInspectorController.cpp | 128 +++++++++ + .../UIProcess/BrowserInspectorController.h | 74 +++++ + .../WebKit/UIProcess/BrowserInspectorPipe.cpp | 62 +++++ + .../WebKit/UIProcess/BrowserInspectorPipe.h | 43 +++ + .../UIProcess/BrowserInspectorTargetAgent.cpp | 110 ++++++++ + .../UIProcess/BrowserInspectorTargetAgent.h | 62 +++++ + .../PopUpSOAuthorizationSession.h | 4 + + .../PopUpSOAuthorizationSession.mm | 1 + + .../UIProcess/InspectorBrowserAgent.cpp | 101 +++++++ + .../WebKit/UIProcess/InspectorBrowserAgent.h | 81 ++++++ + .../UIProcess/InspectorBrowserAgentClient.h | 52 ++++ + .../WebKit/UIProcess/InspectorTargetProxy.cpp | 18 +- + .../WebKit/UIProcess/InspectorTargetProxy.h | 11 +- + .../WebKit/UIProcess/RemoteInspectorPipe.cpp | 159 +++++++++++ + Source/WebKit/UIProcess/RemoteInspectorPipe.h | 70 +++++ + .../AuthenticatorManager.cpp | 1 + + .../UIProcess/WebPageInspectorController.cpp | 56 +++- + .../UIProcess/WebPageInspectorController.h | 8 + + .../WebPageInspectorEmulationAgent.cpp | 61 +++++ + .../WebPageInspectorEmulationAgent.h | 63 +++++ + .../UIProcess/WebPageInspectorInputAgent.cpp | 257 ++++++++++++++++++ + .../UIProcess/WebPageInspectorInputAgent.h | 76 ++++++ + .../UIProcess/WebPageInspectorTargetProxy.cpp | 129 +++++++++ + .../UIProcess/WebPageInspectorTargetProxy.h | 67 +++++ + Source/WebKit/UIProcess/WebPageProxy.cpp | 12 +- + Source/WebKit/UIProcess/WebPageProxy.h | 9 + + .../glib/InspectorBrowserAgentClientGLib.cpp | 130 +++++++++ + .../glib/InspectorBrowserAgentClientGLib.h | 63 +++++ + .../gtk/WebPageInspectorEmulationAgentGtk.cpp | 58 ++++ + .../gtk/WebPageInspectorInputAgentGtk.cpp | 108 ++++++++ + .../gtk/WebPageInspectorTargetProxyGtk.cpp | 45 +++ + .../WebKit/UIProcess/ios/PageClientImplIOS.mm | 2 + + .../mac/InspectorBrowserAgentClientMac.h | 56 ++++ + .../mac/InspectorBrowserAgentClientMac.mm | 95 +++++++ + .../WebKit/UIProcess/mac/PageClientImplMac.mm | 5 + + .../mac/WebPageInspectorEmulationAgentMac.mm | 42 +++ + .../mac/WebPageInspectorInputAgentMac.mm | 37 +++ + .../mac/WebPageInspectorTargetProxyMac.mm | 41 +++ + .../wpe/WebPageInspectorEmulationAgentWPE.cpp | 41 +++ + .../wpe/WebPageInspectorInputAgentWPE.cpp | 99 +++++++ + .../wpe/WebPageInspectorTargetProxyWPE.cpp | 41 +++ + .../WebKit/WebKit.xcodeproj/project.pbxproj | 54 ++++ + .../WebPage/WebPageInspectorTarget.cpp | 7 + + .../WebPage/WebPageInspectorTarget.h | 1 + + Source/WebKit/WebProcess/WebProcess.cpp | 3 +- + Tools/MiniBrowser/gtk/main.c | 28 ++ + Tools/MiniBrowser/mac/AppDelegate.h | 4 +- + Tools/MiniBrowser/mac/AppDelegate.m | 25 +- + .../mac/WK2BrowserWindowController.h | 3 + + .../mac/WK2BrowserWindowController.m | 17 +- + Tools/MiniBrowser/wpe/main.cpp | 37 +++ + 98 files changed, 4162 insertions(+), 53 deletions(-) + create mode 100644 Source/JavaScriptCore/inspector/protocol/Browser.json + create mode 100644 Source/JavaScriptCore/inspector/protocol/Emulation.json + create mode 100644 Source/JavaScriptCore/inspector/protocol/Input.json + create mode 100644 Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.h + create mode 100644 Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.mm + create mode 100644 Source/WebKit/UIProcess/API/glib/WebKitBrowserInspector.cpp + create mode 100644 Source/WebKit/UIProcess/API/glib/WebKitBrowserInspectorPrivate.h + create mode 100644 Source/WebKit/UIProcess/API/gtk/WebKitBrowserInspector.h + create mode 100644 Source/WebKit/UIProcess/API/wpe/WebKitBrowserInspector.h + create mode 100644 Source/WebKit/UIProcess/BrowserInspectorController.cpp + create mode 100644 Source/WebKit/UIProcess/BrowserInspectorController.h + create mode 100644 Source/WebKit/UIProcess/BrowserInspectorPipe.cpp + create mode 100644 Source/WebKit/UIProcess/BrowserInspectorPipe.h + create mode 100644 Source/WebKit/UIProcess/BrowserInspectorTargetAgent.cpp + create mode 100644 Source/WebKit/UIProcess/BrowserInspectorTargetAgent.h + create mode 100644 Source/WebKit/UIProcess/InspectorBrowserAgent.cpp + create mode 100644 Source/WebKit/UIProcess/InspectorBrowserAgent.h + create mode 100644 Source/WebKit/UIProcess/InspectorBrowserAgentClient.h + create mode 100644 Source/WebKit/UIProcess/RemoteInspectorPipe.cpp + create mode 100644 Source/WebKit/UIProcess/RemoteInspectorPipe.h + create mode 100644 Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.cpp + create mode 100644 Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.h + create mode 100644 Source/WebKit/UIProcess/WebPageInspectorInputAgent.cpp + create mode 100644 Source/WebKit/UIProcess/WebPageInspectorInputAgent.h + create mode 100644 Source/WebKit/UIProcess/WebPageInspectorTargetProxy.cpp + create mode 100644 Source/WebKit/UIProcess/WebPageInspectorTargetProxy.h + create mode 100644 Source/WebKit/UIProcess/glib/InspectorBrowserAgentClientGLib.cpp + create mode 100644 Source/WebKit/UIProcess/glib/InspectorBrowserAgentClientGLib.h + create mode 100644 Source/WebKit/UIProcess/gtk/WebPageInspectorEmulationAgentGtk.cpp + create mode 100644 Source/WebKit/UIProcess/gtk/WebPageInspectorInputAgentGtk.cpp + create mode 100644 Source/WebKit/UIProcess/gtk/WebPageInspectorTargetProxyGtk.cpp + create mode 100644 Source/WebKit/UIProcess/mac/InspectorBrowserAgentClientMac.h + create mode 100644 Source/WebKit/UIProcess/mac/InspectorBrowserAgentClientMac.mm + create mode 100644 Source/WebKit/UIProcess/mac/WebPageInspectorEmulationAgentMac.mm + create mode 100644 Source/WebKit/UIProcess/mac/WebPageInspectorInputAgentMac.mm + create mode 100644 Source/WebKit/UIProcess/mac/WebPageInspectorTargetProxyMac.mm + create mode 100644 Source/WebKit/UIProcess/wpe/WebPageInspectorEmulationAgentWPE.cpp + create mode 100644 Source/WebKit/UIProcess/wpe/WebPageInspectorInputAgentWPE.cpp + create mode 100644 Source/WebKit/UIProcess/wpe/WebPageInspectorTargetProxyWPE.cpp + +diff --git a/Source/JavaScriptCore/CMakeLists.txt b/Source/JavaScriptCore/CMakeLists.txt +index f25ff61db99..a7085df58f8 100644 +--- a/Source/JavaScriptCore/CMakeLists.txt ++++ b/Source/JavaScriptCore/CMakeLists.txt +@@ -1142,6 +1142,7 @@ set(JavaScriptCore_INSPECTOR_DOMAINS + ${JAVASCRIPTCORE_DIR}/inspector/protocol/Animation.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/ApplicationCache.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/Audit.json ++ ${JAVASCRIPTCORE_DIR}/inspector/protocol/Browser.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/CSS.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/Canvas.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/Console.json +@@ -1150,8 +1151,10 @@ set(JavaScriptCore_INSPECTOR_DOMAINS + ${JAVASCRIPTCORE_DIR}/inspector/protocol/DOMStorage.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/Database.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/Debugger.json ++ ${JAVASCRIPTCORE_DIR}/inspector/protocol/Emulation.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/GenericTypes.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/Heap.json ++ ${JAVASCRIPTCORE_DIR}/inspector/protocol/Input.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/Inspector.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/LayerTree.json + ${JAVASCRIPTCORE_DIR}/inspector/protocol/Network.json +diff --git a/Source/JavaScriptCore/DerivedSources.make b/Source/JavaScriptCore/DerivedSources.make +index f59212ff01c..c1fb126e017 100644 +--- a/Source/JavaScriptCore/DerivedSources.make ++++ b/Source/JavaScriptCore/DerivedSources.make +@@ -238,6 +238,7 @@ INSPECTOR_DOMAINS := \ + $(JavaScriptCore)/inspector/protocol/Animation.json \ + $(JavaScriptCore)/inspector/protocol/ApplicationCache.json \ + $(JavaScriptCore)/inspector/protocol/Audit.json \ ++ $(JavaScriptCore)/inspector/protocol/Browser.json \ + $(JavaScriptCore)/inspector/protocol/CSS.json \ + $(JavaScriptCore)/inspector/protocol/Canvas.json \ + $(JavaScriptCore)/inspector/protocol/Console.json \ +@@ -246,8 +247,10 @@ INSPECTOR_DOMAINS := \ + $(JavaScriptCore)/inspector/protocol/DOMStorage.json \ + $(JavaScriptCore)/inspector/protocol/Database.json \ + $(JavaScriptCore)/inspector/protocol/Debugger.json \ ++ $(JavaScriptCore)/inspector/protocol/Emulation.json \ + $(JavaScriptCore)/inspector/protocol/GenericTypes.json \ + $(JavaScriptCore)/inspector/protocol/Heap.json \ ++ $(JavaScriptCore)/inspector/protocol/Input.json \ + $(JavaScriptCore)/inspector/protocol/Inspector.json \ + $(JavaScriptCore)/inspector/protocol/LayerTree.json \ + $(JavaScriptCore)/inspector/protocol/Network.json \ +diff --git a/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.cpp b/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.cpp +index 038cb646d31..8a01d7679bf 100644 +--- a/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.cpp ++++ b/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.cpp +@@ -102,7 +102,7 @@ void BackendDispatcher::registerDispatcherForDomain(const String& domain, Supple + m_dispatchers.set(domain, dispatcher); + } + +-void BackendDispatcher::dispatch(const String& message) ++BackendDispatcher::DispatchResult BackendDispatcher::dispatch(const String& message, Mode mode) + { + Ref protect(*this); + +@@ -120,26 +120,26 @@ void BackendDispatcher::dispatch(const String& message) + if (!JSON::Value::parseJSON(message, parsedMessage)) { + reportProtocolError(ParseError, "Message must be in JSON format"_s); + sendPendingErrors(); +- return; ++ return DispatchResult::Finished; + } + + if (!parsedMessage->asObject(messageObject)) { + reportProtocolError(InvalidRequest, "Message must be a JSONified object"_s); + sendPendingErrors(); +- return; ++ return DispatchResult::Finished; + } + + RefPtr requestIdValue; + if (!messageObject->getValue("id"_s, requestIdValue)) { + reportProtocolError(InvalidRequest, "'id' property was not found"_s); + sendPendingErrors(); +- return; ++ return DispatchResult::Finished; + } + + if (!requestIdValue->asInteger(requestId)) { + reportProtocolError(InvalidRequest, "The type of 'id' property must be integer"_s); + sendPendingErrors(); +- return; ++ return DispatchResult::Finished; + } + } + +@@ -151,29 +151,31 @@ void BackendDispatcher::dispatch(const String& message) + if (!messageObject->getValue("method"_s, methodValue)) { + reportProtocolError(InvalidRequest, "'method' property wasn't found"_s); + sendPendingErrors(); +- return; ++ return DispatchResult::Finished; + } + + String methodString; + if (!methodValue->asString(methodString)) { + reportProtocolError(InvalidRequest, "The type of 'method' property must be string"_s); + sendPendingErrors(); +- return; ++ return DispatchResult::Finished; + } + + Vector domainAndMethod = methodString.splitAllowingEmptyEntries('.'); + if (domainAndMethod.size() != 2 || !domainAndMethod[0].length() || !domainAndMethod[1].length()) { + reportProtocolError(InvalidRequest, "The 'method' property was formatted incorrectly. It should be 'Domain.method'"_s); + sendPendingErrors(); +- return; ++ return DispatchResult::Finished; + } + + String domain = domainAndMethod[0]; + SupplementalBackendDispatcher* domainDispatcher = m_dispatchers.get(domain); + if (!domainDispatcher) { ++ if (mode == Mode::ContinueIfDomainIsMissing) ++ return DispatchResult::Continue; + reportProtocolError(MethodNotFound, "'" + domain + "' domain was not found"); + sendPendingErrors(); +- return; ++ return DispatchResult::Finished; + } + + String method = domainAndMethod[1]; +@@ -182,6 +184,7 @@ void BackendDispatcher::dispatch(const String& message) + if (m_protocolErrors.size()) + sendPendingErrors(); + } ++ return DispatchResult::Finished; + } + + // FIXME: remove this function when legacy InspectorObject symbols are no longer needed . +diff --git a/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.h b/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.h +index 95d9d81188e..6f96f174dff 100644 +--- a/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.h ++++ b/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.h +@@ -82,7 +82,10 @@ public: + }; + + void registerDispatcherForDomain(const String& domain, SupplementalBackendDispatcher*); +- void dispatch(const String& message); ++ ++ enum class DispatchResult { Finished, Continue }; ++ enum class Mode { FailIfDomainIsMissing, ContinueIfDomainIsMissing }; ++ DispatchResult dispatch(const String& message, Mode mode = Mode::FailIfDomainIsMissing); + + // Note that 'unused' is a workaround so the compiler can pick the right sendResponse based on arity. + // When is fixed or this class is renamed for the JSON::Object case, +diff --git a/Source/JavaScriptCore/inspector/InspectorTarget.h b/Source/JavaScriptCore/inspector/InspectorTarget.h +index a9f04b8a0c9..045b9ecd39a 100644 +--- a/Source/JavaScriptCore/inspector/InspectorTarget.h ++++ b/Source/JavaScriptCore/inspector/InspectorTarget.h +@@ -45,6 +45,7 @@ public: + // State. + virtual String identifier() const = 0; + virtual InspectorTargetType type() const = 0; ++ virtual String url() const = 0; + + virtual bool isProvisional() const { return false; } + +@@ -52,6 +53,8 @@ public: + virtual void connect(FrontendChannel::ConnectionType) = 0; + virtual void disconnect() = 0; + virtual void sendMessageToTargetBackend(const String&) = 0; ++ virtual void activate(String& error) { error = "Target cannot be activated"; } ++ virtual void close(String& error) { error = "Target cannot be closed"; } + }; + + } // namespace Inspector +diff --git a/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.cpp b/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.cpp +index 1177090fc18..764b62c727c 100644 +--- a/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.cpp ++++ b/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.cpp +@@ -30,11 +30,12 @@ + + namespace Inspector { + +-InspectorTargetAgent::InspectorTargetAgent(FrontendRouter& frontendRouter, BackendDispatcher& backendDispatcher) ++InspectorTargetAgent::InspectorTargetAgent(FrontendRouter& frontendRouter, BackendDispatcher& backendDispatcher, const String& browserContextID) + : InspectorAgentBase("Target"_s) + , m_router(frontendRouter) + , m_frontendDispatcher(makeUnique(frontendRouter)) + , m_backendDispatcher(TargetBackendDispatcher::create(backendDispatcher, this)) ++ , m_browserContextID(browserContextID) + { + } + +@@ -65,6 +66,28 @@ void InspectorTargetAgent::sendMessageToTarget(ErrorString& errorString, const S + target->sendMessageToTargetBackend(message); + } + ++void InspectorTargetAgent::activate(ErrorString& errorString, const String& targetId) ++{ ++ InspectorTarget* target = m_targets.get(targetId); ++ if (!target) { ++ errorString = "Missing target for given targetId"_s; ++ return; ++ } ++ ++ target->activate(errorString); ++} ++ ++void InspectorTargetAgent::close(ErrorString& errorString, const String& targetId) ++{ ++ InspectorTarget* target = m_targets.get(targetId); ++ if (!target) { ++ errorString = "Missing target for given targetId"_s; ++ return; ++ } ++ ++ target->close(errorString); ++} ++ + void InspectorTargetAgent::sendMessageFromTargetToFrontend(const String& targetId, const String& message) + { + ASSERT_WITH_MESSAGE(m_targets.get(targetId), "Sending a message from an untracked target to the frontend."); +@@ -87,14 +110,17 @@ static Protocol::Target::TargetInfo::Type targetTypeToProtocolType(InspectorTarg + return Protocol::Target::TargetInfo::Type::Page; + } + +-static Ref buildTargetInfoObject(const InspectorTarget& target) ++static Ref buildTargetInfoObject(const InspectorTarget& target, const String& browserContextID) + { + auto result = Protocol::Target::TargetInfo::create() + .setTargetId(target.identifier()) + .setType(targetTypeToProtocolType(target.type())) ++ .setUrl(target.url()) + .release(); + if (target.isProvisional()) + result->setIsProvisional(true); ++ if (!browserContextID.isEmpty()) ++ result->setBrowserContextId(browserContextID); + return result; + } + +@@ -108,7 +134,7 @@ void InspectorTargetAgent::targetCreated(InspectorTarget& target) + + target.connect(connectionType()); + +- m_frontendDispatcher->targetCreated(buildTargetInfoObject(target)); ++ m_frontendDispatcher->targetCreated(buildTargetInfoObject(target, m_browserContextID)); + } + + void InspectorTargetAgent::targetDestroyed(InspectorTarget& target) +@@ -135,6 +161,18 @@ void InspectorTargetAgent::didCommitProvisionalTarget(const String& oldTargetID, + m_frontendDispatcher->didCommitProvisionalTarget(oldTargetID, committedTargetID); + } + ++void InspectorTargetAgent::ensureConnected(const String& targetID) ++{ ++ if (!m_isConnected) ++ return; ++ ++ auto* target = m_targets.get(targetID); ++ if (!target) ++ return; ++ ++ target->connect(connectionType()); ++} ++ + FrontendChannel::ConnectionType InspectorTargetAgent::connectionType() const + { + return m_router.hasLocalFrontend() ? Inspector::FrontendChannel::ConnectionType::Local : Inspector::FrontendChannel::ConnectionType::Remote; +@@ -144,7 +182,7 @@ void InspectorTargetAgent::connectToTargets() + { + for (InspectorTarget* target : m_targets.values()) { + target->connect(connectionType()); +- m_frontendDispatcher->targetCreated(buildTargetInfoObject(*target)); ++ m_frontendDispatcher->targetCreated(buildTargetInfoObject(*target, m_browserContextID)); + } + } + +diff --git a/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.h b/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.h +index 38cb318986b..4287e05e559 100644 +--- a/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.h ++++ b/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.h +@@ -41,7 +41,7 @@ class JS_EXPORT_PRIVATE InspectorTargetAgent : public InspectorAgentBase, public + WTF_MAKE_NONCOPYABLE(InspectorTargetAgent); + WTF_MAKE_FAST_ALLOCATED; + public: +- InspectorTargetAgent(FrontendRouter&, BackendDispatcher&); ++ InspectorTargetAgent(FrontendRouter&, BackendDispatcher&, const String& browserContextID); + ~InspectorTargetAgent() override; + + // InspectorAgentBase +@@ -50,11 +50,14 @@ public: + + // TargetBackendDispatcherHandler + void sendMessageToTarget(ErrorString&, const String& targetId, const String& message) final; ++ void activate(ErrorString&, const String& targetId) override; ++ void close(ErrorString&, const String& targetId) override; + + // Target lifecycle. + void targetCreated(InspectorTarget&); + void targetDestroyed(InspectorTarget&); + void didCommitProvisionalTarget(const String& oldTargetID, const String& committedTargetID); ++ void ensureConnected(const String& targetID); + + // Target messages. + void sendMessageFromTargetToFrontend(const String& targetId, const String& message); +@@ -68,6 +71,7 @@ private: + Inspector::FrontendRouter& m_router; + std::unique_ptr m_frontendDispatcher; + Ref m_backendDispatcher; ++ const String m_browserContextID; + HashMap m_targets; + bool m_isConnected { false }; + }; +diff --git a/Source/JavaScriptCore/inspector/protocol/Browser.json b/Source/JavaScriptCore/inspector/protocol/Browser.json +new file mode 100644 +index 00000000000..bed4a3bfe6f +--- /dev/null ++++ b/Source/JavaScriptCore/inspector/protocol/Browser.json +@@ -0,0 +1,40 @@ ++{ ++ "domain": "Browser", ++ "availability": ["web"], ++ "types": [ ++ { ++ "id": "ContextID", ++ "type": "string", ++ "description": "Id of Browser context." ++ } ++ ], ++ "commands": [ ++ { ++ "name": "close", ++ "description": "Close browser." ++ }, ++ { ++ "name": "createContext", ++ "description": "Creates new ephemeral browser context.", ++ "returns": [ ++ { "name": "browserContextId", "$ref": "ContextID", "description": "Unique identifier of the context." } ++ ] ++ }, ++ { ++ "name": "deleteContext", ++ "description": "Deletes browser context previously created with createContect. The command will automatically close all pages that use the context.", ++ "parameters": [ ++ { "name": "browserContextId", "$ref": "ContextID", "description": "Identifier of the context to delete." } ++ ] ++ }, ++ { ++ "name": "createPage", ++ "parameters": [ ++ { "name": "browserContextId", "$ref": "ContextID", "optional": true, "description": "JSON Inspector Protocol message (command) to be dispatched on the backend." } ++ ], ++ "returns": [ ++ { "name": "targetId", "type": "string", "description": "Unique identifier for the page target." } ++ ] ++ } ++ ] ++} +diff --git a/Source/JavaScriptCore/inspector/protocol/DOM.json b/Source/JavaScriptCore/inspector/protocol/DOM.json +index 38cb48bedf2..285027ae5d7 100644 +--- a/Source/JavaScriptCore/inspector/protocol/DOM.json ++++ b/Source/JavaScriptCore/inspector/protocol/DOM.json +@@ -542,6 +542,27 @@ + "parameters": [ + { "name": "allow", "type": "boolean" } + ] ++ }, ++ { ++ "name": "getContentQuads", ++ "description": "Returns quads that describe node position on the page. This method\nmight return multiple quads for inline nodes.", ++ "parameters": [ ++ { ++ "name": "objectId", ++ "description": "JavaScript object id of the node wrapper.", ++ "$ref": "Runtime.RemoteObjectId" ++ } ++ ], ++ "returns": [ ++ { ++ "name": "quads", ++ "description": "Quads that describe node layout relative to viewport.", ++ "type": "array", ++ "items": { ++ "$ref": "Quad" ++ } ++ } ++ ] + } + ], + "events": [ +diff --git a/Source/JavaScriptCore/inspector/protocol/Emulation.json b/Source/JavaScriptCore/inspector/protocol/Emulation.json +new file mode 100644 +index 00000000000..168e3f2b93d +--- /dev/null ++++ b/Source/JavaScriptCore/inspector/protocol/Emulation.json +@@ -0,0 +1,14 @@ ++{ ++ "domain": "Emulation", ++ "availability": ["web"], ++ "commands": [ ++ { ++ "name": "setDeviceMetricsOverride", ++ "description": "Overrides device metrics with provided values.", ++ "parameters": [ ++ { "name": "width", "type": "integer" }, ++ { "name": "height", "type": "integer" } ++ ] ++ } ++ ] ++} +diff --git a/Source/JavaScriptCore/inspector/protocol/Input.json b/Source/JavaScriptCore/inspector/protocol/Input.json +new file mode 100644 +index 00000000000..79bbe73b0df +--- /dev/null ++++ b/Source/JavaScriptCore/inspector/protocol/Input.json +@@ -0,0 +1,160 @@ ++{ ++ "domain": "Input", ++ "availability": ["web"], ++ "types": [ ++ { ++ "id": "TimeSinceEpoch", ++ "description": "UTC time in seconds, counted from January 1, 1970.", ++ "type": "number" ++ } ++ ], ++ "commands": [ ++ { ++ "name": "goBack", ++ "description": "FIXME: move this to Page domain." ++ }, ++ { ++ "name": "dispatchKeyEvent", ++ "description": "Dispatches a key event to the page.", ++ "async": true, ++ "parameters": [ ++ { ++ "name": "type", ++ "description": "Type of the key event.", ++ "type": "string", ++ "enum": [ ++ "keyDown", ++ "keyUp" ++ ] ++ }, ++ { ++ "name": "modifiers", ++ "description": "Bit field representing pressed modifier keys. (default: 0).", ++ "optional": true, ++ "type": "integer" ++ }, ++ { ++ "name": "text", ++ "description": "Text as generated by processing a virtual key code with a keyboard layout. Not needed for\nfor `keyUp` and `rawKeyDown` events (default: \"\")", ++ "optional": true, ++ "type": "string" ++ }, ++ { ++ "name": "unmodifiedText", ++ "description": "Text that would have been generated by the keyboard if no modifiers were pressed (except for\nshift). Useful for shortcut (accelerator) key handling (default: \"\").", ++ "optional": true, ++ "type": "string" ++ }, ++ { ++ "name": "code", ++ "description": "Unique DOM defined string value for each physical key (e.g., 'KeyA') (default: \"\").", ++ "optional": true, ++ "type": "string" ++ }, ++ { ++ "name": "key", ++ "description": "Unique DOM defined string value describing the meaning of the key in the context of active\nmodifiers, keyboard layout, etc (e.g., 'AltGr') (default: \"\").", ++ "optional": true, ++ "type": "string" ++ }, ++ { ++ "name": "windowsVirtualKeyCode", ++ "description": "Windows virtual key code (default: 0).", ++ "optional": true, ++ "type": "integer" ++ }, ++ { ++ "name": "nativeVirtualKeyCode", ++ "description": "Native virtual key code (default: 0).", ++ "optional": true, ++ "type": "integer" ++ }, ++ { ++ "name": "autoRepeat", ++ "description": "Whether the event was generated from auto repeat (default: false).", ++ "optional": true, ++ "type": "boolean" ++ }, ++ { ++ "name": "isKeypad", ++ "description": "Whether the event was generated from the keypad (default: false).", ++ "optional": true, ++ "type": "boolean" ++ }, ++ { ++ "name": "isSystemKey", ++ "description": "Whether the event was a system key event (default: false).", ++ "optional": true, ++ "type": "boolean" ++ } ++ ] ++ }, ++ { ++ "name": "dispatchMouseEvent", ++ "description": "Dispatches a mouse event to the page.", ++ "async": true, ++ "parameters": [ ++ { ++ "name": "type", ++ "description": "Type of the mouse event.", ++ "type": "string", ++ "enum": [ "move", "down", "up", "wheel"] ++ }, ++ { ++ "name": "x", ++ "description": "X coordinate of the event relative to the main frame's viewport in CSS pixels.", ++ "type": "integer" ++ }, ++ { ++ "name": "y", ++ "description": "Y coordinate of the event relative to the main frame's viewport in CSS pixels. 0 refers to\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.", ++ "type": "integer" ++ }, ++ { ++ "name": "modifiers", ++ "description": "Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\n(default: 0).", ++ "optional": true, ++ "type": "integer" ++ }, ++ { ++ "name": "button", ++ "description": "Mouse button (default: \"none\").", ++ "optional": true, ++ "type": "string", ++ "enum": [ ++ "none", ++ "left", ++ "middle", ++ "right", ++ "back", ++ "forward" ++ ] ++ }, ++ { ++ "name": "buttons", ++ "description": "A number indicating which buttons are pressed on the mouse when a mouse event is triggered.\nLeft=1, Right=2, Middle=4, Back=8, Forward=16, None=0.", ++ "optional": true, ++ "type": "integer" ++ }, ++ { ++ "name": "clickCount", ++ "description": "Number of times the mouse button was clicked (default: 0).", ++ "optional": true, ++ "type": "integer" ++ }, ++ { ++ "name": "deltaX", ++ "description": "X delta in CSS pixels for mouse wheel event (default: 0).", ++ "optional": true, ++ "type": "integer" ++ }, ++ { ++ "name": "deltaY", ++ "description": "Y delta in CSS pixels for mouse wheel event (default: 0).", ++ "optional": true, ++ "type": "integer" ++ } ++ ] ++ } ++ ] ++} +diff --git a/Source/JavaScriptCore/inspector/protocol/Page.json b/Source/JavaScriptCore/inspector/protocol/Page.json +index 367d1f235a8..62321e6c893 100644 +--- a/Source/JavaScriptCore/inspector/protocol/Page.json ++++ b/Source/JavaScriptCore/inspector/protocol/Page.json +@@ -131,7 +131,8 @@ + "name": "navigate", + "description": "Navigates current page to the given URL.", + "parameters": [ +- { "name": "url", "type": "string", "description": "URL to navigate the page to." } ++ { "name": "url", "type": "string", "description": "URL to navigate the page to." }, ++ { "name": "frameId", "$ref": "Network.FrameId", "optional": true, "description": "Id of the frame to navigate."} + ] + }, + { +@@ -347,6 +348,21 @@ + ] + }, + { ++ "name": "navigatedWithinDocument", ++ "description": "Fired when same-document navigation happens, e.g. due to history API usage or anchor navigation.", ++ "parameters": [ ++ { ++ "name": "frameId", ++ "description": "Id of the frame.", ++ "$ref": "Network.FrameId" ++ }, ++ { ++ "name": "url", ++ "description": "Frame's new url.", ++ "type": "string" ++ } ++ ] ++ }, { + "name": "defaultAppearanceDidChange", + "description": "Fired when page's default appearance changes, even if there is a forced appearance.", + "parameters": [ +diff --git a/Source/JavaScriptCore/inspector/protocol/Target.json b/Source/JavaScriptCore/inspector/protocol/Target.json +index 240cd42e67e..f635c67ef3f 100644 +--- a/Source/JavaScriptCore/inspector/protocol/Target.json ++++ b/Source/JavaScriptCore/inspector/protocol/Target.json +@@ -10,7 +10,9 @@ + "properties": [ + { "name": "targetId", "type": "string", "description": "Unique identifier for the target." }, + { "name": "type", "type": "string", "enum": ["page", "service-worker", "worker"] }, +- { "name": "isProvisional", "type": "boolean", "optional": true, "description": "True value indicates that this is a provisional page target i.e. Such target may be created when current page starts cross-origin navigation. Eventually each provisional target is either committed and swaps with the current target or gets destroyed, e.g. in case of load request failure." } ++ { "name": "url", "type": "string" }, ++ { "name": "isProvisional", "type": "boolean", "optional": true, "description": "True value indicates that this is a provisional page target i.e. Such target may be created when current page starts cross-origin navigation. Eventually each provisional target is either committed and swaps with the current target or gets destroyed, e.g. in case of load request failure." }, ++ { "name": "browserContextId", "$ref": "Browser.ContextID", "optional": true } + ] + } + ], +@@ -22,6 +24,20 @@ + { "name": "targetId", "type": "string" }, + { "name": "message", "type": "string", "description": "JSON Inspector Protocol message (command) to be dispatched on the backend." } + ] ++ }, ++ { ++ "name": "activate", ++ "description": "Reveals the target on screen.", ++ "parameters": [ ++ { "name": "targetId", "type": "string" } ++ ] ++ }, ++ { ++ "name": "close", ++ "description": "Closes the target.", ++ "parameters": [ ++ { "name": "targetId", "type": "string" } ++ ] + } + ], + "events": [ +diff --git a/Source/WebCore/inspector/InspectorInstrumentation.cpp b/Source/WebCore/inspector/InspectorInstrumentation.cpp +index 4b7bec1b460..97fb3543d40 100644 +--- a/Source/WebCore/inspector/InspectorInstrumentation.cpp ++++ b/Source/WebCore/inspector/InspectorInstrumentation.cpp +@@ -782,6 +782,12 @@ void InspectorInstrumentation::frameClearedScheduledNavigationImpl(Instrumenting + inspectorPageAgent->frameClearedScheduledNavigation(frame); + } + ++void InspectorInstrumentation::didNavigateWithinPageImpl(InstrumentingAgents& instrumentingAgents, Frame& frame) ++{ ++ if (InspectorPageAgent* inspectorPageAgent = instrumentingAgents.inspectorPageAgent()) ++ inspectorPageAgent->didNavigateWithinPage(frame); ++} ++ + void InspectorInstrumentation::defaultAppearanceDidChangeImpl(InstrumentingAgents& instrumentingAgents, bool useDarkAppearance) + { + if (InspectorPageAgent* inspectorPageAgent = instrumentingAgents.inspectorPageAgent()) +diff --git a/Source/WebCore/inspector/InspectorInstrumentation.h b/Source/WebCore/inspector/InspectorInstrumentation.h +index 6698431f316..40dd67f43e9 100644 +--- a/Source/WebCore/inspector/InspectorInstrumentation.h ++++ b/Source/WebCore/inspector/InspectorInstrumentation.h +@@ -228,6 +228,7 @@ public: + static void frameStoppedLoading(Frame&); + static void frameScheduledNavigation(Frame&, Seconds delay); + static void frameClearedScheduledNavigation(Frame&); ++ static void didNavigateWithinPage(Frame&); + static void defaultAppearanceDidChange(Page&, bool useDarkAppearance); + static void willDestroyCachedResource(CachedResource&); + +@@ -428,6 +429,7 @@ private: + static void frameStoppedLoadingImpl(InstrumentingAgents&, Frame&); + static void frameScheduledNavigationImpl(InstrumentingAgents&, Frame&, Seconds delay); + static void frameClearedScheduledNavigationImpl(InstrumentingAgents&, Frame&); ++ static void didNavigateWithinPageImpl(InstrumentingAgents&, Frame&); + static void defaultAppearanceDidChangeImpl(InstrumentingAgents&, bool useDarkAppearance); + static void willDestroyCachedResourceImpl(CachedResource&); + +@@ -1219,6 +1221,13 @@ inline void InspectorInstrumentation::frameClearedScheduledNavigation(Frame& fra + frameClearedScheduledNavigationImpl(*instrumentingAgents, frame); + } + ++inline void InspectorInstrumentation::didNavigateWithinPage(Frame& frame) ++{ ++ FAST_RETURN_IF_NO_FRONTENDS(void()); ++ if (InstrumentingAgents* instrumentingAgents = instrumentingAgentsForFrame(frame)) ++ didNavigateWithinPageImpl(*instrumentingAgents, frame); ++} ++ + inline void InspectorInstrumentation::defaultAppearanceDidChange(Page& page, bool useDarkAppearance) + { + FAST_RETURN_IF_NO_FRONTENDS(void()); +diff --git a/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp b/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp +index 19bd04b805c..21745502b3e 100644 +--- a/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp ++++ b/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp +@@ -61,6 +61,7 @@ + #include "FrameTree.h" + #include "FrameView.h" + #include "FullscreenManager.h" ++#include "FloatQuad.h" + #include "HTMLElement.h" + #include "HTMLFrameOwnerElement.h" + #include "HTMLMediaElement.h" +@@ -89,6 +90,7 @@ + #include "Page.h" + #include "Pasteboard.h" + #include "PseudoElement.h" ++#include "RenderObject.h" + #include "RenderStyle.h" + #include "RenderStyleConstants.h" + #include "ScriptState.h" +@@ -1417,6 +1419,61 @@ void InspectorDOMAgent::setInspectedNode(ErrorString& errorString, int nodeId) + m_suppressEventListenerChangedEvent = false; + } + ++static void frameQuadToViewport(const FrameView* containingView, FloatQuad& quad) ++{ ++ quad.setP1(containingView->contentsToRootView(quad.p1())); ++ quad.setP2(containingView->contentsToRootView(quad.p2())); ++ quad.setP3(containingView->contentsToRootView(quad.p3())); ++ quad.setP4(containingView->contentsToRootView(quad.p4())); ++} ++ ++static RefPtr buildObjectForQuad(const FloatQuad& quad) ++{ ++ auto result = Inspector::Protocol::DOM::Quad::create(); ++ result->addItem(quad.p1().x()); ++ result->addItem(quad.p1().y()); ++ result->addItem(quad.p2().x()); ++ result->addItem(quad.p2().y()); ++ result->addItem(quad.p3().x()); ++ result->addItem(quad.p3().y()); ++ result->addItem(quad.p4().x()); ++ result->addItem(quad.p4().y()); ++ return result; ++} ++ ++static RefPtr> buildArrayOfQuads(const Vector& quads) ++{ ++ auto result = JSON::ArrayOf::create(); ++ for (const auto& quad : quads) ++ result->addItem(buildObjectForQuad(quad)); ++ return result; ++} ++ ++void InspectorDOMAgent::getContentQuads(ErrorString& error, const String& objectId, RefPtr>& out_quads) ++{ ++ Node* node = nodeForObjectId(objectId); ++ if (!node) { ++ error = "Node not found"; ++ return; ++ } ++ RenderObject* renderer = node->renderer(); ++ if (!renderer) { ++ error = "Node doesn't have renderer"; ++ return; ++ } ++ Frame* containingFrame = renderer->document().frame(); ++ if (!containingFrame) { ++ error = "No containing frame"; ++ return; ++ } ++ FrameView* containingView = containingFrame->view(); ++ Vector quads; ++ renderer->absoluteQuads(quads); ++ for (auto& quad : quads) ++ frameQuadToViewport(containingView, quad); ++ out_quads = buildArrayOfQuads(quads); ++} ++ + void InspectorDOMAgent::resolveNode(ErrorString& errorString, int nodeId, const String* objectGroup, RefPtr& result) + { + String objectGroupName = objectGroup ? *objectGroup : emptyString(); +diff --git a/Source/WebCore/inspector/agents/InspectorDOMAgent.h b/Source/WebCore/inspector/agents/InspectorDOMAgent.h +index 51639abeb84..fbb7773978d 100644 +--- a/Source/WebCore/inspector/agents/InspectorDOMAgent.h ++++ b/Source/WebCore/inspector/agents/InspectorDOMAgent.h +@@ -148,6 +148,7 @@ public: + void focus(ErrorString&, int nodeId) override; + void setInspectedNode(ErrorString&, int nodeId) override; + void setAllowEditingUserAgentShadowTrees(ErrorString&, bool allow) final; ++ void getContentQuads(ErrorString&, const String& objectId, RefPtr>& out_quads) override; + + // InspectorInstrumentation + int identifierForNode(Node&); +diff --git a/Source/WebCore/inspector/agents/InspectorPageAgent.cpp b/Source/WebCore/inspector/agents/InspectorPageAgent.cpp +index f2e228b7f74..77ccbe29ea3 100644 +--- a/Source/WebCore/inspector/agents/InspectorPageAgent.cpp ++++ b/Source/WebCore/inspector/agents/InspectorPageAgent.cpp +@@ -412,14 +412,16 @@ void InspectorPageAgent::reload(ErrorString&, const bool* optionalReloadFromOrig + m_inspectedPage.mainFrame().loader().reload(reloadOptions); + } + +-void InspectorPageAgent::navigate(ErrorString&, const String& url) ++void InspectorPageAgent::navigate(ErrorString& errorString, const String& url, const String* frameId) + { + UserGestureIndicator indicator { ProcessingUserGesture }; +- Frame& frame = m_inspectedPage.mainFrame(); ++ Frame* frame = frameId ? assertFrame(errorString, *frameId) : &m_inspectedPage.mainFrame(); ++ if (!frame) ++ return; + +- ResourceRequest resourceRequest { frame.document()->completeURL(url) }; +- FrameLoadRequest frameLoadRequest { *frame.document(), frame.document()->securityOrigin(), resourceRequest, "_self"_s, LockHistory::No, LockBackForwardList::No, MaybeSendReferrer, AllowNavigationToInvalidURL::No, NewFrameOpenerPolicy::Allow, ShouldOpenExternalURLsPolicy::ShouldNotAllow, InitiatedByMainFrame::Unknown }; +- frame.loader().changeLocation(WTFMove(frameLoadRequest)); ++ ResourceRequest resourceRequest { frame->document()->completeURL(url) }; ++ FrameLoadRequest frameLoadRequest { *frame->document(), frame->document()->securityOrigin(), resourceRequest, "_self"_s, LockHistory::No, LockBackForwardList::No, MaybeSendReferrer, AllowNavigationToInvalidURL::No, NewFrameOpenerPolicy::Allow, ShouldOpenExternalURLsPolicy::ShouldNotAllow, InitiatedByMainFrame::Unknown }; ++ frame->loader().changeLocation(WTFMove(frameLoadRequest)); + } + + void InspectorPageAgent::overrideUserAgent(ErrorString&, const String* value) +@@ -761,6 +763,12 @@ void InspectorPageAgent::frameClearedScheduledNavigation(Frame& frame) + m_frontendDispatcher->frameClearedScheduledNavigation(frameId(&frame)); + } + ++void InspectorPageAgent::didNavigateWithinPage(Frame& frame) ++{ ++ String url = frame.document()->url().string(); ++ m_frontendDispatcher->navigatedWithinDocument(frameId(&frame), url); ++} ++ + void InspectorPageAgent::defaultAppearanceDidChange(bool useDarkAppearance) + { + m_frontendDispatcher->defaultAppearanceDidChange(useDarkAppearance ? Inspector::Protocol::Page::Appearance::Dark : Inspector::Protocol::Page::Appearance::Light); +diff --git a/Source/WebCore/inspector/agents/InspectorPageAgent.h b/Source/WebCore/inspector/agents/InspectorPageAgent.h +index 4fd8c0b1016..78af692dc09 100644 +--- a/Source/WebCore/inspector/agents/InspectorPageAgent.h ++++ b/Source/WebCore/inspector/agents/InspectorPageAgent.h +@@ -96,7 +96,7 @@ public: + void enable(ErrorString&) override; + void disable(ErrorString&) override; + void reload(ErrorString&, const bool* optionalReloadFromOrigin, const bool* optionalRevalidateAllResources) override; +- void navigate(ErrorString&, const String& url) override; ++ void navigate(ErrorString&, const String& url, const String* frameId) override; + void overrideUserAgent(ErrorString&, const String* value) override; + void overrideSetting(ErrorString&, const String& setting, const bool* value) override; + void getCookies(ErrorString&, RefPtr>& cookies) override; +@@ -126,6 +126,7 @@ public: + void frameStoppedLoading(Frame&); + void frameScheduledNavigation(Frame&, Seconds delay); + void frameClearedScheduledNavigation(Frame&); ++ void didNavigateWithinPage(Frame&); + void defaultAppearanceDidChange(bool useDarkAppearance); + void applyUserAgentOverride(String&); + void applyEmulatedMedia(String&); +diff --git a/Source/WebCore/loader/FrameLoader.cpp b/Source/WebCore/loader/FrameLoader.cpp +index 26246f7deb6..cf215bed32b 100644 +--- a/Source/WebCore/loader/FrameLoader.cpp ++++ b/Source/WebCore/loader/FrameLoader.cpp +@@ -1179,6 +1179,7 @@ void FrameLoader::loadInSameDocument(const URL& url, SerializedScriptValue* stat + } + + m_client.dispatchDidNavigateWithinPage(); ++ InspectorInstrumentation::didNavigateWithinPage(m_frame); + + m_frame.document()->statePopped(stateObject ? Ref { *stateObject } : SerializedScriptValue::nullValue()); + m_client.dispatchDidPopStateWithinPage(); +diff --git a/Source/WebCore/page/History.cpp b/Source/WebCore/page/History.cpp +index 9c58b06f4c4..3d624733c36 100644 +--- a/Source/WebCore/page/History.cpp ++++ b/Source/WebCore/page/History.cpp +@@ -259,6 +259,7 @@ ExceptionOr History::stateObjectAdded(RefPtr&& data + + if (!urlString.isEmpty()) + frame->document()->updateURLForPushOrReplaceState(fullURL); ++ InspectorInstrumentation::didNavigateWithinPage(*frame); + + if (stateObjectType == StateObjectType::Push) { + frame->loader().history().pushState(WTFMove(data), title, fullURL.string()); +diff --git a/Source/WebCore/platform/PlatformKeyboardEvent.h b/Source/WebCore/platform/PlatformKeyboardEvent.h +index 4aaf5bde32b..e9c047d4f26 100644 +--- a/Source/WebCore/platform/PlatformKeyboardEvent.h ++++ b/Source/WebCore/platform/PlatformKeyboardEvent.h +@@ -147,6 +147,7 @@ namespace WebCore { + static String keyCodeForHardwareKeyCode(unsigned); + static String keyIdentifierForGdkKeyCode(unsigned); + static int windowsKeyCodeForGdkKeyCode(unsigned); ++ static unsigned gdkKeyCodeForWindowsKeyCode(int); + static String singleCharacterString(unsigned); + static bool modifiersContainCapsLock(unsigned); + #endif +@@ -156,6 +157,7 @@ namespace WebCore { + static String keyCodeForHardwareKeyCode(unsigned); + static String keyIdentifierForWPEKeyCode(unsigned); + static int windowsKeyCodeForWPEKeyCode(unsigned); ++ static unsigned WPEKeyCodeForWindowsKeyCode(int); + static String singleCharacterString(unsigned); + #endif + +diff --git a/Source/WebCore/platform/gtk/PlatformKeyboardEventGtk.cpp b/Source/WebCore/platform/gtk/PlatformKeyboardEventGtk.cpp +index 356b09f2fba..8f0c19b6031 100644 +--- a/Source/WebCore/platform/gtk/PlatformKeyboardEventGtk.cpp ++++ b/Source/WebCore/platform/gtk/PlatformKeyboardEventGtk.cpp +@@ -36,8 +36,10 @@ + #include "WindowsKeyboardCodes.h" + #include + #include ++#include + #include + #include ++#include + + namespace WebCore { + +@@ -1293,6 +1295,246 @@ int PlatformKeyboardEvent::windowsKeyCodeForGdkKeyCode(unsigned keycode) + + } + ++static const HashMap& gdkToWindowsKeyCodeMap() ++{ ++ static HashMap* result; ++ static std::once_flag once; ++ std::call_once( ++ once, ++ [] { ++ const unsigned gdkKeyCodes[] = { ++ GDK_KEY_Cancel, ++ // FIXME: non-keypad keys should take precedence, so we skip GDK_KEY_KP_* ++ // GDK_KEY_KP_0, ++ // GDK_KEY_KP_1, ++ // GDK_KEY_KP_2, ++ // GDK_KEY_KP_3, ++ // GDK_KEY_KP_4, ++ // GDK_KEY_KP_5, ++ // GDK_KEY_KP_6, ++ // GDK_KEY_KP_7, ++ // GDK_KEY_KP_8, ++ // GDK_KEY_KP_9, ++ // GDK_KEY_KP_Multiply, ++ // GDK_KEY_KP_Add, ++ // GDK_KEY_KP_Subtract, ++ // GDK_KEY_KP_Decimal, ++ // GDK_KEY_KP_Divide, ++ // GDK_KEY_KP_Page_Up, ++ // GDK_KEY_KP_Page_Down, ++ // GDK_KEY_KP_End, ++ // GDK_KEY_KP_Home, ++ // GDK_KEY_KP_Left, ++ // GDK_KEY_KP_Up, ++ // GDK_KEY_KP_Right, ++ // GDK_KEY_KP_Down, ++ GDK_KEY_BackSpace, ++ // GDK_KEY_ISO_Left_Tab, ++ // GDK_KEY_3270_BackTab, ++ GDK_KEY_Tab, ++ GDK_KEY_Clear, ++ // GDK_KEY_ISO_Enter, ++ // GDK_KEY_KP_Enter, ++ GDK_KEY_Return, ++ GDK_KEY_Menu, ++ GDK_KEY_Pause, ++ GDK_KEY_AudioPause, ++ GDK_KEY_Caps_Lock, ++ GDK_KEY_Kana_Lock, ++ GDK_KEY_Kana_Shift, ++ GDK_KEY_Hangul, ++ GDK_KEY_Hangul_Hanja, ++ GDK_KEY_Kanji, ++ GDK_KEY_Escape, ++ GDK_KEY_space, ++ GDK_KEY_Page_Up, ++ GDK_KEY_Page_Down, ++ GDK_KEY_End, ++ GDK_KEY_Home, ++ GDK_KEY_Left, ++ GDK_KEY_Up, ++ GDK_KEY_Right, ++ GDK_KEY_Down, ++ GDK_KEY_Select, ++ GDK_KEY_Print, ++ GDK_KEY_Execute, ++ GDK_KEY_Insert, ++ GDK_KEY_KP_Insert, ++ GDK_KEY_Delete, ++ GDK_KEY_KP_Delete, ++ GDK_KEY_Help, ++ GDK_KEY_0, ++ GDK_KEY_parenright, ++ GDK_KEY_1, ++ GDK_KEY_exclam, ++ GDK_KEY_2, ++ GDK_KEY_at, ++ GDK_KEY_3, ++ GDK_KEY_numbersign, ++ GDK_KEY_4, ++ GDK_KEY_dollar, ++ GDK_KEY_5, ++ GDK_KEY_percent, ++ GDK_KEY_6, ++ GDK_KEY_asciicircum, ++ GDK_KEY_7, ++ GDK_KEY_ampersand, ++ GDK_KEY_8, ++ GDK_KEY_asterisk, ++ GDK_KEY_9, ++ GDK_KEY_parenleft, ++ GDK_KEY_a, ++ GDK_KEY_A, ++ GDK_KEY_b, ++ GDK_KEY_B, ++ GDK_KEY_c, ++ GDK_KEY_C, ++ GDK_KEY_d, ++ GDK_KEY_D, ++ GDK_KEY_e, ++ GDK_KEY_E, ++ GDK_KEY_f, ++ GDK_KEY_F, ++ GDK_KEY_g, ++ GDK_KEY_G, ++ GDK_KEY_h, ++ GDK_KEY_H, ++ GDK_KEY_i, ++ GDK_KEY_I, ++ GDK_KEY_j, ++ GDK_KEY_J, ++ GDK_KEY_k, ++ GDK_KEY_K, ++ GDK_KEY_l, ++ GDK_KEY_L, ++ GDK_KEY_m, ++ GDK_KEY_M, ++ GDK_KEY_n, ++ GDK_KEY_N, ++ GDK_KEY_o, ++ GDK_KEY_O, ++ GDK_KEY_p, ++ GDK_KEY_P, ++ GDK_KEY_q, ++ GDK_KEY_Q, ++ GDK_KEY_r, ++ GDK_KEY_R, ++ GDK_KEY_s, ++ GDK_KEY_S, ++ GDK_KEY_t, ++ GDK_KEY_T, ++ GDK_KEY_u, ++ GDK_KEY_U, ++ GDK_KEY_v, ++ GDK_KEY_V, ++ GDK_KEY_w, ++ GDK_KEY_W, ++ GDK_KEY_x, ++ GDK_KEY_X, ++ GDK_KEY_y, ++ GDK_KEY_Y, ++ GDK_KEY_z, ++ GDK_KEY_Z, ++ GDK_KEY_Meta_L, ++ GDK_KEY_Meta_R, ++ GDK_KEY_Sleep, ++ GDK_KEY_Num_Lock, ++ GDK_KEY_Scroll_Lock, ++ GDK_KEY_Shift_L, ++ GDK_KEY_Shift_R, ++ GDK_KEY_Control_L, ++ GDK_KEY_Control_R, ++ GDK_KEY_Alt_L, ++ GDK_KEY_Alt_R, ++ GDK_KEY_Back, ++ GDK_KEY_Forward, ++ GDK_KEY_Refresh, ++ GDK_KEY_Stop, ++ GDK_KEY_Search, ++ GDK_KEY_Favorites, ++ GDK_KEY_HomePage, ++ GDK_KEY_AudioMute, ++ GDK_KEY_AudioLowerVolume, ++ GDK_KEY_AudioRaiseVolume, ++ GDK_KEY_AudioNext, ++ GDK_KEY_AudioPrev, ++ GDK_KEY_AudioStop, ++ GDK_KEY_AudioMedia, ++ GDK_KEY_semicolon, ++ GDK_KEY_colon, ++ GDK_KEY_plus, ++ GDK_KEY_equal, ++ GDK_KEY_comma, ++ GDK_KEY_less, ++ GDK_KEY_minus, ++ GDK_KEY_underscore, ++ GDK_KEY_period, ++ GDK_KEY_greater, ++ GDK_KEY_slash, ++ GDK_KEY_question, ++ GDK_KEY_asciitilde, ++ GDK_KEY_quoteleft, ++ GDK_KEY_bracketleft, ++ GDK_KEY_braceleft, ++ GDK_KEY_backslash, ++ GDK_KEY_bar, ++ GDK_KEY_bracketright, ++ GDK_KEY_braceright, ++ GDK_KEY_quoteright, ++ GDK_KEY_quotedbl, ++ GDK_KEY_AudioRewind, ++ GDK_KEY_AudioForward, ++ GDK_KEY_AudioPlay, ++ GDK_KEY_F1, ++ GDK_KEY_F2, ++ GDK_KEY_F3, ++ GDK_KEY_F4, ++ GDK_KEY_F5, ++ GDK_KEY_F6, ++ GDK_KEY_F7, ++ GDK_KEY_F8, ++ GDK_KEY_F9, ++ GDK_KEY_F10, ++ GDK_KEY_F11, ++ GDK_KEY_F12, ++ GDK_KEY_F13, ++ GDK_KEY_F14, ++ GDK_KEY_F15, ++ GDK_KEY_F16, ++ GDK_KEY_F17, ++ GDK_KEY_F18, ++ GDK_KEY_F19, ++ GDK_KEY_F20, ++ GDK_KEY_F21, ++ GDK_KEY_F22, ++ GDK_KEY_F23, ++ GDK_KEY_F24, ++ GDK_KEY_VoidSymbol, ++ GDK_KEY_Red, ++ GDK_KEY_Green, ++ GDK_KEY_Yellow, ++ GDK_KEY_Blue, ++ GDK_KEY_PowerOff, ++ GDK_KEY_AudioRecord, ++ GDK_KEY_Display, ++ GDK_KEY_Subtitle, ++ GDK_KEY_Video ++ }; ++ result = new HashMap(); ++ for (unsigned gdkKeyCode : gdkKeyCodes) { ++ int winKeyCode = PlatformKeyboardEvent::windowsKeyCodeForGdkKeyCode(gdkKeyCode); ++ // If several gdk key codes map to the same win key code first one is used. ++ result->add(winKeyCode, gdkKeyCode); ++ } ++ }); ++ return *result; ++} ++ ++unsigned PlatformKeyboardEvent::gdkKeyCodeForWindowsKeyCode(int keycode) ++{ ++ return gdkToWindowsKeyCodeMap().get(keycode); ++} ++ + String PlatformKeyboardEvent::singleCharacterString(unsigned val) + { + switch (val) { +diff --git a/Source/WebCore/platform/libwpe/PlatformKeyboardEventLibWPE.cpp b/Source/WebCore/platform/libwpe/PlatformKeyboardEventLibWPE.cpp +index cf46da15083..efbda20f28b 100644 +--- a/Source/WebCore/platform/libwpe/PlatformKeyboardEventLibWPE.cpp ++++ b/Source/WebCore/platform/libwpe/PlatformKeyboardEventLibWPE.cpp +@@ -1291,6 +1291,246 @@ int PlatformKeyboardEvent::windowsKeyCodeForWPEKeyCode(unsigned keycode) + return 0; + } + ++static const HashMap& WPEToWindowsKeyCodeMap() ++{ ++ static HashMap* result; ++ static std::once_flag once; ++ std::call_once( ++ once, ++ [] { ++ const unsigned WPEKeyCodes[] = { ++ WPE_KEY_Cancel, ++ // FIXME: non-keypad keys should take precedence, so we skip WPE_KEY_KP_* ++ // WPE_KEY_KP_0, ++ // WPE_KEY_KP_1, ++ // WPE_KEY_KP_2, ++ // WPE_KEY_KP_3, ++ // WPE_KEY_KP_4, ++ // WPE_KEY_KP_5, ++ // WPE_KEY_KP_6, ++ // WPE_KEY_KP_7, ++ // WPE_KEY_KP_8, ++ // WPE_KEY_KP_9, ++ // WPE_KEY_KP_Multiply, ++ // WPE_KEY_KP_Add, ++ // WPE_KEY_KP_Subtract, ++ // WPE_KEY_KP_Decimal, ++ // WPE_KEY_KP_Divide, ++ // WPE_KEY_KP_Page_Up, ++ // WPE_KEY_KP_Page_Down, ++ // WPE_KEY_KP_End, ++ // WPE_KEY_KP_Home, ++ // WPE_KEY_KP_Left, ++ // WPE_KEY_KP_Up, ++ // WPE_KEY_KP_Right, ++ // WPE_KEY_KP_Down, ++ WPE_KEY_BackSpace, ++ // WPE_KEY_ISO_Left_Tab, ++ // WPE_KEY_3270_BackTab, ++ WPE_KEY_Tab, ++ WPE_KEY_Clear, ++ // WPE_KEY_ISO_Enter, ++ // WPE_KEY_KP_Enter, ++ WPE_KEY_Return, ++ WPE_KEY_Menu, ++ WPE_KEY_Pause, ++ WPE_KEY_AudioPause, ++ WPE_KEY_Caps_Lock, ++ WPE_KEY_Kana_Lock, ++ WPE_KEY_Kana_Shift, ++ WPE_KEY_Hangul, ++ WPE_KEY_Hangul_Hanja, ++ WPE_KEY_Kanji, ++ WPE_KEY_Escape, ++ WPE_KEY_space, ++ WPE_KEY_Page_Up, ++ WPE_KEY_Page_Down, ++ WPE_KEY_End, ++ WPE_KEY_Home, ++ WPE_KEY_Left, ++ WPE_KEY_Up, ++ WPE_KEY_Right, ++ WPE_KEY_Down, ++ WPE_KEY_Select, ++ WPE_KEY_Print, ++ WPE_KEY_Execute, ++ WPE_KEY_Insert, ++ WPE_KEY_KP_Insert, ++ WPE_KEY_Delete, ++ WPE_KEY_KP_Delete, ++ WPE_KEY_Help, ++ WPE_KEY_0, ++ WPE_KEY_parenright, ++ WPE_KEY_1, ++ WPE_KEY_exclam, ++ WPE_KEY_2, ++ WPE_KEY_at, ++ WPE_KEY_3, ++ WPE_KEY_numbersign, ++ WPE_KEY_4, ++ WPE_KEY_dollar, ++ WPE_KEY_5, ++ WPE_KEY_percent, ++ WPE_KEY_6, ++ WPE_KEY_asciicircum, ++ WPE_KEY_7, ++ WPE_KEY_ampersand, ++ WPE_KEY_8, ++ WPE_KEY_asterisk, ++ WPE_KEY_9, ++ WPE_KEY_parenleft, ++ WPE_KEY_a, ++ WPE_KEY_A, ++ WPE_KEY_b, ++ WPE_KEY_B, ++ WPE_KEY_c, ++ WPE_KEY_C, ++ WPE_KEY_d, ++ WPE_KEY_D, ++ WPE_KEY_e, ++ WPE_KEY_E, ++ WPE_KEY_f, ++ WPE_KEY_F, ++ WPE_KEY_g, ++ WPE_KEY_G, ++ WPE_KEY_h, ++ WPE_KEY_H, ++ WPE_KEY_i, ++ WPE_KEY_I, ++ WPE_KEY_j, ++ WPE_KEY_J, ++ WPE_KEY_k, ++ WPE_KEY_K, ++ WPE_KEY_l, ++ WPE_KEY_L, ++ WPE_KEY_m, ++ WPE_KEY_M, ++ WPE_KEY_n, ++ WPE_KEY_N, ++ WPE_KEY_o, ++ WPE_KEY_O, ++ WPE_KEY_p, ++ WPE_KEY_P, ++ WPE_KEY_q, ++ WPE_KEY_Q, ++ WPE_KEY_r, ++ WPE_KEY_R, ++ WPE_KEY_s, ++ WPE_KEY_S, ++ WPE_KEY_t, ++ WPE_KEY_T, ++ WPE_KEY_u, ++ WPE_KEY_U, ++ WPE_KEY_v, ++ WPE_KEY_V, ++ WPE_KEY_w, ++ WPE_KEY_W, ++ WPE_KEY_x, ++ WPE_KEY_X, ++ WPE_KEY_y, ++ WPE_KEY_Y, ++ WPE_KEY_z, ++ WPE_KEY_Z, ++ WPE_KEY_Meta_L, ++ WPE_KEY_Meta_R, ++ WPE_KEY_Sleep, ++ WPE_KEY_Num_Lock, ++ WPE_KEY_Scroll_Lock, ++ WPE_KEY_Shift_L, ++ WPE_KEY_Shift_R, ++ WPE_KEY_Control_L, ++ WPE_KEY_Control_R, ++ WPE_KEY_Alt_L, ++ WPE_KEY_Alt_R, ++ WPE_KEY_Back, ++ WPE_KEY_Forward, ++ WPE_KEY_Refresh, ++ WPE_KEY_Stop, ++ WPE_KEY_Search, ++ WPE_KEY_Favorites, ++ WPE_KEY_HomePage, ++ WPE_KEY_AudioMute, ++ WPE_KEY_AudioLowerVolume, ++ WPE_KEY_AudioRaiseVolume, ++ WPE_KEY_AudioNext, ++ WPE_KEY_AudioPrev, ++ WPE_KEY_AudioStop, ++ WPE_KEY_AudioMedia, ++ WPE_KEY_semicolon, ++ WPE_KEY_colon, ++ WPE_KEY_plus, ++ WPE_KEY_equal, ++ WPE_KEY_comma, ++ WPE_KEY_less, ++ WPE_KEY_minus, ++ WPE_KEY_underscore, ++ WPE_KEY_period, ++ WPE_KEY_greater, ++ WPE_KEY_slash, ++ WPE_KEY_question, ++ WPE_KEY_asciitilde, ++ WPE_KEY_quoteleft, ++ WPE_KEY_bracketleft, ++ WPE_KEY_braceleft, ++ WPE_KEY_backslash, ++ WPE_KEY_bar, ++ WPE_KEY_bracketright, ++ WPE_KEY_braceright, ++ WPE_KEY_quoteright, ++ WPE_KEY_quotedbl, ++ WPE_KEY_AudioRewind, ++ WPE_KEY_AudioForward, ++ WPE_KEY_AudioPlay, ++ WPE_KEY_F1, ++ WPE_KEY_F2, ++ WPE_KEY_F3, ++ WPE_KEY_F4, ++ WPE_KEY_F5, ++ WPE_KEY_F6, ++ WPE_KEY_F7, ++ WPE_KEY_F8, ++ WPE_KEY_F9, ++ WPE_KEY_F10, ++ WPE_KEY_F11, ++ WPE_KEY_F12, ++ WPE_KEY_F13, ++ WPE_KEY_F14, ++ WPE_KEY_F15, ++ WPE_KEY_F16, ++ WPE_KEY_F17, ++ WPE_KEY_F18, ++ WPE_KEY_F19, ++ WPE_KEY_F20, ++ WPE_KEY_F21, ++ WPE_KEY_F22, ++ WPE_KEY_F23, ++ WPE_KEY_F24, ++ WPE_KEY_VoidSymbol, ++ WPE_KEY_Red, ++ WPE_KEY_Green, ++ WPE_KEY_Yellow, ++ WPE_KEY_Blue, ++ WPE_KEY_PowerOff, ++ WPE_KEY_AudioRecord, ++ WPE_KEY_Display, ++ WPE_KEY_Subtitle, ++ WPE_KEY_Video ++ }; ++ result = new HashMap(); ++ for (unsigned WPEKeyCode : WPEKeyCodes) { ++ int winKeyCode = PlatformKeyboardEvent::windowsKeyCodeForWPEKeyCode(WPEKeyCode); ++ // If several gdk key codes map to the same win key code first one is used. ++ result->add(winKeyCode, WPEKeyCode); ++ } ++ }); ++ return *result; ++} ++ ++unsigned PlatformKeyboardEvent::WPEKeyCodeForWindowsKeyCode(int keycode) ++{ ++ return WPEToWindowsKeyCodeMap().get(keycode); ++} ++ + String PlatformKeyboardEvent::singleCharacterString(unsigned val) + { + switch (val) { +diff --git a/Source/WebKit/Shared/API/c/wpe/WebKit.h b/Source/WebKit/Shared/API/c/wpe/WebKit.h +index 898e30b370d..74945e06fac 100644 +--- a/Source/WebKit/Shared/API/c/wpe/WebKit.h ++++ b/Source/WebKit/Shared/API/c/wpe/WebKit.h +@@ -78,6 +78,7 @@ + // From Source/WebKit/UIProcess/API/C + #include + #include ++#include + #include + #include + #include +diff --git a/Source/WebKit/Shared/NativeWebKeyboardEvent.h b/Source/WebKit/Shared/NativeWebKeyboardEvent.h +index 6f4e29b7c65..9dd287efc40 100644 +--- a/Source/WebKit/Shared/NativeWebKeyboardEvent.h ++++ b/Source/WebKit/Shared/NativeWebKeyboardEvent.h +@@ -34,6 +34,7 @@ + #if USE(APPKIT) + #include + OBJC_CLASS NSView; ++OBJC_CLASS NSEvent; + + namespace WebCore { + struct KeypressCommand; +@@ -70,6 +71,10 @@ public: + enum class HandledByInputMethod : bool { No, Yes }; + enum class FakedForComposition : bool { No, Yes }; + NativeWebKeyboardEvent(GdkEvent*, const String&, HandledByInputMethod, FakedForComposition, Vector&& commands); ++ NativeWebKeyboardEvent(Type type, const String& text, const String& unmodifiedText, const String& key, const String& code, const String& keyIdentifier, int windowsVirtualKeyCode, int nativeVirtualKeyCode, bool isAutoRepeat, bool isKeypad, bool isSystemKey, OptionSet modifiers, WallTime timestamp, Vector&& commands) ++ : WebKeyboardEvent(type, text, unmodifiedText, key, code, keyIdentifier, windowsVirtualKeyCode, nativeVirtualKeyCode, isAutoRepeat, isKeypad, isSystemKey, modifiers, timestamp, WTFMove(commands)) ++ { ++ } + #elif PLATFORM(IOS_FAMILY) + enum class HandledByInputMethod : bool { No, Yes }; + NativeWebKeyboardEvent(::WebEvent *, HandledByInputMethod); +diff --git a/Source/WebKit/Shared/NativeWebMouseEvent.h b/Source/WebKit/Shared/NativeWebMouseEvent.h +index ba7f93c924f..d19eb2f2932 100644 +--- a/Source/WebKit/Shared/NativeWebMouseEvent.h ++++ b/Source/WebKit/Shared/NativeWebMouseEvent.h +@@ -61,6 +61,10 @@ public: + #elif PLATFORM(GTK) + NativeWebMouseEvent(const NativeWebMouseEvent&); + NativeWebMouseEvent(GdkEvent*, int); ++ NativeWebMouseEvent(Type type, Button button, unsigned short buttons, const WebCore::IntPoint& position, const WebCore::IntPoint& globalPosition, float deltaX, float deltaY, float deltaZ, int clickCount, OptionSet modifiers, WallTime timestamp) ++ : WebMouseEvent(type, button, buttons, position, globalPosition, deltaX, deltaY, deltaZ, clickCount, modifiers, timestamp) ++ { ++ } + #elif PLATFORM(IOS_FAMILY) + NativeWebMouseEvent(::WebEvent *); + NativeWebMouseEvent(Type, Button, unsigned short buttons, const WebCore::IntPoint& position, const WebCore::IntPoint& globalPosition, float deltaX, float deltaY, float deltaZ, int clickCount, OptionSet, WallTime timestamp, double force); +diff --git a/Source/WebKit/Shared/WebEvent.h b/Source/WebKit/Shared/WebEvent.h +index c36100cf5c4..216402f0a24 100644 +--- a/Source/WebKit/Shared/WebEvent.h ++++ b/Source/WebKit/Shared/WebEvent.h +@@ -35,6 +35,7 @@ + #include + #include + #include ++#include + #include + #include + +@@ -138,7 +139,7 @@ public: + WebMouseEvent(); + + #if PLATFORM(MAC) +- WebMouseEvent(Type, Button, unsigned short buttons, const WebCore::IntPoint& position, const WebCore::IntPoint& globalPosition, float deltaX, float deltaY, float deltaZ, int clickCount, OptionSet, WallTime timestamp, double force, SyntheticClickType = NoTap, int eventNumber = -1, int menuType = 0); ++ WebMouseEvent(Type, Button, unsigned short buttons, const WebCore::IntPoint& position, const WebCore::IntPoint& globalPosition, float deltaX, float deltaY, float deltaZ, int clickCount, OptionSet, WallTime timestamp, double force = 0, SyntheticClickType = NoTap, int eventNumber = -1, int menuType = 0); + #else + WebMouseEvent(Type, Button, unsigned short buttons, const WebCore::IntPoint& position, const WebCore::IntPoint& globalPosition, float deltaX, float deltaY, float deltaZ, int clickCount, OptionSet, WallTime timestamp, double force = 0, SyntheticClickType = NoTap); + #endif +@@ -258,6 +259,7 @@ public: + WebKeyboardEvent(Type, const String& text, const String& unmodifiedText, const String& key, const String& code, const String& keyIdentifier, int windowsVirtualKeyCode, int nativeVirtualKeyCode, int macCharCode, bool handledByInputMethod, const Vector&, bool isAutoRepeat, bool isKeypad, bool isSystemKey, OptionSet, WallTime timestamp); + #elif PLATFORM(GTK) + WebKeyboardEvent(Type, const String& text, const String& key, const String& code, const String& keyIdentifier, int windowsVirtualKeyCode, int nativeVirtualKeyCode, bool handledByInputMethod, Vector&& commands, bool isKeypad, OptionSet, WallTime timestamp); ++ WebKeyboardEvent(Type, const String& text, const String& unmodifiedText, const String& key, const String& code, const String& keyIdentifier, int windowsVirtualKeyCode, int nativeVirtualKeyCode, bool isAutoRepeat, bool isKeypad, bool isSystemKey, OptionSet, WallTime timestamp, Vector&& commands); + #elif PLATFORM(IOS_FAMILY) + WebKeyboardEvent(Type, const String& text, const String& unmodifiedText, const String& key, const String& code, const String& keyIdentifier, int windowsVirtualKeyCode, int nativeVirtualKeyCode, int macCharCode, bool handledByInputMethod, bool isAutoRepeat, bool isKeypad, bool isSystemKey, OptionSet, WallTime timestamp); + #elif USE(LIBWPE) +@@ -309,7 +311,7 @@ private: + int32_t m_nativeVirtualKeyCode; + int32_t m_macCharCode; + #if USE(APPKIT) || USE(UIKIT_KEYBOARD_ADDITIONS) || PLATFORM(GTK) +- bool m_handledByInputMethod; ++ bool m_handledByInputMethod = false; + #endif + #if USE(APPKIT) + Vector m_commands; +diff --git a/Source/WebKit/Shared/WebKeyboardEvent.cpp b/Source/WebKit/Shared/WebKeyboardEvent.cpp +index a5a23cf148e..390eaf847b6 100644 +--- a/Source/WebKit/Shared/WebKeyboardEvent.cpp ++++ b/Source/WebKit/Shared/WebKeyboardEvent.cpp +@@ -81,6 +81,28 @@ WebKeyboardEvent::WebKeyboardEvent(Type type, const String& text, const String& + ASSERT(isKeyboardEventType(type)); + } + ++WebKeyboardEvent::WebKeyboardEvent(Type type, const String& text, const String& unmodifiedText, const String& key, const String& code, const String& keyIdentifier, int windowsVirtualKeyCode, int nativeVirtualKeyCode, bool isAutoRepeat, bool isKeypad, bool isSystemKey, OptionSet modifiers, WallTime timestamp, Vector&& commands) ++ : WebEvent(type, modifiers, timestamp) ++ , m_text(text) ++ , m_unmodifiedText(text) ++#if ENABLE(KEYBOARD_KEY_ATTRIBUTE) ++ , m_key(key) ++#endif ++#if ENABLE(KEYBOARD_CODE_ATTRIBUTE) ++ , m_code(code) ++#endif ++ , m_keyIdentifier(keyIdentifier) ++ , m_windowsVirtualKeyCode(windowsVirtualKeyCode) ++ , m_nativeVirtualKeyCode(nativeVirtualKeyCode) ++ , m_macCharCode(0) ++ , m_commands(WTFMove(commands)) ++ , m_isAutoRepeat(isAutoRepeat) ++ , m_isKeypad(isKeypad) ++ , m_isSystemKey(isSystemKey) ++{ ++ ASSERT(isKeyboardEventType(type)); ++} ++ + #elif PLATFORM(IOS_FAMILY) + + WebKeyboardEvent::WebKeyboardEvent(Type type, const String& text, const String& unmodifiedText, const String& key, const String& code, const String& keyIdentifier, int windowsVirtualKeyCode, int nativeVirtualKeyCode, int macCharCode, bool handledByInputMethod, bool isAutoRepeat, bool isKeypad, bool isSystemKey, OptionSet modifiers, WallTime timestamp) +diff --git a/Source/WebKit/Shared/gtk/NativeWebKeyboardEventGtk.cpp b/Source/WebKit/Shared/gtk/NativeWebKeyboardEventGtk.cpp +index 45aa449644b..3a6b4169194 100644 +--- a/Source/WebKit/Shared/gtk/NativeWebKeyboardEventGtk.cpp ++++ b/Source/WebKit/Shared/gtk/NativeWebKeyboardEventGtk.cpp +@@ -43,7 +43,7 @@ NativeWebKeyboardEvent::NativeWebKeyboardEvent(GdkEvent* event, const String& te + } + + NativeWebKeyboardEvent::NativeWebKeyboardEvent(const NativeWebKeyboardEvent& event) +- : WebKeyboardEvent(WebEventFactory::createWebKeyboardEvent(event.nativeEvent(), event.text(), event.handledByInputMethod(), Vector(event.commands()))) ++ : WebKeyboardEvent(event) + , m_nativeEvent(gdk_event_copy(event.nativeEvent())) + , m_text(event.text()) + , m_handledByInputMethod(event.handledByInputMethod() ? HandledByInputMethod::Yes : HandledByInputMethod::No) +diff --git a/Source/WebKit/Shared/gtk/NativeWebMouseEventGtk.cpp b/Source/WebKit/Shared/gtk/NativeWebMouseEventGtk.cpp +index 28d00f0e6fd..23a013e754e 100644 +--- a/Source/WebKit/Shared/gtk/NativeWebMouseEventGtk.cpp ++++ b/Source/WebKit/Shared/gtk/NativeWebMouseEventGtk.cpp +@@ -38,8 +38,8 @@ NativeWebMouseEvent::NativeWebMouseEvent(GdkEvent* event, int eventClickCount) + } + + NativeWebMouseEvent::NativeWebMouseEvent(const NativeWebMouseEvent& event) +- : WebMouseEvent(WebEventFactory::createWebMouseEvent(event.nativeEvent(), event.clickCount())) +- , m_nativeEvent(gdk_event_copy(event.nativeEvent())) ++ : WebMouseEvent(event) ++ , m_nativeEvent(event.nativeEvent() ? gdk_event_copy(event.nativeEvent()) : nullptr) + { + } + +diff --git a/Source/WebKit/Sources.txt b/Source/WebKit/Sources.txt +index edbfb56e49b..623f393da74 100644 +--- a/Source/WebKit/Sources.txt ++++ b/Source/WebKit/Sources.txt +@@ -240,17 +240,22 @@ Shared/WebsiteData/WebsiteData.cpp + + UIProcess/AuxiliaryProcessProxy.cpp + UIProcess/BackgroundProcessResponsivenessTimer.cpp ++UIProcess/BrowserInspectorController.cpp ++UIProcess/BrowserInspectorPipe.cpp ++UIProcess/BrowserInspectorTargetAgent.cpp + UIProcess/DeviceIdHashSaltStorage.cpp + UIProcess/DrawingAreaProxy.cpp + UIProcess/FrameLoadState.cpp + UIProcess/GeolocationPermissionRequestManagerProxy.cpp + UIProcess/GeolocationPermissionRequestProxy.cpp ++UIProcess/InspectorBrowserAgent.cpp + UIProcess/InspectorTargetProxy.cpp + UIProcess/LegacyGlobalSettings.cpp + UIProcess/PageLoadState.cpp + UIProcess/ProcessAssertion.cpp + UIProcess/ProcessThrottler.cpp + UIProcess/ProvisionalPageProxy.cpp ++UIProcess/RemoteInspectorPipe.cpp + UIProcess/RemoteWebInspectorProxy.cpp + UIProcess/ResponsivenessTimer.cpp + UIProcess/StatisticsRequest.cpp +@@ -292,6 +297,9 @@ UIProcess/WebPageDiagnosticLoggingClient.cpp + UIProcess/WebPageGroup.cpp + UIProcess/WebPageInjectedBundleClient.cpp + UIProcess/WebPageInspectorController.cpp ++UIProcess/WebPageInspectorEmulationAgent.cpp ++UIProcess/WebPageInspectorInputAgent.cpp ++UIProcess/WebPageInspectorTargetProxy.cpp + UIProcess/WebPageProxy.cpp + UIProcess/WebPasteboardProxy.cpp + UIProcess/WebPreferences.cpp +diff --git a/Source/WebKit/SourcesCocoa.txt b/Source/WebKit/SourcesCocoa.txt +index a22aaba0310..386dd8586c4 100644 +--- a/Source/WebKit/SourcesCocoa.txt ++++ b/Source/WebKit/SourcesCocoa.txt +@@ -243,6 +243,7 @@ UIProcess/API/Cocoa/_WKApplicationManifest.mm + UIProcess/API/Cocoa/_WKAttachment.mm + UIProcess/API/Cocoa/_WKAutomationSession.mm + UIProcess/API/Cocoa/_WKAutomationSessionConfiguration.mm ++UIProcess/API/Cocoa/_WKBrowserInspector.mm + UIProcess/API/Cocoa/_WKContentRuleListAction.mm + UIProcess/API/Cocoa/_WKContextMenuElementInfo.mm + UIProcess/API/Cocoa/_WKCustomHeaderFields.mm @no-unify +diff --git a/Source/WebKit/SourcesGTK.txt b/Source/WebKit/SourcesGTK.txt +index c9e153d6a2c..70788976bee 100644 +--- a/Source/WebKit/SourcesGTK.txt ++++ b/Source/WebKit/SourcesGTK.txt +@@ -129,6 +129,7 @@ UIProcess/API/glib/WebKitAuthenticationRequest.cpp @no-unify + UIProcess/API/glib/WebKitAutomationSession.cpp @no-unify + UIProcess/API/glib/WebKitBackForwardList.cpp @no-unify + UIProcess/API/glib/WebKitBackForwardListItem.cpp @no-unify ++UIProcess/API/glib/WebKitBrowserInspector.cpp @no-unify + UIProcess/API/glib/WebKitContextMenuClient.cpp @no-unify + UIProcess/API/glib/WebKitCookieManager.cpp @no-unify + UIProcess/API/glib/WebKitCredential.cpp @no-unify +@@ -224,6 +225,7 @@ UIProcess/WebsiteData/unix/WebsiteDataStoreUnix.cpp + + UIProcess/cairo/BackingStoreCairo.cpp @no-unify + ++UIProcess/glib/InspectorBrowserAgentClientGLib.cpp + UIProcess/glib/RemoteInspectorClient.cpp + UIProcess/glib/WebProcessPoolGLib.cpp + UIProcess/glib/WebProcessProxyGLib.cpp +@@ -249,6 +251,9 @@ UIProcess/gtk/WebColorPickerGtk.cpp + UIProcess/gtk/WebContextMenuProxyGtk.cpp + UIProcess/gtk/WebDataListSuggestionsDropdownGtk.cpp + UIProcess/gtk/WebInspectorProxyGtk.cpp ++UIProcess/gtk/WebPageInspectorEmulationAgentGtk.cpp ++UIProcess/gtk/WebPageInspectorInputAgentGtk.cpp ++UIProcess/gtk/WebPageInspectorTargetProxyGtk.cpp + UIProcess/gtk/WebKitInspectorWindow.cpp + UIProcess/gtk/WebPageProxyGtk.cpp @no-unify + UIProcess/gtk/WebPasteboardProxyGtk.cpp +diff --git a/Source/WebKit/SourcesWPE.txt b/Source/WebKit/SourcesWPE.txt +index 5b514d5216e..75bd77c7614 100644 +--- a/Source/WebKit/SourcesWPE.txt ++++ b/Source/WebKit/SourcesWPE.txt +@@ -118,6 +118,7 @@ UIProcess/API/glib/WebKitAuthenticationRequest.cpp @no-unify + UIProcess/API/glib/WebKitAutomationSession.cpp @no-unify + UIProcess/API/glib/WebKitBackForwardList.cpp @no-unify + UIProcess/API/glib/WebKitBackForwardListItem.cpp @no-unify ++UIProcess/API/glib/WebKitBrowserInspector.cpp @no-unify + UIProcess/API/glib/WebKitContextMenuClient.cpp @no-unify + UIProcess/API/glib/WebKitCookieManager.cpp @no-unify + UIProcess/API/glib/WebKitCredential.cpp @no-unify +@@ -186,7 +187,7 @@ UIProcess/Automation/wpe/WebAutomationSessionWPE.cpp + UIProcess/CoordinatedGraphics/DrawingAreaProxyCoordinatedGraphics.cpp + + UIProcess/geoclue/GeoclueGeolocationProvider.cpp +- ++UIProcess/glib/InspectorBrowserAgentClientGLib.cpp + UIProcess/glib/WebProcessPoolGLib.cpp + UIProcess/glib/WebProcessProxyGLib.cpp + UIProcess/glib/WebsiteDataStoreGLib.cpp @no-unify +@@ -211,6 +212,9 @@ UIProcess/soup/WebProcessPoolSoup.cpp + UIProcess/wpe/TextCheckerWPE.cpp + UIProcess/wpe/WebInspectorProxyWPE.cpp + UIProcess/wpe/WebPageProxyWPE.cpp ++UIProcess/wpe/WebPageInspectorEmulationAgentWPE.cpp ++UIProcess/wpe/WebPageInspectorInputAgentWPE.cpp ++UIProcess/wpe/WebPageInspectorTargetProxyWPE.cpp + UIProcess/wpe/WebPasteboardProxyWPE.cpp + UIProcess/wpe/WebPreferencesWPE.cpp + +diff --git a/Source/WebKit/UIProcess/API/APIAttachment.cpp b/Source/WebKit/UIProcess/API/APIAttachment.cpp +index f9a4cadfae1..1386ed63eca 100644 +--- a/Source/WebKit/UIProcess/API/APIAttachment.cpp ++++ b/Source/WebKit/UIProcess/API/APIAttachment.cpp +@@ -28,6 +28,7 @@ + + #if ENABLE(ATTACHMENT_ELEMENT) + ++#include "WebPageProxy.h" + #include + #include + +diff --git a/Source/WebKit/UIProcess/API/C/WKPage.cpp b/Source/WebKit/UIProcess/API/C/WKPage.cpp +index 44637251dff..1e64b9f34fe 100644 +--- a/Source/WebKit/UIProcess/API/C/WKPage.cpp ++++ b/Source/WebKit/UIProcess/API/C/WKPage.cpp +@@ -1734,6 +1734,8 @@ void WKPageSetPageUIClient(WKPageRef pageRef, const WKPageUIClientBase* wkClient + { + if (!m_client.didNotHandleKeyEvent) + return; ++ if (!event.nativeEvent()) ++ return; + m_client.didNotHandleKeyEvent(toAPI(page), event.nativeEvent(), m_client.base.clientInfo); + } + +diff --git a/Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.h b/Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.h +new file mode 100644 +index 00000000000..812c2913e4f +--- /dev/null ++++ b/Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.h +@@ -0,0 +1,50 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * ++ * met: ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#import ++#import ++ ++NS_ASSUME_NONNULL_BEGIN ++ ++@class WKWebView; ++ ++@protocol _WKBrowserInspectorDelegate ++- (WKWebView *)createNewPage; ++- (void)quit; ++@end ++ ++WK_CLASS_AVAILABLE(macos(10.15.0)) ++@interface _WKBrowserInspector : NSObject +++ (void)initializeRemoteInspectorPipe:(id<_WKBrowserInspectorDelegate>)delegate; ++@end ++ ++ ++NS_ASSUME_NONNULL_END ++ +diff --git a/Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.mm b/Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.mm +new file mode 100644 +index 00000000000..2c9aead1b47 +--- /dev/null ++++ b/Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.mm +@@ -0,0 +1,52 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * ++ * met: ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "_WKBrowserInspector.h" ++ ++#include "BrowserInspectorPipe.h" ++#include "InspectorBrowserAgentClientMac.h" ++#include "WebKit2Initialize.h" ++ ++#import "WKWebView.h" ++ ++using namespace WebKit; ++ ++@implementation _WKBrowserInspector ++ +++ (void)initializeRemoteInspectorPipe:(id<_WKBrowserInspectorDelegate>)delegate ++{ ++#if ENABLE(REMOTE_INSPECTOR) ++ InitializeWebKit2(); ++ initializeBrowserInspectorPipe(makeUnique(delegate)); ++#endif ++} ++ ++@end +diff --git a/Source/WebKit/UIProcess/API/glib/WebKitBrowserInspector.cpp b/Source/WebKit/UIProcess/API/glib/WebKitBrowserInspector.cpp +new file mode 100644 +index 00000000000..a893558f98b +--- /dev/null ++++ b/Source/WebKit/UIProcess/API/glib/WebKitBrowserInspector.cpp +@@ -0,0 +1,141 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebKitBrowserInspector.h" ++ ++#include "BrowserInspectorPipe.h" ++#include "InspectorBrowserAgentClientGLib.h" ++#include "WebKitBrowserInspectorPrivate.h" ++#include "WebKitWebViewPrivate.h" ++#include ++#include ++ ++/** ++ * SECTION: WebKitBrowserInspector ++ * @Short_description: Access to the WebKit browser inspector ++ * @Title: WebKitBrowserInspector ++ * ++ * The WebKit Browser Inspector is an experimental API that provides ++ * access to the inspector via the remote debugging protocol. The protocol ++ * allows to create ephemeral contexts and create pages in them and then ++ * manipulate them using the inspector commands. This may be useful for ++ * the browser automation or remote debugging. ++ * ++ * Currently the protocol can be exposed to the parent process via a unix ++ * pipe. ++ */ ++ ++enum { ++ CREATE_NEW_PAGE, ++ ++ LAST_SIGNAL ++}; ++ ++struct _WebKitBrowserInspectorPrivate { ++ int unused { 0 }; ++}; ++ ++WEBKIT_DEFINE_TYPE(WebKitBrowserInspector, webkit_browser_inspector, G_TYPE_OBJECT) ++ ++static guint signals[LAST_SIGNAL] = { 0, }; ++ ++static void webkit_browser_inspector_class_init(WebKitBrowserInspectorClass* findClass) ++{ ++ GObjectClass* gObjectClass = G_OBJECT_CLASS(findClass); ++ ++ /** ++ * WebKitBrowserInspector::create-new-page: ++ * @inspector: the #WebKitBrowserInspector on which the signal is emitted ++ * ++ * Emitted when the inspector is requested to create a new page in the provided ++ * #WebKitWebContext. ++ * ++ * This signal is emitted when inspector receives 'Browser.createPage' command ++ * from its remote client. If the signla is not handled the command will fail. ++ * ++ * Returns: %WebKitWebView that contains created page. ++ */ ++ signals[CREATE_NEW_PAGE] = g_signal_new( ++ "create-new-page", ++ G_TYPE_FROM_CLASS(gObjectClass), ++ G_SIGNAL_RUN_LAST, ++ G_STRUCT_OFFSET(WebKitBrowserInspectorClass, create_new_page), ++ nullptr, nullptr, ++ g_cclosure_marshal_generic, ++#if PLATFORM(GTK) ++ GTK_TYPE_WIDGET, ++#else ++ WEBKIT_TYPE_WEB_VIEW, ++#endif ++ 1, ++ WEBKIT_TYPE_WEB_CONTEXT); ++} ++ ++WebKit::WebPageProxy* webkitBrowserInspectorCreateNewPageInContext(WebKitWebContext* context) ++{ ++ WebKitWebView* newWebView; ++ g_signal_emit(webkit_browser_inspector_get_default(), signals[CREATE_NEW_PAGE], 0, context, &newWebView); ++ if (!newWebView) ++ return nullptr; ++ return &webkitWebViewGetPage(newWebView); ++} ++ ++static gpointer createWebKitBrowserInspector(gpointer) ++{ ++ static GRefPtr browserInspector = adoptGRef(WEBKIT_BROWSER_INSPECTOR(g_object_new(WEBKIT_TYPE_BROWSER_INSPECTOR, nullptr))); ++ return browserInspector.get(); ++} ++ ++/** ++ * webkit_browser_inspector_get_default: ++ * ++ * Gets the default instance of the browser inspector. ++ * ++ * Returns: (transfer none): a #WebKitBrowserInspector ++ */ ++WebKitBrowserInspector* webkit_browser_inspector_get_default(void) ++{ ++ static GOnce onceInit = G_ONCE_INIT; ++ return WEBKIT_BROWSER_INSPECTOR(g_once(&onceInit, createWebKitBrowserInspector, 0)); ++} ++ ++/** ++ * webkit_browser_inspector_initialize_pipe: ++ * ++ * Creates browser inspector and configures pipe handler to communicate with ++ * the parent process. ++ */ ++void webkit_browser_inspector_initialize_pipe(void) ++{ ++#if ENABLE(REMOTE_INSPECTOR) ++ WebKit::initializeBrowserInspectorPipe(makeUnique()); ++#endif ++} +diff --git a/Source/WebKit/UIProcess/API/glib/WebKitBrowserInspectorPrivate.h b/Source/WebKit/UIProcess/API/glib/WebKitBrowserInspectorPrivate.h +new file mode 100644 +index 00000000000..6e9afeac99a +--- /dev/null ++++ b/Source/WebKit/UIProcess/API/glib/WebKitBrowserInspectorPrivate.h +@@ -0,0 +1,36 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#include "WebKitBrowserInspector.h" ++#include "WebPageProxy.h" ++ ++WebKit::WebPageProxy* webkitBrowserInspectorCreateNewPageInContext(WebKitWebContext*); +diff --git a/Source/WebKit/UIProcess/API/glib/WebKitWebContext.cpp b/Source/WebKit/UIProcess/API/glib/WebKitWebContext.cpp +index 126bccf1314..a095db63bc5 100644 +--- a/Source/WebKit/UIProcess/API/glib/WebKitWebContext.cpp ++++ b/Source/WebKit/UIProcess/API/glib/WebKitWebContext.cpp +@@ -373,6 +373,11 @@ static void webkitWebContextConstructed(GObject* object) + if (!webkit_website_data_manager_is_ephemeral(priv->websiteDataManager.get())) + WebKit::LegacyGlobalSettings::singleton().setHSTSStorageDirectory(FileSystem::stringFromFileSystemRepresentation(webkit_website_data_manager_get_hsts_cache_directory(priv->websiteDataManager.get()))); + ++ const gchar *singleprocess = g_getenv("MINIBROWSER_SINGLEPROCESS"); ++ if (singleprocess && *singleprocess) { ++ // processModel is not set at this point, force single process. ++ configuration.setUsesSingleWebProcess(true); ++ } + priv->processPool = WebProcessPool::create(configuration); + priv->processPool->setPrimaryDataStore(webkitWebsiteDataManagerGetDataStore(priv->websiteDataManager.get())); + priv->processPool->setUserMessageHandler([webContext](UserMessage&& message, CompletionHandler&& completionHandler) { +diff --git a/Source/WebKit/UIProcess/API/gtk/PageClientImpl.cpp b/Source/WebKit/UIProcess/API/gtk/PageClientImpl.cpp +index 00b7c6bbc46..c3a6cf416e1 100644 +--- a/Source/WebKit/UIProcess/API/gtk/PageClientImpl.cpp ++++ b/Source/WebKit/UIProcess/API/gtk/PageClientImpl.cpp +@@ -226,6 +226,8 @@ void PageClientImpl::doneWithKeyEvent(const NativeWebKeyboardEvent& event, bool + return; + if (event.fakedForComposition()) + return; ++ if (!event.nativeEvent()) ++ return; + + WebKitWebViewBase* webkitWebViewBase = WEBKIT_WEB_VIEW_BASE(m_viewWidget); + webkitWebViewBaseForwardNextKeyEvent(webkitWebViewBase); +diff --git a/Source/WebKit/UIProcess/API/gtk/WebKitBrowserInspector.h b/Source/WebKit/UIProcess/API/gtk/WebKitBrowserInspector.h +new file mode 100644 +index 00000000000..4ee8204a9b8 +--- /dev/null ++++ b/Source/WebKit/UIProcess/API/gtk/WebKitBrowserInspector.h +@@ -0,0 +1,84 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#if !defined(__WEBKIT2_H_INSIDE__) && !defined(WEBKIT2_COMPILATION) ++#error "Only can be included directly." ++#endif ++ ++#ifndef WebKitBrowserInspector_h ++#define WebKitBrowserInspector_h ++ ++#include ++#include ++#include ++ ++G_BEGIN_DECLS ++ ++#define WEBKIT_TYPE_BROWSER_INSPECTOR (webkit_browser_inspector_get_type()) ++#define WEBKIT_BROWSER_INSPECTOR(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), WEBKIT_TYPE_BROWSER_INSPECTOR, WebKitBrowserInspector)) ++#define WEBKIT_IS_BROWSER_INSPECTOR(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), WEBKIT_TYPE_BROWSER_INSPECTOR)) ++#define WEBKIT_BROWSER_INSPECTOR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), WEBKIT_TYPE_BROWSER_INSPECTOR, WebKitBrowserInspectorClass)) ++#define WEBKIT_IS_BROWSER_INSPECTOR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), WEBKIT_TYPE_BROWSER_INSPECTOR)) ++#define WEBKIT_BROWSER_INSPECTOR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), WEBKIT_TYPE_BROWSER_INSPECTOR, WebKitBrowserInspectorClass)) ++ ++typedef struct _WebKitBrowserInspector WebKitBrowserInspector; ++typedef struct _WebKitBrowserInspectorClass WebKitBrowserInspectorClass; ++typedef struct _WebKitBrowserInspectorPrivate WebKitBrowserInspectorPrivate; ++ ++struct _WebKitBrowserInspector { ++ GObject parent; ++ ++ WebKitBrowserInspectorPrivate *priv; ++}; ++ ++struct _WebKitBrowserInspectorClass { ++ GObjectClass parent_class; ++ ++ WebKitWebView *(* create_new_page) (WebKitBrowserInspector *browser_inspector, ++ WebKitWebContext *context); ++ ++ void (*_webkit_reserved0) (void); ++ void (*_webkit_reserved1) (void); ++ void (*_webkit_reserved2) (void); ++ void (*_webkit_reserved3) (void); ++}; ++ ++WEBKIT_API GType ++webkit_browser_inspector_get_type (void); ++ ++WEBKIT_API WebKitBrowserInspector * ++webkit_browser_inspector_get_default (void); ++ ++WEBKIT_API void ++webkit_browser_inspector_initialize_pipe (void); ++ ++G_END_DECLS ++ ++#endif +diff --git a/Source/WebKit/UIProcess/API/gtk/webkit2.h b/Source/WebKit/UIProcess/API/gtk/webkit2.h +index 16ef7eb6d42..eb3759b05bb 100644 +--- a/Source/WebKit/UIProcess/API/gtk/webkit2.h ++++ b/Source/WebKit/UIProcess/API/gtk/webkit2.h +@@ -32,6 +32,7 @@ + #include + #include + #include ++#include + #include + #include + #include +diff --git a/Source/WebKit/UIProcess/API/wpe/WebKitBrowserInspector.h b/Source/WebKit/UIProcess/API/wpe/WebKitBrowserInspector.h +new file mode 100644 +index 00000000000..675e517596b +--- /dev/null ++++ b/Source/WebKit/UIProcess/API/wpe/WebKitBrowserInspector.h +@@ -0,0 +1,81 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#if !defined(__WEBKIT_H_INSIDE__) && !defined(WEBKIT2_COMPILATION) ++#error "Only can be included directly." ++#endif ++ ++#ifndef WebKitBrowserInspector_h ++#define WebKitBrowserInspector_h ++ ++#include ++#include ++#include ++ ++G_BEGIN_DECLS ++ ++#define WEBKIT_TYPE_BROWSER_INSPECTOR (webkit_browser_inspector_get_type()) ++#define WEBKIT_BROWSER_INSPECTOR(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), WEBKIT_TYPE_BROWSER_INSPECTOR, WebKitBrowserInspector)) ++#define WEBKIT_IS_BROWSER_INSPECTOR(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), WEBKIT_TYPE_BROWSER_INSPECTOR)) ++#define WEBKIT_BROWSER_INSPECTOR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), WEBKIT_TYPE_BROWSER_INSPECTOR, WebKitBrowserInspectorClass)) ++#define WEBKIT_IS_BROWSER_INSPECTOR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), WEBKIT_TYPE_BROWSER_INSPECTOR)) ++#define WEBKIT_BROWSER_INSPECTOR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), WEBKIT_TYPE_BROWSER_INSPECTOR, WebKitBrowserInspectorClass)) ++ ++typedef struct _WebKitBrowserInspector WebKitBrowserInspector; ++typedef struct _WebKitBrowserInspectorClass WebKitBrowserInspectorClass; ++typedef struct _WebKitBrowserInspectorPrivate WebKitBrowserInspectorPrivate; ++ ++struct _WebKitBrowserInspector { ++ GObject parent; ++ ++ WebKitBrowserInspectorPrivate *priv; ++}; ++ ++struct _WebKitBrowserInspectorClass { ++ GObjectClass parent_class; ++ ++ WebKitWebView *(* create_new_page) (WebKitBrowserInspector *browser_inspector, ++ WebKitWebContext *context); ++ ++ void (*_webkit_reserved0) (void); ++ void (*_webkit_reserved1) (void); ++ void (*_webkit_reserved2) (void); ++ void (*_webkit_reserved3) (void); ++}; ++ ++WEBKIT_API GType ++webkit_browser_inspector_get_type (void); ++ ++WEBKIT_API WebKitBrowserInspector * ++webkit_browser_inspector_get_default (void); ++ ++G_END_DECLS ++ ++#endif +diff --git a/Source/WebKit/UIProcess/API/wpe/webkit.h b/Source/WebKit/UIProcess/API/wpe/webkit.h +index 9cc31cb4968..930499e65b6 100644 +--- a/Source/WebKit/UIProcess/API/wpe/webkit.h ++++ b/Source/WebKit/UIProcess/API/wpe/webkit.h +@@ -32,6 +32,7 @@ + #include + #include + #include ++#include + #include + #include + #include +diff --git a/Source/WebKit/UIProcess/BrowserInspectorController.cpp b/Source/WebKit/UIProcess/BrowserInspectorController.cpp +new file mode 100644 +index 00000000000..b4e14cb4390 +--- /dev/null ++++ b/Source/WebKit/UIProcess/BrowserInspectorController.cpp +@@ -0,0 +1,128 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "BrowserInspectorController.h" ++ ++#if ENABLE(REMOTE_INSPECTOR) ++ ++#include "BrowserInspectorTargetAgent.h" ++#include "InspectorBrowserAgent.h" ++#include "InspectorBrowserAgentClient.h" ++#include "WebPageInspectorController.h" ++#include "WebPageProxy.h" ++#include "WebProcessPool.h" ++#include "WebProcessProxy.h" ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++using namespace Inspector; ++ ++namespace WebKit { ++ ++static Vector allPages() ++{ ++ ASSERT(isMainThread()); ++ Vector result; ++ for (WebProcessPool* pool : WebProcessPool::allProcessPools()) { ++ for (auto& process : pool->processes()) { ++ result.appendRange(process->pages().begin(), process->pages().end()); ++ } ++ } ++ return result; ++} ++ ++BrowserInspectorController::BrowserInspectorController(std::unique_ptr client) ++ : m_frontendChannel(nullptr) ++ , m_frontendRouter(FrontendRouter::create()) ++ , m_backendDispatcher(BackendDispatcher::create(m_frontendRouter.copyRef())) ++ , m_browserAgentClient(std::move(client)) ++{ ++ m_agents.append(makeUnique(m_backendDispatcher, m_browserAgentClient.get())); ++ m_agents.append(makeUnique(m_backendDispatcher)); ++} ++ ++BrowserInspectorController::~BrowserInspectorController() = default; ++ ++void BrowserInspectorController::connectFrontend(FrontendChannel& frontendChannel) ++{ ++ ASSERT(!m_frontendChannel); ++ m_frontendChannel = &frontendChannel; ++ // Auto-connect to all new pages. ++ WebPageInspectorController::setCreationListener([this](WebPageInspectorController& inspectorController) { ++ inspectorController.connectFrontend(*m_frontendChannel); ++ }); ++ ++ bool connectingFirstFrontend = !m_frontendRouter->hasFrontends(); ++ m_frontendRouter->connectFrontend(frontendChannel); ++ if (connectingFirstFrontend) ++ m_agents.didCreateFrontendAndBackend(&m_frontendRouter.get(), &m_backendDispatcher.get()); ++ ++ connectToAllPages(); ++} ++ ++void BrowserInspectorController::disconnectFrontend() ++{ ++ ASSERT(m_frontendChannel); ++ disconnectFromAllPages(); ++ ++ m_frontendRouter->disconnectFrontend(*m_frontendChannel); ++ if (!m_frontendRouter->hasFrontends()) ++ m_agents.willDestroyFrontendAndBackend(DisconnectReason::InspectorDestroyed); ++ ++ WebPageInspectorController::setCreationListener(nullptr); ++ m_frontendChannel = nullptr; ++} ++ ++void BrowserInspectorController::dispatchMessageFromFrontend(const String& message) ++{ ++ m_backendDispatcher->dispatch(message); ++} ++ ++void BrowserInspectorController::connectToAllPages() ++{ ++ for (auto* page : allPages()) ++ page->inspectorController().connectFrontend(*m_frontendChannel); ++} ++ ++void BrowserInspectorController::disconnectFromAllPages() ++{ ++ for (auto* page : allPages()) ++ page->inspectorController().disconnectFrontend(*m_frontendChannel); ++} ++ ++} // namespace WebKit ++ ++#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/BrowserInspectorController.h b/Source/WebKit/UIProcess/BrowserInspectorController.h +new file mode 100644 +index 00000000000..d1e7ea17002 +--- /dev/null ++++ b/Source/WebKit/UIProcess/BrowserInspectorController.h +@@ -0,0 +1,74 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#if ENABLE(REMOTE_INSPECTOR) ++ ++#include ++#include ++#include ++ ++namespace Inspector { ++class BackendDispatcher; ++class FrontendChannel; ++class FrontendRouter; ++} ++ ++namespace WebKit { ++ ++class InspectorBrowserAgentClient; ++ ++class BrowserInspectorController { ++ WTF_MAKE_NONCOPYABLE(BrowserInspectorController); ++ WTF_MAKE_FAST_ALLOCATED; ++public: ++ BrowserInspectorController(std::unique_ptr client); ++ ~BrowserInspectorController(); ++ ++ void connectFrontend(Inspector::FrontendChannel&); ++ void disconnectFrontend(); ++ void dispatchMessageFromFrontend(const String& message); ++ ++private: ++ class TargetHandler; ++ void connectToAllPages(); ++ void disconnectFromAllPages(); ++ ++ Inspector::FrontendChannel* m_frontendChannel { nullptr }; ++ Ref m_frontendRouter; ++ Ref m_backendDispatcher; ++ std::unique_ptr m_browserAgentClient; ++ Inspector::AgentRegistry m_agents; ++}; ++ ++} // namespace WebKit ++ ++#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/BrowserInspectorPipe.cpp b/Source/WebKit/UIProcess/BrowserInspectorPipe.cpp +new file mode 100644 +index 00000000000..483b4e46c98 +--- /dev/null ++++ b/Source/WebKit/UIProcess/BrowserInspectorPipe.cpp +@@ -0,0 +1,62 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "BrowserInspectorPipe.h" ++ ++#if ENABLE(REMOTE_INSPECTOR) ++ ++#include "BrowserInspectorController.h" ++#include "RemoteInspectorPipe.h" ++#include ++#include "InspectorBrowserAgentClient.h" ++ ++namespace WebKit { ++ ++void initializeBrowserInspectorPipe(std::unique_ptr client) ++{ ++ class BrowserInspectorPipe { ++ public: ++ BrowserInspectorPipe(std::unique_ptr client) ++ : m_browserInspectorController(std::move(client)) ++ , m_remoteInspectorPipe(m_browserInspectorController) ++ { ++ } ++ ++ BrowserInspectorController m_browserInspectorController; ++ RemoteInspectorPipe m_remoteInspectorPipe; ++ }; ++ ++ static NeverDestroyed pipe(std::move(client)); ++} ++ ++} // namespace WebKit ++ ++#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/BrowserInspectorPipe.h b/Source/WebKit/UIProcess/BrowserInspectorPipe.h +new file mode 100644 +index 00000000000..a0088a43590 +--- /dev/null ++++ b/Source/WebKit/UIProcess/BrowserInspectorPipe.h +@@ -0,0 +1,43 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#if ENABLE(REMOTE_INSPECTOR) ++ ++namespace WebKit { ++ ++class InspectorBrowserAgentClient; ++ ++void initializeBrowserInspectorPipe(std::unique_ptr client); ++ ++} // namespace WebKit ++ ++#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/BrowserInspectorTargetAgent.cpp b/Source/WebKit/UIProcess/BrowserInspectorTargetAgent.cpp +new file mode 100644 +index 00000000000..0d1f5d75c3a +--- /dev/null ++++ b/Source/WebKit/UIProcess/BrowserInspectorTargetAgent.cpp +@@ -0,0 +1,110 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "BrowserInspectorTargetAgent.h" ++ ++#include "WebPageInspectorController.h" ++#include "WebPageProxy.h" ++#include "WebProcessPool.h" ++#include "WebProcessProxy.h" ++#include ++#include ++ ++using namespace Inspector; ++ ++namespace WebKit { ++ ++namespace { ++ ++InspectorTarget* targetForId(const String& targetId) ++{ ++ ASSERT(isMainThread()); ++ for (auto* pool : WebProcessPool::allProcessPools()) { ++ for (auto& process : pool->processes()) { ++ for (auto* page : process->pages()) { ++ auto* result = page->inspectorController().findTarget(targetId); ++ if (result != nullptr) ++ return result; ++ } ++ } ++ } ++ return nullptr; ++} ++ ++} // namespace ++ ++BrowserInspectorTargetAgent::BrowserInspectorTargetAgent(BackendDispatcher& backendDispatcher) ++ : InspectorAgentBase("Target"_s) ++ , m_backendDispatcher(TargetBackendDispatcher::create(backendDispatcher, this)) ++{ ++} ++ ++BrowserInspectorTargetAgent::~BrowserInspectorTargetAgent() = default; ++ ++void BrowserInspectorTargetAgent::didCreateFrontendAndBackend(FrontendRouter*, BackendDispatcher*) ++{ ++} ++ ++void BrowserInspectorTargetAgent::willDestroyFrontendAndBackend(DisconnectReason) ++{ ++} ++ ++void BrowserInspectorTargetAgent::sendMessageToTarget(ErrorString& error, const String& in_targetId, const String& in_message) ++{ ++ auto* target = targetForId(in_targetId); ++ if (target == nullptr) { ++ error = "Cannot find target with provided id."; ++ return; ++ } ++ target->sendMessageToTargetBackend(in_message); ++} ++ ++void BrowserInspectorTargetAgent::activate(ErrorString& error, const String& targetId) ++{ ++ auto* target = targetForId(targetId); ++ if (target == nullptr) { ++ error = "Cannot find target with provided id."; ++ return; ++ } ++ target->activate(error); ++} ++ ++void BrowserInspectorTargetAgent::close(ErrorString& error, const String& targetId) ++{ ++ auto* target = targetForId(targetId); ++ if (target == nullptr) { ++ error = "Cannot find target with provided id."; ++ return; ++ } ++ target->close(error); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/BrowserInspectorTargetAgent.h b/Source/WebKit/UIProcess/BrowserInspectorTargetAgent.h +new file mode 100644 +index 00000000000..8b4d9273574 +--- /dev/null ++++ b/Source/WebKit/UIProcess/BrowserInspectorTargetAgent.h +@@ -0,0 +1,62 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#include "config.h" ++ ++#include ++#include ++#include ++#include ++ ++namespace WebKit { ++ ++class BrowserInspectorTargetAgent final : public Inspector::InspectorAgentBase, public Inspector::TargetBackendDispatcherHandler { ++ WTF_MAKE_NONCOPYABLE(BrowserInspectorTargetAgent); ++ WTF_MAKE_FAST_ALLOCATED; ++public: ++ explicit BrowserInspectorTargetAgent(Inspector::BackendDispatcher&); ++ ~BrowserInspectorTargetAgent() override; ++ ++ // InspectorAgentBase ++ void didCreateFrontendAndBackend(Inspector::FrontendRouter*, Inspector::BackendDispatcher*) override; ++ void willDestroyFrontendAndBackend(Inspector::DisconnectReason) override; ++ ++ // TargetBackendDispatcherHandler ++ void sendMessageToTarget(Inspector::ErrorString&, const String& targetId, const String& message) final; ++ void activate(Inspector::ErrorString&, const String& targetId) override; ++ void close(Inspector::ErrorString&, const String& targetId) override; ++ ++private: ++ Ref m_backendDispatcher; ++}; ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/Cocoa/SOAuthorization/PopUpSOAuthorizationSession.h b/Source/WebKit/UIProcess/Cocoa/SOAuthorization/PopUpSOAuthorizationSession.h +index b6694fe906e..4df2ca7d9cc 100644 +--- a/Source/WebKit/UIProcess/Cocoa/SOAuthorization/PopUpSOAuthorizationSession.h ++++ b/Source/WebKit/UIProcess/Cocoa/SOAuthorization/PopUpSOAuthorizationSession.h +@@ -27,6 +27,8 @@ + + #if HAVE(APP_SSO) + ++#include ++#include + #include "SOAuthorizationSession.h" + + OBJC_CLASS WKSOSecretDelegate; +@@ -38,6 +40,8 @@ class NavigationAction; + + namespace WebKit { + ++class WebPageProxy; ++ + // FSM: Idle => Active => Completed + class PopUpSOAuthorizationSession final : public SOAuthorizationSession { + public: +diff --git a/Source/WebKit/UIProcess/Cocoa/SOAuthorization/PopUpSOAuthorizationSession.mm b/Source/WebKit/UIProcess/Cocoa/SOAuthorization/PopUpSOAuthorizationSession.mm +index 076cfaa676a..bd20a2b95f9 100644 +--- a/Source/WebKit/UIProcess/Cocoa/SOAuthorization/PopUpSOAuthorizationSession.mm ++++ b/Source/WebKit/UIProcess/Cocoa/SOAuthorization/PopUpSOAuthorizationSession.mm +@@ -29,6 +29,7 @@ + #if HAVE(APP_SSO) + + #import "APINavigationAction.h" ++#import "WebPageProxy.h" + #import "WKNavigationDelegatePrivate.h" + #import "WKUIDelegate.h" + #import "WKWebViewConfigurationPrivate.h" +diff --git a/Source/WebKit/UIProcess/InspectorBrowserAgent.cpp b/Source/WebKit/UIProcess/InspectorBrowserAgent.cpp +new file mode 100644 +index 00000000000..5967318c785 +--- /dev/null ++++ b/Source/WebKit/UIProcess/InspectorBrowserAgent.cpp +@@ -0,0 +1,101 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "InspectorBrowserAgent.h" ++ ++#if ENABLE(REMOTE_INSPECTOR) ++ ++#include "InspectorBrowserAgentClient.h" ++#include "WebPageInspectorTarget.h" ++#include "WebPageProxy.h" ++#include ++#include ++#include ++ ++using namespace Inspector; ++ ++namespace WebKit { ++ ++InspectorBrowserAgent::InspectorBrowserAgent(Inspector::BackendDispatcher& backendDispatcher, InspectorBrowserAgentClient* client) ++ : InspectorAgentBase("Browser"_s) ++ , m_backendDispatcher(BrowserBackendDispatcher::create(backendDispatcher, this)) ++ , m_client(client) ++{ ++} ++ ++InspectorBrowserAgent::~InspectorBrowserAgent() = default; ++ ++void InspectorBrowserAgent::didCreateFrontendAndBackend(FrontendRouter*, BackendDispatcher*) ++{ ++} ++ ++void InspectorBrowserAgent::willDestroyFrontendAndBackend(DisconnectReason) ++{ ++} ++ ++void InspectorBrowserAgent::close(ErrorString& error) ++{ ++ if (m_client == nullptr) { ++ error = "no platform delegate to close browser"; ++ } else { ++ m_client->closeAllWindows(); ++ } ++} ++ ++void InspectorBrowserAgent::createContext(ErrorString& error, String* browserContextID) ++{ ++ m_client->createBrowserContext(error, browserContextID); ++} ++ ++void InspectorBrowserAgent::deleteContext(ErrorString& error, const String& browserContextID) ++{ ++ m_client->deleteBrowserContext(error, browserContextID); ++} ++ ++void InspectorBrowserAgent::createPage(ErrorString& error, const String* browserContextID, String* targetID) ++{ ++ RefPtr page = m_client->createPage(error, browserContextID); ++ if (page == nullptr) ++ return; ++ ++ *targetID = WebPageInspectorTarget::toTargetID(page->webPageID()); ++} ++ ++String InspectorBrowserAgent::toBrowserContextIDProtocolString(const PAL::SessionID& sessionID) ++{ ++ StringBuilder builder; ++ appendUnsignedAsHexFixedSize(sessionID.toUInt64(), builder, 16); ++ return builder.toString(); ++} ++ ++} // namespace WebKit ++ ++#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/InspectorBrowserAgent.h b/Source/WebKit/UIProcess/InspectorBrowserAgent.h +new file mode 100644 +index 00000000000..f24c655ab39 +--- /dev/null ++++ b/Source/WebKit/UIProcess/InspectorBrowserAgent.h +@@ -0,0 +1,81 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#if ENABLE(REMOTE_INSPECTOR) ++ ++#include ++#include ++#include ++#include ++ ++namespace Inspector { ++class BackendDispatcher; ++class FrontendChannel; ++class FrontendRouter; ++} ++ ++namespace PAL { ++class SessionID; ++} ++ ++namespace WebKit { ++ ++class InspectorBrowserAgentClient; ++class WebPageInspectorController; ++ ++class InspectorBrowserAgent final : public Inspector::InspectorAgentBase, public Inspector::BrowserBackendDispatcherHandler { ++ WTF_MAKE_NONCOPYABLE(InspectorBrowserAgent); ++ WTF_MAKE_FAST_ALLOCATED; ++public: ++ InspectorBrowserAgent(Inspector::BackendDispatcher&, InspectorBrowserAgentClient*); ++ ~InspectorBrowserAgent() override; ++ ++ // InspectorAgentBase ++ void didCreateFrontendAndBackend(Inspector::FrontendRouter*, Inspector::BackendDispatcher*) override; ++ void willDestroyFrontendAndBackend(Inspector::DisconnectReason) override; ++ ++ // BrowserBackendDispatcherHandler ++ void close(Inspector::ErrorString&) override; ++ void createContext(Inspector::ErrorString&, String* browserContextID) override; ++ void deleteContext(Inspector::ErrorString&, const String& browserContextID) override; ++ void createPage(Inspector::ErrorString&, const String* browserContextID, String* targetId) override; ++ ++ static String toBrowserContextIDProtocolString(const PAL::SessionID&); ++ ++private: ++ Ref m_backendDispatcher; ++ InspectorBrowserAgentClient* m_client; ++}; ++ ++} // namespace WebKit ++ ++#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/InspectorBrowserAgentClient.h b/Source/WebKit/UIProcess/InspectorBrowserAgentClient.h +new file mode 100644 +index 00000000000..f05cd030bac +--- /dev/null ++++ b/Source/WebKit/UIProcess/InspectorBrowserAgentClient.h +@@ -0,0 +1,52 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#if ENABLE(REMOTE_INSPECTOR) ++ ++#include ++ ++namespace WebKit { ++ ++class WebPageProxy; ++ ++class InspectorBrowserAgentClient { ++public: ++ virtual ~InspectorBrowserAgentClient() = default; ++ virtual RefPtr createPage(WTF::String& error, const WTF::String* browserContextID) = 0; ++ virtual void closeAllWindows() = 0; ++ virtual void createBrowserContext(WTF::String& error, WTF::String* browserContextID) = 0; ++ virtual void deleteBrowserContext(WTF::String& error, const WTF::String& browserContextID) = 0; ++}; ++ ++} // namespace WebKit ++ ++#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/InspectorTargetProxy.cpp b/Source/WebKit/UIProcess/InspectorTargetProxy.cpp +index 1b37c1ed439..c45d45de342 100644 +--- a/Source/WebKit/UIProcess/InspectorTargetProxy.cpp ++++ b/Source/WebKit/UIProcess/InspectorTargetProxy.cpp +@@ -32,6 +32,8 @@ + #include "WebPageMessages.h" + #include "WebPageProxy.h" + #include "WebProcessProxy.h" ++#include "ProvisionalPageProxy.h" ++ + + namespace WebKit { + +@@ -39,23 +41,29 @@ using namespace Inspector; + + std::unique_ptr InspectorTargetProxy::create(WebPageProxy& page, const String& targetId, Inspector::InspectorTargetType type) + { +- return makeUnique(page, targetId, type); ++ return makeUnique(page, nullptr, targetId, type); + } + + std::unique_ptr InspectorTargetProxy::create(ProvisionalPageProxy& provisionalPage, const String& targetId, Inspector::InspectorTargetType type) + { +- auto target = InspectorTargetProxy::create(provisionalPage.page(), targetId, type); +- target->m_provisionalPage = makeWeakPtr(provisionalPage); +- return target; ++ return makeUnique(provisionalPage.page(), &provisionalPage, targetId, type); + } + +-InspectorTargetProxy::InspectorTargetProxy(WebPageProxy& page, const String& targetId, Inspector::InspectorTargetType type) ++InspectorTargetProxy::InspectorTargetProxy(WebPageProxy& page, ProvisionalPageProxy* provisionalPage, const String& targetId, Inspector::InspectorTargetType type) + : m_page(page) ++ , m_provisionalPage(makeWeakPtr(provisionalPage)) + , m_identifier(targetId) + , m_type(type) + { + } + ++String InspectorTargetProxy::url() const ++{ ++ if (m_page.provisionalPageProxy()) ++ return m_page.provisionalPageProxy()->provisionalURL().string(); ++ return m_page.pageLoadState().activeURL(); ++} ++ + void InspectorTargetProxy::connect(Inspector::FrontendChannel::ConnectionType connectionType) + { + if (m_provisionalPage) { +diff --git a/Source/WebKit/UIProcess/InspectorTargetProxy.h b/Source/WebKit/UIProcess/InspectorTargetProxy.h +index a2239cec8e1..43415afbc77 100644 +--- a/Source/WebKit/UIProcess/InspectorTargetProxy.h ++++ b/Source/WebKit/UIProcess/InspectorTargetProxy.h +@@ -37,17 +37,18 @@ class WebPageProxy; + // NOTE: This UIProcess side InspectorTarget doesn't care about the frontend channel, since + // any target -> frontend messages will be routed to the WebPageProxy with a targetId. + +-class InspectorTargetProxy final : public Inspector::InspectorTarget { ++class InspectorTargetProxy : public Inspector::InspectorTarget { + WTF_MAKE_FAST_ALLOCATED; + WTF_MAKE_NONCOPYABLE(InspectorTargetProxy); + public: + static std::unique_ptr create(WebPageProxy&, const String& targetId, Inspector::InspectorTargetType); + static std::unique_ptr create(ProvisionalPageProxy&, const String& targetId, Inspector::InspectorTargetType); +- InspectorTargetProxy(WebPageProxy&, const String& targetId, Inspector::InspectorTargetType); ++ InspectorTargetProxy(WebPageProxy&, ProvisionalPageProxy*, const String& targetId, Inspector::InspectorTargetType); + ~InspectorTargetProxy() = default; + + Inspector::InspectorTargetType type() const final { return m_type; } + String identifier() const final { return m_identifier; } ++ String url() const final; + + void didCommitProvisionalTarget(); + bool isProvisional() const override; +@@ -56,11 +57,13 @@ public: + void disconnect() override; + void sendMessageToTargetBackend(const String&) override; + +-private: ++protected: + WebPageProxy& m_page; ++ ++private: ++ WeakPtr m_provisionalPage; + String m_identifier; + Inspector::InspectorTargetType m_type; +- WeakPtr m_provisionalPage; + }; + + } // namespace WebKit +diff --git a/Source/WebKit/UIProcess/RemoteInspectorPipe.cpp b/Source/WebKit/UIProcess/RemoteInspectorPipe.cpp +new file mode 100644 +index 00000000000..87b426e9fff +--- /dev/null ++++ b/Source/WebKit/UIProcess/RemoteInspectorPipe.cpp +@@ -0,0 +1,159 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "RemoteInspectorPipe.h" ++ ++#if ENABLE(REMOTE_INSPECTOR) ++ ++#include "BrowserInspectorController.h" ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#if OS(UNIX) ++#include ++#include ++#endif ++ ++namespace WebKit { ++ ++static const int readFD = 3; ++static const int writeFD = 4; ++ ++class RemoteInspectorPipe::RemoteFrontendChannel : public Inspector::FrontendChannel { ++ WTF_MAKE_FAST_ALLOCATED; ++public: ++ RemoteFrontendChannel() ++ : m_senderQueue(WorkQueue::create("Inspector pipe writer")) ++ { ++ } ++ ++ ~RemoteFrontendChannel() override = default; ++ ++ ConnectionType connectionType() const override ++ { ++ return ConnectionType::Remote; ++ } ++ ++ void sendMessageToFrontend(const String& message) override ++ { ++ m_senderQueue->dispatch([message = message.isolatedCopy()]() { ++ dprintf(writeFD, "%s%c", message.ascii().data(), '\0'); ++ }); ++ } ++ ++private: ++ Ref m_senderQueue; ++}; ++ ++RemoteInspectorPipe::RemoteInspectorPipe(BrowserInspectorController& browserInspectorController) ++ : m_remoteFrontendChannel(makeUnique()) ++ , m_browserInspectorController(browserInspectorController) ++{ ++ start(); ++} ++ ++RemoteInspectorPipe::~RemoteInspectorPipe() ++{ ++ stop(); ++} ++ ++bool RemoteInspectorPipe::start() ++{ ++ if (m_receiverThread) ++ return true; ++ ++ m_browserInspectorController.connectFrontend(*m_remoteFrontendChannel); ++ m_terminated = false; ++ m_receiverThread = Thread::create("Inspector pipe reader", [this] { ++ workerRun(); ++ }); ++ return true; ++} ++ ++void RemoteInspectorPipe::stop() ++{ ++ if (!m_receiverThread) ++ return; ++ ++ m_browserInspectorController.disconnectFrontend(); ++ ++ m_terminated = true; ++ m_receiverThread->waitForCompletion(); ++ m_receiverThread = nullptr; ++} ++ ++void RemoteInspectorPipe::workerRun() ++{ ++ const size_t bufSize = 256 * 1024; ++ auto buffer = makeUniqueArray(bufSize); ++ Vector line; ++ while (!m_terminated) { ++ ssize_t size = read(readFD, buffer.get(), bufSize); ++ if (size == 0) { ++ break; ++ } ++ if (size < 0) { ++ break; ++ } ++ size_t start = 0; ++ size_t end = line.size(); ++ line.append(buffer.get(), size); ++ while (true) { ++ for (; end < line.size(); ++end) { ++ if (line[end] == '\0') ++ break; ++ } ++ if (end == line.size()) ++ break; ++ ++ if (end > start) { ++ String message = String::fromUTF8(line.data() + start, end - start); ++ RunLoop::main().dispatch([this, message] { ++ if (!m_terminated) ++ m_browserInspectorController.dispatchMessageFromFrontend(message); ++ }); ++ } ++ ++end; ++ start = end; ++ } ++ if (start != 0 && start < line.size()) ++ memmove(line.data(), line.data() + start, line.size() - start); ++ line.shrink(line.size() - start); ++ } ++} ++ ++} // namespace WebKit ++ ++#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/RemoteInspectorPipe.h b/Source/WebKit/UIProcess/RemoteInspectorPipe.h +new file mode 100644 +index 00000000000..37b0622557c +--- /dev/null ++++ b/Source/WebKit/UIProcess/RemoteInspectorPipe.h +@@ -0,0 +1,70 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#if ENABLE(REMOTE_INSPECTOR) ++ ++#include ++#include ++#include ++ ++namespace Inspector { ++class FrontendChannel; ++} ++ ++namespace WebKit { ++ ++class BrowserInspectorController; ++ ++class RemoteInspectorPipe { ++ WTF_MAKE_NONCOPYABLE(RemoteInspectorPipe); ++ WTF_MAKE_FAST_ALLOCATED; ++public: ++ explicit RemoteInspectorPipe(BrowserInspectorController&); ++ ~RemoteInspectorPipe(); ++ ++private: ++ class RemoteFrontendChannel; ++ ++ bool start(); ++ void stop(); ++ ++ void workerRun(); ++ ++ RefPtr m_receiverThread; ++ std::atomic m_terminated { false }; ++ std::unique_ptr m_remoteFrontendChannel; ++ BrowserInspectorController& m_browserInspectorController; ++}; ++ ++} // namespace WebKit ++ ++#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.cpp b/Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.cpp +index cce83796f13..eb019b70e0b 100644 +--- a/Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.cpp ++++ b/Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.cpp +@@ -35,6 +35,7 @@ + #include "NfcService.h" + #include "WebPageProxy.h" + #include "WebPreferencesKeys.h" ++#include "WebProcessProxy.h" + #include + #include + #include +diff --git a/Source/WebKit/UIProcess/WebPageInspectorController.cpp b/Source/WebKit/UIProcess/WebPageInspectorController.cpp +index b9a9469ab59..81129896554 100644 +--- a/Source/WebKit/UIProcess/WebPageInspectorController.cpp ++++ b/Source/WebKit/UIProcess/WebPageInspectorController.cpp +@@ -26,9 +26,11 @@ + #include "config.h" + #include "WebPageInspectorController.h" + ++#include "InspectorBrowserAgent.h" + #include "ProvisionalPageProxy.h" + #include "WebFrameProxy.h" + #include "WebPageInspectorTarget.h" ++#include "WebPageInspectorTargetProxy.h" + #include "WebPageProxy.h" + #include + #include +@@ -46,26 +48,59 @@ static String getTargetID(const ProvisionalPageProxy& provisionalPage) + return WebPageInspectorTarget::toTargetID(provisionalPage.webPageID()); + } + ++static WebPageInspectorController::CreationListener& creationListener() { ++ static NeverDestroyed listener; ++ return listener; ++} ++ ++void WebPageInspectorController::setCreationListener(CreationListener listener) ++{ ++ creationListener() = listener; ++} ++ + WebPageInspectorController::WebPageInspectorController(WebPageProxy& page) + : m_page(page) + , m_frontendRouter(FrontendRouter::create()) + , m_backendDispatcher(BackendDispatcher::create(m_frontendRouter.copyRef())) + { +- auto targetAgent = makeUnique(m_frontendRouter.get(), m_backendDispatcher.get()); ++ String browserContextID; ++#if ENABLE(REMOTE_INSPECTOR) ++ browserContextID = InspectorBrowserAgent::toBrowserContextIDProtocolString(page.sessionID()); ++#endif ++ auto targetAgent = makeUnique(m_frontendRouter.get(), m_backendDispatcher.get(), browserContextID); + + m_targetAgent = targetAgent.get(); + + m_agents.append(WTFMove(targetAgent)); ++ ++ if (creationListener()) ++ creationListener()(*this); + } + + void WebPageInspectorController::init() + { ++ // window.open will create page with already running process. ++ if (!m_page.hasRunningProcess()) ++ return; + String pageTargetId = WebPageInspectorTarget::toTargetID(m_page.webPageID()); + createInspectorTarget(pageTargetId, Inspector::InspectorTargetType::Page); + } + ++void WebPageInspectorController::didFinishAttachingToWebProcess() ++{ ++ String pageTargetID = WebPageInspectorTarget::toTargetID(m_page.webPageID()); ++ // Create target only after attaching to a Web Process first time. Before that ++ // we cannot event establish frontend connection. ++ if (m_targets.contains(pageTargetID)) ++ return; ++ createInspectorTarget(pageTargetID, Inspector::InspectorTargetType::Page); ++} ++ + void WebPageInspectorController::pageClosed() + { ++ String pageTargetId = WebPageInspectorTarget::toTargetID(m_page.webPageID()); ++ destroyInspectorTarget(pageTargetId); ++ + disconnectAllFrontends(); + + m_agents.discardValues(); +@@ -134,6 +169,16 @@ void WebPageInspectorController::dispatchMessageFromFrontend(const String& messa + m_backendDispatcher->dispatch(message); + } + ++bool WebPageInspectorController::dispatchMessageToTargetBackend(const String& message) ++{ ++ return m_backendDispatcher->dispatch(message, BackendDispatcher::Mode::ContinueIfDomainIsMissing) == BackendDispatcher::DispatchResult::Finished; ++} ++ ++Inspector::InspectorTarget* WebPageInspectorController::findTarget(const String& targetId) ++{ ++ return m_targets.get(targetId); ++} ++ + #if ENABLE(REMOTE_INSPECTOR) + void WebPageInspectorController::setIndicating(bool indicating) + { +@@ -150,7 +195,12 @@ void WebPageInspectorController::setIndicating(bool indicating) + + void WebPageInspectorController::createInspectorTarget(const String& targetId, Inspector::InspectorTargetType type) + { +- addTarget(InspectorTargetProxy::create(m_page, targetId, type)); ++ std::unique_ptr target; ++ if (type == Inspector::InspectorTargetType::Page) ++ target = WebPageInspectorTargetProxy::create(m_page, *m_targetAgent, targetId); ++ else ++ target = InspectorTargetProxy::create(m_page, targetId, type); ++ addTarget(WTFMove(target)); + } + + void WebPageInspectorController::destroyInspectorTarget(const String& targetId) +@@ -169,7 +219,7 @@ void WebPageInspectorController::sendMessageToInspectorFrontend(const String& ta + + void WebPageInspectorController::didCreateProvisionalPage(ProvisionalPageProxy& provisionalPage) + { +- addTarget(InspectorTargetProxy::create(provisionalPage, getTargetID(provisionalPage), Inspector::InspectorTargetType::Page)); ++ addTarget(WebPageInspectorTargetProxy::create(provisionalPage, *m_targetAgent, getTargetID(provisionalPage))); + } + + void WebPageInspectorController::willDestroyProvisionalPage(const ProvisionalPageProxy& provisionalPage) +diff --git a/Source/WebKit/UIProcess/WebPageInspectorController.h b/Source/WebKit/UIProcess/WebPageInspectorController.h +index 828bc3ccc7e..40a333b7004 100644 +--- a/Source/WebKit/UIProcess/WebPageInspectorController.h ++++ b/Source/WebKit/UIProcess/WebPageInspectorController.h +@@ -48,7 +48,13 @@ public: + WebPageInspectorController(WebPageProxy&); + + void init(); ++ void didFinishAttachingToWebProcess(); ++ ++ using CreationListener = std::function; ++ static void setCreationListener(CreationListener); ++ + void pageClosed(); ++ void didProcessAllPendingKeyboardEvents(); + + bool hasLocalFrontend() const; + +@@ -57,6 +63,8 @@ public: + void disconnectAllFrontends(); + + void dispatchMessageFromFrontend(const String& message); ++ Inspector::InspectorTarget* findTarget(const String& targetId); ++ bool dispatchMessageToTargetBackend(const String&); + + #if ENABLE(REMOTE_INSPECTOR) + void setIndicating(bool); +diff --git a/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.cpp b/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.cpp +new file mode 100644 +index 00000000000..415f36c5647 +--- /dev/null ++++ b/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.cpp +@@ -0,0 +1,61 @@ ++/* ++ * Copyright (C) 2018 Apple Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY ++ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE ++ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ++ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, ++ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, ++ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR ++ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY ++ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorEmulationAgent.h" ++ ++#include "WebPageProxy.h" ++#include ++ ++ ++namespace WebKit { ++ ++using namespace Inspector; ++ ++WebPageInspectorEmulationAgent::WebPageInspectorEmulationAgent(BackendDispatcher& backendDispatcher, WebPageProxy& page) ++ : InspectorAgentBase("Emulation"_s) ++ , m_backendDispatcher(EmulationBackendDispatcher::create(backendDispatcher, this)) ++ , m_page(page) ++{ ++} ++ ++WebPageInspectorEmulationAgent::~WebPageInspectorEmulationAgent() ++{ ++} ++ ++void WebPageInspectorEmulationAgent::didCreateFrontendAndBackend(FrontendRouter*, BackendDispatcher*) ++{ ++} ++ ++void WebPageInspectorEmulationAgent::willDestroyFrontendAndBackend(DisconnectReason) ++{ ++} ++ ++void WebPageInspectorEmulationAgent::setDeviceMetricsOverride(ErrorString& error, int in_width, int in_height) ++{ ++ platformSetSize(error, in_width, in_height); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.h b/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.h +new file mode 100644 +index 00000000000..2f9b7807e23 +--- /dev/null ++++ b/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.h +@@ -0,0 +1,63 @@ ++/* ++ * Copyright (C) 2018 Apple Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY ++ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE ++ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ++ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, ++ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, ++ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR ++ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY ++ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#include ++#include ++ ++#include ++#include ++ ++namespace Inspector { ++class BackendDispatcher; ++class FrontendChannel; ++class FrontendRouter; ++} ++ ++namespace WebKit { ++ ++class WebPageProxy; ++ ++class WebPageInspectorEmulationAgent : public Inspector::InspectorAgentBase, public Inspector::EmulationBackendDispatcherHandler { ++ WTF_MAKE_NONCOPYABLE(WebPageInspectorEmulationAgent); ++ WTF_MAKE_FAST_ALLOCATED; ++public: ++ WebPageInspectorEmulationAgent(Inspector::BackendDispatcher& backendDispatcher, WebPageProxy& page); ++ ~WebPageInspectorEmulationAgent() override; ++ ++ void didCreateFrontendAndBackend(Inspector::FrontendRouter*, Inspector::BackendDispatcher*) override; ++ void willDestroyFrontendAndBackend(Inspector::DisconnectReason) override; ++ ++ void setDeviceMetricsOverride(Inspector::ErrorString&, int in_width, int in_height) override; ++ ++private: ++ void platformSetSize(String& error, int width, int height); ++ ++ Ref m_backendDispatcher; ++ WebPageProxy& m_page; ++}; ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/WebPageInspectorInputAgent.cpp b/Source/WebKit/UIProcess/WebPageInspectorInputAgent.cpp +new file mode 100644 +index 00000000000..16a05604460 +--- /dev/null ++++ b/Source/WebKit/UIProcess/WebPageInspectorInputAgent.cpp +@@ -0,0 +1,257 @@ ++/* ++ * Copyright (C) 2018 Apple Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY ++ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE ++ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ++ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, ++ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, ++ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR ++ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY ++ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorInputAgent.h" ++ ++#include "APINavigation.h" ++#include "NativeWebKeyboardEvent.h" ++#include "NativeWebMouseEvent.h" ++#include "WebPageProxy.h" ++#include ++ ++ ++namespace WebKit { ++ ++using namespace Inspector; ++ ++namespace { ++ ++template ++class CallbackList { ++ WTF_MAKE_FAST_ALLOCATED; ++public: ++ ~CallbackList() ++ { ++ for (const auto& callback : m_callbacks) ++ callback->sendFailure("Page closed"); ++ } ++ ++ void append(Ref&& callback) ++ { ++ m_callbacks.append(WTFMove(callback)); ++ } ++ ++ void sendSuccess() ++ { ++ for (const auto& callback : m_callbacks) ++ callback->sendSuccess(); ++ m_callbacks.clear(); ++ } ++ ++private: ++ Vector> m_callbacks; ++}; ++ ++} // namespace ++ ++class WebPageInspectorInputAgent::InspectorInputObserver : public WebPageProxy::InputProcessingObserver { ++ WTF_MAKE_FAST_ALLOCATED; ++public: ++ using KeyboardCallback = Inspector::InputBackendDispatcherHandler::DispatchKeyEventCallback; ++ using MouseCallback = Inspector::InputBackendDispatcherHandler::DispatchMouseEventCallback; ++ ++ ~InspectorInputObserver() override = default; ++ ++ void didProcessAllPendingKeyboardEvents() override ++ { ++ m_keyboardCallbacks.sendSuccess(); ++ } ++ ++ void didProcessAllPendingMouseEvents() override ++ { ++ m_mouseCallbacks.sendSuccess(); ++ } ++ ++ void addMouseCallback(Ref&& callback) ++ { ++ m_mouseCallbacks.append(WTFMove(callback)); ++ } ++ ++ void addKeyboardCallback(Ref&& callback) ++ { ++ m_keyboardCallbacks.append(WTFMove(callback)); ++ } ++ ++private: ++ CallbackList m_keyboardCallbacks; ++ CallbackList m_mouseCallbacks; ++}; ++ ++WebPageInspectorInputAgent::WebPageInspectorInputAgent(Inspector::BackendDispatcher& backendDispatcher, WebPageProxy& page) ++ : InspectorAgentBase("Input"_s) ++ , m_backendDispatcher(InputBackendDispatcher::create(backendDispatcher, this)) ++ , m_page(page) ++ , m_inputObserver(makeUnique()) ++{ ++ m_page.setObserber(m_inputObserver.get()); ++} ++ ++WebPageInspectorInputAgent::~WebPageInspectorInputAgent() ++{ ++ m_page.setObserber(nullptr); ++} ++ ++void WebPageInspectorInputAgent::didCreateFrontendAndBackend(Inspector::FrontendRouter*, Inspector::BackendDispatcher*) ++{ ++} ++ ++void WebPageInspectorInputAgent::willDestroyFrontendAndBackend(Inspector::DisconnectReason) ++{ ++} ++ ++void WebPageInspectorInputAgent::dispatchKeyEvent(const String& in_type, const int* opt_in_modifiers, const String* opt_in_text, const String* opt_in_unmodifiedText, const String* opt_in_code, const String* opt_in_key, const int* opt_in_windowsVirtualKeyCode, const int* opt_in_nativeVirtualKeyCode, const bool* opt_in_autoRepeat, const bool* opt_in_isKeypad, const bool* opt_in_isSystemKey, Ref&& callback) ++{ ++ WebKit::WebEvent::Type type; ++ if (in_type == "keyDown") { ++ type = WebKit::WebEvent::KeyDown; ++ } else if (in_type == "keyUp") { ++ type = WebKit::WebEvent::KeyUp; ++ } else { ++ callback->sendFailure("Unsupported event type."); ++ return; ++ } ++ OptionSet modifiers; ++ if (opt_in_modifiers) ++ modifiers = modifiers.fromRaw(*opt_in_modifiers); ++ String text; ++ if (opt_in_text) ++ text = *opt_in_text; ++ String unmodifiedText; ++ if (opt_in_unmodifiedText) ++ unmodifiedText = *opt_in_unmodifiedText; ++ String code; ++ if (opt_in_code) ++ code = *opt_in_code; ++ String key; ++ if (opt_in_key) ++ key = *opt_in_key; ++ int windowsVirtualKeyCode = 0; ++ if (opt_in_windowsVirtualKeyCode) ++ windowsVirtualKeyCode = *opt_in_windowsVirtualKeyCode; ++ int nativeVirtualKeyCode = 0; ++ if (opt_in_nativeVirtualKeyCode) ++ nativeVirtualKeyCode = *opt_in_nativeVirtualKeyCode; ++ bool isAutoRepeat = false; ++ if (opt_in_autoRepeat) ++ isAutoRepeat = *opt_in_autoRepeat; ++ bool isKeypad = false; ++ if (opt_in_isKeypad) ++ isKeypad = *opt_in_isKeypad; ++ bool isSystemKey = false; ++ if (opt_in_isSystemKey) ++ isSystemKey = *opt_in_isSystemKey; ++ WallTime timestamp = WallTime::now(); ++ ++ m_inputObserver->addKeyboardCallback(WTFMove(callback)); ++ platformDispatchKeyEvent( ++ type, ++ text, ++ unmodifiedText, ++ key, ++ code, ++ windowsVirtualKeyCode, ++ nativeVirtualKeyCode, ++ isAutoRepeat, ++ isKeypad, ++ isSystemKey, ++ modifiers, ++ timestamp); ++} ++ ++void WebPageInspectorInputAgent::dispatchMouseEvent(const String& in_type, int in_x, int in_y, const int* opt_in_modifiers, const String* opt_in_button, const int* opt_in_buttons, const int* opt_in_clickCount, const int* opt_in_deltaX, const int* opt_in_deltaY, Ref&& callback) ++{ ++ WebEvent::Type type = WebEvent::NoType; ++ if (in_type == "down") ++ type = WebEvent::MouseDown; ++ else if (in_type == "up") ++ type = WebEvent::MouseUp; ++ else if (in_type == "move") ++ type = WebEvent::MouseMove; ++ else { ++ callback->sendFailure("Unsupported event type"); ++ return; ++ } ++ ++ OptionSet modifiers; ++ if (opt_in_modifiers) ++ modifiers = modifiers.fromRaw(*opt_in_modifiers); ++ ++ WebMouseEvent::Button button = WebMouseEvent::NoButton; ++ if (opt_in_button) { ++ if (*opt_in_button == "left") ++ button = WebMouseEvent::LeftButton; ++ else if (*opt_in_button == "middle") ++ button = WebMouseEvent::MiddleButton; ++ else if (*opt_in_button == "right") ++ button = WebMouseEvent::RightButton; ++ else if (*opt_in_button == "none") ++ button = WebMouseEvent::NoButton; ++ else { ++ callback->sendFailure("Unsupported button"); ++ return; ++ } ++ } ++ ++ unsigned short buttons = 0; ++ if (opt_in_buttons) ++ buttons = *opt_in_buttons; ++ ++ int clickCount = 0; ++ if (opt_in_clickCount) ++ clickCount = *opt_in_clickCount; ++ int deltaX = 0; ++ if (opt_in_deltaX) ++ deltaX = *opt_in_deltaX; ++ int deltaY = 0; ++ if (opt_in_deltaY) ++ deltaY = *opt_in_deltaY; ++ m_inputObserver->addMouseCallback(WTFMove(callback)); ++#if PLATFORM(WPE) ++ platformDispatchMouseEvent(type, in_x, in_y, button, modifiers); ++#elif PLATFORM(GTK) ++ WallTime timestamp = WallTime::now(); ++ NativeWebMouseEvent event( ++ type, ++ button, ++ buttons, ++ {in_x, in_y}, ++ WebCore::IntPoint(), ++ deltaX, ++ deltaY, ++ 0, ++ clickCount, ++ modifiers, ++ timestamp); ++ m_page.handleMouseEvent(event); ++#endif ++} ++ ++void WebPageInspectorInputAgent::goBack(Inspector::ErrorString&) ++{ ++ auto navigation = m_page.goBack(); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/WebPageInspectorInputAgent.h b/Source/WebKit/UIProcess/WebPageInspectorInputAgent.h +new file mode 100644 +index 00000000000..9d51a913b23 +--- /dev/null ++++ b/Source/WebKit/UIProcess/WebPageInspectorInputAgent.h +@@ -0,0 +1,76 @@ ++/* ++ * Copyright (C) 2018 Apple Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY ++ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE ++ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ++ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, ++ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, ++ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR ++ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY ++ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#include "WebEvent.h" ++ ++#include ++#include ++ ++#include ++#include ++ ++namespace Inspector { ++class BackendDispatcher; ++class FrontendChannel; ++class FrontendRouter; ++} ++ ++namespace WebKit { ++ ++class NativeWebKeyboardEvent; ++class WebPageProxy; ++ ++class WebPageInspectorInputAgent : public Inspector::InspectorAgentBase, public Inspector::InputBackendDispatcherHandler { ++ WTF_MAKE_NONCOPYABLE(WebPageInspectorInputAgent); ++ WTF_MAKE_FAST_ALLOCATED; ++public: ++ WebPageInspectorInputAgent(Inspector::BackendDispatcher& backendDispatcher, WebPageProxy& page); ++ ~WebPageInspectorInputAgent() override; ++ ++ void didCreateFrontendAndBackend(Inspector::FrontendRouter*, Inspector::BackendDispatcher*) override; ++ void willDestroyFrontendAndBackend(Inspector::DisconnectReason) override; ++ ++ void dispatchKeyEvent(const String& in_type, const int* opt_in_modifiers, const String* opt_in_text, const String* opt_in_unmodifiedText, const String* opt_in_code, const String* opt_in_key, const int* opt_in_windowsVirtualKeyCode, const int* opt_in_nativeVirtualKeyCode, const bool* opt_in_autoRepeat, const bool* opt_in_isKeypad, const bool* opt_in_isSystemKey, Ref&& callback) override; ++ void dispatchMouseEvent(const String& in_type, int in_x, int in_y, const int* opt_in_modifiers, const String* opt_in_button, const int* opt_in_buttons, const int* opt_in_clickCount, const int* opt_in_deltaX, const int* opt_in_deltaY, Ref&& callback) override; ++ void goBack(Inspector::ErrorString&) override; ++ ++private: ++ void platformDispatchKeyEvent(WebKeyboardEvent::Type type, const String& text, const String& unmodifiedText, const String& key, const String& code, int windowsVirtualKeyCode, int nativeVirtualKeyCode, bool isAutoRepeat, bool isKeypad, bool isSystemKey, OptionSet modifiers, WallTime timestamp); ++#if PLATFORM(WPE) ++ void platformDispatchMouseEvent(WebMouseEvent::Type type, int x, int y, WebMouseEvent::Button button, OptionSet modifiers); ++#endif ++ ++ Ref m_backendDispatcher; ++ WebPageProxy& m_page; ++ // Keep track of currently active modifiers across multiple keystrokes. ++ // Most platforms do not track current modifiers from synthesized events. ++ unsigned m_currentModifiers { 0 }; ++ class InspectorInputObserver; ++ std::unique_ptr m_inputObserver; ++}; ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/WebPageInspectorTargetProxy.cpp b/Source/WebKit/UIProcess/WebPageInspectorTargetProxy.cpp +new file mode 100644 +index 00000000000..a697c3f5355 +--- /dev/null ++++ b/Source/WebKit/UIProcess/WebPageInspectorTargetProxy.cpp +@@ -0,0 +1,129 @@ ++/* ++ * Copyright (C) 2018 Apple Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY ++ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE ++ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ++ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, ++ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, ++ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR ++ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY ++ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorTargetProxy.h" ++ ++#include "ProvisionalPageProxy.h" ++#include "WebPageInspectorController.h" ++#include "WebPageInspectorEmulationAgent.h" ++#include "WebPageInspectorInputAgent.h" ++#include "WebPageMessages.h" ++#include "WebPageProxy.h" ++#include "WebProcessProxy.h" ++#include ++#include ++#include ++#include ++ ++namespace WebKit { ++ ++using namespace Inspector; ++ ++namespace { ++ ++class TargetFrontendChannel final : public FrontendChannel { ++ WTF_MAKE_NONCOPYABLE(TargetFrontendChannel); ++public: ++ TargetFrontendChannel(InspectorTargetAgent& targetAgent, const String& targetId, FrontendChannel::ConnectionType type) ++ : m_targetAgent(targetAgent) ++ , m_targetId(targetId) ++ , m_connectionType(type) ++ { ++ } ++ ~TargetFrontendChannel() override = default; ++ ++ ConnectionType connectionType() const override { return m_connectionType; } ++ void sendMessageToFrontend(const String& message) override ++ { ++ m_targetAgent.sendMessageFromTargetToFrontend(m_targetId, message); ++ } ++ ++private: ++ InspectorTargetAgent& m_targetAgent; ++ String m_targetId; ++ FrontendChannel::ConnectionType m_connectionType; ++}; ++ ++} // namespace ++ ++std::unique_ptr WebPageInspectorTargetProxy::create(WebPageProxy& page, Inspector::InspectorTargetAgent& targetAgent, const String& targetId) ++{ ++ return makeUnique(page, nullptr, targetAgent, targetId); ++} ++ ++std::unique_ptr WebPageInspectorTargetProxy::create(ProvisionalPageProxy& provisionalPage, Inspector::InspectorTargetAgent& targetAgent, const String& targetId) ++{ ++ return makeUnique(provisionalPage.page(), &provisionalPage, targetAgent, targetId); ++} ++ ++WebPageInspectorTargetProxy::WebPageInspectorTargetProxy(WebPageProxy& page, ProvisionalPageProxy* provisionalPage, Inspector::InspectorTargetAgent& targetAgent, const String& targetId) ++ : InspectorTargetProxy(page, provisionalPage, targetId, Inspector::InspectorTargetType::Page) ++ , m_frontendRouter(FrontendRouter::create()) ++ , m_backendDispatcher(BackendDispatcher::create(m_frontendRouter.copyRef())) ++ , m_targetAgent(targetAgent) ++{ ++ m_agents.append(std::make_unique(m_backendDispatcher.get(), page)); ++ m_agents.append(std::make_unique(m_backendDispatcher.get(), page)); ++} ++ ++void WebPageInspectorTargetProxy::connect(Inspector::FrontendChannel::ConnectionType connectionType) ++{ ++ InspectorTargetProxy::connect(connectionType); ++ ASSERT(!m_frontendChannel); ++ if (m_frontendChannel) ++ return; ++ m_frontendChannel = std::make_unique(m_targetAgent, identifier(), connectionType); ++ m_frontendRouter->connectFrontend(*m_frontendChannel); ++} ++ ++void WebPageInspectorTargetProxy::disconnect() ++{ ++ ASSERT(m_frontendChannel); ++ m_frontendRouter->disconnectAllFrontends(); ++ m_frontendChannel.reset(); ++ InspectorTargetProxy::disconnect(); ++} ++ ++void WebPageInspectorTargetProxy::sendMessageToTargetBackend(const String& message) ++{ ++ if (m_backendDispatcher->dispatch(message, BackendDispatcher::Mode::ContinueIfDomainIsMissing) == BackendDispatcher::DispatchResult::Finished) ++ return; ++ if (m_page.inspectorController().dispatchMessageToTargetBackend(message)) ++ return; ++ InspectorTargetProxy::sendMessageToTargetBackend(message); ++} ++ ++void WebPageInspectorTargetProxy::activate(String& error) ++{ ++ platformActivate(error); ++} ++ ++void WebPageInspectorTargetProxy::close(String& error) ++{ ++ m_page.closePage(false); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/WebPageInspectorTargetProxy.h b/Source/WebKit/UIProcess/WebPageInspectorTargetProxy.h +new file mode 100644 +index 00000000000..0550a3d8698 +--- /dev/null ++++ b/Source/WebKit/UIProcess/WebPageInspectorTargetProxy.h +@@ -0,0 +1,67 @@ ++/* ++ * Copyright (C) 2018 Apple Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY ++ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE ++ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ++ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, ++ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, ++ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR ++ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY ++ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#include "InspectorTargetProxy.h" ++#include ++#include ++#include ++ ++namespace Inspector { ++class BackendDispatcher; ++class InspectorTargetAgent; ++} ++ ++namespace WebKit { ++ ++class WebPageProxy; ++ ++class WebPageInspectorTargetProxy final : public InspectorTargetProxy { ++ WTF_MAKE_FAST_ALLOCATED; ++ WTF_MAKE_NONCOPYABLE(WebPageInspectorTargetProxy); ++public: ++ static std::unique_ptr create(WebPageProxy&, Inspector::InspectorTargetAgent&, const String& targetId); ++ static std::unique_ptr create(ProvisionalPageProxy&, Inspector::InspectorTargetAgent&, const String& targetId); ++ WebPageInspectorTargetProxy(WebPageProxy&, ProvisionalPageProxy*, Inspector::InspectorTargetAgent&, const String& targetId); ++ ~WebPageInspectorTargetProxy() = default; ++ ++ void connect(Inspector::FrontendChannel::ConnectionType) override; ++ void disconnect() override; ++ void sendMessageToTargetBackend(const String&) override; ++ void activate(String& error) override; ++ void close(String& error) override; ++ ++private: ++ void platformActivate(String& error) const; ++ ++ Ref m_frontendRouter; ++ Ref m_backendDispatcher; ++ Inspector::InspectorTargetAgent& m_targetAgent; ++ Inspector::AgentRegistry m_agents; ++ std::unique_ptr m_frontendChannel; ++}; ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp +index c6b66746b18..3bed21548da 100644 +--- a/Source/WebKit/UIProcess/WebPageProxy.cpp ++++ b/Source/WebKit/UIProcess/WebPageProxy.cpp +@@ -865,6 +865,7 @@ void WebPageProxy::finishAttachingToWebProcess(ProcessLaunchReason reason) + m_pageLoadState.didSwapWebProcesses(); + if (reason != ProcessLaunchReason::InitialProcess) + m_drawingArea->waitForBackingStoreUpdateOnNextPaint(); ++ m_inspectorController->didFinishAttachingToWebProcess(); + } + + void WebPageProxy::didAttachToRunningProcess() +@@ -1616,6 +1617,11 @@ void WebPageProxy::setControlledByAutomation(bool controlled) + m_process->processPool().sendToNetworkingProcess(Messages::NetworkProcess::SetSessionIsControlledByAutomation(m_websiteDataStore->sessionID(), m_controlledByAutomation)); + } + ++void WebPageProxy::setObserber(InputProcessingObserver* observer) ++{ ++ m_inputProcessingObserver = observer; ++} ++ + void WebPageProxy::createInspectorTarget(const String& targetId, Inspector::InspectorTargetType type) + { + m_inspectorController->createInspectorTarget(targetId, type); +@@ -6509,6 +6515,8 @@ void WebPageProxy::didReceiveEvent(uint32_t opaqueType, bool handled) + if (auto* automationSession = process().processPool().automationSession()) + automationSession->mouseEventsFlushedForPage(*this); + pageClient().didFinishProcessingAllPendingMouseEvents(); ++ if (m_inputProcessingObserver) ++ m_inputProcessingObserver->didProcessAllPendingMouseEvents(); + } + + break; +@@ -6535,7 +6543,6 @@ void WebPageProxy::didReceiveEvent(uint32_t opaqueType, bool handled) + case WebEvent::RawKeyDown: + case WebEvent::Char: { + LOG(KeyHandling, "WebPageProxy::didReceiveEvent: %s (queue empty %d)", webKeyboardEventTypeString(type), m_keyEventQueue.isEmpty()); +- + MESSAGE_CHECK(m_process, !m_keyEventQueue.isEmpty()); + NativeWebKeyboardEvent event = m_keyEventQueue.takeFirst(); + +@@ -6550,7 +6557,6 @@ void WebPageProxy::didReceiveEvent(uint32_t opaqueType, bool handled) + // The call to doneWithKeyEvent may close this WebPage. + // Protect against this being destroyed. + Ref protect(*this); +- + pageClient().doneWithKeyEvent(event, handled); + if (!handled) + m_uiClient->didNotHandleKeyEvent(this, event); +@@ -6559,6 +6565,8 @@ void WebPageProxy::didReceiveEvent(uint32_t opaqueType, bool handled) + if (!canProcessMoreKeyEvents) { + if (auto* automationSession = process().processPool().automationSession()) + automationSession->keyboardEventsFlushedForPage(*this); ++ if (m_inputProcessingObserver) ++ m_inputProcessingObserver->didProcessAllPendingKeyboardEvents(); + } + break; + } +diff --git a/Source/WebKit/UIProcess/WebPageProxy.h b/Source/WebKit/UIProcess/WebPageProxy.h +index b84fb9e0ef3..9357ec52c83 100644 +--- a/Source/WebKit/UIProcess/WebPageProxy.h ++++ b/Source/WebKit/UIProcess/WebPageProxy.h +@@ -534,6 +534,14 @@ public: + + void setPageLoadStateObserver(std::unique_ptr&&); + ++ class InputProcessingObserver { ++ public: ++ virtual ~InputProcessingObserver() = default; ++ virtual void didProcessAllPendingKeyboardEvents() = 0; ++ virtual void didProcessAllPendingMouseEvents() = 0; ++ }; ++ void setObserber(InputProcessingObserver*); ++ + void initializeWebPage(); + void setDrawingArea(std::unique_ptr&&); + +@@ -2569,6 +2577,7 @@ private: + #if ENABLE(REMOTE_INSPECTOR) + std::unique_ptr m_inspectorDebuggable; + #endif ++ InputProcessingObserver* m_inputProcessingObserver { nullptr }; + + Optional m_spellDocumentTag; + +diff --git a/Source/WebKit/UIProcess/glib/InspectorBrowserAgentClientGLib.cpp b/Source/WebKit/UIProcess/glib/InspectorBrowserAgentClientGLib.cpp +new file mode 100644 +index 00000000000..665265973c3 +--- /dev/null ++++ b/Source/WebKit/UIProcess/glib/InspectorBrowserAgentClientGLib.cpp +@@ -0,0 +1,130 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "InspectorBrowserAgentClientGLib.h" ++ ++#if ENABLE(REMOTE_INSPECTOR) ++ ++#include "InspectorBrowserAgent.h" ++#include "WebKitBrowserInspectorPrivate.h" ++#include "WebKitWebContextPrivate.h" ++#include "WebKitWebsiteDataManagerPrivate.h" ++#include "WebKitWebViewPrivate.h" ++#include "WebPageProxy.h" ++#include ++#include ++#include ++#include ++ ++namespace WebKit { ++ ++static Vector collectPages(Optional sessionID) ++{ ++ Vector result; ++ for (WebProcessPool* pool : WebProcessPool::allProcessPools()) { ++ for (auto& process : pool->processes()) { ++ for (auto* page : process->pages()) { ++ if (!sessionID || page->sessionID() == sessionID) ++ result.append(page); ++ } ++ } ++ } ++ return result; ++} ++ ++static void closeAllPages(Optional sessionID) ++{ ++ Vector pages = collectPages(sessionID); ++ for (auto* page : pages) ++ page->closePage(false); ++} ++ ++InspectorBrowserAgentClientGlib::InspectorBrowserAgentClientGlib() ++{ ++} ++ ++RefPtr InspectorBrowserAgentClientGlib::createPage(WTF::String& error, const WTF::String* browserContextID) ++{ ++ WebKitWebContext* context = webkit_web_context_get_default(); ++ if (browserContextID != nullptr) { ++ context = m_idToContext.get(*browserContextID); ++ if (context == nullptr) { ++ error = "Context with provided id not found"; ++ return nullptr; ++ } ++ } ++ RefPtr page = webkitBrowserInspectorCreateNewPageInContext(context); ++ if (page == nullptr) ++ error = "Failed to create new page in the context"; ++ return page; ++} ++ ++void InspectorBrowserAgentClientGlib::closeAllWindows() ++{ ++ closeAllPages(Optional()); ++ m_idToContext.clear(); ++ // FIXME(yurys): call g_main_loop_quit() ? ++} ++ ++static PAL::SessionID sessionIDFromContext(WebKitWebContext* context) ++{ ++ WebKitWebsiteDataManager* data_manager = webkit_web_context_get_website_data_manager(context); ++ WebsiteDataStore& websiteDataStore = webkitWebsiteDataManagerGetDataStore(data_manager); ++ return websiteDataStore.sessionID(); ++} ++ ++void InspectorBrowserAgentClientGlib::createBrowserContext(WTF::String& error, WTF::String* browserContextID) ++{ ++ GRefPtr manager = adoptGRef(webkit_website_data_manager_new_ephemeral()); ++ GRefPtr context = adoptGRef(WEBKIT_WEB_CONTEXT(g_object_new(WEBKIT_TYPE_WEB_CONTEXT, "website-data-manager", manager.get(), "process-swap-on-cross-site-navigation-enabled", true, nullptr))); ++ if (!context) { ++ error = "Failed to create GLib ephemeral context"; ++ return; ++ } ++ PAL::SessionID sessionID = sessionIDFromContext(context.get()); ++ String id = InspectorBrowserAgent::toBrowserContextIDProtocolString(sessionID); ++ m_idToContext.set(id, WTFMove(context)); ++ *browserContextID = id; ++} ++ ++void InspectorBrowserAgentClientGlib::deleteBrowserContext(WTF::String& error, const WTF::String& browserContextID) ++{ ++ GRefPtr context = m_idToContext.take(browserContextID); ++ if (context == nullptr) { ++ error = "Context with provided id not found"; ++ return; ++ } ++ closeAllPages(sessionIDFromContext(context.get())); ++} ++ ++} // namespace WebKit ++ ++#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/glib/InspectorBrowserAgentClientGLib.h b/Source/WebKit/UIProcess/glib/InspectorBrowserAgentClientGLib.h +new file mode 100644 +index 00000000000..0fefb4c55b3 +--- /dev/null ++++ b/Source/WebKit/UIProcess/glib/InspectorBrowserAgentClientGLib.h +@@ -0,0 +1,63 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#if ENABLE(REMOTE_INSPECTOR) ++ ++#include "InspectorBrowserAgentClient.h" ++#include "WebKitWebContext.h" ++#include ++#include ++#include ++#include ++ ++namespace WebKit { ++ ++class InspectorBrowserAgentClientGlib : public InspectorBrowserAgentClient { ++ WTF_MAKE_FAST_ALLOCATED; ++public: ++ InspectorBrowserAgentClientGlib(); ++ ~InspectorBrowserAgentClientGlib() override = default; ++ ++ RefPtr createPage(WTF::String& error, const WTF::String* browserContextID) override; ++ void closeAllWindows() override; ++ void createBrowserContext(WTF::String& error, WTF::String* browserContextID) override; ++ void deleteBrowserContext(WTF::String& error, const WTF::String& browserContextID) override; ++ ++private: ++ WebKitWebContext* findContext(WTF::String& error, const WTF::String& browserContextID); ++ ++ HashMap> m_idToContext; ++}; ++ ++} // namespace API ++ ++#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/gtk/WebPageInspectorEmulationAgentGtk.cpp b/Source/WebKit/UIProcess/gtk/WebPageInspectorEmulationAgentGtk.cpp +new file mode 100644 +index 00000000000..25df994c053 +--- /dev/null ++++ b/Source/WebKit/UIProcess/gtk/WebPageInspectorEmulationAgentGtk.cpp +@@ -0,0 +1,58 @@ ++/* ++ * Copyright (C) 2010 Apple Inc. All rights reserved. ++ * Portions Copyright (c) 2010 Motorola Mobility, Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' ++ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, ++ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS ++ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ++ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN ++ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ++ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF ++ * THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorEmulationAgent.h" ++ ++#include "WebPageProxy.h" ++#include ++ ++namespace WebKit { ++void WebPageInspectorEmulationAgent::platformSetSize(String& error, int width, int height) ++{ ++ GtkWidget* viewWidget = m_page.viewWidget(); ++ GtkWidget* window = gtk_widget_get_toplevel(viewWidget); ++ if (!window) { ++ error = "Cannot find parent window"; ++ return; ++ } ++ if (!GTK_IS_WINDOW(window)) { ++ error = "Toplevel is not a window"; ++ return; ++ } ++ GtkAllocation viewAllocation; ++ gtk_widget_get_allocation(viewWidget, &viewAllocation); ++ ++ GtkAllocation windowAllocation; ++ gtk_widget_get_allocation(window, &windowAllocation); ++ ++ width += windowAllocation.width - viewAllocation.width; ++ height += windowAllocation.height - viewAllocation.height; ++ ++ gtk_window_resize(GTK_WINDOW(window), width, height); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/gtk/WebPageInspectorInputAgentGtk.cpp b/Source/WebKit/UIProcess/gtk/WebPageInspectorInputAgentGtk.cpp +new file mode 100644 +index 00000000000..2427ea22acf +--- /dev/null ++++ b/Source/WebKit/UIProcess/gtk/WebPageInspectorInputAgentGtk.cpp +@@ -0,0 +1,108 @@ ++/* ++ * Copyright (C) 2010 Apple Inc. All rights reserved. ++ * Portions Copyright (c) 2010 Motorola Mobility, Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' ++ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, ++ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS ++ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ++ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN ++ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ++ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF ++ * THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorInputAgent.h" ++ ++#include "KeyBindingTranslator.h" ++#include "NativeWebKeyboardEvent.h" ++#include "WebPageProxy.h" ++#include ++ ++namespace WebKit { ++ ++static Vector commandsForKeyEvent(GdkEventType type, unsigned keyVal, unsigned state) ++{ ++ ASSERT(type == GDK_KEY_PRESS || type == GDK_KEY_RELEASE); ++ ++ GUniquePtr event(gdk_event_new(type)); ++ event->key.keyval = keyVal; ++ event->key.time = GDK_CURRENT_TIME; ++ event->key.state = state; ++ // When synthesizing an event, an invalid hardware_keycode value can cause it to be badly processed by GTK+. ++ GUniqueOutPtr keys; ++ int keysCount; ++ if (gdk_keymap_get_entries_for_keyval(gdk_keymap_get_default(), keyVal, &keys.outPtr(), &keysCount) && keysCount) ++ event->key.hardware_keycode = keys.get()[0].keycode; ++ return KeyBindingTranslator().commandsForKeyEvent(&event->key); ++} ++ ++static unsigned modifiersToEventState(OptionSet modifiers) ++{ ++ unsigned state = 0; ++ if (modifiers.contains(WebEvent::Modifier::ControlKey)) ++ state |= GDK_CONTROL_MASK; ++ if (modifiers.contains(WebEvent::Modifier::ShiftKey)) ++ state |= GDK_SHIFT_MASK; ++ if (modifiers.contains(WebEvent::Modifier::AltKey)) ++ state |= GDK_META_MASK; ++ if (modifiers.contains(WebEvent::Modifier::CapsLockKey)) ++ state |= GDK_LOCK_MASK; ++ return state; ++} ++ ++void WebPageInspectorInputAgent::platformDispatchKeyEvent(WebKeyboardEvent::Type type, const String& text, const String& unmodifiedText, const String& key, const String& code, int windowsVirtualKeyCode, int nativeVirtualKeyCode, bool isAutoRepeat, bool isKeypad, bool isSystemKey, OptionSet modifiers, WallTime timestamp) ++{ ++ Vector commands; ++ const guint keyVal = WebCore::PlatformKeyboardEvent::gdkKeyCodeForWindowsKeyCode(windowsVirtualKeyCode); ++ String keyIdentifier; ++ if (keyVal) { ++ GdkEventType event = GDK_NOTHING; ++ switch (type) ++ { ++ case WebKeyboardEvent::KeyDown: ++ event = GDK_KEY_PRESS; ++ break; ++ case WebKeyboardEvent::KeyUp: ++ event = GDK_KEY_RELEASE; ++ break; ++ default: ++ fprintf(stderr, "Unsupported event type = %d\n", type); ++ break; ++ } ++ unsigned state = modifiersToEventState(modifiers); ++ commands = commandsForKeyEvent(event, keyVal, state); ++ keyIdentifier = WebCore::PlatformKeyboardEvent::keyIdentifierForGdkKeyCode(keyVal); ++ } ++ NativeWebKeyboardEvent event( ++ type, ++ text, ++ unmodifiedText, ++ key, ++ code, ++ keyIdentifier, ++ windowsVirtualKeyCode, ++ nativeVirtualKeyCode, ++ isAutoRepeat, ++ isKeypad, ++ isSystemKey, ++ modifiers, ++ timestamp, ++ WTFMove(commands)); ++ m_page.handleKeyboardEvent(event); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/gtk/WebPageInspectorTargetProxyGtk.cpp b/Source/WebKit/UIProcess/gtk/WebPageInspectorTargetProxyGtk.cpp +new file mode 100644 +index 00000000000..b6981cae157 +--- /dev/null ++++ b/Source/WebKit/UIProcess/gtk/WebPageInspectorTargetProxyGtk.cpp +@@ -0,0 +1,45 @@ ++/* ++ * Copyright (C) 2010 Apple Inc. All rights reserved. ++ * Portions Copyright (c) 2010 Motorola Mobility, Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' ++ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, ++ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS ++ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ++ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN ++ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ++ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF ++ * THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorTargetProxy.h" ++ ++#include "WebPageProxy.h" ++#include ++#include ++ ++namespace WebKit { ++ ++void WebPageInspectorTargetProxy::platformActivate(String& error) const ++{ ++ GtkWidget* parent = gtk_widget_get_toplevel(m_page.viewWidget()); ++ if (WebCore::widgetIsOnscreenToplevelWindow(parent)) ++ gtk_window_present(GTK_WINDOW(parent)); ++ else ++ error = "The view is not on screen"; ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/ios/PageClientImplIOS.mm b/Source/WebKit/UIProcess/ios/PageClientImplIOS.mm +index e139968d4f0..98093c684db 100644 +--- a/Source/WebKit/UIProcess/ios/PageClientImplIOS.mm ++++ b/Source/WebKit/UIProcess/ios/PageClientImplIOS.mm +@@ -408,6 +408,8 @@ IntRect PageClientImpl::rootViewToAccessibilityScreen(const IntRect& rect) + + void PageClientImpl::doneWithKeyEvent(const NativeWebKeyboardEvent& event, bool eventWasHandled) + { ++ if (!event.nativeEvent()) ++ return; + [m_contentView _didHandleKeyEvent:event.nativeEvent() eventWasHandled:eventWasHandled]; + } + +diff --git a/Source/WebKit/UIProcess/mac/InspectorBrowserAgentClientMac.h b/Source/WebKit/UIProcess/mac/InspectorBrowserAgentClientMac.h +new file mode 100644 +index 00000000000..719a0bb54d7 +--- /dev/null ++++ b/Source/WebKit/UIProcess/mac/InspectorBrowserAgentClientMac.h +@@ -0,0 +1,56 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#pragma once ++ ++#include "InspectorBrowserAgentClient.h" ++#include ++ ++OBJC_PROTOCOL(_WKBrowserInspectorDelegate); ++ ++namespace WebKit { ++ ++class InspectorBrowserAgentClientMac : public InspectorBrowserAgentClient { ++ WTF_MAKE_FAST_ALLOCATED; ++public: ++ InspectorBrowserAgentClientMac(_WKBrowserInspectorDelegate* delegate); ++ ~InspectorBrowserAgentClientMac() override = default; ++ ++ RefPtr createPage(WTF::String& error, const WTF::String* browserContextID) override; ++ void closeAllWindows() override; ++ void createBrowserContext(WTF::String& error, WTF::String* browserContextID) override; ++ void deleteBrowserContext(WTF::String& error, const WTF::String& browserContextID) override; ++ private: ++ ++ _WKBrowserInspectorDelegate* delegate_; ++}; ++ ++ ++} // namespace API +diff --git a/Source/WebKit/UIProcess/mac/InspectorBrowserAgentClientMac.mm b/Source/WebKit/UIProcess/mac/InspectorBrowserAgentClientMac.mm +new file mode 100644 +index 00000000000..8426bd70ba9 +--- /dev/null ++++ b/Source/WebKit/UIProcess/mac/InspectorBrowserAgentClientMac.mm +@@ -0,0 +1,95 @@ ++/* ++ * Copyright (C) 2019 Microsoft Corporation. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions are ++ * met: ++ * ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above ++ * copyright notice, this list of conditions and the following disclaimer ++ * in the documentation and/or other materials provided with the ++ * distribution. ++ * * Neither the name of Microsoft Corporation nor the names of its ++ * contributors may be used to endorse or promote products derived from ++ * this software without specific prior written permission. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#import "config.h" ++#import "InspectorBrowserAgentClientMac.h" ++ ++#import ++#import ++#import "WebPageProxy.h" ++#import "WebProcessPool.h" ++#import "_WKBrowserInspector.h" ++#import "WKWebView.h" ++#import "WKWebViewInternal.h" ++ ++namespace WebKit { ++ ++/* ++static Vector collectPages(Optional sessionID) ++{ ++ Vector result; ++ for (WebProcessPool* pool : WebProcessPool::allProcessPools()) { ++ for (auto& process : pool->processes()) { ++ for (auto* page : process->pages()) { ++ if (!sessionID || page->sessionID() == sessionID) ++ result.append(page); ++ } ++ } ++ } ++ return result; ++} ++ ++static void closeAllPages(Optional sessionID) ++{ ++ Vector pages = collectPages(sessionID); ++ for (auto* page : pages) ++ page->closePage(false); ++} ++*/ ++ ++InspectorBrowserAgentClientMac::InspectorBrowserAgentClientMac(_WKBrowserInspectorDelegate* delegate) ++ : delegate_(delegate) ++{ ++} ++ ++RefPtr InspectorBrowserAgentClientMac::createPage(WTF::String& error, const WTF::String* browserContextID) ++{ ++ WKWebView *webView = [delegate_ createNewPage]; ++ return [webView _page]; ++} ++ ++void InspectorBrowserAgentClientMac::closeAllWindows() ++{ ++ [delegate_ quit]; ++} ++ ++void InspectorBrowserAgentClientMac::createBrowserContext(WTF::String& error, WTF::String* browserContextID) ++{ ++ error = "Failed to create Mac ephemeral context"; ++ fprintf(stderr, "InspectorBrowserAgentClientMac::createBrowserContext - NOT IMPLEMENTED\n"); ++} ++ ++void InspectorBrowserAgentClientMac::deleteBrowserContext(WTF::String& error, const WTF::String& browserContextID) ++{ ++ error = "Context with provided id not found"; ++ fprintf(stderr, "InspectorBrowserAgentClientMac::deleteBrowserContext - NOT IMPLEMENTED\n"); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/mac/PageClientImplMac.mm b/Source/WebKit/UIProcess/mac/PageClientImplMac.mm +index 22653d74398..bf27558fdfd 100644 +--- a/Source/WebKit/UIProcess/mac/PageClientImplMac.mm ++++ b/Source/WebKit/UIProcess/mac/PageClientImplMac.mm +@@ -455,6 +455,8 @@ IntRect PageClientImpl::rootViewToAccessibilityScreen(const IntRect& rect) + + void PageClientImpl::doneWithKeyEvent(const NativeWebKeyboardEvent& event, bool eventWasHandled) + { ++ if (!event.nativeEvent()) ++ return; + m_impl->doneWithKeyEvent(event.nativeEvent(), eventWasHandled); + } + +@@ -930,6 +932,9 @@ void PageClientImpl::didRestoreScrollPosition() + + bool PageClientImpl::windowIsFrontWindowUnderMouse(const NativeWebMouseEvent& event) + { ++ // Simulated event. ++ if (!event.nativeEvent()) ++ return false; + return m_impl->windowIsFrontWindowUnderMouse(event.nativeEvent()); + } + +diff --git a/Source/WebKit/UIProcess/mac/WebPageInspectorEmulationAgentMac.mm b/Source/WebKit/UIProcess/mac/WebPageInspectorEmulationAgentMac.mm +new file mode 100644 +index 00000000000..857195777b5 +--- /dev/null ++++ b/Source/WebKit/UIProcess/mac/WebPageInspectorEmulationAgentMac.mm +@@ -0,0 +1,42 @@ ++/* ++ * Copyright (C) 2010 Apple Inc. All rights reserved. ++ * Portions Copyright (c) 2010 Motorola Mobility, Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' ++ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, ++ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS ++ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ++ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN ++ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ++ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF ++ * THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorEmulationAgent.h" ++ ++#include "WebPageProxy.h" ++ ++namespace WebKit { ++void WebPageInspectorEmulationAgent::platformSetSize(String& error, int width, int height) ++{ ++ NSWindow* window = m_page.platformWindow(); ++ NSRect frame = [window frame]; ++ frame.origin.y += frame.size.height; ++ frame.origin.y -= height; ++ frame.size = NSMakeSize(width, height); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/mac/WebPageInspectorInputAgentMac.mm b/Source/WebKit/UIProcess/mac/WebPageInspectorInputAgentMac.mm +new file mode 100644 +index 00000000000..0f09fd52ae1 +--- /dev/null ++++ b/Source/WebKit/UIProcess/mac/WebPageInspectorInputAgentMac.mm +@@ -0,0 +1,37 @@ ++/* ++ * Copyright (C) 2010 Apple Inc. All rights reserved. ++ * Portions Copyright (c) 2010 Motorola Mobility, Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' ++ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, ++ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS ++ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ++ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN ++ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ++ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF ++ * THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorInputAgent.h" ++ ++namespace WebKit { ++ ++void WebPageInspectorInputAgent::platformDispatchKeyEvent(WebKeyboardEvent::Type type, const String& text, const String& unmodifiedText, const String& key, const String& code, int windowsVirtualKeyCode, int nativeVirtualKeyCode, bool isAutoRepeat, bool isKeypad, bool isSystemKey, OptionSet modifiers, WallTime timestamp) ++{ ++ fprintf(stderr, "Mac does not support dispatching key events"); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/mac/WebPageInspectorTargetProxyMac.mm b/Source/WebKit/UIProcess/mac/WebPageInspectorTargetProxyMac.mm +new file mode 100644 +index 00000000000..b0f0172a028 +--- /dev/null ++++ b/Source/WebKit/UIProcess/mac/WebPageInspectorTargetProxyMac.mm +@@ -0,0 +1,41 @@ ++/* ++ * Copyright (C) 2010 Apple Inc. All rights reserved. ++ * Portions Copyright (c) 2010 Motorola Mobility, Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' ++ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, ++ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS ++ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ++ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN ++ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ++ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF ++ * THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorTargetProxy.h" ++ ++#if PLATFORM(MAC) ++ ++namespace WebKit { ++ ++void WebPageInspectorTargetProxy::platformActivate(String& error) const ++{ ++ error = "Not Implemented"; ++} ++ ++} // namespace WebKit ++ ++#endif +diff --git a/Source/WebKit/UIProcess/wpe/WebPageInspectorEmulationAgentWPE.cpp b/Source/WebKit/UIProcess/wpe/WebPageInspectorEmulationAgentWPE.cpp +new file mode 100644 +index 00000000000..5465c0ae99d +--- /dev/null ++++ b/Source/WebKit/UIProcess/wpe/WebPageInspectorEmulationAgentWPE.cpp +@@ -0,0 +1,41 @@ ++/* ++ * Copyright (C) 2010 Apple Inc. All rights reserved. ++ * Portions Copyright (c) 2010 Motorola Mobility, Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' ++ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, ++ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS ++ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ++ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN ++ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ++ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF ++ * THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorEmulationAgent.h" ++ ++#include "WebPageProxy.h" ++#include ++ ++namespace WebKit { ++ ++void WebPageInspectorEmulationAgent::platformSetSize(String& error, int width, int height) ++{ ++ struct wpe_view_backend* backend = m_page.viewBackend(); ++ wpe_view_backend_dispatch_set_size(backend, width, height); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/wpe/WebPageInspectorInputAgentWPE.cpp b/Source/WebKit/UIProcess/wpe/WebPageInspectorInputAgentWPE.cpp +new file mode 100644 +index 00000000000..772ca6bc674 +--- /dev/null ++++ b/Source/WebKit/UIProcess/wpe/WebPageInspectorInputAgentWPE.cpp +@@ -0,0 +1,99 @@ ++/* ++ * Copyright (C) 2010 Apple Inc. All rights reserved. ++ * Portions Copyright (c) 2010 Motorola Mobility, Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' ++ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, ++ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS ++ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ++ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN ++ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ++ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF ++ * THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorInputAgent.h" ++ ++#include "NativeWebKeyboardEvent.h" ++#include "WebPageProxy.h" ++#include ++#include ++ ++namespace WebKit { ++ ++void WebPageInspectorInputAgent::platformDispatchKeyEvent(String& error, const String& type, const String& keyRef) ++{ ++} ++ ++static unsigned toWPEButton(WebMouseEvent::Button button) ++{ ++ switch (button) { ++ case WebMouseEvent::NoButton: ++ case WebMouseEvent::LeftButton: ++ return 1; ++ case WebMouseEvent::MiddleButton: ++ return 3; ++ case WebMouseEvent::RightButton: ++ return 2; ++ } ++ return 1; ++} ++ ++static unsigned toWPEModifiers(OptionSet modifiers) ++{ ++ unsigned result = 0; ++ if (modifiers.contains(WebEvent::Modifier::ControlKey)) ++ result |= wpe_input_keyboard_modifier_control; ++ if (modifiers.contains(WebEvent::Modifier::ShiftKey)) ++ result |= wpe_input_keyboard_modifier_shift; ++ if (modifiers.contains(WebEvent::Modifier::AltKey)) ++ result |= wpe_input_keyboard_modifier_alt; ++ if (modifiers.contains(WebEvent::Modifier::CapsLockKey)) ++ fprintf(stderr, "Unsupported modifier CapsLock will be ignored.\n"); ++ return result; ++} ++ ++void WebPageInspectorInputAgent::platformDispatchKeyEvent(WebKeyboardEvent::Type type, const String& text, const String& unmodifiedText, const String& key, const String& code, int windowsVirtualKeyCode, int nativeVirtualKeyCode, bool isAutoRepeat, bool isKeypad, bool isSystemKey, OptionSet modifiers, WallTime timestamp) ++{ ++ unsigned keyCode = WebCore::PlatformKeyboardEvent::WPEKeyCodeForWindowsKeyCode(windowsVirtualKeyCode); ++ struct wpe_input_xkb_keymap_entry* entries; ++ uint32_t entriesCount; ++ fprintf(stderr, "platformDispatchKeyEvent %s => %d\n", key.ascii().data(), keyCode); ++ wpe_input_xkb_context_get_entries_for_key_code(wpe_input_xkb_context_get_default(), keyCode, &entries, &entriesCount); ++ bool pressed = type == WebKeyboardEvent::KeyDown; ++ struct wpe_input_keyboard_event event = { 0, keyCode, entriesCount ? entries[0].hardware_key_code : 0, pressed, toWPEModifiers(modifiers) }; ++ // event.time = timestamp.secondsSinceEpoch().milliseconds(); ++ wpe_view_backend_dispatch_keyboard_event(m_page.viewBackend(), &event); ++ free(entries); ++} ++ ++void WebPageInspectorInputAgent::platformDispatchMouseEvent(WebMouseEvent::Type type, int x, int y, WebMouseEvent::Button button, OptionSet modifiers) ++{ ++ wpe_input_pointer_event_type eventType = wpe_input_pointer_event_type_null; ++ uint32_t eventButton = 0; ++ uint32_t state = 0; ++ if (type == WebEvent::MouseDown || type == WebEvent::MouseUp) { ++ eventType = wpe_input_pointer_event_type_button; ++ state = (type == WebEvent::MouseDown); ++ eventButton = toWPEButton(button); ++ } else if (type == WebEvent::MouseMove) { ++ eventType = wpe_input_pointer_event_type_motion; ++ } ++ struct wpe_input_pointer_event event { eventType, 0, x, y, eventButton, state, toWPEModifiers(modifiers) }; ++ wpe_view_backend_dispatch_pointer_event(m_page.viewBackend(), &event); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/wpe/WebPageInspectorTargetProxyWPE.cpp b/Source/WebKit/UIProcess/wpe/WebPageInspectorTargetProxyWPE.cpp +new file mode 100644 +index 00000000000..d64407d5822 +--- /dev/null ++++ b/Source/WebKit/UIProcess/wpe/WebPageInspectorTargetProxyWPE.cpp +@@ -0,0 +1,41 @@ ++/* ++ * Copyright (C) 2010 Apple Inc. All rights reserved. ++ * Portions Copyright (c) 2010 Motorola Mobility, Inc. All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' ++ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, ++ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS ++ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ++ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN ++ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ++ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF ++ * THE POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include "config.h" ++#include "WebPageInspectorTargetProxy.h" ++ ++#include "WebPageProxy.h" ++#include ++ ++namespace WebKit { ++ ++void WebPageInspectorTargetProxy::platformActivate(String& error) const ++{ ++ struct wpe_view_backend* backend = m_page.viewBackend(); ++ wpe_view_backend_add_activity_state(backend, wpe_view_activity_state_visible | wpe_view_activity_state_focused | wpe_view_activity_state_in_window); ++} ++ ++} // namespace WebKit +diff --git a/Source/WebKit/WebKit.xcodeproj/project.pbxproj b/Source/WebKit/WebKit.xcodeproj/project.pbxproj +index 21f3f6ad8cd..d0191b4cafd 100644 +--- a/Source/WebKit/WebKit.xcodeproj/project.pbxproj ++++ b/Source/WebKit/WebKit.xcodeproj/project.pbxproj +@@ -1663,6 +1663,19 @@ + CEE4AE2B1A5DCF430002F49B /* UIKitSPI.h in Headers */ = {isa = PBXBuildFile; fileRef = CEE4AE2A1A5DCF430002F49B /* UIKitSPI.h */; }; + D3B9484711FF4B6500032B39 /* WebPopupMenu.h in Headers */ = {isa = PBXBuildFile; fileRef = D3B9484311FF4B6500032B39 /* WebPopupMenu.h */; }; + D3B9484911FF4B6500032B39 /* WebSearchPopupMenu.h in Headers */ = {isa = PBXBuildFile; fileRef = D3B9484511FF4B6500032B39 /* WebSearchPopupMenu.h */; }; ++ D71A94322370E025002C4D9E /* InspectorBrowserAgentClientMac.h in Headers */ = {isa = PBXBuildFile; fileRef = D71A94302370E025002C4D9E /* InspectorBrowserAgentClientMac.h */; }; ++ D71A94342370E07A002C4D9E /* InspectorBrowserAgentClient.h in Headers */ = {isa = PBXBuildFile; fileRef = D71A94332370E07A002C4D9E /* InspectorBrowserAgentClient.h */; }; ++ D71A94382370F032002C4D9E /* BrowserInspectorController.h in Headers */ = {isa = PBXBuildFile; fileRef = D71A94372370F032002C4D9E /* BrowserInspectorController.h */; }; ++ D71A943A2370F061002C4D9E /* RemoteInspectorPipe.h in Headers */ = {isa = PBXBuildFile; fileRef = D71A94392370F060002C4D9E /* RemoteInspectorPipe.h */; }; ++ D71A94412371F67E002C4D9E /* WebPageInspectorTargetProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = D71A943E2371F67E002C4D9E /* WebPageInspectorTargetProxy.h */; }; ++ D71A94422371F67E002C4D9E /* WebPageInspectorEmulationAgent.h in Headers */ = {isa = PBXBuildFile; fileRef = D71A943F2371F67E002C4D9E /* WebPageInspectorEmulationAgent.h */; }; ++ D71A94432371F67E002C4D9E /* WebPageInspectorInputAgent.h in Headers */ = {isa = PBXBuildFile; fileRef = D71A94402371F67E002C4D9E /* WebPageInspectorInputAgent.h */; }; ++ D71A944A2372290B002C4D9E /* _WKBrowserInspector.h in Headers */ = {isa = PBXBuildFile; fileRef = D71A94492372290B002C4D9E /* _WKBrowserInspector.h */; settings = {ATTRIBUTES = (Private, ); }; }; ++ D71A944C237239FB002C4D9E /* BrowserInspectorPipe.h in Headers */ = {isa = PBXBuildFile; fileRef = D71A944B237239FB002C4D9E /* BrowserInspectorPipe.h */; }; ++ D79902B1236E9404005D6F7E /* WebPageInspectorEmulationAgentMac.mm in Sources */ = {isa = PBXBuildFile; fileRef = D79902AE236E9404005D6F7E /* WebPageInspectorEmulationAgentMac.mm */; }; ++ D79902B2236E9404005D6F7E /* WebPageInspectorTargetProxyMac.mm in Sources */ = {isa = PBXBuildFile; fileRef = D79902AF236E9404005D6F7E /* WebPageInspectorTargetProxyMac.mm */; }; ++ D79902B3236E9404005D6F7E /* WebPageInspectorInputAgentMac.mm in Sources */ = {isa = PBXBuildFile; fileRef = D79902B0236E9404005D6F7E /* WebPageInspectorInputAgentMac.mm */; }; ++ D7EB04E72372A73B00F744CE /* InspectorBrowserAgentClientMac.mm in Sources */ = {isa = PBXBuildFile; fileRef = D7EB04E62372A73B00F744CE /* InspectorBrowserAgentClientMac.mm */; }; + E105FE5418D7B9DE008F57A8 /* EditingRange.h in Headers */ = {isa = PBXBuildFile; fileRef = E105FE5318D7B9DE008F57A8 /* EditingRange.h */; }; + E11D35AE16B63D1B006D23D7 /* com.apple.WebProcess.sb in Resources */ = {isa = PBXBuildFile; fileRef = E1967E37150AB5E200C73169 /* com.apple.WebProcess.sb */; }; + E14A954A16E016A40068DE82 /* NetworkProcessPlatformStrategies.h in Headers */ = {isa = PBXBuildFile; fileRef = E14A954816E016A40068DE82 /* NetworkProcessPlatformStrategies.h */; }; +@@ -4692,6 +4705,20 @@ + D3B9484311FF4B6500032B39 /* WebPopupMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebPopupMenu.h; sourceTree = ""; }; + D3B9484411FF4B6500032B39 /* WebSearchPopupMenu.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = WebSearchPopupMenu.cpp; sourceTree = ""; }; + D3B9484511FF4B6500032B39 /* WebSearchPopupMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebSearchPopupMenu.h; sourceTree = ""; }; ++ D71A942C2370DF81002C4D9E /* WKBrowserInspector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WKBrowserInspector.h; sourceTree = ""; }; ++ D71A94302370E025002C4D9E /* InspectorBrowserAgentClientMac.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InspectorBrowserAgentClientMac.h; sourceTree = ""; }; ++ D71A94332370E07A002C4D9E /* InspectorBrowserAgentClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InspectorBrowserAgentClient.h; sourceTree = ""; }; ++ D71A94372370F032002C4D9E /* BrowserInspectorController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BrowserInspectorController.h; sourceTree = ""; }; ++ D71A94392370F060002C4D9E /* RemoteInspectorPipe.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RemoteInspectorPipe.h; sourceTree = ""; }; ++ D71A943E2371F67E002C4D9E /* WebPageInspectorTargetProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebPageInspectorTargetProxy.h; sourceTree = ""; }; ++ D71A943F2371F67E002C4D9E /* WebPageInspectorEmulationAgent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebPageInspectorEmulationAgent.h; sourceTree = ""; }; ++ D71A94402371F67E002C4D9E /* WebPageInspectorInputAgent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebPageInspectorInputAgent.h; sourceTree = ""; }; ++ D71A94492372290B002C4D9E /* _WKBrowserInspector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _WKBrowserInspector.h; sourceTree = ""; }; ++ D71A944B237239FB002C4D9E /* BrowserInspectorPipe.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BrowserInspectorPipe.h; sourceTree = ""; }; ++ D79902AE236E9404005D6F7E /* WebPageInspectorEmulationAgentMac.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WebPageInspectorEmulationAgentMac.mm; sourceTree = ""; }; ++ D79902AF236E9404005D6F7E /* WebPageInspectorTargetProxyMac.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WebPageInspectorTargetProxyMac.mm; sourceTree = ""; }; ++ D79902B0236E9404005D6F7E /* WebPageInspectorInputAgentMac.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WebPageInspectorInputAgentMac.mm; sourceTree = ""; }; ++ D7EB04E62372A73B00F744CE /* InspectorBrowserAgentClientMac.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = InspectorBrowserAgentClientMac.mm; sourceTree = ""; }; + DF58C6311371AC5800F9A37C /* NativeWebWheelEvent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NativeWebWheelEvent.h; sourceTree = ""; }; + DF58C6351371ACA000F9A37C /* NativeWebWheelEventMac.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = NativeWebWheelEventMac.mm; sourceTree = ""; }; + E105FE5318D7B9DE008F57A8 /* EditingRange.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EditingRange.h; sourceTree = ""; }; +@@ -6294,6 +6321,7 @@ + 37C4C08318149C2A003688B9 /* Cocoa */ = { + isa = PBXGroup; + children = ( ++ D71A94492372290B002C4D9E /* _WKBrowserInspector.h */, + 1A43E826188F38E2009E4D30 /* Deprecated */, + 37A5E01218BBF937000A081E /* _WKActivatedElementInfo.h */, + 37A5E01118BBF937000A081E /* _WKActivatedElementInfo.mm */, +@@ -7774,6 +7802,13 @@ + BC032DC310F438260058C15A /* UIProcess */ = { + isa = PBXGroup; + children = ( ++ D71A944B237239FB002C4D9E /* BrowserInspectorPipe.h */, ++ D71A943F2371F67E002C4D9E /* WebPageInspectorEmulationAgent.h */, ++ D71A94402371F67E002C4D9E /* WebPageInspectorInputAgent.h */, ++ D71A943E2371F67E002C4D9E /* WebPageInspectorTargetProxy.h */, ++ D71A94392370F060002C4D9E /* RemoteInspectorPipe.h */, ++ D71A94372370F032002C4D9E /* BrowserInspectorController.h */, ++ D71A94332370E07A002C4D9E /* InspectorBrowserAgentClient.h */, + BC032DC410F4387C0058C15A /* API */, + 512F588D12A8836F00629530 /* Authentication */, + 9955A6E81C79809000EB6A93 /* Automation */, +@@ -8051,6 +8086,7 @@ + BC0C376610F807660076D7CB /* C */ = { + isa = PBXGroup; + children = ( ++ D71A942C2370DF81002C4D9E /* WKBrowserInspector.h */, + 5123CF18133D25E60056F800 /* cg */, + 6EE849C41368D9040038D481 /* mac */, + BCB63477116BF10600603215 /* WebKit2_C.h */, +@@ -8646,6 +8682,11 @@ + BCCF085C113F3B7500C650C5 /* mac */ = { + isa = PBXGroup; + children = ( ++ D7EB04E62372A73B00F744CE /* InspectorBrowserAgentClientMac.mm */, ++ D71A94302370E025002C4D9E /* InspectorBrowserAgentClientMac.h */, ++ D79902AE236E9404005D6F7E /* WebPageInspectorEmulationAgentMac.mm */, ++ D79902B0236E9404005D6F7E /* WebPageInspectorInputAgentMac.mm */, ++ D79902AF236E9404005D6F7E /* WebPageInspectorTargetProxyMac.mm */, + B878B613133428DC006888E9 /* CorrectionPanel.h */, + B878B614133428DC006888E9 /* CorrectionPanel.mm */, + C1817362205844A900DFDA65 /* DisplayLink.cpp */, +@@ -9323,6 +9364,7 @@ + 510F59101DDE296900412FF5 /* _WKIconLoadingDelegate.h in Headers */, + 37A64E5518F38E3C00EB30F1 /* _WKInputDelegate.h in Headers */, + 5CAFDE452130846300B1F7E1 /* _WKInspector.h in Headers */, ++ D71A944A2372290B002C4D9E /* _WKBrowserInspector.h in Headers */, + 5CAFDE472130846A00B1F7E1 /* _WKInspectorInternal.h in Headers */, + A5C0F0AB2000658200536536 /* _WKInspectorWindow.h in Headers */, + 31B362952141EBCD007BFA53 /* _WKInternalDebugFeature.h in Headers */, +@@ -9434,6 +9476,7 @@ + 7C89D2981A6753B2003A5FDE /* APIPageConfiguration.h in Headers */, + 1AC1336C18565C7A00F3EC05 /* APIPageHandle.h in Headers */, + 1AFDD3151891B54000153970 /* APIPolicyClient.h in Headers */, ++ D71A94382370F032002C4D9E /* BrowserInspectorController.h in Headers */, + 7CE4D2201A4914CA00C7F152 /* APIProcessPoolConfiguration.h in Headers */, + F634445612A885C8000612D8 /* APISecurityOrigin.h in Headers */, + 1AFDE6621954E9B100C48FFA /* APISessionState.h in Headers */, +@@ -9552,6 +9595,7 @@ + BC06F43A12DBCCFB002D78DE /* GeolocationPermissionRequestProxy.h in Headers */, + 2DA944A41884E4F000ED86DB /* GestureTypes.h in Headers */, + 2DA049B8180CCD0A00AAFA9E /* GraphicsLayerCARemote.h in Headers */, ++ D71A94342370E07A002C4D9E /* InspectorBrowserAgentClient.h in Headers */, + C0CE72AD1247E78D00BC0EC4 /* HandleMessage.h in Headers */, + 1AC75A1B1B3368270056745B /* HangDetectionDisabler.h in Headers */, + 57AC8F50217FEED90055438C /* HidConnection.h in Headers */, +@@ -9675,8 +9719,10 @@ + 41DC45961E3D6E2200B11F51 /* NetworkRTCProvider.h in Headers */, + 413075AB1DE85F330039EC69 /* NetworkRTCSocket.h in Headers */, + 5C20CBA01BB1ECD800895BB1 /* NetworkSession.h in Headers */, ++ D71A94422371F67E002C4D9E /* WebPageInspectorEmulationAgent.h in Headers */, + 532159551DBAE7290054AA3C /* NetworkSessionCocoa.h in Headers */, + 417915B92257046F00D6F97E /* NetworkSocketChannel.h in Headers */, ++ D71A943A2370F061002C4D9E /* RemoteInspectorPipe.h in Headers */, + 570DAAC22303730300E8FC04 /* NfcConnection.h in Headers */, + 570DAAAE23026F5C00E8FC04 /* NfcService.h in Headers */, + 31A2EC5614899C0900810D71 /* NotificationPermissionRequest.h in Headers */, +@@ -9758,6 +9804,7 @@ + CD2865EE2255562000606AC7 /* ProcessTaskStateObserver.h in Headers */, + 463FD4821EB94EC000A2982C /* ProcessTerminationReason.h in Headers */, + 86E67A251910B9D100004AB7 /* ProcessThrottler.h in Headers */, ++ D71A944C237239FB002C4D9E /* BrowserInspectorPipe.h in Headers */, + 83048AE61ACA45DC0082C832 /* ProcessThrottlerClient.h in Headers */, + A1E688701F6E2BAB007006A6 /* QuarantineSPI.h in Headers */, + 57FD318222B3515E008D0E8B /* RedirectSOAuthorizationSession.h in Headers */, +@@ -9920,6 +9967,7 @@ + F430E94422473DFF005FE053 /* WebContentMode.h in Headers */, + 31A505FA1680025500A930EB /* WebContextClient.h in Headers */, + BC09B8F9147460F7005F5625 /* WebContextConnectionClient.h in Headers */, ++ D71A94412371F67E002C4D9E /* WebPageInspectorTargetProxy.h in Headers */, + BCDE059B11CDA8AE00E41AF1 /* WebContextInjectedBundleClient.h in Headers */, + 51871B5C127CB89D00F76232 /* WebContextMenu.h in Headers */, + BC032D7710F4378D0058C15A /* WebContextMenuClient.h in Headers */, +@@ -10153,6 +10201,7 @@ + BCD25F1711D6BDE100169B0E /* WKBundleFrame.h in Headers */, + BCF049E611FE20F600F86A58 /* WKBundleFramePrivate.h in Headers */, + BC49862F124D18C100D834E1 /* WKBundleHitTestResult.h in Headers */, ++ D71A94432371F67E002C4D9E /* WebPageInspectorInputAgent.h in Headers */, + BC204EF211C83EC8008F3375 /* WKBundleInitialize.h in Headers */, + 65B86F1E12F11DE300B7DD8A /* WKBundleInspector.h in Headers */, + 1A8B66B41BC45B010082DF77 /* WKBundleMac.h in Headers */, +@@ -10346,6 +10395,7 @@ + 1AB8A1F818400BB800E9AE69 /* WKPageContextMenuClient.h in Headers */, + 8372DB251A674C8F00C697C5 /* WKPageDiagnosticLoggingClient.h in Headers */, + 1AB8A1F418400B8F00E9AE69 /* WKPageFindClient.h in Headers */, ++ D71A94322370E025002C4D9E /* InspectorBrowserAgentClientMac.h in Headers */, + 1AB8A1F618400B9D00E9AE69 /* WKPageFindMatchesClient.h in Headers */, + 1AB8A1F018400B0000E9AE69 /* WKPageFormClient.h in Headers */, + BC7B633712A45ABA00D174A4 /* WKPageGroup.h in Headers */, +@@ -11302,6 +11352,7 @@ + 2D92A781212B6A7100F493FD /* MessageReceiverMap.cpp in Sources */, + 2D92A782212B6A7100F493FD /* MessageSender.cpp in Sources */, + 2D92A77A212B6A6100F493FD /* Module.cpp in Sources */, ++ D79902B1236E9404005D6F7E /* WebPageInspectorEmulationAgentMac.mm in Sources */, + 57B826452304F14000B72EB0 /* NearFieldSoftLink.mm in Sources */, + 2D913443212CF9F000128AFD /* NetscapeBrowserFuncs.cpp in Sources */, + 2D913444212CF9F000128AFD /* NetscapePlugin.cpp in Sources */, +@@ -11326,6 +11377,7 @@ + 1A2D8439127F65D5001EB962 /* NPObjectMessageReceiverMessageReceiver.cpp in Sources */, + 2D92A792212B6AD400F493FD /* NPObjectProxy.cpp in Sources */, + 2D92A793212B6AD400F493FD /* NPRemoteObjectMap.cpp in Sources */, ++ D7EB04E72372A73B00F744CE /* InspectorBrowserAgentClientMac.mm in Sources */, + 2D913447212CF9F000128AFD /* NPRuntimeObjectMap.cpp in Sources */, + 2D913448212CF9F000128AFD /* NPRuntimeUtilities.cpp in Sources */, + 2D92A794212B6AD400F493FD /* NPVariantData.cpp in Sources */, +@@ -11365,11 +11417,13 @@ + A1ADAFB62368E6A8009CB776 /* SharedMemory.cpp in Sources */, + 2DE6943D18BD2A68005C15E5 /* SmartMagnificationControllerMessageReceiver.cpp in Sources */, + 1A334DED16DE8F88006A8E38 /* StorageAreaMapMessageReceiver.cpp in Sources */, ++ D79902B3236E9404005D6F7E /* WebPageInspectorInputAgentMac.mm in Sources */, + 9368EEDE2303A90200BDB11A /* StorageManagerSetMessageReceiver.cpp in Sources */, + 2D92A783212B6A7100F493FD /* StringReference.cpp in Sources */, + 2D11B7512126A282006F8878 /* UnifiedSource1-mm.mm in Sources */, + 2D11B7522126A282006F8878 /* UnifiedSource1.cpp in Sources */, + 2D11B7542126A282006F8878 /* UnifiedSource2.cpp in Sources */, ++ D79902B2236E9404005D6F7E /* WebPageInspectorTargetProxyMac.mm in Sources */, + 2D11B7532126A282006F8878 /* UnifiedSource2-mm.mm in Sources */, + 2D11B7562126A282006F8878 /* UnifiedSource3.cpp in Sources */, + 2D11B7552126A282006F8878 /* UnifiedSource3-mm.mm in Sources */, +diff --git a/Source/WebKit/WebProcess/WebPage/WebPageInspectorTarget.cpp b/Source/WebKit/WebProcess/WebPage/WebPageInspectorTarget.cpp +index a70f6fd5209..f02e5c774a4 100644 +--- a/Source/WebKit/WebProcess/WebPage/WebPageInspectorTarget.cpp ++++ b/Source/WebKit/WebProcess/WebPage/WebPageInspectorTarget.cpp +@@ -26,6 +26,8 @@ + #include "config.h" + #include "WebPageInspectorTarget.h" + ++#include "FrameInfoData.h" ++#include "WebFrame.h" + #include "WebPage.h" + #include "WebPageInspectorTargetFrontendChannel.h" + #include +@@ -45,6 +47,11 @@ String WebPageInspectorTarget::identifier() const + return toTargetID(m_page.identifier()); + } + ++String WebPageInspectorTarget::url() const ++{ ++ return m_page.mainWebFrame()->info().request.url().string(); ++} ++ + void WebPageInspectorTarget::connect(Inspector::FrontendChannel::ConnectionType connectionType) + { + if (m_channel) +diff --git a/Source/WebKit/WebProcess/WebPage/WebPageInspectorTarget.h b/Source/WebKit/WebProcess/WebPage/WebPageInspectorTarget.h +index 6cbd7fad5ff..176c46f186b 100644 +--- a/Source/WebKit/WebProcess/WebPage/WebPageInspectorTarget.h ++++ b/Source/WebKit/WebProcess/WebPage/WebPageInspectorTarget.h +@@ -44,6 +44,7 @@ public: + Inspector::InspectorTargetType type() const final { return Inspector::InspectorTargetType::Page; } + + String identifier() const final; ++ String url() const final; + + void connect(Inspector::FrontendChannel::ConnectionType) override; + void disconnect() override; +diff --git a/Source/WebKit/WebProcess/WebProcess.cpp b/Source/WebKit/WebProcess/WebProcess.cpp +index 6c16fa01df1..2b79bf41601 100644 +--- a/Source/WebKit/WebProcess/WebProcess.cpp ++++ b/Source/WebKit/WebProcess/WebProcess.cpp +@@ -625,7 +625,8 @@ void WebProcess::setCacheModel(CacheModel cacheModel) + unsigned cacheMaxDeadCapacity = 0; + Seconds deadDecodedDataDeletionInterval; + unsigned backForwardCacheSize = 0; +- calculateMemoryCacheSizes(cacheModel, cacheTotalCapacity, cacheMinDeadCapacity, cacheMaxDeadCapacity, deadDecodedDataDeletionInterval, backForwardCacheSize); ++ // FIXME(yurys): forcefully disable cache becaus it swallows Runtime.executionContextCreated events on goBack navigation. ++ // calculateMemoryCacheSizes(cacheModel, cacheTotalCapacity, cacheMinDeadCapacity, cacheMaxDeadCapacity, deadDecodedDataDeletionInterval, backForwardCacheSize); + + auto& memoryCache = MemoryCache::singleton(); + memoryCache.setCapacities(cacheMinDeadCapacity, cacheMaxDeadCapacity, cacheTotalCapacity); +diff --git a/Tools/MiniBrowser/gtk/main.c b/Tools/MiniBrowser/gtk/main.c +index 93d93592bc4..6e27ef37742 100644 +--- a/Tools/MiniBrowser/gtk/main.c ++++ b/Tools/MiniBrowser/gtk/main.c +@@ -53,6 +53,7 @@ static const char *cookiesFile; + static const char *cookiesPolicy; + static const char *proxy; + static gboolean darkMode; ++static gboolean inspectorPipe; + static gboolean printVersion; + + typedef enum { +@@ -121,6 +122,7 @@ static const GOptionEntry commandLineOptions[] = + { "ignore-tls-errors", 0, 0, G_OPTION_ARG_NONE, &ignoreTLSErrors, "Ignore TLS errors", NULL }, + { "content-filter", 0, 0, G_OPTION_ARG_FILENAME, &contentFilter, "JSON with content filtering rules", "FILE" }, + { "version", 'v', 0, G_OPTION_ARG_NONE, &printVersion, "Print the WebKitGTK version", NULL }, ++ { "inspector-pipe", 0, 0, G_OPTION_ARG_NONE, &inspectorPipe, "Open pipe connection to the remote inspector", NULL }, + { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &uriArguments, 0, "[URL…]" }, + { 0, 0, 0, 0, 0, 0, 0 } + }; +@@ -492,6 +494,29 @@ static void filterSavedCallback(WebKitUserContentFilterStore *store, GAsyncResul + g_main_loop_quit(data->mainLoop); + } + ++static WebKitWebView *createNewPage(WebKitBrowserInspector *browser_inspector, WebKitWebContext *context) ++{ ++ WebKitWebView *newWebView = WEBKIT_WEB_VIEW(g_object_new(WEBKIT_TYPE_WEB_VIEW, ++ "web-context", context, ++ "is-ephemeral", webkit_web_context_is_ephemeral(context), ++ "is-controlled-by-automation", TRUE, ++ NULL)); ++ GtkWidget *newWindow = browser_window_new(NULL, context); ++ browser_window_append_view(BROWSER_WINDOW(newWindow), newWebView); ++ gtk_widget_grab_focus(GTK_WIDGET(newWebView)); ++ gtk_widget_show(GTK_WIDGET(newWindow)); ++ webkit_web_view_load_uri(newWebView, "about:blank"); ++ return newWebView; ++} ++ ++static void configureBrowserInspectorPipe() ++{ ++ WebKitBrowserInspector* browserInspector = webkit_browser_inspector_get_default(); ++ g_signal_connect(browserInspector, "create-new-page", G_CALLBACK(createNewPage), NULL); ++ ++ webkit_browser_inspector_initialize_pipe(); ++} ++ + int main(int argc, char *argv[]) + { + #if ENABLE_DEVELOPER_MODE +@@ -539,6 +564,9 @@ int main(int argc, char *argv[]) + WebKitWebContext *webContext = g_object_new(WEBKIT_TYPE_WEB_CONTEXT, "website-data-manager", manager, "process-swap-on-cross-site-navigation-enabled", TRUE, NULL); + g_object_unref(manager); + ++ if (inspectorPipe) ++ configureBrowserInspectorPipe(); ++ + if (cookiesPolicy) { + WebKitCookieManager *cookieManager = webkit_web_context_get_cookie_manager(webContext); + GEnumClass *enumClass = g_type_class_ref(WEBKIT_TYPE_COOKIE_ACCEPT_POLICY); +diff --git a/Tools/MiniBrowser/mac/AppDelegate.h b/Tools/MiniBrowser/mac/AppDelegate.h +index 45ef1a6424e..928486be325 100644 +--- a/Tools/MiniBrowser/mac/AppDelegate.h ++++ b/Tools/MiniBrowser/mac/AppDelegate.h +@@ -23,9 +23,11 @@ + * THE POSSIBILITY OF SUCH DAMAGE. + */ + ++#import ++ + @class ExtensionManagerWindowController; + +-@interface BrowserAppDelegate : NSObject { ++@interface BrowserAppDelegate : NSObject { + NSMutableSet *_browserWindowControllers; + ExtensionManagerWindowController *_extensionManagerWindowController; + +diff --git a/Tools/MiniBrowser/mac/AppDelegate.m b/Tools/MiniBrowser/mac/AppDelegate.m +index b6af4ef724f..46c39768156 100644 +--- a/Tools/MiniBrowser/mac/AppDelegate.m ++++ b/Tools/MiniBrowser/mac/AppDelegate.m +@@ -61,7 +61,9 @@ - (id)init + _browserWindowControllers = [[NSMutableSet alloc] init]; + _extensionManagerWindowController = [[ExtensionManagerWindowController alloc] init]; + } +- ++ NSArray *arguments = [[NSProcessInfo processInfo] arguments]; ++ if ([arguments containsObject: @"--inspector-pipe"]) ++ [_WKBrowserInspector initializeRemoteInspectorPipe:self]; + return self; + } + +@@ -158,9 +160,9 @@ - (BrowserWindowController *)createBrowserWindowController:(id)sender + } + + if (!useWebKit2) +- controller = [[WK1BrowserWindowController alloc] initWithWindowNibName:@"BrowserWindow"]; ++ controller = [[[WK1BrowserWindowController alloc] initWithWindowNibName:@"BrowserWindow"] autorelease]; + else +- controller = [[WK2BrowserWindowController alloc] initWithConfiguration:defaultConfiguration()]; ++ controller = [[[WK2BrowserWindowController alloc] initWithConfiguration:defaultConfiguration()] autorelease]; + + if (makeEditable) + controller.editable = YES; +@@ -345,4 +347,21 @@ - (IBAction)clearDefaultStoreWebsiteData:(id)sender + }]; + } + ++#pragma mark WKBrowserInspectorDelegate ++ ++- (WKWebView *)createNewPage ++{ ++ WK2BrowserWindowController *controller = [[[WK2BrowserWindowController alloc] initWithConfiguration:defaultConfiguration()] autorelease]; ++ [_browserWindowControllers addObject:controller]; ++ ++ [[controller window] makeKeyAndOrderFront:self]; ++ [controller loadURLString:[SettingsController shared].defaultURL]; ++ return [controller webView]; ++} ++ ++- (void)quit ++{ ++ [NSApp performSelector:@selector(terminate:) withObject:nil afterDelay:0.0]; ++} ++ + @end +diff --git a/Tools/MiniBrowser/mac/WK2BrowserWindowController.h b/Tools/MiniBrowser/mac/WK2BrowserWindowController.h +index 6f0949b0f4a..e774433031a 100644 +--- a/Tools/MiniBrowser/mac/WK2BrowserWindowController.h ++++ b/Tools/MiniBrowser/mac/WK2BrowserWindowController.h +@@ -25,8 +25,11 @@ + + #import "BrowserWindowController.h" + ++@class WKWebView; ++ + @interface WK2BrowserWindowController : BrowserWindowController + + - (instancetype)initWithConfiguration:(WKWebViewConfiguration *)configuration; ++- (WKWebView *)webView; + + @end +diff --git a/Tools/MiniBrowser/mac/WK2BrowserWindowController.m b/Tools/MiniBrowser/mac/WK2BrowserWindowController.m +index 3ca15403d5f..5905526473d 100644 +--- a/Tools/MiniBrowser/mac/WK2BrowserWindowController.m ++++ b/Tools/MiniBrowser/mac/WK2BrowserWindowController.m +@@ -105,7 +105,7 @@ - (void)awakeFromNib + // telling WebKit to load every icon referenced by the page. + if ([[SettingsController shared] loadsAllSiteIcons]) + _webView._iconLoadingDelegate = self; +- ++ + _webView._observedRenderingProgressEvents = _WKRenderingProgressEventFirstLayout + | _WKRenderingProgressEventFirstVisuallyNonEmptyLayout + | _WKRenderingProgressEventFirstPaintWithSignificantArea +@@ -139,14 +139,10 @@ - (instancetype)initWithConfiguration:(WKWebViewConfiguration *)configuration + + - (void)dealloc + { +- [_webView removeObserver:self forKeyPath:@"title"]; +- [_webView removeObserver:self forKeyPath:@"URL"]; +- + [progressIndicator unbind:NSHiddenBinding]; + [progressIndicator unbind:NSValueBinding]; + + [_textFinder release]; +- + [_webView release]; + [_configuration release]; + +@@ -369,9 +365,15 @@ - (BOOL)windowShouldClose:(id)sender + - (void)windowWillClose:(NSNotification *)notification + { + [(BrowserAppDelegate *)[[NSApplication sharedApplication] delegate] browserWindowWillClose:self.window]; ++ [_webView removeObserver:self forKeyPath:@"title"]; ++ [_webView removeObserver:self forKeyPath:@"URL"]; + [self autorelease]; + } + ++- (void)webViewDidClose:(WKWebView *)webView { ++ [self.window close]; ++} ++ + #define DefaultMinimumZoomFactor (.5) + #define DefaultMaximumZoomFactor (3.0) + #define DefaultZoomFactorRatio (1.2) +@@ -845,4 +847,9 @@ - (IBAction)saveAsWebArchive:(id)sender + }]; + } + ++- (WKWebView *)webView ++{ ++ return _webView; ++} ++ + @end +diff --git a/Tools/MiniBrowser/wpe/main.cpp b/Tools/MiniBrowser/wpe/main.cpp +index 2d183d39412..d94d4f06fc5 100644 +--- a/Tools/MiniBrowser/wpe/main.cpp ++++ b/Tools/MiniBrowser/wpe/main.cpp +@@ -172,6 +172,41 @@ static WebKitWebView* createWebView(WebKitWebView* webView, WebKitNavigationActi + return newWebView; + } + ++static WebKitWebView *createNewPage(WebKitBrowserInspector*, WebKitWebContext *context) ++{ ++ auto backend = createViewBackend(1280, 720); ++ struct wpe_view_backend* wpeBackend = backend->backend(); ++ if (!wpeBackend) ++ return nullptr; ++ ++ auto* viewBackend = webkit_web_view_backend_new(wpeBackend, ++ [](gpointer data) { ++ delete static_cast(data); ++ }, backend.release()); ++ ++ auto* newWebView = webkit_web_view_new_with_context(viewBackend, context); ++ ++ g_signal_connect(newWebView, "close", G_CALLBACK(webViewClose), nullptr); ++ ++ webkit_web_view_load_uri(newWebView, "about:blank"); ++ ++ return newWebView; ++ ++} ++ ++static void closeAll(WebKitBrowserInspector*, GMainLoop* mainLoop) ++{ ++ g_main_loop_quit(mainLoop); ++} ++ ++static void configureBrowserInspector(GMainLoop* mainLoop, WebKitWebView *firstWebView) ++{ ++ WebKitBrowserInspector* browserInspector = webkit_browser_inspector_get_default(); ++ g_signal_connect(browserInspector, "create-new-page", G_CALLBACK(createNewPage), NULL); ++ // FIXME: This signal is received only when closeAll is called. We should not rely on that. ++ g_signal_connect(firstWebView, "close", G_CALLBACK(closeAll), mainLoop); ++} ++ + int main(int argc, char *argv[]) + { + #if ENABLE_DEVELOPER_MODE +@@ -301,6 +336,8 @@ int main(int argc, char *argv[]) + g_signal_connect(webView, "permission-request", G_CALLBACK(decidePermissionRequest), nullptr); + g_signal_connect(webView, "create", G_CALLBACK(createWebView), nullptr); + ++ configureBrowserInspector(loop, webView); ++ + if (ignoreTLSErrors) + webkit_web_context_set_tls_errors_policy(webContext, WEBKIT_TLS_ERRORS_POLICY_IGNORE); + +-- +2.17.1 + diff --git a/browser_patches/webkit/pw_run.sh b/browser_patches/webkit/pw_run.sh new file mode 100755 index 0000000000..71c060241b --- /dev/null +++ b/browser_patches/webkit/pw_run.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +function runOSX() { + # if script is run as-is + if [ -d $SCRIPT_PATH/checkout/WebKitBuild/Release/MiniBrowser.app ]; then + DYLIB_PATH="$SCRIPT_PATH/checkout/WebKitBuild/Release" + elif [ -d $SCRIPT_PATH/MiniBrowser.app ]; then + DYLIB_PATH="$SCRIPT_PATH" + else + echo "Cannot find a MiniBrowser.app in neither location" 1>&2 + exit 1 + fi + MINIBROWSER="$DYLIB_PATH/MiniBrowser.app/Contents/MacOS/MiniBrowser" + DYLD_FRAMEWORK_PATH=$DYLIB_PATH DYLD_LIBRARY_PATH=$DYLIB_PATH $MINIBROWSER "$@" +} + +function runLinux() { + # if script is run as-is + if [ -d $SCRIPT_PATH/checkout/WebKitBuild ]; then + LD_PATH="$SCRIPT_PATH/checkout/WebKitBuild/DependenciesGTK/Root/lib:$SCRIPT_PATH/checkout/WebKitBuild/Release/bin" + MINIBROWSER="$SCRIPT_PATH/checkout/WebKitBuild/Release/bin/MiniBrowser" + elif [ -f $SCRIPT_PATH/MiniBrowser ]; then + LD_PATH="$SCRIPT_PATH" + MINIBROWSER="$SCRIPT_PATH/MiniBrowser" + else + echo "Cannot find a MiniBrowser.app in neither location" 1>&2 + exit 1 + fi + LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LD_PATH $MINIBROWSER "$@" +} + +SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)" +if [ "$(uname)" == "Darwin" ]; then + runOSX "$@" +elif [ "$(uname)" == "Linux" ]; then + runLinux "$@" +else + echo "ERROR: cannot run on this platform!" 1>&2 + exit 1; +fi diff --git a/chromium.js b/chromium.js new file mode 100644 index 0000000000..be153287e4 --- /dev/null +++ b/chromium.js @@ -0,0 +1,30 @@ +/** + * 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 {helper} = require('./lib/helper'); +const api = require('./lib/api'); +for (const className in api.Chromium) { + // Playwright-web excludes certain classes from bundle, e.g. BrowserFetcher. + if (typeof api.Chromium[className] === 'function') + helper.installAsyncStackHooks(api.Chromium[className]); +} + +// If node does not support async await, use the compiled version. +const {Playwright} = require('./lib/chromium/Playwright'); +const packageJson = require('./package.json'); +const isPlaywrightCore = packageJson.name === 'playwright-core'; + +module.exports = new Playwright(__dirname, packageJson.playwright.chromium_revision, isPlaywrightCore); diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000000..186d72e896 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,4107 @@ + +# Playwright API Tip-Of-Tree + + +- API Translations: [中文|Chinese](https://zhaoqize.github.io/playwright-api-zh_CN/#/) +- Troubleshooting: [troubleshooting.md](https://github.com/Microsoft/playwright/blob/master/docs/troubleshooting.md) + + +##### Table of Contents + + +- [Playwright API Tip-Of-Tree](#playwright-api-tip-of-tree) + - [Table of Contents](#table-of-contents) + - [Overview](#overview) + - [playwright vs playwright-core](#playwright-vs-playwright-core) + - [Environment Variables](#environment-variables) + - [Working with Chrome Extensions](#working-with-chrome-extensions) + - [class: Playwright](#class-playwright) + - [playwright.connect(options)](#playwrightconnectoptions) + - [playwright.createBrowserFetcher([options])](#playwrightcreatebrowserfetcheroptions) + - [playwright.defaultArgs([options])](#playwrightdefaultargsoptions) + - [playwright.devices](#playwrightdevices) + - [playwright.errors](#playwrighterrors) + - [playwright.executablePath()](#playwrightexecutablepath) + - [playwright.launch([options])](#playwrightlaunchoptions) + - [class: BrowserFetcher](#class-browserfetcher) + - [browserFetcher.canDownload(revision)](#browserfetchercandownloadrevision) + - [browserFetcher.download(revision[, progressCallback])](#browserfetcherdownloadrevision-progresscallback) + - [browserFetcher.localRevisions()](#browserfetcherlocalrevisions) + - [browserFetcher.platform()](#browserfetcherplatform) + - [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) + - [page.$$eval(selector, pageFunction[, ...args])](#pageevalselector-pagefunction-args) + - [page.$eval(selector, pageFunction[, ...args])](#pageevalselector-pagefunction-args) + - [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.dblclick(selector[, options])](#pagedblclickselector-options) + - [page.deleteCookie(...cookies)](#pagedeletecookiecookies) + - [page.emulate(options)](#pageemulateoptions) + - [page.emulateMedia(type)](#pageemulatemediatype) + - [page.emulateMediaFeatures(features)](#pageemulatemediafeaturesfeatures) + - [page.emulateMediaType(type)](#pageemulatemediatypetype) + - [page.emulateTimezone(timezoneId)](#pageemulatetimezonetimezoneid) + - [page.evaluate(pageFunction[, ...args])](#pageevaluatepagefunction-args) + - [page.evaluateHandle(pageFunction[, ...args])](#pageevaluatehandlepagefunction-args) + - [page.evaluateOnNewDocument(pageFunction[, ...args])](#pageevaluateonnewdocumentpagefunction-args) + - [page.exposeFunction(name, playwrightFunction)](#pageexposefunctionname-playwrightfunction) + - [page.fill(selector, value)](#pagefillselector-value) + - [page.focus(selector)](#pagefocusselector) + - [page.frames()](#pageframes) + - [page.goBack([options])](#pagegobackoptions) + - [page.goForward([options])](#pagegoforwardoptions) + - [page.goto(url[, options])](#pagegotourl-options) + - [page.hover(selector[, options])](#pagehoverselector-options) + - [page.isClosed()](#pageisclosed) + - [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)](#pagesetrequestinterceptionvalue) + - [page.setUserAgent(userAgent)](#pagesetuseragentuseragent) + - [page.setViewport(viewport)](#pagesetviewportviewport) + - [page.tap(selector[, options])](#pagetapselector-options) + - [page.target()](#pagetarget) + - [page.title()](#pagetitle) + - [page.touchscreen](#pagetouchscreen) + - [page.tracing](#pagetracing) + - [page.tripleclick(selector[, options])](#pagetripleclickselector-options) + - [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.waitForXPath(xpath[, options])](#pagewaitforxpathxpath-options) + - [page.workers()](#pageworkers) + - [class: Worker](#class-worker) + - [worker.evaluate(pageFunction[, ...args])](#workerevaluatepagefunction-args) + - [worker.evaluateHandle(pageFunction[, ...args])](#workerevaluatehandlepagefunction-args) + - [worker.executionContext()](#workerexecutioncontext) + - [worker.url()](#workerurl) + - [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.dblclick(x, y[, options])](#mousedblclickx-y-options) + - [mouse.down([options])](#mousedownoptions) + - [mouse.move(x, y[, options])](#mousemovex-y-options) + - [mouse.tripleclick(x, y[, options])](#mousetripleclickx-y-options) + - [mouse.up([options])](#mouseupoptions) + - [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.text()](#consolemessagetext) + - [consoleMessage.type()](#consolemessagetype) + - [class: Frame](#class-frame) + - [frame.$(selector)](#frameselector) + - [frame.$$(selector)](#frameselector) + - [frame.$$eval(selector, pageFunction[, ...args])](#frameevalselector-pagefunction-args) + - [frame.$eval(selector, pageFunction[, ...args])](#frameevalselector-pagefunction-args) + - [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.dblclick(selector[, options])](#framedblclickselector-options) + - [frame.evaluate(pageFunction[, ...args])](#frameevaluatepagefunction-args) + - [frame.evaluateHandle(pageFunction[, ...args])](#frameevaluatehandlepagefunction-args) + - [frame.executionContext()](#frameexecutioncontext) + - [frame.fill(selector, value)](#framefillselector-value) + - [frame.focus(selector)](#framefocusselector) + - [frame.goto(url[, options])](#framegotourl-options) + - [frame.hover(selector[, options])](#framehoverselector-options) + - [frame.isDetached()](#frameisdetached) + - [frame.name()](#framename) + - [frame.parentFrame()](#frameparentframe) + - [frame.select(selector, ...values)](#frameselectselector-values) + - [frame.setContent(html[, options])](#framesetcontenthtml-options) + - [frame.tap(selector[, options])](#frametapselector-options) + - [frame.title()](#frametitle) + - [frame.tripleclick(selector[, options])](#frametripleclickselector-options) + - [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.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) + - [elementHandle.$$eval(selector, pageFunction[, ...args])](#elementhandleevalselector-pagefunction-args) + - [elementHandle.$eval(selector, pageFunction[, ...args])](#elementhandleevalselector-pagefunction-args) + - [elementHandle.$x(expression)](#elementhandlexexpression) + - [elementHandle.asElement()](#elementhandleaselement) + - [elementHandle.boundingBox()](#elementhandleboundingbox) + - [elementHandle.boxModel()](#elementhandleboxmodel) + - [elementHandle.click([options])](#elementhandleclickoptions) + - [elementHandle.contentFrame()](#elementhandlecontentframe) + - [elementHandle.dblclick([options])](#elementhandledblclickoptions) + - [elementHandle.dispose()](#elementhandledispose) + - [elementHandle.evaluate(pageFunction[, ...args])](#elementhandleevaluatepagefunction-args) + - [elementHandle.evaluateHandle(pageFunction[, ...args])](#elementhandleevaluatehandlepagefunction-args) + - [elementHandle.executionContext()](#elementhandleexecutioncontext) + - [elementHandle.fill(value)](#elementhandlefillvalue) + - [elementHandle.focus()](#elementhandlefocus) + - [elementHandle.getProperties()](#elementhandlegetproperties) + - [elementHandle.getProperty(propertyName)](#elementhandlegetpropertypropertyname) + - [elementHandle.hover([options])](#elementhandlehoveroptions) + - [elementHandle.isIntersectingViewport()](#elementhandleisintersectingviewport) + - [elementHandle.jsonValue()](#elementhandlejsonvalue) + - [elementHandle.press(key[, options])](#elementhandlepresskey-options) + - [elementHandle.screenshot([options])](#elementhandlescreenshotoptions) + - [elementHandle.select(...values)](#elementhandleselectvalues) + - [elementHandle.tap([options])](#elementhandletapoptions) + - [elementHandle.toString()](#elementhandletostring) + - [elementHandle.tripleclick([options])](#elementhandletripleclickoptions) + - [elementHandle.type(text[, options])](#elementhandletypetext-options) + - [elementHandle.uploadFile(...filePaths)](#elementhandleuploadfilefilepaths) + - [class: Request](#class-request) + - [request.abort([errorCode])](#requestaborterrorcode) + - [request.continue([overrides])](#requestcontinueoverrides) + - [request.failure()](#requestfailure) + - [request.frame()](#requestframe) + - [request.headers()](#requestheaders) + - [request.isNavigationRequest()](#requestisnavigationrequest) + - [request.method()](#requestmethod) + - [request.postData()](#requestpostdata) + - [request.redirectChain()](#requestredirectchain) + - [request.resourceType()](#requestresourcetype) + - [request.respond(response)](#requestrespondresponse) + - [request.response()](#requestresponse) + - [request.url()](#requesturl) + - [class: Response](#class-response) + - [response.buffer()](#responsebuffer) + - [response.frame()](#responseframe) + - [response.fromCache()](#responsefromcache) + - [response.fromServiceWorker()](#responsefromserviceworker) + - [response.headers()](#responseheaders) + - [response.json()](#responsejson) + - [response.ok()](#responseok) + - [response.remoteAddress()](#responseremoteaddress) + - [response.request()](#responserequest) + - [response.securityDetails()](#responsesecuritydetails) + - [response.status()](#responsestatus) + - [response.statusText()](#responsestatustext) + - [response.text()](#responsetext) + - [response.url()](#responseurl) + - [class: SecurityDetails](#class-securitydetails) + - [securityDetails.issuer()](#securitydetailsissuer) + - [securityDetails.protocol()](#securitydetailsprotocol) + - [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.detach()](#cdpsessiondetach) + - [cdpSession.send(method[, params])](#cdpsessionsendmethod-params) + - [class: Coverage](#class-coverage) + - [coverage.startCSSCoverage([options])](#coveragestartcsscoverageoptions) + - [coverage.startJSCoverage([options])](#coveragestartjscoverageoptions) + - [coverage.stopCSSCoverage()](#coveragestopcsscoverage) + - [coverage.stopJSCoverage()](#coveragestopjscoverage) + - [class: TimeoutError](#class-timeouterror) + + +### Overview + +Playwright is a Node library which provides a high-level API to control Chromium or Chrome over the DevTools Protocol. + +The Playwright API is hierarchical and mirrors the browser structure. + +> **NOTE** On the following diagram, faded entities are not currently represented in Playwright. + +![playwright overview](https://user-images.githubusercontent.com/746130/40333229-5df5480c-5d0c-11e8-83cb-c3e371de7374.png) + +- [`Playwright`](#class-playwright) 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)) + +### playwright vs playwright-core + +Every release since v1.7.0 we publish two packages: +- [playwright](https://www.npmjs.com/package/playwright) +- [playwright-core](https://www.npmjs.com/package/playwright-core) + +`playwright` is a *product* for browser automation. When installed, it downloads a version of +Chromium, which it then drives using `playwright-core`. Being an end-user product, `playwright` supports a bunch of convenient `PLAYWRIGHT_*` env variables to tweak its behavior. + +`playwright-core` is a *library* to help drive anything that supports DevTools protocol. `playwright-core` doesn't download Chromium when installed. Being a library, `playwright-core` is fully driven +through its programmatic interface and disregards all the `PLAYWRIGHT_*` env variables. + +To sum up, the only differences between `playwright-core` and `playwright` are: +- `playwright-core` doesn't automatically download Chromium when installed. +- `playwright-core` ignores all `PLAYWRIGHT_*` env variables. + +In most cases, you'll be fine using the `playwright` package. + +However, you should use `playwright-core` if: +- you're building another end-user product or library atop of DevTools protocol. For example, one might build a PDF generator using `playwright-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 Playwright to use in Chrome Extension / browser with the DevTools protocol where downloading an additional Chromium binary is unnecessary. + +When using `playwright-core`, remember to change the *include* line: + +```js +const playwright = require('playwright-core'); +``` + +You will then need to call [`playwright.connect([options])`](#playwrightconnectoptions) or [`playwright.launch([options])`](#playwrightlaunchoptions) with an explicit `executablePath` option. + +### Environment Variables + +Playwright looks for certain [environment variables](https://en.wikipedia.org/wiki/Environment_variable) to aid its operations. +If Playwright 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. +- `PLAYWRIGHT_SKIP_CHROMIUM_DOWNLOAD` - do not download bundled Chromium during installation step. +- `PLAYWRIGHT_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`. +- `PLAYWRIGHT_CHROMIUM_REVISION` - specify a certain version of Chromium you'd like Playwright to use. See [playwright.launch([options])](#playwrightlaunchoptions) on how executable path is inferred. **BEWARE**: Playwright is only [guaranteed to work](https://github.com/Microsoft/playwright/#q-why-doesnt-playwright-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk. +- `PLAYWRIGHT_EXECUTABLE_PATH` - specify an executable path to be used in `playwright.launch`. See [playwright.launch([options])](#playwrightlaunchoptions) on how the executable path is inferred. **BEWARE**: Playwright is only [guaranteed to work](https://github.com/Microsoft/playwright/#q-why-doesnt-playwright-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk. + +> **NOTE** PLAYWRIGHT_* env variables are not accounted for in the [`playwright-core`](https://www.npmjs.com/package/playwright-core) package. + + +### Working with Chrome Extensions + +Playwright 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 playwright = require('playwright'); + +(async () => { + const pathToExtension = require('path').join(__dirname, 'my-extension'); + const browser = await playwright.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: Playwright + +Playwright module provides a method to launch a Chromium instance. +The following is a typical example of using Playwright to drive automation: +```js +const playwright = require('playwright'); + +(async () => { + const browser = await playwright.launch(); + const page = await browser.newPage(); + await page.goto('https://www.google.com'); + // other actions... + await browser.close(); +})(); +``` + +#### playwright.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 Playwright 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 Playwright 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 Playwright to use. +- returns: <[Promise]<[Browser]>> + +This methods attaches Playwright to an existing Chromium instance. + +#### playwright.createBrowserFetcher([options]) +- `options` <[Object]> + - `host` <[string]> A download host to be used. Defaults to `https://storage.googleapis.com`. + - `path` <[string]> A path for the downloads folder. Defaults to `/.local-chromium`, where `` is playwright's package root. + - `platform` <[string]> Possible values are: `mac`, `win32`, `win64`, `linux`. Defaults to the current platform. +- returns: <[BrowserFetcher]> + +#### playwright.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. + +#### playwright.devices +- returns: <[Object]> + +Returns a list of devices to be used with [`page.emulate(options)`](#pageemulateoptions). Actual list of +devices can be found in [lib/DeviceDescriptors.js](https://github.com/Microsoft/playwright/blob/master/lib/DeviceDescriptors.js). + +```js +const playwright = require('playwright'); +const iPhone = playwright.devices['iPhone 6']; + +(async () => { + const browser = await playwright.launch(); + const page = await browser.newPage(); + await page.emulate(iPhone); + await page.goto('https://www.google.com'); + // other actions... + await browser.close(); +})(); +``` + +> **NOTE** The old way (Playwright versions <= v1.14.0) devices can be obtained with `require('playwright/DeviceDescriptors')`. + +#### playwright.errors +- returns: <[Object]> + - `TimeoutError` <[function]> A class of [TimeoutError]. + +Playwright 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 Playwright uses specific error classes. +These classes are available via [`playwright.errors`](#playwrighterrors) + +An example of handling a timeout error: +```js +try { + await page.waitForSelector('.foo'); +} catch (e) { + if (e instanceof playwright.errors.TimeoutError) { + // Do something if this is a timeout. + } +} +``` + +> **NOTE** The old way (Playwright versions <= v1.14.0) errors can be obtained with `require('playwright/Errors')`. + +#### playwright.executablePath() +- returns: <[string]> A path where Playwright expects to find bundled Chromium. Chromium might not exist there if the download was skipped with [`PLAYWRIGHT_SKIP_CHROMIUM_DOWNLOAD`](#environment-variables). + +> **NOTE** `playwright.executablePath()` is affected by the `PLAYWRIGHT_EXECUTABLE_PATH` and `PLAYWRIGHT_CHROMIUM_REVISION` env variables. See [Environment Variables](#environment-variables) for details. + + +#### playwright.launch([options]) +- `options` <[Object]> Set of configurable options to set on the browser. Can have the following fields: + - `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 Chromium or Chrome 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**: Playwright is only [guaranteed to work](https://github.com/Microsoft/playwright/#q-why-doesnt-playwright-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk. + - `slowMo` <[number]> Slows down Playwright 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/). + - `ignoreDefaultArgs` <[boolean]|[Array]<[string]>> If `true`, then do not use [`playwright.defaultArgs()`](#playwrightdefaultargsoptions). 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`. +- 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 playwright.launch({ + ignoreDefaultArgs: ['--mute-audio'] +}); +``` + +> **NOTE** Playwright 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 [playwright.launch([options])](#playwrightlaunchoptions) 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. + +### class: BrowserFetcher + +BrowserFetcher can download and manage different versions of Chromium. + +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/). + +An example of using BrowserFetcher to download a specific version of Chromium and running +Playwright against it: + +```js +const browserFetcher = playwright.createBrowserFetcher(); +const revisionInfo = await browserFetcher.download('533271'); +const browser = await playwright.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.localRevisions() +- returns: <[Promise]<[Array]<[string]>>> A list of all revisions available locally on disk. + +#### browserFetcher.platform() +- returns: <[string]> One of `mac`, `linux`, `win32` or `win64`. + +#### browserFetcher.remove(revision) +- `revision` <[string]> a revision to remove. 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 + +### class: Browser + +* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) + +A Browser is created when Playwright connects to a Chromium instance, either through [`playwright.launch`](#playwrightlaunchoptions) or [`playwright.connect`](#playwrightconnectoptions). + +An example of using a [Browser] to create a [Page]: +```js +const playwright = require('playwright'); + +(async () => { + const browser = await playwright.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 playwright = require('playwright'); + +(async () => { + const browser = await playwright.launch(); + // Store the endpoint to be able to reconnect to Chromium + const browserWSEndpoint = browser.wsEndpoint(); + // Disconnect playwright from Chromium + browser.disconnect(); + + // Use the endpoint to reestablish a connection + const browser2 = await playwright.connect({browserWSEndpoint}); + // Close Chromium + await browser2.close(); +})(); +``` +#### event: 'disconnected' +Emitted when Playwright 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 playwright.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 Playwright 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 [`playwright.connect`](#playwrightconnectoptions) 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 +[playwright.connect](#playwrightconnectoptions). 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](https://nodejs.org/api/events.html#events_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. + +Playwright 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](https://nodejs.org/api/events.html#events_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 playwright = require('playwright'); + +(async () => { + const browser = await playwright.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 Node's native [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter) methods, such as `on`, `once` or `removeListener`. + +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 `removeListener` method: + +```js +function logRequest(interceptedRequest) { + console.log('A request was made:', interceptedRequest.url()); +} +page.on('request', logRequest); +// Sometime later... +page.removeListener('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`. Playwright 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' +- <[Request]> + +Emitted when a page issues a request. The [request] object is read-only. +In order to intercept and mutate requests, see `page.setRequestInterception`. + +#### event: 'requestfailed' +- <[Request]> + +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' +- <[Request]> + +Emitted when a request finishes successfully. + +#### event: 'response' +- <[Response]> + +Emitted when a [response] is received. + +#### event: 'workercreated' +- <[Worker]> + +Emitted when a dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is spawned by the page. + +#### event: 'workerdestroyed' +- <[Worker]> + +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 divsCounts = await page.$$eval('div', divs => divs.length); +``` + +#### 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. + +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 ` + diff --git a/test/assets/cached/one-style.css b/test/assets/cached/one-style.css new file mode 100644 index 0000000000..04e7110b41 --- /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 0000000000..4760f2b9f7 --- /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 0000000000..7fa1b211a4 --- /dev/null +++ b/test/assets/consolelog.html @@ -0,0 +1,11 @@ + + + + console.log test + + + + + diff --git a/test/assets/csp.html b/test/assets/csp.html new file mode 100644 index 0000000000..34fc1fc1a5 --- /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 0000000000..a9b3c8b34e --- /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 0000000000..bcd9845b93 --- /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 0000000000..bfb89f8f75 --- /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 0000000000..0fd97e962a --- /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 0000000000..3beae21829 --- /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 0000000000..df4e9c276c --- /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 0000000000..60f1eab971 --- /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 0000000000..a87defb098 --- /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 0000000000..5b8186a3bf --- /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 0000000000..80a4123fbd --- /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 0000000000..e69de29bb2 diff --git a/test/assets/error.html b/test/assets/error.html new file mode 100644 index 0000000000..130400c006 --- /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 0000000000..1903e176f5 --- /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 0000000000..9a0a1095d1 --- /dev/null +++ b/test/assets/es6/es6import.js @@ -0,0 +1,2 @@ +import num from './es6module.js'; +window.__es6injected = num; \ No newline at end of file diff --git a/test/assets/es6/es6module.js b/test/assets/es6/es6module.js new file mode 100644 index 0000000000..a4012bff06 --- /dev/null +++ b/test/assets/es6/es6module.js @@ -0,0 +1 @@ +export default 42; \ No newline at end of file diff --git a/test/assets/es6/es6pathimport.js b/test/assets/es6/es6pathimport.js new file mode 100644 index 0000000000..99919621a8 --- /dev/null +++ b/test/assets/es6/es6pathimport.js @@ -0,0 +1,2 @@ +import num from './es6/es6module.js'; +window.__es6injected = num; \ No newline at end of file diff --git a/test/assets/file-to-upload.txt b/test/assets/file-to-upload.txt new file mode 100644 index 0000000000..b4ad118489 --- /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/frames/frame.html b/test/assets/frames/frame.html new file mode 100644 index 0000000000..8f20d2da9f --- /dev/null +++ b/test/assets/frames/frame.html @@ -0,0 +1,8 @@ + + + +
Hi, I'm frame
diff --git a/test/assets/frames/frameset.html b/test/assets/frames/frameset.html new file mode 100644 index 0000000000..4d56f88839 --- /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 0000000000..de1987586f --- /dev/null +++ b/test/assets/frames/nested-frames.html @@ -0,0 +1,25 @@ + + + + diff --git a/test/assets/frames/one-frame.html b/test/assets/frames/one-frame.html new file mode 100644 index 0000000000..e941d795a2 --- /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 0000000000..be22256d16 --- /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 0000000000..5b5436e874 --- /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 0000000000..b2ee853eda --- /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 0000000000..b6be975038 --- /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 0000000000..0bdbb1220e --- /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 0000000000..bacaf9e9a0 --- /dev/null +++ b/test/assets/historyapi.html @@ -0,0 +1,5 @@ + diff --git a/test/assets/injectedfile.js b/test/assets/injectedfile.js new file mode 100644 index 0000000000..6cb04f1bba --- /dev/null +++ b/test/assets/injectedfile.js @@ -0,0 +1,2 @@ +window.__injected = 42; +window.__injectedError = new Error('hi'); \ No newline at end of file diff --git a/test/assets/injectedstyle.css b/test/assets/injectedstyle.css new file mode 100644 index 0000000000..aa1634c255 --- /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 0000000000..aaba6a5e2a --- /dev/null +++ b/test/assets/input/button.html @@ -0,0 +1,22 @@ + + + + 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 0000000000..ca56762e2b --- /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 0000000000..55fd7c5006 --- /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 0000000000..fd962c7518 --- /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 0000000000..3c4d57033c --- /dev/null +++ b/test/assets/input/mouse-helper.js @@ -0,0 +1,62 @@ +// 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 0000000000..1bce66cf5e --- /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 0000000000..885d3739d5 --- /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 0000000000..879a537a76 --- /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 0000000000..f71a0054b5 --- /dev/null +++ b/test/assets/input/textarea.html @@ -0,0 +1,19 @@ + + + + Textarea test + + + + +
+ + + + \ No newline at end of file diff --git a/test/assets/input/touches.html b/test/assets/input/touches.html new file mode 100644 index 0000000000..4392cfacbd --- /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/jscoverage/eval.html b/test/assets/jscoverage/eval.html new file mode 100644 index 0000000000..838ae28763 --- /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 0000000000..889c86bed5 --- /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 0000000000..bdef59885b --- /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 0000000000..a537a7da6a --- /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 0000000000..3bd241b50e --- /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 0000000000..3bd241b50e --- /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 0000000000..49eeeea6ae --- /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 0000000000..e477750320 --- /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 0000000000..59c4a5a70b --- /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 0000000000..8e94b2fe29 --- /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 0000000000..7991a4ec40 --- /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 0000000000..d45e2a4129 --- /dev/null +++ b/test/assets/offscreenbuttons.html @@ -0,0 +1,36 @@ + + + + + + + + + + + + + diff --git a/test/assets/one-style.css b/test/assets/one-style.css new file mode 100644 index 0000000000..7b26410d8a --- /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 0000000000..4760f2b9f7 --- /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 0000000000..828cfb1c70 --- /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 0000000000..b855162c25 --- /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 0000000000..d138be1d22 --- /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 0000000000..e4e04b1f8a --- /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 0000000000..88aff620ff --- /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 0000000000..bef85d985b --- /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 0000000000..e69de29bb2 diff --git a/test/assets/serviceworkers/fetch/style.css b/test/assets/serviceworkers/fetch/style.css new file mode 100644 index 0000000000..7b26410d8a --- /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 0000000000..a9d28acb09 --- /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 0000000000..d44c7eab94 --- /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 0000000000..7242e673b5 --- /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 0000000000..965f99fd3d --- /dev/null +++ b/test/assets/simple-extension/content-script.js @@ -0,0 +1,3 @@ +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 0000000000..a0bb3f4eae --- /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 0000000000..da2cd082ed --- /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 0000000000..6d95903051 --- /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 0000000000..d027e97038 --- /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 0000000000..88a86ce412 --- /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 0000000000..7de2d9fd9e --- /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 0000000000..d0d229a192 --- /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)); + } +})(); \ No newline at end of file diff --git a/test/assets/wrappedlink.html b/test/assets/wrappedlink.html new file mode 100644 index 0000000000..429b6e9156 --- /dev/null +++ b/test/assets/wrappedlink.html @@ -0,0 +1,32 @@ + + + diff --git a/test/browser.spec.js b/test/browser.spec.js new file mode 100644 index 0000000000..75d9f0265b --- /dev/null +++ b/test/browser.spec.js @@ -0,0 +1,73 @@ +/** + * 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. + */ + +module.exports.addTests = function({testRunner, expect, headless, playwright, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe.skip(WEBKIT)('Browser.version', function() { + it('should return whether we are in headless', async({browser}) => { + const version = await browser.version(); + expect(version.length).toBeGreaterThan(0); + if (CHROME || WEBKIT) + expect(version.startsWith('Headless')).toBe(headless); + else + expect(version.startsWith('Firefox/')).toBe(true); + }); + }); + + describe.skip(WEBKIT)('Browser.userAgent', function() { + it('should include WebKit', async({browser}) => { + const userAgent = await browser.userAgent(); + expect(userAgent.length).toBeGreaterThan(0); + if (CHROME || WEBKIT) + expect(userAgent).toContain('WebKit'); + else + expect(userAgent).toContain('Gecko'); + }); + }); + + describe.skip(WEBKIT)('Browser.target', function() { + it('should return browser target', async({browser}) => { + const target = browser.target(); + expect(target.type()).toBe('browser'); + }); + }); + + describe('Browser.process', function() { + it('should return child_process instance', async function({browser}) { + const process = await browser.process(); + expect(process.pid).toBeGreaterThan(0); + }); + it.skip(WEBKIT)('should not return child_process for remote browser', async function({browser}) { + const browserWSEndpoint = browser.wsEndpoint(); + const remoteBrowser = await playwright.connect({browserWSEndpoint}); + expect(remoteBrowser.process()).toBe(null); + remoteBrowser.disconnect(); + }); + }); + + describe.skip(WEBKIT)('Browser.isConnected', () => { + it('should set the browser connected state', async({browser}) => { + const browserWSEndpoint = browser.wsEndpoint(); + const newBrowser = await playwright.connect({browserWSEndpoint}); + expect(newBrowser.isConnected()).toBe(true); + newBrowser.disconnect(); + expect(newBrowser.isConnected()).toBe(false); + }); + }); +}; diff --git a/test/browsercontext.spec.js b/test/browsercontext.spec.js new file mode 100644 index 0000000000..4c2cd071d9 --- /dev/null +++ b/test/browsercontext.spec.js @@ -0,0 +1,156 @@ +/** + * 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 utils = require('./utils'); + +module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('BrowserContext', function() { + it('should have default context', async function({browser, server}) { + expect(browser.browserContexts().length).toBe(1); + const defaultContext = browser.browserContexts()[0]; + expect(defaultContext.isIncognito()).toBe(false); + let error = null; + await defaultContext.close().catch(e => error = e); + expect(browser.defaultBrowserContext()).toBe(defaultContext); + expect(error.message).toContain('cannot be closed'); + }); + it('should create new incognito context', async function({browser, server}) { + 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.skip(WEBKIT)('should close all belonging targets once closing context', async function({browser, server}) { + 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); + }); + it('window.open should use parent tab context', async function({browser, server}) { + 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 => window.open(url), server.EMPTY_PAGE) + ]); + expect(popupTarget.browserContext()).toBe(context); + await context.close(); + }); + it.skip(WEBKIT)('should fire target events', async function({browser, server}) { + 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(); + }); + it('should wait for a target', async function({browser, server}) { + const context = await browser.createIncognitoBrowserContext(); + let resolved = false; + const targetPromise = context.waitForTarget(target => target.url() === server.EMPTY_PAGE); + targetPromise.then(() => resolved = true); + const page = await context.newPage(); + expect(resolved).toBe(false); + await page.goto(server.EMPTY_PAGE); + const target = await targetPromise; + expect(await target.page()).toBe(page); + await context.close(); + }); + it('should timeout waiting for a non-existent target', async function({browser, server}) { + const context = await browser.createIncognitoBrowserContext(); + const error = await context.waitForTarget(target => target.url() === server.EMPTY_PAGE, {timeout: 1}).catch(e => e); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + await context.close(); + }); + it('should isolate localStorage and cookies', async function({browser, server}) { + // 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); + }); + it.skip(WEBKIT)('should work across sessions', async function({browser, server}) { + expect(browser.browserContexts().length).toBe(1); + const context = await browser.createIncognitoBrowserContext(); + expect(browser.browserContexts().length).toBe(2); + const remoteBrowser = await playwright.connect({ + browserWSEndpoint: browser.wsEndpoint() + }); + const contexts = remoteBrowser.browserContexts(); + expect(contexts.length).toBe(2); + remoteBrowser.disconnect(); + await context.close(); + }); + }); +}; diff --git a/test/chromiumonly.spec.js b/test/chromiumonly.spec.js new file mode 100644 index 0000000000..8d80b4a097 --- /dev/null +++ b/test/chromiumonly.spec.js @@ -0,0 +1,144 @@ +/** + * 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.addLauncherTests = function({testRunner, expect, defaultBrowserOptions, playwright}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Chromium-Specific Launcher tests', function() { + describe('Playwright.launch |browserURL| option', function() { + it('should be able to connect using browserUrl, with and without trailing slash', async({server}) => { + const originalBrowser = await playwright.launch(Object.assign({}, defaultBrowserOptions, { + args: ['--remote-debugging-port=21222'] + })); + const browserURL = 'http://127.0.0.1:21222'; + + const browser1 = await playwright.connect({browserURL}); + const page1 = await browser1.newPage(); + expect(await page1.evaluate(() => 7 * 8)).toBe(56); + browser1.disconnect(); + + const browser2 = await playwright.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({server}) => { + const originalBrowser = await playwright.launch(Object.assign({}, defaultBrowserOptions, { + args: ['--remote-debugging-port=21222'] + })); + const browserURL = 'http://127.0.0.1:21222'; + + let error = null; + await playwright.connect({browserURL, browserWSEndpoint: originalBrowser.wsEndpoint()}).catch(e => error = e); + 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({server}) => { + const originalBrowser = await playwright.launch(Object.assign({}, defaultBrowserOptions, { + args: ['--remote-debugging-port=21222'] + })); + const browserURL = 'http://127.0.0.1:32333'; + + let error = null; + await playwright.connect({browserURL}).catch(e => error = e); + expect(error.message).toContain('Failed to fetch browser webSocket url from'); + originalBrowser.close(); + }); + }); + + describe('Playwright.launch |pipe| option', function() { + it('should support the pipe option', async() => { + const options = Object.assign({pipe: true}, defaultBrowserOptions); + const browser = await playwright.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 options = Object.assign({}, defaultBrowserOptions); + options.args = ['--remote-debugging-pipe'].concat(options.args || []); + const browser = await playwright.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 options = Object.assign({pipe: true}, defaultBrowserOptions); + const browser = await playwright.launch(options); + const disconnectedEventPromise = new Promise(resolve => browser.once('disconnected', resolve)); + // Emulate user exiting browser. + browser.process().kill(); + await disconnectedEventPromise; + }); + }); + + describe('Page.waitForFileChooser', () => { + it('should fail gracefully when trying to work with filechoosers within multiple connections', async() => { + // 1. Launch a browser and connect to all pages. + const originalBrowser = await playwright.launch(defaultBrowserOptions); + await originalBrowser.pages(); + // 2. Connect a remote browser and connect to first page. + const remoteBrowser = await playwright.connect({browserWSEndpoint: originalBrowser.wsEndpoint()}); + const [page] = await remoteBrowser.pages(); + // 3. Make sure |page.waitForFileChooser()| does not work with multiclient. + let error = null; + await page.waitForFileChooser().catch(e => error = e); + expect(error.message).toBe('File chooser handling does not work with multiple connections to the same page'); + originalBrowser.close(); + }); + + }); + }); +}; + +module.exports.addPageTests = function({testRunner, expect}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Chromium-Specific Page Tests', function() { + it('Page.setRequestInterception should work with intervention headers', async({server, page}) => { + 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.js b/test/click.spec.js new file mode 100644 index 0000000000..344809b030 --- /dev/null +++ b/test/click.spec.js @@ -0,0 +1,310 @@ +/** + * 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 utils = require('./utils'); + +module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Page.click', function() { + it('should click the button', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + await page.click('button'); + expect(await page.evaluate(() => result)).toBe('Clicked'); + }); + it('should click svg', async({page, server}) => { + await page.setContent(` + + + + `); + await page.click('circle'); + expect(await page.evaluate(() => window.__CLICKED)).toBe(42); + }); + it.skip(FFOX || WEBKIT)('should click the button if window.Node is removed', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + await page.evaluate(() => delete window.Node); + await page.click('button'); + expect(await page.evaluate(() => result)).toBe('Clicked'); + }); + // @see https://github.com/GoogleChrome/puppeteer/issues/4281 + it('should click on a span with an inline element inside', async({page, server}) => { + await page.setContent(` + + + `); + await page.click('span'); + expect(await page.evaluate(() => window.CLICKED)).toBe(42); + }); + it('should not throw UnhandledPromiseRejection when page closes', async({page, server}) => { + const newPage = await page.browser().newPage(); + await Promise.all([ + newPage.close(), + newPage.mouse.click(1, 2), + ]).catch(e => {}); + }); + it('should click the button after navigation ', async({page, server}) => { + 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(() => result)).toBe('Clicked'); + }); + it.skip(FFOX || WEBKIT)('should click with disabled javascript', async({page, server}) => { + 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.skip(FFOX)('should click when one of inline box children is outside of viewport', async({page, server}) => { + await page.setContent(` + + woofdoggo + `); + await page.click('span'); + expect(await page.evaluate(() => window.CLICKED)).toBe(42); + }); + it.skip(FFOX || WEBKIT)('should select the text by triple clicking', async({page, server}) => { + 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.tripleclick('textarea'); + 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({page, server}) => { + 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({page, server}) => { + await page.goto(server.PREFIX + '/wrappedlink.html'); + await page.click('a'); + expect(await page.evaluate(() => window.__clicked)).toBe(true); + }); + + it('should click on checkbox input and toggle', async({page, server}) => { + await page.goto(server.PREFIX + '/input/checkbox.html'); + expect(await page.evaluate(() => result.check)).toBe(null); + await page.click('input#agree'); + expect(await page.evaluate(() => result.check)).toBe(true); + expect(await page.evaluate(() => result.events)).toEqual([ + 'mouseover', + 'mouseenter', + 'mousemove', + 'mousedown', + 'mouseup', + 'click', + 'input', + 'change', + ]); + await page.click('input#agree'); + expect(await page.evaluate(() => result.check)).toBe(false); + }); + + it('should click on checkbox label and toggle', async({page, server}) => { + await page.goto(server.PREFIX + '/input/checkbox.html'); + expect(await page.evaluate(() => result.check)).toBe(null); + await page.click('label[for="agree"]'); + expect(await page.evaluate(() => result.check)).toBe(true); + expect(await page.evaluate(() => result.events)).toEqual([ + 'click', + 'input', + 'change', + ]); + await page.click('label[for="agree"]'); + expect(await page.evaluate(() => result.check)).toBe(false); + }); + + it('should fail to click a missing button', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + let error = null; + await page.click('button.does-not-exist').catch(e => error = e); + expect(error.message).toBe('No node found for selector: button.does-not-exist'); + }); + // @see https://github.com/GoogleChrome/puppeteer/issues/161 + it('should not hang with touch-enabled viewports', async({page, server}) => { + await page.setViewport(playwright.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({page, server}) => { + 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'); + }); + it.skip(FFOX || WEBKIT)('should double click the button', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + await page.evaluate(() => { + window.double = false; + const button = document.querySelector('button'); + button.addEventListener('dblclick', event => { + window.double = true; + }); + }); + const button = await page.$('button'); + await button.dblclick(); + expect(await page.evaluate('double')).toBe(true); + expect(await page.evaluate('result')).toBe('Clicked'); + }); + it('should click a partially obscured button', async({page, server}) => { + 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(() => window.result)).toBe('Clicked'); + }); + it('should click a rotated button', async({page, server}) => { + await page.goto(server.PREFIX + '/input/rotatedButton.html'); + await page.click('button'); + expect(await page.evaluate(() => result)).toBe('Clicked'); + }); + it('should fire contextmenu event on right click', async({page, server}) => { + 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/GoogleChrome/puppeteer/issues/206 + it('should click links which cause navigation', async({page, server}) => { + await page.setContent(`empty.html`); + // This await should not hang. + await page.click('a'); + }); + it('should click the button inside an iframe', async({page, server}) => { + 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(() => window.result)).toBe('Clicked'); + }); + // @see https://github.com/GoogleChrome/puppeteer/issues/4110 + xit('should click the button with fixed position inside an iframe', async({page, server}) => { + 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 => button.style.setProperty('position', 'fixed')); + await frame.click('button'); + expect(await frame.evaluate(() => window.result)).toBe('Clicked'); + }); + it.skip(WEBKIT)('should click the button with deviceScaleFactor set', async({page, server}) => { + 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(() => window.result)).toBe('Clicked'); + }); + + it.skip(FFOX || WEBKIT)('should click the button with relative point', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + await page.click('button', { relativePoint: { x: 20, y: 10 } }); + expect(await page.evaluate(() => result)).toBe('Clicked'); + expect(await page.evaluate(() => offsetX)).toBe(20); + expect(await page.evaluate(() => offsetY)).toBe(10); + }); + it.skip(FFOX || WEBKIT)('should click a very large button with relative point', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + await page.$eval('button', button => button.style.height = button.style.width = '2000px'); + await page.click('button', { relativePoint: { x: 1900, y: 1910 } }); + expect(await page.evaluate(() => window.result)).toBe('Clicked'); + expect(await page.evaluate(() => offsetX)).toBe(1900); + expect(await page.evaluate(() => offsetY)).toBe(1910); + }); + xit('should click a button in scrolling container with relative point', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + await page.$eval('button', button => { + const container = document.createElement('div'); + container.style.overflow = 'auto'; + container.style.width = '200px'; + container.style.height = '200px'; + button.parentElement.insertBefore(container, button); + container.appendChild(button); + button.style.height = '2000px'; + button.style.width = '2000px'; + }); + await page.click('button', { relativePoint: { x: 1900, y: 1910 } }); + expect(await page.evaluate(() => window.result)).toBe('Clicked'); + expect(await page.evaluate(() => offsetX)).toBe(1900); + expect(await page.evaluate(() => offsetY)).toBe(1910); + }); + + it.skip(FFOX || WEBKIT)('should update modifiers correctly', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + await page.click('button', { modifiers: ['Shift'] }); + expect(await page.evaluate(() => shiftKey)).toBe(true); + await page.click('button', { modifiers: [] }); + expect(await page.evaluate(() => shiftKey)).toBe(false); + + await page.keyboard.down('Shift'); + await page.click('button', { modifiers: [] }); + expect(await page.evaluate(() => shiftKey)).toBe(false); + await page.click('button'); + expect(await page.evaluate(() => shiftKey)).toBe(true); + await page.keyboard.up('Shift'); + await page.click('button'); + expect(await page.evaluate(() => shiftKey)).toBe(false); + }); + }); +}; diff --git a/test/cookies.spec.js b/test/cookies.spec.js new file mode 100644 index 0000000000..72ba2507e0 --- /dev/null +++ b/test/cookies.spec.js @@ -0,0 +1,395 @@ +/** + * 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. + */ + +module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Page.cookies', function() { + it('should return no cookies in pristine browser context', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + expect(await page.cookies()).toEqual([]); + }); + it('should get a cookie', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + document.cookie = 'username=John Doe'; + }); + expect(await page.cookies()).toEqual([{ + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + expires: -1, + size: 16, + httpOnly: false, + secure: false, + session: true, + sameSite: 'None', + }]); + }); + it('should properly report httpOnly cookie', async({page, server}) => { + server.setRoute('/empty.html', (req, res) => { + res.setHeader('Set-Cookie', ';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.skip(FFOX || WEBKIT)('should properly report "Strict" sameSite cookie', async({page, server}) => { + server.setRoute('/empty.html', (req, res) => { + res.setHeader('Set-Cookie', ';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.skip(FFOX || WEBKIT)('should properly report "Lax" sameSite cookie', async({page, server}) => { + server.setRoute('/empty.html', (req, res) => { + res.setHeader('Set-Cookie', ';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({page, server}) => { + 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)); + expect(cookies).toEqual([ + { + name: 'password', + value: '1234', + domain: 'localhost', + path: '/', + expires: -1, + size: 12, + httpOnly: false, + secure: false, + session: true, + sameSite: 'None', + }, + { + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + expires: -1, + size: 16, + httpOnly: false, + secure: false, + session: true, + sameSite: 'None', + }, + ]); + }); + it.skip(WEBKIT)('should get cookies from multiple urls', async({page, server}) => { + 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)); + expect(cookies).toEqual([{ + name: 'birdo', + value: 'tweets', + domain: 'baz.com', + path: '/', + expires: -1, + size: 11, + httpOnly: false, + secure: true, + session: true, + sameSite: 'None', + }, { + name: 'doggo', + value: 'woofs', + domain: 'foo.com', + path: '/', + expires: -1, + size: 10, + httpOnly: false, + secure: true, + session: true, + sameSite: 'None', + }]); + }); + }); + + describe.skip(WEBKIT)('Page.setCookie', function() { + it('should work', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'password', + value: '123456' + }); + expect(await page.evaluate(() => document.cookie)).toEqual('password=123456'); + }); + it('should isolate cookies in browser contexts', async({page, server, browser}) => { + 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(); + }); + it('should set multiple cookies', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'password', + value: '123456' + }, { + name: 'foo', + value: 'bar' + }); + expect(await page.evaluate(() => { + const cookies = document.cookie.split(';'); + return cookies.map(cookie => cookie.trim()).sort(); + })).toEqual([ + 'foo=bar', + 'password=123456', + ]); + }); + it('should have |expires| set to |-1| for session cookies', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'password', + value: '123456' + }); + const cookies = await page.cookies(); + expect(cookies[0].session).toBe(true); + expect(cookies[0].expires).toBe(-1); + }); + it('should set cookie with reasonable defaults', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'password', + value: '123456' + }); + const cookies = await page.cookies(); + expect(cookies.sort((a, b) => a.name.localeCompare(b.name))).toEqual([{ + name: 'password', + value: '123456', + domain: 'localhost', + path: '/', + expires: -1, + size: 14, + httpOnly: false, + secure: false, + session: true, + sameSite: 'None', + }]); + }); + it('should set a cookie with a path', async({page, server}) => { + await page.goto(server.PREFIX + '/grid.html'); + await page.setCookie({ + name: 'gridcookie', + value: 'GRID', + path: '/grid.html' + }); + expect(await page.cookies()).toEqual([{ + name: 'gridcookie', + value: 'GRID', + domain: 'localhost', + path: '/grid.html', + expires: -1, + size: 14, + httpOnly: false, + secure: false, + session: true, + sameSite: 'None', + }]); + expect(await page.evaluate('document.cookie')).toBe('gridcookie=GRID'); + await page.goto(server.EMPTY_PAGE); + expect(await page.cookies()).toEqual([]); + 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 function({page}) { + await page.goto('about:blank'); + let error = null; + try { + await page.setCookie({name: 'example-cookie', value: 'best'}); + } catch (e) { + error = e; + } + 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 function({page, server}) { + 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 (e) { + error = e; + } + 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 function({page}) { + let error = null; + await page.goto('data:,Hello%2C%20World!'); + try { + await page.setCookie({name: 'example-cookie', value: 'best'}); + } catch (e) { + error = e; + } + expect(error.message).toContain('At least one of the url and domain needs to be specified'); + }); + it('should default to setting secure cookie for HTTPS websites', async({page, server}) => { + 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({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const HTTP_URL = 'http://example.com'; + await page.setCookie({ + url: HTTP_URL, + name: 'foo', + value: 'bar', + }); + const [cookie] = await page.cookies(HTTP_URL); + expect(cookie.secure).toBe(false); + }); + it('should set a cookie on a different domain', async({page, server}) => { + 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(''); + expect(await page.cookies()).toEqual([]); + expect(await page.cookies('https://www.example.com')).toEqual([{ + name: 'example-cookie', + value: 'best', + domain: 'www.example.com', + path: '/', + expires: -1, + size: 18, + httpOnly: false, + secure: true, + session: true, + sameSite: 'None', + }]); + }); + it('should set cookies from a frame', async({page, server}) => { + await page.goto(server.PREFIX + '/grid.html'); + await page.setCookie({name: 'localhost-cookie', value: 'best'}); + await page.evaluate(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('127-cookie=worst'); + + expect(await page.cookies()).toEqual([{ + name: 'localhost-cookie', + value: 'best', + domain: 'localhost', + path: '/', + expires: -1, + size: 20, + httpOnly: false, + secure: false, + session: true, + sameSite: 'None', + }]); + + expect(await page.cookies(server.CROSS_PROCESS_PREFIX)).toEqual([{ + name: '127-cookie', + value: 'worst', + domain: '127.0.0.1', + path: '/', + expires: -1, + size: 15, + httpOnly: false, + secure: false, + session: true, + sameSite: 'None', + }]); + }); + }); + + describe('Page.deleteCookie', function() { + it.skip(WEBKIT)('should work', async({page, server}) => { + 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.spec.js b/test/coverage.spec.js new file mode 100644 index 0000000000..aee11c12a7 --- /dev/null +++ b/test/coverage.spec.js @@ -0,0 +1,216 @@ +/** + * 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. + */ + +module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('JSCoverage', function() { + it('should work', async function({page, server}) { + 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 function({page, server}) { + 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 function({page, server}) { + 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 function({page, server}) { + 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 function({page, server}) { + 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 function({page, server}) { + 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 function({page, server}) { + 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 function({page, server}) { + 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 function({page, server}) { + 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 function({page, server}) { + 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 function({page, server}) { + 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 function({page, server}) { + await page.coverage.startJSCoverage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + debugger; // eslint-disable-line no-debugger + }); + await page.coverage.stopJSCoverage(); + }); + }); + + describe('CSSCoverage', function() { + it('should work', async function({page, server}) { + 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 function({page, server}) { + 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 function({page, server}) { + 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 function({page, server}) { + 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 function({page, server}) { + 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 function({page, server}) { + 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 function({page, server}) { + 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); + }); + describe('resetOnNavigation', function() { + it('should report stylesheets across navigations', async function({page, server}) { + 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 function({page, server}) { + 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); + }); + }); + it('should work with a recently loaded stylesheet', async function({page, server}) { + await page.coverage.startCSSCoverage(); + await page.evaluate(async url => { + document.body.textContent = 'hello, world'; + + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = url; + document.head.appendChild(link); + await new Promise(x => link.onload = x); + }, server.PREFIX + '/csscoverage/stylesheet1.css'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(1); + }); + }); +}; diff --git a/test/defaultbrowsercontext.spec.js b/test/defaultbrowsercontext.spec.js new file mode 100644 index 0000000000..c55792263d --- /dev/null +++ b/test/defaultbrowsercontext.spec.js @@ -0,0 +1,96 @@ +/** + * 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. + */ + +module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, playwright, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe.skip(FFOX)('DefaultBrowserContext', function() { + beforeEach(async state => { + state.browser = await playwright.launch(defaultBrowserOptions); + state.page = await state.browser.newPage(); + }); + afterEach(async state => { + await state.browser.close(); + delete state.browser; + delete state.page; + }); + it('page.cookies() should work', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + document.cookie = 'username=John Doe'; + }); + expect(await page.cookies()).toEqual([{ + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + expires: -1, + size: 16, + httpOnly: false, + secure: false, + session: true, + sameSite: 'None', + }]); + }); + it.skip(WEBKIT)('page.setCookie() should work', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'username', + value: 'John Doe' + }); + expect(await page.evaluate(() => document.cookie)).toBe('username=John Doe'); + expect(await page.cookies()).toEqual([{ + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + expires: -1, + size: 16, + httpOnly: false, + secure: false, + session: true, + sameSite: 'None', + }]); + }); + it.skip(WEBKIT)('page.deleteCookie() should work', async({page, server}) => { + 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'); + expect(await page.cookies()).toEqual([{ + name: 'cookie1', + value: '1', + domain: 'localhost', + path: '/', + expires: -1, + size: 8, + httpOnly: false, + secure: false, + session: true, + sameSite: 'None', + }]); + }); + }); +}; diff --git a/test/dialog.spec.js b/test/dialog.spec.js new file mode 100644 index 0000000000..0d9e58b752 --- /dev/null +++ b/test/dialog.spec.js @@ -0,0 +1,50 @@ +/** + * 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. + */ + +module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe.skip(WEBKIT)('Page.Events.Dialog', function() { + it('should fire', async({page, server}) => { + page.on('dialog', dialog => { + expect(dialog.type()).toBe('alert'); + expect(dialog.defaultValue()).toBe(''); + expect(dialog.message()).toBe('yo'); + dialog.accept(); + }); + await page.evaluate(() => alert('yo')); + }); + it('should allow accepting prompts', async({page, server}) => { + page.on('dialog', dialog => { + expect(dialog.type()).toBe('prompt'); + expect(dialog.defaultValue()).toBe('yes.'); + expect(dialog.message()).toBe('question?'); + dialog.accept('answer!'); + }); + const result = await page.evaluate(() => prompt('question?', 'yes.')); + expect(result).toBe('answer!'); + }); + it('should dismiss the prompt', async({page, server}) => { + 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 0000000000..c58f0e90a6 --- /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.js b/test/elementhandle.spec.js new file mode 100644 index 0000000000..feff82e641 --- /dev/null +++ b/test/elementhandle.spec.js @@ -0,0 +1,215 @@ +/** + * 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 utils = require('./utils'); + +module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe.skip(WEBKIT)('ElementHandle.boundingBox', function() { + it('should work', async({page, server}) => { + 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({page, server}) => { + 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 (CHROME) + expect(box).toEqual({ x: 28, y: 260, width: 264, height: 18 }); + else + expect(box).toEqual({ x: 28, y: 182, width: 247, height: 18 }); + }); + it('should return null for invisible elements', async({page, server}) => { + await page.setContent('
hi
'); + const element = await page.$('div'); + expect(await element.boundingBox()).toBe(null); + }); + it('should force a layout', async({page, server}) => { + await page.setViewport({ width: 500, height: 500 }); + await page.setContent('
hello
'); + const elementHandle = await page.$('div'); + await page.evaluate(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({page, server}) => { + await page.setContent(` + + + + `); + const element = await page.$('#therect'); + const pptrBoundingBox = await element.boundingBox(); + const webBoundingBox = await page.evaluate(e => { + const rect = e.getBoundingClientRect(); + return {x: rect.x, y: rect.y, width: rect.width, height: rect.height}; + }, element); + expect(pptrBoundingBox).toEqual(webBoundingBox); + }); + }); + + describe.skip(FFOX || WEBKIT)('ElementHandle.boxModel', function() { + it('should work', async({page, server}) => { + 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; + left: 1px; + 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 = ` + box-sizing: border-box; + position: absolute; + border-left: 1px solid black; + padding-left: 2px; + margin-left: 3px; + left: 4px; + top: 5px; + width: 6px; + 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({page, server}) => { + await page.setContent('
hi
'); + const element = await page.$('div'); + expect(await element.boxModel()).toBe(null); + }); + }); + + describe.skip(WEBKIT)('ElementHandle.contentFrame', function() { + it('should work', async({page,server}) => { + 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() { + it('should work', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await button.click(); + expect(await page.evaluate(() => result)).toBe('Clicked'); + }); + it('should work for Shadow DOM v1', async({page, server}) => { + await page.goto(server.PREFIX + '/shadow.html'); + const buttonHandle = await page.evaluateHandle(() => button); + await buttonHandle.click(); + expect(await page.evaluate(() => clicked)).toBe(true); + }); + it('should work for TextNodes', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + const buttonTextNode = await page.evaluateHandle(() => document.querySelector('button').firstChild); + let error = null; + await buttonTextNode.click().catch(err => error = err); + expect(error.message).toBe('Node is not of type HTMLElement'); + }); + it('should throw for detached nodes', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await page.evaluate(button => button.remove(), button); + let error = null; + await button.click().catch(err => error = err); + expect(error.message).toBe('Node is detached from document'); + }); + it('should throw for hidden nodes', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await page.evaluate(button => button.style.display = 'none', button); + const error = await button.click().catch(err => err); + expect(error.message).toBe('Node is either not visible or not an HTMLElement'); + }); + it('should throw for recursively hidden nodes', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await page.evaluate(button => button.parentElement.style.display = 'none', button); + const error = await button.click().catch(err => err); + expect(error.message).toBe('Node is either not visible or not an HTMLElement'); + }); + it('should throw for
elements', async({page, server}) => { + await page.setContent('hello
goodbye'); + const br = await page.$('br'); + const error = await br.click().catch(err => err); + expect(error.message).toBe('Node is either not visible or not an HTMLElement'); + }); + }); + + describe('ElementHandle.hover', function() { + it('should work', async({page, server}) => { + 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({page, server}) => { + 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); + } + }); + }); +}; diff --git a/test/emulation.spec.js b/test/emulation.spec.js new file mode 100644 index 0000000000..7a64cfe565 --- /dev/null +++ b/test/emulation.spec.js @@ -0,0 +1,187 @@ +/** + * 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. + */ + +module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + const iPhone = playwright.devices['iPhone 6']; + const iPhoneLandscape = playwright.devices['iPhone 6 landscape']; + + describe('Page.viewport', function() { + it('should get the proper viewport size', async({page, server}) => { + expect(page.viewport()).toEqual({width: 800, height: 600}); + await page.setViewport({width: 123, height: 456}); + expect(page.viewport()).toEqual({width: 123, height: 456}); + }); + it.skip(WEBKIT)('should support mobile emulation', async({page, server}) => { + 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.skip(WEBKIT)('should support touch emulation', async({page, server}) => { + 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 = function(e) { + fulfill('Received touch'); + }; + window.dispatchEvent(new Event('touchstart')); + + fulfill('Did not receive touch'); + + return promise; + } + }); + it.skip(WEBKIT)('should be detectable by Modernizr', async({page, server}) => { + 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.skip(WEBKIT)('should detect touch when applying viewport with touches', async({page, server}) => { + await page.setViewport({ width: 800, height: 600, hasTouch: true }); + await page.addScriptTag({url: server.PREFIX + '/modernizr.js'}); + expect(await page.evaluate(() => Modernizr.touchevents)).toBe(true); + }); + it.skip(FFOX || WEBKIT)('should support landscape emulation', async({page, server}) => { + 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.skip(WEBKIT)('should work', async({page, server}) => { + 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'); + }); + it('should support clicking', async({page, server}) => { + await page.emulate(iPhone); + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await page.evaluate(button => button.style.marginTop = '200px', button); + await button.click(); + expect(await page.evaluate(() => result)).toBe('Clicked'); + }); + }); + + describe('Page.emulateMedia', function() { + it.skip(WEBKIT)('should be an alias for Page.emulateMediaType', async({page, server}) => { + expect(page.emulateMedia).toEqual(page.emulateMediaType); + }); + }); + + describe.skip(WEBKIT)('Page.emulateMediaType', function() { + it('should work', async({page, server}) => { + 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({page, server}) => { + let error = null; + await page.emulateMediaType('bad').catch(e => error = e); + expect(error.message).toBe('Unsupported media type: bad'); + }); + }); + + describe.skip(FFOX || WEBKIT)('Page.emulateMediaFeatures', function() { + it('should work', async({page, server}) => { + 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); + expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').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); + expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').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); + expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches)).toBe(false); + }); + it('should throw in case of bad argument', async({page, server}) => { + let error = null; + await page.emulateMediaFeatures([{ name: 'bad', value: '' }]).catch(e => error = e); + expect(error.message).toBe('Unsupported media feature: bad'); + }); + }); + + describe.skip(FFOX || WEBKIT)('Page.emulateTimezone', function() { + it('should work', async({page, server}) => { + page.evaluate(() => { + globalThis.date = new Date(1479579154987); + }); + await page.emulateTimezone('America/Jamaica'); + expect(await page.evaluate(() => date.toString())).toBe('Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)'); + + await page.emulateTimezone('Pacific/Honolulu'); + expect(await page.evaluate(() => 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(() => date.toString())).toBe('Sat Nov 19 2016 15:12:34 GMT-0300 (Argentina Standard Time)'); + + await page.emulateTimezone('Europe/Berlin'); + expect(await page.evaluate(() => date.toString())).toBe('Sat Nov 19 2016 19:12:34 GMT+0100 (Central European Standard Time)'); + }); + + it('should throw for invalid timezone IDs', async({page, server}) => { + let error = null; + await page.emulateTimezone('Foo/Bar').catch(e => error = e); + expect(error.message).toBe('Invalid timezone ID: Foo/Bar'); + await page.emulateTimezone('Baz/Qux').catch(e => error = e); + expect(error.message).toBe('Invalid timezone ID: Baz/Qux'); + }); + }); + +}; diff --git a/test/evaluation.spec.js b/test/evaluation.spec.js new file mode 100644 index 0000000000..e7e2e38eec --- /dev/null +++ b/test/evaluation.spec.js @@ -0,0 +1,305 @@ +/** + * 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 utils = require('./utils'); + +const bigint = typeof BigInt !== 'undefined'; + +module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Page.evaluate', function() { + it('should work', async({page, server}) => { + const result = await page.evaluate(() => 7 * 3); + expect(result).toBe(21); + }); + (bigint ? it.skip(FFOX || WEBKIT) : xit)('should transfer BigInt', async({page, server}) => { + const result = await page.evaluate(a => a, BigInt(42)); + expect(result).toBe(BigInt(42)); + }); + it('should transfer NaN', async({page, server}) => { + const result = await page.evaluate(a => a, NaN); + expect(Object.is(result, NaN)).toBe(true); + }); + it('should transfer -0', async({page, server}) => { + const result = await page.evaluate(a => a, -0); + expect(Object.is(result, -0)).toBe(true); + }); + it('should transfer Infinity', async({page, server}) => { + const result = await page.evaluate(a => a, Infinity); + expect(Object.is(result, Infinity)).toBe(true); + }); + it('should transfer -Infinity', async({page, server}) => { + const result = await page.evaluate(a => a, -Infinity); + expect(Object.is(result, -Infinity)).toBe(true); + }); + it('should transfer arrays', async({page, server}) => { + 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({page, server}) => { + const result = await page.evaluate(a => Array.isArray(a), [1, 2, 3]); + expect(result).toBe(true); + }); + it('should modify global environment', async({page}) => { + await page.evaluate(() => window.globalVar = 123); + expect(await page.evaluate('globalVar')).toBe(123); + }); + it('should evaluate in the page context', async({page, server}) => { + await page.goto(server.PREFIX + '/global-var.html'); + expect(await page.evaluate('globalVar')).toBe(123); + }); + it.skip(FFOX || WEBKIT)('should return undefined for objects with symbols', async({page, server}) => { + expect(await page.evaluate(() => [Symbol('foo4')])).toBe(undefined); + }); + it('should work with function shorthands', async({page, server}) => { + 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({page, server}) => { + const result = await page.evaluate(a => a['中文字符'], {'中文字符': 42}); + expect(result).toBe(42); + }); + it('should throw when evaluation triggers reload', async({page, server}) => { + let error = null; + await page.evaluate(() => { + location.reload(); + return new Promise(() => {}); + }).catch(e => error = e); + expect(error.message).toContain('Protocol error'); + }); + it('should await promise', async({page, server}) => { + const result = await page.evaluate(() => Promise.resolve(8 * 7)); + expect(result).toBe(56); + }); + it('should work right after framenavigated', async({page, server}) => { + let frameEvaluation = null; + page.on('framenavigated', async frame => { + frameEvaluation = frame.evaluate(() => 6 * 7); + }); + await page.goto(server.EMPTY_PAGE); + expect(await frameEvaluation).toBe(42); + }); + it.skip(WEBKIT)('should work from-inside an exposed function', async({page, server}) => { + // Setup inpage callback, which calls Page.evaluate + await page.exposeFunction('callController', async function(a, b) { + return await page.evaluate((a, b) => a * b, a, b); + }); + const result = await page.evaluate(async function() { + return await callController(9, 3); + }); + expect(result).toBe(27); + }); + it('should reject promise with exception', async({page, server}) => { + let error = null; + await page.evaluate(() => not_existing_object.property).catch(e => error = e); + expect(error).toBeTruthy(); + expect(error.message).toContain('not_existing_object'); + }); + it('should support thrown strings as error messages', async({page, server}) => { + let error = null; + await page.evaluate(() => { throw 'qwerty'; }).catch(e => error = e); + expect(error).toBeTruthy(); + expect(error.message).toContain('qwerty'); + }); + it('should support thrown numbers as error messages', async({page, server}) => { + let error = null; + await page.evaluate(() => { throw 100500; }).catch(e => error = e); + expect(error).toBeTruthy(); + expect(error.message).toContain('100500'); + }); + it('should return complex objects', async({page, server}) => { + const object = {foo: 'bar!'}; + const result = await page.evaluate(a => a, object); + expect(result).not.toBe(object); + expect(result).toEqual(object); + }); + (bigint ? it.skip(FFOX || WEBKIT) : xit)('should return BigInt', async({page, server}) => { + const result = await page.evaluate(() => BigInt(42)); + expect(result).toBe(BigInt(42)); + }); + it('should return NaN', async({page, server}) => { + const result = await page.evaluate(() => NaN); + expect(Object.is(result, NaN)).toBe(true); + }); + it('should return -0', async({page, server}) => { + const result = await page.evaluate(() => -0); + expect(Object.is(result, -0)).toBe(true); + }); + it('should return Infinity', async({page, server}) => { + const result = await page.evaluate(() => Infinity); + expect(Object.is(result, Infinity)).toBe(true); + }); + it('should return -Infinity', async({page, server}) => { + const result = await page.evaluate(() => -Infinity); + expect(Object.is(result, -Infinity)).toBe(true); + }); + it('should accept "undefined" as one of multiple parameters', async({page, server}) => { + 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({page}) => { + expect(await page.evaluate(() => ({a: undefined}))).toEqual({}); + }); + it('should return undefined for non-serializable objects', async({page, server}) => { + expect(await page.evaluate(() => window)).toBe(undefined); + }); + it('should fail for circular object', async({page, server}) => { + const result = await page.evaluate(() => { + const a = {}; + const b = {a}; + a.b = b; + return a; + }); + expect(result).toBe(undefined); + }); + it.skip(FFOX)('should be able to throw a tricky error', async({page, server}) => { + const windowHandle = await page.evaluateHandle(() => window); + const errorText = await windowHandle.jsonValue().catch(e => e.message); + const error = await page.evaluate(errorText => { + throw new Error(errorText); + }, errorText).catch(e => e); + expect(error.message).toContain(errorText); + }); + it('should accept a string', async({page, server}) => { + const result = await page.evaluate('1 + 2'); + expect(result).toBe(3); + }); + it('should accept a string with semi colons', async({page, server}) => { + const result = await page.evaluate('1 + 5;'); + expect(result).toBe(6); + }); + it('should accept a string with comments', async({page, server}) => { + const result = await page.evaluate('2 + 5;\n// do some math!'); + expect(result).toBe(7); + }); + it('should accept element handle as an argument', async({page, server}) => { + await page.setContent('
42
'); + const element = await page.$('section'); + const text = await page.evaluate(e => e.textContent, element); + expect(text).toBe('42'); + }); + it('should throw if underlying element was disposed', async({page, server}) => { + await page.setContent('
39
'); + const element = await page.$('section'); + expect(element).toBeTruthy(); + await element.dispose(); + let error = null; + await page.evaluate(e => e.textContent, element).catch(e => error = e); + expect(error.message).toContain('JSHandle is disposed'); + }); + it('should throw if elementHandles are from other frames', async({page, server}) => { + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + const bodyHandle = await page.frames()[1].$('body'); + let error = null; + await page.evaluate(body => body.innerHTML, bodyHandle).catch(e => error = e); + expect(error).toBeTruthy(); + expect(error.message).toContain('JSHandles can be evaluated only in the context they were created'); + }); + it('should simulate a user gesture', async({page, server}) => { + const result = await page.evaluate(() => { + document.body.appendChild(document.createTextNode('test')); + document.execCommand('selectAll'); + return document.execCommand('copy'); + }); + expect(result).toBe(true); + }); + it('should throw a nice error after a navigation', async({page, server}) => { + const executionContext = await page.mainFrame().executionContext(); + + await Promise.all([ + page.waitForNavigation(), + executionContext.evaluate(() => window.location.reload()) + ]); + const error = await executionContext.evaluate(() => null).catch(e => e); + expect(error.message).toContain('navigation'); + }); + it.skip(FFOX)('should not throw an error when evaluation does a navigation', async({page, server}) => { + await page.goto(server.PREFIX + '/one-style.html'); + const result = await page.evaluate(() => { + window.location = '/empty.html'; + return [42]; + }); + expect(result).toEqual([42]); + }); + // Works in WebKit, but slow + it.skip(FFOX || WEBKIT)('should transfer 100Mb of data from page to node.js', async({page, server}) => { + const a = await page.evaluate(() => 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({page, server}) => { + let error = null; + await page.evaluate(() => new Promise(() => { + throw new Error('Error in promise'); + })).catch(e => error = e); + expect(error.message).toContain('Error in promise'); + }); + }); + + describe.skip(WEBKIT)('Page.evaluateOnNewDocument', function() { + it('should evaluate before anything else on the page', async({page, server}) => { + await page.evaluateOnNewDocument(function(){ + window.injected = 123; + }); + await page.goto(server.PREFIX + '/tamperable.html'); + expect(await page.evaluate(() => window.result)).toBe(123); + }); + it('should work with CSP', async({page, server}) => { + server.setCSP('/empty.html', 'script-src ' + server.PREFIX); + await page.evaluateOnNewDocument(function(){ + window.injected = 123; + }); + await page.goto(server.PREFIX + '/empty.html'); + expect(await page.evaluate(() => window.injected)).toBe(123); + + // Make sure CSP works. + await page.addScriptTag({content: 'window.e = 10;'}).catch(e => void e); + expect(await page.evaluate(() => window.e)).toBe(undefined); + }); + }); + + describe('Frame.evaluate', function() { + it('should have different execution contexts', async({page, server}) => { + 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(() => window.FOO = 'foo'); + await page.frames()[1].evaluate(() => window.FOO = 'bar'); + expect(await page.frames()[0].evaluate(() => window.FOO)).toBe('foo'); + expect(await page.frames()[1].evaluate(() => window.FOO)).toBe('bar'); + }); + it('should have correct execution contexts', async({page, server}) => { + 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({page, server}) => { + 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.js b/test/fixtures.spec.js new file mode 100644 index 0000000000..17230a3f20 --- /dev/null +++ b/test/fixtures.spec.js @@ -0,0 +1,79 @@ +/** + * 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. + */ + +const path = require('path'); + +module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, playwright, playwrightPath, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe.skip(WEBKIT)('Fixtures', function() { + it.skip(FFOX)('dumpio option should work with pipe option ', async({server}) => { + 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'), playwrightPath, 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({server}) => { + let dumpioData = ''; + const {spawn} = require('child_process'); + const options = Object.assign({}, defaultBrowserOptions, {dumpio: true}); + const res = spawn('node', + [path.join(__dirname, 'fixtures', 'dumpio.js'), playwrightPath, JSON.stringify(options)]); + if (CHROME || WEBKIT) + res.stderr.on('data', data => dumpioData += data.toString('utf8')); + else + res.stdout.on('data', data => dumpioData += data.toString('utf8')); + await new Promise(resolve => res.on('close', resolve)); + + if (CHROME || WEBKIT) + expect(dumpioData).toContain('DevTools listening on ws://'); + else + expect(dumpioData).toContain('Juggler listening on ws://'); + }); + it('should close the browser when the node process closes', async({ server }) => { + 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'), playwrightPath, 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 playwright.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 0000000000..d80de2dce3 --- /dev/null +++ b/test/fixtures/closeme.js @@ -0,0 +1,5 @@ +(async() => { + const [, , playwrightRoot, options] = process.argv; + const browser = await require(playwrightRoot).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 0000000000..b20fd6b8ff --- /dev/null +++ b/test/fixtures/dumpio.js @@ -0,0 +1,8 @@ +(async() => { + const [, , playwrightRoot, options] = process.argv; + const browser = await require(playwrightRoot).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.js b/test/frame.spec.js new file mode 100644 index 0000000000..e2d9be5f95 --- /dev/null +++ b/test/frame.spec.js @@ -0,0 +1,211 @@ +/** + * 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 utils = require('./utils'); + +module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Frame.executionContext', function() { + it('should work', async({page, server}) => { + 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(() => window.a = 1), + context2.evaluate(() => window.a = 2) + ]); + const [a1, a2] = await Promise.all([ + context1.evaluate(() => window.a), + context2.evaluate(() => window.a) + ]); + expect(a1).toBe(1); + expect(a2).toBe(2); + }); + }); + + describe('Frame.evaluateHandle', function() { + it('should work', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const mainFrame = page.mainFrame(); + const windowHandle = await mainFrame.evaluateHandle(() => window); + expect(windowHandle).toBeTruthy(); + }); + }); + + describe('Frame.evaluate', function() { + it.skip(FFOX)('should throw for detached frames', async({page, server}) => { + const frame1 = await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await utils.detachFrame(page, 'frame1'); + let error = null; + await frame1.evaluate(() => 7 * 8).catch(e => error = e); + expect(error.message).toContain('Execution Context is not available in detached frame'); + }); + }); + + describe('Frame Management', function() { + it.skip(WEBKIT)('should handle nested frames', async({page, server}) => { + 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)' + ]); + }); + it('should send events when frames are manipulated dynamically', async({page, server}) => { + 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); + }); + it.skip(WEBKIT)('should send "framenavigated" when navigating on anchor URLs', async({page, server}) => { + 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({page, server}) => { + 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({page, server}) => { + let hasEvents = false; + page.on('frameattached', frame => hasEvents = true); + page.on('framedetached', frame => hasEvents = true); + await page.goto(server.EMPTY_PAGE); + expect(hasEvents).toBe(false); + }); + it('should detach child frames on navigation', async({page, server}) => { + 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); + }); + it('should support framesets', async({page, server}) => { + 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); + }); + it('should report frame from-inside shadow DOM', async({page, server}) => { + await page.goto(server.PREFIX + '/shadow.html'); + await page.evaluate(async url => { + 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); + }); + it('should report frame.name()', async({page, server}) => { + await utils.attachFrame(page, 'theFrameId', server.EMPTY_PAGE); + await page.evaluate(url => { + 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'); + }); + it('should report frame.parent()', async({page, server}) => { + 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()); + }); + it('should report different frame instance when frame re-attaches', async({page, server}) => { + const frame1 = await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await page.evaluate(() => { + window.frame = document.querySelector('#frame1'); + window.frame.remove(); + }); + expect(frame1.isDetached()).toBe(true); + const [frame2] = await Promise.all([ + utils.waitEvent(page, 'frameattached'), + page.evaluate(() => document.body.appendChild(window.frame)), + ]); + expect(frame2.isDetached()).toBe(false); + expect(frame1).not.toBe(frame2); + }); + }); +}; diff --git a/test/golden-chromium/csscoverage-involved.txt b/test/golden-chromium/csscoverage-involved.txt new file mode 100644 index 0000000000..9b851d0bd3 --- /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..31a0935cda7dee10bb848bf981f26efe041edb1f GIT binary patch literal 326 zcmeAS@N?(olHy`uVBq!ia0vp^DImBI42E5#xo|41ePP)|E 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 zcmeAS@N?(olHy`uVBq!ia0vp^DImYvN4 fzW~T20yHdVoU??1aarlqK9FipS3j3^P64pCDOIKH5=01Lltqk1N(!ldMxTLG=%91Z0tOs89O(;;-%;TcYIGc$?0u}Y 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..31a0935cda7dee10bb848bf981f26efe041edb1f GIT binary patch literal 326 zcmeAS@N?(olHy`uVBq!ia0vp^DImBI42E5#xo|41ePP)|E literal 0 HcmV?d00001 diff --git a/test/golden-firefox/screenshot-sanity.png b/test/golden-firefox/screenshot-sanity.png new file mode 100644 index 0000000000000000000000000000000000000000..07890a04b342a9442de1c54927955f2613640695 GIT binary patch literal 26146 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 siv2j|fBXqN|ICKzN 0 ? { diff: PNG.sync.write(diff) } : null; +} + +/** + * @param {?Object} actual + * @param {!Buffer} expectedBuffer + * @return {?{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 + * @return {!{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 + * @return {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/golden-webkit/grid-cell-0.png b/test/golden-webkit/grid-cell-0.png new file mode 100644 index 0000000000000000000000000000000000000000..5ae546557b8a1e04107349a7345971d062f655a8 GIT binary patch literal 478 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-mSQK*5Dp-y;YjIVU|^i;>EalY z(fD?jVg^&7M8kgRZINLzYdE4@Bsh#EQVeFeMr_dGUv~Z2sh%X02%bkdaaRpkUZ1*D zGGm?ek!6W&lhtz)rzIac^zQifkM(c&HvhgSUS}bfUb_1(|35~>hSwD}_n-d|-Fvd7 zxmI3D!{mqw7W72@*JjD%KW)OS&d$vJ{O6zEQl=Ispirw*V}^;>^2>=kVI()nYY0Y6z`cO=vv&^&0+K8(+iG<&xa4s zJ%4$c=oTS?ITM$^ys|w`N6bY@P;;)2htbRz#>*nmlp7Xx0`I+79qpPoLPCsoj+m|6~tys9ax{#HF2FrtokDA&)M!foZ q|HAtC%NGQ&BaVaD|NGU?_4k^ZV(oR})qG%tF?hQAxvXS7s=T7CPfo`sKJkq4m~-5 z$dmTWwkuW2uRgeSNvF|tq+#%%MM20jkZG@J=%M@Q_5P=6mX(EV)&fMt)#WAVg+RU) z^5sI3Zj!n-T%qumBf|>7`v!hlgSW1o5J>^<2o|xzgy`kA3XfM8N%^3xrFU(b@F%la zTE`4j@`9Qir3>t}@CHhqA@}7~3NVfTVat$p!)SoCkOLbABcGiXr3grZ;i~-^j&WJq z+eu5Kx>52uWVmV^$7G_WekJrLdxcTr#Z=7!l5Lkn9)i#|7fc_m2hjI*>JU65Zqbmg zboTk?=H{Q2LBWxc@-~kYcd^Da5kkF?y-a8kKIu+4t(m)s`j>%==~VfhZSM!M=S);F zX=y(vFHhvT>)%l#?ek(GOJ6@U94%m7n;+1$g4AN|gjyB*d{t*>=cAyzuPbjFt{tGk z;(jhwUai-`MmMT&B8~s*IEnJ=N6j`9m?c5h-biCxgP!z9OyZ%zNadYP0h=GO%(5#q z#@@V%{=90@=aAFF38CY+x3~3ApUx66VXr5cNee?y=5{mD+*;pk5)ApSqc)j}@($~+ zzR`DzDm3gjnG($oH>PdIiCUf}abf`zGqWcI6@QlBXG)%M3J&{aXON05t}4W6@iqnC zBV4Y;LqP^2;o^^sAMAu{(JTU|!v$qiX!l_dfS>%svDYBgZ<+P@MWVJis@9nCv&*4v zlQ?;xJ#jXFx%aA1u`WBT4@Ds4Y;(t2KW(i}1Gi)jt3>`@&n<;{-#|`(8E5cXqM<4c z?S6BYBSx)D;ImPQHC4nnFH5%HcyKna&Y&tRuY;n;>g~2ve#E?7?EOuM$Lq$N%@bJ`48>D4FnRvlj<`Q{B{1sOXVQRSVt^6PsIGA3uMd$gv9x4Ah{xQfLGPf7aqeg3@XV z99S9Q`&`@pHalhGjmQ7Wog3p(j9fPS5@Xzos4R};aWs3=`%jj~(@}w6zf*2I1dTq! z@fZ<`;r&%4SBkl&O2MaBw-5S-H{ogzLybfmOJp+HRD({ZAG{|rwlsFzDZb?IMZ91h z-+Cd88IP51tZhC-B9Q=~wD)dTuHy6tZY;W-C7;`+o$h&W;uTUx^UbaQk+Q!5l!XQt zNoR`9Qvvt0tbJi7N2Ga&LQ5eN!^s_LFYq8z>L>Mo#Yki#n zJ4$jG{P2OCl_eE3`2vC^!#gQXTng!1ij|m?=V?)O4?Siq?%xZ&9QBWoCRIN_Ka=B@e$uRBixsUc`ObG$ZfsjDqPh;pDR}zD{3hpd%4hqEQ?y@eMTqVm%bY-=;A4n{Bp zh!fvsLVTc?ZX?N~^*3`jaak*@Wz1{iC?8)BViS|P7SYPYnktHZtTFPBvc{(j|6y8R z%wgS`d8gvgVnX&pE&P4s_jFsb_9=10Q}pW^LLn)_)wRypl@zP_#l@l2G2hv5e5#=x zc_)7<;rm)tQgU(#E425yRrN!3itw2GR=U_u{ypnP2D>MrE%oJdc>@Jvh{G}fye%}v5A3uK1uE@2LI4mx4 z3z!b940Z!tot^}$4weR{ffeHMGPgFdvMWJFaH5ip#TQFv{tZn;v~U0b literal 0 HcmV?d00001 diff --git a/test/golden-webkit/screenshot-element-bounding-box.png b/test/golden-webkit/screenshot-element-bounding-box.png new file mode 100644 index 0000000000000000000000000000000000000000..5b07911c669e5de0940151e9d4197df494f6f792 GIT binary patch literal 477 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-mSQK*5Dp-y;YjIVU|^i$>EalY z(fIbpes88knd2Y7w{|zPBwh%TA^kExDZOb9u?l9HCw}P22seW-yC5teYRV;%b%%7pswVA>X0mEn2BejSR~& zt!%HKE$q1c_So~!8x3u57TXDRTG(El{CL8P9~^7FexCjrwDQT95dZp5*P?FtC)_q$ z8RB)bt>{FOlIPYa)!An^*;uW~IllDGxrTaOzrAtl^UoJ=5ec(g%9q6A%plV3dTqH{ z$k~!$Cduf?qmMs+sHN%&MR;@dlWaz+qWP{FXW&4e$UJARgg+X?Lg5HlmA%EW2GCUKFzVYkl*OkEdV(@hJ Kb6Mw<&;$UM1kPvx literal 0 HcmV?d00001 diff --git a/test/golden-webkit/screenshot-element-fractional-offset.png b/test/golden-webkit/screenshot-element-fractional-offset.png new file mode 100644 index 0000000000000000000000000000000000000000..c9e0eb15e92af1e7a0fd1657cb99af102753a585 GIT binary patch literal 135 zcmeAS@N?(olHy`uVBq!ia0vp^ra&yl!3HD~?p=8Sq*#ibJVQ8upoSx*1IV}Yba4!c zXnlL*ATNUf&mo6Pf6M)ruh3Gy&+_B6%1x21?pvR$4|2N&O1LJ9v<8YGF?@d6b;Yz8 Wu&A!t`?L&b5`(9!pUXO@geCxgyD3iq literal 0 HcmV?d00001 diff --git a/test/golden-webkit/screenshot-element-fractional.png b/test/golden-webkit/screenshot-element-fractional.png new file mode 100644 index 0000000000000000000000000000000000000000..390ef19d2e2192cfd5cfc26a91a8992656c80f57 GIT binary patch literal 136 zcmeAS@N?(olHy`uVBq!ia0vp^#y~8_!3HFA4=h;^q*#ibJVQ8upoSx*1IV}cba4!c zXnlL*ATNUf&mo6Pf6M)*uL$A(sdgYUeU7wjZ%m{|3)uRkfU7q#me-was~zs30;PU|8Ms#7iM6{OWnf2pybBQ zppZ1m7!8EclrWkLMsvYvE*Q-Pqq$%-7mVfty5xdSXWq=~V_?{Ek=?F@XXVqP;yJ)p OErX}4pUXO@geCyMcp4r6 literal 0 HcmV?d00001 diff --git a/test/golden-webkit/screenshot-element-padding-border.png b/test/golden-webkit/screenshot-element-padding-border.png new file mode 100644 index 0000000000000000000000000000000000000000..7260617ec272c551cfdf90f62a184ab02fce7c7f GIT binary patch literal 184 zcmeAS@N?(olHy`uVBq!ia0vp^W+2SL1|)l2v+e>ZmSQK*5Dp-y;YjHK^7B1i978f1 z-`+ChYA_IBIOtMSQMa!30k=rx%#}g+CTZvQ70Ntr-?pB;;&k2qYkvxQ*wZmSQK*5Dp-y;YjHK^7B1i978f1 z-`+ChYA_IBIOtMSQMa!30k=rx%#}g+CTZvQ70Ntr-?pB;;&k2qYkvxQ*w&hYkTjItLIC5Rfux5Kux|dWKR^P-4UYq(ecJlQ@4lNNq3G7_t)PotD%pivJLV^t{zQos})Q$IaHnl%H%t>A6en4>b#l;vQ zod`)gj=~FTQ~NtH=0@ow$EPtaF-CK(M6TVVHQmeQbLCUgW-)sswEG>Ci7!1DdRRmW zSc1r=ejGvpLw#>o8N?!s3n8eH&7`s=dKi{wT-hR52v@G~LWK^)EWt+WVc01@dYaM! zMF?Ihi(8bgth?MFVQXzd_S+$Y%Dqr`uI)1wE0QS?urzvz^bmrhlO#XY1fSvz5xaq{ zLAW-_l;D*?9ybB(H3`uz1c6ul9(gYCDiMGu1>RDR^WS)D@*v(}WU*MOit;lqs(Lfl zx|j6gh~-p<)J?gPN!e>}R%zf--pS`H1r;`$2_}=6>t5RBc>6wF6Ts*_Oh&nwd+cDG<8?KO|26Ynh<`76AZAg*LlkePwg8mTS)is)xU^2F95H3-m_ z3PIiN3YP7wrd5@)Rhx>a!XoqLFj_DoYF`l3zmAIw&C9zkA|_^GFJJjCkj>$P=so_^+nSxYB29b0iJq-Z~AdPVK*>e?{X z6slut8e!*t&t7FYe`rV_3OL3W4IZr}gFjj^MVm%OfSvt(t#b?BQcC*Q8deDjnsZN~n>TOT z*xM^15K=kD-i*%;2<=-6hUo3~((>}iaBy(+OPyJ^wzpX*UTPZ|gD-TVG%_q)7V z(PQd!xVSDJBVv&Jv%43y@@bX_vK5=Y+{0e3mDU|$b_&7~K}6Qn@>gx1_pjR?dZF#k zL(B(C)Zh+Sb|=C^443H!Tdw}R#!Yo`WxR%I6Q>%2{LiE`VnqmljvK2qhTP!eDl}dB zW?alRlvP$ns~*jN6U|#N6!F8-4t)agyuWDOop^J?vIYktg2~W?UcP1_`N5r^;o4ep zGd+KhDto zK!*J4)vGqPD}G!*>Y>`IUxq`xxw6F9m>#5dvQrW!hIjvB~nLo=~wrLhlgwDw`uLtl*28q z-tLYs_qhb+xlAiJBb1f#9LLIk+C`7p<)(0jGq&#!kBrdsy#G?dpEGz@PcO~Q`!Xvl zE4WQ;<|z0pJFap$4if`|HU+wokI(T1t4Xxl&q3^=&H3aloVRY>8mdByN5J6UJ>J3W z4r|PCpx)kIXMZV*O1X=IlT8=O-Sxl*9C2~+ci(H88?TrJ`^TR*`o7GuNErGS_QS6aCR4Y((T>pgP=)>zm?DxF5xM;7k z7#R{~zu^-k;dd+n9<>G!hOY;m8jY=yY>^aFuo9=ewHrcJi$6Z8Ly&9|Dl3aG%eai< z_U+p-*Uithw`Ha2X68L?qv(j2q(r09`=>|ik%@^B3*yF3H0S~Z<}neY6cw#NP#$B` z_=XFe(vk3X`KgqXtrMs1tTNleyXl( z-p?xJMvj;KX!4D~JfDLn#~X_(s;G;~nm!^+%c3f5HKjgjAJ#qi!9%)~J3CadF%R%-~T&6Ln&~g@}mA zgQMNmRR^vDJXneQn0+pqOt_!&<@wQxj*ZX3M~?YN0ES- zxT!z$j=QJw6Ia*Bn3&clLd!lotEAi&8{r4xnq3VIxPkbjR?;uVz}>a@@$nXR`WP6v zPedyel)t+}S#tsM*&Fk;S6a$tQFJJx2DdXh^8NegZ_ldR^yNHOV${cS`psHt zGeF&j~ye0ELv=xz0G`^i&ApvYZ47m_)j|G(T3I@UuU!aLbTmdY8}G zZQuq#QD+aF!WH)FOmFp>WDLH$dgpaQLbzvy-wV_`xmr-R z)jm-;)tq?bnpoxf?HI;SSy;ho*^Wr4ZQcU?%9EI-_sYzR5&yB_rxGa5)*K359`EV5kyGgwGy zsG`0;4aCgM49$~X&X4bqN@2sBm}z?(14oL1OTe)~OWmvL!>n=NH>t#f$jU3$^3}d_ z=+H_-9j&yKjGWQt*biL3#JX$?XFP#18i}m0k3@PwwqZj<;Nv zFOAVga_C8mB$^jPbc07By{oyL7Dr!mFvD3_qt#57ZEamy&lX59S8|S!Tb1JkpL0tc zPm!EwlPt#4^N5=9y-szi;K~u~Ow}(3-#CmWewmf{xGbU zADyN!V)-I;PaD1lSlDSR7-Bl4!aD&Nu0LM= zRGH3Ld8&X95KeRYh%?jUN|m330AhUU_TKau=l)=XcZD`TF^DIImv4 z>KPge+1YV3F58kiHH?`UO16qw%HIh;;pXF0l9z`r^Bad8ANzzTvGxxPD2U9O#|ez^ zqk8rj9Pyl9FRD;|y`Qb{-~n@&9yQBkS+9*G*Yn_Dd{Bn0Q3M|O8HKc!DxGpd-`tpt zb{QVyt(#9D@GRvhWZqTad~2>)Ow%^yQo4JO+Q-Ky&$=6i&nfEAebhNYd>uXg?)WkQ zmb>fYC{d^82fe&0pTB+$RQsfW`LIl)@pw3~M%3syMB2l{Q6OWn41N?_w`^Cf;_50K z#cy&EBk-iT&EUmBq9>p=wQeX;|AW7{x+!%>dnP;S`RzEd$##JnH*K|TG`cVBZ1Tg} z=C9`#$6um^H3O^`_k~w(1_{rHZdlaYh?pU+ST={;0qWY^7G`~%380~FIpJsdvMy6fDvbp(Ww4ZboS{l@;i+jKT05u* zjb3{l7L~WNt9yKU0gMr8?)MnJRWMhPF)*wo-yJCU7_7nBur=FGE-@)yUKCbHCL$^- z$(ni-vE^y=eTDNG1Nc3xNGDAh<00}jW_fcn(+CO(rxzC*5Ec@0 zULLqs@i4DlJ~t;vd9jO6Nl7VYG@42{YO|=MMEow#dpPURO?P)uwY`{i^F`TWEIcn9 z@~Y0ufHf86{sHE{MtZY!O-#anoTQ&6^t}nBF$?Z)1qi)HB1f;lAg#Qd?$f%##!=+E zps@4n}j5;d!YrKQL3U?O8_iv@oXI};tDsTm7r$mNBBgd5mT{FIIaG_ba_llapk zGt1Nk_kDcTyia+s`pvFQKHTI*9o@$V<0+Q+2DSC{g5w$Hr4m!XNBN8QSq>N4F08CH zzRJ>tYF{obb8~YC01T355ql4uB`zo8cz+rN1<3lA*Zj!V&2-kuDV7X0!8;T=dkO`Z z`5iP(9Axm@uKD`-%#v81tc_Jb(1DzTfCXtEXUv-Us%)`FoQ-caVK-j@7^DaU!l~)o za3OO$QmOG#ptyX_AkgwprQ%~IbHKm4|#zQU_>NaJ!h zLI@BfERuT9tCyOYNl3}5;jF5xu5n3tT=A2xdD|O~7)e*o%Brf(lf7{^IGnLJze|{? zA;Vw~S^XF9FVeD9(Y8Onb8Uf~7Jgnqu-2%zX^*Hmjd&JT-%qCXfR zZC%}Z{qcOu;4vYI83#MhH5^K({K1Ik%IX{ACzaF6 zmA}B>MdjKb;LnMA<*6(vDA?NGuBf3TMzU5BvRS?9;Y-`Xf_w5I9|b`QK7Q64K9iI= zd%>Z+GA%EAdwK#^M@rb(+RE0-jCgH~{Z;p>2_scM9zFNGASo%iFj69*nRG_ZMenq< zR+sDZm0_NL^pRwOdm^zvQ5ml#tz9bN2^l428uEMZJ*AsB1*`d6{RyEknCM!^_wV1q z*Of3QFJagAP>7wA)1q%68V4$v^GZue!2wnIlmFSPS+^)@w}%&mgoSnP+zC9`o@cv$ zoeZB8b{-15JS>aPcvvU&D5J*Q$M@vWJ;QJ0%HSsKa%xc#^`l3R^dIy-Lyx&f%*w$2GD_t)d*V`Z-S$;qd6f%pZC z4URm#yn?nr*o!T9^zH0;Ul7sO>&I>EIN?C!zI%-HkqCQC`IU_V!-%AxPgcCFS^Q29 ztSdTi1QY)&?61`!4}p2+ssfht+3CBW<(KWsD?jP*BaD^r-@mMxSn(gIU#{&xp?=>j z{`SSJ0ze%Aes(hw5D;hyA!jT14}41pM~HZ=qdWDboi~Iai7`RCD4?i>L_}CgE2F%< zye5`@ehn+y9wvRbIGD=>&h#Ugjh~e~rm;IXH~=bAL3?+}GJbmuYozLHKzaM)$zz1A zNd~eEaDl+_>f)ex_FLY8b%`9L=Z)c4o|}DIa*JK`0I{OKhgtH*-kWPgSn`W7HR|5I z8^ogcVxnl!G*CCQA09sY>ILcGXx%I1E}=)@PHoj59_e;S6S@lNGTlrZ(qsW{0F|TK zU&yZwWGr(w3ms$Qu;OAdb`Fj@EaFea{}nOFX4w)UTGMu`GftcojIvViZO$J*emKv5 zrv}4BfthS#mVIzh^wD>!ht}2&V`Gdk7)(`5D_A$Paj`FBb!&T7s)v{{!e47Ro_VzF zE$Wkcw8coV105#@<4kY%xvy;J`ro(R#K~iBMNxrQ?Dz?pS|K!C@3b zHRY6`=bAAToA#BINj41Nl!#Z>g6d8Jq_PX6xD9tvi`@jwZ`;VOTnPrM9wZMY`z~=R zNuIEZ{Admc{PMF9K3h{ABOtyY4>?Rhq!OH1ae5iye@(q$`z2;xsV&)lwI6 z@kx{(KjwKTL<@CdTndMaa!_A}Wt5&%vIdkp-4qZI$XEc;+)0Qo~47Rdhy#lFylWIor3 zr2Uh-!{?xwH30c-kwHMQCM|-e13lW%0d)&Ne$5fKp(bQ}@2(fPYLUlCnb|3Lmo1XO zq%zoe#4;Y-kh{YQe`o~%|Bzn&k8Ag!U9b~U-;houtC=PwUlF)5{gNJU&)4fCvA;7) zqylx#PFQ_|Oe-YVOF?5RdLsUV&lP&7Y1CRIiJEm8<^zXU5LY9c3`_clS>KmDnK8Hu&*ee*~e^lQ7z~MYkIN3#MQC@TYqVQRlb^8eTO^Q2V zw)kPxgi9hBYCFiQ%j}uDGNM1akk$7-?EWnve&;I9OGsq9j0mY;594aQo=7HmN?|PK z;+qvybGt-*J^X}3!`4*CiZF44aPz!Y$ovV%{&#tl=q>5jamGPKZ%o4az8QGi$R1z2 zlvy#7U~o<=kt|V$#p+PK=kd(pzTSye2&q~imsUtV>22lNCb^){`vFg+WyZi z?nkmRKVHxZTVdHYy_mhcqa#0%$^i%`sCnt@5N=)|PYLH-#Po1cyksULV2JM*iF^sG zL%{jQTB67(4-yC3#hV1kf$+wAKPwWPnDsqEsknDGPs4rf;*wfql6hQs{ z{{H0M&^-gSagHBKC_w~#=7}WL#piWN`aJnMhjGTdi=&5`_ zYAP$R1y}(b(aWif2&+H8d(oyTc40DmFm2=CQwfWqcfk%_=6!5Cl+=3QZn3;=%X=J1 zBfx=b7##dEF+Cj!NA6zV%`q&x`1I-1&(db+UFvO&?IW?03XtRrG6Uc~9|tWD@C2>E zTd3LK(8M}2cJno*>@%tCK{h*ZD0WoC6+DD~IqDiQ;vmX_%u;zvf}42e=(ztM;=HY$ z#l}{uHjsL6e9}$HJd_G07LgrT&i4wW>1~^K?(L{4U>6&#yV_~@V>z<=Mws>6mPuhs zcx+r;zT2YivHCUZ_#xO_Cjzc39zq@|Ly|>Sl@;o6}`6hv6U~ z{J#0LW5U0H(J#FbmN(-f;q+0Z2@sAu;1Ez}VC1~%rH5?QUGoUI=`ZHycVGv*k17rh z*Pw}&BA%ccx`3bS=`1WPbrXy#Do4Sls2O65AWoEGRV3{B^XFtQl_Z?22~vZ4C7vvr zgwYD)0ma{=&7x0!eY{2v&^*>eraJv32^Wi!Dll@|+S9<0fzEmD6Q1u zCJK;>m1~^}fW$bd6sXA$rR~>0gGH@bw(sYu5X(Mt(WR8y-5P~T-|p%)!t^`5H%)?|Ho*T(6P8X{w449{PS^or+F>IedUf>`)P zBvq3BP+D3VuA`&le(8=~NBq8&#WBZghs+jB%?*my);txLYW$j-=r1`bIXNVfl3gw` zf!0H55_aY&+rIP^pe`c6so_9o_s#yA4A>i`6$F2Uxb&~|QB_=yjG;DBwlk)QrV2rKbzt?|%&)eJI z9|VX=9(iBZeE1Ha6#CyD5?Xb|6M}p1b9S-^a7>E=msF|NxW*+vf1uxngL!}IAdC#3 zv!KhK4W1YVI|o$dEVCLIsJ7`^Nmy;xGhyNbp5h$M!X)(~3B-N%s`<(omY47k=HS9* zxQT)1F;r+vc@ASbQRAv2+-hPDovU@Lg5h{bXg|Hyy@;W4N)7&WG(?dW-QBHPjr;RI zyA(!x_ar1fd3P^kymWJ0B7VLHQb{fCYTf<4wL0l`9GAVg%t6b3-@%r@o`uh5gG$VM zSeTw`s*YK%B%OC}C_V9s#6ZWuvfz)}RW)X^n*^;xCtyUfchEZyPj+!;EAQVEnSa-lkf6G#>T$7T*R$olr&kY!BF69zZL|dG=;-o%k31@$jKga)lDvWCNOX`; zw~ld1l{2=)AlRih;Rde97(|D7FLtK=kQ^<5)~MFGaiYWSO9>$I-lcUu&LvEYs~PIA z(-Ez87SgSS?jH*rFaE}0mQ>NMY@GIN2hkzs>>K94E6CRo{wT{k=hJmFNvRwia&TjWd)UreF9Ob&nx2lgJ>RVY1SJxf3<2ad zMB>^rSCWlWyI!c`6aD_Z`7qJ6Q{ZR*qZcmF7-o4eVadUabP?kPCOGT85K~ke=kI!t zi`jZk(^g`A;_~s~3qdS#%JW=Dn|51Y^{j%ivcZA1NKQxhUBA7v^EpsWRHD(Vn6M}> z<6M>GNLaMoA&$7?=z<57AWRFcpjMR>`~zO>P-K4AZjdQz)5^~u6dWwA!vFX!F~gKp zjty>TWF%SWE#I{^i%LbQX$;&^DJj@AsF3L zX`OpDkIQ<^O?@YXzJ2@VJUMf_`7qzIC4SCB)No`v*3i){#_Ue}KPgydAJKOHFzigX zJ)Fc_3Ivos>(_RHsw6InUf;;aQ~CE1_nqO1q%mCjPQUaBQRk!UBa5rJD~i( z%vuFYLO{V(Qc;m+{@~8}7+LsvRykNQxh&8fvvjZ7FnAZ&09cBHUumzAe_Y-CWa#)L z1~o&dvvr@?@)~gKK`r!X!%p+r%c$iD-EKAH6}gk0w`-M8aq%qnP{5E-%Uf5_(a|ENEbHX*_CSF_c~tIclh`024uSOGjzE-@3c(k zvX;qB-5<}K=QU-4#QDl$q!_1%VNCodq3rvPSlA&^)k(nZsNDryGnIVK@v|?p1e@V4 zqh+o*HW4|pKKs|7Sq4k8f4vGn@$}oc;ep?LlC>!4Q4R@xAaZeWA-L36Nku_M7O=H_ zlceqOD^a+NBL2S^Ko}2y`;iMli55;#Z->%vB50wBtGkaRUpg3mjK*%3`?DDbY=(6| zp%^Jtjl+Qwr=Z`Phn4<6&}}4*&rD?0&l+|j$c-8 zW_E2?TH}f?SFn$`WyCMc0&~Fd*4#Mr44jZ<{npAGd%-f#k>l|4s-tn|oRRyt=1d8^ z(PUed`Rck4oVz1F%2094TSk8jaQPlrL@^G#FC~$0&hf!Xv32{|>-pykHPQQ zQV#+a^ebxKiMhEKO`%uPK-JC2xhJ@5BKz+tV2BFmZjtDV&N)CMkY=R5AjWg` zb#n}2#5;BwjyFJJ3}lgF$ks1G8+*A8A8)sg1~hOu6%W`xH0dSCj#DVhwQgS7V4R`Xjj1?XAdoGbcsmp6glP6d-!_0F7hs=-3Q$JA%AU{PZs2 z?f)&Zh4D<@BiWBxC%`5U}AJ{I7ZVwkQjeHr3LbU zYk59VbYzDi#Vz{HbPq~{0Sa(-&TH5*cr3pl??CF`OxIKO*W+>rfJH&59`KNvm^@A* z0$+`h>8s#PryeyAx4r_RGNgN^nQK+p7|_Eq4AT-G2v~#vr|8+z$?MBBWKzZ>Y%%$G z*O8-Am}`Hy<$8B(Yb{VxSKc(u4fbSwx*%BX$0lMoTG1Y#+>juO1*#|PQ& zj94D~4*sSo?=IKxOYt7xhH6V6o;f1lzIl^}+;areWj{Cw`8%->o9_4bDZpQ62*WUD zagMl9YFZlkA`FkEoAeSV`|`lA%kP?f?#5Cl;y^Uf9hJf(e*$ejCWoYzvVyb}XI}kI zk~oQZH4WclDc&_7kG!Q$ya;x+KaS`x`f~YCzRf%F!bLS?6!xfm%~n&}P-J>JYeNr$rr%^N4fq{zszCBdvSUd!dYN~%+iMCWgFACm z#^1ss!((IVpxPKgOPYBT=}%>QnrwxOZC;G;oeQY zD8~GbhbWMu#DM^Q!$M-9(`OkJ-~s0>z}UAg#o%^ZgueZ^V6{*6Z#ZJ5uMrXwO86d% z0L)2#kN){R>Sj`}Cxt5^BB4%uU-fdE0Zbu(GwFlBWu;Q-_^su&4b%Sq{)3Z43~OthFKCQ|%PK2vT!om&|@>Pn1%{lcRiTv_yCM|iPA3$ zp6CQ=Pe@`9sW*e1td-cR5XYQ~?A!Kw2-B9r9N0p^;F4swX+9X@*87 zZe}Roux(2Vi`UW7!7CvbN=w(NrhJnMmfIyfe3=ewlTY>8{t!@75=b$rw-U`sfF-W3 zQ7ZF$r57+QYPfJ?V!6+9-#T}^(09UKyuh;`uV%a>q(HQ0J`#B2B+|AHgY}qqNDmp& z@2uS0qu;&_toA*cYUxWT_1?l$Q&WSWSzglVHOcu3%2BxmmddLpjzE^mw+?0z^W?P5 zP6|QA#L$f0iReo8?_u-0Q~rHnl>^IMG3pmy{${S=;B-OapUOC$_JHRG%10IxfwkIM za?3{g&b2@|@HHqXDL1zU#32anny8?thzlf_>cfio@KR+ADcv*iPxM5)`YtoMtsNao zw{H{Abtj=}YvmbayvU$h%2V`Qd$D&ilfn3j1MiI+H}K46^rCzl_74GV(%df2O}oG2 zY}JyrAQ;7^1S~pWDKZ=G-!0kxwUhGv_nb4mY*(I5`yG^?h@Kv8jRdKCdn(N0;#Xe2 zdI!6hLvLa}jSUcCY#bb#Ub86D*wy$N3QJcXb?pu&I=K6sq&V6&=b)|(++x-%QOI#@ zs`reVo=@^G9$_f7P4<8Cn|^NOz7?lkZi!%Q2*AOmu>)T7P>BoPBuFj?e!Xc`RT#)>E?$a?6o`$F5p~AjIz!;Q-^}*3<4nc#hxz zF^1-kvpBYTgtLq4E%lMlS#^1z@3`cVdzXitkVM_#FIt{%LFpiO>KDi%-tYSw-`S5xbp9q9GBcpv|_Y z?F*lmmExUbKvZQQ$E1CTH&RvGz~JR-=FtYIczN7UE`Yy?Iic;<&sS{pExSzCK00by$j!$Lc0b*Ynaz*1#9CWd%ozE)yFGC z`O4jvfqM*{1{Sx^=wKD;K$Na>IVjGEjtVFD^~sPR~s*Ld037ICQP zsSTzMpULg6OlaHBD`e|OsZU`Y(u~g*75~LXebqHGPa2gyv(!z#&MVdvc(9v+lY;BA zHFK*Y5l#cXEmKuBAO~0?37t6`@|4?yoTLXIH2#^)HnKME3&gQjQ#7Oe+6$8S2$5qM+& zue^CEef@*`aC^6W>roXb^Pj3JNfZ*}dbf?#WtGr;gdVq|mNX@Mw;5u3gjQ#Z$hP)5 z_w3`6F0!CVSx~4Gr{iBY+OqmgPg#O0@re^l*cRQXI5I4v*~0+`e;?pPswWL{nya%d z%rhqg3kPyPlT-|Qs0h4knr4FWQvK8;ar+E%s*bsXDrJ?sozQoPSaM0h&#f1eB1XdJ zS3G_-3l!ml;)nx1F1skhvQ6qd5^42s!R^R!zM((r@j(wx(m#;126NDc;X(&$;foAzHO#j|s{Cd|dJ=Fb;Aeh*? zrMg~qZY?FkUApGWmy5cYg(HV*Y)8L-{G1l@QdoDKS1Cs-01FaeZO}#0+TE=JUIH1lf<$3{?f*!Ex6y$+(BbsMHy)@3=ciyJ`zVxJ7V;ndaD0m8-2!=qqn$%fTa z58v~ua`Qc4%h54`%U;luh;TuliVJD$>xTpcKzzofWFSTZjKj~_*-Z_16dbDUD~NYS zhr~4!YaajPIgf{DG4c!a1lrb9g}}Ki$I9LH@8x%z3ff1}AI1KHcOt(qN2)TsPM?2b zX(oz5|9rLmryYuB^SBQR_%bt3wk}PL++C^T;i6k8%fG7JUE!`q2&}=AqrC=kKPf1= zYn&hO#33zub!XPT?+J+%HLIbRugcOBW^a$GTfJMW)oMF}Rz zu0(j~_<31Eh(^Zi`sHheGa+7=+rxc(XAYXK$gUN0&cLyG%-X^D`Qj*gUNLV!tcjYu zWJnJ&PyAGp*{H>Rt@cS%w8zzQS|icyPkQqjCIf=jIXe_n$Gtv&{3t(caU?w8;r;zY zVUrLbjJVM>ZSL3c@g4f=w{F#yx$Yv%>+WB9W!JF_K`)h_m6l5I>F3qrWM+A$lzxAR zIz2g3WF}j9aR8^}(p*`!xnHJjZ2PSxK%2=PU1=cRv7U`1k3=GK=L`CuPqAGd%fVXv zmzsn^x2{rMJdckr&8*XH%pDcG`qw!fyN)05I*bYs1&oJGs0p{M4QBOG+YB~|WuGaB zR>ywsKD^)$!4)=x(TYC4>(u;h`1#ZcC*ZdCNTH!p7dA^c8yjwk{(~4N#Ne??+avb>eLvh*{@(CvzR%G@ogWq)8GYE^!(cE1e(6%*BB`WlyjC ziEE1FmK9v<70?}F`(cAZ;%WAc+MQuNQSwfnmwtVP@YIgoo{|R&B7fXeq+r4<#YtlC zZb;E*6^}+4Q1|{CrGgdXIus|IcF4vLD_CN}e)!yG`{nJq3b#cHD4=_ujhFWluU?K4 z@cuY8VkSO+=HxhhSqL{3`StJfo07c(v?nkXwVg}-yD@f~tB){CKWU`gxD|xLY8d8i zUi#q-!vx}11lgs|{f%-4RgV9Sat`VG|9EN}uD(N`yBcXz7>t||f#h%9BDC}ckldr) zyiY#Y*dz?dc$H_*=N99TdopWs%xeHWL-DTAIr-To_%JM%UPX^|8oyC8?fXyZBDhFy zw;SfVZ=;`K&m}+x>(g(?ZER2_nReT1&MvKj-7`LyBWV2bn}(NaCV^ptO``&g%X2^9 z-&lEeDSH@ZjkB<~vCs=*!8xa2#m2p%!-H};DLN4wvM2$w2$Y|VqVu7T(mbDnZ`E#Q z^OawW?C-ydKStV+JYHTh-`6RTXAUqPdpWpwSz-7I4rqT=c=U+l)U|n6MYs5~an+5H zX<_IP?i23U?cZ&Ny~|jK^-V9u`>*3ac1sE5YgG26XM&@yfg~L^ls2<8;suOW2#Q>5 zV7M|A^8IJoFiZ^`q1k_Ef zdez|-^QYM(d#%E3I8ePz&|n9rnR$04G!5JR)vDo2MvvKgcbRva*_W|rbb>ffycUR z&v;kCyi1X5x+N#S8`R8rdSvc7tbBG+SzPK3isR~D!srKA%oAAy{}MN;+w%+VBskr$ zIId+3;`oARx8SwvbpNqgE21L=2WqaqwQSs07#keSHS>%u@dd$q7Er>h z%T=yfo}iKa?ccv&_j%#UNCvhfIIiv=dXhzbXdJpiMHN(q+>>HWH%Ldu=OdVmH0JI4 zvrjg3gdImqpOSZLxw|Qa#;off=B9swqDTKd+j-52Kd;mx15q7N(K^>bW7RU z+1Q|{<+xMQK*yW)>LU{Snm3N+_}iKTmzGbTT^TsJxrb@;^)CQCcagl@_=I@+u6DBb z5oltwEiqpM!6e_KUBLm5wb7S2xOhVpb^J3+9?dU*HMqYhGd^(wKLtv5u#KI7Tri8? z*~lW;nr#su17C7G;%C|?GcYg!vIXn(5sFcIj=kjTIt`ZBf%1)}e~O zY1t#%+1Xhd!TNo5&Io2bPa(|?Scm6G*gv}EOk|TKpM6-h6q#8%yAJ*~_8(5IP`ber zj05&m4hY6TVCr`Z-I!y)+v;F$lI1yi7QOLD;FMw-j9ZrjV@t%Y|Dm^5$APg~=7XeB zw6ezAf)}(J-Tj#glvrrxPQW{;@8AYo1jnU8h|FHT7=TgDWQRb69Ac z17mGI+{7HYt@C?{J23hkE^>`|W84#9to-fEO1 zG>Y40i`{0C=_B4ir~`9#2;(mM?%G%=y||-1-{nA1U{f{(#&!z;EGl%IfAsKSGO$(h z8^Dm-KFQQUE-jJUJ{cd%w`3&GXemc1xl22N4!qX3wmOhzij0bS z0V*}KpddNW$1LB*FW(`kKs0(tJAHq574+)7sHl*Zk&yxEViG_>`!cjfX1{?{E|X8s zMhodVdF^4Mneno~-@#n5P8@Y=-X2P`&F;SY-qGtXyD8*jM$cN)#~dA6vf$oZB9M6%4yV@ope z&Yw?SsNAkxKY-nV;{_c~|JVM!qT=#?olQQgwcRJ#OKea&X{;+=IzlP?R2YwhA*8?m zZtFuKXfKC!9w~40Ma6VA7{KM6BaO=-fsm$xnyQ<~2=_yY{$@|eS0+c1HlcU7ZB-L3 z=8xDM0nx+75*`by3|G6z$7>FN0B)0~e*$DsE+}zo4W(pqD|F{P*k$x2@-we(|h8;swOX)4Kh&ovS!aJBi;!i-(__)g2f=9(%9UrfX?)|s}x%3XY;XToCi^ex|Us} zytTTV=rl5QfE5fOW>HaUfn4;`=NPM^0ouh)O+6Ijhg%byUi(RB=CU3YAUto?oftlO zLzp3IFlInl8L?tz$|Pr!sbXct2?9mfo*A$@MY4fyM1Q3NGgY&o7ZLj%Nyb2G$0DTUR@4lP{uYMCa z&rt~GxUf0U2sq#SUf{_)X@E=rOGbC%q`*uBENa6qFup%nvNH>-#4lCQ5_; zN_fq(P^E&QG9?yPQ!L8zuZv=s);zgD`0M$fv*ewfARgEms+lCFx_p@x1d}ga;a38U zgi5UBkL~O>H=fqhDF~>x!kkbwY)28Sn(;Bi z$iC}A^k5G)cIAsRCgdJ$@Zs1Q>9PCRhs+Y*rLT&@Ma;w;O%|{5yoj_bC23woDHD_{ z+^p-T-wHX_xUjjKWeeNL33Jg_B3F<(XJr&6tfS7Ycxm3jtJbL3aqq|yU(90_8XOYc z^^#*Eg`rCK?U7ZZ6ICV^IkVOtouE^Er}%b)6m^DHQnB*+uX!KwA<&o-6dcSZCN_YV z8QW!y+Fcz{HMe>Dg3wZ&93XYIgoM~hp z7&oe@%3R*xD%IsOW3mo8Mcr6Ybz1en8k!Q4l9SPFzhVRtWR0hZ+$Vd6RVG_Lh~uxC zD7^$03ZW3j)$FmZx$;5?urI|YPrhYa#9~SfGlGMKl}u3M7hKP2sGn@mT$Fpv?VE9KUNFWv95SY8gWMk(65Yb6m4v2VUy$uLJi@?Zm(B@wMZ&A z>MzmAT&=q=*!itcMSPtMyRHVdTj4WrAZR$nRzinj-xb@ukHG~hWDqkiulo8o&}aC~ zkfaN@3b+jVc@J6998XP=e^bq9 z`RM7TX)Wo1IvagDUg#CnF}S!Xi$obT__DlL%Lg5iGSlrRwM2G3yQO~keoT#Q>^d1L zh32S7$V*s3K8#+RM`@{Rgu$u9wBDRTm8;c}N07~zi~WZh zR`3Tx+y45&TOXytv{fB%d_T%*av{Zx7L5k~`LmO)Yaey50(;Sc@h7_jDrv3ONkK-z z!X3dbveRk|C#9yQ2J15fu8?xbo*lbF@~?x11|NCB3J(J+?u|E3%Jfu(!NT+2QB^st`uLF1(R~?fN|qE|T?dVUronTnUb61A zl62)$S~fy(aY~xb z+?5XF^>gDi&yCTnoQN(wFG%^|FmL`6$$*^1&e*}?Z!6Yl2}J(Ph3PR14#(q?z~lr0 zC`Xc@zJ)1ps(thrXVH~Nz|I#IY%0(4mkMu`kW=d1zmErm#UzyPqrMLNaw3}WJW_C~ zy?HJ5;YT2J83LM9G!kWK0S#$Pgd-t&B)iO&c>KeJG!+*P@xj!h@WHWCmj*}8qjJ|N z?wlEefriAftJ1G{@%fg4*Ky81_Md0D%RFq0$Ii1nstgNAx1dY#2$U-du~9e8tN#htw~R5b*4RHNCf- z+b%0DO|~1%5mW7#@${2<#mjZ=%(JVzy9ERgjQF*M$I6_k^uBlQeg5)A0q}>NV)$&T zl;;s})2_YAzPfhoMy01|9psDKr|Is6g%GgBG zAyO(CGdl>O6s3g7JY~o{9Yhq#a8gKR$dGxSXGt9MJPUEm^E?mtz3X}Y|KIPf-&*(H zb=UpgwLA}wefHjGxA*7sdA(o5`@4zFyA_F{EQ90G(b4326Jujh5Gd8R0Q%zOJ@+F6 z_4czpfu=BPZ5VPdo-<0xrCC^iRY2R(fQ+WHmTEA zti{P4J9iXSN>4O2HC4~IPne(WW|L=BS>Jcoe^v&psm{z}aGw@APC}lOuilN%+Y1_y z{`HP}$MHbx<2Q_khKA)_A73fon(`RR-F~hw*H}hgwGSEWqWu(CyJEk6s{mWdRUtvD z)r17LUtnO+87PifU(|VIU~uy%7YulBo(x&O_^Qe-0}B)Ts;Xfa>$G+5Q$tZ4JfD?( z9?HhfB|YbJWvODW3LKB-pGKR`5=3R?jhzeZW#@b<6YA7xY?W~%o!4O2vD}z{ zIvXK!JhF+}wA?zxrNM^yv^j2Y;DNe5oib?_)AZN*z@+Kn^>t}f5|wUY=+5E!2r)0Y zKoK}%$aVEPz&ewU!eVxw?+q3G7@t!7#X_AxY8$9{|Rlm7_CsR6) zT*D6sekj5ps1J?WxwMY2Oa}DvdOuKpf4KD^afMZ2R9;RF(rP3^WgT|6o^m(M_rHXS zMMYKB;q$+?#90;l32XBS7-u?Vd=}G*BxiQr0|zvhw!8ns&+&7^o#%7K(K4O2>~D&a zoF~Jy_6MC8bYKEf71Lwi-&wg`Lp9g&BBR^AmHX233jQ3-5x5+$D-*6ZdrTg-!~OI^ zS9(+7cehrSq=xN(ZB4O7dx|2#ZOvjk4OfLi8yfC@GnFQZzgGDEi|^v1m$(>cfqHi5 z@{h~}q&;~J88;^7Ltx7&Nwwr~__p2dq){ofUZXcTNJdJ+I;2e~(%Ek}1*rK>3cBKtRUp<^2 z`_?gvl>s5=Q&59#iFk4jl!69YXRM3NQ3;|c^W1)VxDV_YO8WHz4rTF(Q(b#(!*wy! zXiuSaHlwM`p$vTDECmd^?JpRSHx{Q@x`o}}SRC#l+>pnWdSTvQBK->02KVWOP8d*H z489vHp!I=5s);i}fzg>T}?*pk`4B4>@XM{ADX zf;S3c=UelGHxd$pTCO3XHUIg~>fU)|+pFrsh3Q&}jt@7hlRl-GCN3jXC=U z?-;!vU)fdUl-!$P%OlM}4qrU`RfAG7`#5V73fsH~S#iA?W<1iRbZ@ZSHLCbQmYHnn zIr=3>#g2Uw?+yIhTIxb>_^RmvY}+I~_gS&MieJI_6GieRsnx5*t;ys(VtjiAMOY!K zXqB_)SWQ1u9DF7ZBqa_c_4I4*aenl)PI5YHY17tIbtkxl>1Xxrd;J;Dd8E^MsY{!f z^x~IxD%6mo6 zBz|g($Wx&ToXbzO8*S;jzj-;75pR`Zn+oH_$1)wBZ6CVTliWQfvG_#Z{v1=o%ZGm} zxE6LkFZbt5yI50y794_*OqYCD=~&f!XS&m<*P|3^FLD1Kz$p)&Ms6Qta9f z7-@zV7H~iV61K!DMB^-|Z}iCF_9!sXETT*w>oYBfC6RS@3{FfElHHCeXiRVymVF(T z!H#B$r-4%zg{s@(;hMXZ=Vzkyt~?N}wv7Q;3^<>hque-<{I2@`4vGJbHbSy|;}iVH z2RCQ8)?IqvDPH<;ljCEy=8MJO6_dS{kDvJ;=W3IRZYK=uUD@5;Wjb{%69Nu_>thiU zL-QJHR0k!j!MSn$ki#*Aqt$s0bwL;e$(BEIp0&RV2{{7pwVrP*5*_hwy0G)R`3h7< z1hB3)44YH?&aDLhuHdx8+UhP?rRs` z15w}ezvA0tJU`Z2hCCLy^Yl;|r351V&b#`c;ENM8JrN_Bu%zATctCuIDDhCtM zHpShM;r(r9GrBhZ>^O=`KV!>alU7rzIBt2g(XsGYdpgpUB$6)?xunKgv+lR>5f z8z%Ei!^vpFRD$m%txnF~P%%B~xVIRqEZgh*)pX4&Hl_c%Qc=LASEbT>8|5zk{1oCQhY@3hC6DMH&bM4&Z`$vsqt>j?`?g%;_;v!A zTv6`)=Hw23*G?T1y5A?2c`zGpE=Hy5eKyF!o3)d&R+Y_A0IJyBbUeq(*Jz9@ zoW+bc(;p2y=LuT2)rFy<>SzAEeJr=jWmk*QlLN&9nr=2%r)OrI?wE$PqWvlDMcnp9 zQ4&nlOjoWP1r`%3j;|p+i7}IcZZ1ONWTX;#CF z3$I3vS&?EX+zz`Nb+=L9p}>#onLu^0o;r3RSxL7#I{Y0&*Ob-T#i7>E(rl+B*RxOX zjxW551u)Gxi!r>lC3!Vi7g%V=IMNMb-jrLgiTwkgvpymhuCz==am{9$nO{(d5cPqp z41=oP!AD=1eH4%##lVhcU+I@C;KY?N%h@4cgfV3`VcszL zKnT|`>8BHrX(@#2B(htVWIHtycGm}II4d2YfO`^-j*5zsyLYcDY>VZBNd9pG%RuTj ze*pLmEi?9SK3ZtHLyDSf4Q^h4ff!L}0X|2VZ-_FIV;fo)TA$FZzTq2WVQxY4dJv_> z5z_uX2^Dj2$pnPSTVuh0n zK|{t2BD!eUnL(k_i|$E-4HwF(9J{;0b3Z<}6yWtk&<7U7Vh}zPosd8V;V#u6Da?e_ zHB?JKW=JE9w8+=@c#-{_No(J=ERE6yi(dsg0`abV-@GxVcc>n49#K|Su4!znI!Dz0 zL{P0=wJ*kT;h3j+Sy|a*k(yW6!oBkP<^MP|qhYJ)%K;>Xuf7s-byz?Vv4CbC*WkOVAfsjGVjeg z4B42Y^c7cCorBJ;XE>x_6wa-aEnC5e?LKS7id{XY2czkmPUa;J}` z)DDz`Squ11s%?zE_|>(twsz&_+3f7>Om*wQyqEskb5>1@R=EcCXCMOVtcDmTClv%# zn6)B%dY}Ua1v?j=dWYfYB^M~!kR8p$Z5-HTu5AW>^M=a`XBL+~AF)lW)|YGA{o_ib z-i%eqH(7BS&%Ilbld1CZ@}3_)d{DNwL{9?g3FSGmm8k%6&eMe1;q#!@J^}E#CCaEW z@tSTX9bCAc;4|K`#4HnhcdHCP+n=97bP3X?4B@fpKk4l+-Jyk%I z^4v`kEmN(^oiT#Wh#q-Now;zs+%H!M3GzMv615jsbr$Z@ZYL?eV{uSH=M! z+3jofIIf-W$qNlKgaU{`{vqQan>xak#9?rAv;r9wO|BkH9EW6fTYTifmFnIFSoW-% zE(@0#FTeJ(!#9fiE>-q=xwtKSkFfaePQXtJLR)#(ZvJ55eX>+!^)1A!MKKB_N^P`A>gWaSm;Ee3BXy%!0V<<|K@GIzxoLPu2da$B?q9@JiK2p3V=*#jla zZ_MYN!e7Bd31Y!@oq#j7)Uam!vr!LR?HR>3e-oc_(F<2(O%6(OE@{e?<&(!zP+;tL zO^aGtu6cTTW;*iaQ*9@T0(`2-#4oU8y66WP11*xp)+q0ve+uG;=e7%#d&Ez!HYYh3 z*vv+0PX#{TwX)m0Lj>MzieB6uh5Y0PPMQOi{(87C>=-2qXN|%z1e0H8`Gd)O^TVE> zLtW)ea$W_rd`2EO<^=`)?nl9YKv`WXeon+|OH9o$G=02idTBMhK^S-0zdkq#oo+e1 zbXgu3%#Klr=yL}-+uTNV7hfl<9j=VQ`Iq)o=3CaG*;v#mT3X`!d!>Egjl>AidhAZg;;cFhBvr?F zF`AuIQC2Z!NHECFXBBqpRX0pOeL|y!F?oipx`6*sUrWU`W=Ho`P2%t~!WjOU)lgx( ztK)7NopEaS)Xd_%Y3m8o#mP>HK{fa8v*b|2>s;-6Ulnj|N$ik!M)+hD(fT|&F|i`v zccY&@o%^;ScJoG&_9Nt>do}6yO2aB$%Dm2$aG8t#r6I0x7x6(( zB=HXM$xxHMd;i{J!_}x)m?znh7cJvZ5_mJPS3N zwzDA@FGi87(@iU7(3@MY`a3vj5xE}JYIpbckhTOy{gfm0p1d|tywfQdfMf>AsEpSi z`1}je=xKCC;5?G#RN}PK0<=v-q&VaL{(eunoA|FtsyrQC`*CbZ+C0sPIif<1fY--q zf7I3@A7$0h%XTk}`);_ypgC8(ub`4_=>N+4qA#y8n= zCL;(81@kQ8z6IqG2z!6ja&q>vrCNoo8#fV}L)aTPR{jU@`q4;3=1vFp+$BS!XSl7i zyqPcdun1^}_)Ewk@WkP$z%liE4S*|BYi$6Gm$yd4jReELdD))Z|1V&j@Op@H_k7Rh z7hqWhp`w8%q|Lvw)v&9NH^^G1h){Uj2%X+9rR*4uPK->V^RA=$zi^cZ18_C$YN*F= zIR~@;JPOk@E8;C1*{hBZ1_+GP=<=@fFq6na+P_zU>>-pW&PjK&C91$OhT2sam-Eb! zpHvNN3-vzp)q+FrXRqkwz3J3{&5nss*>Eg=0HC!=d;V9U99^VIvg4U+hEu~(%u|TR zhj_|ECZ*+c$CUz6A!<}huBi$TFa3sU_k@15#X#kiB>WGywF+@*PFlIwQLBPL*n-#> z(P9-dQld*gPrj48XwTguuZ9&(ou^4`*F)ewT$8BWZz%W0fd`BJ>)!$0FL@#$Ho4pz zEn-9mxk_RWNxL9mHKCNqq3-`yO+@V_S=qLzUbSJs48{E_A zVmoY(#Sj{51tJZ6M<_eaf!F&{Nc16pv$4-c2c}HdNyF9E zo)m5%N4ZYhg(PSF%vCt#;>rr)f^xgR%0`WO2g~Klq_B`~p|nAT3JMZIjA0keb)Lrq zEV!|K<0j!UzXuZ{Eo@p~Qzbd{axhQKb-Rb>$%j`7&J1l+R&n!!DDBQ9WvuDHkhMQ| zS|Ytj(;Wf~F9->JTAL6f)?F=gu(9!kC&7>;d9(K}f=byH>#A!4cgqjm7>f=>r!{X8 zPSyVfp|?!muUwkPWymZ<-Ma@JXEw|EtF*5VjT)j=Ai1*#0xc*R1honJ_f|@M&?=0{ zM>OT+$c~T*5u9i|TU3ERZc&Li9hdHY#*0gi zgQakxj|XdD%6N6Uv%ZVFQTSOb9%fY(%Dj(mo=%1BR6%TzRhfA* z~|m-$FlRn_EBc{xS9nnJrCgFLRa^$#qRDHS`t zwzei3n;jO1Cka7MG)I!6fN_DWNF9hih5E0=#?J4h(IeLAi*asas#(5LU8h1MW5~C2I zRFu^CixG^j)gFhBTTV@$_uKHrdtaz!yRN*~8eTem9!e8aD#y_(l2O#%26s}zGEc@| z3`P>>!C7ZDResq#v?J%vaW`?$AbgB$7~Q#OgGPd~A)WHcKgjKOFq4sb^^F;iPygbz zf-ROno9DDgWQx?+%o&ow!0<~0u5Bst@QoYsaYqltLf5j+&rMQ&jFTl04?h8e+XI+r zWDAgv3ZN&cKV z=6E+#oqR3lICq4rrQ2hcURu(pJDl}@rgH_`uhF-~bmPXO;`&y!G1iZFso>ZjRx@%T zj@Z35=F`^KDV=$gfIN`JA1tB+UH)RVYNfSjo-qTLkh6iJ>uYg0gZhL^&#=<;Q%ei4 z|0n7am9DBo`NaI|#KeRi44ap_A8FKf?(D{}WHDHE_NT0V9uM zhHRYm(-7c-;K5>vM*XRwYt9G7h8$Wzh8Ee)#O0@^Z~vM@aGHM3rOeTxEkb6!m2BM4 z^Pf(b#+l=xO-zi_GHH7GONBWM5n@ko*rRH zSj3fw)2=;!rQDq{t0S~PoJx+(v0(eD&I@-Wp;bD0z748ii>8CMAu8vsePq7D-RuTT zTAo?22atuHb52f9lfBtS$1j=E+hCyl=$)#*CjK>HbA%S9t%5gJ-QK7hJV^UgKQS`m z0gUUER$Lhass%vVnZ~YDU~jmtn^87-veX>rSxb;@)Xmw5QGr5*h}ducF8djzB#d#c z)*1QfqgrHp1&WpH3lj$ntJQbcjPLoay*riO67e*{euMp2xtrEbwnDhD&YjK6DIW>G zndq|TCyDM#LgqVoK>eV;c*C*!#|BT2 zf_Mfg5I+&TZII{vWM^omYsxLi0FvdkFWZck6v-N$+`y;vuzD~LAX!0o!l2poY$EyU35)kB~v`s^5T-O1QV1&NV&e>Q9SVp$W8;dZn9*J zvs$CUNw`lgsj!UK9FXd$bFS&Y3>uGN6q21wdh48nlASjJ5?t=Yc;zjl@f{eMoMklY zG@1yILH9H|=7|$dD)_`dvetmVj>Qqw-+m30|M6cD5|?%80+=(Ye^8W4BJIo=B+j8W zz5f6lq5D^eP_N))I8CE1@|V$t(`W*mKf4`{tUFWm^0183CkoUX%kz~sC!Svnb|ym6 zq+R&)av>v%dRX}L#Km87SE&x6m^EaV!(N@cZEyj_^rJKK&&$43r;#w+5widENB@46 zc&}kkiKJnqvkQ|YItq;X9Qb1#?1dqKK<05l3(bUuRZgT zaBqzNbs4O&i;7kN`eqY9?nblYRZE9(&`_JI7M`SwAC{pc+r(7TSS;UcS!dM-z;IY|0)tCNm)bdrk5?=9A+`0lIcM6@{ad%eRKM@QSQBvBEc0M;D}XK zNx8n)pnb0)?o()%)0v-@WYqM%&Twr zs4@VMKuVJThtKP<7CdO18m5{0pXoxqj+F{g{`-NSCNp8h?`}*53lhY}2y4X6$rv1x zKk?aP21q+5*WK_|^h``oD4lTScp75!#t2qX{KVZ=xpws#h7P2w)qf*EEvM?UMZdv*HFTo!$L56pB1!CdnB|UOd#-Ye zOcCTSVcC)Dl^mlMf1pL8{Xm0X7376rUTL4HxSP0#IgK3xz)cN3N=qu3S zCc#oF_Vc8oD3OnJv_K2(U%oI}zw!!7;?>1td(0Y{5T5Fi>xl4$uvCeg=T?iCCA=)# zY2bfTwZ3tO+IZ6H-hkd)1*%2~&VoF!v7ZKe!%ey7d?M6?!w-MM<%BhiPKWM3X;GZF zA(z|Nb}8N##g2br*q{xL1m%cc`3XXw2evWPFT-V*PilI{;nzIGQ7A^NPcEhhLVN1^ z`5TQAz0fK|@LJX6Y765e(hL9sfdYji4#ac(`2et?He0UUiI=UE(86+^hN~E_5dP!) zWl|IhmJx4Zg6G~M!8d!4z^FVa* zQ;L@oU-MHHBgKyVPpqQOGtf@@Rzak>teIKL^=LjHR!m>M1y$7jg!AL{o=29-~PF18~D3JYTF;>0gnAFi8~k#XlJG(0oVQbAJkH^b^7x(Dix_-*6-mDDI%9@N*xYVIyH zbTWfAljJ%lDVK3Im*ZR43>cT*gsN=4G5;EcKOjb(8WeoCYW3}b<(^=`c@ZyYJqXn# zIDj>B0P8LUm&;3)JwkqKKe7edSW=X6d$lFLKr+hxu3D`SDWXoFw|=5B+fLVjfOJa>Q|P6bK<{B3$2 zyMb(1l}e`p+KIpDsn~OE`a3a(rQtDIkvFNn&AMTSoTbaXpr6CN2`0mggKYM|!od706P;@$N6|Je?@}4%Qu5^JYMVG_3J-p`%xd>6bf7g|x6O{f{Z^ zFBXg%_E+~#1Wj)DzQdzOrxd~jUqV|<@CNH0=vESlKT9elVtu)P*xiVqvBIqyPU-9h zM>x!K?JnUN69ydu+viOFnUV`jGpuk7KidfQ+1-4>v02n`i0NkJ5Uy&=dx;wu7A)n; zcw=SV8|gXzg&qmezOOA5n`T>RDXBH=o!)-m-~>xU30OnT*9L9wt~3*&IHwr)_9k0d zP~R)Pf4zuOZ<7p1Q`usB2oGlCTvFNzfeN_im$2mFW5*L8y#<}Q4SKw5Suq@qg9<-) z8=)mjGECIz1SC0o0;BML&li-02~MAUz?9j0kORbKkx4gut=3!6TWf49+2teyd05Io zO^RCKnE<*Wp`CEJ)b3xHIWc+f=gvJ@>zYGBAt8rQgKDRrS(fJUbK1a3?0}@3J_UxM z`*rhr6Ml^eL1FmW-t550NZf~mF}M7kk_P>7w4E?UL$%v&r+fvluiV|c`g=lJ;|BZU z;pluj?5YWHjt%u1X&)98!(E9*M|xB0t#%d|u0g9%I*m>RsBU71TN1`(sbNb_ z48Mggf@Z%8XskR>nw{@vyNAo2MlVdS*4(gm+sxUxH7?EyHL2Ep$)$VkdILMqgyH1C zVC(wkv{C1^Qh2U%%=^z=o(~?WnbU(TokJ-0I(3hBw5LUb&c>{TE^v3sNhnNKtjFlb#&#u*06Z@n@(`9Bc}mJQMpNec17}>v0EWaMnQ| z)=*I=9>j$Pd>INAuQEXoSL|OLY5x78#Ua%5!IH%M(#1PzgBy^%oAIT+tOx5zKc&+Q z-_A`f#lHDRgnQwI-seW1CmyMIrV#WsUzM-i{|InTtK$SBl}hJg`w37xqvO#hlkuaA za^(SX&jYl={jd36mU`2oz-PS>#Jur#kHNQ<&Y7w+f_ebW!ND=vmwRpYH2i+$rI}kg z)FZfUC-19Eo1R!lClOBB{!#7DZapHc;%mxxnH6O-aFe{UbFx1^^~YbJD@0{WOG~R! zY=6(9pK2}XAuF(a@dI7?7Fx4?x$+hRAQx@P)RTpw8_wK;9W#4g9cKY`&g0g=Nq%Sf zbfN-NP@3O&q6Zsh(m|;A)j~7_vKEZ3>PRQ#;ujjh-EPERqr8>Y#Y@vyZ99{HLqbm2 zkZTjdOD;t9Ie*yRQ~yv zcUF3w69UfZ>^Lj`k?B6iqjN~s(c+L^h*a7$CLWf)j{Ab6!FVga<)8WA1f?icf(lqm zBW*0!Pj}=dSAo*Ta%vnmIox~tGAF}L(4oJQydbtSDL*G1x1yz*|NQ)XPU%sjjR`JS zA3EU2YierTeSe=;V11E_S_lC}_}5>=ql=AH7~tl681FqeH<%;=3$zoLn#|Yd zri(y9=q-iopGKb+aFZ5jP7{o4*UK-$Cyks&%I;unc+)>;61dT*$*{}8NLpTxpSl*$ zm9jvlfZ<309&PeL^86B^k#VU6j?g-_^twp4TwyP;gzv2L#{8}3mzc$ghi)T`4`m5> zMz^1c+kN`LN`$#kvOk8?8=c>QX^KgnSP`@yPRPT5WXvqTZh3D@1XAYS30Wv+Tc~tz zJ?VTYJm1WBa3iwMKED%lwWV|AFhil`-FR6q`=@wKMLo#E>e0+NxkpU;yIB0i_vAm_ zbuFzMX@IA^jq{yGHy$`Y=$}t3!r!u8n+;&0RhV4uiF(@InJvQq{MVbg!)GUH7wH2I z9(h5rqoAO`*r8OQ91rZ=AOp4|(OE-Mr8xr5FIE9(eq`oD{tFMtBOi8(v&pu|&Roe& zviUI(>0H>`4TmV7MvKFz@fEa=x(h|~OX;SXxOzDL(Ad-d`7+@l1KTy*iooMz!dv_% z-QgwdRp26O9Vn)K?vNm>I!l6d$Vw{LQM7greo>b9c89dS;kc#;)H@K9s*q5UQ3e!a zYo0z%$bX)+z%F{n`4z~6bt}CnQ0~xgP3P5lm#b${7Xcl0CwOz@wNq{8eV4-#3nMn7 z+=J5R1Hz^Cq@<+8_KR1-eId%UV{+e-{ozQboCD$mL%}i%LcBF!*;8dZ8S=|}&}Oc) zX&COhmP5nE9%xzE@M9$0sR&ND*8NT@I)x)>K9G@U8*NU`{k%^XLl_b++Jj7 z*;poH_FgK#~ps^!nnf7vzWrLm66>)HzhTDh73(|R; zUNbw_;b(4itl|yUon}o!LW#U?CRweU)zG{n?WEghT|&^fxyVB(51gQDl_!%s5mU17 z@L^^pE#fc>wpM0}J%6r&hAaAG#T!i-2_^Qrne^CBuN1= zCW6-LY5B-ShL+c!prO}=L9)PZ<_u7G!@LfpjF(+H$qWRT&#C6 za=x+574_~>Qbgjd0?KFWc3jaM(`cbSb95`wGWk;F~Zq++E`Pw??VB2gzDeAVt z6?la0?q{ca(yZOz=!tZD)k8BL{(^=$m zSR^~Uq~>?AY@36HlG$#Tw6P4s|Nc(rheon)BIHA?zAcii9AdytT1}(9bNWNjyT^QK zIjez{w~OV<73&*xVe(EDQlMRt|M4o7^i%*;ma zDgYg3jh!2C1`piqLK=}X_|So&H>`U*;)z5|D0MC7+b?@{LN3wwHIK@_m;dj8_8htV z-LUcch@*6Q`g~yfs9ocQ#9g2MYpVpiB$ae%ncCJL@$Q{F1Rj-|LmdH{0JQe=Lm>l_ z|LI*sG~>axc>BLrD()?-O*@9e%C-;<8V>LKY#rx7oosx6g*1cjeELJ6?7H>jWNH5k zfvjAqqLF7`q5v|^Hl%PNn^XBKf+79kp8AtMDUOeo&CSi1dOwf+*(oaq3grK4vR_ri z0C-gwOE`<4IfY^`7q_MO!b@@JrTYq3z87-(-$MSY{COo(PPKihs2|m^0lq%kup`_5 zP_8-Lo1YK_zm+_g7fKZMzf{gX+NZ~zC(9kwA&6`?)}KzuIu<@%oeNbb2misuFRRku z&YparaXZq#sa+;p>71YKGSs!>1S2L&yUz6Gb;y*1_b({n{&n!WPpPOvvvt|ZG}|e? zPR6Ns@2CYyOOMK|@|5TewLYKtu!VNW8S3un_zqf(-tro5JFfo}WMXGIkvd7*g};jAtb3{_8#XqRZ_gC` zzR}_trd-Sn{ohyfEc$Y8FE}?|+p<7E?dsp})=E0$Pg4*5{chnx5Oa^$Ww5 z-q+8i_;`dV7ef2idJ%qv`U3_AG4;WV#mryY^hS3RwWTqCExB_9i7KXvG8FsFFwOb5 zAbQrDp4m@GPZ!t}iu4H6BGOjFtxPDF&j3vXM4oDnF%9@hOG>{SLmJbZx5pd9egw$hYw#Q&%@C&-)tHnQte`i z?UzTIV?It%C6ei2jFIv8kc?{SS8o2uG%-Phw0;%l5M(?>OHcn60!IA8#E%v?op*O0 zrS=?9`eW5~BfhU9h1#6ANjrmruRBqSm=wJTq!o>+_Et)Mz`RpN2JDd`+i^i z-+%6E(YtCNe(k06sN4t4v5)4J|C@VeA3XR((N5ppLXesjb|9Y*^eQ(0IJzJSBs7Ps z8p+q;vn70k;`t$7=yo{X?E+b^jKTj!M^8*{yxBM|V=Z^_j=VDdQAxJR%T&E{#*^G9 zi<^QgG1h*q_~83&uaxYMiWpstQnDt4fdcUvD(rvUn|z^eG8yRpbV30(OsO5hSsXaf zLcs{qcln{YoD{CG$8KJvKvqF}j+=93=$~>)5RZ`9frj1Pw>B;6c>xVcQL@bT;X6e0 zfhW}fPNo%t_Qv(NxD@D^z+uDXw5(6gAV}@Jom)4^-2@MMR%cUurG6ume27yA9@JeS!UYzE~wuk29t$6wc(%Z%PU?peRJ zz5Qg4>7=Qu*g7%%RpQFdVfd^5-2cRi-(U<>yUr+b_)f0@o59CS*FJm-CSY)jqEMx2 zIjDuF9q~14zxA$kgHf= zKlgc%zmc>h2l#pesVD9-ss7KU6FJv`zlArFw*KMW{2#AV-u`*Q5RT%c1Q=GZk6^2)aaSeqU=Hx#pu`&hs zG!$o{t3;7EMHPhVL6nH^n>S=2KuN)k9JtHNJ44S~CJ>4bRLW@oH^TmR_shNOjOr#$ z1e5RX;M)bc)maEHtcHo)a%a zt$usDoaWuBsR?7jX}ej3Td*dYQomcJk*C^INs;%cY)yQ485|uKF3YYFu_x`YG>(7j z4T_BeWR8}nx|!X@M?0bTHk-6-Ggi=9+yDRmkKeaGj37W*m+?PMSO1?S0ksVckCy+2 z82@Uy_QyC<biBrlRr(m8X!Pq*tGhVO}V0Ot#lKYuw+M>;^;b!wtHYI_KT5^TQxf zaEYHE9)NGaW+Z@T_uKq%Kj;>coXMY!VFFaz{_gZY?mU*rCdE3r!bs%dx9_55#|J-r zekg08Ph9c4l~2MAwKBf zPRke9$nLt)h{pUBbO|q&9idgz-u7)sgH3YREYT*>_-`hHR1}BWRY;m|>oSxX#>&c^ z2`yvha=k@;Fw-9L*7VVq+w}0PQMBwWzOX%y?!O zy5V?nKIn<0;ax-O(2utk{iq3xoU@p$rbjxv_(baT-A6m8-@p5%lt~eEph-0$aCdbx zioVH$j!^20Sm#$7LwZ=Hnk>4UWx)gP2U;9CmMu=|w_a9E8CX-j%P7aCl z<2mwX`f{!OE7n{GE%FDR&)H!FBB%r$HM{N2JF`X$8XLHcO~|40$_2_L;Ql+hyII)S z$XE}yTU+POS&eUcvEuI;23&YZco!V}8fu_O<6Y~*Fq_L2G=et9YStP?Zko4bWDcP^ z#08EIUZZVdm{7HzY_I|92n4wj93Rhd0ttA3$T(6oOsGu1cIkr?W&VU@Akx@k)(r74 zgKW?7(kP#7Ueota1e*|swFU;0TzaS%=!u>I7zgWY@%_ln8E|CkVi?;k@q^HHfD{tX75{c zT!dCy7BEPZ_R2+PTrqaGrQs zkMA)q)?dhef_dG^q60nhVoI`T=+jYHS7 z^nvpa>j1Dm9zvzPc<{FT)sA*|ojwvq7j<7SZ`I}O&wCy^C`K-Jn2kP_qDV*ExX5Gz0fpI7xV=i zX~j;C%p?Q7{}Cc0B9Dz{{_li>?y#QW&>$Pa!$L?t{04nzXXg)o^Or3+??hVNaS#?n z>n@Obtk&JCVSsQgpe+4zLA`dYo1HYunxWY89102Llz9rdw;=vAx6$IKvV}f6KUQt; zhH|~ke%{p*9_ z`1|Hf_-L?uE?-&5e6@LWFyVlbaC9tK+FK7yZ{klAd4K1dl$}lXJ-dJtBZjQ_>r!szS*s4X-pJe?o1k}> zQ%5&(7x3pp81Bs;G_e>pdMU(1AlzH(RO@3?rmWsg@4$8yATlm=DVD?ir@R+5D$<6) z0m;1jtE0`2t#|cnmKZ}AE-eI7ePZy2@K~k`mV%!@&x+ix*1VvoUi-kJ#>$`7yVjZT zMT=?O}Emct9hx$lfpMmGoF#k^$2m#bS(+6sd=jTX$DUh(mlfkpL! zw$88037a}}BL=K^1F3*(>_XtD#b|+W6}vI--=QLNyz^>2wZ%0d4tnd|@9zot5QgC_ z&3|NC7p7P%p2`_%m?%iU`f7o<{0xfq-xw*y$cb?$oySc076)G4lMXlbplqUGUc zHkYO&p?=QHWhU%f%m)5rsnD*`Wzd4s!>YZiCjuuJ&G)6}gVfr_n4r}N$tA@Ia(@|5 z)&tSs3G0r8FM3n5tj5O1-vwA1iHv(aKVoJ#V{VF>If-J|XMMmtGcv&`CUr*@#iRS< z!GkFAb6cISO;djLWtzEa78VsnZ;9~Pvpb#3l|79}=E2os89iSfVAg)s`Gt7X?y5MF zVt)O4;{)^Fh7g98?lGz4PG;a$8&x`bN}UBzmJygCO|x1W8k(2N$&Yn@%p8B`Fv)Pl z-#>H5oS}krdsCM2Fv{=69tzqPb@@KAt_XV5b;GIEJ_@Kf~$->734Tup5w zef{4rNzvbzyhPZN%ME^cYLyhz#asL`jWiVUJIKBu%L%v2<>J!GEkPbVmb3kP=!Lg{ zE(r1gO+5S0qyoy=g`Sj#q0gMDE{F(^6|a06(<=B?WWQQmuV%?8gyH#kIU{iIFho#_ z(=?qSl#&ZY7}hT7%OhE~;PFdQbUu&bnmeor-N_{5+lM3ZoWf;ll!Q_Wp$MJY)_7L@ z{&io7y6}Q{AU*KZFRras!Q;_5y=mD)TYt*+07KXYFY9mUuI&(>ed_6a&)$-u9h6?Za-{+qi}gia>pvZy z6+r-X^JHf-t=`;LJjB8myx1$!J2#q`@P3Gp$vk%YhCo8=udmg48$&no~ zHkOx>Y!N+kP`*qKmKHy?YieuVA^2KQ<}XFT1vBnS;GUW)o9nqC?qWTgSm+L(lAyhx zyW|s4&=zhWR9}`|5iqG{h4lpr>8G%=W1nkOoU$pFj;OqQdQ`LfK@JcDVl~5=Ie!aE z=!y+y<>1V`_~k>WoPHsC4YNJxZ=EJIX!Iauc(0cQ@)

tzTz8UgO*=y!5o#-p&~3 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 F{{sg#p-TV& literal 0 HcmV?d00001 diff --git a/test/golden-webkit/screenshot-offscreen-clip.png b/test/golden-webkit/screenshot-offscreen-clip.png new file mode 100644 index 0000000000000000000000000000000000000000..fa7bfdc3e38bc467f88fbd727bc6d0f546156409 GIT binary patch literal 1540 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 Jo}AIs{{zrbKGy&M literal 0 HcmV?d00001 diff --git a/test/golden-webkit/transparent.png b/test/golden-webkit/transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..47adcb4de07447f03529af7efe5f536bd72f06ad GIT binary patch literal 329 zcmeAS@N?(olHy`uVBq!ia0vp^DImEakt zG3V_?M_vX6fdd8y_CJ5z%k(WnROjZrnUN`T?lbP;=`>V1HbKHUX&5q|F>xfY9NGSF Sl_oH37(8A5T-G@yGywpMxoGGB literal 0 HcmV?d00001 diff --git a/test/headful.spec.js b/test/headful.spec.js new file mode 100644 index 0000000000..61ab37c602 --- /dev/null +++ b/test/headful.spec.js @@ -0,0 +1,152 @@ +/** + * 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 path = require('path'); +const os = require('os'); +const fs = require('fs'); +const util = require('util'); +const utils = require('./utils'); +const {waitEvent} = utils; + +const rmAsync = util.promisify(require('rimraf')); +const mkdtempAsync = util.promisify(fs.mkdtemp); + +const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-'); + +module.exports.addTests = function({testRunner, expect, playwright, defaultBrowserOptions, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + const headfulOptions = Object.assign({}, defaultBrowserOptions, { + headless: false + }); + const headlessOptions = Object.assign({}, defaultBrowserOptions, { + headless: true + }); + const extensionPath = path.join(__dirname, 'assets', 'simple-extension'); + const extensionOptions = Object.assign({}, defaultBrowserOptions, { + headless: false, + args: [ + `--disable-extensions-except=${extensionPath}`, + `--load-extension=${extensionPath}`, + ], + }); + + describe('HEADFUL', function() { + it('background_page target type should be available', async() => { + const browserWithExtension = await playwright.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({}) => { + const browserWithExtension = await playwright.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(() => window.MAGIC)).toBe(42); + await browserWithExtension.close(); + }); + it('should have default url when launching browser', async function() { + const browser = await playwright.launch(extensionOptions); + const pages = (await browser.pages()).map(page => page.url()); + expect(pages).toEqual(['about:blank']); + await browser.close(); + }); + it('headless should be able to read cookies written by headful', async({server}) => { + const userDataDir = await mkdtempAsync(TMP_FOLDER); + // Write a cookie in headful chrome + const headfulBrowser = await playwright.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 playwright.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/GoogleChrome/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(e => {}); + expect(cookie).toBe('foo=true'); + }); + // TODO: Support OOOPIF. @see https://github.com/GoogleChrome/puppeteer/issues/2548 + xit('OOPIF: should report google.com frame', async({server}) => { + // https://google.com is isolated by default in Chromium embedder. + const browser = await playwright.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('should close browser with beforeunload page', async({server}) => { + const browser = await playwright.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({server}) => { + const browser = await playwright.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 browser = await playwright.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/ignorehttpserrors.spec.js b/test/ignorehttpserrors.spec.js new file mode 100644 index 0000000000..0d25fd280d --- /dev/null +++ b/test/ignorehttpserrors.spec.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. + */ + +module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, playwright, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + describe.skip(WEBKIT)('ignoreHTTPSErrors', function() { + beforeAll(async state => { + state.browser = await playwright.launch({...defaultBrowserOptions, ignoreHTTPSErrors: true}); + }); + afterAll(async state => { + await state.browser.close(); + delete state.browser; + }); + beforeEach(async state => { + state.context = await state.browser.createIncognitoBrowserContext(); + state.page = await state.context.newPage(); + }); + afterEach(async state => { + await state.context.close(); + delete state.context; + delete state.page; + }); + + describe('Response.securityDetails', function() { + it('should work', async({page, httpsServer}) => { + const [serverRequest, response] = await Promise.all([ + httpsServer.waitForRequest('/empty.html'), + page.goto(httpsServer.EMPTY_PAGE) + ]); + const securityDetails = response.securityDetails(); + expect(securityDetails.issuer()).toBe('playwright-tests'); + const protocol = serverRequest.socket.getProtocol().replace('v', ' '); + expect(securityDetails.protocol()).toBe(protocol); + expect(securityDetails.subjectName()).toBe('playwright-tests'); + expect(securityDetails.validFrom()).toBe(1550084863); + expect(securityDetails.validTo()).toBe(33086084863); + }); + it('should be |null| for non-secure requests', async({page, server}) => { + const response = await page.goto(server.EMPTY_PAGE); + expect(response.securityDetails()).toBe(null); + }); + it('Network redirects should report SecurityDetails', async({page, httpsServer}) => { + 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({page, httpsServer}) => { + let error = null; + const response = await page.goto(httpsServer.EMPTY_PAGE).catch(e => error = e); + expect(error).toBe(null); + expect(response.ok()).toBe(true); + }); + it('should work with request interception', async({page, server, httpsServer}) => { + await page.setRequestInterception(true); + page.on('request', request => request.continue()); + const response = await page.goto(httpsServer.EMPTY_PAGE); + expect(response.status()).toBe(200); + }); + it('should work with mixed content', async({page, server, httpsServer}) => { + 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/GoogleChrome/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.js b/test/input.spec.js new file mode 100644 index 0000000000..764dfb8c1f --- /dev/null +++ b/test/input.spec.js @@ -0,0 +1,223 @@ +/** + * 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 FILE_TO_UPLOAD = path.join(__dirname, '/assets/file-to-upload.txt'); + +module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + describe.skip(WEBKIT)('input', function() { + it('should upload the file', async({page, server}) => { + await page.goto(server.PREFIX + '/input/fileupload.html'); + const filePath = path.relative(process.cwd(), FILE_TO_UPLOAD); + const input = await page.$('input'); + await input.uploadFile(filePath); + expect(await page.evaluate(e => e.files[0].name, input)).toBe('file-to-upload.txt'); + expect(await page.evaluate(e => { + 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'); + }); + }); + + describe.skip(FFOX || WEBKIT)('Page.waitForFileChooser', function() { + it('should work when file input is attached to DOM', async({page, server}) => { + 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({page, server}) => { + 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({page, server}) => { + let error = null; + await page.waitForFileChooser({timeout: 1}).catch(e => error = e); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it('should respect default timeout when there is no custom timeout', async({page, server}) => { + page.setDefaultTimeout(1); + let error = null; + await page.waitForFileChooser().catch(e => error = e); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it('should prioritize exact timeout over default timeout', async({page, server}) => { + page.setDefaultTimeout(0); + let error = null; + await page.waitForFileChooser({timeout: 1}).catch(e => error = e); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it('should work with no timeout', async({page, server}) => { + 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({page, server}) => { + await page.setContent(``); + const [fileChooser1, fileChooser2] = await Promise.all([ + page.waitForFileChooser(), + page.waitForFileChooser(), + page.$eval('input', input => input.click()), + ]); + expect(fileChooser1 === fileChooser2).toBe(true); + }); + }); + + describe.skip(FFOX || WEBKIT)('FileChooser.accept', function() { + it('should accept single file', async({page, server}) => { + 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 => input.files.length)).toBe(1); + expect(await page.$eval('input', input => input.files[0].name)).toBe('file-to-upload.txt'); + }); + it('should be able to read selected file', async({page, server}) => { + await page.setContent(``); + page.waitForFileChooser().then(chooser => chooser.accept([FILE_TO_UPLOAD])); + expect(await page.$eval('input', async picker => { + 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({page, server}) => { + await page.setContent(``); + page.waitForFileChooser().then(chooser => chooser.accept([FILE_TO_UPLOAD])); + expect(await page.$eval('input', async picker => { + 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 => { + 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({page, server}) => { + 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(e => error = e); + expect(error).not.toBe(null); + }); + it('should fail when accepting file chooser twice', async({page, server}) => { + await page.setContent(``); + const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', input => input.click()), + ]); + await fileChooser.accept([]); + let error = null; + await fileChooser.accept([]).catch(e => error = e); + expect(error.message).toBe('Cannot accept FileChooser which is already handled!'); + }); + }); + + describe.skip(FFOX || WEBKIT)('FileChooser.cancel', function() { + it('should cancel dialog', async({page, server}) => { + // 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 => input.click()), + ]); + await fileChooser1.cancel(); + // If this resolves, than we successfully canceled file chooser. + await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', input => input.click()), + ]); + }); + it('should fail when canceling file chooser twice', async({page, server}) => { + await page.setContent(``); + const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', input => input.click()), + ]); + await fileChooser.cancel(); + let error = null; + await fileChooser.cancel().catch(e => error = e); + expect(error.message).toBe('Cannot cancel FileChooser which is already handled!'); + }); + }); + + describe.skip(FFOX || WEBKIT)('FileChooser.isMultiple', () => { + it('should work for single file pick', async({page, server}) => { + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser.isMultiple()).toBe(false); + }); + it('should work for "multiple"', async({page, server}) => { + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser.isMultiple()).toBe(true); + }); + it('should work for "webkitdirectory"', async({page, server}) => { + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser.isMultiple()).toBe(true); + }); + }); +}; diff --git a/test/jshandle.spec.js b/test/jshandle.spec.js new file mode 100644 index 0000000000..7d120336c6 --- /dev/null +++ b/test/jshandle.spec.js @@ -0,0 +1,199 @@ +/** + * 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. + */ + +module.exports.addTests = function({testRunner, expect, CHROME, FFOX, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Page.evaluateHandle', function() { + it('should work', async({page, server}) => { + const windowHandle = await page.evaluateHandle(() => window); + expect(windowHandle).toBeTruthy(); + }); + it('should accept object handle as an argument', async({page, server}) => { + const navigatorHandle = await page.evaluateHandle(() => navigator); + const text = await page.evaluate(e => e.userAgent, navigatorHandle); + expect(text).toContain('Mozilla'); + }); + it('should accept object handle to primitive types', async({page, server}) => { + 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({page, server}) => { + const aHandle = await page.evaluateHandle(() => document.body); + let error = null; + await page.evaluateHandle( + opts => opts.elem.querySelector('p'), + { elem: aHandle } + ).catch(e => error = e); + expect(error.message).toContain('Are you passing a nested JSHandle?'); + }); + it('should accept object handle to unserializable value', async({page, server}) => { + 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({page, server}) => { + const aHandle = await page.evaluateHandle(() => { + window.FOO = 123; + return window; + }); + expect(await page.evaluate(e => e.FOO, aHandle)).toBe(123); + }); + it('should work with primitives', async({page, server}) => { + const aHandle = await page.evaluateHandle(() => { + window.FOO = 123; + return window; + }); + expect(await page.evaluate(e => e.FOO, aHandle)).toBe(123); + }); + }); + + describe('JSHandle.getProperty', function() { + it('should work', async({page, server}) => { + 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({page, server}) => { + const aHandle = await page.evaluateHandle(() => ({foo: 'bar'})); + const json = await aHandle.jsonValue(); + expect(json).toEqual({foo: 'bar'}); + }); + it.skip(FFOX)('should not work with dates', async({page, server}) => { + 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({page, server}) => { + const windowHandle = await page.evaluateHandle('window'); + let error = null; + await windowHandle.jsonValue().catch(e => error = e); + if (WEBKIT) + expect(error.message).toContain('Object has too long reference chain'); + else if (CHROME) + expect(error.message).toContain('Object reference chain is too long'); + else if (FFOX) + expect(error.message).toContain('Object is not serializable'); + }); + }); + + describe('JSHandle.getProperties', function() { + it('should work', async({page, server}) => { + 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({page, server}) => { + const aHandle = await page.evaluateHandle(() => { + class A { + constructor() { + this.a = '1'; + } + } + class B extends A { + 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({page, server}) => { + const aHandle = await page.evaluateHandle(() => document.body); + const element = aHandle.asElement(); + expect(element).toBeTruthy(); + }); + it('should return null for non-elements', async({page, server}) => { + const aHandle = await page.evaluateHandle(() => 2); + const element = aHandle.asElement(); + expect(element).toBeFalsy(); + }); + it('should return ElementHandle for TextNodes', async({page, server}) => { + await page.setContent('

ee!
'); + const aHandle = await page.evaluateHandle(() => document.querySelector('div').firstChild); + const element = aHandle.asElement(); + expect(element).toBeTruthy(); + expect(await page.evaluate(e => e.nodeType === HTMLElement.TEXT_NODE, element)); + }); + it('should work with nullified Node', async({page, server}) => { + await page.setContent('
test
'); + await page.evaluate(() => delete Node); + const handle = await page.evaluateHandle(() => document.querySelector('section')); + const element = handle.asElement(); + expect(element).not.toBe(null); + }); + }); + + describe('JSHandle.toString', function() { + it('should work for primitives', async({page, server}) => { + 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({page, server}) => { + const aHandle = await page.evaluateHandle(() => window); + expect(aHandle.toString()).toBe('JSHandle@object'); + }); + it('should work for promises', async({page, server}) => { + // wrap the promise in an object, otherwise we will await. + const wrapperHandle = await page.evaluateHandle(() => ({b: Promise.resolve(123)})); + const bHandle = await wrapperHandle.getProperty('b'); + expect(bHandle.toString()).toBe('JSHandle@promise'); + }); + it('should work with different subtypes', async({page, server}) => { + 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'); + // TODO(yurys): change subtype from array to typedarray in WebKit. + expect((await page.evaluateHandle('new Int32Array()')).toString()).toBe(WEBKIT ? 'JSHandle@array' : 'JSHandle@typedarray'); + expect((await page.evaluateHandle('new Proxy({}, {})')).toString()).toBe('JSHandle@proxy'); + }); + }); +}; diff --git a/test/keyboard.spec.js b/test/keyboard.spec.js new file mode 100644 index 0000000000..e9597f610a --- /dev/null +++ b/test/keyboard.spec.js @@ -0,0 +1,255 @@ +/** + * 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 utils = require('./utils'); +const os = require('os'); + +module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Keyboard', function() { + it('should type into a textarea', async({page, server}) => { + 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); + }); + it('should press the metaKey', async({page, server}) => { + await page.goto(server.PREFIX + '/empty.html'); + await page.evaluate(() => { + window.keyPromise = new Promise(resolve => document.addEventListener('keydown', event => resolve(event.key))); + }); + await page.keyboard.press('Meta'); + expect(await page.evaluate('keyPromise')).toBe(FFOX && os.platform() !== 'darwin' ? 'OS' : 'Meta'); + }); + it('should move with the arrow keys', async({page, server}) => { + 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({page, server}) => { + 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'); + }); + it.skip(FFOX || WEBKIT)('ElementHandle.press should support |text| option', async({page, server}) => { + 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('ё'); + }); + it.skip(WEBKIT)('should send a character with sendCharacter', async({page, server}) => { + 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'); + }); + it.skip(WEBKIT)('should report shiftKey', async({page, server}) => { + await page.goto(server.PREFIX + '/input/keyboard.html'); + const keyboard = page.keyboard; + const codeForKey = {'Shift': 16, 'Alt': 18, 'Control': 17}; + for (const modifierKey in codeForKey) { + await keyboard.down(modifierKey); + expect(await page.evaluate(() => getResult())).toBe('Keydown: ' + modifierKey + ' ' + modifierKey + 'Left ' + codeForKey[modifierKey] + ' [' + modifierKey + ']'); + await keyboard.down('!'); + // Shift+! will generate a keypress + if (modifierKey === 'Shift') + expect(await page.evaluate(() => getResult())).toBe('Keydown: ! Digit1 49 [' + modifierKey + ']\nKeypress: ! Digit1 33 33 [' + modifierKey + ']'); + else + expect(await page.evaluate(() => getResult())).toBe('Keydown: ! Digit1 49 [' + modifierKey + ']'); + + await keyboard.up('!'); + expect(await page.evaluate(() => getResult())).toBe('Keyup: ! Digit1 49 [' + modifierKey + ']'); + await keyboard.up(modifierKey); + expect(await page.evaluate(() => getResult())).toBe('Keyup: ' + modifierKey + ' ' + modifierKey + 'Left ' + codeForKey[modifierKey] + ' []'); + } + }); + it.skip(WEBKIT)('should report multiple modifiers', async({page, server}) => { + await page.goto(server.PREFIX + '/input/keyboard.html'); + const keyboard = page.keyboard; + await keyboard.down('Control'); + expect(await page.evaluate(() => getResult())).toBe('Keydown: Control ControlLeft 17 [Control]'); + await keyboard.down('Alt'); + expect(await page.evaluate(() => getResult())).toBe('Keydown: Alt AltLeft 18 [Alt Control]'); + await keyboard.down(';'); + expect(await page.evaluate(() => getResult())).toBe('Keydown: ; Semicolon 186 [Alt Control]'); + await keyboard.up(';'); + expect(await page.evaluate(() => getResult())).toBe('Keyup: ; Semicolon 186 [Alt Control]'); + await keyboard.up('Control'); + expect(await page.evaluate(() => getResult())).toBe('Keyup: Control ControlLeft 17 [Alt]'); + await keyboard.up('Alt'); + expect(await page.evaluate(() => getResult())).toBe('Keyup: Alt AltLeft 18 []'); + }); + it.skip(WEBKIT)('should send proper codes while typing', async({page, server}) => { + await page.goto(server.PREFIX + '/input/keyboard.html'); + await page.keyboard.type('!'); + expect(await page.evaluate(() => getResult())).toBe( + [ 'Keydown: ! Digit1 49 []', + 'Keypress: ! Digit1 33 33 []', + 'Keyup: ! Digit1 49 []'].join('\n')); + await page.keyboard.type('^'); + expect(await page.evaluate(() => getResult())).toBe( + [ 'Keydown: ^ Digit6 54 []', + 'Keypress: ^ Digit6 94 94 []', + 'Keyup: ^ Digit6 54 []'].join('\n')); + }); + it.skip(WEBKIT)('should send proper codes while typing with shift', async({page, server}) => { + await page.goto(server.PREFIX + '/input/keyboard.html'); + const keyboard = page.keyboard; + await keyboard.down('Shift'); + await page.keyboard.type('~'); + expect(await page.evaluate(() => 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.skip(WEBKIT)('should not type canceled events', async({page, server}) => { + 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(() => textarea.value)).toBe('He Wrd!'); + }); + it.skip(WEBKIT)('should specify repeat property', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + await page.evaluate(() => document.querySelector('textarea').addEventListener('keydown', e => window.lastEvent = e, true)); + await page.keyboard.down('a'); + expect(await page.evaluate(() => window.lastEvent.repeat)).toBe(false); + await page.keyboard.press('a'); + expect(await page.evaluate(() => window.lastEvent.repeat)).toBe(true); + + await page.keyboard.down('b'); + expect(await page.evaluate(() => window.lastEvent.repeat)).toBe(false); + await page.keyboard.down('b'); + expect(await page.evaluate(() => window.lastEvent.repeat)).toBe(true); + + await page.keyboard.up('a'); + await page.keyboard.down('a'); + expect(await page.evaluate(() => window.lastEvent.repeat)).toBe(false); + }); + it.skip(WEBKIT)('should type all kinds of characters', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + const text = 'This text goes onto two lines.\nThis character is 嗨.'; + await page.keyboard.type(text); + expect(await page.evaluate('result')).toBe(text); + }); + it.skip(WEBKIT)('should specify location', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.evaluate(() => { + window.addEventListener('keydown', event => window.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.skip(WEBKIT)('should throw on unknown keys', async({page, server}) => { + let error = await page.keyboard.press('NotARealKey').catch(e => e); + expect(error.message).toBe('Unknown key: "NotARealKey"'); + + error = await page.keyboard.press('ё').catch(e => e); + expect(error && error.message).toBe('Unknown key: "ё"'); + + error = await page.keyboard.press('😊').catch(e => e); + expect(error && error.message).toBe('Unknown key: "😊"'); + }); + it.skip(WEBKIT)('should type emoji', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.type('textarea', '👹 Tokyo street Japan 🇯🇵'); + expect(await page.$eval('textarea', textarea => textarea.value)).toBe('👹 Tokyo street Japan 🇯🇵'); + }); + it.skip(WEBKIT)('should type emoji into an iframe', async({page, server}) => { + 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 => textarea.value)).toBe('👹 Tokyo street Japan 🇯🇵'); + }); + it.skip(WEBKIT)('should press the meta key', async({page}) => { + await page.evaluate(() => { + window.result = null; + document.addEventListener('keydown', event => { + window.result = [event.key, event.code, event.metaKey]; + }); + }); + await page.keyboard.press('Meta'); + const [key, code, metaKey] = await page.evaluate('result'); + if (FFOX && os.platform() !== 'darwin') + expect(key).toBe('OS'); + else + expect(key).toBe('Meta'); + + if (FFOX) + expect(code).toBe('OSLeft'); + else + expect(code).toBe('MetaLeft'); + + if (FFOX && os.platform() !== 'darwin') + expect(metaKey).toBe(false); + else + expect(metaKey).toBe(true); + + }); + }); +}; diff --git a/test/launcher.spec.js b/test/launcher.spec.js new file mode 100644 index 0000000000..767cc0aa20 --- /dev/null +++ b/test/launcher.spec.js @@ -0,0 +1,427 @@ +/** + * 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 fs = require('fs'); +const os = require('os'); +const path = require('path'); +const {helper} = require('../lib/helper'); +const rmAsync = helper.promisify(require('rimraf')); +const mkdtempAsync = helper.promisify(fs.mkdtemp); +const readFileAsync = helper.promisify(fs.readFile); +const statAsync = helper.promisify(fs.stat); +const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-'); +const utils = require('./utils'); + +module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, playwright, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Playwright', function() { + describe('BrowserFetcher', function() { + it.skip(WEBKIT)('should download and extract linux binary', async({server}) => { + const downloadsFolder = await mkdtempAsync(TMP_FOLDER); + const browserFetcher = playwright.createBrowserFetcher({ + platform: 'linux', + path: downloadsFolder, + host: server.PREFIX + }); + let revisionInfo = browserFetcher.revisionInfo('123456'); + 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(await browserFetcher.canDownload('100000')).toBe(false); + expect(await browserFetcher.canDownload('123456')).toBe(true); + + revisionInfo = await browserFetcher.download('123456'); + expect(revisionInfo.local).toBe(true); + expect(await readFileAsync(revisionInfo.executablePath, 'utf8')).toBe('LINUX BINARY\n'); + const expectedPermissions = os.platform() === 'win32' ? 0666 : 0755; + expect((await statAsync(revisionInfo.executablePath)).mode & 0777).toBe(expectedPermissions); + expect(await browserFetcher.localRevisions()).toEqual(['123456']); + await browserFetcher.remove('123456'); + expect(await browserFetcher.localRevisions()).toEqual([]); + await rmAsync(downloadsFolder); + }); + }); + describe.skip(WEBKIT)('Browser.disconnect', function() { + it('should reject navigation when browser closes', async({server}) => { + server.setRoute('/one-style.css', () => {}); + const browser = await playwright.launch(defaultBrowserOptions); + const remote = await playwright.connect({...defaultBrowserOptions, browserWSEndpoint: browser.wsEndpoint()}); + const page = await remote.newPage(); + const navigationPromise = page.goto(server.PREFIX + '/one-style.html', {timeout: 60000}).catch(e => e); + 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({server}) => { + server.setRoute('/empty.html', () => {}); + const browser = await playwright.launch(defaultBrowserOptions); + const remote = await playwright.connect({...defaultBrowserOptions, browserWSEndpoint: browser.wsEndpoint()}); + const page = await remote.newPage(); + const watchdog = page.waitForSelector('div', {timeout: 60000}).catch(e => e); + remote.disconnect(); + const error = await watchdog; + expect(error.message).toContain('Protocol error'); + await browser.close(); + }); + }); + describe('Browser.close', function() { + it.skip(WEBKIT)('should terminate network waiters', async({context, server}) => { + const browser = await playwright.launch(defaultBrowserOptions); + const remote = await playwright.connect({...defaultBrowserOptions, browserWSEndpoint: browser.wsEndpoint()}); + const newPage = await remote.newPage(); + const results = await Promise.all([ + newPage.waitForRequest(server.EMPTY_PAGE).catch(e => e), + newPage.waitForResponse(server.EMPTY_PAGE).catch(e => e), + browser.close() + ]); + for (let i = 0; i < 2; i++) { + const message = results[i].message; + expect(message).toContain('Target closed'); + expect(message).not.toContain('Timeout'); + } + }); + }); + describe.skip(WEBKIT)('Playwright.launch', function() { + it('should reject all promises when browser is closed', async() => { + const browser = await playwright.launch(defaultBrowserOptions); + const page = await browser.newPage(); + let error = null; + const neverResolves = page.evaluate(() => new Promise(r => {})).catch(e => error = e); + await browser.close(); + await neverResolves; + expect(error.message).toContain('Protocol error'); + }); + it('should reject if executable path is invalid', async({server}) => { + let waitError = null; + const options = Object.assign({}, defaultBrowserOptions, {executablePath: 'random-invalid-path'}); + await playwright.launch(options).catch(e => waitError = e); + expect(waitError.message).toContain('Failed to launch'); + }); + it('userDataDir option', async({server}) => { + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({userDataDir}, defaultBrowserOptions); + const browser = await playwright.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/GoogleChrome/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(e => {}); + }); + it('userDataDir argument', async({server}) => { + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({}, defaultBrowserOptions); + if (CHROME || WEBKIT) { + options.args = [ + ...(defaultBrowserOptions.args || []), + `--user-data-dir=${userDataDir}` + ]; + } else { + options.args = [ + ...(defaultBrowserOptions.args || []), + `-profile`, + userDataDir, + ]; + } + const browser = await playwright.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/GoogleChrome/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(e => {}); + }); + it('userDataDir option should restore state', async({server}) => { + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({userDataDir}, defaultBrowserOptions); + const browser = await playwright.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 playwright.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/GoogleChrome/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(e => {}); + }); + // This mysteriously fails on Windows on AppVeyor. See https://github.com/GoogleChrome/puppeteer/issues/4111 + xit('userDataDir option should restore cookies', async({server}) => { + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({userDataDir}, defaultBrowserOptions); + const browser = await playwright.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 playwright.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/GoogleChrome/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(e => {}); + }); + it('should return the default arguments', async() => { + if (CHROME || WEBKIT) { + expect(playwright.defaultArgs()).toContain('--no-first-run'); + expect(playwright.defaultArgs()).toContain('--headless'); + expect(playwright.defaultArgs({headless: false})).not.toContain('--headless'); + expect(playwright.defaultArgs({userDataDir: 'foo'})).toContain('--user-data-dir=foo'); + } else { + expect(playwright.defaultArgs({browser: 'firefox'})).toContain('-headless'); + expect(playwright.defaultArgs({browser: 'firefox', headless: false})).not.toContain('-headless'); + expect(playwright.defaultArgs({browser: 'firefox', userDataDir: 'foo'})).toContain('-profile'); + expect(playwright.defaultArgs({browser: 'firefox', userDataDir: 'foo'})).toContain('foo'); + } + }); + it('should work with no default arguments', async() => { + const options = Object.assign({}, defaultBrowserOptions); + options.ignoreDefaultArgs = true; + const browser = await playwright.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() => { + // Make sure we launch with `--enable-automation` by default. + const defaultArgs = playwright.defaultArgs(defaultBrowserOptions); + const browser = await playwright.launch(Object.assign({}, defaultBrowserOptions, { + // Ignore first and third default argument. + ignoreDefaultArgs: [ defaultArgs[0], defaultArgs[2] ], + })); + const spawnargs = browser.process().spawnargs; + 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.skip(FFOX)('should have default URL when launching browser', async function() { + const browser = await playwright.launch(defaultBrowserOptions); + const pages = (await browser.pages()).map(page => page.url()); + expect(pages).toEqual(['about:blank']); + await browser.close(); + }); + it.skip(FFOX)('should have custom URL when launching browser', async function({server}) { + const options = Object.assign({}, defaultBrowserOptions); + options.args = [server.EMPTY_PAGE].concat(options.args || []); + const browser = await playwright.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 options = Object.assign({}, defaultBrowserOptions, { + defaultViewport: { + width: 456, + height: 789 + } + }); + const browser = await playwright.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 options = Object.assign({}, defaultBrowserOptions, { + defaultViewport: null + }); + const browser = await playwright.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({server}) => { + const options = Object.assign({}, defaultBrowserOptions, { + defaultViewport: null + }); + const browser = await playwright.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(); + }); + }); + describe.skip(WEBKIT)('Playwright.connect', function() { + it('should be able to connect multiple times to the same browser', async({server}) => { + const originalBrowser = await playwright.launch(defaultBrowserOptions); + const browser = await playwright.connect({ + ...defaultBrowserOptions, + browserWSEndpoint: originalBrowser.wsEndpoint() + }); + const page = await browser.newPage(); + expect(await page.evaluate(() => 7 * 8)).toBe(56); + browser.disconnect(); + + const secondPage = await originalBrowser.newPage(); + expect(await secondPage.evaluate(() => 7 * 6)).toBe(42, 'original browser should still work'); + await originalBrowser.close(); + }); + it('should be able to close remote browser', async({server}) => { + const originalBrowser = await playwright.launch(defaultBrowserOptions); + const remoteBrowser = await playwright.connect({ + ...defaultBrowserOptions, + browserWSEndpoint: originalBrowser.wsEndpoint() + }); + await Promise.all([ + utils.waitEvent(originalBrowser, 'disconnected'), + remoteBrowser.close(), + ]); + }); + it('should support ignoreHTTPSErrors option', async({httpsServer}) => { + const originalBrowser = await playwright.launch(defaultBrowserOptions); + const browserWSEndpoint = originalBrowser.wsEndpoint(); + + const browser = await playwright.connect({...defaultBrowserOptions, 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(e => error = e) + ]); + 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 be able to reconnect to a disconnected browser', async({server}) => { + const originalBrowser = await playwright.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 playwright.connect({...defaultBrowserOptions, 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/GoogleChrome/puppeteer/issues/4197#issuecomment-481793410 + it('should be able to connect to the same page simultaneously', async({server}) => { + const browserOne = await playwright.launch(defaultBrowserOptions); + const browserTwo = await playwright.connect({ ...defaultBrowserOptions, 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(); + }); + + }); + describe('Playwright.executablePath', function() { + it('should work', async({server}) => { + const executablePath = playwright.executablePath(); + expect(fs.existsSync(executablePath)).toBe(true); + expect(fs.realpathSync(executablePath)).toBe(executablePath); + }); + }); + }); + + describe.skip(WEBKIT)('Top-level requires', function() { + it('should require top-level Errors', async() => { + const Errors = require(path.join(utils.projectRoot(), '/Errors')); + expect(Errors.TimeoutError).toBe(playwright.errors.TimeoutError); + }); + it('should require top-level DeviceDescriptors', async() => { + const Devices = require(path.join(utils.projectRoot(), '/DeviceDescriptors')); + expect(Devices['iPhone 6']).toBe(playwright.devices['iPhone 6']); + }); + }); + + describe.skip(WEBKIT)('Browser target events', function() { + it('should work', async({server}) => { + const browser = await playwright.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.skip(WEBKIT)('Browser.Events.disconnected', function() { + it('should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', async() => { + const originalBrowser = await playwright.launch(defaultBrowserOptions); + const browserWSEndpoint = originalBrowser.wsEndpoint(); + const remoteBrowser1 = await playwright.connect({browserWSEndpoint}); + const remoteBrowser2 = await playwright.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/mouse.spec.js b/test/mouse.spec.js new file mode 100644 index 0000000000..92a0aef73a --- /dev/null +++ b/test/mouse.spec.js @@ -0,0 +1,162 @@ +/** + * 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'); + +function dimensions() { + const rect = document.querySelector('textarea').getBoundingClientRect(); + return { + x: rect.left, + y: rect.top, + width: rect.width, + height: rect.height + }; +} + +module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Mouse', function() { + it('should click the document', async({page, server}) => { + await page.evaluate(() => { + window.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(() => window.clickPromise); + expect(event.type).toBe('click'); + expect(event.detail).toBe(1); + expect(event.clientX).toBe(50); + expect(event.clientY).toBe(60); + expect(event.isTrusted).toBe(true); + expect(event.button).toBe(0); + }); + it('should resize the textarea', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + const {x, y, width, height} = await page.evaluate(dimensions); + const mouse = page.mouse; + // The test becomes flaky on WebKit without next line. + if (WEBKIT) + await page.evaluate(() => new Promise(requestAnimationFrame)); + await mouse.move(x + width - 4, y + height - 4); + await mouse.down(); + await mouse.move(x + width + 100, y + height + 100); + await mouse.up(); + const newDimensions = await page.evaluate(dimensions); + expect(newDimensions.width).toBe(Math.round(width + 104)); + expect(newDimensions.height).toBe(Math.round(height + 104)); + }); + it('should select the text with mouse', async({page, server}) => { + 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(200,200); + await page.mouse.up(); + expect(await page.evaluate(() => { + const textarea = document.querySelector('textarea'); + return textarea.value.substring(textarea.selectionStart, textarea.selectionEnd); + })).toBe(text); + }); + it('should trigger hover state', async({page, server}) => { + 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'); + }); + it.skip(FFOX || WEBKIT)('should trigger hover state with removed window.Node', async({page, server}) => { + 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({page, server}) => { + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.evaluate(() => document.querySelector('#button-3').addEventListener('mousedown', e => window.lastEvent = e, true)); + const modifiers = {'Shift': 'shiftKey', 'Control': 'ctrlKey', 'Alt': 'altKey', 'Meta': 'metaKey'}; + // In Firefox, the Meta modifier only exists on Mac + if (FFOX && os.platform() !== 'darwin') + delete modifiers['Meta']; + for (const modifier in modifiers) { + await page.keyboard.down(modifier); + await page.click('#button-3'); + if (!(await page.evaluate(mod => window.lastEvent[mod], modifiers[modifier]))) + throw new Error(modifiers[modifier] + ' should be true'); + await page.keyboard.up(modifier); + } + await page.click('#button-3'); + for (const modifier in modifiers) { + if ((await page.evaluate(mod => window.lastEvent[mod], modifiers[modifier]))) + throw new Error(modifiers[modifier] + ' should be false'); + } + }); + it('should tween mouse movement', async({page, server}) => { + // The test becomes flaky on WebKit without next line. + if (WEBKIT) + await page.evaluate(() => new Promise(requestAnimationFrame)); + await page.mouse.move(100, 100); + await page.evaluate(() => { + window.result = []; + document.addEventListener('mousemove', event => { + window.result.push([event.clientX, event.clientY]); + }); + }); + await page.mouse.move(200, 300, {steps: 5}); + expect(await page.evaluate('result')).toEqual([ + [120, 140], + [140, 180], + [160, 220], + [180, 260], + [200, 300] + ]); + }); + // @see https://crbug.com/929806 + it.skip(WEBKIT)('should work with mobile viewports and cross process navigations', async({page, server}) => { + 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 => { + window.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.js b/test/navigation.spec.js new file mode 100644 index 0000000000..5af9c269c8 --- /dev/null +++ b/test/navigation.spec.js @@ -0,0 +1,574 @@ +/** + * 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 utils = require('./utils'); + +module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Page.goto', function() { + it('should work', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + expect(page.url()).toBe(server.EMPTY_PAGE); + }); + it.skip(WEBKIT)('should work with anchor navigation', async({page, server}) => { + 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({page, server}) => { + 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({page, server}) => { + const response = await page.goto('about:blank'); + expect(response).toBe(null); + }); + it('should return response when page changes its URL after load', async({page, server}) => { + const response = await page.goto(server.PREFIX + '/historyapi.html'); + expect(response.status()).toBe(200); + }); + it('should work with subframes return 204', async({page, server}) => { + server.setRoute('/frames/frame.html', (req, res) => { + res.statusCode = 204; + res.end(); + }); + await page.goto(server.PREFIX + '/frames/one-frame.html'); + }); + it.skip(WEBKIT)('should fail when server returns 204', async({page, server}) => { + server.setRoute('/empty.html', (req, res) => { + res.statusCode = 204; + res.end(); + }); + let error = null; + await page.goto(server.EMPTY_PAGE).catch(e => error = e); + expect(error).not.toBe(null); + if (CHROME || WEBKIT) + expect(error.message).toContain('net::ERR_ABORTED'); + else + expect(error.message).toContain('NS_BINDING_ABORTED'); + }); + it('should navigate to empty page with domcontentloaded', async({page, server}) => { + const response = await page.goto(server.EMPTY_PAGE, {waitUntil: 'domcontentloaded'}); + expect(response.status()).toBe(200); + }); + it.skip(WEBKIT)('should work when page calls history API in beforeunload', async({page, server}) => { + 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); + }); + it.skip(FFOX || WEBKIT)('should navigate to empty page with networkidle0', async({page, server}) => { + const response = await page.goto(server.EMPTY_PAGE, {waitUntil: 'networkidle0'}); + expect(response.status()).toBe(200); + }); + it.skip(FFOX || WEBKIT)('should navigate to empty page with networkidle2', async({page, server}) => { + const response = await page.goto(server.EMPTY_PAGE, {waitUntil: 'networkidle2'}); + expect(response.status()).toBe(200); + }); + it.skip(WEBKIT)('should fail when navigating to bad url', async({page, server}) => { + let error = null; + await page.goto('asdfasdf').catch(e => error = e); + if (CHROME || WEBKIT) + expect(error.message).toContain('Cannot navigate to invalid URL'); + else + expect(error.message).toContain('Invalid url'); + }); + // FIXME: shows dialog in WebKit. + it.skip(WEBKIT)('should fail when navigating to bad SSL', async({page, httpsServer}) => { + // Make sure that network events do not emit 'undefined'. + // @see https://crbug.com/750469 + page.on('request', request => expect(request).toBeTruthy()); + page.on('requestfinished', request => expect(request).toBeTruthy()); + page.on('requestfailed', request => expect(request).toBeTruthy()); + let error = null; + await page.goto(httpsServer.EMPTY_PAGE).catch(e => error = e); + if (CHROME || WEBKIT) + expect(error.message).toContain('net::ERR_CERT_AUTHORITY_INVALID'); + else + expect(error.message).toContain('SSL_ERROR_UNKNOWN'); + }); + // FIXME: shows dialog in WebKit. + it.skip(WEBKIT)('should fail when navigating to bad SSL after redirects', async({page, server, httpsServer}) => { + 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(e => error = e); + if (CHROME || WEBKIT) + expect(error.message).toContain('net::ERR_CERT_AUTHORITY_INVALID'); + else + expect(error.message).toContain('SSL_ERROR_UNKNOWN'); + }); + it.skip(FFOX || WEBKIT)('should throw if networkidle is passed as an option', async({page, server}) => { + let error = null; + await page.goto(server.EMPTY_PAGE, {waitUntil: 'networkidle'}).catch(err => error = err); + expect(error.message).toContain('"networkidle" option is no longer supported'); + }); + it.skip(WEBKIT)('should fail when main resources failed to load', async({page, server}) => { + let error = null; + await page.goto('http://localhost:44123/non-existing-url').catch(e => error = e); + if (CHROME || WEBKIT) + 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({page, server}) => { + // Hang for request to the empty.html + server.setRoute('/empty.html', (req, res) => { }); + let error = null; + await page.goto(server.PREFIX + '/empty.html', {timeout: 1}).catch(e => error = e); + const message = WEBKIT ? 'Navigation Timeout Exceeded: 1ms' : 'Navigation timeout of 1 ms exceeded'; + expect(error.message).toContain(message); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it('should fail when exceeding default maximum navigation timeout', async({page, server}) => { + // Hang for request to the empty.html + server.setRoute('/empty.html', (req, res) => { }); + let error = null; + page.setDefaultNavigationTimeout(1); + await page.goto(server.PREFIX + '/empty.html').catch(e => error = e); + const message = WEBKIT ? 'Navigation Timeout Exceeded: 1ms' : 'Navigation timeout of 1 ms exceeded'; + expect(error.message).toContain(message); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it('should fail when exceeding default maximum timeout', async({page, server}) => { + // Hang for request to the empty.html + server.setRoute('/empty.html', (req, res) => { }); + let error = null; + page.setDefaultTimeout(1); + await page.goto(server.PREFIX + '/empty.html').catch(e => error = e); + const message = WEBKIT ? 'Navigation Timeout Exceeded: 1ms' : 'Navigation timeout of 1 ms exceeded'; + expect(error.message).toContain(message); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it('should prioritize default navigation timeout over default timeout', async({page, server}) => { + // Hang for request to the empty.html + server.setRoute('/empty.html', (req, res) => { }); + let error = null; + page.setDefaultTimeout(0); + page.setDefaultNavigationTimeout(1); + await page.goto(server.PREFIX + '/empty.html').catch(e => error = e); + const message = WEBKIT ? 'Navigation Timeout Exceeded: 1ms' : 'Navigation timeout of 1 ms exceeded'; + expect(error.message).toContain(message); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it('should disable timeout when its set to 0', async({page, server}) => { + let error = null; + let loaded = false; + page.once('load', () => loaded = true); + await page.goto(server.PREFIX + '/grid.html', {timeout: 0, waitUntil: ['load']}).catch(e => error = e); + expect(error).toBe(null); + expect(loaded).toBe(true); + }); + it('should work when navigating to valid url', async({page, server}) => { + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok()).toBe(true); + }); + it.skip(FFOX)('should work when navigating to data url', async({page, server}) => { + const response = await page.goto('data:text/html,hello'); + expect(response.ok()).toBe(true); + }); + it('should work when navigating to 404', async({page, server}) => { + 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({page, server}) => { + server.setRedirect('/redirect/1.html', '/redirect/2.html'); + server.setRedirect('/redirect/2.html', '/redirect/3.html'); + server.setRedirect('/redirect/3.html', server.EMPTY_PAGE); + const response = await page.goto(server.PREFIX + '/redirect/1.html'); + expect(response.ok()).toBe(true); + expect(response.url()).toBe(server.EMPTY_PAGE); + }); + it.skip(FFOX || WEBKIT)('should wait for network idle to succeed navigation', async({page, server}) => { + 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({page, server}) => { + 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.skip(WEBKIT)('should not leak listeners during bad navigation', async({page, server}) => { + let warning = null; + const warningHandler = w => warning = w; + process.on('warning', warningHandler); + for (let i = 0; i < 20; ++i) + await page.goto('asdf').catch(e => {/* swallow navigation error */}); + process.removeListener('warning', warningHandler); + expect(warning).toBe(null); + }); + it('should not leak listeners during navigation of 11 pages', async({page, context, server}) => { + 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); + }); + it.skip(FFOX)('should navigate to dataURL and fire dataURL requests', async({page, server}) => { + 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); + }); + it.skip(FFOX || WEBKIT)('should navigate to URL with hash and fire requests without hash', async({page, server}) => { + 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.skip(WEBKIT)('should work with self requesting page', async({page, server}) => { + const response = await page.goto(server.PREFIX + '/self-request.html'); + expect(response.status()).toBe(200); + expect(response.url()).toContain('self-request.html'); + }); + it.skip(WEBKIT)('should fail when navigating and show the url at the error message', async function({page, server, httpsServer}) { + const url = httpsServer.PREFIX + '/redirect/1.html'; + let error = null; + try { + await page.goto(url); + } catch (e) { + error = e; + } + expect(error.message).toContain(url); + }); + it.skip(WEBKIT)('should send referer', async({page, server}) => { + 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() { + it('should work', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.evaluate(url => window.location.href = url, server.PREFIX + '/grid.html') + ]); + expect(response.ok()).toBe(true); + expect(response.url()).toContain('grid.html'); + }); + it.skip(WEBKIT)('should work with both domcontentloaded and load', async({page, server}) => { + 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; + }); + it('should work with clicking on anchor links', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setContent(`
foobar`); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.click('a'), + ]); + expect(response).toBe(null); + expect(page.url()).toBe(server.EMPTY_PAGE + '#foobar'); + }); + it('should work with history.pushState()', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setContent(` + SPA + + `); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.click('a'), + ]); + expect(response).toBe(null); + expect(page.url()).toBe(server.PREFIX + '/wow.html'); + }); + it('should work with history.replaceState()', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setContent(` + SPA + + `); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.click('a'), + ]); + expect(response).toBe(null); + expect(page.url()).toBe(server.PREFIX + '/replaced.html'); + }); + it.skip(FFOX)('should work with DOM history.back()/history.forward()', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.setContent(` + back + forward + + `); + expect(page.url()).toBe(server.PREFIX + '/second.html'); + const [backResponse] = await Promise.all([ + page.waitForNavigation(), + page.click('a#back'), + ]); + expect(backResponse).toBe(null); + expect(page.url()).toBe(server.PREFIX + '/first.html'); + const [forwardResponse] = await Promise.all([ + page.waitForNavigation(), + page.click('a#forward'), + ]); + expect(forwardResponse).toBe(null); + expect(page.url()).toBe(server.PREFIX + '/second.html'); + }); + it.skip(WEBKIT)('should work when subframe issues window.stop()', async({page, server}) => { + server.setRoute('/frames/style.css', (req, res) => {}); + 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.skip(WEBKIT)('should work', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.goto(server.PREFIX + '/grid.html'); + + let response = await page.goBack(); + expect(response.ok()).toBe(true); + expect(response.url()).toContain(server.EMPTY_PAGE); + + response = await page.goForward(); + expect(response.ok()).toBe(true); + expect(response.url()).toContain('/grid.html'); + + response = await page.goForward(); + expect(response).toBe(null); + }); + it.skip(WEBKIT)('should work with HistoryAPI', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + history.pushState({}, '', '/first.html'); + history.pushState({}, '', '/second.html'); + }); + expect(page.url()).toBe(server.PREFIX + '/second.html'); + + await page.goBack(); + expect(page.url()).toBe(server.PREFIX + '/first.html'); + await page.goBack(); + expect(page.url()).toBe(server.EMPTY_PAGE); + await page.goForward(); + expect(page.url()).toBe(server.PREFIX + '/first.html'); + }); + }); + + describe('Frame.goto', function() { + it.skip(WEBKIT)('should navigate subframes', async({page, server}) => { + 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.skip(WEBKIT)('should reject when frame detaches', async({page, server}) => { + await page.goto(server.PREFIX + '/frames/one-frame.html'); + + server.setRoute('/empty.html', () => {}); + const navigationPromise = page.frames()[1].goto(server.EMPTY_PAGE).catch(e => e); + 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.skip(WEBKIT)('should return matching responses', async({page, server}) => { + // 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]); + } + }); + }); + + describe('Frame.waitForNavigation', function() { + it.skip(WEBKIT)('should work', async({page, server}) => { + await page.goto(server.PREFIX + '/frames/one-frame.html'); + const frame = page.frames()[1]; + const [response] = await Promise.all([ + frame.waitForNavigation(), + frame.evaluate(url => 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.skip(WEBKIT)('should fail when frame detaches', async({page, server}) => { + 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(e => error = e); + await Promise.all([ + server.waitForRequest('/empty.html'), + frame.evaluate(() => window.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({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => window._foo = 10); + await page.reload(); + expect(await page.evaluate(() => window._foo)).toBe(undefined); + }); + }); +}; diff --git a/test/network.spec.js b/test/network.spec.js new file mode 100644 index 0000000000..36f8eb44d8 --- /dev/null +++ b/test/network.spec.js @@ -0,0 +1,445 @@ +/** + * 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 fs = require('fs'); +const path = require('path'); +const utils = require('./utils'); + +module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Page.Events.Request', function() { + it('should fire for navigation requests', async({page, server}) => { + 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({page, server}) => { + 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({page, server}) => { + 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({page, server}) => { + 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()); + }); + // FIXME: needs frameAttached event, otherwise it introduces too many hacks in the lib. + it.skip(WEBKIT)('should work for subframe navigation request', async({page, server}) => { + 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({page, server}) => { + 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({page, server}) => { + const response = await page.goto(server.EMPTY_PAGE); + if (CHROME) + expect(response.request().headers()['user-agent']).toContain('Chrome'); + else if (FFOX) + expect(response.request().headers()['user-agent']).toContain('Firefox'); + else if (WEBKIT) + expect(response.request().headers()['user-agent']).toContain('WebKit'); + }); + }); + + describe('Response.headers', function() { + it('should work', async({page, server}) => { + server.setRoute('/empty.html', (req, res) => { + res.setHeader('foo', 'bar'); + res.end(); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.headers()['foo']).toBe('bar'); + }); + }); + + describe.skip(FFOX)('Response.fromCache', function() { + it.skip(WEBKIT)('should return |false| for non-cached content', async({page, server}) => { + const response = await page.goto(server.EMPTY_PAGE); + expect(response.fromCache()).toBe(false); + }); + + it.skip(WEBKIT)('should work', async({page, server}) => { + 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); + }); + }); + + describe.skip(FFOX)('Response.fromServiceWorker', function() { + it.skip(WEBKIT)('should return |false| for non-service-worker content', async({page, server}) => { + const response = await page.goto(server.EMPTY_PAGE); + expect(response.fromServiceWorker()).toBe(false); + }); + + // FIXME: WebKit responses contain sw.js + it.skip(WEBKIT)('Response.fromServiceWorker', async({page, server}) => { + 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 window.activationPromise); + await page.reload(); + + expect(responses.size).toBe(2); + expect(responses.get('sw.html').status()).toBe(200); + expect(responses.get('sw.html').fromServiceWorker()).toBe(true); + expect(responses.get('style.css').status()).toBe(200); + expect(responses.get('style.css').fromServiceWorker()).toBe(true); + }); + }); + + describe('Request.postData', function() { + it('should work', async({page, server}) => { + 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({page, server}) => { + const response = await page.goto(server.EMPTY_PAGE); + expect(response.request().postData()).toBe(undefined); + }); + }); + + describe('Response.text', function() { + it('should work', async({page, server}) => { + const response = await page.goto(server.PREFIX + '/simple.json'); + expect(await response.text()).toBe('{"foo": "bar"}\n'); + }); + it('should return uncompressed text', async({page, server}) => { + server.enableGzip('/simple.json'); + const response = await page.goto(server.PREFIX + '/simple.json'); + expect(response.headers()['content-encoding']).toBe('gzip'); + expect(await response.text()).toBe('{"foo": "bar"}\n'); + }); + it('should throw when requesting body of redirected response', async({page, server}) => { + 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(e => error = e); + expect(error.message).toContain('Response body is unavailable for redirect responses'); + }); + it('should wait until response completes', async({page, server}) => { + 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!'); + }); + }); + + describe('Response.json', function() { + it('should work', async({page, server}) => { + const response = await page.goto(server.PREFIX + '/simple.json'); + expect(await response.json()).toEqual({foo: 'bar'}); + }); + }); + + describe('Response.buffer', function() { + it('should work', async({page, server}) => { + 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({page, server}) => { + 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({page, server}) => { + server.setRoute('/cool', (req, res) => { + res.writeHead(200, 'cool!'); + res.end(); + }); + const response = await page.goto(server.PREFIX + '/cool'); + expect(response.statusText()).toBe('cool!'); + }); + }); + + describe('Network Events', function() { + it('Page.Events.Request', async({page, server}) => { + 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); + }); + // FIXME: WebKit doesn't provide remoteIPAddress in the response. + it.skip(WEBKIT)('Page.Events.Response', async({page, server}) => { + 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); + }); + + // FIXME: requires request interception. + it.skip(WEBKIT)('Page.Events.RequestFailed', async({page, server}) => { + 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 (CHROME || WEBKIT) + expect(failedRequests[0].failure().errorText).toBe('net::ERR_FAILED'); + else + expect(failedRequests[0].failure().errorText).toBe('NS_ERROR_FAILURE'); + expect(failedRequests[0].frame()).toBeTruthy(); + }); + // FIXME: WebKit requestfinished comes after goto. + it.skip(WEBKIT)('Page.Events.RequestFinished', async({page, server}) => { + 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.skip(WEBKIT)('should fire events in proper order', async({page, server}) => { + const events = []; + page.on('request', request => events.push('request')); + page.on('response', response => events.push('response')); + page.on('requestfinished', request => events.push('requestfinished')); + await page.goto(server.EMPTY_PAGE); + expect(events).toEqual(['request', 'response', 'requestfinished']); + }); + it.skip(WEBKIT)('should support redirects', async({page, server}) => { + 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', () => { + it('should work', async({page, server}) => { + 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); + }); + it.skip(WEBKIT)('should work with request interception', async({page, server}) => { + 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({page, server}) => { + const requests = []; + page.on('request', request => requests.push(request)); + await page.goto(server.PREFIX + '/pptr.png'); + expect(requests[0].isNavigationRequest()).toBe(true); + }); + }); + + describe('Page.setExtraHTTPHeaders', function() { + it('should work', async({page, server}) => { + 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({page, server}) => { + let error = null; + try { + await page.setExtraHTTPHeaders({ 'foo': 1 }); + } catch (e) { + error = e; + } + expect(error.message).toBe('Expected value of header "foo" to be String, but "number" is found.'); + }); + }); + + // FIXME: WebKit doesn't support network interception. + describe.skip(FFOX || WEBKIT)('Page.authenticate', function() { + it('should work', async({page, server}) => { + 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({page, server}) => { + // 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({page, server}) => { + // 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); + }); + }); +}; + diff --git a/test/oopif.spec.js b/test/oopif.spec.js new file mode 100644 index 0000000000..0bc5496799 --- /dev/null +++ b/test/oopif.spec.js @@ -0,0 +1,61 @@ +/** + * 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. + */ + +module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, playwright, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('OOPIF', function() { + beforeAll(async function(state) { + state.browser = await playwright.launch(Object.assign({}, defaultBrowserOptions, { + args: (defaultBrowserOptions.args || []).concat(['--site-per-process']), + })); + }); + beforeEach(async function(state) { + state.context = await state.browser.createIncognitoBrowserContext(); + state.page = await state.context.newPage(); + }); + afterEach(async function(state) { + await state.context.close(); + state.page = null; + state.context = null; + }); + afterAll(async function(state) { + await state.browser.close(); + state.browser = null; + }); + xit('should report oopif frames', async function({page, server, context}) { + 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 function({page, server, context}) { + 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 {!Playwright.BrowserContext} context + */ +function oopifs(context) { + return context.targets().filter(target => target._targetInfo.type === 'iframe'); +} diff --git a/test/page.spec.js b/test/page.spec.js new file mode 100644 index 0000000000..ec9de5a6d9 --- /dev/null +++ b/test/page.spec.js @@ -0,0 +1,1265 @@ +/** + * 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 fs = require('fs'); +const path = require('path'); +const utils = require('./utils'); +const {waitEvent} = utils; + +module.exports.addTests = function({testRunner, expect, headless, playwright, FFOX, CHROME, WEBKIT}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('Page.close', function() { + it('should reject all promises when page is closed', async({context}) => { + const newPage = await context.newPage(); + let error = null; + await Promise.all([ + newPage.evaluate(() => new Promise(r => {})).catch(e => error = e), + newPage.close(), + ]); + expect(error.message).toContain('Protocol error'); + }); + it('should not be visible in browser.pages', async({browser}) => { + const newPage = await browser.newPage(); + expect(await browser.pages()).toContain(newPage); + await newPage.close(); + expect(await browser.pages()).not.toContain(newPage); + }); + it.skip(WEBKIT)('should run beforeunload if asked for', async({context, server}) => { + 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 (CHROME || WEBKIT) + expect(dialog.message()).toBe(''); + else + expect(dialog.message()).toBe('This page is asking you to confirm that you want to leave - data you have entered may not be saved.'); + await dialog.accept(); + await pageClosingPromise; + }); + it.skip(WEBKIT)('should *not* run beforeunload by default', async({context, server}) => { + 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({context}) => { + const newPage = await context.newPage(); + expect(newPage.isClosed()).toBe(false); + await newPage.close(); + expect(newPage.isClosed()).toBe(true); + }); + it.skip(FFOX || WEBKIT)('should terminate network waiters', async({context, server}) => { + const newPage = await context.newPage(); + const results = await Promise.all([ + newPage.waitForRequest(server.EMPTY_PAGE).catch(e => e), + newPage.waitForResponse(server.EMPTY_PAGE).catch(e => e), + 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({page, server}) => { + await Promise.all([ + page.goto('about:blank'), + utils.waitEvent(page, 'load'), + ]); + }); + }); + + describe('Async stacks', () => { + it.skip(WEBKIT)('should work', async({page, server}) => { + server.setRoute('/empty.html', (req, res) => { + res.statusCode = 204; + res.end(); + }); + let error = null; + await page.goto(server.EMPTY_PAGE).catch(e => error = e); + expect(error).not.toBe(null); + expect(error.stack).toContain(__filename); + }); + }); + + describe.skip(FFOX || WEBKIT)('Page.Events.error', function() { + it('should throw when page crashes', async({page}) => { + let error = null; + page.on('error', err => error = err); + page.goto('chrome://crash').catch(e => {}); + await waitEvent(page, 'error'); + expect(error.message).toBe('Page crashed!'); + }); + }); + + describe.skip(WEBKIT)('Page.Events.Popup', function() { + it('should work', async({page}) => { + 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({page}) => { + 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', async({page, server}) => { + 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({page, server}) => { + 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 => 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({page, server}) => { + 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); + }); + }); + + // Permissions API is not implemented in WebKit (see https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API) + describe('BrowserContext.overridePermissions', function() { + function getPermission(page, name) { + return page.evaluate(name => navigator.permissions.query({name}).then(result => result.state), name); + } + + it.skip(WEBKIT)('should be prompt by default', async({page, server, context}) => { + await page.goto(server.EMPTY_PAGE); + expect(await getPermission(page, 'geolocation')).toBe('prompt'); + }); + it.skip(WEBKIT)('should deny permission when not listed', async({page, server, context}) => { + await page.goto(server.EMPTY_PAGE); + await context.overridePermissions(server.EMPTY_PAGE, []); + expect(await getPermission(page, 'geolocation')).toBe('denied'); + }); + it.skip(WEBKIT)('should fail when bad permission is given', async({page, server, context}) => { + await page.goto(server.EMPTY_PAGE); + let error = null; + await context.overridePermissions(server.EMPTY_PAGE, ['foo']).catch(e => error = e); + expect(error.message).toBe('Unknown permission: foo'); + }); + it.skip(WEBKIT)('should grant permission when listed', async({page, server, context}) => { + await page.goto(server.EMPTY_PAGE); + await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']); + expect(await getPermission(page, 'geolocation')).toBe('granted'); + }); + it.skip(WEBKIT)('should reset permissions', async({page, server, context}) => { + await page.goto(server.EMPTY_PAGE); + await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']); + expect(await getPermission(page, 'geolocation')).toBe('granted'); + await context.clearPermissionOverrides(); + expect(await getPermission(page, 'geolocation')).toBe('prompt'); + }); + it.skip(WEBKIT)('should trigger permission onchange', async({page, server, context}) => { + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + window.events = []; + return navigator.permissions.query({name: 'geolocation'}).then(function(result) { + window.events.push(result.state); + result.onchange = function() { + window.events.push(result.state); + }; + }); + }); + expect(await page.evaluate(() => window.events)).toEqual(['prompt']); + await context.overridePermissions(server.EMPTY_PAGE, []); + expect(await page.evaluate(() => window.events)).toEqual(['prompt', 'denied']); + await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']); + expect(await page.evaluate(() => window.events)).toEqual(['prompt', 'denied', 'granted']); + await context.clearPermissionOverrides(); + expect(await page.evaluate(() => window.events)).toEqual(['prompt', 'denied', 'granted', 'prompt']); + }); + it.skip(WEBKIT)('should isolate permissions between browser contexs', async({page, server, context, browser}) => { + 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(); + }); + }); + + // FIXME: not supported in WebKit (as well as Emulation domain in general). + // It was removed from WebKit in https://webkit.org/b/126630 + describe.skip(FFOX || WEBKIT)('Page.setGeolocation', function() { + it('should work', async({page, server, context}) => { + 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({page, server, context}) => { + let error = null; + try { + await page.setGeolocation({longitude: 200, latitude: 10}); + } catch (e) { + error = e; + } + expect(error.message).toContain('Invalid longitude "200"'); + }); + }); + + describe.skip(FFOX || WEBKIT)('Page.setOfflineMode', function() { + it('should work', async({page, server}) => { + await page.setOfflineMode(true); + let error = null; + await page.goto(server.EMPTY_PAGE).catch(e => error = e); + expect(error).toBeTruthy(); + await page.setOfflineMode(false); + const response = await page.reload(); + expect(response.status()).toBe(200); + }); + it('should emulate navigator.onLine', async({page, server}) => { + 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); + }); + }); + + describe.skip(FFOX || WEBKIT)('ExecutionContext.queryObjects', function() { + it('should work', async({page, server}) => { + // Instantiate an object + await page.evaluate(() => window.set = new Set(['hello', 'world'])); + const prototypeHandle = await page.evaluateHandle(() => Set.prototype); + const objectsHandle = await page.queryObjects(prototypeHandle); + const count = await page.evaluate(objects => objects.length, objectsHandle); + expect(count).toBe(1); + const values = await page.evaluate(objects => Array.from(objects[0].values()), objectsHandle); + expect(values).toEqual(['hello', 'world']); + }); + it('should work for non-blank page', async({page, server}) => { + // Instantiate an object + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => window.set = new Set(['hello', 'world'])); + const prototypeHandle = await page.evaluateHandle(() => Set.prototype); + const objectsHandle = await page.queryObjects(prototypeHandle); + const count = await page.evaluate(objects => objects.length, objectsHandle); + expect(count).toBe(1); + }); + it('should fail for disposed handles', async({page, server}) => { + const prototypeHandle = await page.evaluateHandle(() => HTMLBodyElement.prototype); + await prototypeHandle.dispose(); + let error = null; + await page.queryObjects(prototypeHandle).catch(e => error = e); + expect(error.message).toBe('Prototype JSHandle is disposed!'); + }); + it('should fail primitive values as prototypes', async({page, server}) => { + const prototypeHandle = await page.evaluateHandle(() => 42); + let error = null; + await page.queryObjects(prototypeHandle).catch(e => error = e); + expect(error.message).toBe('Prototype JSHandle must not be referencing primitive value'); + }); + }); + + describe('Page.Events.Console', function() { + it('should work', async({page, server}) => { + 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(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({page, server}) => { + 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({page, server}) => { + 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({page, server}) => { + await page.goto('about:blank'); + const [message] = await Promise.all([ + waitEvent(page, 'console'), + page.evaluate(async url => fetch(url).catch(e => {}), server.EMPTY_PAGE) + ]); + expect(message.text()).toContain('Access-Control-Allow-Origin'); + if (CHROME || WEBKIT) + expect(message.type()).toEqual('error'); + else + expect(message.type()).toEqual('warn'); + }); + it.skip(FFOX || WEBKIT)('should have location when fetch fails', async({page, server}) => { + // 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.skip(WEBKIT)('should have location for console API calls', async({page, server}) => { + 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: 7, + columnNumber: 14, + }); + }); + // @see https://github.com/GoogleChrome/puppeteer/issues/3865 + it.skip(FFOX || WEBKIT)('should not throw when there are console messages in detached iframes', async({browser, page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.evaluate(async() => { + // 1. Create a popup that Playwright 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({page, server}) => { + page.goto('about:blank'); + await waitEvent(page, 'domcontentloaded'); + }); + }); + + describe.skip(FFOX || WEBKIT)('Page.metrics', function() { + it('should get metrics from a page', async({page, server}) => { + await page.goto('about:blank'); + const metrics = await page.metrics(); + checkMetrics(metrics); + }); + it('metrics event fired on console.timeStamp', async({page, server}) => { + const metricsPromise = new Promise(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({page, server}) => { + 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({page, server}) => { + 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({page, server}) => { + let error = null; + await page.waitForRequest(() => false, {timeout: 1}).catch(e => error = e); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it('should respect default timeout', async({page, server}) => { + let error = null; + page.setDefaultTimeout(1); + await page.waitForRequest(() => false).catch(e => error = e); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it('should work with no timeout', async({page, server}) => { + 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({page, server}) => { + 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({page, server}) => { + let error = null; + await page.waitForResponse(() => false, {timeout: 1}).catch(e => error = e); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it('should respect default timeout', async({page, server}) => { + let error = null; + page.setDefaultTimeout(1); + await page.waitForResponse(() => false).catch(e => error = e); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it('should work with predicate', async({page, server}) => { + 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({page, server}) => { + 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'); + }); + }); + + describe('Page.exposeFunction', function() { + it.skip(WEBKIT)('should work', async({page, server}) => { + await page.exposeFunction('compute', function(a, b) { + return a * b; + }); + const result = await page.evaluate(async function() { + return await compute(9, 4); + }); + expect(result).toBe(36); + }); + it.skip(WEBKIT)('should throw exception in page context', async({page, server}) => { + await page.exposeFunction('woof', function() { + throw new Error('WOOF WOOF'); + }); + const {message, stack} = await page.evaluate(async() => { + try { + await woof(); + } catch (e) { + return {message: e.message, stack: e.stack}; + } + }); + expect(message).toBe('WOOF WOOF'); + expect(stack).toContain(__filename); + }); + it.skip(WEBKIT)('should support throwing "null"', async({page, server}) => { + await page.exposeFunction('woof', function() { + throw null; + }); + const thrown = await page.evaluate(async() => { + try { + await woof(); + } catch (e) { + return e; + } + }); + expect(thrown).toBe(null); + }); + it.skip(WEBKIT)('should be callable from-inside evaluateOnNewDocument', async({page, server}) => { + let called = false; + await page.exposeFunction('woof', function() { + called = true; + }); + await page.evaluateOnNewDocument(() => woof()); + await page.reload(); + expect(called).toBe(true); + }); + it.skip(WEBKIT)('should survive navigation', async({page, server}) => { + 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 compute(9, 4); + }); + expect(result).toBe(36); + }); + it.skip(WEBKIT)('should await returned promise', async({page, server}) => { + await page.exposeFunction('compute', function(a, b) { + return Promise.resolve(a * b); + }); + + const result = await page.evaluate(async function() { + return await compute(3, 5); + }); + expect(result).toBe(15); + }); + it.skip(WEBKIT)('should work on frames', async({page, server}) => { + 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 compute(3, 5); + }); + expect(result).toBe(15); + }); + it.skip(WEBKIT)('should work on frames before navigation', async({page, server}) => { + 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 compute(3, 5); + }); + expect(result).toBe(15); + }); + it.skip(WEBKIT)('should work with complex objects', async({page, server}) => { + await page.exposeFunction('complexObject', function(a, b) { + return {x: a.x + b.x}; + }); + const result = await page.evaluate(async() => complexObject({x: 5}, {x: 2})); + expect(result.x).toBe(7); + }); + }); + + describe('Page.Events.PageError', function() { + it.skip(WEBKIT)('should fire', async({page, server}) => { + 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({page, server}) => { + 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({page, server}) => { + 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.skip(WEBKIT)('should emulate device user-agent', async({page, server}) => { + await page.goto(server.PREFIX + '/mobile.html'); + expect(await page.evaluate(() => navigator.userAgent)).not.toContain('iPhone'); + await page.setUserAgent(playwright.devices['iPhone 6'].userAgent); + expect(await page.evaluate(() => navigator.userAgent)).toContain('iPhone'); + }); + }); + + describe('Page.setContent', function() { + const expectedOutput = '
hello
'; + it('should work', async({page, server}) => { + await page.setContent('
hello
'); + const result = await page.content(); + expect(result).toBe(expectedOutput); + }); + it('should work with doctype', async({page, server}) => { + 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({page, server}) => { + const doctype = ''; + await page.setContent(`${doctype}
hello
`); + const result = await page.content(); + expect(result).toBe(`${doctype}${expectedOutput}`); + }); + it.skip(FFOX)('should respect timeout', async({page, server}) => { + const imgPath = '/img.png'; + // stall for image + server.setRoute(imgPath, (req, res) => {}); + let error = null; + await page.setContent(``, {timeout: 1}).catch(e => error = e); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it.skip(FFOX)('should respect default navigation timeout', async({page, server}) => { + page.setDefaultNavigationTimeout(1); + const imgPath = '/img.png'; + // stall for image + server.setRoute(imgPath, (req, res) => {}); + let error = null; + await page.setContent(``).catch(e => error = e); + expect(error).toBeInstanceOf(playwright.errors.TimeoutError); + }); + it.skip(FFOX)('should await resources to load', async({page, server}) => { + 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({page, server}) => { + for (let i = 0; i < 20; ++i) + await page.setContent('
yo
'); + }); + it('should work with tricky content', async({page, server}) => { + await page.setContent('
hello world
' + '\x7F'); + expect(await page.$eval('div', div => div.textContent)).toBe('hello world'); + }); + it('should work with accents', async({page, server}) => { + await page.setContent('
aberración
'); + expect(await page.$eval('div', div => div.textContent)).toBe('aberración'); + }); + it('should work with emojis', async({page, server}) => { + await page.setContent('
🐥
'); + expect(await page.$eval('div', div => div.textContent)).toBe('🐥'); + }); + it('should work with newline', async({page, server}) => { + await page.setContent('
\n
'); + expect(await page.$eval('div', div => div.textContent)).toBe('\n'); + }); + }); + + describe.skip(FFOX || WEBKIT)('Page.setBypassCSP', function() { + it('should bypass CSP meta tag', async({page, server}) => { + // Make sure CSP prohibits addScriptTag. + await page.goto(server.PREFIX + '/csp.html'); + await page.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e); + expect(await page.evaluate(() => window.__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(() => window.__injected)).toBe(42); + }); + + it('should bypass CSP header', async({page, server}) => { + // 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(e => void e); + expect(await page.evaluate(() => window.__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(() => window.__injected)).toBe(42); + }); + + it('should bypass after cross-process navigation', async({page, server}) => { + await page.setBypassCSP(true); + await page.goto(server.PREFIX + '/csp.html'); + await page.addScriptTag({content: 'window.__injected = 42;'}); + expect(await page.evaluate(() => window.__injected)).toBe(42); + + await page.goto(server.CROSS_PROCESS_PREFIX + '/csp.html'); + await page.addScriptTag({content: 'window.__injected = 42;'}); + expect(await page.evaluate(() => window.__injected)).toBe(42); + }); + it('should bypass CSP in iframes as well', async({page, server}) => { + 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(e => void e); + expect(await frame.evaluate(() => window.__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(e => void e); + expect(await frame.evaluate(() => window.__injected)).toBe(42); + } + }); + }); + + describe('Page.addScriptTag', function() { + it('should throw an error if no options are provided', async({page, server}) => { + let error = null; + try { + await page.addScriptTag('/injectedfile.js'); + } catch (e) { + error = e; + } + expect(error.message).toBe('Provide an object with a `url`, `path` or `content` property'); + }); + + it('should work with a url', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const scriptHandle = await page.addScriptTag({ url: '/injectedfile.js' }); + expect(scriptHandle.asElement()).not.toBeNull(); + expect(await page.evaluate(() => __injected)).toBe(42); + }); + + it('should work with a url and type=module', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({ url: '/es6/es6import.js', type: 'module' }); + expect(await page.evaluate(() => __es6injected)).toBe(42); + }); + + it('should work with a path and type=module', async({page, server}) => { + 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(() => __es6injected)).toBe(42); + }); + + it('should work with a content and type=module', async({page, server}) => { + 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(() => __es6injected)).toBe(42); + }); + + it('should throw an error if loading from url fail', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + let error = null; + try { + await page.addScriptTag({ url: '/nonexistfile.js' }); + } catch (e) { + error = e; + } + expect(error.message).toBe('Loading script from /nonexistfile.js failed'); + }); + + it('should work with a path', async({page, server}) => { + 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(() => __injected)).toBe(42); + }); + + it.skip(WEBKIT)('should include sourcemap when path is provided', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({ path: path.join(__dirname, 'assets/injectedfile.js') }); + const result = await page.evaluate(() => __injectedError.stack); + expect(result).toContain(path.join('assets', 'injectedfile.js')); + }); + + it('should work with content', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const scriptHandle = await page.addScriptTag({ content: 'window.__injected = 35;' }); + expect(scriptHandle.asElement()).not.toBeNull(); + expect(await page.evaluate(() => __injected)).toBe(35); + }); + + // @see https://github.com/GoogleChrome/puppeteer/issues/4840 + xit('should throw when added with content to the CSP page', async({page, server}) => { + await page.goto(server.PREFIX + '/csp.html'); + let error = null; + await page.addScriptTag({ content: 'window.__injected = 35;' }).catch(e => error = e); + expect(error).toBeTruthy(); + }); + + it('should throw when added with URL to the CSP page', async({page, server}) => { + await page.goto(server.PREFIX + '/csp.html'); + let error = null; + await page.addScriptTag({ url: server.CROSS_PROCESS_PREFIX + '/injectedfile.js' }).catch(e => error = e); + expect(error).toBeTruthy(); + }); + }); + + describe('Page.addStyleTag', function() { + it('should throw an error if no options are provided', async({page, server}) => { + let error = null; + try { + await page.addStyleTag('/injectedstyle.css'); + } catch (e) { + error = e; + } + expect(error.message).toBe('Provide an object with a `url`, `path` or `content` property'); + }); + + it('should work with a url', async({page, server}) => { + 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({page, server}) => { + await page.goto(server.EMPTY_PAGE); + let error = null; + try { + await page.addStyleTag({ url: '/nonexistfile.js' }); + } catch (e) { + error = e; + } + expect(error.message).toBe('Loading style from /nonexistfile.js failed'); + }); + + it('should work with a path', async({page, server}) => { + 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({page, server}) => { + 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 => style.innerHTML, styleHandle); + expect(styleContent).toContain(path.join('assets', 'injectedstyle.css')); + }); + + it('should work with content', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const styleHandle = await page.addStyleTag({ content: 'body { background-color: green; }' }); + expect(styleHandle.asElement()).not.toBeNull(); + expect(await page.evaluate(`window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')`)).toBe('rgb(0, 128, 0)'); + }); + + it.skip(FFOX || WEBKIT)('should throw when added with content to the CSP page', async({page, server}) => { + await page.goto(server.PREFIX + '/csp.html'); + let error = null; + await page.addStyleTag({ content: 'body { background-color: green; }' }).catch(e => error = e); + expect(error).toBeTruthy(); + }); + + it.skip(WEBKIT)('should throw when added with URL to the CSP page', async({page, server}) => { + await page.goto(server.PREFIX + '/csp.html'); + let error = null; + await page.addStyleTag({ url: server.CROSS_PROCESS_PREFIX + '/injectedstyle.css' }).catch(e => error = e); + expect(error).toBeTruthy(); + }); + }); + + describe('Page.url', function() { + it('should work', async({page, server}) => { + expect(page.url()).toBe('about:blank'); + await page.goto(server.EMPTY_PAGE); + expect(page.url()).toBe(server.EMPTY_PAGE); + }); + }); + + describe('Page.setJavaScriptEnabled', function() { + it.skip(WEBKIT)('should work', async({page, server}) => { + await page.setJavaScriptEnabled(false); + await page.goto('data:text/html, '); + let error = null; + await page.evaluate('something').catch(e => error = e); + 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() { + // FIXME: 'if-modified-since' is not set for some reason even if cache is on. + it.skip(WEBKIT)('should enable or disable the cache based on the state passed', async({page, server}) => { + await page.goto(server.PREFIX + '/cached/one-style.html'); + const [cachedRequest] = await Promise.all([ + server.waitForRequest('/cached/one-style.html'), + page.reload(), + ]); + // Rely on "if-modified-since" caching in our test server. + expect(cachedRequest.headers['if-modified-since']).not.toBe(undefined); + + await page.setCacheEnabled(false); + const [nonCachedRequest] = await Promise.all([ + server.waitForRequest('/cached/one-style.html'), + page.reload(), + ]); + expect(nonCachedRequest.headers['if-modified-since']).toBe(undefined); + }); + it.skip(WEBKIT)('should stay disabled when toggling request interception on/off', async({page, server}) => { + 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); + }); + }); + + // Printing to pdf is currently only supported in headless + describe.skip(FFOX || WEBKIT || !headless)('Page.pdf', function() { + it('should be able to save file', async({page, server}) => { + 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({page, server}) => { + await page.goto(server.PREFIX + '/title.html'); + expect(await page.title()).toBe('Woof-Woof'); + }); + }); + + describe('Page.select', function() { + it('should select single option', async({page, server}) => { + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('select', 'blue'); + expect(await page.evaluate(() => result.onInput)).toEqual(['blue']); + expect(await page.evaluate(() => result.onChange)).toEqual(['blue']); + }); + it('should select only first option', async({page, server}) => { + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('select', 'blue', 'green', 'red'); + expect(await page.evaluate(() => result.onInput)).toEqual(['blue']); + expect(await page.evaluate(() => result.onChange)).toEqual(['blue']); + }); + it.skip(FFOX)('should not throw when select causes navigation', async({page, server}) => { + await page.goto(server.PREFIX + '/input/select.html'); + await page.$eval('select', select => select.addEventListener('input', () => window.location = '/empty.html')); + await Promise.all([ + page.select('select', 'blue'), + page.waitForNavigation(), + ]); + expect(page.url()).toContain('empty.html'); + }); + it('should select multiple options', async({page, server}) => { + await page.goto(server.PREFIX + '/input/select.html'); + await page.evaluate(() => makeMultiple()); + await page.select('select', 'blue', 'green', 'red'); + expect(await page.evaluate(() => result.onInput)).toEqual(['blue', 'green', 'red']); + expect(await page.evaluate(() => result.onChange)).toEqual(['blue', 'green', 'red']); + }); + it('should respect event bubbling', async({page, server}) => { + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('select', 'blue'); + expect(await page.evaluate(() => result.onBubblingInput)).toEqual(['blue']); + expect(await page.evaluate(() => result.onBubblingChange)).toEqual(['blue']); + }); + it('should throw when element is not a element.'); + }); + it('should return [] on no matched values', async({page, server}) => { + 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({page, server}) => { + await page.goto(server.PREFIX + '/input/select.html'); + await page.evaluate(() => 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({page, server}) => { + 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({page, server}) => { + 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({page, server}) => { + await page.goto(server.PREFIX + '/input/select.html'); + await page.evaluate(() => makeMultiple()); + await page.select('select','blue','black','magenta'); + await page.select('select'); + expect(await page.$eval('select', select => Array.from(select.options).every(option => !option.selected))).toEqual(true); + }); + it('should deselect all options when passed no values for a select without multiple',async({page, server}) => { + 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 => Array.from(select.options).every(option => !option.selected))).toEqual(true); + }); + it('should throw if passed in non-strings', async({page, server}) => { + await page.setContent(''); + let error = null; + try { + await page.select('select', 12); + } catch (e) { + error = e; + } + expect(error.message).toContain('Values must be strings'); + }); + // @see https://github.com/GoogleChrome/puppeteer/issues/3327 + it.skip(FFOX || WEBKIT)('should work when re-defining top-level Event class', async({page, server}) => { + await page.goto(server.PREFIX + '/input/select.html'); + await page.evaluate(() => window.Event = null); + await page.select('select', 'blue'); + expect(await page.evaluate(() => result.onInput)).toEqual(['blue']); + expect(await page.evaluate(() => result.onChange)).toEqual(['blue']); + }); + }); + + describe.skip(FFOX || WEBKIT)('Page.fill', function() { + it('should fill textarea', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.fill('textarea', 'some value'); + expect(await page.evaluate(() => result)).toBe('some value'); + }); + it('should fill input', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.fill('input', 'some value'); + expect(await page.evaluate(() => result)).toBe('some value'); + }); + it('should throw on non-text inputs', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + for (const type of ['email', 'number', 'date']) { + await page.$eval('input', (input, type) => input.setAttribute('type', type), type); + let error = null; + await page.fill('input', '').catch(e => error = e); + expect(error.message).toContain('Cannot fill input of type'); + } + }); + it('should fill different input types', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + for (const type of ['password', 'search', 'tel', 'text', 'url']) { + await page.$eval('input', (input, type) => input.setAttribute('type', type), type); + await page.fill('input', 'text ' + type); + expect(await page.evaluate(() => result)).toBe('text ' + type); + } + }); + it('should fill contenteditable', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.fill('div[contenteditable]', 'some value'); + expect(await page.$eval('div[contenteditable]', div => div.textContent)).toBe('some value'); + }); + it('should fill elements with existing value and selection', async({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + + await page.$eval('input', input => input.value = 'value one'); + await page.fill('input', 'another value'); + expect(await page.evaluate(() => result)).toBe('another value'); + + await page.$eval('input', input => { + input.selectionStart = 1; + input.selectionEnd = 2; + }); + await page.fill('input', 'maybe this one'); + expect(await page.evaluate(() => result)).toBe('maybe this one'); + + await page.$eval('div[contenteditable]', div => { + div.innerHTML = 'some text some more text and even more text'; + const range = document.createRange(); + range.selectNodeContents(div.querySelector('span')); + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + }); + await page.fill('div[contenteditable]', 'replace with this'); + expect(await page.$eval('div[contenteditable]', div => div.textContent)).toBe('replace with this'); + }); + it('should throw when element is not an ,