diff --git a/.eslintignore b/.eslintignore index 687648ad61..11cbd52c0c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -9,6 +9,6 @@ node6-testrunner/* lib/ *.js src/generated/* -src/chromium/protocol.d.ts -src/firefox/protocol.d.ts -src/webkit/protocol.d.ts +src/chromium/protocol.ts +src/firefox/protocol.ts +src/webkit/protocol.ts diff --git a/.gitignore b/.gitignore index 61a0cef2d3..6cd316572f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ /test/output-webkit /test/test-user-data-dir* /.local-chromium/ -/.local-browser/ +/.local-firefox/ /.local-webkit/ /.dev_profile* .DS_Store @@ -15,10 +15,9 @@ package-lock.json yarn.lock /node6 /src/generated/* -/src/chromium/protocol.d.ts -/src/firefox/protocol.d.ts -/src/webkit/protocol.d.ts +/src/chromium/protocol.ts +/src/firefox/protocol.ts +/src/webkit/protocol.ts /utils/browser/playwright-web.js -/index.d.ts lib/ playwright-*.tgz diff --git a/.npmignore b/.npmignore index ae04ec7472..d800013baf 100644 --- a/.npmignore +++ b/.npmignore @@ -6,6 +6,9 @@ !lib/**/*.js # Injected files are included via lib/generated, see src/injected/README.md lib/injected/ +#types +!lib/**/*.d.ts +!index.d.ts # root for "playwright" package !index.js diff --git a/.travis.yml b/.travis.yml index 7143ca7b01..5c55d19f65 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,20 @@ language: node_js -dist: trusty +dist: bionic addons: apt: packages: # This is required to run new chrome on old trusty - - libnss3 + # - libnss3 + # This is required to run webkit + - libwoff1 + - libopus0 + - libwebp6 + - libwebpdemux2 + - libenchant1c2a + - libgudev-1.0-0 + - libsecret-1-0 + - libhyphen0 + - libgdk-pixbuf2.0-0 notifications: email: false cache: diff --git a/browser_patches/checkout_build_archive_upload.sh b/browser_patches/checkout_build_archive_upload.sh index 106a169928..14052e58d0 100755 --- a/browser_patches/checkout_build_archive_upload.sh +++ b/browser_patches/checkout_build_archive_upload.sh @@ -63,6 +63,13 @@ fi echo "-- preparing checkout" ./prepare_checkout.sh $BROWSER_NAME +cd ./$BROWSER_NAME/checkout +if ! [[ $(git rev-parse --abbrev-ref HEAD) == "playwright-build" ]]; then + echo "ERROR: Default branch is not playwright-build!" + exit 1 +fi +cd - + echo "-- cleaning" ./$BROWSER_NAME/clean.sh diff --git a/browser_patches/contributing.md b/browser_patches/contributing.md index eded253bb0..472ae32447 100644 --- a/browser_patches/contributing.md +++ b/browser_patches/contributing.md @@ -11,15 +11,16 @@ and develop from there. From the `playwright` repo, run the following command: ```sh -$ ./browser_patches/prepare_checkout.sh firefox +$ ./browser_patches/prepare_checkout.sh firefox ``` - (you can optionally pass "webkit" for a webkit checkout) +If you don't have a checkout, don't pass a path and one will be created for you in `./browser_patches/firefox/checkout` + > **NOTE:** this command downloads GBs of data. + This command will: -- create a git browser checkout at `./browser_patches/firefox/checkout` - create a `browser_upstream` remote in the checkout - create a `playwright-build` branch and apply all playwright-required patches to it. @@ -41,7 +42,7 @@ Once you're happy with the work you did in the browser-land, you want to export Assuming that you're in the root of the `playwright` repo and that your browser checkout has your feature branch checked out: ```sh -$ ./browser_patches/export.sh firefox +$ ./browser_patches/export.sh firefox ``` This script will: @@ -49,6 +50,8 @@ This script will: - update the `./browser_patches/firefox/UPSTREAM_CONFIG.sh` if necessary - bump the `./browser_patches/firefox/BUILD_NUMBER` number. +If you omit the path to your checkout, the script will assume one is located at `./browser_patches/firefox/checkout` + Send a PR to the PlayWright repo to be reviewed. ## 4. Rolling PlayWright to the new browser build @@ -62,12 +65,3 @@ $ ./browser_patches/tools/check_cdn.sh ``` As the builds appear, you can roll to a new browser version in the `./package.json` file. - - -# FAQ - -## Q: Can I reuse my other browser checkout? - -Yes, you can. For this: -- pass path to your browser checkout as a second argument to `prepare_checkout.sh` script. -- pass path to your browser checkout as a second argument to `export.sh` when exporting changes. \ No newline at end of file diff --git a/browser_patches/export.sh b/browser_patches/export.sh index d2fadd6bbc..e08e29c2eb 100755 --- a/browser_patches/export.sh +++ b/browser_patches/export.sh @@ -74,8 +74,8 @@ cd $CHECKOUT_PATH # Setting up |$REMOTE_BROWSER_UPSTREAM| remote and fetch the $BASE_BRANCH if git remote get-url $REMOTE_BROWSER_UPSTREAM >/dev/null; then - if ! [[ $(git remote get-url $REMOTE_BROWSER_UPSTREAM) == "$REMOTE_URL" ]]; then - echo "ERROR: remote $REMOTE_BROWSER_UPSTREAM is not pointng to '$REMOTE_URL'! run `prepare_checkout.sh` first" + if ! [[ $(git config --get remote.$REMOTE_BROWSER_UPSTREAM.url || echo "") == "$REMOTE_URL" ]]; then + echo "ERROR: remote $REMOTE_BROWSER_UPSTREAM is not pointing to '$REMOTE_URL'! run `prepare_checkout.sh` first" exit 1 fi else diff --git a/browser_patches/firefox/build.sh b/browser_patches/firefox/build.sh index a5aa57a609..ca64f909b5 100755 --- a/browser_patches/firefox/build.sh +++ b/browser_patches/firefox/build.sh @@ -6,15 +6,6 @@ trap "cd $(pwd -P)" EXIT cd "$(dirname $0)" cd "checkout" -BUILD_BRANCH="playwright-build" - -if ! [[ $(git rev-parse --abbrev-ref HEAD) == "$BUILD_BRANCH" ]]; then - echo "ERROR: Cannot build any branch other than $BUILD_BRANCH" - exit 1; -else - echo "-- checking git branch is $BUILD_BRANCH - 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. diff --git a/browser_patches/webkit/BUILD_NUMBER b/browser_patches/webkit/BUILD_NUMBER index 2d1420d537..8463e0903f 100644 --- a/browser_patches/webkit/BUILD_NUMBER +++ b/browser_patches/webkit/BUILD_NUMBER @@ -1 +1 @@ -1012 +1021 diff --git a/browser_patches/webkit/UPSTREAM_CONFIG.sh b/browser_patches/webkit/UPSTREAM_CONFIG.sh index 349da77075..72d8e53eff 100644 --- a/browser_patches/webkit/UPSTREAM_CONFIG.sh +++ b/browser_patches/webkit/UPSTREAM_CONFIG.sh @@ -1,3 +1,3 @@ REMOTE_URL="https://github.com/webkit/webkit" BASE_BRANCH="master" -BASE_REVISION="031545c904ac108f0063861f58a3e4e2a299b0c0" +BASE_REVISION="131efe8ad014ffa190946fea083b8f96b16f6e89" diff --git a/browser_patches/webkit/build.sh b/browser_patches/webkit/build.sh index a5ee0ae6c8..9f44a97623 100755 --- a/browser_patches/webkit/build.sh +++ b/browser_patches/webkit/build.sh @@ -6,15 +6,6 @@ trap "cd $(pwd -P)" EXIT cd "$(dirname $0)" cd "checkout" -BUILD_BRANCH="playwright-build" - -if ! [[ $(git rev-parse --abbrev-ref HEAD) == "$BUILD_BRANCH" ]]; then - echo "ERROR: Cannot build any branch other than $BUILD_BRANCH" - exit 1; -else - echo "-- checking git branch is $BUILD_BRANCH - OK" -fi - if [[ "$(uname)" == "Darwin" ]]; then ./Tools/Scripts/build-webkit --release elif [[ "$(uname)" == "Linux" ]]; then diff --git a/browser_patches/webkit/patches/0001-chore-bootstrap.patch b/browser_patches/webkit/patches/0001-chore-bootstrap.patch index 305e911d38..2390ee517c 100644 --- a/browser_patches/webkit/patches/0001-chore-bootstrap.patch +++ b/browser_patches/webkit/patches/0001-chore-bootstrap.patch @@ -1,6 +1,6 @@ -From c71917697866e90900049e7b08979fdb23b63958 Mon Sep 17 00:00:00 2001 +From ba883b0572c9da4ac17e2dc3d2f772e71f103274 Mon Sep 17 00:00:00 2001 From: Pavel Feldman -Date: Wed, 4 Dec 2019 23:24:50 -0800 +Date: Mon, 9 Dec 2019 12:19:23 -0800 Subject: [PATCH] chore: bootstrap --- @@ -8,36 +8,36 @@ Subject: [PATCH] chore: bootstrap Source/JavaScriptCore/DerivedSources.make | 4 + .../inspector/InspectorBackendDispatcher.cpp | 21 +- .../inspector/InspectorBackendDispatcher.h | 5 +- - .../inspector/InspectorTarget.h | 3 + - .../inspector/agents/InspectorTargetAgent.cpp | 46 +- + .../inspector/InspectorTarget.h | 5 + + .../inspector/agents/InspectorTargetAgent.cpp | 52 +- .../inspector/agents/InspectorTargetAgent.h | 6 +- .../inspector/protocol/Browser.json | 106 ++++ .../inspector/protocol/DOM.json | 39 ++ .../inspector/protocol/Dialog.json | 36 ++ - .../inspector/protocol/Emulation.json | 21 + + .../inspector/protocol/Emulation.json | 22 + .../inspector/protocol/Input.json | 160 ++++++ - .../inspector/protocol/Page.json | 90 +++- - .../inspector/protocol/Target.json | 18 +- + .../inspector/protocol/Page.json | 103 +++- + .../inspector/protocol/Target.json | 22 +- Source/WebCore/html/FileInputType.cpp | 6 + - .../inspector/InspectorInstrumentation.cpp | 14 +- + .../inspector/InspectorInstrumentation.cpp | 23 +- .../inspector/InspectorInstrumentation.h | 21 + - .../inspector/agents/InspectorDOMAgent.cpp | 103 ++++ - .../inspector/agents/InspectorDOMAgent.h | 2 + - .../inspector/agents/InspectorPageAgent.cpp | 509 +++++++++++++++++- - .../inspector/agents/InspectorPageAgent.h | 18 +- + .../inspector/agents/InspectorDOMAgent.cpp | 113 +++- + .../inspector/agents/InspectorDOMAgent.h | 4 + + .../agents/InspectorDOMStorageAgent.h | 1 + + .../inspector/agents/InspectorPageAgent.cpp | 534 +++++++++++++++++- + .../inspector/agents/InspectorPageAgent.h | 23 +- .../agents/page/PageRuntimeAgent.cpp | 14 +- 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 +++++++++ + .../platform/gtk/PlatformKeyboardEventGtk.cpp | 242 ++++++++ + .../libwpe/PlatformKeyboardEventLibWPE.cpp | 240 ++++++++ .../soup/NetworkStorageSessionSoup.cpp | 9 +- .../WebKit/NetworkProcess/NetworkProcess.cpp | 30 +- Source/WebKit/NetworkProcess/NetworkProcess.h | 5 + .../NetworkProcess/NetworkProcess.messages.in | 4 + 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 +- @@ -52,7 +52,7 @@ Subject: [PATCH] chore: bootstrap .../UIProcess/API/Cocoa/WKWebsiteDataStore.h | 3 +- .../UIProcess/API/Cocoa/WKWebsiteDataStore.mm | 6 + .../UIProcess/API/Cocoa/_WKBrowserInspector.h | 33 ++ - .../API/Cocoa/_WKBrowserInspector.mm | 28 + + .../API/Cocoa/_WKBrowserInspector.mm | 30 + .../API/glib/WebKitBrowserInspector.cpp | 114 ++++ .../API/glib/WebKitBrowserInspectorPrivate.h | 9 + .../UIProcess/API/glib/WebKitUIClient.cpp | 4 + @@ -64,12 +64,12 @@ Subject: [PATCH] chore: bootstrap Source/WebKit/UIProcess/API/gtk/webkit2.h | 1 + .../API/wpe/WebKitBrowserInspector.h | 54 ++ Source/WebKit/UIProcess/API/wpe/webkit.h | 1 + - .../UIProcess/BrowserInspectorController.cpp | 101 ++++ - .../UIProcess/BrowserInspectorController.h | 47 ++ + .../UIProcess/BrowserInspectorController.cpp | 116 ++++ + .../UIProcess/BrowserInspectorController.h | 55 ++ .../WebKit/UIProcess/BrowserInspectorPipe.cpp | 35 ++ .../WebKit/UIProcess/BrowserInspectorPipe.h | 16 + - .../UIProcess/BrowserInspectorTargetAgent.cpp | 83 +++ - .../UIProcess/BrowserInspectorTargetAgent.h | 35 ++ + .../UIProcess/BrowserInspectorTargetAgent.cpp | 111 ++++ + .../UIProcess/BrowserInspectorTargetAgent.h | 44 ++ .../PopUpSOAuthorizationSession.h | 4 + .../PopUpSOAuthorizationSession.mm | 1 + Source/WebKit/UIProcess/Cocoa/UIDelegate.h | 2 + @@ -79,17 +79,18 @@ Subject: [PATCH] chore: bootstrap .../UIProcess/InspectorBrowserAgentClient.h | 33 ++ .../WebKit/UIProcess/InspectorDialogAgent.cpp | 64 +++ .../WebKit/UIProcess/InspectorDialogAgent.h | 48 ++ - .../WebKit/UIProcess/InspectorTargetProxy.cpp | 18 +- - .../WebKit/UIProcess/InspectorTargetProxy.h | 11 +- + .../WebKit/UIProcess/InspectorTargetProxy.cpp | 34 +- + .../WebKit/UIProcess/InspectorTargetProxy.h | 13 +- .../WebKit/UIProcess/RemoteInspectorPipe.cpp | 132 +++++ Source/WebKit/UIProcess/RemoteInspectorPipe.h | 43 ++ .../AuthenticatorManager.cpp | 1 + - .../UIProcess/WebPageInspectorController.cpp | 56 +- - .../UIProcess/WebPageInspectorController.h | 8 + - .../WebPageInspectorEmulationAgent.cpp | 47 ++ + .../Mock/MockAuthenticatorManager.cpp | 4 +- + .../UIProcess/WebPageInspectorController.cpp | 66 ++- + .../UIProcess/WebPageInspectorController.h | 22 + + .../WebPageInspectorEmulationAgent.cpp | 48 ++ .../WebPageInspectorEmulationAgent.h | 42 ++ - .../UIProcess/WebPageInspectorInputAgent.cpp | 235 ++++++++ - .../UIProcess/WebPageInspectorInputAgent.h | 54 ++ + .../UIProcess/WebPageInspectorInputAgent.cpp | 236 ++++++++ + .../UIProcess/WebPageInspectorInputAgent.h | 57 ++ .../UIProcess/WebPageInspectorTargetProxy.cpp | 109 ++++ .../UIProcess/WebPageInspectorTargetProxy.h | 45 ++ Source/WebKit/UIProcess/WebPageProxy.cpp | 20 +- @@ -102,26 +103,26 @@ Subject: [PATCH] chore: bootstrap .../WebKit/UIProcess/ios/PageClientImplIOS.mm | 2 + .../mac/InspectorBrowserAgentClientMac.h | 29 + .../mac/InspectorBrowserAgentClientMac.mm | 54 ++ - .../WebKit/UIProcess/mac/PageClientImplMac.mm | 5 + + .../WebKit/UIProcess/mac/PageClientImplMac.h | 2 + + .../WebKit/UIProcess/mac/PageClientImplMac.mm | 21 + .../mac/WebPageInspectorEmulationAgentMac.mm | 21 + - .../mac/WebPageInspectorInputAgentMac.mm | 14 + - .../mac/WebPageInspectorTargetProxyMac.mm | 18 + + .../mac/WebPageInspectorInputAgentMac.mm | 68 +++ + .../mac/WebPageInspectorTargetProxyMac.mm | 20 + .../wpe/WebPageInspectorEmulationAgentWPE.cpp | 18 + .../wpe/WebPageInspectorInputAgentWPE.cpp | 76 +++ .../wpe/WebPageInspectorTargetProxyWPE.cpp | 18 + - .../WebKit/WebKit.xcodeproj/project.pbxproj | 59 +- + .../WebKit/WebKit.xcodeproj/project.pbxproj | 58 ++ .../WebPage/WebPageInspectorTarget.cpp | 7 + .../WebPage/WebPageInspectorTarget.h | 1 + - Source/WebKit/WebProcess/WebProcess.cpp | 3 +- Tools/MiniBrowser/gtk/BrowserWindow.h | 2 +- Tools/MiniBrowser/gtk/main.c | 28 + Tools/MiniBrowser/mac/AppDelegate.h | 14 +- - Tools/MiniBrowser/mac/AppDelegate.m | 192 ++++++- + Tools/MiniBrowser/mac/AppDelegate.m | 199 ++++++- Tools/MiniBrowser/mac/SettingsController.m | 2 +- .../mac/WK2BrowserWindowController.h | 3 + - .../mac/WK2BrowserWindowController.m | 37 +- + .../mac/WK2BrowserWindowController.m | 38 +- Tools/MiniBrowser/wpe/main.cpp | 37 ++ - 117 files changed, 4663 insertions(+), 73 deletions(-) + 118 files changed, 4909 insertions(+), 92 deletions(-) create mode 100644 Source/JavaScriptCore/inspector/protocol/Browser.json create mode 100644 Source/JavaScriptCore/inspector/protocol/Dialog.json create mode 100644 Source/JavaScriptCore/inspector/protocol/Emulation.json @@ -166,10 +167,10 @@ Subject: [PATCH] chore: bootstrap create mode 100644 Source/WebKit/UIProcess/wpe/WebPageInspectorTargetProxyWPE.cpp diff --git a/Source/JavaScriptCore/CMakeLists.txt b/Source/JavaScriptCore/CMakeLists.txt -index 0f8c4194064..a28f84c44ba 100644 +index c9a82a7854f..6e2aff19e20 100644 --- a/Source/JavaScriptCore/CMakeLists.txt +++ b/Source/JavaScriptCore/CMakeLists.txt -@@ -1143,16 +1143,20 @@ set(JavaScriptCore_INSPECTOR_DOMAINS +@@ -1141,16 +1141,20 @@ set(JavaScriptCore_INSPECTOR_DOMAINS ${JAVASCRIPTCORE_DIR}/inspector/protocol/Animation.json ${JAVASCRIPTCORE_DIR}/inspector/protocol/ApplicationCache.json ${JAVASCRIPTCORE_DIR}/inspector/protocol/Audit.json @@ -191,10 +192,10 @@ index 0f8c4194064..a28f84c44ba 100644 ${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..ca6ef5f8d07 100644 +index 3866e2bcb37..c3b0dab1cc4 100644 --- a/Source/JavaScriptCore/DerivedSources.make +++ b/Source/JavaScriptCore/DerivedSources.make -@@ -238,16 +238,20 @@ INSPECTOR_DOMAINS := \ +@@ -239,16 +239,20 @@ INSPECTOR_DOMAINS := \ $(JavaScriptCore)/inspector/protocol/Animation.json \ $(JavaScriptCore)/inspector/protocol/ApplicationCache.json \ $(JavaScriptCore)/inspector/protocol/Audit.json \ @@ -320,28 +321,32 @@ index 95d9d81188e..6f96f174dff 100644 // 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 +index 4b95964db4d..e9a8079d513 100644 --- a/Source/JavaScriptCore/inspector/InspectorTarget.h +++ b/Source/JavaScriptCore/inspector/InspectorTarget.h -@@ -45,6 +45,7 @@ public: +@@ -45,8 +45,11 @@ 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 String oldTargetID() const { return String(); } ++ virtual String openerID() const { return String(); } + bool isPaused() const { return m_isPaused; } + void pause(); + void resume(); +@@ -56,6 +59,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 + private: + WTF::Function m_resumeCallback; diff --git a/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.cpp b/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.cpp -index 1177090fc18..764b62c727c 100644 +index 8fcb5a1e557..6363ca2d549 100644 --- a/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.cpp +++ b/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.cpp @@ -30,11 +30,12 @@ @@ -358,7 +363,7 @@ index 1177090fc18..764b62c727c 100644 { } -@@ -65,6 +66,28 @@ void InspectorTargetAgent::sendMessageToTarget(ErrorString& errorString, const S +@@ -87,6 +88,28 @@ void InspectorTargetAgent::sendMessageToTarget(ErrorString& errorString, const S target->sendMessageToTargetBackend(message); } @@ -387,7 +392,7 @@ index 1177090fc18..764b62c727c 100644 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 +@@ -109,16 +132,23 @@ static Protocol::Target::TargetInfo::Type targetTypeToProtocolType(InspectorTarg return Protocol::Target::TargetInfo::Type::Page; } @@ -399,15 +404,22 @@ index 1177090fc18..764b62c727c 100644 .setType(targetTypeToProtocolType(target.type())) + .setUrl(target.url()) .release(); - if (target.isProvisional()) +- if (target.isProvisional()) ++ if (target.isProvisional()) { result->setIsProvisional(true); ++ result->setOldTargetId(target.oldTargetID()); ++ } + if (target.isPaused()) + result->setIsPaused(true); + if (!browserContextID.isEmpty()) + result->setBrowserContextId(browserContextID); ++ if (!target.openerID().isEmpty()) ++ result->setOpenerId(target.openerID()); return result; } -@@ -108,7 +134,7 @@ void InspectorTargetAgent::targetCreated(InspectorTarget& target) - +@@ -134,7 +164,7 @@ void InspectorTargetAgent::targetCreated(InspectorTarget& target) + target.pause(); target.connect(connectionType()); - m_frontendDispatcher->targetCreated(buildTargetInfoObject(target)); @@ -415,7 +427,7 @@ index 1177090fc18..764b62c727c 100644 } void InspectorTargetAgent::targetDestroyed(InspectorTarget& target) -@@ -135,6 +161,18 @@ void InspectorTargetAgent::didCommitProvisionalTarget(const String& oldTargetID, +@@ -159,6 +189,18 @@ void InspectorTargetAgent::didCommitProvisionalTarget(const String& oldTargetID, m_frontendDispatcher->didCommitProvisionalTarget(oldTargetID, committedTargetID); } @@ -434,7 +446,7 @@ index 1177090fc18..764b62c727c 100644 FrontendChannel::ConnectionType InspectorTargetAgent::connectionType() const { return m_router.hasLocalFrontend() ? Inspector::FrontendChannel::ConnectionType::Local : Inspector::FrontendChannel::ConnectionType::Remote; -@@ -144,7 +182,7 @@ void InspectorTargetAgent::connectToTargets() +@@ -168,7 +210,7 @@ void InspectorTargetAgent::connectToTargets() { for (InspectorTarget* target : m_targets.values()) { target->connect(connectionType()); @@ -444,7 +456,7 @@ index 1177090fc18..764b62c727c 100644 } diff --git a/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.h b/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.h -index 38cb318986b..4287e05e559 100644 +index 1eb7abb2fa2..5a71d29af64 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 @@ -456,9 +468,9 @@ index 38cb318986b..4287e05e559 100644 ~InspectorTargetAgent() override; // InspectorAgentBase -@@ -50,11 +50,14 @@ public: - - // TargetBackendDispatcherHandler +@@ -52,11 +52,14 @@ public: + void setPauseOnStart(ErrorString&, bool pauseOnStart) override; + void resume(ErrorString&, const String& targetId) override; void sendMessageToTarget(ErrorString&, const String& targetId, const String& message) final; + void activate(ErrorString&, const String& targetId) override; + void close(ErrorString&, const String& targetId) override; @@ -471,14 +483,14 @@ index 38cb318986b..4287e05e559 100644 // Target messages. void sendMessageFromTargetToFrontend(const String& targetId, const String& message); -@@ -68,6 +71,7 @@ private: +@@ -70,6 +73,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 }; - }; + bool m_shouldPauseOnStart { false }; diff --git a/Source/JavaScriptCore/inspector/protocol/Browser.json b/Source/JavaScriptCore/inspector/protocol/Browser.json new file mode 100644 index 00000000000..063e5e1346a @@ -692,10 +704,10 @@ index 00000000000..79edea03fed +} diff --git a/Source/JavaScriptCore/inspector/protocol/Emulation.json b/Source/JavaScriptCore/inspector/protocol/Emulation.json new file mode 100644 -index 00000000000..af0f39e5249 +index 00000000000..759390956ea --- /dev/null +++ b/Source/JavaScriptCore/inspector/protocol/Emulation.json -@@ -0,0 +1,21 @@ +@@ -0,0 +1,22 @@ +{ + "domain": "Emulation", + "availability": ["web"], @@ -705,7 +717,8 @@ index 00000000000..af0f39e5249 + "description": "Overrides device metrics with provided values.", + "parameters": [ + { "name": "width", "type": "integer" }, -+ { "name": "height", "type": "integer" } ++ { "name": "height", "type": "integer" }, ++ { "name": "deviceScaleFactor", "type": "number" } + ] + }, + { @@ -884,7 +897,7 @@ index 00000000000..79bbe73b0df + ] +} diff --git a/Source/JavaScriptCore/inspector/protocol/Page.json b/Source/JavaScriptCore/inspector/protocol/Page.json -index 367d1f235a8..b2ed9177528 100644 +index 367d1f235a8..6800d524cc9 100644 --- a/Source/JavaScriptCore/inspector/protocol/Page.json +++ b/Source/JavaScriptCore/inspector/protocol/Page.json @@ -108,6 +108,40 @@ @@ -950,7 +963,7 @@ index 367d1f235a8..b2ed9177528 100644 ] }, { -@@ -288,6 +331,27 @@ +@@ -288,19 +331,49 @@ "returns": [ { "name": "data", "type": "string", "description": "Base64-encoded web archive." } ] @@ -974,11 +987,35 @@ index 367d1f235a8..b2ed9177528 100644 + "description": "Intercepts file chooser dialog", + "parameters": [ + { "name": "enabled", "type": "boolean", "description": "True to enable." } ++ ] ++ }, ++ { ++ "name": "setDefaultBackgroundColorOverride", ++ "description": "Sets or clears an override of the default background color of the frame. This override is used if the content does not specify one.", ++ "parameters": [ ++ { "name": "color", "$ref": "DOM.RGBAColor", "optional": true, "description": "RGBA of the default background color. If not specified, any existing override will be cleared." } + ] } ], "events": [ -@@ -346,12 +410,36 @@ + { + "name": "domContentEventFired", + "parameters": [ +- { "name": "timestamp", "type": "number" } ++ { "name": "timestamp", "type": "number" }, ++ { "name": "frameId", "$ref": "Network.FrameId", "description": "Id of the frame that has fired DOMContentLoaded event." } + ] + }, + { + "name": "loadEventFired", + "parameters": [ +- { "name": "timestamp", "type": "number" } ++ { "name": "timestamp", "type": "number" }, ++ { "name": "frameId", "$ref": "Network.FrameId", "description": "Id of the frame that has fired load event." } + ] + }, + { +@@ -346,12 +419,36 @@ { "name": "frameId", "$ref": "Network.FrameId", "description": "Id of the frame that has cleared its scheduled navigation." } ] }, @@ -1016,21 +1053,25 @@ index 367d1f235a8..b2ed9177528 100644 ] } diff --git a/Source/JavaScriptCore/inspector/protocol/Target.json b/Source/JavaScriptCore/inspector/protocol/Target.json -index 240cd42e67e..f635c67ef3f 100644 +index 52920cded24..30bcc1d463e 100644 --- a/Source/JavaScriptCore/inspector/protocol/Target.json +++ b/Source/JavaScriptCore/inspector/protocol/Target.json -@@ -10,7 +10,9 @@ +@@ -10,8 +10,12 @@ "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": "Whether this is a provisional page target." }, +- { "name": "isPaused", "type": "boolean", "optional": true, "description": "Whether the target is paused on start and has to be explicitely resumed by inspector." } + { "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": "oldTargetId", "type": "string", "optional": true, "description": "Unique identifier of the target which is going to be replaced if this target is committed. Only set for provisional targets." }, ++ { "name": "openerId", "type": "string", "optional": true, "description": "Unique identifier of the opening target. Only set for pages created by window.open()." }, ++ { "name": "isPaused", "type": "boolean", "optional": true, "description": "Whether the target is paused on start and has to be explicitely resumed by inspector." }, ++ { "name": "url", "type": "string" }, + { "name": "browserContextId", "$ref": "Browser.ContextID", "optional": true } ] } ], -@@ -22,6 +24,20 @@ +@@ -37,6 +41,20 @@ { "name": "targetId", "type": "string" }, { "name": "message", "type": "string", "description": "JSON Inspector Protocol message (command) to be dispatched on the backend." } ] @@ -1076,7 +1117,7 @@ index 4e41fd3f807..1f7be602cb2 100644 return; diff --git a/Source/WebCore/inspector/InspectorInstrumentation.cpp b/Source/WebCore/inspector/InspectorInstrumentation.cpp -index cb6ed9f6c84..458def75277 100644 +index cb6ed9f6c84..4402d67463d 100644 --- a/Source/WebCore/inspector/InspectorInstrumentation.cpp +++ b/Source/WebCore/inspector/InspectorInstrumentation.cpp @@ -121,7 +121,7 @@ static Frame* frameForScriptExecutionContext(ScriptExecutionContext& context) @@ -1088,7 +1129,31 @@ index cb6ed9f6c84..458def75277 100644 return; if (auto* pageDebuggerAgent = instrumentingAgents.pageDebuggerAgent()) -@@ -783,6 +783,12 @@ void InspectorInstrumentation::frameClearedScheduledNavigationImpl(Instrumenting +@@ -656,20 +656,17 @@ void InspectorInstrumentation::didReceiveScriptResponseImpl(InstrumentingAgents& + + void InspectorInstrumentation::domContentLoadedEventFiredImpl(InstrumentingAgents& instrumentingAgents, Frame& frame) + { +- if (!frame.isMainFrame()) +- return; +- + if (InspectorPageAgent* pageAgent = instrumentingAgents.inspectorPageAgent()) +- pageAgent->domContentEventFired(); ++ pageAgent->domContentEventFired(frame); + } + + void InspectorInstrumentation::loadEventFiredImpl(InstrumentingAgents& instrumentingAgents, Frame* frame) + { +- if (!frame || !frame->isMainFrame()) ++ if (!frame) + return; + + if (InspectorPageAgent* pageAgent = instrumentingAgents.inspectorPageAgent()) +- pageAgent->loadEventFired(); ++ pageAgent->loadEventFired(*frame); + } + + void InspectorInstrumentation::frameDetachedFromParentImpl(InstrumentingAgents& instrumentingAgents, Frame& frame) +@@ -783,6 +780,12 @@ void InspectorInstrumentation::frameClearedScheduledNavigationImpl(Instrumenting inspectorPageAgent->frameClearedScheduledNavigation(frame); } @@ -1101,7 +1166,7 @@ index cb6ed9f6c84..458def75277 100644 void InspectorInstrumentation::defaultAppearanceDidChangeImpl(InstrumentingAgents& instrumentingAgents, bool useDarkAppearance) { if (InspectorPageAgent* inspectorPageAgent = instrumentingAgents.inspectorPageAgent()) -@@ -1251,6 +1257,12 @@ void InspectorInstrumentation::renderLayerDestroyedImpl(InstrumentingAgents& ins +@@ -1251,6 +1254,12 @@ void InspectorInstrumentation::renderLayerDestroyedImpl(InstrumentingAgents& ins layerTreeAgent->renderLayerDestroyed(renderLayer); } @@ -1189,7 +1254,7 @@ index 6698431f316..486a6781d81 100644 { return context ? instrumentingAgentsForContext(*context) : nullptr; diff --git a/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp b/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp -index aecc79bc0ca..57ce50c1f94 100644 +index aecc79bc0ca..71f8863378b 100644 --- a/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp +++ b/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp @@ -61,12 +61,16 @@ @@ -1223,16 +1288,40 @@ index aecc79bc0ca..57ce50c1f94 100644 #include "StaticNodeList.h" #include "StyleProperties.h" #include "StyleResolver.h" -@@ -1475,6 +1481,61 @@ void InspectorDOMAgent::setInspectedNode(ErrorString& errorString, int nodeId) +@@ -128,7 +134,8 @@ using namespace HTMLNames; + static const size_t maxTextSize = 10000; + static const UChar ellipsisUChar[] = { 0x2026, 0 }; + +-static Color parseColor(const JSON::Object* colorObject) ++// static ++Color InspectorDOMAgent::parseColor(const JSON::Object* colorObject) + { + if (!colorObject) + return Color::transparent; +@@ -157,7 +164,7 @@ static Color parseConfigColor(const String& fieldName, const JSON::Object* confi + RefPtr colorObject; + configObject->getObject(fieldName, colorObject); + +- return parseColor(colorObject.get()); ++ return InspectorDOMAgent::parseColor(colorObject.get()); + } + + static bool parseQuad(const JSON::Array& quadArray, FloatQuad* quad) +@@ -1475,6 +1482,66 @@ void InspectorDOMAgent::setInspectedNode(ErrorString& errorString, int nodeId) m_suppressEventListenerChangedEvent = false; } -+static void frameQuadToViewport(const FrameView* containingView, FloatQuad& quad) ++static FloatPoint contentsToRootView(FrameView& containingView, const FloatPoint& point) +{ -+ quad.setP1(containingView->contentsToRootView(quad.p1())); -+ quad.setP2(containingView->contentsToRootView(quad.p2())); -+ quad.setP3(containingView->contentsToRootView(quad.p3())); -+ quad.setP4(containingView->contentsToRootView(quad.p4())); ++ return containingView.convertToRootView(point - toFloatSize(containingView.documentScrollPositionRelativeToViewOrigin())); ++} ++ ++static void frameQuadToViewport(FrameView& containingView, FloatQuad& quad) ++{ ++ quad.setP1(contentsToRootView(containingView, quad.p1())); ++ quad.setP2(contentsToRootView(containingView, quad.p2())); ++ quad.setP3(contentsToRootView(containingView, quad.p3())); ++ quad.setP4(contentsToRootView(containingView, quad.p4())); +} + +static RefPtr buildObjectForQuad(const FloatQuad& quad) @@ -1261,31 +1350,31 @@ index aecc79bc0ca..57ce50c1f94 100644 +{ + Node* node = nodeForObjectId(objectId); + if (!node) { -+ error = "Node not found"; ++ error = "Node not found"_s; + return; + } + RenderObject* renderer = node->renderer(); + if (!renderer) { -+ error = "Node doesn't have renderer"; ++ error = "Node doesn't have renderer"_s; + return; + } + Frame* containingFrame = renderer->document().frame(); -+ if (!containingFrame) { -+ error = "No containing frame"; ++ FrameView* containingView = containingFrame ? containingFrame->view() : nullptr; ++ if (!containingView) { ++ error = "Internal error: no containing view"_s; + return; + } -+ FrameView* containingView = containingFrame->view(); + Vector quads; + renderer->absoluteQuads(quads); + for (auto& quad : quads) -+ frameQuadToViewport(containingView, quad); ++ frameQuadToViewport(*containingView, quad); + out_quads = buildArrayOfQuads(quads); +} + void InspectorDOMAgent::resolveNode(ErrorString& errorString, int nodeId, const String* objectGroup, RefPtr& result) { String objectGroupName = objectGroup ? *objectGroup : emptyString(); -@@ -2686,4 +2747,46 @@ void InspectorDOMAgent::setAllowEditingUserAgentShadowTrees(ErrorString&, bool a +@@ -2686,4 +2753,46 @@ void InspectorDOMAgent::setAllowEditingUserAgentShadowTrees(ErrorString&, bool a m_allowEditingUserAgentShadowTrees = allow; } @@ -1333,10 +1422,26 @@ index aecc79bc0ca..57ce50c1f94 100644 + } // namespace WebCore diff --git a/Source/WebCore/inspector/agents/InspectorDOMAgent.h b/Source/WebCore/inspector/agents/InspectorDOMAgent.h -index 51639abeb84..16080f2c017 100644 +index 51639abeb84..0ed9a1d80d5 100644 --- a/Source/WebCore/inspector/agents/InspectorDOMAgent.h +++ b/Source/WebCore/inspector/agents/InspectorDOMAgent.h -@@ -148,6 +148,8 @@ public: +@@ -54,6 +54,7 @@ namespace WebCore { + + class AXCoreObject; + class CharacterData; ++class Color; + class DOMEditor; + class Document; + class Element; +@@ -88,6 +89,7 @@ public: + static String toErrorString(Exception&&); + + static String documentURLString(Document*); ++ static Color parseColor(const JSON::Object*); + + // We represent embedded doms as a part of the same hierarchy. Hence we treat children of frame owners differently. + // We also skip whitespace text nodes conditionally. Following methods encapsulate these specifics. +@@ -148,6 +150,8 @@ public: void focus(ErrorString&, int nodeId) override; void setInspectedNode(ErrorString&, int nodeId) override; void setAllowEditingUserAgentShadowTrees(ErrorString&, bool allow) final; @@ -1345,8 +1450,20 @@ index 51639abeb84..16080f2c017 100644 // InspectorInstrumentation int identifierForNode(Node&); +diff --git a/Source/WebCore/inspector/agents/InspectorDOMStorageAgent.h b/Source/WebCore/inspector/agents/InspectorDOMStorageAgent.h +index b578660fbb3..a7c968bc9f8 100644 +--- a/Source/WebCore/inspector/agents/InspectorDOMStorageAgent.h ++++ b/Source/WebCore/inspector/agents/InspectorDOMStorageAgent.h +@@ -40,6 +40,7 @@ class DOMStorageFrontendDispatcher; + + namespace WebCore { + ++class Color; + class Frame; + class Page; + class SecurityOrigin; diff --git a/Source/WebCore/inspector/agents/InspectorPageAgent.cpp b/Source/WebCore/inspector/agents/InspectorPageAgent.cpp -index f2e228b7f74..1e6ef4eec98 100644 +index f2e228b7f74..3484cb0d9cd 100644 --- a/Source/WebCore/inspector/agents/InspectorPageAgent.cpp +++ b/Source/WebCore/inspector/agents/InspectorPageAgent.cpp @@ -32,6 +32,8 @@ @@ -1432,7 +1549,26 @@ index f2e228b7f74..1e6ef4eec98 100644 } void InspectorPageAgent::overrideUserAgent(ErrorString&, const String* value) -@@ -691,6 +713,7 @@ void InspectorPageAgent::loadEventFired() +@@ -678,19 +700,21 @@ void InspectorPageAgent::setShowPaintRects(ErrorString&, bool show) + m_overlay->setShowPaintRects(show); + } + +-void InspectorPageAgent::domContentEventFired() ++void InspectorPageAgent::domContentEventFired(Frame& frame) + { +- m_isFirstLayoutAfterOnLoad = true; +- m_frontendDispatcher->domContentEventFired(timestamp()); ++ if (frame.isMainFrame()) ++ m_isFirstLayoutAfterOnLoad = true; ++ m_frontendDispatcher->domContentEventFired(timestamp(), frameId(&frame)); + } + +-void InspectorPageAgent::loadEventFired() ++void InspectorPageAgent::loadEventFired(Frame& frame) + { +- m_frontendDispatcher->loadEventFired(timestamp()); ++ m_frontendDispatcher->loadEventFired(timestamp(), frameId(&frame)); + } void InspectorPageAgent::frameNavigated(Frame& frame) { @@ -1440,7 +1576,7 @@ index f2e228b7f74..1e6ef4eec98 100644 m_frontendDispatcher->frameNavigated(buildObjectForFrame(&frame)); } -@@ -761,6 +784,12 @@ void InspectorPageAgent::frameClearedScheduledNavigation(Frame& frame) +@@ -761,6 +785,12 @@ void InspectorPageAgent::frameClearedScheduledNavigation(Frame& frame) m_frontendDispatcher->frameClearedScheduledNavigation(frameId(&frame)); } @@ -1453,7 +1589,7 @@ index f2e228b7f74..1e6ef4eec98 100644 void InspectorPageAgent::defaultAppearanceDidChange(bool useDarkAppearance) { m_frontendDispatcher->defaultAppearanceDidChange(useDarkAppearance ? Inspector::Protocol::Page::Appearance::Dark : Inspector::Protocol::Page::Appearance::Light); -@@ -815,6 +844,25 @@ void InspectorPageAgent::didRecalculateStyle() +@@ -815,6 +845,25 @@ void InspectorPageAgent::didRecalculateStyle() m_overlay->update(); } @@ -1479,7 +1615,7 @@ index f2e228b7f74..1e6ef4eec98 100644 Ref InspectorPageAgent::buildObjectForFrame(Frame* frame) { ASSERT_ARG(frame, frame); -@@ -986,4 +1034,455 @@ void InspectorPageAgent::archive(ErrorString& errorString, String* data) +@@ -986,4 +1035,469 @@ void InspectorPageAgent::archive(ErrorString& errorString, String* data) #endif } @@ -1862,7 +1998,7 @@ index f2e228b7f74..1e6ef4eec98 100644 + axNode->setPressed(Inspector::Protocol::Page::AXNode::Pressed::Mixed); + break; + } -+ } ++ } + unsigned level = axObject->hierarchicalLevel() ? axObject->hierarchicalLevel() : axObject->headingLevel(); + if (level) + axNode->setLevel(level); @@ -1878,11 +2014,11 @@ index f2e228b7f74..1e6ef4eec98 100644 + String invalidValue = axObject->invalidStatus(); + if (invalidValue != "false") { + if (invalidValue == "grammar") -+ axNode->setInvalid(Inspector::Protocol::Page::AXNode::Invalid::Grammar); ++ axNode->setInvalid(Inspector::Protocol::Page::AXNode::Invalid::Grammar); + else if (invalidValue == "spelling") -+ axNode->setInvalid(Inspector::Protocol::Page::AXNode::Invalid::Spelling); ++ axNode->setInvalid(Inspector::Protocol::Page::AXNode::Invalid::Spelling); + else // Future versions of ARIA may allow additional truthy values. Ex. format, order, or size. -+ axNode->setInvalid(Inspector::Protocol::Page::AXNode::Invalid::True); ++ axNode->setInvalid(Inspector::Protocol::Page::AXNode::Invalid::True); + } + switch (axObject->orientation()) { + case AccessibilityOrientation::Undefined: @@ -1933,10 +2069,24 @@ index f2e228b7f74..1e6ef4eec98 100644 +void InspectorPageAgent::setInterceptFileChooserDialog(ErrorString&, bool enabled) { + m_interceptFileChooserDialog = enabled; +} ++ ++void InspectorPageAgent::setDefaultBackgroundColorOverride(ErrorString& errorString, const JSON::Object* color) ++{ ++ FrameView* view = m_inspectedPage.mainFrame().view(); ++ if (!view) { ++ errorString = "Internal error: No frame view to set color two"_s; ++ return; ++ } ++ if (!color) { ++ view->updateBackgroundRecursively(Optional()); ++ return; ++ } ++ view->updateBackgroundRecursively(InspectorDOMAgent::parseColor(color)); ++} + } // namespace WebCore diff --git a/Source/WebCore/inspector/agents/InspectorPageAgent.h b/Source/WebCore/inspector/agents/InspectorPageAgent.h -index 4fd8c0b1016..eb18b0fc48e 100644 +index 4fd8c0b1016..17a92a83f4c 100644 --- a/Source/WebCore/inspector/agents/InspectorPageAgent.h +++ b/Source/WebCore/inspector/agents/InspectorPageAgent.h @@ -40,10 +40,15 @@ @@ -1966,7 +2116,7 @@ index 4fd8c0b1016..eb18b0fc48e 100644 void overrideUserAgent(ErrorString&, const String* value) override; void overrideSetting(ErrorString&, const String& setting, const bool* value) override; void getCookies(ErrorString&, RefPtr>& cookies) override; -@@ -113,8 +120,11 @@ public: +@@ -113,12 +120,16 @@ public: void getCompositingBordersVisible(ErrorString&, bool* out_param) override; void setCompositingBordersVisible(ErrorString&, bool) override; void snapshotNode(ErrorString&, int nodeId, String* outDataURL) override; @@ -1976,10 +2126,17 @@ index 4fd8c0b1016..eb18b0fc48e 100644 + void insertText(ErrorString&, const String& text) override; + void accessibilitySnapshot(ErrorString&, RefPtr& out_axNode) override; + void setInterceptFileChooserDialog(ErrorString&, bool enabled) override; ++ void setDefaultBackgroundColorOverride(ErrorString&, const JSON::Object*) override; // InspectorInstrumentation - void domContentEventFired(); -@@ -126,6 +136,7 @@ public: +- void domContentEventFired(); +- void loadEventFired(); ++ void domContentEventFired(Frame&); ++ void loadEventFired(Frame&); + void frameNavigated(Frame&); + void frameDetached(Frame&); + void loaderDetachedFromFrame(DocumentLoader&); +@@ -126,6 +137,7 @@ public: void frameStoppedLoading(Frame&); void frameScheduledNavigation(Frame&, Seconds delay); void frameClearedScheduledNavigation(Frame&); @@ -1987,7 +2144,7 @@ index 4fd8c0b1016..eb18b0fc48e 100644 void defaultAppearanceDidChange(bool useDarkAppearance); void applyUserAgentOverride(String&); void applyEmulatedMedia(String&); -@@ -134,6 +145,7 @@ public: +@@ -134,6 +146,7 @@ public: void didLayout(); void didScroll(); void didRecalculateStyle(); @@ -1995,7 +2152,7 @@ index 4fd8c0b1016..eb18b0fc48e 100644 Frame* frameForId(const String& frameId); WEBCORE_EXPORT String frameId(Frame*); -@@ -153,6 +165,7 @@ private: +@@ -153,6 +166,7 @@ private: RefPtr m_backendDispatcher; Page& m_inspectedPage; @@ -2003,7 +2160,7 @@ index 4fd8c0b1016..eb18b0fc48e 100644 InspectorClient* m_client { nullptr }; InspectorOverlay* m_overlay { nullptr }; -@@ -165,6 +178,7 @@ private: +@@ -165,6 +179,7 @@ private: String m_bootstrapScript; bool m_isFirstLayoutAfterOnLoad { false }; bool m_showPaintRects { false }; @@ -2047,7 +2204,7 @@ index 8c4a104da04..3dc08926a75 100644 } } diff --git a/Source/WebCore/loader/FrameLoader.cpp b/Source/WebCore/loader/FrameLoader.cpp -index 9b4211b4212..5881bd624df 100644 +index e105be0ba92..2557eacc4b7 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 @@ -2071,10 +2228,10 @@ index 9c58b06f4c4..3d624733c36 100644 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 +index 16b3719f77d..d96fd15db01 100644 --- a/Source/WebCore/platform/PlatformKeyboardEvent.h +++ b/Source/WebCore/platform/PlatformKeyboardEvent.h -@@ -147,6 +147,7 @@ namespace WebCore { +@@ -132,6 +132,7 @@ namespace WebCore { static String keyCodeForHardwareKeyCode(unsigned); static String keyIdentifierForGdkKeyCode(unsigned); static int windowsKeyCodeForGdkKeyCode(unsigned); @@ -2082,7 +2239,7 @@ index 4aaf5bde32b..e9c047d4f26 100644 static String singleCharacterString(unsigned); static bool modifiersContainCapsLock(unsigned); #endif -@@ -156,6 +157,7 @@ namespace WebCore { +@@ -141,6 +142,7 @@ namespace WebCore { static String keyCodeForHardwareKeyCode(unsigned); static String keyIdentifierForWPEKeyCode(unsigned); static int windowsKeyCodeForWPEKeyCode(unsigned); @@ -2624,7 +2781,7 @@ index dfc490d2a0b..0bcd1d98ad8 100644 Vector NetworkStorageSession::getCookies(const URL& url) diff --git a/Source/WebKit/NetworkProcess/NetworkProcess.cpp b/Source/WebKit/NetworkProcess/NetworkProcess.cpp -index 5f23cc66aa1..488c3c3bbae 100644 +index 932357e966a..a5858700a7a 100644 --- a/Source/WebKit/NetworkProcess/NetworkProcess.cpp +++ b/Source/WebKit/NetworkProcess/NetworkProcess.cpp @@ -26,7 +26,6 @@ @@ -2635,8 +2792,8 @@ index 5f23cc66aa1..488c3c3bbae 100644 #include "ArgumentCoders.h" #include "Attachment.h" #include "AuthenticationManager.h" -@@ -580,6 +579,35 @@ void NetworkProcess::destroySession(PAL::SessionID sessionID) - m_storageQuotaManagers.remove(sessionID); +@@ -561,6 +560,35 @@ void NetworkProcess::destroySession(PAL::SessionID sessionID) + m_storageManagerSet->remove(sessionID); } +void NetworkProcess::getAllCookies(PAL::SessionID sessionID, CompletionHandler&&)>&& completionHandler) @@ -2672,7 +2829,7 @@ index 5f23cc66aa1..488c3c3bbae 100644 void NetworkProcess::dumpResourceLoadStatistics(PAL::SessionID sessionID, CompletionHandler&& completionHandler) { diff --git a/Source/WebKit/NetworkProcess/NetworkProcess.h b/Source/WebKit/NetworkProcess/NetworkProcess.h -index f62f01d380f..e33439eb278 100644 +index 4d4add00ce3..c27f9ffe6be 100644 --- a/Source/WebKit/NetworkProcess/NetworkProcess.h +++ b/Source/WebKit/NetworkProcess/NetworkProcess.h @@ -74,6 +74,7 @@ class SessionID; @@ -2695,10 +2852,10 @@ index f62f01d380f..e33439eb278 100644 void clearPrevalentResource(PAL::SessionID, const RegistrableDomain&, CompletionHandler&&); void clearUserInteraction(PAL::SessionID, const RegistrableDomain&, CompletionHandler&&); diff --git a/Source/WebKit/NetworkProcess/NetworkProcess.messages.in b/Source/WebKit/NetworkProcess/NetworkProcess.messages.in -index 3677677480b..2873e1c0f59 100644 +index 0257d8d23ef..0d573802a70 100644 --- a/Source/WebKit/NetworkProcess/NetworkProcess.messages.in +++ b/Source/WebKit/NetworkProcess/NetworkProcess.messages.in -@@ -79,6 +79,10 @@ messages -> NetworkProcess LegacyReceiver { +@@ -80,6 +80,10 @@ messages -> NetworkProcess LegacyReceiver { PrepareToSuspend(bool isSuspensionImminent) -> () Async ProcessDidResume() @@ -2722,7 +2879,7 @@ index 898e30b370d..74945e06fac 100644 #include #include diff --git a/Source/WebKit/Shared/NativeWebKeyboardEvent.h b/Source/WebKit/Shared/NativeWebKeyboardEvent.h -index 6f4e29b7c65..9dd287efc40 100644 +index 05938ef3564..b050b30281c 100644 --- a/Source/WebKit/Shared/NativeWebKeyboardEvent.h +++ b/Source/WebKit/Shared/NativeWebKeyboardEvent.h @@ -34,6 +34,7 @@ @@ -2744,23 +2901,8 @@ index 6f4e29b7c65..9dd287efc40 100644 #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 0fa557e9faa..db299d91de3 100644 ---- a/Source/WebKit/Shared/NativeWebMouseEvent.h -+++ b/Source/WebKit/Shared/NativeWebMouseEvent.h -@@ -56,6 +56,10 @@ namespace WebKit { - - class NativeWebMouseEvent : public WebMouseEvent { - public: -+ 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) -+ { -+ } - #if USE(APPKIT) - NativeWebMouseEvent(NSEvent *, NSEvent *lastPressureEvent, NSView *); - #elif PLATFORM(GTK) diff --git a/Source/WebKit/Shared/WebEvent.h b/Source/WebKit/Shared/WebEvent.h -index c36100cf5c4..216402f0a24 100644 +index f77a16bef13..73f99282f08 100644 --- a/Source/WebKit/Shared/WebEvent.h +++ b/Source/WebKit/Shared/WebEvent.h @@ -35,6 +35,7 @@ @@ -2788,7 +2930,7 @@ index c36100cf5c4..216402f0a24 100644 #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: +@@ -301,7 +303,7 @@ private: int32_t m_nativeVirtualKeyCode; int32_t m_macCharCode; #if USE(APPKIT) || USE(UIKIT_KEYBOARD_ADDITIONS) || PLATFORM(GTK) @@ -2798,10 +2940,10 @@ index c36100cf5c4..216402f0a24 100644 #if USE(APPKIT) Vector m_commands; diff --git a/Source/WebKit/Shared/WebKeyboardEvent.cpp b/Source/WebKit/Shared/WebKeyboardEvent.cpp -index a5a23cf148e..390eaf847b6 100644 +index b5955a8b797..3d0c7c8e40f 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& +@@ -77,6 +77,28 @@ WebKeyboardEvent::WebKeyboardEvent(Type type, const String& text, const String& ASSERT(isKeyboardEventType(type)); } @@ -2859,10 +3001,10 @@ index 58e37fe3827..429d245ea99 100644 } diff --git a/Source/WebKit/Sources.txt b/Source/WebKit/Sources.txt -index 3f7d3fb6216..f396e22edf5 100644 +index 73ff4d27bee..4f92dfdfe50 100644 --- a/Source/WebKit/Sources.txt +++ b/Source/WebKit/Sources.txt -@@ -241,17 +241,23 @@ Shared/WebsiteData/WebsiteData.cpp +@@ -246,17 +246,23 @@ Shared/WebsiteData/WebsiteData.cpp UIProcess/AuxiliaryProcessProxy.cpp UIProcess/BackgroundProcessResponsivenessTimer.cpp @@ -2886,7 +3028,7 @@ index 3f7d3fb6216..f396e22edf5 100644 UIProcess/RemoteWebInspectorProxy.cpp UIProcess/ResponsivenessTimer.cpp UIProcess/StatisticsRequest.cpp -@@ -293,6 +299,9 @@ UIProcess/WebPageDiagnosticLoggingClient.cpp +@@ -298,6 +304,9 @@ UIProcess/WebPageDiagnosticLoggingClient.cpp UIProcess/WebPageGroup.cpp UIProcess/WebPageInjectedBundleClient.cpp UIProcess/WebPageInspectorController.cpp @@ -2897,10 +3039,10 @@ index 3f7d3fb6216..f396e22edf5 100644 UIProcess/WebPasteboardProxy.cpp UIProcess/WebPreferences.cpp diff --git a/Source/WebKit/SourcesCocoa.txt b/Source/WebKit/SourcesCocoa.txt -index 63c4dfa10ab..c9a7dcf3dea 100644 +index 9d242e2a064..819e404bc23 100644 --- a/Source/WebKit/SourcesCocoa.txt +++ b/Source/WebKit/SourcesCocoa.txt -@@ -243,6 +243,7 @@ UIProcess/API/Cocoa/_WKApplicationManifest.mm +@@ -247,6 +247,7 @@ UIProcess/API/Cocoa/_WKApplicationManifest.mm UIProcess/API/Cocoa/_WKAttachment.mm UIProcess/API/Cocoa/_WKAutomationSession.mm UIProcess/API/Cocoa/_WKAutomationSessionConfiguration.mm @@ -2982,10 +3124,10 @@ index 54513035b26..2d3200e4f6e 100644 virtual void setStatusText(WebKit::WebPageProxy*, const WTF::String&) { } virtual void mouseDidMoveOverElement(WebKit::WebPageProxy&, const WebKit::WebHitTestResultData&, OptionSet, Object*) { } diff --git a/Source/WebKit/UIProcess/API/C/WKPage.cpp b/Source/WebKit/UIProcess/API/C/WKPage.cpp -index 19e273187ae..14b3ef74afb 100644 +index 4d8f6cf2e46..3373ec7aa1b 100644 --- a/Source/WebKit/UIProcess/API/C/WKPage.cpp +++ b/Source/WebKit/UIProcess/API/C/WKPage.cpp -@@ -1674,6 +1674,8 @@ void WKPageSetPageUIClient(WKPageRef pageRef, const WKPageUIClientBase* wkClient +@@ -1675,6 +1675,8 @@ void WKPageSetPageUIClient(WKPageRef pageRef, const WKPageUIClientBase* wkClient completionHandler(String()); } @@ -2994,7 +3136,7 @@ index 19e273187ae..14b3ef74afb 100644 void setStatusText(WebPageProxy* page, const String& text) final { if (!m_client.setStatusText) -@@ -1734,6 +1736,8 @@ void WKPageSetPageUIClient(WKPageRef pageRef, const WKPageUIClientBase* wkClient +@@ -1735,6 +1737,8 @@ void WKPageSetPageUIClient(WKPageRef pageRef, const WKPageUIClientBase* wkClient { if (!m_client.didNotHandleKeyEvent) return; @@ -3068,7 +3210,7 @@ index 48467b7a833..eaca62adb3c 100644 Vector result; diff --git a/Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.h b/Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.h new file mode 100644 -index 00000000000..aebcbc62682 +index 00000000000..7ed58e57553 --- /dev/null +++ b/Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.h @@ -0,0 +1,33 @@ @@ -3099,7 +3241,7 @@ index 00000000000..aebcbc62682 + +WK_CLASS_AVAILABLE(macos(10.14.0)) +@interface _WKBrowserInspector : NSObject -++ (void)initializeRemoteInspectorPipe:(id<_WKBrowserInspectorDelegate>)delegate; +++ (void)initializeRemoteInspectorPipe:(id<_WKBrowserInspectorDelegate>)delegate headless:(BOOL)headless; +@end + + @@ -3107,10 +3249,10 @@ index 00000000000..aebcbc62682 + diff --git a/Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.mm b/Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.mm new file mode 100644 -index 00000000000..d9497d2c862 +index 00000000000..b0bdfb97e28 --- /dev/null +++ b/Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.mm -@@ -0,0 +1,28 @@ +@@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + @@ -3119,6 +3261,7 @@ index 00000000000..d9497d2c862 + +#include "BrowserInspectorPipe.h" +#include "InspectorBrowserAgentClientMac.h" ++#include "PageClientImplMac.h" +#include "WebsiteDataStore.h" + +#import "WKWebView.h" @@ -3127,10 +3270,11 @@ index 00000000000..d9497d2c862 + +@implementation _WKBrowserInspector + -++ (void)initializeRemoteInspectorPipe:(id<_WKBrowserInspectorDelegate>)delegate +++ (void)initializeRemoteInspectorPipe:(id<_WKBrowserInspectorDelegate>)delegate headless:(BOOL)headless +{ +#if ENABLE(REMOTE_INSPECTOR) + WebsiteDataStore::defaultDataStore(); ++ PageClientImpl::setHeadless(headless); + initializeBrowserInspectorPipe(makeUnique(delegate)); +#endif +} @@ -3275,10 +3419,10 @@ index 00000000000..ab6b7621d10 + +WebKit::WebPageProxy* webkitBrowserInspectorCreateNewPageInContext(WebKitWebContext*); diff --git a/Source/WebKit/UIProcess/API/glib/WebKitUIClient.cpp b/Source/WebKit/UIProcess/API/glib/WebKitUIClient.cpp -index 47801342ea6..ef163b6615a 100644 +index f769407fdc6..ba010ed593b 100644 --- a/Source/WebKit/UIProcess/API/glib/WebKitUIClient.cpp +++ b/Source/WebKit/UIProcess/API/glib/WebKitUIClient.cpp -@@ -90,6 +90,10 @@ private: +@@ -91,6 +91,10 @@ private: { webkitWebViewRunJavaScriptPrompt(m_webView, message.utf8(), defaultValue.utf8(), WTFMove(completionHandler)); } @@ -3290,10 +3434,10 @@ index 47801342ea6..ef163b6615a 100644 bool canRunBeforeUnloadConfirmPanel() const final { return true; } diff --git a/Source/WebKit/UIProcess/API/glib/WebKitWebContext.cpp b/Source/WebKit/UIProcess/API/glib/WebKitWebContext.cpp -index 126bccf1314..a095db63bc5 100644 +index 33a9b7d5ad0..406c6431bd8 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) +@@ -385,6 +385,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()))); @@ -3499,10 +3643,10 @@ index 9cc31cb4968..930499e65b6 100644 #include diff --git a/Source/WebKit/UIProcess/BrowserInspectorController.cpp b/Source/WebKit/UIProcess/BrowserInspectorController.cpp new file mode 100644 -index 00000000000..bd9351774f9 +index 00000000000..260916b29cf --- /dev/null +++ b/Source/WebKit/UIProcess/BrowserInspectorController.cpp -@@ -0,0 +1,101 @@ +@@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + @@ -3549,19 +3693,22 @@ index 00000000000..bd9351774f9 + , m_browserAgentClient(std::move(client)) +{ + m_agents.append(makeUnique(m_backendDispatcher, m_browserAgentClient.get())); -+ m_agents.append(makeUnique(m_backendDispatcher)); ++ auto targetAgent = makeUnique(m_backendDispatcher); ++ m_browserTargetAgent = targetAgent.get(); ++ m_agents.append(WTFMove(targetAgent)); +} + -+BrowserInspectorController::~BrowserInspectorController() = default; ++BrowserInspectorController::~BrowserInspectorController() ++{ ++ if (m_frontendChannel) ++ disconnectFrontend(); ++} + +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); -+ }); ++ WebPageInspectorController::setObserver(this); + + bool connectingFirstFrontend = !m_frontendRouter->hasFrontends(); + m_frontendRouter->connectFrontend(frontendChannel); @@ -3580,7 +3727,7 @@ index 00000000000..bd9351774f9 + if (!m_frontendRouter->hasFrontends()) + m_agents.willDestroyFrontendAndBackend(DisconnectReason::InspectorDestroyed); + -+ WebPageInspectorController::setCreationListener(nullptr); ++ WebPageInspectorController::setObserver(nullptr); + m_frontendChannel = nullptr; +} + @@ -3601,15 +3748,27 @@ index 00000000000..bd9351774f9 + page->inspectorController().disconnectFrontend(*m_frontendChannel); +} + ++void BrowserInspectorController::didCreateInspectorController(WebPageInspectorController& inspectorController) ++{ ++ ASSERT(m_frontendChannel); ++ // Auto-connect to all new pages. ++ inspectorController.connectFrontend(*m_frontendChannel); ++} ++ ++void BrowserInspectorController::didCreateTarget(InspectorTarget& target) ++{ ++ m_browserTargetAgent->didCreateTarget(target); ++} ++ +} // 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..9de68f71fbd +index 00000000000..c487dd06ce4 --- /dev/null +++ b/Source/WebKit/UIProcess/BrowserInspectorController.h -@@ -0,0 +1,47 @@ +@@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + @@ -3617,6 +3776,7 @@ index 00000000000..9de68f71fbd + +#if ENABLE(REMOTE_INSPECTOR) + ++#include "WebPageInspectorController.h" +#include +#include +#include @@ -3629,9 +3789,10 @@ index 00000000000..9de68f71fbd + +namespace WebKit { + ++class BrowserInspectorTargetAgent; +class InspectorBrowserAgentClient; + -+class BrowserInspectorController { ++class BrowserInspectorController : private WebPageInspectorControllerObserver { + WTF_MAKE_NONCOPYABLE(BrowserInspectorController); + WTF_MAKE_FAST_ALLOCATED; +public: @@ -3644,6 +3805,11 @@ index 00000000000..9de68f71fbd + +private: + class TargetHandler; ++ ++ // WebPageInspectorControllerObserver ++ void didCreateInspectorController(WebPageInspectorController&) override; ++ void didCreateTarget(Inspector::InspectorTarget&) override; ++ + void connectToAllPages(); + void disconnectFromAllPages(); + @@ -3652,6 +3818,7 @@ index 00000000000..9de68f71fbd + Ref m_backendDispatcher; + std::unique_ptr m_browserAgentClient; + Inspector::AgentRegistry m_agents; ++ BrowserInspectorTargetAgent* m_browserTargetAgent { nullptr }; +}; + +} // namespace WebKit @@ -3722,10 +3889,10 @@ index 00000000000..ac0caaabaed +#endif // ENABLE(REMOTE_INSPECTOR) diff --git a/Source/WebKit/UIProcess/BrowserInspectorTargetAgent.cpp b/Source/WebKit/UIProcess/BrowserInspectorTargetAgent.cpp new file mode 100644 -index 00000000000..a2873d5e0f7 +index 00000000000..2d089e80978 --- /dev/null +++ b/Source/WebKit/UIProcess/BrowserInspectorTargetAgent.cpp -@@ -0,0 +1,83 @@ +@@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + @@ -3776,6 +3943,28 @@ index 00000000000..a2873d5e0f7 + +void BrowserInspectorTargetAgent::willDestroyFrontendAndBackend(DisconnectReason) +{ ++ m_shouldPauseOnStart = false; ++} ++ ++void BrowserInspectorTargetAgent::setPauseOnStart(ErrorString&, bool pauseOnStart) ++{ ++ m_shouldPauseOnStart = pauseOnStart; ++} ++ ++void BrowserInspectorTargetAgent::resume(ErrorString& errorString, const String& targetId) ++{ ++ auto* target = targetForId(targetId); ++ if (!target) { ++ errorString = "Missing target for given targetId"_s; ++ return; ++ } ++ ++ if (!target->isPaused()) { ++ errorString = "Target for given targetId is not paused"_s; ++ return; ++ } ++ ++ target->resume(); +} + +void BrowserInspectorTargetAgent::sendMessageToTarget(ErrorString& error, const String& in_targetId, const String& in_message) @@ -3808,13 +3997,19 @@ index 00000000000..a2873d5e0f7 + target->close(error); +} + ++void BrowserInspectorTargetAgent::didCreateTarget(InspectorTarget& target) ++{ ++ if (m_shouldPauseOnStart) ++ target.pause(); ++} ++ +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/BrowserInspectorTargetAgent.h b/Source/WebKit/UIProcess/BrowserInspectorTargetAgent.h new file mode 100644 -index 00000000000..5c274280846 +index 00000000000..4dbfe313f4e --- /dev/null +++ b/Source/WebKit/UIProcess/BrowserInspectorTargetAgent.h -@@ -0,0 +1,35 @@ +@@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + @@ -3827,6 +4022,10 @@ index 00000000000..5c274280846 +#include +#include + ++namespace Inspector { ++class InspectorTarget; ++} ++ +namespace WebKit { + +class BrowserInspectorTargetAgent final : public Inspector::InspectorAgentBase, public Inspector::TargetBackendDispatcherHandler { @@ -3844,9 +4043,14 @@ index 00000000000..5c274280846 + 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; ++ void setPauseOnStart(Inspector::ErrorString&, bool pauseOnStart) override; ++ void resume(Inspector::ErrorString&, const String& in_targetId) override; ++ ++ void didCreateTarget(Inspector::InspectorTarget&); + +private: + Ref m_backendDispatcher; ++ bool m_shouldPauseOnStart { false }; +}; + +} // namespace WebKit @@ -4426,10 +4630,17 @@ index 00000000000..203c203a0e2 + +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/InspectorTargetProxy.cpp b/Source/WebKit/UIProcess/InspectorTargetProxy.cpp -index 1b37c1ed439..c45d45de342 100644 +index 55c8173ab24..c580bb17868 100644 --- a/Source/WebKit/UIProcess/InspectorTargetProxy.cpp +++ b/Source/WebKit/UIProcess/InspectorTargetProxy.cpp -@@ -32,6 +32,8 @@ +@@ -26,12 +26,15 @@ + #include "config.h" + #include "InspectorTargetProxy.h" + ++#include "APIPageConfiguration.h" + #include "ProvisionalPageProxy.h" + #include "WebFrameProxy.h" + #include "WebPageInspectorTarget.h" #include "WebPageMessages.h" #include "WebPageProxy.h" #include "WebProcessProxy.h" @@ -4438,7 +4649,7 @@ index 1b37c1ed439..c45d45de342 100644 namespace WebKit { -@@ -39,23 +41,29 @@ using namespace Inspector; +@@ -39,23 +42,29 @@ using namespace Inspector; std::unique_ptr InspectorTargetProxy::create(WebPageProxy& page, const String& targetId, Inspector::InspectorTargetType type) { @@ -4473,11 +4684,31 @@ index 1b37c1ed439..c45d45de342 100644 void InspectorTargetProxy::connect(Inspector::FrontendChannel::ConnectionType connectionType) { if (m_provisionalPage) { +@@ -103,4 +112,19 @@ bool InspectorTargetProxy::isProvisional() const + return !!m_provisionalPage; + } + ++String InspectorTargetProxy::oldTargetID() const ++{ ++ if (!m_provisionalPage) ++ return String(); ++ return WebPageInspectorTarget::toTargetID(m_page.webPageID()); ++} ++ ++String InspectorTargetProxy::openerID() const ++{ ++ auto* opener = m_page.configuration().relatedPage(); ++ if (!opener) ++ return String(); ++ return WebPageInspectorTarget::toTargetID(opener->webPageID()); ++} ++ + } // namespace WebKit diff --git a/Source/WebKit/UIProcess/InspectorTargetProxy.h b/Source/WebKit/UIProcess/InspectorTargetProxy.h -index a2239cec8e1..43415afbc77 100644 +index a2239cec8e1..3985edf2081 100644 --- a/Source/WebKit/UIProcess/InspectorTargetProxy.h +++ b/Source/WebKit/UIProcess/InspectorTargetProxy.h -@@ -37,17 +37,18 @@ class WebPageProxy; +@@ -37,30 +37,35 @@ 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. @@ -4498,7 +4729,10 @@ index a2239cec8e1..43415afbc77 100644 void didCommitProvisionalTarget(); bool isProvisional() const override; -@@ -56,11 +57,13 @@ public: ++ String oldTargetID() const override; ++ String openerID() const override; + + void connect(Inspector::FrontendChannel::ConnectionType) override; void disconnect() override; void sendMessageToTargetBackend(const String&) override; @@ -4713,11 +4947,27 @@ index baabe8def1d..83d089d87d0 100644 #include #include #include +diff --git a/Source/WebKit/UIProcess/WebAuthentication/Mock/MockAuthenticatorManager.cpp b/Source/WebKit/UIProcess/WebAuthentication/Mock/MockAuthenticatorManager.cpp +index 2c4f9ddabf0..ae9e0b80708 100644 +--- a/Source/WebKit/UIProcess/WebAuthentication/Mock/MockAuthenticatorManager.cpp ++++ b/Source/WebKit/UIProcess/WebAuthentication/Mock/MockAuthenticatorManager.cpp +@@ -53,9 +53,9 @@ void MockAuthenticatorManager::respondReceivedInternal(Respond&& respond) + void MockAuthenticatorManager::filterTransports(TransportSet& transports) const + { + if (!m_testConfiguration.nfc) +- transports.remove(AuthenticatorTransport::Nfc); ++ transports.remove(WebCore::AuthenticatorTransport::Nfc); + if (!m_testConfiguration.local) +- transports.remove(AuthenticatorTransport::Internal); ++ transports.remove(WebCore::AuthenticatorTransport::Internal); + } + + } // namespace WebKit diff --git a/Source/WebKit/UIProcess/WebPageInspectorController.cpp b/Source/WebKit/UIProcess/WebPageInspectorController.cpp -index b9a9469ab59..81129896554 100644 +index 1ee28bf7163..6ac7ab2efd9 100644 --- a/Source/WebKit/UIProcess/WebPageInspectorController.cpp +++ b/Source/WebKit/UIProcess/WebPageInspectorController.cpp -@@ -26,9 +26,11 @@ +@@ -26,10 +26,13 @@ #include "config.h" #include "WebPageInspectorController.h" @@ -4727,20 +4977,19 @@ index b9a9469ab59..81129896554 100644 #include "WebPageInspectorTarget.h" +#include "WebPageInspectorTargetProxy.h" #include "WebPageProxy.h" ++#include "WebPreferences.h" #include #include -@@ -46,26 +48,59 @@ static String getTargetID(const ProvisionalPageProxy& provisionalPage) + #include +@@ -46,26 +49,56 @@ static String getTargetID(const ProvisionalPageProxy& provisionalPage) return WebPageInspectorTarget::toTargetID(provisionalPage.webPageID()); } -+static WebPageInspectorController::CreationListener& creationListener() { -+ static NeverDestroyed listener; -+ return listener; -+} ++WebPageInspectorControllerObserver* WebPageInspectorController::s_observer = nullptr; + -+void WebPageInspectorController::setCreationListener(CreationListener listener) ++void WebPageInspectorController::setObserver(WebPageInspectorControllerObserver* observer) +{ -+ creationListener() = listener; ++ s_observer = observer; +} + WebPageInspectorController::WebPageInspectorController(WebPageProxy& page) @@ -4759,8 +5008,8 @@ index b9a9469ab59..81129896554 100644 m_agents.append(WTFMove(targetAgent)); + -+ if (creationListener()) -+ creationListener()(*this); ++ if (s_observer) ++ s_observer->didCreateInspectorController(*this); } void WebPageInspectorController::init() @@ -4790,7 +5039,17 @@ index b9a9469ab59..81129896554 100644 disconnectAllFrontends(); m_agents.discardValues(); -@@ -134,6 +169,16 @@ void WebPageInspectorController::dispatchMessageFromFrontend(const String& messa +@@ -80,6 +113,9 @@ void WebPageInspectorController::connectFrontend(Inspector::FrontendChannel& fro + { + bool connectingFirstFrontend = !m_frontendRouter->hasFrontends(); + ++ if (connectingFirstFrontend) ++ disableBackForwardCache(); ++ + m_frontendRouter->connectFrontend(frontendChannel); + + if (connectingFirstFrontend) +@@ -134,6 +170,16 @@ void WebPageInspectorController::dispatchMessageFromFrontend(const String& messa m_backendDispatcher->dispatch(message); } @@ -4807,7 +5066,7 @@ index b9a9469ab59..81129896554 100644 #if ENABLE(REMOTE_INSPECTOR) void WebPageInspectorController::setIndicating(bool indicating) { -@@ -150,7 +195,12 @@ void WebPageInspectorController::setIndicating(bool indicating) +@@ -150,7 +196,12 @@ void WebPageInspectorController::setIndicating(bool indicating) void WebPageInspectorController::createInspectorTarget(const String& targetId, Inspector::InspectorTargetType type) { @@ -4821,7 +5080,7 @@ index b9a9469ab59..81129896554 100644 } void WebPageInspectorController::destroyInspectorTarget(const String& targetId) -@@ -169,7 +219,7 @@ void WebPageInspectorController::sendMessageToInspectorFrontend(const String& ta +@@ -186,7 +237,7 @@ void WebPageInspectorController::setContinueLoadingCallback(const ProvisionalPag void WebPageInspectorController::didCreateProvisionalPage(ProvisionalPageProxy& provisionalPage) { @@ -4830,25 +5089,65 @@ index b9a9469ab59..81129896554 100644 } void WebPageInspectorController::willDestroyProvisionalPage(const ProvisionalPageProxy& provisionalPage) +@@ -214,8 +265,17 @@ void WebPageInspectorController::didCommitProvisionalPage(WebCore::PageIdentifie + + void WebPageInspectorController::addTarget(std::unique_ptr&& target) + { ++ if (s_observer) ++ s_observer->didCreateTarget(*target); + m_targetAgent->targetCreated(*target); + m_targets.set(target->identifier(), WTFMove(target)); + } + ++void WebPageInspectorController::disableBackForwardCache() ++{ ++ // Navigation to cached pages doesn't fire some of the events (e.g. execution context created) ++ // that inspector depends on. So we disable the cache when front-end connects. ++ m_page.preferences().setUsesBackForwardCache(false); ++} ++ + } // namespace WebKit diff --git a/Source/WebKit/UIProcess/WebPageInspectorController.h b/Source/WebKit/UIProcess/WebPageInspectorController.h -index 828bc3ccc7e..40a333b7004 100644 +index 78caedf0c0c..be5db786c07 100644 --- a/Source/WebKit/UIProcess/WebPageInspectorController.h +++ b/Source/WebKit/UIProcess/WebPageInspectorController.h -@@ -48,7 +48,13 @@ public: +@@ -37,10 +37,22 @@ namespace Inspector { + class BackendDispatcher; + class FrontendChannel; + class FrontendRouter; ++class InspectorTarget; + } + + namespace WebKit { + ++class WebPageInspectorController; ++ ++class WebPageInspectorControllerObserver { ++public: ++ virtual void didCreateInspectorController(WebPageInspectorController&) = 0; ++ virtual void didCreateTarget(Inspector::InspectorTarget&) = 0; ++ ++protected: ++ virtual ~WebPageInspectorControllerObserver() = default; ++}; ++ + class WebPageInspectorController { + WTF_MAKE_NONCOPYABLE(WebPageInspectorController); + WTF_MAKE_FAST_ALLOCATED; +@@ -48,7 +60,12 @@ public: WebPageInspectorController(WebPageProxy&); void init(); + void didFinishAttachingToWebProcess(); + -+ using CreationListener = std::function; -+ static void setCreationListener(CreationListener); ++ static void setObserver(WebPageInspectorControllerObserver*); + void pageClosed(); + void didProcessAllPendingKeyboardEvents(); bool hasLocalFrontend() const; -@@ -57,6 +63,8 @@ public: +@@ -57,6 +74,8 @@ public: void disconnectAllFrontends(); void dispatchMessageFromFrontend(const String& message); @@ -4857,12 +5156,29 @@ index 828bc3ccc7e..40a333b7004 100644 #if ENABLE(REMOTE_INSPECTOR) void setIndicating(bool); +@@ -75,6 +94,7 @@ public: + + private: + void addTarget(std::unique_ptr&&); ++ void disableBackForwardCache(); + + WebPageProxy& m_page; + Ref m_frontendRouter; +@@ -82,6 +102,8 @@ private: + Inspector::AgentRegistry m_agents; + Inspector::InspectorTargetAgent* m_targetAgent; + HashMap> m_targets; ++ ++ static WebPageInspectorControllerObserver* s_observer; + }; + + } // namespace WebKit diff --git a/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.cpp b/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.cpp new file mode 100644 -index 00000000000..e903413c95d +index 00000000000..f10c1651e64 --- /dev/null +++ b/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.cpp -@@ -0,0 +1,47 @@ +@@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + @@ -4897,9 +5213,10 @@ index 00000000000..e903413c95d +{ +} + -+void WebPageInspectorEmulationAgent::setDeviceMetricsOverride(ErrorString& error, int in_width, int in_height) ++void WebPageInspectorEmulationAgent::setDeviceMetricsOverride(ErrorString& error, int in_width, int in_height, double in_deviceScaleFactor) +{ + platformSetSize(error, in_width, in_height); ++ m_page.setCustomDeviceScaleFactor(in_deviceScaleFactor); +} + +void WebPageInspectorEmulationAgent::setJavaScriptEnabled(ErrorString&, bool enabled) @@ -4912,7 +5229,7 @@ index 00000000000..e903413c95d +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.h b/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.h new file mode 100644 -index 00000000000..b02753590b3 +index 00000000000..0025b0be853 --- /dev/null +++ b/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.h @@ -0,0 +1,42 @@ @@ -4947,7 +5264,7 @@ index 00000000000..b02753590b3 + void didCreateFrontendAndBackend(Inspector::FrontendRouter*, Inspector::BackendDispatcher*) override; + void willDestroyFrontendAndBackend(Inspector::DisconnectReason) override; + -+ void setDeviceMetricsOverride(Inspector::ErrorString&, int in_width, int in_height) override; ++ void setDeviceMetricsOverride(Inspector::ErrorString&, int in_width, int in_height, double in_deviceScaleFactor) override; + void setJavaScriptEnabled(Inspector::ErrorString&, bool enabled) override; + +private: @@ -4960,10 +5277,10 @@ index 00000000000..b02753590b3 +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/WebPageInspectorInputAgent.cpp b/Source/WebKit/UIProcess/WebPageInspectorInputAgent.cpp new file mode 100644 -index 00000000000..6bd5242c9c2 +index 00000000000..276f3da57fa --- /dev/null +++ b/Source/WebKit/UIProcess/WebPageInspectorInputAgent.cpp -@@ -0,0 +1,235 @@ +@@ -0,0 +1,236 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + @@ -5175,8 +5492,9 @@ index 00000000000..6bd5242c9c2 + m_inputObserver->addMouseCallback(WTFMove(callback)); +#if PLATFORM(WPE) + platformDispatchMouseEvent(type, in_x, in_y, button, modifiers); -+#elif PLATFORM(GTK) || PLATFORM(MAC) -+ WallTime timestamp = WallTime::now(); ++#elif PLATFORM(MAC) ++ platformDispatchMouseEvent(in_type, in_x, in_y, opt_in_modifiers, opt_in_button, opt_in_clickCount); ++#elif PLATFORM(GTK) + NativeWebMouseEvent event( + type, + button, @@ -5201,10 +5519,10 @@ index 00000000000..6bd5242c9c2 +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/WebPageInspectorInputAgent.h b/Source/WebKit/UIProcess/WebPageInspectorInputAgent.h new file mode 100644 -index 00000000000..36531345add +index 00000000000..5804c52add6 --- /dev/null +++ b/Source/WebKit/UIProcess/WebPageInspectorInputAgent.h -@@ -0,0 +1,54 @@ +@@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + @@ -5248,6 +5566,9 @@ index 00000000000..36531345add +#if PLATFORM(WPE) + void platformDispatchMouseEvent(WebMouseEvent::Type type, int x, int y, WebMouseEvent::Button button, OptionSet modifiers); +#endif ++#if PLATFORM(MAC) ++ void platformDispatchMouseEvent(const String& type, int x, int y, const int* modifier, const String* button, const int* clickCount); ++#endif + + Ref m_backendDispatcher; + WebPageProxy& m_page; @@ -5426,10 +5747,10 @@ index 00000000000..0655b5ea376 + +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp -index 81b65c6e2db..2b7677a25a2 100644 +index 35cd3ac33fc..05f3cc29314 100644 --- a/Source/WebKit/UIProcess/WebPageProxy.cpp +++ b/Source/WebKit/UIProcess/WebPageProxy.cpp -@@ -867,6 +867,7 @@ void WebPageProxy::finishAttachingToWebProcess(ProcessLaunchReason reason) +@@ -868,6 +868,7 @@ void WebPageProxy::finishAttachingToWebProcess(ProcessLaunchReason reason) m_pageLoadState.didSwapWebProcesses(); if (reason != ProcessLaunchReason::InitialProcess) m_drawingArea->waitForBackingStoreUpdateOnNextPaint(); @@ -5437,7 +5758,7 @@ index 81b65c6e2db..2b7677a25a2 100644 } void WebPageProxy::didAttachToRunningProcess() -@@ -1620,6 +1621,11 @@ void WebPageProxy::setControlledByAutomation(bool controlled) +@@ -1623,6 +1624,11 @@ void WebPageProxy::setControlledByAutomation(bool controlled) m_process->processPool().sendToNetworkingProcess(Messages::NetworkProcess::SetSessionIsControlledByAutomation(m_websiteDataStore->sessionID(), m_controlledByAutomation)); } @@ -5449,7 +5770,7 @@ index 81b65c6e2db..2b7677a25a2 100644 void WebPageProxy::createInspectorTarget(const String& targetId, Inspector::InspectorTargetType type) { m_inspectorController->createInspectorTarget(targetId, type); -@@ -5330,6 +5336,8 @@ void WebPageProxy::runJavaScriptAlert(FrameIdentifier frameID, SecurityOriginDat +@@ -5339,6 +5345,8 @@ void WebPageProxy::runJavaScriptAlert(FrameIdentifier frameID, SecurityOriginDat if (auto* automationSession = process().processPool().automationSession()) automationSession->willShowJavaScriptDialog(*this); } @@ -5458,7 +5779,7 @@ index 81b65c6e2db..2b7677a25a2 100644 m_uiClient->runJavaScriptAlert(*this, message, frame, WTFMove(securityOrigin), WTFMove(reply)); } -@@ -5349,6 +5357,8 @@ void WebPageProxy::runJavaScriptConfirm(FrameIdentifier frameID, SecurityOriginD +@@ -5358,6 +5366,8 @@ void WebPageProxy::runJavaScriptConfirm(FrameIdentifier frameID, SecurityOriginD if (auto* automationSession = process().processPool().automationSession()) automationSession->willShowJavaScriptDialog(*this); } @@ -5467,7 +5788,7 @@ index 81b65c6e2db..2b7677a25a2 100644 m_uiClient->runJavaScriptConfirm(*this, message, frame, WTFMove(securityOrigin), WTFMove(reply)); } -@@ -5368,6 +5378,8 @@ void WebPageProxy::runJavaScriptPrompt(FrameIdentifier frameID, SecurityOriginDa +@@ -5377,6 +5387,8 @@ void WebPageProxy::runJavaScriptPrompt(FrameIdentifier frameID, SecurityOriginDa if (auto* automationSession = process().processPool().automationSession()) automationSession->willShowJavaScriptDialog(*this); } @@ -5476,7 +5797,7 @@ index 81b65c6e2db..2b7677a25a2 100644 m_uiClient->runJavaScriptPrompt(*this, message, defaultValue, frame, WTFMove(securityOrigin), WTFMove(reply)); } -@@ -5526,6 +5538,8 @@ void WebPageProxy::runBeforeUnloadConfirmPanel(FrameIdentifier frameID, Security +@@ -5536,6 +5548,8 @@ void WebPageProxy::runBeforeUnloadConfirmPanel(FrameIdentifier frameID, Security return; } } @@ -5485,7 +5806,7 @@ index 81b65c6e2db..2b7677a25a2 100644 // Since runBeforeUnloadConfirmPanel() can spin a nested run loop we need to turn off the responsiveness timer. m_process->responsivenessTimer().stop(); -@@ -6543,6 +6557,8 @@ void WebPageProxy::didReceiveEvent(uint32_t opaqueType, bool handled) +@@ -6558,6 +6572,8 @@ void WebPageProxy::didReceiveEvent(uint32_t opaqueType, bool handled) if (auto* automationSession = process().processPool().automationSession()) automationSession->mouseEventsFlushedForPage(*this); pageClient().didFinishProcessingAllPendingMouseEvents(); @@ -5494,7 +5815,7 @@ index 81b65c6e2db..2b7677a25a2 100644 } break; -@@ -6569,7 +6585,6 @@ void WebPageProxy::didReceiveEvent(uint32_t opaqueType, bool handled) +@@ -6584,7 +6600,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()); @@ -5502,7 +5823,7 @@ index 81b65c6e2db..2b7677a25a2 100644 MESSAGE_CHECK(m_process, !m_keyEventQueue.isEmpty()); NativeWebKeyboardEvent event = m_keyEventQueue.takeFirst(); -@@ -6584,7 +6599,6 @@ void WebPageProxy::didReceiveEvent(uint32_t opaqueType, bool handled) +@@ -6604,7 +6619,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); @@ -5510,7 +5831,7 @@ index 81b65c6e2db..2b7677a25a2 100644 pageClient().doneWithKeyEvent(event, handled); if (!handled) m_uiClient->didNotHandleKeyEvent(this, event); -@@ -6593,6 +6607,8 @@ void WebPageProxy::didReceiveEvent(uint32_t opaqueType, bool handled) +@@ -6613,6 +6627,8 @@ void WebPageProxy::didReceiveEvent(uint32_t opaqueType, bool handled) if (!canProcessMoreKeyEvents) { if (auto* automationSession = process().processPool().automationSession()) automationSession->keyboardEventsFlushedForPage(*this); @@ -5520,7 +5841,7 @@ index 81b65c6e2db..2b7677a25a2 100644 break; } diff --git a/Source/WebKit/UIProcess/WebPageProxy.h b/Source/WebKit/UIProcess/WebPageProxy.h -index 488ac80306d..700b332f427 100644 +index 10947ff0dbb..c1a42229051 100644 --- a/Source/WebKit/UIProcess/WebPageProxy.h +++ b/Source/WebKit/UIProcess/WebPageProxy.h @@ -35,6 +35,7 @@ @@ -5555,7 +5876,7 @@ index 488ac80306d..700b332f427 100644 void initializeWebPage(); void setDrawingArea(std::unique_ptr&&); -@@ -2229,6 +2240,7 @@ private: +@@ -2234,6 +2245,7 @@ private: bool m_treatsSHA1CertificatesAsInsecure { true }; RefPtr m_inspector; @@ -5563,7 +5884,7 @@ index 488ac80306d..700b332f427 100644 #if ENABLE(FULLSCREEN_API) std::unique_ptr m_fullScreenManager; -@@ -2577,6 +2589,7 @@ private: +@@ -2582,6 +2594,7 @@ private: #if ENABLE(REMOTE_INSPECTOR) std::unique_ptr m_inspectorDebuggable; #endif @@ -5990,11 +6311,68 @@ index 00000000000..e3062b3651f +} + +} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/mac/PageClientImplMac.h b/Source/WebKit/UIProcess/mac/PageClientImplMac.h +index 8016b56c160..bf5422a3a63 100644 +--- a/Source/WebKit/UIProcess/mac/PageClientImplMac.h ++++ b/Source/WebKit/UIProcess/mac/PageClientImplMac.h +@@ -53,6 +53,8 @@ class PageClientImpl final : public PageClientImplCocoa + #endif + { + public: ++ static void setHeadless(bool headless); ++ + PageClientImpl(NSView *, WKWebView *); + virtual ~PageClientImpl(); + diff --git a/Source/WebKit/UIProcess/mac/PageClientImplMac.mm b/Source/WebKit/UIProcess/mac/PageClientImplMac.mm -index 22653d74398..bf27558fdfd 100644 +index 22653d74398..5086bc7375f 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) +@@ -104,6 +104,13 @@ static NSString * const kAXLoadCompleteNotification = @"AXLoadComplete"; + namespace WebKit { + using namespace WebCore; + ++static bool _headless = false; ++ ++// static ++void PageClientImpl::setHeadless(bool headless) { ++ _headless = true; ++} ++ + PageClientImpl::PageClientImpl(NSView* view, WKWebView *webView) + : PageClientImplCocoa(webView) + , m_view(view) +@@ -162,6 +169,9 @@ NSWindow *PageClientImpl::activeWindow() const + + bool PageClientImpl::isViewWindowActive() + { ++ if (_headless) ++ return true; ++ + ASSERT(hasProcessPrivilege(ProcessPrivilege::CanCommunicateWithWindowServer)); + NSWindow *activeViewWindow = activeWindow(); + return activeViewWindow.isKeyWindow || [NSApp keyWindow] == activeViewWindow; +@@ -169,6 +179,9 @@ bool PageClientImpl::isViewWindowActive() + + bool PageClientImpl::isViewFocused() + { ++ if (_headless) ++ return true; ++ + // FIXME: This is called from the WebPageProxy constructor before we have a WebViewImpl. + // Once WebViewImpl and PageClient merge, this won't be a problem. + if (!m_impl) +@@ -192,6 +205,9 @@ void PageClientImpl::makeFirstResponder() + + bool PageClientImpl::isViewVisible() + { ++ if (_headless) ++ return true; ++ + NSView *activeView = this->activeView(); + NSWindow *activeViewWindow = activeWindow(); + +@@ -455,6 +471,8 @@ IntRect PageClientImpl::rootViewToAccessibilityScreen(const IntRect& rect) void PageClientImpl::doneWithKeyEvent(const NativeWebKeyboardEvent& event, bool eventWasHandled) { @@ -6003,7 +6381,7 @@ index 22653d74398..bf27558fdfd 100644 m_impl->doneWithKeyEvent(event.nativeEvent(), eventWasHandled); } -@@ -930,6 +932,9 @@ void PageClientImpl::didRestoreScrollPosition() +@@ -930,6 +948,9 @@ void PageClientImpl::didRestoreScrollPosition() bool PageClientImpl::windowIsFrontWindowUnderMouse(const NativeWebMouseEvent& event) { @@ -6042,18 +6420,72 @@ index 00000000000..d364ca6d955 +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/mac/WebPageInspectorInputAgentMac.mm b/Source/WebKit/UIProcess/mac/WebPageInspectorInputAgentMac.mm new file mode 100644 -index 00000000000..7ce9b71b0fb +index 00000000000..9ce6d7bec29 --- /dev/null +++ b/Source/WebKit/UIProcess/mac/WebPageInspectorInputAgentMac.mm -@@ -0,0 +1,14 @@ +@@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + -+#include "config.h" -+#include "WebPageInspectorInputAgent.h" ++#import "config.h" ++#import "NativeWebMouseEvent.h" ++#import "WebPageInspectorInputAgent.h" ++#import "WebPageProxy.h" ++#import ++#import + +namespace WebKit { + ++using namespace WebCore; ++ ++void WebPageInspectorInputAgent::platformDispatchMouseEvent(const String& type, int x, int y, const int* optionalModifiers, const String* button, const int* optionalClickCount) { ++ IntPoint locationInWindow(x, y); ++ ++ NSEventModifierFlags modifiers = optionalModifiers ? *optionalModifiers : 0; ++ int clickCount = optionalClickCount ? *optionalClickCount : 0; ++ ++ NSTimeInterval timestamp = [NSDate timeIntervalSinceReferenceDate]; ++ NSWindow *window = m_page.platformWindow(); ++ NSInteger windowNumber = window.windowNumber; ++ ++ NSEventType downEventType = (NSEventType)0; ++ NSEventType dragEventType = (NSEventType)0; ++ NSEventType upEventType = (NSEventType)0; ++ ++ if (!button || *button == "none") { ++ downEventType = NSEventTypeMouseMoved; ++ dragEventType = NSEventTypeMouseMoved; ++ upEventType = NSEventTypeMouseMoved; ++ } else if (*button == "left") { ++ downEventType = NSEventTypeLeftMouseDown; ++ dragEventType = NSEventTypeLeftMouseDragged; ++ upEventType = NSEventTypeLeftMouseUp; ++ } else if (*button == "middle") { ++ downEventType = NSEventTypeOtherMouseDown; ++ dragEventType = NSEventTypeLeftMouseDragged; ++ upEventType = NSEventTypeOtherMouseUp; ++ } else if (*button == "right") { ++ downEventType = NSEventTypeRightMouseDown; ++ upEventType = NSEventTypeRightMouseUp; ++ } ++ ++ NSInteger eventNumber = 0; ++ ++ NSEvent* event = nil; ++ if (type == "move") { ++ event = [NSEvent mouseEventWithType:dragEventType location:locationInWindow modifierFlags:modifiers timestamp:timestamp windowNumber:windowNumber context:nil eventNumber:eventNumber clickCount:clickCount pressure:0.0f]; ++ } else if (type == "down") { ++ event = [NSEvent mouseEventWithType:downEventType location:locationInWindow modifierFlags:modifiers timestamp:timestamp windowNumber:windowNumber context:nil eventNumber:eventNumber clickCount:clickCount pressure:WebCore::ForceAtClick]; ++ } else if (type == "up") { ++ event = [NSEvent mouseEventWithType:upEventType location:locationInWindow modifierFlags:modifiers timestamp:timestamp windowNumber:windowNumber context:nil eventNumber:eventNumber clickCount:clickCount pressure:0.0f]; ++ } ++ ++ if (event) { ++ NativeWebMouseEvent nativeEvent(event, nil, [window contentView]); ++ m_page.handleMouseEvent(nativeEvent); ++ } ++} ++ +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"); @@ -6062,15 +6494,16 @@ index 00000000000..7ce9b71b0fb +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/mac/WebPageInspectorTargetProxyMac.mm b/Source/WebKit/UIProcess/mac/WebPageInspectorTargetProxyMac.mm new file mode 100644 -index 00000000000..06a7e286abf +index 00000000000..2061f6d129b --- /dev/null +++ b/Source/WebKit/UIProcess/mac/WebPageInspectorTargetProxyMac.mm -@@ -0,0 +1,18 @@ +@@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + -+#include "config.h" -+#include "WebPageInspectorTargetProxy.h" ++#import "config.h" ++#import "WebPageInspectorTargetProxy.h" ++#import "WebPageProxy.h" + +#if PLATFORM(MAC) + @@ -6078,7 +6511,8 @@ index 00000000000..06a7e286abf + +void WebPageInspectorTargetProxy::platformActivate(String& error) const +{ -+ error = "Not Implemented"; ++ NSWindow* window = m_page.platformWindow(); ++ [window makeKeyAndOrderFront:nil]; +} + +} // namespace WebKit @@ -6215,10 +6649,10 @@ index 00000000000..74dace1cc7c + +} // namespace WebKit diff --git a/Source/WebKit/WebKit.xcodeproj/project.pbxproj b/Source/WebKit/WebKit.xcodeproj/project.pbxproj -index 5cb81ccc202..2bf6772abb6 100644 +index 95ce08d8379..28bed022575 100644 --- a/Source/WebKit/WebKit.xcodeproj/project.pbxproj +++ b/Source/WebKit/WebKit.xcodeproj/project.pbxproj -@@ -1667,6 +1667,20 @@ +@@ -1673,6 +1673,20 @@ 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 */; }; @@ -6239,7 +6673,7 @@ index 5cb81ccc202..2bf6772abb6 100644 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 */; }; -@@ -4704,6 +4718,21 @@ +@@ -4761,6 +4775,21 @@ 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 = ""; }; @@ -6261,7 +6695,7 @@ index 5cb81ccc202..2bf6772abb6 100644 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 = ""; }; -@@ -6305,6 +6334,7 @@ +@@ -6478,6 +6507,7 @@ 37C4C08318149C2A003688B9 /* Cocoa */ = { isa = PBXGroup; children = ( @@ -6269,7 +6703,7 @@ index 5cb81ccc202..2bf6772abb6 100644 1A43E826188F38E2009E4D30 /* Deprecated */, 37A5E01218BBF937000A081E /* _WKActivatedElementInfo.h */, 37A5E01118BBF937000A081E /* _WKActivatedElementInfo.mm */, -@@ -7783,6 +7813,14 @@ +@@ -7960,6 +7990,14 @@ BC032DC310F438260058C15A /* UIProcess */ = { isa = PBXGroup; children = ( @@ -6284,7 +6718,7 @@ index 5cb81ccc202..2bf6772abb6 100644 BC032DC410F4387C0058C15A /* API */, 512F588D12A8836F00629530 /* Authentication */, 9955A6E81C79809000EB6A93 /* Automation */, -@@ -8060,6 +8098,7 @@ +@@ -8238,6 +8276,7 @@ BC0C376610F807660076D7CB /* C */ = { isa = PBXGroup; children = ( @@ -6292,7 +6726,7 @@ index 5cb81ccc202..2bf6772abb6 100644 5123CF18133D25E60056F800 /* cg */, 6EE849C41368D9040038D481 /* mac */, BCB63477116BF10600603215 /* WebKit2_C.h */, -@@ -8655,6 +8694,11 @@ +@@ -8833,6 +8872,11 @@ BCCF085C113F3B7500C650C5 /* mac */ = { isa = PBXGroup; children = ( @@ -6304,7 +6738,7 @@ index 5cb81ccc202..2bf6772abb6 100644 B878B613133428DC006888E9 /* CorrectionPanel.h */, B878B614133428DC006888E9 /* CorrectionPanel.mm */, C1817362205844A900DFDA65 /* DisplayLink.cpp */, -@@ -9334,6 +9378,7 @@ +@@ -9513,6 +9557,7 @@ 510F59101DDE296900412FF5 /* _WKIconLoadingDelegate.h in Headers */, 37A64E5518F38E3C00EB30F1 /* _WKInputDelegate.h in Headers */, 5CAFDE452130846300B1F7E1 /* _WKInspector.h in Headers */, @@ -6312,7 +6746,7 @@ index 5cb81ccc202..2bf6772abb6 100644 5CAFDE472130846A00B1F7E1 /* _WKInspectorInternal.h in Headers */, 9979CA58237F49F10039EC05 /* _WKInspectorPrivate.h in Headers */, A5C0F0AB2000658200536536 /* _WKInspectorWindow.h in Headers */, -@@ -9447,6 +9492,7 @@ +@@ -9626,6 +9671,7 @@ 7C89D2981A6753B2003A5FDE /* APIPageConfiguration.h in Headers */, 1AC1336C18565C7A00F3EC05 /* APIPageHandle.h in Headers */, 1AFDD3151891B54000153970 /* APIPolicyClient.h in Headers */, @@ -6320,7 +6754,7 @@ index 5cb81ccc202..2bf6772abb6 100644 7CE4D2201A4914CA00C7F152 /* APIProcessPoolConfiguration.h in Headers */, F634445612A885C8000612D8 /* APISecurityOrigin.h in Headers */, 1AFDE6621954E9B100C48FFA /* APISessionState.h in Headers */, -@@ -9566,6 +9612,7 @@ +@@ -9745,6 +9791,7 @@ BC06F43A12DBCCFB002D78DE /* GeolocationPermissionRequestProxy.h in Headers */, 2DA944A41884E4F000ED86DB /* GestureTypes.h in Headers */, 2DA049B8180CCD0A00AAFA9E /* GraphicsLayerCARemote.h in Headers */, @@ -6328,7 +6762,7 @@ index 5cb81ccc202..2bf6772abb6 100644 C0CE72AD1247E78D00BC0EC4 /* HandleMessage.h in Headers */, 1AC75A1B1B3368270056745B /* HangDetectionDisabler.h in Headers */, 57AC8F50217FEED90055438C /* HidConnection.h in Headers */, -@@ -9689,8 +9736,10 @@ +@@ -9868,8 +9915,10 @@ 41DC45961E3D6E2200B11F51 /* NetworkRTCProvider.h in Headers */, 413075AB1DE85F330039EC69 /* NetworkRTCSocket.h in Headers */, 5C20CBA01BB1ECD800895BB1 /* NetworkSession.h in Headers */, @@ -6339,7 +6773,7 @@ index 5cb81ccc202..2bf6772abb6 100644 570DAAC22303730300E8FC04 /* NfcConnection.h in Headers */, 570DAAAE23026F5C00E8FC04 /* NfcService.h in Headers */, 31A2EC5614899C0900810D71 /* NotificationPermissionRequest.h in Headers */, -@@ -9772,6 +9821,7 @@ +@@ -9951,6 +10000,7 @@ CD2865EE2255562000606AC7 /* ProcessTaskStateObserver.h in Headers */, 463FD4821EB94EC000A2982C /* ProcessTerminationReason.h in Headers */, 86E67A251910B9D100004AB7 /* ProcessThrottler.h in Headers */, @@ -6347,15 +6781,7 @@ index 5cb81ccc202..2bf6772abb6 100644 83048AE61ACA45DC0082C832 /* ProcessThrottlerClient.h in Headers */, A1E688701F6E2BAB007006A6 /* QuarantineSPI.h in Headers */, 57FD318222B3515E008D0E8B /* RedirectSOAuthorizationSession.h in Headers */, -@@ -9820,7 +9870,6 @@ - 511F8A7B138B460900A95F44 /* SecItemShimLibrary.h in Headers */, - E18E690C169B563F009B6670 /* SecItemShimProxy.h in Headers */, - E18E6918169B667B009B6670 /* SecItemShimProxyMessages.h in Headers */, -- 7AA746D523593D8100095050 /* SecItemSPI.h in Headers */, - 570AB8F320AE3BD700B8BE87 /* SecKeyProxyStore.h in Headers */, - 514D9F5719119D35000063A7 /* ServicesController.h in Headers */, - 1AFDE65A1954A42B00C48FFA /* SessionState.h in Headers */, -@@ -9934,6 +9983,7 @@ +@@ -10112,6 +10162,7 @@ F430E94422473DFF005FE053 /* WebContentMode.h in Headers */, 31A505FA1680025500A930EB /* WebContextClient.h in Headers */, BC09B8F9147460F7005F5625 /* WebContextConnectionClient.h in Headers */, @@ -6363,7 +6789,7 @@ index 5cb81ccc202..2bf6772abb6 100644 BCDE059B11CDA8AE00E41AF1 /* WebContextInjectedBundleClient.h in Headers */, 51871B5C127CB89D00F76232 /* WebContextMenu.h in Headers */, BC032D7710F4378D0058C15A /* WebContextMenuClient.h in Headers */, -@@ -10167,6 +10217,7 @@ +@@ -10345,6 +10396,7 @@ BCD25F1711D6BDE100169B0E /* WKBundleFrame.h in Headers */, BCF049E611FE20F600F86A58 /* WKBundleFramePrivate.h in Headers */, BC49862F124D18C100D834E1 /* WKBundleHitTestResult.h in Headers */, @@ -6371,7 +6797,7 @@ index 5cb81ccc202..2bf6772abb6 100644 BC204EF211C83EC8008F3375 /* WKBundleInitialize.h in Headers */, 65B86F1E12F11DE300B7DD8A /* WKBundleInspector.h in Headers */, 1A8B66B41BC45B010082DF77 /* WKBundleMac.h in Headers */, -@@ -10215,6 +10266,7 @@ +@@ -10393,6 +10445,7 @@ 5C795D71229F3757003FF1C4 /* WKContextMenuElementInfoPrivate.h in Headers */, 51A555F6128C6C47009ABCEC /* WKContextMenuItem.h in Headers */, 51A55601128C6D92009ABCEC /* WKContextMenuItemTypes.h in Headers */, @@ -6379,7 +6805,7 @@ index 5cb81ccc202..2bf6772abb6 100644 A1EA02381DABFF7E0096021F /* WKContextMenuListener.h in Headers */, BCC938E11180DE440085E5FE /* WKContextPrivate.h in Headers */, 9FB5F395169E6A80002C25BF /* WKContextPrivateMac.h in Headers */, -@@ -10363,6 +10415,7 @@ +@@ -10542,6 +10595,7 @@ 1AB8A1F818400BB800E9AE69 /* WKPageContextMenuClient.h in Headers */, 8372DB251A674C8F00C697C5 /* WKPageDiagnosticLoggingClient.h in Headers */, 1AB8A1F418400B8F00E9AE69 /* WKPageFindClient.h in Headers */, @@ -6387,7 +6813,7 @@ index 5cb81ccc202..2bf6772abb6 100644 1AB8A1F618400B9D00E9AE69 /* WKPageFindMatchesClient.h in Headers */, 1AB8A1F018400B0000E9AE69 /* WKPageFormClient.h in Headers */, BC7B633712A45ABA00D174A4 /* WKPageGroup.h in Headers */, -@@ -11318,6 +11371,7 @@ +@@ -11567,6 +11621,7 @@ 2D92A781212B6A7100F493FD /* MessageReceiverMap.cpp in Sources */, 2D92A782212B6A7100F493FD /* MessageSender.cpp in Sources */, 2D92A77A212B6A6100F493FD /* Module.cpp in Sources */, @@ -6395,7 +6821,7 @@ index 5cb81ccc202..2bf6772abb6 100644 57B826452304F14000B72EB0 /* NearFieldSoftLink.mm in Sources */, 2D913443212CF9F000128AFD /* NetscapeBrowserFuncs.cpp in Sources */, 2D913444212CF9F000128AFD /* NetscapePlugin.cpp in Sources */, -@@ -11342,6 +11396,7 @@ +@@ -11591,6 +11646,7 @@ 1A2D8439127F65D5001EB962 /* NPObjectMessageReceiverMessageReceiver.cpp in Sources */, 2D92A792212B6AD400F493FD /* NPObjectProxy.cpp in Sources */, 2D92A793212B6AD400F493FD /* NPRemoteObjectMap.cpp in Sources */, @@ -6403,7 +6829,7 @@ index 5cb81ccc202..2bf6772abb6 100644 2D913447212CF9F000128AFD /* NPRuntimeObjectMap.cpp in Sources */, 2D913448212CF9F000128AFD /* NPRuntimeUtilities.cpp in Sources */, 2D92A794212B6AD400F493FD /* NPVariantData.cpp in Sources */, -@@ -11381,11 +11436,13 @@ +@@ -11630,11 +11686,13 @@ A1ADAFB62368E6A8009CB776 /* SharedMemory.cpp in Sources */, 2DE6943D18BD2A68005C15E5 /* SmartMagnificationControllerMessageReceiver.cpp in Sources */, 1A334DED16DE8F88006A8E38 /* StorageAreaMapMessageReceiver.cpp in Sources */, @@ -6454,20 +6880,6 @@ index 6cbd7fad5ff..176c46f186b 100644 void connect(Inspector::FrontendChannel::ConnectionType) override; void disconnect() override; -diff --git a/Source/WebKit/WebProcess/WebProcess.cpp b/Source/WebKit/WebProcess/WebProcess.cpp -index 0c92cd9b030..0ed5b37c4d5 100644 ---- a/Source/WebKit/WebProcess/WebProcess.cpp -+++ b/Source/WebKit/WebProcess/WebProcess.cpp -@@ -628,7 +628,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/BrowserWindow.h b/Tools/MiniBrowser/gtk/BrowserWindow.h index 1570d65effb..456f96cf589 100644 --- a/Tools/MiniBrowser/gtk/BrowserWindow.h @@ -6570,18 +6982,20 @@ index 45ef1a6424e..6e015fcb8bc 100644 IBOutlet NSMenuItem *_newWebKit1WindowItem; diff --git a/Tools/MiniBrowser/mac/AppDelegate.m b/Tools/MiniBrowser/mac/AppDelegate.m -index b6af4ef724f..5df2a69d79a 100644 +index b6af4ef724f..365582e402d 100644 --- a/Tools/MiniBrowser/mac/AppDelegate.m +++ b/Tools/MiniBrowser/mac/AppDelegate.m -@@ -34,6 +34,7 @@ +@@ -33,7 +33,9 @@ + #import #import #import ++#import #import +#import #import #import #import -@@ -52,16 +53,44 @@ @interface NSApplication (TouchBar) +@@ -52,16 +54,44 @@ @interface NSApplication (TouchBar) @property (getter=isAutomaticCustomizeTouchBarMenuItemEnabled) BOOL automaticCustomizeTouchBarMenuItemEnabled; @end @@ -6625,11 +7039,11 @@ index b6af4ef724f..5df2a69d79a 100644 } - + if ([arguments containsObject: @"--inspector-pipe"]) -+ [_WKBrowserInspector initializeRemoteInspectorPipe:self]; ++ [_WKBrowserInspector initializeRemoteInspectorPipe:self headless:_headless]; return self; } -@@ -88,7 +117,7 @@ - (void)awakeFromNib +@@ -88,7 +118,7 @@ - (void)awakeFromNib configuration.networkCacheSpeculativeValidationEnabled = YES; dataStore = [[WKWebsiteDataStore alloc] _initWithConfiguration:configuration]; } @@ -6638,7 +7052,18 @@ index b6af4ef724f..5df2a69d79a 100644 return dataStore; } -@@ -109,7 +138,7 @@ - (void)awakeFromNib +@@ -103,13 +133,18 @@ - (void)awakeFromNib + configuration.preferences._developerExtrasEnabled = YES; + configuration.preferences._mediaDevicesEnabled = YES; + configuration.preferences._mockCaptureDevicesEnabled = YES; ++ configuration.preferences._hiddenPageDOMTimerThrottlingEnabled = NO; ++ configuration.preferences._hiddenPageDOMTimerThrottlingAutoIncreases = NO; ++ configuration.preferences._pageVisibilityBasedProcessSuppressionEnabled = NO; ++ configuration.preferences._domTimersThrottlingEnabled = NO; ++ configuration.preferences._requestAnimationFrameEnabled = YES; + + _WKProcessPoolConfiguration *processConfiguration = [[[_WKProcessPoolConfiguration alloc] init] autorelease]; + if ([SettingsController shared].perWindowWebProcessesDisabled) processConfiguration.usesSingleWebProcess = YES; if ([SettingsController shared].processSwapOnWindowOpenWithOpenerEnabled) processConfiguration.processSwapsOnWindowOpenWithOpener = true; @@ -6647,7 +7072,7 @@ index b6af4ef724f..5df2a69d79a 100644 configuration.processPool = [[[WKProcessPool alloc] _initWithConfiguration:processConfiguration] autorelease]; NSArray<_WKExperimentalFeature *> *experimentalFeatures = [WKPreferences _experimentalFeatures]; -@@ -145,6 +174,9 @@ - (void)awakeFromNib +@@ -145,6 +180,9 @@ - (void)awakeFromNib - (BrowserWindowController *)createBrowserWindowController:(id)sender { @@ -6657,7 +7082,7 @@ index b6af4ef724f..5df2a69d79a 100644 BrowserWindowController *controller = nil; BOOL useWebKit2 = NO; BOOL makeEditable = NO; -@@ -158,9 +190,9 @@ - (BrowserWindowController *)createBrowserWindowController:(id)sender +@@ -158,9 +196,9 @@ - (BrowserWindowController *)createBrowserWindowController:(id)sender } if (!useWebKit2) @@ -6669,7 +7094,7 @@ index b6af4ef724f..5df2a69d79a 100644 if (makeEditable) controller.editable = YES; -@@ -185,6 +217,9 @@ - (IBAction)newWindow:(id)sender +@@ -185,6 +223,9 @@ - (IBAction)newWindow:(id)sender - (IBAction)newPrivateWindow:(id)sender { @@ -6679,7 +7104,7 @@ index b6af4ef724f..5df2a69d79a 100644 WKWebViewConfiguration *privateConfiguraton = [defaultConfiguration() copy]; privateConfiguraton.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore]; -@@ -214,6 +249,11 @@ - (void)browserWindowWillClose:(NSWindow *)window +@@ -214,6 +255,11 @@ - (void)browserWindowWillClose:(NSWindow *)window - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { @@ -6691,7 +7116,7 @@ index b6af4ef724f..5df2a69d79a 100644 WebHistory *webHistory = [[WebHistory alloc] init]; [WebHistory setOptionalSharedHistory:webHistory]; [webHistory release]; -@@ -255,6 +295,9 @@ - (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filenam +@@ -255,6 +301,9 @@ - (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filenam - (IBAction)openDocument:(id)sender { @@ -6701,7 +7126,7 @@ index b6af4ef724f..5df2a69d79a 100644 BrowserWindowController *browserWindowController = [self frontmostBrowserWindowController]; if (browserWindowController) { -@@ -284,6 +327,9 @@ - (IBAction)openDocument:(id)sender +@@ -284,6 +333,9 @@ - (IBAction)openDocument:(id)sender - (void)didChangeSettings { @@ -6711,7 +7136,7 @@ index b6af4ef724f..5df2a69d79a 100644 [self _updateNewWindowKeyEquivalents]; // Let all of the BrowserWindowControllers know that a setting changed, so they can attempt to dynamically update. -@@ -312,6 +358,8 @@ - (void)_updateNewWindowKeyEquivalents +@@ -312,6 +364,8 @@ - (void)_updateNewWindowKeyEquivalents - (IBAction)showExtensionsManager:(id)sender { @@ -6720,7 +7145,7 @@ index b6af4ef724f..5df2a69d79a 100644 [_extensionManagerWindowController showWindow:sender]; } -@@ -345,4 +393,134 @@ - (IBAction)clearDefaultStoreWebsiteData:(id)sender +@@ -345,4 +399,135 @@ - (IBAction)clearDefaultStoreWebsiteData:(id)sender }]; } @@ -6763,6 +7188,7 @@ index b6af4ef724f..5df2a69d79a 100644 + + WKWebViewConfiguration *configuration = [self sessionConfiguration:sessionID]; + WKWebView* webView = [[WKWebView alloc] initWithFrame:[window.contentView bounds] configuration:configuration]; ++ webView._windowOcclusionDetectionEnabled = NO; + if (!webView) + return nil; + @@ -6885,7 +7311,7 @@ index 6f0949b0f4a..e774433031a 100644 @end diff --git a/Tools/MiniBrowser/mac/WK2BrowserWindowController.m b/Tools/MiniBrowser/mac/WK2BrowserWindowController.m -index 0063266ed33..346c3fe3704 100644 +index 0063266ed33..a884647b11f 100644 --- a/Tools/MiniBrowser/mac/WK2BrowserWindowController.m +++ b/Tools/MiniBrowser/mac/WK2BrowserWindowController.m @@ -72,6 +72,7 @@ @implementation WK2BrowserWindowController { @@ -6896,16 +7322,18 @@ index 0063266ed33..346c3fe3704 100644 BOOL _useShrinkToFit; -@@ -82,6 +83,8 @@ @implementation WK2BrowserWindowController { +@@ -82,7 +83,10 @@ @implementation WK2BrowserWindowController { - (void)awakeFromNib { + self.window.styleMask &= ~NSWindowStyleMaskFullSizeContentView; + _webView = [[WKWebView alloc] initWithFrame:[containerView bounds] configuration:_configuration]; ++ _webView._windowOcclusionDetectionEnabled = NO; [self didChangeSettings]; -@@ -105,7 +108,7 @@ - (void)awakeFromNib + _webView.allowsMagnification = YES; +@@ -105,7 +109,7 @@ - (void)awakeFromNib // telling WebKit to load every icon referenced by the page. if ([[SettingsController shared] loadsAllSiteIcons]) _webView._iconLoadingDelegate = self; @@ -6914,7 +7342,7 @@ index 0063266ed33..346c3fe3704 100644 _webView._observedRenderingProgressEvents = _WKRenderingProgressEventFirstLayout | _WKRenderingProgressEventFirstVisuallyNonEmptyLayout | _WKRenderingProgressEventFirstPaintWithSignificantArea -@@ -113,6 +116,7 @@ - (void)awakeFromNib +@@ -113,6 +117,7 @@ - (void)awakeFromNib | _WKRenderingProgressEventFirstPaintAfterSuppressedIncrementalRendering; _zoomTextOnly = NO; @@ -6922,7 +7350,7 @@ index 0063266ed33..346c3fe3704 100644 _webView._usePlatformFindUI = NO; -@@ -139,14 +143,10 @@ - (instancetype)initWithConfiguration:(WKWebViewConfiguration *)configuration +@@ -139,14 +144,10 @@ - (instancetype)initWithConfiguration:(WKWebViewConfiguration *)configuration - (void)dealloc { @@ -6937,7 +7365,7 @@ index 0063266ed33..346c3fe3704 100644 [_webView release]; [_configuration release]; -@@ -372,9 +372,15 @@ - (BOOL)windowShouldClose:(id)sender +@@ -372,9 +373,15 @@ - (BOOL)windowShouldClose:(id)sender - (void)windowWillClose:(NSNotification *)notification { [(BrowserAppDelegate *)[[NSApplication sharedApplication] delegate] browserWindowWillClose:self.window]; @@ -6953,7 +7381,7 @@ index 0063266ed33..346c3fe3704 100644 #define DefaultMinimumZoomFactor (.5) #define DefaultMaximumZoomFactor (3.0) #define DefaultZoomFactorRatio (1.2) -@@ -512,9 +518,11 @@ - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSStrin +@@ -512,9 +519,11 @@ - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSStrin [alert setInformativeText:message]; [alert addButtonWithTitle:@"OK"]; @@ -6965,7 +7393,7 @@ index 0063266ed33..346c3fe3704 100644 }]; } -@@ -528,9 +536,11 @@ - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSStr +@@ -528,9 +537,11 @@ - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSStr [alert addButtonWithTitle:@"OK"]; [alert addButtonWithTitle:@"Cancel"]; @@ -6977,7 +7405,7 @@ index 0063266ed33..346c3fe3704 100644 }]; } -@@ -548,13 +558,25 @@ - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSSt +@@ -548,13 +559,25 @@ - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSSt [input setStringValue:defaultText]; [alert setAccessoryView:input]; @@ -7003,7 +7431,7 @@ index 0063266ed33..346c3fe3704 100644 #if __has_feature(objc_generics) - (void)webView:(WKWebView *)webView runOpenPanelWithParameters:(WKOpenPanelParameters *)parameters initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSArray * URLs))completionHandler #else -@@ -848,4 +870,9 @@ - (IBAction)saveAsWebArchive:(id)sender +@@ -848,4 +871,9 @@ - (IBAction)saveAsWebArchive:(id)sender }]; } diff --git a/docs/api.md b/docs/api.md index afe0318acd..e0afffdce4 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1506,8 +1506,7 @@ Page is guaranteed to have a main frame which persists during navigations. - `width` <[number]> width of clipping area - `height` <[number]> height of clipping area - `omitBackground` <[boolean]> Hides default white background and allows capturing screenshots with transparency. Defaults to `false`. - - `encoding` <[string]> The encoding of the image, can be either `base64` or `binary`. Defaults to `binary`. -- returns: <[Promise]<[string]|[Buffer]>> Promise which resolves to buffer or a base64 string (depending on the value of `encoding`) with captured screenshot. +- returns: <[Promise]<[Buffer]>> Promise which resolves to buffer with the captured screenshot. > **NOTE** Screenshots take at least 1/6 second on OS X. See https://crbug.com/741689 for discussion. @@ -3456,8 +3455,12 @@ If `key` is a single character and no modifier keys besides `Shift` are being he > **NOTE** Modifier keys DO effect `elementHandle.press`. Holding down `Shift` will type the text in upper case. #### elementHandle.screenshot([options]) -- `options` <[Object]> Same options as in [page.screenshot](#pagescreenshotoptions). -- returns: <[Promise]<[string]|[Buffer]>> Promise which resolves to buffer or a base64 string (depending on the value of `options.encoding`) with captured screenshot. +- `options` <[Object]> Screenshot options. + - `path` <[string]> The file path to save the image to. The screenshot type will be inferred from file extension. If `path` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). If no path is provided, the image won't be saved to the disk. + - `type` <"png"|"jpeg"> Specify screenshot type, defaults to 'png'. + - `quality` <[number]> The quality of the image, between 0-100. Not applicable to `png` images. + - `omitBackground` <[boolean]> Hides default white background and allows capturing screenshots with transparency. Defaults to `false`. +- returns: <[Promise]<|[Buffer]>> Promise which resolves to buffer with the captured screenshot. This method scrolls element into view if needed, and then uses [page.screenshot](#pagescreenshotoptions) to take a screenshot of the element. If the element is detached from DOM, the method throws an error. diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000000..8b3a05e394 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +import * as chromium from './chromium'; +import * as firefox from './firefox'; +import * as webkit from './webkit'; +declare function pickBrowser(browser: 'chromium'): typeof chromium; +declare function pickBrowser(browser: 'firefox'): typeof firefox; +declare function pickBrowser(browser: 'webkit'): typeof webkit; +export = pickBrowser; \ No newline at end of file diff --git a/package.json b/package.json index 375a96b1f0..21d50f67c2 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "playwright": { "chromium_revision": "719491", "firefox_revision": "1004", - "webkit_revision": "1011" + "webkit_revision": "1016" }, "scripts": { "unit": "node test/test.js", @@ -18,7 +18,7 @@ "wunit": "cross-env BROWSER=webkit node test/test.js", "debug-unit": "node --inspect-brk test/test.js", "test-doclint": "node utils/doclint/check_public_api/test/test.js && node utils/doclint/preprocessor/test.js", - "test": "npm run lint --silent && npm run coverage && npm run test-doclint && npm run test-types && node utils/testrunner/test/test.js", + "test": "npm run lint --silent && npm run coverage && npm run test-doclint && node utils/testrunner/test/test.js", "prepare": "node install.js", "lint": "([ \"$CI\" = true ] && eslint --quiet -f codeframe --ext js,ts ./src || eslint --ext js,ts ./src) && npm run tsc && npm run doc", "doc": "node utils/doclint/cli.js", @@ -28,7 +28,6 @@ "watch": "node utils/runWebpack.js --mode='development' --watch --silent | tsc -w -p .", "apply-next-version": "node utils/apply_next_version.js", "bundle": "npx browserify -r ./index.js:playwright -o utils/browser/playwright-web.js", - "test-types": "node utils/doclint/generate_types && npx -p typescript@2.1 tsc -p utils/doclint/generate_types/test/", "unit-bundle": "node utils/browser/test.js" }, "author": { @@ -39,7 +38,9 @@ "debug": "^4.1.0", "extract-zip": "^1.6.6", "https-proxy-agent": "^3.0.0", + "jpeg-js": "^0.3.6", "mime": "^2.0.3", + "pngjs": "^3.4.0", "proxy-from-env": "^1.0.0", "rimraf": "^2.6.1", "ws": "^6.1.0" @@ -47,8 +48,10 @@ "devDependencies": { "@types/debug": "0.0.31", "@types/extract-zip": "^1.6.2", + "@types/jpeg-js": "^0.3.7", "@types/mime": "^2.0.0", "@types/node": "^8.10.34", + "@types/pngjs": "^3.4.0", "@types/rimraf": "^2.0.2", "@types/ws": "^6.0.1", "@typescript-eslint/eslint-plugin": "^2.6.1", @@ -57,12 +60,10 @@ "cross-env": "^5.0.5", "eslint": "^6.6.0", "esprima": "^4.0.0", - "jpeg-js": "^0.3.4", "minimist": "^1.2.0", "ncp": "^2.0.0", "node-stream-zip": "^1.8.2", "pixelmatch": "^4.0.2", - "pngjs": "^3.3.3", "progress": "^2.0.1", "text-diff": "^1.0.1", "ts-loader": "^6.1.2", diff --git a/src/webkit/BrowserFetcher.ts b/src/browserFetcher.ts similarity index 70% rename from src/webkit/BrowserFetcher.ts rename to src/browserFetcher.ts index 7b72dedb04..c61be4e7eb 100644 --- a/src/webkit/BrowserFetcher.ts +++ b/src/browserFetcher.ts @@ -18,36 +18,12 @@ import * as extract from 'extract-zip'; import * as fs from 'fs'; import * as ProxyAgent from 'https-proxy-agent'; -import * as os from 'os'; import * as path from 'path'; +// @ts-ignore import { getProxyForUrl } from 'proxy-from-env'; import * as removeRecursive from 'rimraf'; import * as URL from 'url'; -import * as util from 'util'; -import { assert, helper } from '../helper'; -import {execSync} from 'child_process'; - -const DEFAULT_DOWNLOAD_HOST = 'https://playwrightaccount.blob.core.windows.net'; - -const supportedPlatforms = ['linux', 'mac']; -const downloadURLs = { - linux: '%s/builds/webkit/%s/minibrowser-linux.zip', - mac: '%s/builds/webkit/%s/minibrowser-mac-%s.zip', -}; -let cachedMacVersion = undefined; -function getMacVersion() { - if (!cachedMacVersion) { - const [major, minor] = execSync('sw_vers -productVersion').toString('utf8').trim().split('.'); - cachedMacVersion = major + '.' + minor; - } - return cachedMacVersion; -} - -function downloadURL(platform: string, host: string, revision: string): string { - if (platform === 'mac') - return util.format(downloadURLs['mac'], host, revision, getMacVersion()); - return util.format(downloadURLs[platform], host, revision); -} +import { assert, helper } from './helper'; const readdirAsync = helper.promisify(fs.readdir.bind(fs)); const mkdirAsync = helper.promisify(fs.mkdir.bind(fs)); @@ -61,34 +37,23 @@ function existsAsync(filePath) { return promise; } +type ParamsGetter = (platform: string, revision: string) => { downloadUrl: string, executablePath: string }; + +export type OnProgressCallback = (downloadedBytes: number, totalBytes: number) => void; + export class BrowserFetcher { private _downloadsFolder: string; - private _downloadHost: string; private _platform: string; + private _params: ParamsGetter; - constructor(projectRoot: string, options: BrowserFetcherOptions = {}) { - this._downloadsFolder = options.path || path.join(projectRoot, '.local-webkit'); - this._downloadHost = options.host || DEFAULT_DOWNLOAD_HOST; - this._platform = options.platform || ''; - if (!this._platform) { - const platform = os.platform(); - if (platform === 'darwin') - this._platform = 'mac'; - else if (platform === 'linux') - this._platform = 'linux'; - else if (platform === 'win32') - this._platform = 'linux'; // Windows gets linux binaries and uses WSL - assert(this._platform, 'Unsupported platform: ' + os.platform()); - } - assert(supportedPlatforms.includes(this._platform), 'Unsupported platform: ' + this._platform); - } - - platform(): string { - return this._platform; + constructor(downloadsFolder: string, platform: string, params: ParamsGetter) { + this._downloadsFolder = downloadsFolder; + this._platform = platform; + this._params = params; } canDownload(revision: string): Promise { - const url = downloadURL(this._platform, this._downloadHost, revision); + const url = this._params(this._platform, revision).downloadUrl; let resolve; const promise = new Promise(x => resolve = x); const request = httpRequest(url, 'HEAD', response => { @@ -100,8 +65,9 @@ export class BrowserFetcher { }); return promise; } - async download(revision: string, progressCallback: ((arg0: number, arg1: number) => void) | null): Promise { - const url = downloadURL(this._platform, this._downloadHost, revision); + + async download(revision: string, progressCallback: OnProgressCallback | null): Promise { + const url = this._params(this._platform, revision).downloadUrl; const zipPath = path.join(this._downloadsFolder, `download-${this._platform}-${revision}.zip`); const folderPath = this._getFolderPath(revision); if (await existsAsync(folderPath)) @@ -136,14 +102,9 @@ export class BrowserFetcher { revisionInfo(revision: string): BrowserFetcherRevisionInfo { const folderPath = this._getFolderPath(revision); - let executablePath = ''; - if (this._platform === 'linux' || this._platform === 'mac') - executablePath = path.join(folderPath, 'pw_run.sh'); - else - throw new Error('Unsupported platform: ' + this._platform); - const url = downloadURL(this._platform, this._downloadHost, revision); + const params = this._params(this._platform, revision); const local = fs.existsSync(folderPath); - return {revision, executablePath, folderPath, local, url}; + return {revision, executablePath: path.join(folderPath, params.executablePath), folderPath, local, url: params.downloadUrl}; } _getFolderPath(revision: string): string { @@ -157,12 +118,10 @@ function parseFolderPath(folderPath: string): { platform: string; revision: stri if (splits.length !== 2) return null; const [platform, revision] = splits; - if (!supportedPlatforms.includes(platform)) - return null; return {platform, revision}; } -function downloadFile(url: string, destinationPath: string, progressCallback: ((arg0: number, arg1: number) => void) | null): Promise { +function downloadFile(url: string, destinationPath: string, progressCallback: OnProgressCallback | null): Promise { let fulfill, reject; let downloadedBytes = 0; let totalBytes = 0; @@ -244,7 +203,7 @@ export type BrowserFetcherOptions = { host ?: string, }; -type BrowserFetcherRevisionInfo = { +export type BrowserFetcherRevisionInfo = { folderPath: string, executablePath: string, url: string, diff --git a/src/chromium/Browser.ts b/src/chromium/Browser.ts index 2c65fdb9c3..cc616f4136 100644 --- a/src/chromium/Browser.ts +++ b/src/chromium/Browser.ts @@ -21,17 +21,17 @@ import { Events } from './events'; import { assert, helper } from '../helper'; import { BrowserContext } from './BrowserContext'; import { Connection, ConnectionEvents, CDPSession } from './Connection'; -import { Page, Viewport } from './Page'; +import { Page } from '../page'; import { Target } from './Target'; import { Protocol } from './protocol'; import { Chromium } from './features/chromium'; -import { Screenshotter } from './Screenshotter'; +import * as types from '../types'; +import { FrameManager } from './FrameManager'; export class Browser extends EventEmitter { private _ignoreHTTPSErrors: boolean; - private _defaultViewport: Viewport; + private _defaultViewport: types.Viewport; private _process: childProcess.ChildProcess; - private _screenshotter = new Screenshotter(); _connection: Connection; _client: CDPSession; private _closeCallback: () => Promise; @@ -44,7 +44,7 @@ export class Browser extends EventEmitter { connection: Connection, contextIds: string[], ignoreHTTPSErrors: boolean, - defaultViewport: Viewport | null, + defaultViewport: types.Viewport | null, process: childProcess.ChildProcess | null, closeCallback?: (() => Promise)) { const browser = new Browser(connection, contextIds, ignoreHTTPSErrors, defaultViewport, process, closeCallback); @@ -56,7 +56,7 @@ export class Browser extends EventEmitter { connection: Connection, contextIds: string[], ignoreHTTPSErrors: boolean, - defaultViewport: Viewport | null, + defaultViewport: types.Viewport | null, process: childProcess.ChildProcess | null, closeCallback?: (() => Promise)) { super(); @@ -107,7 +107,7 @@ export class Browser extends EventEmitter { const {browserContextId} = targetInfo; const context = (browserContextId && this._contexts.has(browserContextId)) ? this._contexts.get(browserContextId) : this._defaultContext; - const target = new Target(targetInfo, context, () => this._connection.createSession(targetInfo), this._ignoreHTTPSErrors, this._defaultViewport, this._screenshotter); + const target = new Target(targetInfo, context, () => this._connection.createSession(targetInfo), this._ignoreHTTPSErrors, this._defaultViewport); assert(!this._targets.has(event.targetInfo.targetId), 'Target should not exist before targetCreated'); this._targets.set(event.targetInfo.targetId, target); @@ -119,7 +119,7 @@ export class Browser extends EventEmitter { const target = this._targets.get(event.targetId); target._initializedCallback(false); this._targets.delete(event.targetId); - target._closedCallback(); + target._didClose(); if (await target._initializedPromise) this.chromium.emit(Events.Chromium.TargetDestroyed, target); } @@ -134,11 +134,11 @@ export class Browser extends EventEmitter { this.chromium.emit(Events.Chromium.TargetChanged, target); } - async newPage(): Promise { + async newPage(): Promise> { return this._defaultContext.newPage(); } - async _createPageInContext(contextId: string | null): Promise { + async _createPageInContext(contextId: string | null): Promise> { const { targetId } = await this._client.send('Target.createTarget', { url: 'about:blank', browserContextId: contextId || undefined }); const target = this._targets.get(targetId); assert(await target._initializedPromise, 'Failed to create target for page'); @@ -146,14 +146,24 @@ export class Browser extends EventEmitter { return page; } - async _closeTarget(target: Target) { - await this._client.send('Target.closeTarget', { targetId: target._targetId }); + async _closePage(page: Page) { + await this._client.send('Target.closeTarget', { targetId: Target.fromPage(page)._targetId }); } _allTargets(): Target[] { return Array.from(this._targets.values()).filter(target => target._isInitialized); } + async _pages(context: BrowserContext): Promise[]> { + const targets = this._allTargets().filter(target => target.browserContext() === context && target.type() === 'page'); + const pages = await Promise.all(targets.map(target => target.page())); + return pages.filter(page => !!page); + } + + async _activatePage(page: Page) { + await (page._delegate as FrameManager)._client.send('Target.activateTarget', {targetId: Target.fromPage(page)._targetId}); + } + async _waitForTarget(predicate: (arg0: Target) => boolean, options: { timeout?: number; } | undefined = {}): Promise { const { timeout = 30000 @@ -180,7 +190,7 @@ export class Browser extends EventEmitter { } } - async pages(): Promise { + async pages(): Promise[]> { const contextPages = await Promise.all(this.browserContexts().map(context => context.pages())); // Flatten array. return contextPages.reduce((acc, x) => acc.concat(x), []); diff --git a/src/chromium/BrowserContext.ts b/src/chromium/BrowserContext.ts index 7ae966dba2..b20ecd8fc3 100644 --- a/src/chromium/BrowserContext.ts +++ b/src/chromium/BrowserContext.ts @@ -20,8 +20,7 @@ import { filterCookies, NetworkCookie, rewriteCookies, SetNetworkCookieParam } f import { Browser } from './Browser'; import { CDPSession } from './Connection'; import { Permissions } from './features/permissions'; -import { Page } from './Page'; -import { Target } from './Target'; +import { Page } from '../page'; export class BrowserContext { readonly permissions: Permissions; @@ -35,24 +34,15 @@ export class BrowserContext { this.permissions = new Permissions(client, contextId); } - _targets(): Target[] { - return this._browser._allTargets().filter(target => target.browserContext() === this); - } - - async pages(): Promise { - const pages = await Promise.all( - this._targets() - .filter(target => target.type() === 'page') - .map(target => target.page()) - ); - return pages.filter(page => !!page); + pages(): Promise[]> { + return this._browser._pages(this); } isIncognito(): boolean { return !!this._id; } - newPage(): Promise { + newPage(): Promise> { return this._browser._createPageInContext(this._id); } diff --git a/src/chromium/BrowserFetcher.ts b/src/chromium/BrowserFetcher.ts deleted file mode 100644 index 10ca8ffad4..0000000000 --- a/src/chromium/BrowserFetcher.ts +++ /dev/null @@ -1,261 +0,0 @@ -/** - * Copyright 2017 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. - */ - -import * as extract from 'extract-zip'; -import * as fs from 'fs'; -import * as ProxyAgent from 'https-proxy-agent'; -import * as os from 'os'; -import * as path from 'path'; -// @ts-ignore -import { getProxyForUrl } from 'proxy-from-env'; -import * as removeRecursive from 'rimraf'; -import * as URL from 'url'; -import * as util from 'util'; -import { assert, helper } from '../helper'; - -const DEFAULT_DOWNLOAD_HOST = 'https://storage.googleapis.com'; - -const supportedPlatforms = ['mac', 'linux', 'win32', 'win64']; -const downloadURLs = { - linux: '%s/chromium-browser-snapshots/Linux_x64/%d/%s.zip', - mac: '%s/chromium-browser-snapshots/Mac/%d/%s.zip', - win32: '%s/chromium-browser-snapshots/Win/%d/%s.zip', - win64: '%s/chromium-browser-snapshots/Win_x64/%d/%s.zip', -}; - -function archiveName(platform: string, revision: string): string { - if (platform === 'linux') - return 'chrome-linux'; - if (platform === 'mac') - return 'chrome-mac'; - if (platform === 'win32' || platform === 'win64') { - // Windows archive name changed at r591479. - return parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32'; - } - return null; -} - -function downloadURL(platform: string, host: string, revision: string): string { - return util.format(downloadURLs[platform], host, revision, archiveName(platform, revision)); -} - -const readdirAsync = helper.promisify(fs.readdir.bind(fs)); -const mkdirAsync = helper.promisify(fs.mkdir.bind(fs)); -const unlinkAsync = helper.promisify(fs.unlink.bind(fs)); -const chmodAsync = helper.promisify(fs.chmod.bind(fs)); - -function existsAsync(filePath) { - let fulfill = null; - const promise = new Promise(x => fulfill = x); - fs.access(filePath, err => fulfill(!err)); - return promise; -} - -export class BrowserFetcher { - private _downloadsFolder: string; - private _downloadHost: string; - private _platform: string; - - constructor(projectRoot: string, options: BrowserFetcherOptions = {}) { - this._downloadsFolder = options.path || path.join(projectRoot, '.local-chromium'); - this._downloadHost = options.host || DEFAULT_DOWNLOAD_HOST; - this._platform = options.platform || ''; - if (!this._platform) { - const platform = os.platform(); - if (platform === 'darwin') - this._platform = 'mac'; - else if (platform === 'linux') - this._platform = 'linux'; - else if (platform === 'win32') - this._platform = os.arch() === 'x64' ? 'win64' : 'win32'; - assert(this._platform, 'Unsupported platform: ' + os.platform()); - } - assert(supportedPlatforms.includes(this._platform), 'Unsupported platform: ' + this._platform); - } - - platform(): string { - return this._platform; - } - - canDownload(revision: string): Promise { - const url = downloadURL(this._platform, this._downloadHost, revision); - let resolve; - const promise = new Promise(x => resolve = x); - const request = httpRequest(url, 'HEAD', response => { - resolve(response.statusCode === 200); - }); - request.on('error', error => { - console.error(error); - resolve(false); - }); - return promise; - } - async download(revision: string, progressCallback: ((arg0: number, arg1: number) => void) | null): Promise { - const url = downloadURL(this._platform, this._downloadHost, revision); - const zipPath = path.join(this._downloadsFolder, `download-${this._platform}-${revision}.zip`); - const folderPath = this._getFolderPath(revision); - if (await existsAsync(folderPath)) - return this.revisionInfo(revision); - if (!(await existsAsync(this._downloadsFolder))) - await mkdirAsync(this._downloadsFolder); - try { - await downloadFile(url, zipPath, progressCallback); - await extractZip(zipPath, folderPath); - } finally { - if (await existsAsync(zipPath)) - await unlinkAsync(zipPath); - } - const revisionInfo = this.revisionInfo(revision); - if (revisionInfo) - await chmodAsync(revisionInfo.executablePath, 0o755); - return revisionInfo; - } - - async localRevisions(): Promise { - if (!await existsAsync(this._downloadsFolder)) - return []; - const fileNames = await readdirAsync(this._downloadsFolder); - return fileNames.map(fileName => parseFolderPath(fileName)).filter(entry => entry && entry.platform === this._platform).map(entry => entry.revision); - } - - async remove(revision: string) { - const folderPath = this._getFolderPath(revision); - assert(await existsAsync(folderPath), `Failed to remove: revision ${revision} is not downloaded`); - await new Promise(fulfill => removeRecursive(folderPath, fulfill)); - } - - revisionInfo(revision: string): BrowserFetcherRevisionInfo { - const folderPath = this._getFolderPath(revision); - let executablePath = ''; - if (this._platform === 'mac') - executablePath = path.join(folderPath, archiveName(this._platform, revision), 'Chromium.app', 'Contents', 'MacOS', 'Chromium'); - else if (this._platform === 'linux') - executablePath = path.join(folderPath, archiveName(this._platform, revision), 'chrome'); - else if (this._platform === 'win32' || this._platform === 'win64') - executablePath = path.join(folderPath, archiveName(this._platform, revision), 'chrome.exe'); - else - throw new Error('Unsupported platform: ' + this._platform); - const url = downloadURL(this._platform, this._downloadHost, revision); - const local = fs.existsSync(folderPath); - return {revision, executablePath, folderPath, local, url}; - } - - _getFolderPath(revision: string): string { - return path.join(this._downloadsFolder, this._platform + '-' + revision); - } -} - -function parseFolderPath(folderPath: string): { platform: string; revision: string; } | null { - const name = path.basename(folderPath); - const splits = name.split('-'); - if (splits.length !== 2) - return null; - const [platform, revision] = splits; - if (!supportedPlatforms.includes(platform)) - return null; - return {platform, revision}; -} - -function downloadFile(url: string, destinationPath: string, progressCallback: ((arg0: number, arg1: number) => void) | null): Promise { - let fulfill, reject; - let downloadedBytes = 0; - let totalBytes = 0; - - const promise = new Promise((x, y) => { fulfill = x; reject = y; }); - - const request = httpRequest(url, 'GET', response => { - if (response.statusCode !== 200) { - const error = new Error(`Download failed: server returned code ${response.statusCode}. URL: ${url}`); - // consume response data to free up memory - response.resume(); - reject(error); - return; - } - const file = fs.createWriteStream(destinationPath); - file.on('finish', () => fulfill()); - file.on('error', error => reject(error)); - response.pipe(file); - totalBytes = parseInt(response.headers['content-length'], 10); - if (progressCallback) - response.on('data', onData); - }); - request.on('error', error => reject(error)); - return promise; - - function onData(chunk) { - downloadedBytes += chunk.length; - progressCallback(downloadedBytes, totalBytes); - } -} - -function extractZip(zipPath: string, folderPath: string): Promise { - return new Promise((fulfill, reject) => extract(zipPath, {dir: folderPath}, err => { - if (err) - reject(err); - else - fulfill(); - })); -} - -function httpRequest(url: string, method: string, response: (r: any) => void) { - let options: any = URL.parse(url); - options.method = method; - - const proxyURL = getProxyForUrl(url); - if (proxyURL) { - if (url.startsWith('http:')) { - const proxy = URL.parse(proxyURL); - options = { - path: options.href, - host: proxy.hostname, - port: proxy.port, - }; - } else { - const parsedProxyURL: any = URL.parse(proxyURL); - parsedProxyURL.secureProxy = parsedProxyURL.protocol === 'https:'; - - options.agent = new ProxyAgent(parsedProxyURL); - options.rejectUnauthorized = false; - } - } - - const requestCallback = res => { - if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) - httpRequest(res.headers.location, method, response); - else - response(res); - }; - const request = options.protocol === 'https:' ? - require('https').request(options, requestCallback) : - require('http').request(options, requestCallback); - request.end(); - return request; -} - -export type BrowserFetcherOptions = { - platform?: string, - path?: string, - host ?: string, -}; - -type BrowserFetcherRevisionInfo = { - folderPath: string, - executablePath: string, - url: string, - local: boolean, - revision: string, -}; diff --git a/src/chromium/EmulationManager.ts b/src/chromium/EmulationManager.ts deleted file mode 100644 index c6eeab9875..0000000000 --- a/src/chromium/EmulationManager.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright 2017 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. - */ - -import { CDPSession } from './Connection'; -import { Viewport } from './Page'; -import { Protocol } from './protocol'; - -export class EmulationManager { - private _client: CDPSession; - private _emulatingMobile = false; - private _hasTouch = false; - - constructor(client: CDPSession) { - this._client = client; - } - - async emulateViewport(viewport: Viewport): Promise { - const mobile = viewport.isMobile || false; - const width = viewport.width; - const height = viewport.height; - const deviceScaleFactor = viewport.deviceScaleFactor || 1; - const screenOrientation: Protocol.Emulation.ScreenOrientation = viewport.isLandscape ? { angle: 90, type: 'landscapePrimary' } : { angle: 0, type: 'portraitPrimary' }; - const hasTouch = viewport.hasTouch || false; - - await Promise.all([ - this._client.send('Emulation.setDeviceMetricsOverride', { mobile, width, height, deviceScaleFactor, screenOrientation }), - this._client.send('Emulation.setTouchEmulationEnabled', { - enabled: hasTouch - }) - ]); - - const reloadNeeded = this._emulatingMobile !== mobile || this._hasTouch !== hasTouch; - this._emulatingMobile = mobile; - this._hasTouch = hasTouch; - return reloadNeeded; - } -} diff --git a/src/chromium/ExecutionContext.ts b/src/chromium/ExecutionContext.ts index 89d69a952d..12a4443a48 100644 --- a/src/chromium/ExecutionContext.ts +++ b/src/chromium/ExecutionContext.ts @@ -149,7 +149,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate { await releaseObject(this._client, toRemoteObject(handle)); } - async handleJSONValue(handle: js.JSHandle): Promise { + async handleJSONValue(handle: js.JSHandle): Promise { const remoteObject = toRemoteObject(handle); if (remoteObject.objectId) { const response = await this._client.send('Runtime.callFunctionOn', { diff --git a/src/chromium/FrameManager.ts b/src/chromium/FrameManager.ts index f131c50683..352d3e9239 100644 --- a/src/chromium/FrameManager.ts +++ b/src/chromium/FrameManager.ts @@ -21,14 +21,30 @@ import * as frames from '../frames'; import { assert, debugError } from '../helper'; import * as js from '../javascript'; import * as network from '../network'; -import { TimeoutSettings } from '../TimeoutSettings'; import { CDPSession } from './Connection'; import { EVALUATION_SCRIPT_URL, ExecutionContextDelegate } from './ExecutionContext'; import { DOMWorldDelegate } from './JSHandle'; import { LifecycleWatcher } from './LifecycleWatcher'; -import { NetworkManager } from './NetworkManager'; -import { Page } from './Page'; +import { NetworkManager, NetworkManagerEvents } from './NetworkManager'; +import { Page } from '../page'; import { Protocol } from './protocol'; +import { Events as CommonEvents } from '../events'; +import { toConsoleMessageLocation, exceptionToError, releaseObject } from './protocolHelper'; +import * as dialog from '../dialog'; +import * as console from '../console'; +import { PageDelegate } from '../page'; +import { RawMouseImpl, RawKeyboardImpl } from './Input'; +import { CRScreenshotDelegate } from './Screenshotter'; +import { Accessibility } from './features/accessibility'; +import { Coverage } from './features/coverage'; +import { PDF } from './features/pdf'; +import { Workers } from './features/workers'; +import { Overrides } from './features/overrides'; +import { Interception } from './features/interception'; +import { Browser } from './Browser'; +import { BrowserContext } from './BrowserContext'; +import * as types from '../types'; +import * as input from '../input'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; @@ -47,32 +63,56 @@ type FrameData = { lifecycleEvents: Set, }; -export class FrameManager extends EventEmitter implements frames.FrameDelegate { +export class FrameManager extends EventEmitter implements frames.FrameDelegate, PageDelegate { _client: CDPSession; - private _page: Page; + private _page: Page; private _networkManager: NetworkManager; - _timeoutSettings: TimeoutSettings; private _frames = new Map(); private _contextIdToContext = new Map(); private _isolatedWorlds = new Set(); private _mainFrame: frames.Frame; + rawMouse: RawMouseImpl; + rawKeyboard: RawKeyboardImpl; + screenshotterDelegate: CRScreenshotDelegate; - constructor(client: CDPSession, page: Page, ignoreHTTPSErrors: boolean, timeoutSettings: TimeoutSettings) { + constructor(client: CDPSession, browserContext: BrowserContext, ignoreHTTPSErrors: boolean) { super(); this._client = client; - this._page = page; + this.rawKeyboard = new RawKeyboardImpl(client); + this.rawMouse = new RawMouseImpl(client); + this.screenshotterDelegate = new CRScreenshotDelegate(client); this._networkManager = new NetworkManager(client, ignoreHTTPSErrors, this); - this._timeoutSettings = timeoutSettings; + this._page = new Page(this, browserContext, ignoreHTTPSErrors); + (this._page as any).accessibility = new Accessibility(client); + (this._page as any).coverage = new Coverage(client); + (this._page as any).pdf = new PDF(client); + (this._page as any).workers = new Workers(client, this._page._addConsoleMessage.bind(this._page), error => this._page.emit(CommonEvents.Page.PageError, error)); + (this._page as any).overrides = new Overrides(client); + (this._page as any).interception = new Interception(this._networkManager); + this._networkManager.on(NetworkManagerEvents.Request, event => this._page.emit(CommonEvents.Page.Request, event)); + this._networkManager.on(NetworkManagerEvents.Response, event => this._page.emit(CommonEvents.Page.Response, event)); + this._networkManager.on(NetworkManagerEvents.RequestFailed, event => this._page.emit(CommonEvents.Page.RequestFailed, event)); + this._networkManager.on(NetworkManagerEvents.RequestFinished, event => this._page.emit(CommonEvents.Page.RequestFinished, event)); + + this._client.on('Inspector.targetCrashed', event => this._onTargetCrashed()); + this._client.on('Log.entryAdded', event => this._onLogEntryAdded(event)); + this._client.on('Page.domContentEventFired', event => this._page.emit(CommonEvents.Page.DOMContentLoaded)); + this._client.on('Page.fileChooserOpened', event => this._onFileChooserOpened(event)); this._client.on('Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId)); - this._client.on('Page.frameNavigated', event => this._onFrameNavigated(event.frame)); - this._client.on('Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)); this._client.on('Page.frameDetached', event => this._onFrameDetached(event.frameId)); + this._client.on('Page.frameNavigated', event => this._onFrameNavigated(event.frame)); this._client.on('Page.frameStoppedLoading', event => this._onFrameStoppedLoading(event.frameId)); + this._client.on('Page.javascriptDialogOpening', event => this._onDialog(event)); + this._client.on('Page.lifecycleEvent', event => this._onLifecycleEvent(event)); + this._client.on('Page.loadEventFired', event => this._page.emit(CommonEvents.Page.Load)); + this._client.on('Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)); + this._client.on('Runtime.bindingCalled', event => this._onBindingCalled(event)); + this._client.on('Runtime.consoleAPICalled', event => this._onConsoleAPI(event)); + this._client.on('Runtime.exceptionThrown', exception => this._handleException(exception.exceptionDetails)); this._client.on('Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context)); this._client.on('Runtime.executionContextDestroyed', event => this._onExecutionContextDestroyed(event.executionContextId)); this._client.on('Runtime.executionContextsCleared', event => this._onExecutionContextsCleared()); - this._client.on('Page.lifecycleEvent', event => this._onLifecycleEvent(event)); } async initialize() { @@ -82,6 +122,8 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { ]); this._handleFrameTree(frameTree); await Promise.all([ + this._client.send('Log.enable', {}), + this._client.send('Page.setInterceptFileChooserDialog', {enabled: true}), this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }), this._client.send('Runtime.enable', {}).then(() => this._ensureIsolatedWorld(UTILITY_WORLD_NAME)), this._networkManager.initialize(), @@ -104,7 +146,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { const { referer = this._networkManager.extraHTTPHeaders()['referer'], waitUntil = ['load'], - timeout = this._timeoutSettings.navigationTimeout(), + timeout = this._page._timeoutSettings.navigationTimeout(), } = options; const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout); @@ -142,7 +184,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { assertNoLegacyNavigationOptions(options); const { waitUntil = ['load'], - timeout = this._timeoutSettings.navigationTimeout(), + timeout = this._page._timeoutSettings.navigationTimeout(), } = options; const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout); const error = await Promise.race([ @@ -159,7 +201,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { async setFrameContent(frame: frames.Frame, html: string, options: frames.NavigateOptions = {}) { const { waitUntil = ['load'], - timeout = this._timeoutSettings.navigationTimeout(), + timeout = this._page._timeoutSettings.navigationTimeout(), } = options; const context = await frame._utilityContext(); // We rely upon the fact that document.open() will reset frame lifecycle with "init" @@ -213,7 +255,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { this._handleFrameTree(child); } - page(): Page { + page(): Page { return this._page; } @@ -234,7 +276,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { return; assert(parentFrameId); const parentFrame = this._frames.get(parentFrameId); - const frame = new frames.Frame(this, this._timeoutSettings, parentFrame); + const frame = new frames.Frame(this, this._page._timeoutSettings, parentFrame); const data: FrameData = { id: frameId, loaderId: '', @@ -243,6 +285,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { frame[frameDataSymbol] = data; this._frames.set(frameId, frame); this.emit(FrameManagerEvents.FrameAttached, frame); + this._page.emit(CommonEvents.Page.FrameAttached, frame); } _onFrameNavigated(framePayload: Protocol.Page.Frame) { @@ -265,7 +308,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { data.id = framePayload.id; } else { // Initial main frame navigation. - frame = new frames.Frame(this, this._timeoutSettings, null); + frame = new frames.Frame(this, this._page._timeoutSettings, null); const data: FrameData = { id: framePayload.id, loaderId: '', @@ -281,6 +324,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { frame._navigated(framePayload.url, framePayload.name); this.emit(FrameManagerEvents.FrameNavigated, frame); + this._page.emit(CommonEvents.Page.FrameNavigated, frame); } async _ensureIsolatedWorld(name: string) { @@ -305,6 +349,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { frame._navigated(url, frame.name()); this.emit(FrameManagerEvents.FrameNavigatedWithinDocument, frame); this.emit(FrameManagerEvents.FrameNavigated, frame); + this._page.emit(CommonEvents.Page.FrameNavigated, frame); } _onFrameDetached(frameId: string) { @@ -356,6 +401,155 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { frame._detach(); this._frames.delete(this._frameData(frame).id); this.emit(FrameManagerEvents.FrameDetached, frame); + this._page.emit(CommonEvents.Page.FrameDetached, frame); + } + + async _onConsoleAPI(event: Protocol.Runtime.consoleAPICalledPayload) { + if (event.executionContextId === 0) { + // DevTools protocol stores the last 1000 console messages. These + // messages are always reported even for removed execution contexts. In + // this case, they are marked with executionContextId = 0 and are + // reported upon enabling Runtime agent. + // + // Ignore these messages since: + // - there's no execution context we can use to operate with message + // arguments + // - these messages are reported before Playwright clients can subscribe + // to the 'console' + // page event. + // + // @see https://github.com/GoogleChrome/puppeteer/issues/3865 + return; + } + const context = this.executionContextById(event.executionContextId); + const values = event.args.map(arg => context._createHandle(arg)); + this._page._addConsoleMessage(event.type, values, toConsoleMessageLocation(event.stackTrace)); + } + + async exposeBinding(name: string, bindingFunction: string) { + await this._client.send('Runtime.addBinding', {name: name}); + await this._client.send('Page.addScriptToEvaluateOnNewDocument', {source: bindingFunction}); + await Promise.all(this.frames().map(frame => frame.evaluate(bindingFunction).catch(debugError))); + } + + _onBindingCalled(event: Protocol.Runtime.bindingCalledPayload) { + const context = this.executionContextById(event.executionContextId); + this._page._onBindingCalled(event.payload, context); + } + + _onDialog(event : Protocol.Page.javascriptDialogOpeningPayload) { + this._page.emit(CommonEvents.Page.Dialog, new dialog.Dialog( + event.type as dialog.DialogType, + event.message, + async (accept: boolean, promptText?: string) => { + await this._client.send('Page.handleJavaScriptDialog', { accept, promptText }); + }, + event.defaultPrompt)); + } + + _handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails) { + this._page.emit(CommonEvents.Page.PageError, exceptionToError(exceptionDetails)); + } + + _onTargetCrashed() { + this._page.emit('error', new Error('Page crashed!')); + } + + _onLogEntryAdded(event: Protocol.Log.entryAddedPayload) { + const {level, text, args, source, url, lineNumber} = event.entry; + if (args) + args.map(arg => releaseObject(this._client, arg)); + if (source !== 'worker') + this._page.emit(CommonEvents.Page.Console, new console.ConsoleMessage(level, text, [], {url, lineNumber})); + } + + async _onFileChooserOpened(event: Protocol.Page.fileChooserOpenedPayload) { + const frame = this.frame(event.frameId); + const utilityWorld = await frame._utilityDOMWorld(); + const handle = await (utilityWorld.delegate as DOMWorldDelegate).adoptBackendNodeId(event.backendNodeId, utilityWorld); + this._page._onFileChooserOpened(handle); + } + + setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise { + return this._networkManager.setExtraHTTPHeaders(extraHTTPHeaders); + } + + setUserAgent(userAgent: string): Promise { + return this._networkManager.setUserAgent(userAgent); + } + + async setJavaScriptEnabled(enabled: boolean): Promise { + await this._client.send('Emulation.setScriptExecutionDisabled', { value: !enabled }); + } + + async setBypassCSP(enabled: boolean): Promise { + await this._client.send('Page.setBypassCSP', { enabled }); + } + + async setViewport(viewport: types.Viewport): Promise { + const { + width, + height, + isMobile = false, + deviceScaleFactor = 1, + hasTouch = false, + isLandscape = false, + } = viewport; + const screenOrientation: Protocol.Emulation.ScreenOrientation = isLandscape ? { angle: 90, type: 'landscapePrimary' } : { angle: 0, type: 'portraitPrimary' }; + await Promise.all([ + this._client.send('Emulation.setDeviceMetricsOverride', { mobile: isMobile, width, height, deviceScaleFactor, screenOrientation }), + this._client.send('Emulation.setTouchEmulationEnabled', { + enabled: hasTouch + }) + ]); + } + + async setEmulateMedia(mediaType: input.MediaType | null, mediaColorScheme: input.MediaColorScheme | null): Promise { + const features = mediaColorScheme ? [{ name: 'prefers-color-scheme', value: mediaColorScheme }] : []; + await this._client.send('Emulation.setEmulatedMedia', { media: mediaType || '', features }); + } + + setCacheEnabled(enabled: boolean): Promise { + return this._networkManager.setCacheEnabled(enabled); + } + + async reload(options?: frames.NavigateOptions): Promise { + const [response] = await Promise.all([ + this._page.waitForNavigation(options), + this._client.send('Page.reload') + ]); + return response; + } + + private async _go(delta: number, options?: frames.NavigateOptions): Promise { + const history = await this._client.send('Page.getNavigationHistory'); + const entry = history.entries[history.currentIndex + delta]; + if (!entry) + return null; + const [response] = await Promise.all([ + this._page.waitForNavigation(options), + this._client.send('Page.navigateToHistoryEntry', {entryId: entry.id}), + ]); + return response; + } + + goBack(options?: frames.NavigateOptions): Promise { + return this._go(-1, options); + } + + goForward(options?: frames.NavigateOptions): Promise { + return this._go(+1, options); + } + + async evaluateOnNewDocument(source: string): Promise { + await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source }); + } + + async closePage(runBeforeUnload: boolean): Promise { + if (runBeforeUnload) + await this._client.send('Page.close'); + else + await this._page.browser()._closePage(this._page); } } diff --git a/src/chromium/JSHandle.ts b/src/chromium/JSHandle.ts index b6434f8f50..9e1c6918d8 100644 --- a/src/chromium/JSHandle.ts +++ b/src/chromium/JSHandle.ts @@ -23,7 +23,6 @@ import * as frames from '../frames'; import { CDPSession } from './Connection'; import { FrameManager } from './FrameManager'; import { Protocol } from './protocol'; -import { ScreenshotOptions } from './Screenshotter'; import { ExecutionContextDelegate } from './ExecutionContext'; export class DOMWorldDelegate implements dom.DOMWorldDelegate { @@ -51,7 +50,7 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate { } isJavascriptEnabled(): boolean { - return this._frameManager.page()._javascriptEnabled; + return this._frameManager.page()._state.javascriptEnabled; } isElement(remoteObject: any): boolean { @@ -91,20 +90,20 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate { return { width: layoutMetrics.layoutViewport.clientWidth, height: layoutMetrics.layoutViewport.clientHeight }; } - screenshot(handle: dom.ElementHandle, options: ScreenshotOptions = {}): Promise { + screenshot(handle: dom.ElementHandle, options?: types.ElementScreenshotOptions): Promise { const page = this._frameManager.page(); - return page._screenshotter.screenshotElement(page, handle, options); + return page._screenshotter.screenshotElement(handle, options); } async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise { await handle.evaluate(input.setFileInputFunction, files); } - async adoptElementHandle(handle: dom.ElementHandle, to: dom.DOMWorld): Promise { + async adoptElementHandle(handle: dom.ElementHandle, to: dom.DOMWorld): Promise> { const nodeInfo = await this._client.send('DOM.describeNode', { objectId: toRemoteObject(handle).objectId, }); - return this.adoptBackendNodeId(nodeInfo.node.backendNodeId, to); + return this.adoptBackendNodeId(nodeInfo.node.backendNodeId, to) as Promise>; } async adoptBackendNodeId(backendNodeId: Protocol.DOM.BackendNodeId, to: dom.DOMWorld): Promise { diff --git a/src/chromium/Launcher.ts b/src/chromium/Launcher.ts index faff28bc3b..22eb323c92 100644 --- a/src/chromium/Launcher.ts +++ b/src/chromium/Launcher.ts @@ -24,14 +24,15 @@ import * as readline from 'readline'; import * as removeFolder from 'rimraf'; import * as URL from 'url'; import { Browser } from './Browser'; -import { BrowserFetcher } from './BrowserFetcher'; +import { BrowserFetcher, BrowserFetcherOptions } from '../browserFetcher'; import { Connection } from './Connection'; import { TimeoutError } from '../Errors'; import { assert, debugError, helper } from '../helper'; -import { Viewport } from './Page'; +import * as types from '../types'; import { PipeTransport } from './PipeTransport'; import { WebSocketTransport } from './WebSocketTransport'; import { ConnectionTransport } from '../ConnectionTransport'; +import * as util from 'util'; const mkdtempAsync = helper.promisify(fs.mkdtemp); const removeFolderAsync = helper.promisify(removeFolder); @@ -289,7 +290,7 @@ export class Launcher { } _resolveExecutablePath(): { executablePath: string; missingText: string | null; } { - const browserFetcher = new BrowserFetcher(this._projectRoot); + const browserFetcher = createBrowserFetcher(this._projectRoot); const revisionInfo = browserFetcher.revisionInfo(this._preferredRevision); const missingText = !revisionInfo.local ? `Chromium revision is not downloaded. Run "npm install" or "yarn install"` : null; return {executablePath: revisionInfo.executablePath, missingText}; @@ -392,6 +393,55 @@ export type LauncherLaunchOptions = { export type LauncherBrowserOptions = { ignoreHTTPSErrors?: boolean, - defaultViewport?: Viewport | null, + defaultViewport?: types.Viewport | null, slowMo?: number, }; + +export function createBrowserFetcher(projectRoot: string, options: BrowserFetcherOptions = {}): BrowserFetcher { + const downloadURLs = { + linux: '%s/chromium-browser-snapshots/Linux_x64/%d/%s.zip', + mac: '%s/chromium-browser-snapshots/Mac/%d/%s.zip', + win32: '%s/chromium-browser-snapshots/Win/%d/%s.zip', + win64: '%s/chromium-browser-snapshots/Win_x64/%d/%s.zip', + }; + + const defaultOptions = { + path: path.join(projectRoot, '.local-chromium'), + host: 'https://storage.googleapis.com', + platform: (() => { + const platform = os.platform(); + if (platform === 'darwin') + return 'mac'; + if (platform === 'linux') + return 'linux'; + if (platform === 'win32') + return os.arch() === 'x64' ? 'win64' : 'win32'; + return platform; + })() + }; + options = { + ...defaultOptions, + ...options, + }; + assert(!!downloadURLs[options.platform], 'Unsupported platform: ' + options.platform); + + return new BrowserFetcher(options.path, options.platform, (platform: string, revision: string) => { + let archiveName = ''; + let executablePath = ''; + if (platform === 'linux') { + archiveName = 'chrome-linux'; + executablePath = path.join(archiveName, 'chrome'); + } else if (platform === 'mac') { + archiveName = 'chrome-mac'; + executablePath = path.join(archiveName, 'Chromium.app', 'Contents', 'MacOS', 'Chromium'); + } else if (platform === 'win32' || platform === 'win64') { + // Windows archive name changed at r591479. + archiveName = parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32'; + executablePath = path.join(archiveName, 'chrome.exe'); + } + return { + downloadUrl: util.format(downloadURLs[platform], options.host, revision, archiveName), + executablePath + }; + }); +} diff --git a/src/chromium/Page.ts b/src/chromium/Page.ts deleted file mode 100644 index dcd20a7b84..0000000000 --- a/src/chromium/Page.ts +++ /dev/null @@ -1,591 +0,0 @@ -/** - * Copyright 2017 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. - */ - -import { EventEmitter } from 'events'; -import { assert, debugError, helper } from '../helper'; -import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption, mediaTypes, mediaColorSchemes } from '../input'; -import { TimeoutSettings } from '../TimeoutSettings'; -import { Browser } from './Browser'; -import { BrowserContext } from './BrowserContext'; -import { CDPSession, CDPSessionEvents } from './Connection'; -import { EmulationManager } from './EmulationManager'; -import { Events } from './events'; -import { Accessibility } from './features/accessibility'; -import { Coverage } from './features/coverage'; -import { Overrides } from './features/overrides'; -import { Interception } from './features/interception'; -import { PDF } from './features/pdf'; -import { Workers } from './features/workers'; -import { FrameManager, FrameManagerEvents } from './FrameManager'; -import { RawMouseImpl, RawKeyboardImpl } from './Input'; -import { NetworkManagerEvents } from './NetworkManager'; -import { Protocol } from './protocol'; -import { getExceptionMessage, releaseObject } from './protocolHelper'; -import { Target } from './Target'; -import * as input from '../input'; -import * as types from '../types'; -import * as frames from '../frames'; -import * as js from '../javascript'; -import * as dom from '../dom'; -import * as network from '../network'; -import * as dialog from '../dialog'; -import * as console from '../console'; -import { DOMWorldDelegate } from './JSHandle'; -import { Screenshotter, ScreenshotOptions } from './Screenshotter'; - -export type Viewport = { - width: number; - height: number; - deviceScaleFactor?: number; - isMobile?: boolean; - isLandscape?: boolean; - hasTouch?: boolean; -} - -export class Page extends EventEmitter { - private _closed = false; - _client: CDPSession; - _target: Target; - private _keyboard: input.Keyboard; - private _mouse: input.Mouse; - private _timeoutSettings: TimeoutSettings; - private _frameManager: FrameManager; - private _emulationManager: EmulationManager; - readonly accessibility: Accessibility; - readonly coverage: Coverage; - readonly overrides: Overrides; - readonly interception: Interception; - readonly pdf: PDF; - readonly workers: Workers; - private _pageBindings = new Map(); - _javascriptEnabled = true; - private _viewport: Viewport | null = null; - _screenshotter: Screenshotter; - private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>(); - private _disconnectPromise: Promise | undefined; - private _emulatedMediaType: string | undefined; - - static async create(client: CDPSession, target: Target, ignoreHTTPSErrors: boolean, defaultViewport: Viewport | null, screenshotter: Screenshotter): Promise { - const page = new Page(client, target, ignoreHTTPSErrors, screenshotter); - await page._initialize(); - if (defaultViewport) - await page.setViewport(defaultViewport); - return page; - } - - constructor(client: CDPSession, target: Target, ignoreHTTPSErrors: boolean, screenshotter: Screenshotter) { - super(); - this._client = client; - this._target = target; - this._keyboard = new input.Keyboard(new RawKeyboardImpl(client)); - this._mouse = new input.Mouse(new RawMouseImpl(client), this._keyboard); - this._timeoutSettings = new TimeoutSettings(); - this.accessibility = new Accessibility(client); - this._frameManager = new FrameManager(client, this, ignoreHTTPSErrors, this._timeoutSettings); - this._emulationManager = new EmulationManager(client); - this.coverage = new Coverage(client); - this.pdf = new PDF(client); - this.workers = new Workers(client, this._addConsoleMessage.bind(this), this._handleException.bind(this)); - this.overrides = new Overrides(client); - this.interception = new Interception(this._frameManager.networkManager()); - - this._screenshotter = screenshotter; - - client.on('Target.attachedToTarget', event => { - if (event.targetInfo.type !== 'worker') { - // If we don't detach from service workers, they will never die. - client.send('Target.detachFromTarget', { - sessionId: event.sessionId - }).catch(debugError); - return; - } - }); - - this._frameManager.on(FrameManagerEvents.FrameAttached, event => this.emit(Events.Page.FrameAttached, event)); - this._frameManager.on(FrameManagerEvents.FrameDetached, event => this.emit(Events.Page.FrameDetached, event)); - this._frameManager.on(FrameManagerEvents.FrameNavigated, event => this.emit(Events.Page.FrameNavigated, event)); - - const networkManager = this._frameManager.networkManager(); - networkManager.on(NetworkManagerEvents.Request, event => this.emit(Events.Page.Request, event)); - networkManager.on(NetworkManagerEvents.Response, event => this.emit(Events.Page.Response, event)); - networkManager.on(NetworkManagerEvents.RequestFailed, event => this.emit(Events.Page.RequestFailed, event)); - networkManager.on(NetworkManagerEvents.RequestFinished, event => this.emit(Events.Page.RequestFinished, event)); - - client.on('Page.domContentEventFired', event => this.emit(Events.Page.DOMContentLoaded)); - client.on('Page.loadEventFired', event => this.emit(Events.Page.Load)); - client.on('Runtime.consoleAPICalled', event => this._onConsoleAPI(event)); - client.on('Runtime.bindingCalled', event => this._onBindingCalled(event)); - client.on('Page.javascriptDialogOpening', event => this._onDialog(event)); - client.on('Runtime.exceptionThrown', exception => this._handleException(exception.exceptionDetails)); - client.on('Inspector.targetCrashed', event => this._onTargetCrashed()); - client.on('Log.entryAdded', event => this._onLogEntryAdded(event)); - client.on('Page.fileChooserOpened', event => this._onFileChooserOpened(event)); - this._target._isClosedPromise.then(() => { - this.emit(Events.Page.Close); - this._closed = true; - }); - } - - async _initialize() { - await Promise.all([ - this._frameManager.initialize(), - this._client.send('Target.setAutoAttach', {autoAttach: true, waitForDebuggerOnStart: false, flatten: true}), - this._client.send('Performance.enable', {}), - this._client.send('Log.enable', {}), - this._client.send('Page.setInterceptFileChooserDialog', {enabled: true}) - ]); - } - - async _onFileChooserOpened(event: Protocol.Page.fileChooserOpenedPayload) { - if (!this._fileChooserInterceptors.size) - return; - const frame = this._frameManager.frame(event.frameId); - const utilityWorld = await frame._utilityDOMWorld(); - const handle = await (utilityWorld.delegate as DOMWorldDelegate).adoptBackendNodeId(event.backendNodeId, utilityWorld); - const interceptors = Array.from(this._fileChooserInterceptors); - this._fileChooserInterceptors.clear(); - const multiple = await handle.evaluate((element: HTMLInputElement) => !!element.multiple); - const fileChooser = { element: handle, multiple }; - for (const interceptor of interceptors) - interceptor.call(null, fileChooser); - this.emit(Events.Page.FileChooser, fileChooser); - } - - async waitForFileChooser(options: { timeout?: number; } = {}): Promise { - const { - timeout = this._timeoutSettings.timeout(), - } = options; - let callback; - const promise = new Promise(x => callback = x); - this._fileChooserInterceptors.add(callback); - return helper.waitWithTimeout(promise, 'waiting for file chooser', timeout).catch(e => { - this._fileChooserInterceptors.delete(callback); - throw e; - }); - } - - browser(): Browser { - return this._target.browser(); - } - - browserContext(): BrowserContext { - return this._target.browserContext(); - } - - _onTargetCrashed() { - this.emit('error', new Error('Page crashed!')); - } - - _onLogEntryAdded(event: Protocol.Log.entryAddedPayload) { - const {level, text, args, source, url, lineNumber} = event.entry; - if (args) - args.map(arg => releaseObject(this._client, arg)); - if (source !== 'worker') - this.emit(Events.Page.Console, new console.ConsoleMessage(level, text, [], {url, lineNumber})); - } - - mainFrame(): frames.Frame { - return this._frameManager.mainFrame(); - } - - get keyboard(): input.Keyboard { - return this._keyboard; - } - - frames(): frames.Frame[] { - return this._frameManager.frames(); - } - - setDefaultNavigationTimeout(timeout: number) { - this._timeoutSettings.setDefaultNavigationTimeout(timeout); - } - - setDefaultTimeout(timeout: number) { - this._timeoutSettings.setDefaultTimeout(timeout); - } - - async $(selector: string | types.Selector): Promise { - return this.mainFrame().$(selector); - } - - evaluateHandle: types.EvaluateHandle = async (pageFunction, ...args) => { - const context = await this.mainFrame().executionContext(); - return context.evaluateHandle(pageFunction, ...args as any); - } - - $eval: types.$Eval = (selector, pageFunction, ...args) => { - return this.mainFrame().$eval(selector, pageFunction, ...args as any); - } - - $$eval: types.$$Eval = (selector, pageFunction, ...args) => { - return this.mainFrame().$$eval(selector, pageFunction, ...args as any); - } - - async $$(selector: string | types.Selector): Promise { - return this.mainFrame().$$(selector); - } - - async $x(expression: string): Promise { - return this.mainFrame().$x(expression); - } - - async addScriptTag(options: { url?: string; path?: string; content?: string; type?: string; }): Promise { - return this.mainFrame().addScriptTag(options); - } - - async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise { - return this.mainFrame().addStyleTag(options); - } - - async exposeFunction(name: string, playwrightFunction: Function) { - if (this._pageBindings.has(name)) - throw new Error(`Failed to add page binding with name ${name}: window['${name}'] already exists!`); - this._pageBindings.set(name, playwrightFunction); - - const expression = helper.evaluationString(addPageBinding, name); - await this._client.send('Runtime.addBinding', {name: name}); - await this._client.send('Page.addScriptToEvaluateOnNewDocument', {source: expression}); - await Promise.all(this.frames().map(frame => frame.evaluate(expression).catch(debugError))); - - function addPageBinding(bindingName: string) { - const binding = window[bindingName]; - window[bindingName] = (...args) => { - const me = window[bindingName]; - let callbacks = me['callbacks']; - if (!callbacks) { - callbacks = new Map(); - me['callbacks'] = callbacks; - } - const seq = (me['lastSeq'] || 0) + 1; - me['lastSeq'] = seq; - const promise = new Promise((resolve, reject) => callbacks.set(seq, {resolve, reject})); - binding(JSON.stringify({name: bindingName, seq, args})); - return promise; - }; - } - } - - async setExtraHTTPHeaders(headers: { [s: string]: string; }) { - return this._frameManager.networkManager().setExtraHTTPHeaders(headers); - } - - async setUserAgent(userAgent: string) { - return this._frameManager.networkManager().setUserAgent(userAgent); - } - - _handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails) { - const message = getExceptionMessage(exceptionDetails); - const err = new Error(message); - err.stack = ''; // Don't report clientside error with a node stack attached - this.emit(Events.Page.PageError, err); - } - - async _onConsoleAPI(event: Protocol.Runtime.consoleAPICalledPayload) { - if (event.executionContextId === 0) { - // DevTools protocol stores the last 1000 console messages. These - // messages are always reported even for removed execution contexts. In - // this case, they are marked with executionContextId = 0 and are - // reported upon enabling Runtime agent. - // - // Ignore these messages since: - // - there's no execution context we can use to operate with message - // arguments - // - these messages are reported before Playwright clients can subscribe - // to the 'console' - // page event. - // - // @see https://github.com/GoogleChrome/puppeteer/issues/3865 - return; - } - const context = this._frameManager.executionContextById(event.executionContextId); - const values = event.args.map(arg => context._createHandle(arg)); - this._addConsoleMessage(event.type, values, event.stackTrace); - } - - async _onBindingCalled(event: Protocol.Runtime.bindingCalledPayload) { - const {name, seq, args} = JSON.parse(event.payload); - let expression = null; - try { - const result = await this._pageBindings.get(name)(...args); - expression = helper.evaluationString(deliverResult, name, seq, result); - } catch (error) { - if (error instanceof Error) - expression = helper.evaluationString(deliverError, name, seq, error.message, error.stack); - else - expression = helper.evaluationString(deliverErrorValue, name, seq, error); - } - this._client.send('Runtime.evaluate', { expression, contextId: event.executionContextId }).catch(debugError); - - function deliverResult(name: string, seq: number, result: any) { - window[name]['callbacks'].get(seq).resolve(result); - window[name]['callbacks'].delete(seq); - } - - function deliverError(name: string, seq: number, message: string, stack: string) { - const error = new Error(message); - error.stack = stack; - window[name]['callbacks'].get(seq).reject(error); - window[name]['callbacks'].delete(seq); - } - - function deliverErrorValue(name: string, seq: number, value: any) { - window[name]['callbacks'].get(seq).reject(value); - window[name]['callbacks'].delete(seq); - } - } - - _addConsoleMessage(type: string, args: js.JSHandle[], stackTrace: Protocol.Runtime.StackTrace | undefined) { - if (!this.listenerCount(Events.Page.Console)) { - args.forEach(arg => arg.dispose()); - return; - } - const location = stackTrace && stackTrace.callFrames.length ? { - url: stackTrace.callFrames[0].url, - lineNumber: stackTrace.callFrames[0].lineNumber, - columnNumber: stackTrace.callFrames[0].columnNumber, - } : {}; - this.emit(Events.Page.Console, new console.ConsoleMessage(type, undefined, args, location)); - } - - _onDialog(event : Protocol.Page.javascriptDialogOpeningPayload) { - this.emit(Events.Page.Dialog, new dialog.Dialog( - event.type as dialog.DialogType, - event.message, - async (accept: boolean, promptText?: string) => { - await this._client.send('Page.handleJavaScriptDialog', { accept, promptText }); - }, - event.defaultPrompt)); - } - - url(): string { - return this.mainFrame().url(); - } - - async content(): Promise { - return await this._frameManager.mainFrame().content(); - } - - async setContent(html: string, options: { timeout?: number; waitUntil?: string | string[]; } | undefined) { - await this._frameManager.mainFrame().setContent(html, options); - } - - async goto(url: string, options: { referer?: string; timeout?: number; waitUntil?: string | string[]; } | undefined): Promise { - return await this._frameManager.mainFrame().goto(url, options); - } - - async reload(options: { timeout?: number; waitUntil?: string | string[]; } = {}): Promise { - const [response] = await Promise.all([ - this.waitForNavigation(options), - this._client.send('Page.reload') - ]); - return response; - } - - async waitForNavigation(options: { timeout?: number; waitUntil?: string | string[]; } = {}): Promise { - return await this._frameManager.mainFrame().waitForNavigation(options); - } - - _sessionClosePromise() { - if (!this._disconnectPromise) - this._disconnectPromise = new Promise(fulfill => this._client.once(CDPSessionEvents.Disconnected, () => fulfill(new Error('Target closed')))); - return this._disconnectPromise; - } - - async waitForRequest(urlOrPredicate: (string | Function), options: { timeout?: number; } = {}): Promise { - const { - timeout = this._timeoutSettings.timeout(), - } = options; - return helper.waitForEvent(this._frameManager.networkManager(), NetworkManagerEvents.Request, request => { - if (helper.isString(urlOrPredicate)) - return (urlOrPredicate === request.url()); - if (typeof urlOrPredicate === 'function') - return !!(urlOrPredicate(request)); - return false; - }, timeout, this._sessionClosePromise()); - } - - async waitForResponse(urlOrPredicate: (string | Function), options: { timeout?: number; } = {}): Promise { - const { - timeout = this._timeoutSettings.timeout(), - } = options; - return helper.waitForEvent(this._frameManager.networkManager(), NetworkManagerEvents.Response, response => { - if (helper.isString(urlOrPredicate)) - return (urlOrPredicate === response.url()); - if (typeof urlOrPredicate === 'function') - return !!(urlOrPredicate(response)); - return false; - }, timeout, this._sessionClosePromise()); - } - - async goBack(options: { timeout?: number; waitUntil?: string | string[]; } | undefined): Promise { - return this._go(-1, options); - } - - async goForward(options: { timeout?: number; waitUntil?: string | string[]; } | undefined): Promise { - return this._go(+1, options); - } - - async _go(delta, options: { timeout?: number; waitUntil?: string | string[]; } | undefined): Promise { - const history = await this._client.send('Page.getNavigationHistory'); - const entry = history.entries[history.currentIndex + delta]; - if (!entry) - return null; - const [response] = await Promise.all([ - this.waitForNavigation(options), - this._client.send('Page.navigateToHistoryEntry', {entryId: entry.id}), - ]); - return response; - } - - async emulate(options: { viewport: Viewport; userAgent: string; }) { - await Promise.all([ - this.setViewport(options.viewport), - this.setUserAgent(options.userAgent) - ]); - } - - async setJavaScriptEnabled(enabled: boolean) { - if (this._javascriptEnabled === enabled) - return; - this._javascriptEnabled = enabled; - await this._client.send('Emulation.setScriptExecutionDisabled', { value: !enabled }); - } - - async setBypassCSP(enabled: boolean) { - await this._client.send('Page.setBypassCSP', { enabled }); - } - - async emulateMedia(options: { - type?: string, - colorScheme?: 'dark' | 'light' | 'no-preference' }) { - assert(!options.type || mediaTypes.has(options.type), 'Unsupported media type: ' + options.type); - assert(!options.colorScheme || mediaColorSchemes.has(options.colorScheme), 'Unsupported color scheme: ' + options.colorScheme); - const media = typeof options.type === 'undefined' ? this._emulatedMediaType : options.type; - const features = typeof options.colorScheme === 'undefined' ? [] : [{ name: 'prefers-color-scheme', value: options.colorScheme }]; - await this._client.send('Emulation.setEmulatedMedia', { media: media || '', features }); - this._emulatedMediaType = options.type; - } - - async setViewport(viewport: Viewport) { - const needsReload = await this._emulationManager.emulateViewport(viewport); - this._viewport = viewport; - if (needsReload) - await this.reload(); - } - - viewport(): Viewport | null { - return this._viewport; - } - - evaluate: types.Evaluate = (pageFunction, ...args) => { - return this._frameManager.mainFrame().evaluate(pageFunction, ...args as any); - } - - async evaluateOnNewDocument(pageFunction: Function | string, ...args: any[]) { - const source = helper.evaluationString(pageFunction, ...args); - await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source }); - } - - async setCacheEnabled(enabled: boolean = true) { - await this._frameManager.networkManager().setCacheEnabled(enabled); - } - - screenshot(options: ScreenshotOptions = {}): Promise { - return this._screenshotter.screenshotPage(this, options); - } - - async title(): Promise { - return this.mainFrame().title(); - } - - async close(options: { runBeforeUnload: (boolean | undefined); } = {runBeforeUnload: undefined}) { - assert(!!this._client._connection, 'Protocol error: Connection closed. Most likely the page has been closed.'); - const runBeforeUnload = !!options.runBeforeUnload; - if (runBeforeUnload) { - await this._client.send('Page.close'); - } else { - await this.browser()._closeTarget(this._target); - await this._target._isClosedPromise; - } - } - - isClosed(): boolean { - return this._closed; - } - - get mouse(): input.Mouse { - return this._mouse; - } - - click(selector: string | types.Selector, options?: ClickOptions) { - return this.mainFrame().click(selector, options); - } - - dblclick(selector: string | types.Selector, options?: MultiClickOptions) { - return this.mainFrame().dblclick(selector, options); - } - - tripleclick(selector: string | types.Selector, options?: MultiClickOptions) { - return this.mainFrame().tripleclick(selector, options); - } - - fill(selector: string | types.Selector, value: string) { - return this.mainFrame().fill(selector, value); - } - - focus(selector: string | types.Selector) { - return this.mainFrame().focus(selector); - } - - hover(selector: string | types.Selector, options?: PointerActionOptions) { - return this.mainFrame().hover(selector, options); - } - - select(selector: string | types.Selector, ...values: (string | dom.ElementHandle | SelectOption)[]): Promise { - return this.mainFrame().select(selector, ...values); - } - - type(selector: string | types.Selector, text: string, options: { delay: (number | undefined); } | undefined) { - return this.mainFrame().type(selector, text, options); - } - - waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: { visible?: boolean; hidden?: boolean; timeout?: number; polling?: string | number; } = {}, ...args: any[]): Promise { - return this.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args); - } - - waitForSelector(selector: string | types.Selector, options: types.TimeoutOptions = {}): Promise { - return this.mainFrame().waitForSelector(selector, options); - } - - waitForXPath(xpath: string, options: types.TimeoutOptions = {}): Promise { - return this.mainFrame().waitForXPath(xpath, options); - } - - waitForFunction(pageFunction: Function | string, options: types.WaitForFunctionOptions, ...args: any[]): Promise { - return this.mainFrame().waitForFunction(pageFunction, options, ...args); - } -} - -type MediaFeature = { - name: string, - value: string -} - -type FileChooser = { - element: dom.ElementHandle, - multiple: boolean -}; diff --git a/src/chromium/Playwright.ts b/src/chromium/Playwright.ts index 076af17086..588c257cfb 100644 --- a/src/chromium/Playwright.ts +++ b/src/chromium/Playwright.ts @@ -15,27 +15,31 @@ * limitations under the License. */ import { Browser } from './Browser'; -import { BrowserFetcher, BrowserFetcherOptions } from './BrowserFetcher'; +import { BrowserFetcher, BrowserFetcherOptions, BrowserFetcherRevisionInfo, OnProgressCallback } from '../browserFetcher'; import { ConnectionTransport } from '../ConnectionTransport'; import { DeviceDescriptors } from '../DeviceDescriptors'; import * as Errors from '../Errors'; -import { Launcher, LauncherBrowserOptions, LauncherChromeArgOptions, LauncherLaunchOptions } from './Launcher'; -import {download, RevisionInfo} from '../download'; +import { Launcher, LauncherBrowserOptions, LauncherChromeArgOptions, LauncherLaunchOptions, createBrowserFetcher } from './Launcher'; export class Playwright { private _projectRoot: string; private _launcher: Launcher; readonly _revision: string; - downloadBrowser: (options?: { onProgress?: (downloadedBytes: number, totalBytes: number) => void; }) => Promise; constructor(projectRoot: string, preferredRevision: string) { this._projectRoot = projectRoot; this._launcher = new Launcher(projectRoot, preferredRevision); this._revision = preferredRevision; - this.downloadBrowser = download.bind(null, this.createBrowserFetcher(), preferredRevision, 'Chromium'); } - launch(options: (LauncherLaunchOptions & LauncherChromeArgOptions & LauncherBrowserOptions) | undefined): Promise { + async downloadBrowser(options?: BrowserFetcherOptions & { onProgress?: OnProgressCallback }): Promise { + const fetcher = this.createBrowserFetcher(options); + const revisionInfo = fetcher.revisionInfo(this._revision); + await fetcher.download(this._revision, options ? options.onProgress : undefined); + return revisionInfo; + } + + launch(options?: (LauncherLaunchOptions & LauncherChromeArgOptions & LauncherBrowserOptions) | undefined): Promise { return this._launcher.launch(options); } @@ -65,7 +69,7 @@ export class Playwright { return this._launcher.defaultArgs(options); } - createBrowserFetcher(options?: BrowserFetcherOptions | undefined): BrowserFetcher { - return new BrowserFetcher(this._projectRoot, options); + createBrowserFetcher(options?: BrowserFetcherOptions): BrowserFetcher { + return createBrowserFetcher(this._projectRoot, options); } } diff --git a/src/chromium/Screenshotter.ts b/src/chromium/Screenshotter.ts index fe5666b184..c5203472bd 100644 --- a/src/chromium/Screenshotter.ts +++ b/src/chromium/Screenshotter.ts @@ -1,181 +1,40 @@ -/** - * 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. - */ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. -import * as fs from 'fs'; -import { Page } from './Page'; -import { assert, helper } from '../helper'; -import * as mime from 'mime'; -import { Protocol } from './protocol'; import * as dom from '../dom'; +import { ScreenshotterDelegate } from '../screenshotter'; +import * as types from '../types'; +import { CDPSession } from './api'; -const writeFileAsync = helper.promisify(fs.writeFile); +export class CRScreenshotDelegate implements ScreenshotterDelegate { + private _session: CDPSession; -export type ScreenshotOptions = { - type?: 'png' | 'jpeg', - path?: string, - fullPage?: boolean, - clip?: {x: number, y: number, width: number, height: number}, - quality?: number, - omitBackground?: boolean, - encoding?: string, -} - -export class Screenshotter { - private _queue = new TaskQueue(); - - async screenshotPage(page: Page, options: ScreenshotOptions = {}): Promise { - const format = this._format(options); - return this._queue.postTask(() => this._screenshot(page, format, options)); + constructor(session: CDPSession) { + this._session = session; } - async screenshotElement(page: Page, handle: dom.ElementHandle, options: ScreenshotOptions = {}): Promise { - const format = this._format(options); - return this._queue.postTask(async () => { - let needsViewportReset = false; - let boundingBox = await handle.boundingBox(); - assert(boundingBox, 'Node is either not visible or not an HTMLElement'); - - const viewport = page.viewport(); - - if (viewport && (boundingBox.width > viewport.width || boundingBox.height > viewport.height)) { - const newViewport = { - width: Math.max(viewport.width, Math.ceil(boundingBox.width)), - height: Math.max(viewport.height, Math.ceil(boundingBox.height)), - }; - await page.setViewport(Object.assign({}, viewport, newViewport)); - - needsViewportReset = true; - } - - await handle._scrollIntoViewIfNeeded(); - - boundingBox = await handle.boundingBox(); - assert(boundingBox, 'Node is either not visible or not an HTMLElement'); - assert(boundingBox.width !== 0, 'Node has 0 width.'); - assert(boundingBox.height !== 0, 'Node has 0 height.'); - - const { layoutViewport: { pageX, pageY } } = await page._client.send('Page.getLayoutMetrics'); - - const clip = Object.assign({}, boundingBox); - clip.x += pageX; - clip.y += pageY; - - const imageData = await this._screenshot(page, format, {...options, clip}); - - if (needsViewportReset) - await page.setViewport(viewport); - - return imageData; - }); + async getBoundingBox(handle: dom.ElementHandle): Promise { + const rect = await handle.boundingBox(); + if (!rect) + return rect; + const { layoutViewport: { pageX, pageY } } = await this._session.send('Page.getLayoutMetrics'); + rect.x += pageX; + rect.y += pageY; + return rect; } - private async _screenshot(page: Page, format: 'png' | 'jpeg', options: ScreenshotOptions): Promise { - await page._client.send('Target.activateTarget', {targetId: page._target._targetId}); - let clip = options.clip ? processClip(options.clip) : undefined; - const viewport = page.viewport(); - - if (options.fullPage) { - const metrics = await page._client.send('Page.getLayoutMetrics'); - const width = Math.ceil(metrics.contentSize.width); - const height = Math.ceil(metrics.contentSize.height); - - // Overwrite clip for full page at all times. - clip = { x: 0, y: 0, width, height, scale: 1 }; - const { - isMobile = false, - deviceScaleFactor = 1, - isLandscape = false - } = viewport || {}; - const screenOrientation: Protocol.Emulation.ScreenOrientation = isLandscape ? { angle: 90, type: 'landscapePrimary' } : { angle: 0, type: 'portraitPrimary' }; - await page._client.send('Emulation.setDeviceMetricsOverride', { mobile: isMobile, width, height, deviceScaleFactor, screenOrientation }); - } - const shouldSetDefaultBackground = options.omitBackground && format === 'png'; - if (shouldSetDefaultBackground) - await page._client.send('Emulation.setDefaultBackgroundColorOverride', { color: { r: 0, g: 0, b: 0, a: 0 } }); - const result = await page._client.send('Page.captureScreenshot', { format, quality: options.quality, clip }); - if (shouldSetDefaultBackground) - await page._client.send('Emulation.setDefaultBackgroundColorOverride'); - - if (options.fullPage && viewport) - await page.setViewport(viewport); - - const buffer = options.encoding === 'base64' ? result.data : Buffer.from(result.data, 'base64'); - if (options.path) - await writeFileAsync(options.path, buffer); - return buffer; - - function processClip(clip) { - const x = Math.round(clip.x); - const y = Math.round(clip.y); - const width = Math.round(clip.width + clip.x - x); - const height = Math.round(clip.height + clip.y - y); - return {x, y, width, height, scale: 1}; - } + canCaptureOutsideViewport(): boolean { + return false; } - private _format(options: ScreenshotOptions): 'png' | 'jpeg' { - let format: 'png' | 'jpeg' | null = null; - // options.type takes precedence over inferring the type from options.path - // because it may be a 0-length file with no extension created beforehand (i.e. as a temp file). - if (options.type) { - assert(options.type === 'png' || options.type === 'jpeg', 'Unknown options.type value: ' + options.type); - format = options.type; - } else if (options.path) { - const mimeType = mime.getType(options.path); - if (mimeType === 'image/png') - format = 'png'; - else if (mimeType === 'image/jpeg') - format = 'jpeg'; - assert(format, 'Unsupported screenshot mime type: ' + mimeType); - } + async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise { + await this._session.send('Emulation.setDefaultBackgroundColorOverride', { color }); + } - if (!format) - format = 'png'; - - if (options.quality) { - assert(format === 'jpeg', 'options.quality is unsupported for the ' + format + ' screenshots'); - assert(typeof options.quality === 'number', 'Expected options.quality to be a number but found ' + (typeof options.quality)); - assert(Number.isInteger(options.quality), 'Expected options.quality to be an integer'); - assert(options.quality >= 0 && options.quality <= 100, 'Expected options.quality to be between 0 and 100 (inclusive), got ' + options.quality); - } - assert(!options.clip || !options.fullPage, 'options.clip and options.fullPage are exclusive'); - if (options.clip) { - assert(typeof options.clip.x === 'number', 'Expected options.clip.x to be a number but found ' + (typeof options.clip.x)); - assert(typeof options.clip.y === 'number', 'Expected options.clip.y to be a number but found ' + (typeof options.clip.y)); - assert(typeof options.clip.width === 'number', 'Expected options.clip.width to be a number but found ' + (typeof options.clip.width)); - assert(typeof options.clip.height === 'number', 'Expected options.clip.height to be a number but found ' + (typeof options.clip.height)); - assert(options.clip.width !== 0, 'Expected options.clip.width not to be 0.'); - assert(options.clip.height !== 0, 'Expected options.clip.height not to be 0.'); - } - return format; - } -} - -class TaskQueue { - private _chain: Promise; - - constructor() { - this._chain = Promise.resolve(); - } - - postTask(task: () => any): Promise { - const result = this._chain.then(task); - this._chain = result.catch(() => {}); - return result; + async screenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions): Promise { + const clip = options.clip ? { ...options.clip, scale: 1 } : undefined; + const result = await this._session.send('Page.captureScreenshot', { format, quality: options.quality, clip }); + return Buffer.from(result.data, 'base64'); } } diff --git a/src/chromium/Target.ts b/src/chromium/Target.ts index 922eacb1b2..12b5ab27b0 100644 --- a/src/chromium/Target.ts +++ b/src/chromium/Target.ts @@ -15,14 +15,18 @@ * limitations under the License. */ +import * as types from '../types'; import { Browser } from './Browser'; import { BrowserContext } from './BrowserContext'; -import { CDPSession } from './Connection'; -import { Events } from './events'; +import { CDPSession, CDPSessionEvents } from './Connection'; +import { Events as CommonEvents } from '../events'; import { Worker } from './features/workers'; -import { Page, Viewport } from './Page'; +import { Page } from '../page'; import { Protocol } from './protocol'; -import { Screenshotter } from './Screenshotter'; +import { debugError } from '../helper'; +import { FrameManager } from './FrameManager'; + +const targetSymbol = Symbol('target'); export class Target { private _targetInfo: Protocol.Target.TargetInfo; @@ -30,30 +34,30 @@ export class Target { _targetId: string; private _sessionFactory: () => Promise; private _ignoreHTTPSErrors: boolean; - private _defaultViewport: Viewport; - private _screenshotter: Screenshotter; - private _pagePromise: Promise | null = null; + private _defaultViewport: types.Viewport; + private _pagePromise: Promise> | null = null; + private _page: Page | null = null; private _workerPromise: Promise | null = null; _initializedPromise: Promise; _initializedCallback: (value?: unknown) => void; - _isClosedPromise: Promise; - _closedCallback: (value?: unknown) => void; _isInitialized: boolean; + static fromPage(page: Page): Target { + return (page as any)[targetSymbol]; + } + constructor( targetInfo: Protocol.Target.TargetInfo, browserContext: BrowserContext, sessionFactory: () => Promise, ignoreHTTPSErrors: boolean, - defaultViewport: Viewport | null, - screenshotter: Screenshotter) { + defaultViewport: types.Viewport | null) { this._targetInfo = targetInfo; this._browserContext = browserContext; this._targetId = targetInfo.targetId; this._sessionFactory = sessionFactory; this._ignoreHTTPSErrors = ignoreHTTPSErrors; this._defaultViewport = defaultViewport; - this._screenshotter = screenshotter; this._initializedPromise = new Promise(fulfill => this._initializedCallback = fulfill).then(async success => { if (!success) return false; @@ -61,22 +65,42 @@ export class Target { if (!opener || !opener._pagePromise || this.type() !== 'page') return true; const openerPage = await opener._pagePromise; - if (!openerPage.listenerCount(Events.Page.Popup)) + if (!openerPage.listenerCount(CommonEvents.Page.Popup)) return true; const popupPage = await this.page(); - openerPage.emit(Events.Page.Popup, popupPage); + openerPage.emit(CommonEvents.Page.Popup, popupPage); return true; }); - this._isClosedPromise = new Promise(fulfill => this._closedCallback = fulfill); this._isInitialized = this._targetInfo.type !== 'page' || this._targetInfo.url !== ''; if (this._isInitialized) this._initializedCallback(true); } - async page(): Promise { + _didClose() { + if (this._page) + this._page._didClose(); + } + + async page(): Promise | null> { if ((this._targetInfo.type === 'page' || this._targetInfo.type === 'background_page') && !this._pagePromise) { - this._pagePromise = this._sessionFactory() - .then(client => Page.create(client, this, this._ignoreHTTPSErrors, this._defaultViewport, this._screenshotter)); + this._pagePromise = this._sessionFactory().then(async client => { + const frameManager = new FrameManager(client, this._browserContext, this._ignoreHTTPSErrors); + const page = frameManager.page(); + this._page = page; + page[targetSymbol] = this; + client.once(CDPSessionEvents.Disconnected, () => page._didDisconnect()); + client.on('Target.attachedToTarget', event => { + if (event.targetInfo.type !== 'worker') { + // If we don't detach from service workers, they will never die. + client.send('Target.detachFromTarget', { sessionId: event.sessionId }).catch(debugError); + } + }); + await frameManager.initialize(); + await client.send('Target.setAutoAttach', {autoAttach: true, waitForDebuggerOnStart: false, flatten: true}); + if (this._defaultViewport) + await page.setViewport(this._defaultViewport); + return page; + }); } return this._pagePromise; } diff --git a/src/chromium/api.ts b/src/chromium/api.ts index 6cab510735..360089e0b5 100644 --- a/src/chromium/api.ts +++ b/src/chromium/api.ts @@ -11,7 +11,7 @@ export { ExecutionContext, JSHandle } from '../javascript'; export { Request, Response } from '../network'; export { Browser } from './Browser'; export { BrowserContext } from './BrowserContext'; -export { BrowserFetcher } from './BrowserFetcher'; +export { BrowserFetcher } from '../browserFetcher'; export { CDPSession } from './Connection'; export { Accessibility } from './features/accessibility'; export { Chromium } from './features/chromium'; @@ -21,7 +21,7 @@ export { Overrides } from './features/overrides'; export { PDF } from './features/pdf'; export { Permissions } from './features/permissions'; export { Worker, Workers } from './features/workers'; -export { Page } from './Page'; +export { Page } from '../page'; export { Playwright } from './Playwright'; export { Target } from './Target'; diff --git a/src/chromium/events.ts b/src/chromium/events.ts index 87f200c65a..e6680b26b2 100644 --- a/src/chromium/events.ts +++ b/src/chromium/events.ts @@ -16,26 +16,6 @@ */ export const Events = { - Page: { - Close: 'close', - Console: 'console', - Dialog: 'dialog', - FileChooser: 'filechooser', - DOMContentLoaded: 'domcontentloaded', - // Can't use just 'error' due to node.js special treatment of error events. - // @see https://nodejs.org/api/events.html#events_error_events - PageError: 'pageerror', - Request: 'request', - Response: 'response', - RequestFailed: 'requestfailed', - RequestFinished: 'requestfinished', - FrameAttached: 'frameattached', - FrameDetached: 'framedetached', - FrameNavigated: 'framenavigated', - Load: 'load', - Popup: 'popup', - }, - Browser: { Disconnected: 'disconnected' }, diff --git a/src/chromium/features/chromium.ts b/src/chromium/features/chromium.ts index 613552b3b4..bf16aca0a8 100644 --- a/src/chromium/features/chromium.ts +++ b/src/chromium/features/chromium.ts @@ -19,10 +19,11 @@ import { assert } from '../../helper'; import { Browser } from '../Browser'; import { BrowserContext } from '../BrowserContext'; import { CDPSession, Connection } from '../Connection'; -import { Page } from '../Page'; +import { Page } from '../../page'; import { readProtocolStream } from '../protocolHelper'; import { Target } from '../Target'; import { Worker } from './workers'; +import { FrameManager } from '../FrameManager'; export class Chromium extends EventEmitter { private _connection: Connection; @@ -47,9 +48,9 @@ export class Chromium extends EventEmitter { return target._worker(); } - async startTracing(page: Page | undefined, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) { + async startTracing(page: Page | undefined, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) { assert(!this._recording, 'Cannot start recording trace while already recording trace.'); - this._tracingClient = page ? page._client : this._client; + this._tracingClient = page ? (page._delegate as FrameManager)._client : this._client; const defaultCategories = [ '-*', 'devtools.timeline', 'v8.execute', 'disabled-by-default-devtools.timeline', @@ -91,8 +92,8 @@ export class Chromium extends EventEmitter { return context ? targets.filter(t => t.browserContext() === context) : targets; } - pageTarget(page: Page): Target { - return page._target; + pageTarget(page: Page): Target { + return Target.fromPage(page); } waitForTarget(predicate: (arg0: Target) => boolean, options: { timeout?: number; } | undefined = {}): Promise { diff --git a/src/chromium/features/workers.ts b/src/chromium/features/workers.ts index 1d7a74cbcf..dea3fa1b21 100644 --- a/src/chromium/features/workers.ts +++ b/src/chromium/features/workers.ts @@ -14,6 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { EventEmitter } from 'events'; import { CDPSession, Connection } from '../Connection'; import { debugError } from '../../helper'; @@ -21,10 +22,12 @@ import { Protocol } from '../protocol'; import { Events } from '../events'; import * as types from '../../types'; import * as js from '../../javascript'; +import * as console from '../../console'; import { ExecutionContextDelegate } from '../ExecutionContext'; +import { toConsoleMessageLocation, exceptionToError } from '../protocolHelper'; -type AddToConsoleCallback = (type: string, args: js.JSHandle[], stackTrace: Protocol.Runtime.StackTrace | undefined) => void; -type HandleExceptionCallback = (exceptionDetails: Protocol.Runtime.ExceptionDetails) => void; +type AddToConsoleCallback = (type: string, args: js.JSHandle[], location: console.ConsoleMessageLocation) => void; +type HandleExceptionCallback = (error: Error) => void; export class Workers extends EventEmitter { private _workers = new Map(); @@ -74,8 +77,8 @@ export class Worker extends EventEmitter { // This might fail if the target is closed before we recieve all execution contexts. this._client.send('Runtime.enable', {}).catch(debugError); - this._client.on('Runtime.consoleAPICalled', event => addToConsole(event.type, event.args.map(jsHandleFactory), event.stackTrace)); - this._client.on('Runtime.exceptionThrown', exception => handleException(exception.exceptionDetails)); + this._client.on('Runtime.consoleAPICalled', event => addToConsole(event.type, event.args.map(jsHandleFactory), toConsoleMessageLocation(event.stackTrace))); + this._client.on('Runtime.exceptionThrown', exception => handleException(exceptionToError(exception.exceptionDetails))); } url(): string { diff --git a/src/chromium/protocolHelper.ts b/src/chromium/protocolHelper.ts index 39d5df02c1..32c2e71490 100644 --- a/src/chromium/protocolHelper.ts +++ b/src/chromium/protocolHelper.ts @@ -94,4 +94,17 @@ export async function readProtocolStream(client: CDPSession, handle: string, pat } } +export function toConsoleMessageLocation(stackTrace: Protocol.Runtime.StackTrace | undefined) { + return stackTrace && stackTrace.callFrames.length ? { + url: stackTrace.callFrames[0].url, + lineNumber: stackTrace.callFrames[0].lineNumber, + columnNumber: stackTrace.callFrames[0].columnNumber, + } : {}; +} +export function exceptionToError(exceptionDetails: Protocol.Runtime.ExceptionDetails): Error { + const message = getExceptionMessage(exceptionDetails); + const err = new Error(message); + err.stack = ''; // Don't report clientside error with a node stack attached + return err; +} diff --git a/src/console.ts b/src/console.ts index 7af0ab93e2..edb057a73e 100644 --- a/src/console.ts +++ b/src/console.ts @@ -3,7 +3,7 @@ import * as js from './javascript'; -type ConsoleMessageLocation = { +export type ConsoleMessageLocation = { url?: string, lineNumber?: number, columnNumber?: number, diff --git a/src/dom.ts b/src/dom.ts index 85e5b51ef1..1c2aa498c9 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -10,7 +10,6 @@ import * as cssSelectorEngineSource from './generated/cssSelectorEngineSource'; import * as xpathSelectorEngineSource from './generated/xpathSelectorEngineSource'; import { assert, helper, debugError } from './helper'; import Injected from './injected/injected'; -import { SelectorRoot } from './injected/selectorEngine'; export interface DOMWorldDelegate { keyboard: input.Keyboard; @@ -22,12 +21,12 @@ export interface DOMWorldDelegate { contentQuads(handle: ElementHandle): Promise; layoutViewport(): Promise<{ width: number, height: number }>; boundingBox(handle: ElementHandle): Promise; - screenshot(handle: ElementHandle, options?: any): Promise; + screenshot(handle: ElementHandle, options?: types.ScreenshotOptions): Promise; setInputFiles(handle: ElementHandle, files: input.FilePayload[]): Promise; - adoptElementHandle(handle: ElementHandle, to: DOMWorld): Promise; + adoptElementHandle(handle: ElementHandle, to: DOMWorld): Promise>; } -export type ScopedSelector = types.Selector & { scope?: ElementHandle }; +type ScopedSelector = types.Selector & { scope?: ElementHandle }; type ResolvedSelector = { scope?: ElementHandle, selector: string, visible?: boolean, disposeScope?: boolean }; export class DOMWorld { @@ -60,7 +59,7 @@ export class DOMWorld { return this._injectedPromise; } - async adoptElementHandle(handle: ElementHandle): Promise { + async adoptElementHandle(handle: ElementHandle): Promise> { assert(handle.executionContext() !== this.context, 'Should not adopt to the same context'); return this.delegate.adoptElementHandle(handle, this); } @@ -75,10 +74,10 @@ export class DOMWorld { return { scope: selector.scope, selector: normalizeSelector(selector.selector), visible: selector.visible }; } - async $(selector: string | ScopedSelector): Promise { + async $(selector: string | ScopedSelector): Promise | null> { const resolved = await this.resolveSelector(selector); const handle = await this.context.evaluateHandle( - (injected: Injected, selector: string, scope: SelectorRoot | undefined, visible: boolean | undefined) => { + (injected: Injected, selector: string, scope?: Node, visible?: boolean) => { const element = injected.querySelector(selector, scope || document); if (visible === undefined || !element) return element; @@ -93,10 +92,10 @@ export class DOMWorld { return handle.asElement(); } - async $$(selector: string | ScopedSelector): Promise { + async $$(selector: string | ScopedSelector): Promise[]> { const resolved = await this.resolveSelector(selector); const arrayHandle = await this.context.evaluateHandle( - (injected: Injected, selector: string, scope: SelectorRoot | undefined, visible: boolean | undefined) => { + (injected: Injected, selector: string, scope?: Node, visible?: boolean) => { const elements = injected.querySelectorAll(selector, scope || document); if (visible !== undefined) return elements.filter(element => injected.isVisible(element) === visible); @@ -131,7 +130,7 @@ export class DOMWorld { $$eval: types.$$Eval = async (selector, pageFunction, ...args) => { const resolved = await this.resolveSelector(selector); const arrayHandle = await this.context.evaluateHandle( - (injected: Injected, selector: string, scope: SelectorRoot | undefined, visible: boolean | undefined) => { + (injected: Injected, selector: string, scope?: Node, visible?: boolean) => { const elements = injected.querySelectorAll(selector, scope || document); if (visible !== undefined) return elements.filter(element => injected.isVisible(element) === visible); @@ -145,8 +144,8 @@ export class DOMWorld { } } -export class ElementHandle extends js.JSHandle { - private readonly _world: DOMWorld; +export class ElementHandle extends js.JSHandle { + readonly _world: DOMWorld; constructor(context: js.ExecutionContext, remoteObject: any) { super(context, remoteObject); @@ -154,7 +153,7 @@ export class ElementHandle extends js.JSHandle { this._world = context._domWorld; } - asElement(): ElementHandle | null { + asElement(): ElementHandle | null { return this; } @@ -163,13 +162,15 @@ export class ElementHandle extends js.JSHandle { } async _scrollIntoViewIfNeeded() { - const error = await this.evaluate(async (element, pageJavascriptEnabled) => { - if (!element.isConnected) + const error = await this.evaluate(async (node: Node, pageJavascriptEnabled: boolean) => { + if (!node.isConnected) return 'Node is detached from document'; - if (element.nodeType !== Node.ELEMENT_NODE) + if (node.nodeType !== Node.ELEMENT_NODE) return 'Node is not of type HTMLElement'; + const element = node as Element; // force-scroll if page's javascript is disabled. if (!pageJavascriptEnabled) { + // @ts-ignore because only Chromium still supports 'instant' element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); return false; } @@ -183,8 +184,10 @@ export class ElementHandle extends js.JSHandle { // there are rafs. requestAnimationFrame(() => {}); }); - if (visibleRatio !== 1.0) + if (visibleRatio !== 1.0) { + // @ts-ignore because only Chromium still supports 'instant' element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); + } return false; }, this._world.delegate.isJavascriptEnabled()); if (error) @@ -254,8 +257,10 @@ export class ElementHandle extends js.JSHandle { private async _viewportPointAndScroll(relativePoint: types.Point): Promise<{point: types.Point, scrollX: number, scrollY: number}> { const [box, border] = await Promise.all([ this.boundingBox(), - this.evaluate((e: Element) => { - const style = e.ownerDocument.defaultView.getComputedStyle(e); + this.evaluate((node: Node) => { + if (node.nodeType !== Node.ELEMENT_NODE) + return { x: 0, y: 0 }; + const style = node.ownerDocument.defaultView.getComputedStyle(node as Element); return { x: parseInt(style.borderLeftWidth, 10), y: parseInt(style.borderTopWidth, 10) }; }).catch(debugError), ]); @@ -270,17 +275,17 @@ export class ElementHandle extends js.JSHandle { point.y += border.y; } const metrics = await this._world.delegate.layoutViewport(); - // Give one extra pixel to avoid any issues on viewport edge. + // Give 20 extra pixels to avoid any issues on viewport edge. let scrollX = 0; - if (point.x < 1) - scrollX = point.x - 1; - if (point.x > metrics.width - 1) - scrollX = point.x - metrics.width + 1; + if (point.x < 20) + scrollX = point.x - 20; + if (point.x > metrics.width - 20) + scrollX = point.x - metrics.width + 20; let scrollY = 0; - if (point.y < 1) - scrollY = point.y - 1; - if (point.y > metrics.height - 1) - scrollY = point.y - metrics.height + 1; + if (point.y < 20) + scrollY = point.y - 20; + if (point.y > metrics.height - 20) + scrollY = point.y - metrics.height + 20; return { point, scrollX, scrollY }; } @@ -334,13 +339,25 @@ export class ElementHandle extends js.JSHandle { } async setInputFiles(...files: (string|input.FilePayload)[]) { - const multiple = await this.evaluate((element: HTMLInputElement) => !!element.multiple); + const multiple = await this.evaluate((node: Node) => { + if (node.nodeType !== Node.ELEMENT_NODE || (node as Element).tagName !== 'INPUT') + throw new Error('Node is not an HTMLInputElement'); + const input = node as HTMLInputElement; + return input.multiple; + }); assert(multiple || files.length <= 1, 'Non-multiple file input can only accept single file!'); await this._world.delegate.setInputFiles(this, await input.loadFiles(files)); } async focus() { - await this.evaluate(element => element.focus()); + const errorMessage = await this.evaluate((element: Node) => { + if (!element['focus']) + return 'Node is not an HTML or SVG element.'; + (element as HTMLElement|SVGElement).focus(); + return false; + }); + if (errorMessage) + throw new Error(errorMessage); } async type(text: string, options: { delay: (number | undefined); } | undefined) { @@ -357,7 +374,7 @@ export class ElementHandle extends js.JSHandle { return this._world.delegate.boundingBox(this); } - async screenshot(options: any = {}): Promise { + async screenshot(options?: types.ElementScreenshotOptions): Promise { return this._world.delegate.screenshot(this, options); } @@ -372,7 +389,7 @@ export class ElementHandle extends js.JSHandle { return this._world.$(this._scopedSelector(selector)); } - $$(selector: string | types.Selector): Promise { + $$(selector: string | types.Selector): Promise[]> { return this._world.$$(this._scopedSelector(selector)); } @@ -384,12 +401,15 @@ export class ElementHandle extends js.JSHandle { return this._world.$$eval(this._scopedSelector(selector), pageFunction, ...args as any); } - $x(expression: string): Promise { + $x(expression: string): Promise[]> { return this._world.$$({ scope: this, selector: 'xpath=' + expression }); } isIntersectingViewport(): Promise { - return this.evaluate(async element => { + return this.evaluate(async (node: Node) => { + if (node.nodeType !== Node.ELEMENT_NODE) + throw new Error('Node is not of type HTMLElement'); + const element = node as Element; const visibleRatio = await new Promise(resolve => { const observer = new IntersectionObserver(entries => { resolve(entries[0].intersectionRatio); @@ -439,7 +459,7 @@ export function waitForFunctionTask(pageFunction: Function | string, options: ty export function waitForSelectorTask(selector: string | types.Selector, timeout: number): Task { return async (domWorld: DOMWorld) => { const resolved = await domWorld.resolveSelector(selector); - return domWorld.context.evaluateHandle((injected: Injected, selector: string, scope: SelectorRoot | undefined, visible: boolean | undefined, timeout: number) => { + return domWorld.context.evaluateHandle((injected: Injected, selector: string, scope: Node | undefined, visible: boolean | undefined, timeout: number) => { if (visible !== undefined) return injected.pollRaf(predicate, timeout); return injected.pollMutation(predicate, timeout); diff --git a/src/download.ts b/src/download.ts deleted file mode 100644 index a1a44d0d80..0000000000 --- a/src/download.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -export async function download( - browserFetcher: - import('./chromium/BrowserFetcher').BrowserFetcher | - import('./firefox/BrowserFetcher').BrowserFetcher | - import('./webkit/BrowserFetcher').BrowserFetcher, - revision: string, - browserName: string, - {onProgress}: {onProgress?: (downloadedBytes: number, totalBytes: number) => void} = {}) : Promise { - const revisionInfo = browserFetcher.revisionInfo(revision); - await browserFetcher.download(revision, onProgress); - return revisionInfo; -} - -export type RevisionInfo = { - folderPath: string, - executablePath: string, - url: string, - local: boolean, - revision: string, -}; diff --git a/src/webkit/TaskQueue.ts b/src/events.ts similarity index 51% rename from src/webkit/TaskQueue.ts rename to src/events.ts index 99b2de4469..5e0f2292f0 100644 --- a/src/webkit/TaskQueue.ts +++ b/src/events.ts @@ -15,16 +15,24 @@ * limitations under the License. */ -export class TaskQueue { - private _chain: Promise; - - constructor() { - this._chain = Promise.resolve(); - } - - postTask(task: () => any): Promise { - const result = this._chain.then(task); - this._chain = result.catch(() => {}); - return result; - } -} +export const Events = { + Page: { + Close: 'close', + Console: 'console', + Dialog: 'dialog', + FileChooser: 'filechooser', + DOMContentLoaded: 'domcontentloaded', + // Can't use just 'error' due to node.js special treatment of error events. + // @see https://nodejs.org/api/events.html#events_error_events + PageError: 'pageerror', + Request: 'request', + Response: 'response', + RequestFailed: 'requestfailed', + RequestFinished: 'requestfinished', + FrameAttached: 'frameattached', + FrameDetached: 'framedetached', + FrameNavigated: 'framenavigated', + Load: 'load', + Popup: 'popup', + }, +}; diff --git a/src/firefox/Browser.ts b/src/firefox/Browser.ts index 852e604dbf..865a353f7a 100644 --- a/src/firefox/Browser.ts +++ b/src/firefox/Browser.ts @@ -18,14 +18,15 @@ import { EventEmitter } from 'events'; import { assert, helper, RegisteredListener } from '../helper'; import { filterCookies, NetworkCookie, SetNetworkCookieParam, rewriteCookies } from '../network'; -import { Connection, ConnectionEvents } from './Connection'; +import { Connection, ConnectionEvents, JugglerSessionEvents } from './Connection'; import { Events } from './events'; import { Permissions } from './features/permissions'; -import { Page, Viewport } from './Page'; +import { Page } from './Page'; +import * as types from '../types'; export class Browser extends EventEmitter { private _connection: Connection; - _defaultViewport: Viewport; + _defaultViewport: types.Viewport; private _process: import('child_process').ChildProcess; private _closeCallback: () => void; _targets: Map; @@ -33,14 +34,14 @@ export class Browser extends EventEmitter { private _contexts: Map; private _eventListeners: RegisteredListener[]; - static async create(connection: Connection, defaultViewport: Viewport | null, process: import('child_process').ChildProcess | null, closeCallback: () => void) { + static async create(connection: Connection, defaultViewport: types.Viewport | null, process: import('child_process').ChildProcess | null, closeCallback: () => void) { const {browserContextIds} = await connection.send('Target.getBrowserContexts'); const browser = new Browser(connection, browserContextIds, defaultViewport, process, closeCallback); await connection.send('Target.enable'); return browser; } - constructor(connection: Connection, browserContextIds: Array, defaultViewport: Viewport | null, process: import('child_process').ChildProcess | null, closeCallback: () => void) { + constructor(connection: Connection, browserContextIds: Array, defaultViewport: types.Viewport | null, process: import('child_process').ChildProcess | null, closeCallback: () => void) { super(); this._connection = connection; this._defaultViewport = defaultViewport; @@ -150,6 +151,12 @@ export class Browser extends EventEmitter { return Array.from(this._targets.values()); } + async _pages(context: BrowserContext): Promise { + const targets = this._allTargets().filter(target => target.browserContext() === context && target.type() === 'page'); + const pages = await Promise.all(targets.map(target => target.page())); + return pages.filter(page => !!page); + } + async _onTargetCreated({targetId, url, browserContextId, openerId, type}) { const context = browserContextId ? this._contexts.get(browserContextId) : this._defaultContext; const target = new Target(this._connection, this, context, targetId, type, url, openerId); @@ -166,7 +173,7 @@ export class Browser extends EventEmitter { _onTargetDestroyed({targetId}) { const target = this._targets.get(targetId); this._targets.delete(targetId); - target._closedCallback(); + target._didClose(); } _onTargetInfoChanged({targetId, url}) { @@ -182,15 +189,14 @@ export class Browser extends EventEmitter { export class Target { _pagePromise?: Promise; + private _page: Page | null = null; private _browser: Browser; _context: BrowserContext; - private _connection: any; + private _connection: Connection; private _targetId: string; private _type: 'page' | 'browser'; _url: string; private _openerId: string; - _isClosedPromise: Promise; - _closedCallback: (value?: unknown) => void; constructor(connection: any, browser: Browser, context: BrowserContext, targetId: string, type: 'page' | 'browser', url: string, openerId: string | undefined) { this._browser = browser; @@ -200,9 +206,12 @@ export class Target { this._type = type; this._url = url; this._openerId = openerId; - this._isClosedPromise = new Promise(fulfill => this._closedCallback = fulfill); } + _didClose() { + if (this._page) + this._page._didClose(); + } opener(): Target | null { return this._openerId ? this._browser._targets.get(this._openerId) : null; @@ -222,10 +231,18 @@ export class Target { return this._context; } - async page() { + page(): Promise { if (this._type === 'page' && !this._pagePromise) { - const session = await this._connection.createSession(this._targetId); - this._pagePromise = Page.create(session, this, this._browser._defaultViewport); + this._pagePromise = new Promise(async f => { + const session = await this._connection.createSession(this._targetId); + const page = new Page(session, this._context); + this._page = page; + session.once(JugglerSessionEvents.Disconnected, () => page._didDisconnect()); + await page._frameManager._initialize(); + if (this._browser._defaultViewport) + await page.setViewport(this._browser._defaultViewport); + f(page); + }); } return this._pagePromise; } @@ -248,17 +265,8 @@ export class BrowserContext { this.permissions = new Permissions(connection, browserContextId); } - _targets(): Array { - return this._browser._allTargets().filter(target => target.browserContext() === this); - } - - async pages(): Promise> { - const pages = await Promise.all( - this._targets() - .filter(target => target.type() === 'page') - .map(target => target.page()) - ); - return pages.filter(page => !!page); + pages(): Promise { + return this._browser._pages(this); } isIncognito(): boolean { diff --git a/src/firefox/BrowserFetcher.ts b/src/firefox/BrowserFetcher.ts deleted file mode 100644 index 4cb6f21a29..0000000000 --- a/src/firefox/BrowserFetcher.ts +++ /dev/null @@ -1,283 +0,0 @@ -/** - * Copyright 2017 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. - */ - -import * as os from 'os'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as extract from 'extract-zip'; -import * as util from 'util'; -import * as URL from 'url'; -import {helper, assert} from '../helper'; -import * as removeRecursive from 'rimraf'; -// @ts-ignore -import * as ProxyAgent from 'https-proxy-agent'; -// @ts-ignore -import {getProxyForUrl} from 'proxy-from-env'; -const DEFAULT_DOWNLOAD_HOST = 'https://playwrightaccount.blob.core.windows.net/builds'; - -const downloadURLs = { - chromium: { - linux: '%s/chromium-browser-snapshots/Linux_x64/%s/%s.zip', - mac: '%s/chromium-browser-snapshots/Mac/%s/%s.zip', - win32: '%s/chromium-browser-snapshots/Win/%s/%s.zip', - win64: '%s/chromium-browser-snapshots/Win_x64/%s/%s.zip', - }, - firefox: { - linux: '%s/firefox/%s/%s.zip', - mac: '%s/firefox/%s/%s.zip', - win32: '%s/firefox/%s/%s.zip', - win64: '%s/firefox/%s/%s.zip', - }, -}; - -function archiveName(product: string, platform: string, revision: string): string { - if (product === 'chromium') { - if (platform === 'linux') - return 'chrome-linux'; - if (platform === 'mac') - return 'chrome-mac'; - if (platform === 'win32' || platform === 'win64') { - // Windows archive name changed at r591479. - return parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32'; - } - } else if (product === 'firefox') { - if (platform === 'linux') - return 'firefox-linux'; - if (platform === 'mac') - return 'firefox-mac'; - if (platform === 'win32' || platform === 'win64') - return 'firefox-' + platform; - } - return null; -} - -function downloadURL(product: string, platform: string, host: string, revision: string): string { - return util.format(downloadURLs[product][platform], host, revision, archiveName(product, platform, revision)); -} - -const readdirAsync = helper.promisify(fs.readdir.bind(fs)); -const mkdirAsync = helper.promisify(fs.mkdir.bind(fs)); -const unlinkAsync = helper.promisify(fs.unlink.bind(fs)); -const chmodAsync = helper.promisify(fs.chmod.bind(fs)); - -function existsAsync(filePath) { - let fulfill = null; - const promise = new Promise(x => fulfill = x); - fs.access(filePath, err => fulfill(!err)); - return promise; -} - -export class BrowserFetcher { - _product: string; - _downloadsFolder: string; - _downloadHost: string; - _platform: string; - constructor(projectRoot: string, options: BrowserFetcherOptions | undefined = {}) { - this._product = (options.browser || 'chromium').toLowerCase(); - assert(this._product === 'chromium' || this._product === 'firefox', `Unkown product: "${options.browser}"`); - this._downloadsFolder = options.path || path.join(projectRoot, '.local-browser'); - this._downloadHost = options.host || DEFAULT_DOWNLOAD_HOST; - this._platform = options.platform || ''; - if (!this._platform) { - const platform = os.platform(); - if (platform === 'darwin') - this._platform = 'mac'; - else if (platform === 'linux') - this._platform = 'linux'; - else if (platform === 'win32') - this._platform = os.arch() === 'x64' ? 'win64' : 'win32'; - assert(this._platform, 'Unsupported platform: ' + os.platform()); - } - assert(downloadURLs[this._product][this._platform], 'Unsupported platform: ' + this._platform); - } - - platform(): string { - return this._platform; - } - - canDownload(revision: string): Promise { - const url = downloadURL(this._product, this._platform, this._downloadHost, revision); - let resolve; - const promise = new Promise(x => resolve = x); - const request = httpRequest(url, 'HEAD', response => { - resolve(response.statusCode === 200); - }); - request.on('error', error => { - console.error(error); - resolve(false); - }); - return promise; - } - - async download(revision: string, progressCallback: ((arg0: number, arg1: number) => void) | null): Promise { - const url = downloadURL(this._product, this._platform, this._downloadHost, revision); - const zipPath = path.join(this._downloadsFolder, `download-${this._product}-${this._platform}-${revision}.zip`); - const folderPath = this._getFolderPath(revision); - if (await existsAsync(folderPath)) - return this.revisionInfo(revision); - if (!(await existsAsync(this._downloadsFolder))) - await mkdirAsync(this._downloadsFolder); - try { - await downloadFile(url, zipPath, progressCallback); - await extractZip(zipPath, folderPath); - } finally { - if (await existsAsync(zipPath)) - await unlinkAsync(zipPath); - } - const revisionInfo = this.revisionInfo(revision); - if (revisionInfo) - await chmodAsync(revisionInfo.executablePath, 0o755); - return revisionInfo; - } - - async localRevisions(): Promise> { - if (!await existsAsync(this._downloadsFolder)) - return []; - const fileNames = await readdirAsync(this._downloadsFolder); - return fileNames.map(fileName => parseFolderPath(fileName)).filter(entry => entry && entry.platform === this._platform).map(entry => entry.revision); - } - - async remove(revision: string) { - const folderPath = this._getFolderPath(revision); - assert(await existsAsync(folderPath), `Failed to remove: revision ${revision} is not downloaded`); - await new Promise(fulfill => removeRecursive(folderPath, fulfill)); - } - - revisionInfo(revision: string): RevisionInfo { - const folderPath = this._getFolderPath(revision); - let executablePath = ''; - if (this._product === 'chromium') { - if (this._platform === 'mac') - executablePath = path.join(folderPath, archiveName(this._product, this._platform, revision), 'Chromium.app', 'Contents', 'MacOS', 'Chromium'); - else if (this._platform === 'linux') - executablePath = path.join(folderPath, archiveName(this._product, this._platform, revision), 'chrome'); - else if (this._platform === 'win32' || this._platform === 'win64') - executablePath = path.join(folderPath, archiveName(this._product, this._platform, revision), 'chrome.exe'); - else - throw new Error('Unsupported platform: ' + this._platform); - } else if (this._product === 'firefox') { - if (this._platform === 'mac') - executablePath = path.join(folderPath, 'firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox'); - else if (this._platform === 'linux') - executablePath = path.join(folderPath, 'firefox', 'firefox'); - else if (this._platform === 'win32' || this._platform === 'win64') - executablePath = path.join(folderPath, 'firefox', 'firefox.exe'); - else - throw new Error('Unsupported platform: ' + this._platform); - } - const url = downloadURL(this._product, this._platform, this._downloadHost, revision); - const local = fs.existsSync(folderPath); - return {revision, executablePath, folderPath, local, url}; - } - - _getFolderPath(revision: string): string { - return path.join(this._downloadsFolder, this._product + '-' + this._platform + '-' + revision); - } -} - -function parseFolderPath(folderPath: string): { platform: string; revision: string; } | null { - const name = path.basename(folderPath); - const splits = name.split('-'); - if (splits.length !== 3) - return null; - const [product, platform, revision] = splits; - if (!downloadURLs[product][platform]) - return null; - return {platform, revision}; -} - -function downloadFile(url: string, destinationPath: string, progressCallback: ((arg0: number, arg1: number) => void) | null): Promise { - let fulfill, reject; - let downloadedBytes = 0; - let totalBytes = 0; - - const promise = new Promise((x, y) => { fulfill = x; reject = y; }); - - const request = httpRequest(url, 'GET', response => { - if (response.statusCode !== 200) { - const error = new Error(`Download failed: server returned code ${response.statusCode}. URL: ${url}`); - // consume response data to free up memory - response.resume(); - reject(error); - return; - } - const file = fs.createWriteStream(destinationPath); - file.on('finish', () => fulfill()); - file.on('error', error => reject(error)); - response.pipe(file); - totalBytes = parseInt(response.headers['content-length'], 10); - if (progressCallback) - response.on('data', onData); - }); - request.on('error', error => reject(error)); - return promise; - - function onData(chunk) { - downloadedBytes += chunk.length; - progressCallback(downloadedBytes, totalBytes); - } -} - -function extractZip(zipPath: string, folderPath: string): Promise { - return new Promise((fulfill, reject) => extract(zipPath, {dir: folderPath}, err => { - if (err) - reject(err); - else - fulfill(); - })); -} - -function httpRequest(url: string, method: string, onResponse: (response: any) => void) { - const options: any = URL.parse(url); - options.method = method; - - const proxyURL = getProxyForUrl(url); - if (proxyURL) { - const parsedProxyURL: any = URL.parse(proxyURL); - parsedProxyURL.secureProxy = parsedProxyURL.protocol === 'https:'; - - options.agent = new ProxyAgent(parsedProxyURL); - options.rejectUnauthorized = false; - } - - const requestCallback = res => { - if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) - httpRequest(res.headers.location, method, onResponse); - else - onResponse(res); - }; - const request = options.protocol === 'https:' ? - require('https').request(options, requestCallback) : - require('http').request(options, requestCallback); - request.end(); - return request; -} - -interface BrowserFetcherOptions { - browser?: string; - platform?: string; - path?: string; - host?: string; -} - -interface RevisionInfo { - folderPath: string; - executablePath: string; - url: string; - local: boolean; - revision: string; -} diff --git a/src/firefox/ExecutionContext.ts b/src/firefox/ExecutionContext.ts index 19ed5058db..cdb61b750e 100644 --- a/src/firefox/ExecutionContext.ts +++ b/src/firefox/ExecutionContext.ts @@ -134,7 +134,7 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate { }); } - async handleJSONValue(handle: js.JSHandle): Promise { + async handleJSONValue(handle: js.JSHandle): Promise { const payload = handle._remoteObject; if (!payload.objectId) return deserializeValue(payload); diff --git a/src/firefox/FrameManager.ts b/src/firefox/FrameManager.ts index 142a1b8727..8d5d446a6e 100644 --- a/src/firefox/FrameManager.ts +++ b/src/firefox/FrameManager.ts @@ -18,7 +18,7 @@ import { EventEmitter } from 'events'; import { TimeoutError } from '../Errors'; import * as frames from '../frames'; -import { assert, helper, RegisteredListener } from '../helper'; +import { assert, helper, RegisteredListener, debugError } from '../helper'; import * as js from '../javascript'; import * as dom from '../dom'; import { TimeoutSettings } from '../TimeoutSettings'; @@ -28,6 +28,9 @@ import { NavigationWatchdog, NextNavigationWatchdog } from './NavigationWatchdog import { Page } from './Page'; import { NetworkManager } from './NetworkManager'; import { DOMWorldDelegate } from './JSHandle'; +import { Events } from './events'; +import * as dialog from '../dialog'; +import { Protocol } from './protocol'; export const FrameManagerEvents = { FrameNavigated: Symbol('FrameManagerEvents.FrameNavigated'), @@ -71,9 +74,23 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { helper.addEventListener(this._session, 'Page.sameDocumentNavigation', this._onSameDocumentNavigation.bind(this)), helper.addEventListener(this._session, 'Runtime.executionContextCreated', this._onExecutionContextCreated.bind(this)), helper.addEventListener(this._session, 'Runtime.executionContextDestroyed', this._onExecutionContextDestroyed.bind(this)), + helper.addEventListener(this._session, 'Page.uncaughtError', this._onUncaughtError.bind(this)), + helper.addEventListener(this._session, 'Runtime.console', this._onConsole.bind(this)), + helper.addEventListener(this._session, 'Page.dialogOpened', this._onDialogOpened.bind(this)), + helper.addEventListener(this._session, 'Page.bindingCalled', this._onBindingCalled.bind(this)), + helper.addEventListener(this._session, 'Page.fileChooserOpened', this._onFileChooserOpened.bind(this)), ]; } + async _initialize() { + await Promise.all([ + this._session.send('Runtime.enable'), + this._session.send('Network.enable'), + this._session.send('Page.enable'), + this._session.send('Page.setInterceptFileChooserDialog', { enabled: true }) + ]); + } + executionContextById(executionContextId) { return this._contextIdToContext.get(executionContextId) || null; } @@ -173,6 +190,44 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { } } + _onUncaughtError(params) { + const error = new Error(params.message); + error.stack = params.stack; + this._page.emit(Events.Page.PageError, error); + } + + _onConsole({type, args, executionContextId, location}) { + const context = this.executionContextById(executionContextId); + this._page._addConsoleMessage(type, args.map(arg => context._createHandle(arg)), location); + } + + _onDialogOpened(params) { + this._page.emit(Events.Page.Dialog, new dialog.Dialog( + params.type as dialog.DialogType, + params.message, + async (accept: boolean, promptText?: string) => { + await this._session.send('Page.handleDialog', { dialogId: params.dialogId, accept, promptText }).catch(debugError); + }, + params.defaultValue)); + } + + _onBindingCalled(event: Protocol.Page.bindingCalledPayload) { + const context = this.executionContextById(event.executionContextId); + this._page._onBindingCalled(event.payload, context); + } + + async _onFileChooserOpened({executionContextId, element}) { + const context = this.executionContextById(executionContextId); + const handle = context._createHandle(element).asElement()!; + this._page._onFileChooserOpened(handle); + } + + async _exposeBinding(name: string, bindingFunction: string) { + await this._session.send('Page.addBinding', {name: name}); + await this._session.send('Page.addScriptToEvaluateOnNewDocument', {script: bindingFunction}); + await Promise.all(this.frames().map(frame => frame.evaluate(bindingFunction).catch(debugError))); + } + dispose() { helper.removeEventListeners(this._eventListeners); } diff --git a/src/firefox/JSHandle.ts b/src/firefox/JSHandle.ts index 3a6843a101..51f300e29b 100644 --- a/src/firefox/JSHandle.ts +++ b/src/firefox/JSHandle.ts @@ -22,6 +22,7 @@ import * as types from '../types'; import * as frames from '../frames'; import { JugglerSession } from './Connection'; import { FrameManager } from './FrameManager'; +import { Protocol } from './protocol'; export class DOMWorldDelegate implements dom.DOMWorldDelegate { readonly keyboard: input.Keyboard; @@ -92,38 +93,22 @@ export class DOMWorldDelegate implements dom.DOMWorldDelegate { return this._frameManager._page.evaluate(() => ({ width: innerWidth, height: innerHeight })); } - async screenshot(handle: dom.ElementHandle, options: any = {}): Promise { - const clip = await this._session.send('Page.getBoundingBox', { - frameId: this._frameId, - objectId: toRemoteObject(handle).objectId, - }); - if (!clip) - throw new Error('Node is either not visible or not an HTMLElement'); - assert(clip.width, 'Node has 0 width.'); - assert(clip.height, 'Node has 0 height.'); - await handle._scrollIntoViewIfNeeded(); - - return await this._frameManager._page.screenshot(Object.assign({}, options, { - clip: { - x: clip.x, - y: clip.y, - width: clip.width, - height: clip.height, - }, - })); + async screenshot(handle: dom.ElementHandle, options?: types.ElementScreenshotOptions): Promise { + const page = this._frameManager._page; + return page._screenshotter.screenshotElement(handle, options); } async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise { await handle.evaluate(input.setFileInputFunction, files); } - async adoptElementHandle(handle: dom.ElementHandle, to: dom.DOMWorld): Promise { + async adoptElementHandle(handle: dom.ElementHandle, to: dom.DOMWorld): Promise> { assert(false, 'Multiple isolated worlds are not implemented'); return handle; } } -function toRemoteObject(handle: dom.ElementHandle): any { +function toRemoteObject(handle: dom.ElementHandle): Protocol.RemoteObject { return handle._remoteObject; } diff --git a/src/firefox/Launcher.ts b/src/firefox/Launcher.ts index 5017a4fcbd..883240410e 100644 --- a/src/firefox/Launcher.ts +++ b/src/firefox/Launcher.ts @@ -20,11 +20,11 @@ import * as removeFolder from 'rimraf'; import * as childProcess from 'child_process'; import {Connection} from './Connection'; import {Browser} from './Browser'; -import {BrowserFetcher} from './BrowserFetcher'; +import {BrowserFetcher, BrowserFetcherOptions} from '../browserFetcher'; import * as readline from 'readline'; import * as fs from 'fs'; import * as util from 'util'; -import {helper, debugError} from '../helper'; +import {helper, debugError, assert} from '../helper'; import {TimeoutError} from '../Errors'; import {WebSocketTransport} from './WebSocketTransport'; @@ -227,7 +227,7 @@ export class Launcher { } _resolveExecutablePath() { - const browserFetcher = new BrowserFetcher(this._projectRoot, { browser: 'firefox' }); + const browserFetcher = createBrowserFetcher(this._projectRoot); const revisionInfo = browserFetcher.revisionInfo(this._preferredRevision); const missingText = !revisionInfo.local ? `Firefox revision is not downloaded. Run "npm install" or "yarn install"` : null; return {executablePath: revisionInfo.executablePath, missingText}; @@ -275,3 +275,46 @@ function waitForWSEndpoint(firefoxProcess: import('child_process').ChildProcess, } }); } + +export function createBrowserFetcher(projectRoot: string, options: BrowserFetcherOptions = {}): BrowserFetcher { + const downloadURLs = { + linux: '%s/builds/firefox/%s/firefox-linux.zip', + mac: '%s/builds/firefox/%s/firefox-mac.zip', + win32: '%s/builds/firefox/%s/firefox-win32.zip', + win64: '%s/builds/firefox/%s/firefox-win64.zip', + }; + + const defaultOptions = { + path: path.join(projectRoot, '.local-firefox'), + host: 'https://playwrightaccount.blob.core.windows.net', + platform: (() => { + const platform = os.platform(); + if (platform === 'darwin') + return 'mac'; + if (platform === 'linux') + return 'linux'; + if (platform === 'win32') + return os.arch() === 'x64' ? 'win64' : 'win32'; + return platform; + })() + }; + options = { + ...defaultOptions, + ...options, + }; + assert(!!downloadURLs[options.platform], 'Unsupported platform: ' + options.platform); + + return new BrowserFetcher(options.path, options.platform, (platform: string, revision: string) => { + let executablePath = ''; + if (platform === 'linux') + executablePath = path.join('firefox', 'firefox'); + else if (platform === 'mac') + executablePath = path.join('firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox'); + else if (platform === 'win32' || platform === 'win64') + executablePath = path.join('firefox', 'firefox.exe'); + return { + downloadUrl: util.format(downloadURLs[platform], options.host, revision), + executablePath + }; + }); +} diff --git a/src/firefox/Page.ts b/src/firefox/Page.ts index f0de1717f3..b44c900b01 100644 --- a/src/firefox/Page.ts +++ b/src/firefox/Page.ts @@ -16,83 +16,68 @@ */ import { EventEmitter } from 'events'; -import * as fs from 'fs'; -import * as mime from 'mime'; +import * as console from '../console'; +import * as dom from '../dom'; import { TimeoutError } from '../Errors'; +import * as frames from '../frames'; import { assert, debugError, helper, RegisteredListener } from '../helper'; +import * as input from '../input'; +import * as js from '../javascript'; +import * as network from '../network'; +import { Screenshotter } from '../screenshotter'; import { TimeoutSettings } from '../TimeoutSettings'; -import { BrowserContext, Target } from './Browser'; -import { JugglerSession, JugglerSessionEvents } from './Connection'; +import * as types from '../types'; +import { BrowserContext } from './Browser'; +import { JugglerSession } from './Connection'; import { Events } from './events'; import { Accessibility } from './features/accessibility'; import { Interception } from './features/interception'; import { FrameManager, FrameManagerEvents, normalizeWaitUntil } from './FrameManager'; -import { RawMouseImpl, RawKeyboardImpl } from './Input'; +import { RawKeyboardImpl, RawMouseImpl } from './Input'; import { NavigationWatchdog } from './NavigationWatchdog'; import { NetworkManager, NetworkManagerEvents } from './NetworkManager'; -import * as input from '../input'; -import * as types from '../types'; -import * as js from '../javascript'; -import * as dom from '../dom'; -import * as network from '../network'; -import * as frames from '../frames'; -import * as dialog from '../dialog'; -import * as console from '../console'; - -const writeFileAsync = helper.promisify(fs.writeFile); +import { FFScreenshotDelegate } from './Screenshotter'; export class Page extends EventEmitter { private _timeoutSettings: TimeoutSettings; private _session: JugglerSession; - private _target: Target; + private _browserContext: BrowserContext; private _keyboard: input.Keyboard; private _mouse: input.Mouse; readonly accessibility: Accessibility; readonly interception: Interception; private _closed: boolean; + private _closedCallback: () => void; + private _closedPromise: Promise; + private _disconnected = false; + private _disconnectedCallback: (e: Error) => void; + private _disconnectedPromise: Promise; private _pageBindings: Map; private _networkManager: NetworkManager; _frameManager: FrameManager; _javascriptEnabled = true; private _eventListeners: RegisteredListener[]; - private _viewport: Viewport; - private _disconnectPromise: Promise; + private _viewport: types.Viewport; private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>(); + _screenshotter: Screenshotter; - static async create(session: JugglerSession, target: Target, defaultViewport: Viewport | null) { - const page = new Page(session, target); - await Promise.all([ - session.send('Runtime.enable'), - session.send('Network.enable'), - session.send('Page.enable'), - session.send('Page.setInterceptFileChooserDialog', { enabled: true }) - ]); - - if (defaultViewport) - await page.setViewport(defaultViewport); - return page; - } - - constructor(session: JugglerSession, target: Target) { + constructor(session: JugglerSession, browserContext: BrowserContext) { super(); this._timeoutSettings = new TimeoutSettings(); this._session = session; - this._target = target; + this._browserContext = browserContext; this._keyboard = new input.Keyboard(new RawKeyboardImpl(session)); this._mouse = new input.Mouse(new RawMouseImpl(session), this._keyboard); this.accessibility = new Accessibility(session); this._closed = false; + this._closedPromise = new Promise(f => this._closedCallback = f); + this._disconnectedPromise = new Promise(f => this._disconnectedCallback = f); this._pageBindings = new Map(); this._networkManager = new NetworkManager(session); this._frameManager = new FrameManager(session, this, this._networkManager, this._timeoutSettings); this._networkManager.setFrameManager(this._frameManager); this.interception = new Interception(this._networkManager); this._eventListeners = [ - helper.addEventListener(this._session, 'Page.uncaughtError', this._onUncaughtError.bind(this)), - helper.addEventListener(this._session, 'Runtime.console', this._onConsole.bind(this)), - helper.addEventListener(this._session, 'Page.dialogOpened', this._onDialogOpened.bind(this)), - helper.addEventListener(this._session, 'Page.bindingCalled', this._onBindingCalled.bind(this)), - helper.addEventListener(this._session, 'Page.fileChooserOpened', this._onFileChooserOpened.bind(this)), helper.addEventListener(this._frameManager, FrameManagerEvents.Load, () => this.emit(Events.Page.Load)), helper.addEventListener(this._frameManager, FrameManagerEvents.DOMContentLoaded, () => this.emit(Events.Page.DOMContentLoaded)), helper.addEventListener(this._frameManager, FrameManagerEvents.FrameAttached, frame => this.emit(Events.Page.FrameAttached, frame)), @@ -104,13 +89,23 @@ export class Page extends EventEmitter { helper.addEventListener(this._networkManager, NetworkManagerEvents.RequestFailed, request => this.emit(Events.Page.RequestFailed, request)), ]; this._viewport = null; - this._target._isClosedPromise.then(() => { - this._closed = true; - this._frameManager.dispose(); - this._networkManager.dispose(); - helper.removeEventListeners(this._eventListeners); - this.emit(Events.Page.Close); - }); + this._screenshotter = new Screenshotter(this, new FFScreenshotDelegate(session, this._frameManager), browserContext.browser()); + } + + _didClose() { + assert(!this._closed, 'Page closed twice'); + this._closed = true; + this._frameManager.dispose(); + this._networkManager.dispose(); + helper.removeEventListeners(this._eventListeners); + this.emit(Events.Page.Close); + this._closedCallback(); + } + + _didDisconnect() { + assert(!this._disconnected, 'Page disconnected twice'); + this._disconnected = true; + this._disconnectedCallback(new Error('Target closed')); } async setExtraHTTPHeaders(headers) { @@ -118,8 +113,8 @@ export class Page extends EventEmitter { } async emulateMedia(options: { - type?: ''|'screen'|'print', - colorScheme?: 'dark' | 'light' | 'no-preference' }) { + type?: input.MediaType, + colorScheme?: input.MediaColorScheme }) { assert(!options.type || input.mediaTypes.has(options.type), 'Unsupported media type: ' + options.type); assert(!options.colorScheme || input.mediaColorSchemes.has(options.colorScheme), 'Unsupported color scheme: ' + options.colorScheme); await this._session.send('Page.setEmulatedMedia', options); @@ -129,11 +124,7 @@ export class Page extends EventEmitter { if (this._pageBindings.has(name)) throw new Error(`Failed to add page binding with name ${name}: window['${name}'] already exists!`); this._pageBindings.set(name, playwrightFunction); - - const expression = helper.evaluationString(addPageBinding, name); - await this._session.send('Page.addBinding', {name: name}); - await this._session.send('Page.addScriptToEvaluateOnNewDocument', {script: expression}); - await Promise.all(this.frames().map(frame => frame.evaluate(expression).catch(debugError))); + await this._frameManager._exposeBinding(name, helper.evaluationString(addPageBinding, name)); function addPageBinding(bindingName: string) { const binding: (string) => void = window[bindingName]; @@ -153,8 +144,8 @@ export class Page extends EventEmitter { } } - async _onBindingCalled(event: any) { - const {name, seq, args} = JSON.parse(event.payload); + async _onBindingCalled(payload: string, context: js.ExecutionContext) { + const {name, seq, args} = JSON.parse(payload); let expression = null; try { const result = await this._pageBindings.get(name)(...args); @@ -165,7 +156,7 @@ export class Page extends EventEmitter { else expression = helper.evaluationString(deliverErrorValue, name, seq, error); } - this._session.send('Runtime.evaluate', { expression, executionContextId: event.executionContextId }).catch(debugError); + context.evaluate(expression).catch(debugError); function deliverResult(name: string, seq: number, result: any) { window[name]['callbacks'].get(seq).resolve(result); @@ -185,12 +176,6 @@ export class Page extends EventEmitter { } } - _sessionClosePromise() { - if (!this._disconnectPromise) - this._disconnectPromise = new Promise(fulfill => this._session.once(JugglerSessionEvents.Disconnected, () => fulfill(new Error('Target closed')))); - return this._disconnectPromise; - } - async waitForRequest(urlOrPredicate: (string | Function), options: { timeout?: number; } | undefined = {}): Promise { const { timeout = this._timeoutSettings.timeout(), @@ -201,7 +186,7 @@ export class Page extends EventEmitter { if (typeof urlOrPredicate === 'function') return !!(urlOrPredicate(request)); return false; - }, timeout, this._sessionClosePromise()); + }, timeout, this._disconnectedPromise); } async waitForResponse(urlOrPredicate: (string | Function), options: { timeout?: number; } | undefined = {}): Promise { @@ -214,7 +199,7 @@ export class Page extends EventEmitter { if (typeof urlOrPredicate === 'function') return !!(urlOrPredicate(response)); return false; - }, timeout, this._sessionClosePromise()); + }, timeout, this._disconnectedPromise); } setDefaultNavigationTimeout(timeout: number) { @@ -242,7 +227,7 @@ export class Page extends EventEmitter { await this._session.send('Page.setCacheDisabled', {cacheDisabled: !enabled}); } - async emulate(options: { viewport: Viewport; userAgent: string; }) { + async emulate(options: { viewport: types.Viewport; userAgent: string; }) { await Promise.all([ this.setViewport(options.viewport), this.setUserAgent(options.userAgent), @@ -250,20 +235,14 @@ export class Page extends EventEmitter { } browserContext(): BrowserContext { - return this._target.browserContext(); - } - - _onUncaughtError(params) { - const error = new Error(params.message); - error.stack = params.stack; - this.emit(Events.Page.PageError, error); + return this._browserContext; } viewport() { return this._viewport; } - async setViewport(viewport: Viewport) { + async setViewport(viewport: types.Viewport) { const { width, height, @@ -275,8 +254,8 @@ export class Page extends EventEmitter { await this._session.send('Page.setViewport', { viewport: { width, height, isMobile, deviceScaleFactor, hasTouch, isLandscape }, }); - const oldIsMobile = this._viewport ? this._viewport.isMobile : false; - const oldHasTouch = this._viewport ? this._viewport.hasTouch : false; + const oldIsMobile = this._viewport ? !!this._viewport.isMobile : false; + const oldHasTouch = this._viewport ? !!this._viewport.hasTouch : false; this._viewport = viewport; if (oldIsMobile !== isMobile || oldHasTouch !== hasTouch) await this.reload(); @@ -288,7 +267,7 @@ export class Page extends EventEmitter { } browser() { - return this._target.browser(); + return this._browserContext.browser(); } url() { @@ -299,16 +278,6 @@ export class Page extends EventEmitter { return this._frameManager.frames(); } - _onDialogOpened(params) { - this.emit(Events.Page.Dialog, new dialog.Dialog( - params.type as dialog.DialogType, - params.message, - async (accept: boolean, promptText?: string) => { - await this._session.send('Page.handleDialog', { dialogId: params.dialogId, accept, promptText }).catch(debugError); - }, - params.defaultValue)); - } - mainFrame(): frames.Frame { return this._frameManager.mainFrame(); } @@ -419,26 +388,8 @@ export class Page extends EventEmitter { return watchDog.navigationResponse(); } - async screenshot(options: { fullPage?: boolean; clip?: { width: number; height: number; x: number; y: number; }; encoding?: string; path?: string; } = {}): Promise { - const {data} = await this._session.send('Page.screenshot', { - mimeType: getScreenshotMimeType(options), - fullPage: options.fullPage, - clip: processClip(options.clip), - }); - const buffer = options.encoding === 'base64' ? data : Buffer.from(data, 'base64'); - if (options.path) - await writeFileAsync(options.path, buffer); - return buffer; - - function processClip(clip) { - if (!clip) - return undefined; - const x = Math.round(clip.x); - const y = Math.round(clip.y); - const width = Math.round(clip.width + clip.x - x); - const height = Math.round(clip.height + clip.y - y); - return {x, y, width, height}; - } + screenshot(options: types.ScreenshotOptions = {}): Promise { + return this._screenshotter.screenshotPage(options); } evaluate: types.Evaluate = (pageFunction, ...args) => { @@ -530,12 +481,13 @@ export class Page extends EventEmitter { } async close(options: any = {}) { + assert(!this._disconnected, 'Protocol error: Connection closed. Most likely the page has been closed.'); const { runBeforeUnload = false, } = options; await this._session.send('Page.close', { runBeforeUnload }); if (!runBeforeUnload) - await this._target._isClosedPromise; + await this._closedPromise; } async content() { @@ -546,9 +498,12 @@ export class Page extends EventEmitter { return await this._frameManager.mainFrame().setContent(html); } - _onConsole({type, args, executionContextId, location}) { - const context = this._frameManager.executionContextById(executionContextId); - this.emit(Events.Page.Console, new console.ConsoleMessage(type, undefined, args.map(arg => context._createHandle(arg)), location)); + _addConsoleMessage(type: string, args: js.JSHandle[], location: console.ConsoleMessageLocation) { + if (!this.listenerCount(Events.Page.Console)) { + args.forEach(arg => arg.dispose()); + return; + } + this.emit(Events.Page.Console, new console.ConsoleMessage(type, undefined, args, location)); } isClosed(): boolean { @@ -568,11 +523,11 @@ export class Page extends EventEmitter { }); } - async _onFileChooserOpened({executionContextId, element}) { - if (!this._fileChooserInterceptors.size) + async _onFileChooserOpened(handle: dom.ElementHandle) { + if (!this._fileChooserInterceptors.size) { + await handle.dispose(); return; - const context = this._frameManager.executionContextById(executionContextId); - const handle = context._createHandle(element).asElement()!; + } const interceptors = Array.from(this._fileChooserInterceptors); this._fileChooserInterceptors.clear(); const multiple = await handle.evaluate((element: HTMLInputElement) => !!element.multiple); @@ -583,34 +538,6 @@ export class Page extends EventEmitter { } } -function getScreenshotMimeType(options) { - // options.type takes precedence over inferring the type from options.path - // because it may be a 0-length file with no extension created beforehand (i.e. as a temp file). - if (options.type) { - if (options.type === 'png') - return 'image/png'; - if (options.type === 'jpeg') - return 'image/jpeg'; - throw new Error('Unknown options.type value: ' + options.type); - } - if (options.path) { - const fileType = mime.getType(options.path); - if (fileType === 'image/png' || fileType === 'image/jpeg') - return fileType; - throw new Error('Unsupported screenshot mime type: ' + fileType); - } - return 'image/png'; -} - -export type Viewport = { - width: number; - height: number; - deviceScaleFactor?: number; - isMobile?: boolean; - isLandscape?: boolean; - hasTouch?: boolean; -} - type FileChooser = { element: dom.ElementHandle, multiple: boolean diff --git a/src/firefox/Playwright.ts b/src/firefox/Playwright.ts index dcf0061312..13e7542bf1 100644 --- a/src/firefox/Playwright.ts +++ b/src/firefox/Playwright.ts @@ -15,24 +15,28 @@ * limitations under the License. */ import { Browser } from './Browser'; -import { BrowserFetcher } from './BrowserFetcher'; +import { BrowserFetcher, BrowserFetcherOptions, OnProgressCallback, BrowserFetcherRevisionInfo } from '../browserFetcher'; import { ConnectionTransport } from '../ConnectionTransport'; import { DeviceDescriptors } from '../DeviceDescriptors'; import * as Errors from '../Errors'; -import { Launcher } from './Launcher'; -import {download, RevisionInfo} from '../download'; +import { Launcher, createBrowserFetcher } from './Launcher'; export class Playwright { private _projectRoot: string; private _launcher: Launcher; readonly _revision: string; - downloadBrowser: (options?: { onProgress?: (downloadedBytes: number, totalBytes: number) => void; }) => Promise; constructor(projectRoot: string, preferredRevision: string) { this._projectRoot = projectRoot; this._launcher = new Launcher(projectRoot, preferredRevision); this._revision = preferredRevision; - this.downloadBrowser = download.bind(null, this.createBrowserFetcher(), preferredRevision, 'Chromium'); + } + + async downloadBrowser(options?: BrowserFetcherOptions & { onProgress?: OnProgressCallback }): Promise { + const fetcher = this.createBrowserFetcher(options); + const revisionInfo = fetcher.revisionInfo(this._revision); + await fetcher.download(this._revision, options ? options.onProgress : undefined); + return revisionInfo; } launch(options: any): Promise { @@ -65,7 +69,7 @@ export class Playwright { return this._launcher.defaultArgs(options); } - createBrowserFetcher(options?: any | undefined): BrowserFetcher { - return new BrowserFetcher(this._projectRoot, { browser: 'firefox', ...options }); + createBrowserFetcher(options?: BrowserFetcherOptions): BrowserFetcher { + return createBrowserFetcher(this._projectRoot, options); } } diff --git a/src/firefox/Screenshotter.ts b/src/firefox/Screenshotter.ts new file mode 100644 index 0000000000..f8feb96823 --- /dev/null +++ b/src/firefox/Screenshotter.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { ScreenshotterDelegate } from '../screenshotter'; +import * as types from '../types'; +import * as dom from '../dom'; +import { JugglerSession } from './Connection'; +import { FrameManager } from './FrameManager'; + +export class FFScreenshotDelegate implements ScreenshotterDelegate { + private _session: JugglerSession; + private _frameManager: FrameManager; + + constructor(session: JugglerSession, frameManager: FrameManager) { + this._session = session; + this._frameManager = frameManager; + } + + getBoundingBox(handle: dom.ElementHandle): Promise { + const frameId = this._frameManager._frameData(handle.executionContext().frame()).frameId; + return this._session.send('Page.getBoundingBox', { + frameId, + objectId: handle._remoteObject.objectId, + }); + } + + canCaptureOutsideViewport(): boolean { + return true; + } + + async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise { + } + + async screenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions): Promise { + const { data } = await this._session.send('Page.screenshot', { + mimeType: ('image/' + format) as ('image/png' | 'image/jpeg'), + fullPage: options.fullPage, + clip: options.clip, + }); + return Buffer.from(data, 'base64'); + } +} diff --git a/src/firefox/api.ts b/src/firefox/api.ts index 90d0f12cba..b6e684c276 100644 --- a/src/firefox/api.ts +++ b/src/firefox/api.ts @@ -4,7 +4,7 @@ export { TimeoutError } from '../Errors'; export { Keyboard, Mouse } from '../input'; export { Browser, BrowserContext } from './Browser'; -export { BrowserFetcher } from './BrowserFetcher'; +export { BrowserFetcher } from '../browserFetcher'; export { Dialog } from '../dialog'; export { ExecutionContext, JSHandle } from '../javascript'; export { ElementHandle } from '../dom'; diff --git a/src/frames.ts b/src/frames.ts index fefe473a21..36ed432d70 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -122,12 +122,12 @@ export class Frame { return context.evaluate(pageFunction, ...args as any); } - async $(selector: string | types.Selector): Promise { + async $(selector: string | types.Selector): Promise | null> { const domWorld = await this._mainDOMWorld(); return domWorld.$(types.clearSelector(selector)); } - async $x(expression: string): Promise { + async $x(expression: string): Promise[]> { const domWorld = await this._mainDOMWorld(); return domWorld.$$('xpath=' + expression); } @@ -142,7 +142,7 @@ export class Frame { return domWorld.$$eval(selector, pageFunction, ...args as any); } - async $$(selector: string | types.Selector): Promise { + async $$(selector: string | types.Selector): Promise[]> { const domWorld = await this._mainDOMWorld(); return domWorld.$$(types.clearSelector(selector)); } diff --git a/src/helper.ts b/src/helper.ts index ef56e325da..de659b0d58 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -14,6 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import * as debug from 'debug'; import { TimeoutError } from './Errors'; @@ -22,7 +23,7 @@ export const debugError = debug(`playwright:error`); export type RegisteredListener = { emitter: NodeJS.EventEmitter; eventName: (string | symbol); - handler: (_: any) => void; + handler: (...args: any[]) => void; }; class Helper { @@ -62,7 +63,7 @@ class Helper { static addEventListener( emitter: NodeJS.EventEmitter, eventName: (string | symbol), - handler: (_: any) => void): RegisteredListener { + handler: (...args: any[]) => void): RegisteredListener { emitter.on(eventName, handler); return { emitter, eventName, handler }; } @@ -70,7 +71,7 @@ class Helper { static removeEventListeners(listeners: Array<{ emitter: NodeJS.EventEmitter; eventName: (string | symbol); - handler: (_: any) => void; + handler: (...args: any[]) => void; }>) { for (const listener of listeners) listener.emitter.removeListener(listener.eventName, listener.handler); diff --git a/src/injected/injected.ts b/src/injected/injected.ts index 7f46facc3b..b3d6434de6 100644 --- a/src/injected/injected.ts +++ b/src/injected/injected.ts @@ -17,9 +17,11 @@ class Injected { this.engines.set(engine.name, engine); } - querySelector(selector: string, root: SelectorRoot): Element | undefined { + querySelector(selector: string, root: Node): Element | undefined { const parsed = this._parseSelector(selector); - let element = root; + if (!root['querySelector']) + throw new Error('Node is not queryable.'); + let element = root as SelectorRoot; for (const { engine, selector } of parsed) { const next = engine.query((element as Element).shadowRoot || element, selector); if (!next) @@ -29,9 +31,11 @@ class Injected { return element as Element; } - querySelectorAll(selector: string, root: SelectorRoot): Element[] { + querySelectorAll(selector: string, root: Node): Element[] { const parsed = this._parseSelector(selector); - let set = new Set([ root ]); + if (!root['querySelectorAll']) + throw new Error('Node is not queryable.'); + let set = new Set([ root as SelectorRoot ]); for (const { engine, selector } of parsed) { const newSet = new Set(); for (const prev of set) { diff --git a/src/input.ts b/src/input.ts index 9e876265db..2adf924fca 100644 --- a/src/input.ts +++ b/src/input.ts @@ -287,9 +287,10 @@ export class Mouse { } } -export const selectFunction = (element: HTMLSelectElement, ...optionsToSelect: (Node | SelectOption)[]) => { - if (element.nodeName.toLowerCase() !== 'select') +export const selectFunction = (node: Node, ...optionsToSelect: (Node | SelectOption)[]) => { + if (node.nodeName.toLowerCase() !== 'select') throw new Error('Element is not a