Add initial implementation of WebDriver process to run the HTTP server
authorcarlosgc@webkit.org <carlosgc@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 18 Jul 2017 07:20:33 +0000 (07:20 +0000)
committercarlosgc@webkit.org <carlosgc@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 18 Jul 2017 07:20:33 +0000 (07:20 +0000)
https://bugs.webkit.org/show_bug.cgi?id=166682

Reviewed by Brian Burg.

.:

Enable WebDriver in the GTK port by default.

* Source/CMakeLists.txt:
* Source/cmake/OptionsGTK.cmake:
* Source/cmake/WebKitFS.cmake:
* Source/cmake/WebKitFeatures.cmake:

Source/WebDriver:

Add WebDriver process that runs the HTTP server and implements an initial set of commands. Most of the code is
cross-platform, only the HTTP server implementation, the code to launch the browser and the communication with
the remote inspector requires platform specific code. This patch includes the GTK port implementation, using
libsoup for the HTTP server, and GLib for launching the browser and communicating with the remote
inspector. This implementation follows the w3c spec (https://www.w3.org/TR/webdriver) as close as possible, but
using the official selenium python tests as reference.

* CMakeLists.txt: Added.
* Capabilities.h: Added.
* CommandResult.cpp: Added.
* CommandResult.h: Added.
* HTTPServer.cpp: Added.
* HTTPServer.h: Added.
* PlatformGTK.cmake: Added.
* Session.cpp: Added.
* Session.h: Added.
* SessionHost.cpp: Added.
* SessionHost.h: Added.
* WebDriverMain.cpp: Added.
* WebDriverService.cpp: Added.
* WebDriverService.h: Added.
* config.h: Added.
* glib/SessionHostGlib.cpp: Added.
* gtk/WebDriverServiceGtk.cpp: Added.
* soup/HTTPServerSoup.cpp: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@219605 268f45cc-cd09-0410-ab3c-d52691b4dbfc

24 files changed:
ChangeLog
Source/CMakeLists.txt
Source/WebDriver/CMakeLists.txt [new file with mode: 0644]
Source/WebDriver/Capabilities.h [new file with mode: 0644]
Source/WebDriver/ChangeLog [new file with mode: 0644]
Source/WebDriver/CommandResult.cpp [new file with mode: 0644]
Source/WebDriver/CommandResult.h [new file with mode: 0644]
Source/WebDriver/HTTPServer.cpp [new file with mode: 0644]
Source/WebDriver/HTTPServer.h [new file with mode: 0644]
Source/WebDriver/PlatformGTK.cmake [new file with mode: 0644]
Source/WebDriver/Session.cpp [new file with mode: 0644]
Source/WebDriver/Session.h [new file with mode: 0644]
Source/WebDriver/SessionHost.cpp [new file with mode: 0644]
Source/WebDriver/SessionHost.h [new file with mode: 0644]
Source/WebDriver/WebDriverMain.cpp [new file with mode: 0644]
Source/WebDriver/WebDriverService.cpp [new file with mode: 0644]
Source/WebDriver/WebDriverService.h [new file with mode: 0644]
Source/WebDriver/config.h [new file with mode: 0644]
Source/WebDriver/glib/SessionHostGlib.cpp [new file with mode: 0644]
Source/WebDriver/gtk/WebDriverServiceGtk.cpp [new file with mode: 0644]
Source/WebDriver/soup/HTTPServerSoup.cpp [new file with mode: 0644]
Source/cmake/OptionsGTK.cmake
Source/cmake/WebKitFS.cmake
Source/cmake/WebKitFeatures.cmake

index fbe131a..2463569 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,17 @@
+2017-07-17  Carlos Garcia Campos  <cgarcia@igalia.com>
+
+        Add initial implementation of WebDriver process to run the HTTP server
+        https://bugs.webkit.org/show_bug.cgi?id=166682
+
+        Reviewed by Brian Burg.
+
+        Enable WebDriver in the GTK port by default.
+
+        * Source/CMakeLists.txt:
+        * Source/cmake/OptionsGTK.cmake:
+        * Source/cmake/WebKitFS.cmake:
+        * Source/cmake/WebKitFeatures.cmake:
+
 2017-07-17  Konstantin Tokarev  <annulen@yandex.ru>
 
         [cmake] Set library types before their targets are created
index da35b62..0b38c50 100644 (file)
@@ -38,6 +38,10 @@ if (ENABLE_WEBKIT)
     add_subdirectory(WebKit)
 endif ()
 
+if (ENABLE_WEBDRIVER)
+    add_subdirectory(WebDriver)
+endif ()
+
 WEBKIT_INCLUDE_CONFIG_FILES_IF_EXISTS()
 
 # -----------------------------------------------------------------------------
diff --git a/Source/WebDriver/CMakeLists.txt b/Source/WebDriver/CMakeLists.txt
new file mode 100644 (file)
index 0000000..7c69f6a
--- /dev/null
@@ -0,0 +1,54 @@
+set_property(DIRECTORY . PROPERTY FOLDER "WebDriver")
+
+set(WebDriver_INCLUDE_DIRECTORIES
+    "${WEBDRIVER_DIR}"
+    "${JAVASCRIPTCORE_DIR}"
+    "${JAVASCRIPTCORE_DIR}/runtime"
+    "${DERIVED_SOURCES_WEBDRIVER_DIR}"
+)
+
+set(WebDriver_SOURCES
+    CommandResult.cpp
+    HTTPServer.cpp
+    Session.cpp
+    SessionHost.cpp
+    WebDriverMain.cpp
+    WebDriverService.cpp
+)
+
+set(WebDriver_LIBRARIES
+    JavaScriptCore
+)
+
+set(WebDriver_SCRIPTS
+    ${WEBKIT2_DIR}/UIProcess/Automation/atoms/ElementAttribute.js
+    ${WEBKIT2_DIR}/UIProcess/Automation/atoms/ElementDisplayed.js
+    ${WEBKIT2_DIR}/UIProcess/Automation/atoms/FindNodes.js
+    ${WEBKIT2_DIR}/UIProcess/Automation/atoms/FormElementClear.js
+    ${WEBKIT2_DIR}/UIProcess/Automation/atoms/FormSubmit.js
+)
+
+set(JavaScriptCore_SCRIPTS_DIR "${FORWARDING_HEADERS_DIR}/JavaScriptCore/Scripts")
+MAKE_JS_FILE_ARRAYS(
+    ${DERIVED_SOURCES_WEBDRIVER_DIR}/WebDriverAtoms.cpp
+    ${DERIVED_SOURCES_WEBDRIVER_DIR}/WebDriverAtoms.h
+    WebDriver
+    WebDriver_SCRIPTS
+    Session.cpp
+)
+list(APPEND WebDriver_SOURCES ${DERIVED_SOURCES_WEBDRIVER_DIR}/WebDriverAtoms.cpp)
+
+WEBKIT_INCLUDE_CONFIG_FILES_IF_EXISTS()
+
+include_directories(${WebDriver_INCLUDE_DIRECTORIES})
+include_directories(SYSTEM ${WebDriver_SYSTEM_INCLUDE_DIRECTORIES})
+add_executable(WebDriver ${WebDriver_SOURCES})
+target_link_libraries(WebDriver ${WebDriver_LIBRARIES})
+
+if (WebDriver_Process_OUTPUT_NAME)
+    set_target_properties(WebDriver PROPERTIES OUTPUT_NAME ${WebDriver_Process_OUTPUT_NAME})
+endif ()
+
+install(TARGETS WebDriver
+    RUNTIME DESTINATION "${EXEC_INSTALL_DIR}"
+)
diff --git a/Source/WebDriver/Capabilities.h b/Source/WebDriver/Capabilities.h
new file mode 100644 (file)
index 0000000..ed01a5b
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2017 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <wtf/Forward.h>
+#include <wtf/Vector.h>
+#include <wtf/text/WTFString.h>
+
+namespace WebDriver {
+
+struct Capabilities {
+    String browserName;
+    String browserVersion;
+    String platform;
+#if PLATFORM(GTK)
+    String browserBinary;
+    Vector<String> browserArguments;
+    bool useOverlayScrollbars { true };
+#endif
+};
+
+} // namespace WebDriver
diff --git a/Source/WebDriver/ChangeLog b/Source/WebDriver/ChangeLog
new file mode 100644 (file)
index 0000000..d036229
--- /dev/null
@@ -0,0 +1,32 @@
+2017-07-13  Carlos Garcia Campos  <cgarcia@igalia.com>
+
+        Add initial implementation of WebDriver process to run the HTTP server
+        https://bugs.webkit.org/show_bug.cgi?id=166682
+
+        Reviewed by Brian Burg.
+
+        Add WebDriver process that runs the HTTP server and implements an initial set of commands. Most of the code is
+        cross-platform, only the HTTP server implementation, the code to launch the browser and the communication with
+        the remote inspector requires platform specific code. This patch includes the GTK port implementation, using
+        libsoup for the HTTP server, and GLib for launching the browser and communicating with the remote
+        inspector. This implementation follows the w3c spec (https://www.w3.org/TR/webdriver) as close as possible, but
+        using the official selenium python tests as reference.
+
+        * CMakeLists.txt: Added.
+        * Capabilities.h: Added.
+        * CommandResult.cpp: Added.
+        * CommandResult.h: Added.
+        * HTTPServer.cpp: Added.
+        * HTTPServer.h: Added.
+        * PlatformGTK.cmake: Added.
+        * Session.cpp: Added.
+        * Session.h: Added.
+        * SessionHost.cpp: Added.
+        * SessionHost.h: Added.
+        * WebDriverMain.cpp: Added.
+        * WebDriverService.cpp: Added.
+        * WebDriverService.h: Added.
+        * config.h: Added.
+        * glib/SessionHostGlib.cpp: Added.
+        * gtk/WebDriverServiceGtk.cpp: Added.
+        * soup/HTTPServerSoup.cpp: Added.
diff --git a/Source/WebDriver/CommandResult.cpp b/Source/WebDriver/CommandResult.cpp
new file mode 100644 (file)
index 0000000..1b6ac11
--- /dev/null
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2017 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+#include "CommandResult.h"
+
+#include <inspector/InspectorValues.h>
+
+using namespace Inspector;
+
+namespace WebDriver {
+
+// These error codes are specified in JSON-RPC 2.0, Section 5.1.
+enum ProtocolErrorCode {
+    ParseError = -32700,
+    InvalidRequest = -32600,
+    MethodNotFound = -32601,
+    InvalidParams = -32602,
+    InternalError = -32603,
+    ServerError = -32000
+};
+
+CommandResult::CommandResult(RefPtr<InspectorValue>&& result, std::optional<ErrorCode> errorCode)
+    : m_errorCode(errorCode)
+{
+    if (!m_errorCode) {
+        m_result = WTFMove(result);
+        return;
+    }
+
+    if (!result)
+        return;
+
+    RefPtr<InspectorObject> errorObject;
+    if (!result->asObject(errorObject))
+        return;
+
+    int error;
+    if (!errorObject->getInteger("code", error))
+        return;
+    String errorMessage;
+    if (!errorObject->getString("message", errorMessage))
+        return;
+
+    switch (error) {
+    case ProtocolErrorCode::ParseError:
+    case ProtocolErrorCode::InvalidRequest:
+    case ProtocolErrorCode::MethodNotFound:
+    case ProtocolErrorCode::InvalidParams:
+    case ProtocolErrorCode::InternalError:
+        m_errorCode = ErrorCode::UnknownError;
+        m_errorMessage = errorMessage;
+        break;
+    case ProtocolErrorCode::ServerError: {
+        String errorName;
+        auto position = errorMessage.find(';');
+        if (position != notFound) {
+            errorName = errorMessage.substring(0, position);
+            m_errorMessage = errorMessage.substring(position + 1);
+        } else
+            errorName = errorMessage;
+
+        if (errorName == "WindowNotFound")
+            m_errorCode = ErrorCode::NoSuchWindow;
+        else if (errorName == "FrameNotFound")
+            m_errorCode = ErrorCode::NoSuchFrame;
+        else if (errorName == "NotImplemented")
+            m_errorCode = ErrorCode::UnsupportedOperation;
+        else if (errorName == "JavaScriptError")
+            m_errorCode = ErrorCode::JavascriptError;
+        else if (errorName == "JavaScriptTimeout")
+            m_errorCode = ErrorCode::ScriptTimeout;
+        else if (errorName == "NodeNotFound")
+            m_errorCode = ErrorCode::StaleElementReference;
+        else if (errorName == "MissingParameter" || errorName == "InvalidParameter")
+            m_errorCode = ErrorCode::InvalidArgument;
+        else if (errorName == "InvalidElementState")
+            m_errorCode = ErrorCode::InvalidElementState;
+
+        break;
+    }
+    }
+}
+
+CommandResult::CommandResult(ErrorCode errorCode, std::optional<String> errorMessage)
+    : m_errorCode(errorCode)
+    , m_errorMessage(errorMessage)
+{
+}
+
+unsigned CommandResult::httpStatusCode() const
+{
+    if (!m_errorCode)
+        return 200;
+
+    // §6.6 Handling Errors.
+    // https://www.w3.org/TR/webdriver/#handling-errors
+    switch (m_errorCode.value()) {
+    case ErrorCode::InvalidArgument:
+    case ErrorCode::InvalidElementState:
+    case ErrorCode::NoSuchElement:
+    case ErrorCode::NoSuchFrame:
+    case ErrorCode::NoSuchWindow:
+    case ErrorCode::StaleElementReference:
+        return 400;
+    case ErrorCode::InvalidSessionID:
+    case ErrorCode::UnknownCommand:
+        return 404;
+    case ErrorCode::ScriptTimeout:
+        return 408;
+    case ErrorCode::JavascriptError:
+    case ErrorCode::SessionNotCreated:
+    case ErrorCode::UnknownError:
+    case ErrorCode::UnsupportedOperation:
+        return 500;
+    }
+
+    ASSERT_NOT_REACHED();
+    return 200;
+}
+
+String CommandResult::errorString() const
+{
+    ASSERT(isError());
+
+    switch (m_errorCode.value()) {
+    case ErrorCode::InvalidArgument:
+        return ASCIILiteral("invalid argument");
+    case ErrorCode::InvalidElementState:
+        return ASCIILiteral("invalid element state");
+    case ErrorCode::InvalidSessionID:
+        return ASCIILiteral("invalid session id");
+    case ErrorCode::JavascriptError:
+        return ASCIILiteral("javascript error");
+    case ErrorCode::NoSuchElement:
+        return ASCIILiteral("no such element");
+    case ErrorCode::NoSuchFrame:
+        return ASCIILiteral("no such frame");
+    case ErrorCode::NoSuchWindow:
+        return ASCIILiteral("no such window");
+    case ErrorCode::ScriptTimeout:
+        return ASCIILiteral("script timeout");
+    case ErrorCode::SessionNotCreated:
+        return ASCIILiteral("session not created");
+    case ErrorCode::StaleElementReference:
+        return ASCIILiteral("stale element reference");
+    case ErrorCode::UnknownCommand:
+        return ASCIILiteral("unknown command");
+    case ErrorCode::UnknownError:
+        return ASCIILiteral("unknown error");
+    case ErrorCode::UnsupportedOperation:
+        return ASCIILiteral("unsupported operation");
+    }
+
+    ASSERT_NOT_REACHED();
+    return emptyString();
+}
+
+} // namespace WebDriver
diff --git a/Source/WebDriver/CommandResult.h b/Source/WebDriver/CommandResult.h
new file mode 100644 (file)
index 0000000..d521c03
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2017 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <wtf/Forward.h>
+#include <wtf/text/WTFString.h>
+
+namespace Inspector {
+class InspectorValue;
+}
+
+namespace WebDriver {
+
+class CommandResult {
+public:
+    // §6.6 Handling Errors.
+    // https://www.w3.org/TR/webdriver/#handling-errors
+    enum class ErrorCode {
+        InvalidArgument,
+        InvalidElementState,
+        InvalidSessionID,
+        JavascriptError,
+        NoSuchElement,
+        NoSuchFrame,
+        NoSuchWindow,
+        ScriptTimeout,
+        SessionNotCreated,
+        StaleElementReference,
+        UnknownCommand,
+        UnknownError,
+        UnsupportedOperation,
+    };
+
+    static CommandResult success(RefPtr<Inspector::InspectorValue>&& result = nullptr)
+    {
+        return CommandResult(WTFMove(result));
+    }
+
+    static CommandResult fail(RefPtr<Inspector::InspectorValue>&& result = nullptr)
+    {
+        return CommandResult(WTFMove(result), CommandResult::ErrorCode::UnknownError);
+    }
+
+    static CommandResult fail(ErrorCode errorCode, std::optional<String> errorMessage = std::nullopt)
+    {
+        return CommandResult(errorCode, errorMessage);
+    }
+
+    unsigned httpStatusCode() const;
+    const RefPtr<Inspector::InspectorValue>& result() const { return m_result; };
+    bool isError() const { return !!m_errorCode; }
+    ErrorCode errorCode() const { ASSERT(isError()); return m_errorCode.value(); }
+    String errorString() const;
+    std::optional<String> errorMessage() const { ASSERT(isError()); return m_errorMessage; }
+
+private:
+    explicit CommandResult(RefPtr<Inspector::InspectorValue>&&, std::optional<ErrorCode> = std::nullopt);
+    explicit CommandResult(ErrorCode, std::optional<String> = std::nullopt);
+
+    RefPtr<Inspector::InspectorValue> m_result;
+    std::optional<ErrorCode> m_errorCode;
+    std::optional<String> m_errorMessage;
+};
+
+} // namespace WebDriver
diff --git a/Source/WebDriver/HTTPServer.cpp b/Source/WebDriver/HTTPServer.cpp
new file mode 100644 (file)
index 0000000..da496f6
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+#include "HTTPServer.h"
+
+namespace WebDriver {
+
+HTTPServer::HTTPServer(HTTPRequestHandler& requestHandler)
+    : m_requestHandler(requestHandler)
+{
+}
+
+} // namespace WebDriver
diff --git a/Source/WebDriver/HTTPServer.h b/Source/WebDriver/HTTPServer.h
new file mode 100644 (file)
index 0000000..16850c3
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2017 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <wtf/Forward.h>
+#include <wtf/Vector.h>
+#include <wtf/text/WTFString.h>
+
+#if USE(SOUP)
+#include <wtf/glib/GRefPtr.h>
+typedef struct _SoupServer SoupServer;
+#endif
+
+namespace WebDriver {
+
+class HTTPRequestHandler {
+public:
+    struct Request {
+        String method;
+        String path;
+        const char* data { nullptr };
+        size_t dataLength { 0 };
+    };
+    struct Response {
+        unsigned statusCode { 0 };
+        CString data;
+        String contentType;
+    };
+    virtual void handleRequest(Request&&, Function<void (Response&&)>&& replyHandler) = 0;
+};
+
+class HTTPServer {
+public:
+    explicit HTTPServer(HTTPRequestHandler&);
+    ~HTTPServer() = default;
+
+    bool listen(unsigned port);
+    void disconnect();
+
+private:
+    HTTPRequestHandler& m_requestHandler;
+
+#if USE(SOUP)
+    GRefPtr<SoupServer> m_soupServer;
+#endif
+};
+
+} // namespace WebDriver
diff --git a/Source/WebDriver/PlatformGTK.cmake b/Source/WebDriver/PlatformGTK.cmake
new file mode 100644 (file)
index 0000000..0deb59b
--- /dev/null
@@ -0,0 +1,20 @@
+set(WebDriver_Process_OUTPUT_NAME WebKitWebDriver)
+
+add_definitions(-DLIBEXECDIR="${CMAKE_INSTALL_FULL_LIBEXECDIR}")
+
+list(APPEND WebDriver_SYSTEM_INCLUDE_DIRECTORIES
+    "${GLIB_INCLUDE_DIRS}"
+    "${LIBSOUP_INCLUDE_DIRS}"
+)
+
+list(APPEND WebDriver_SOURCES
+    glib/SessionHostGlib.cpp
+
+    gtk/WebDriverServiceGtk.cpp
+
+    soup/HTTPServerSoup.cpp
+)
+
+list(APPEND WebDriver_LIBRARIES
+    ${LIBSOUP_LIBRARIES}
+)
diff --git a/Source/WebDriver/Session.cpp b/Source/WebDriver/Session.cpp
new file mode 100644 (file)
index 0000000..35ced0e
--- /dev/null
@@ -0,0 +1,1358 @@
+/*
+ * Copyright (C) 2017 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+#include "Session.h"
+
+#include "CommandResult.h"
+#include "SessionHost.h"
+#include "WebDriverAtoms.h"
+#include <inspector/InspectorValues.h>
+#include <wtf/CryptographicallyRandomNumber.h>
+#include <wtf/HexNumber.h>
+#include <wtf/UUID.h>
+
+using namespace Inspector;
+
+namespace WebDriver {
+
+// The web element identifier is a constant defined by the spec in Section 11 Elements.
+// https://www.w3.org/TR/webdriver/#elements
+static const String webElementIdentifier = ASCIILiteral("element-6066-11e4-a52e-4f735466cecf");
+
+Session::Session(std::unique_ptr<SessionHost>&& host)
+    : m_host(WTFMove(host))
+    , m_id(createCanonicalUUIDString())
+{
+}
+
+Session::~Session()
+{
+}
+
+const Capabilities& Session::capabilities() const
+{
+    return m_host->capabilities();
+}
+
+void Session::close(Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::success());
+        return;
+    }
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("handle"), m_toplevelBrowsingContext.value());
+    m_host->sendCommandToBackend(ASCIILiteral("closeBrowsingContext"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        m_toplevelBrowsingContext = std::nullopt;
+        completionHandler(CommandResult::success());
+    });
+}
+
+void Session::setTimeouts(const Timeouts& timeouts, Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (timeouts.script)
+        m_timeouts.script = timeouts.script;
+    if (timeouts.pageLoad)
+        m_timeouts.pageLoad = timeouts.pageLoad;
+    if (timeouts.implicit)
+        m_timeouts.implicit = timeouts.implicit;
+    completionHandler(CommandResult::success());
+}
+
+void Session::createTopLevelBrowsingContext(Function<void (CommandResult&&)>&& completionHandler)
+{
+    ASSERT(!m_toplevelBrowsingContext.value());
+    m_host->startAutomationSession(m_id, [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)]() mutable {
+        m_host->sendCommandToBackend(ASCIILiteral("createBrowsingContext"), nullptr, [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) mutable {
+            if (response.isError || !response.responseObject) {
+                completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+                return;
+            }
+            String handle;
+            if (!response.responseObject->getString(ASCIILiteral("handle"), handle)) {
+                completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+                return;
+            }
+            m_toplevelBrowsingContext = handle;
+            completionHandler(CommandResult::success());
+        });
+    });
+}
+
+void Session::go(const String& url, Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("handle"), m_toplevelBrowsingContext.value());
+    parameters->setString(ASCIILiteral("url"), url);
+    m_host->sendCommandToBackend(ASCIILiteral("navigateBrowsingContext"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        m_browsingContext = std::nullopt;
+        completionHandler(CommandResult::success());
+    });
+}
+
+void Session::getCurrentURL(Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("handle"), m_toplevelBrowsingContext.value());
+    m_host->sendCommandToBackend(ASCIILiteral("getBrowsingContext"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError || !response.responseObject) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        RefPtr<InspectorObject> browsingContext;
+        if (!response.responseObject->getObject("context", browsingContext)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        String url;
+        if (!browsingContext->getString("url", url)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        completionHandler(CommandResult::success(InspectorValue::create(url)));
+    });
+}
+
+void Session::back(Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("handle"), m_toplevelBrowsingContext.value());
+    m_host->sendCommandToBackend(ASCIILiteral("goBackInBrowsingContext"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        m_browsingContext = std::nullopt;
+        completionHandler(CommandResult::success());
+    });
+}
+
+void Session::forward(Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("handle"), m_toplevelBrowsingContext.value());
+    m_host->sendCommandToBackend(ASCIILiteral("goForwardInBrowsingContext"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        m_browsingContext = std::nullopt;
+        completionHandler(CommandResult::success());
+    });
+}
+
+void Session::refresh(Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("handle"), m_toplevelBrowsingContext.value());
+    m_host->sendCommandToBackend(ASCIILiteral("reloadBrowsingContext"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        m_browsingContext = std::nullopt;
+        completionHandler(CommandResult::success());
+    });
+}
+
+void Session::getTitle(Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("browsingContextHandle"), m_toplevelBrowsingContext.value());
+    parameters->setString(ASCIILiteral("function"), ASCIILiteral("function() { return document.title; }"));
+    parameters->setArray(ASCIILiteral("arguments"), InspectorArray::create());
+    m_host->sendCommandToBackend(ASCIILiteral("evaluateJavaScriptFunction"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError || !response.responseObject) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        String valueString;
+        if (!response.responseObject->getString(ASCIILiteral("result"), valueString)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        RefPtr<InspectorValue> resultValue;
+        if (!InspectorValue::parseJSON(valueString, resultValue)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        completionHandler(CommandResult::success(WTFMove(resultValue)));
+    });
+}
+
+void Session::getWindowHandle(Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("handle"), m_toplevelBrowsingContext.value());
+    m_host->sendCommandToBackend(ASCIILiteral("getBrowsingContext"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError || !response.responseObject) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        RefPtr<InspectorObject> browsingContext;
+        if (!response.responseObject->getObject(ASCIILiteral("context"), browsingContext)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        String handle;
+        if (!browsingContext->getString(ASCIILiteral("handle"), handle)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        completionHandler(CommandResult::success(InspectorValue::create(handle)));
+    });
+}
+
+void Session::closeWindow(Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    close(WTFMove(completionHandler));
+}
+
+void Session::switchToWindow(const String& windowHandle, Function<void (CommandResult&&)>&& completionHandler)
+{
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("browsingContextHandle"), windowHandle);
+    m_host->sendCommandToBackend(ASCIILiteral("switchToBrowsingContext"), WTFMove(parameters), [this, protectedThis = makeRef(*this), windowHandle, completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        m_toplevelBrowsingContext = windowHandle;
+        completionHandler(CommandResult::success());
+    });
+}
+
+void Session::getWindowHandles(Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    m_host->sendCommandToBackend(ASCIILiteral("getBrowsingContexts"), InspectorObject::create(), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError || !response.responseObject) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        RefPtr<InspectorArray> browsingContextArray;
+        if (!response.responseObject->getArray(ASCIILiteral("contexts"), browsingContextArray)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        RefPtr<InspectorArray> windowHandles = InspectorArray::create();
+        for (unsigned i = 0; i < browsingContextArray->length(); ++i) {
+            RefPtr<InspectorValue> browsingContextValue = browsingContextArray->get(i);
+            RefPtr<InspectorObject> browsingContext;
+            if (!browsingContextValue->asObject(browsingContext)) {
+                completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+                return;
+            }
+
+            String handle;
+            if (!browsingContext->getString(ASCIILiteral("handle"), handle)) {
+                completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+                return;
+            }
+
+            windowHandles->pushString(handle);
+        }
+        completionHandler(CommandResult::success(WTFMove(windowHandles)));
+    });
+}
+
+void Session::switchToFrame(RefPtr<InspectorValue>&& frameID, Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    if (frameID->isNull()) {
+        m_browsingContext = std::nullopt;
+        completionHandler(CommandResult::success());
+        return;
+    }
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("browsingContextHandle"), m_toplevelBrowsingContext.value());
+    if (m_browsingContext)
+        parameters->setString(ASCIILiteral("frameHandle"), m_browsingContext.value());
+
+    int frameIndex;
+    if (frameID->asInteger(frameIndex)) {
+        if (frameIndex < 0 || frameIndex > USHRT_MAX) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchFrame));
+            return;
+        }
+        parameters->setInteger(ASCIILiteral("ordinal"), frameIndex);
+    } else {
+        String frameElementID = extractElementID(*frameID);
+        if (!frameElementID.isEmpty())
+            parameters->setString(ASCIILiteral("nodeHandle"), frameElementID);
+        else {
+            String frameName;
+            if (!frameID->asString(frameName)) {
+                completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchFrame));
+                return;
+            }
+            parameters->setString(ASCIILiteral("name"), frameName);
+        }
+    }
+
+    m_host->sendCommandToBackend(ASCIILiteral("resolveChildFrameHandle"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError || !response.responseObject) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        String frameHandle;
+        if (!response.responseObject->getString(ASCIILiteral("result"), frameHandle)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        m_browsingContext = frameHandle;
+        completionHandler(CommandResult::success());
+    });
+}
+
+void Session::switchToParentFrame(Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    if (!m_browsingContext) {
+        completionHandler(CommandResult::success());
+        return;
+    }
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("browsingContextHandle"), m_toplevelBrowsingContext.value());
+    parameters->setString(ASCIILiteral("frameHandle"), m_browsingContext.value());
+    m_host->sendCommandToBackend(ASCIILiteral("resolveParentFrameHandle"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError || !response.responseObject) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        String frameHandle;
+        if (!response.responseObject->getString(ASCIILiteral("result"), frameHandle)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        m_browsingContext = frameHandle;
+        completionHandler(CommandResult::success());
+    });
+}
+
+void Session::getWindowPosition(Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("handle"), m_toplevelBrowsingContext.value());
+    m_host->sendCommandToBackend(ASCIILiteral("getBrowsingContext"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError || !response.responseObject) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        RefPtr<InspectorObject> browsingContext;
+        if (!response.responseObject->getObject(ASCIILiteral("context"), browsingContext)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        RefPtr<InspectorObject> windowOrigin;
+        if (!browsingContext->getObject(ASCIILiteral("windowOrigin"), windowOrigin)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        completionHandler(CommandResult::success(WTFMove(windowOrigin)));
+    });
+}
+
+void Session::setWindowPosition(int windowX, int windowY, Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("handle"), m_toplevelBrowsingContext.value());
+    RefPtr<InspectorObject> windowOrigin = InspectorObject::create();
+    windowOrigin->setInteger("x", windowX);
+    windowOrigin->setInteger("y", windowY);
+    parameters->setObject(ASCIILiteral("origin"), WTFMove(windowOrigin));
+    m_host->sendCommandToBackend(ASCIILiteral("moveWindowOfBrowsingContext"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        completionHandler(CommandResult::success());
+    });
+}
+
+void Session::getWindowSize(Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("handle"), m_toplevelBrowsingContext.value());
+    m_host->sendCommandToBackend(ASCIILiteral("getBrowsingContext"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError || !response.responseObject) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        RefPtr<InspectorObject> browsingContext;
+        if (!response.responseObject->getObject(ASCIILiteral("context"), browsingContext)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        RefPtr<InspectorObject> windowSize;
+        if (!browsingContext->getObject(ASCIILiteral("windowSize"), windowSize)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        completionHandler(CommandResult::success(WTFMove(windowSize)));
+    });
+}
+
+void Session::setWindowSize(int windowWidth, int windowHeight, Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("handle"), m_toplevelBrowsingContext.value());
+    RefPtr<InspectorObject> windowSize = InspectorObject::create();
+    windowSize->setInteger("width", windowWidth);
+    windowSize->setInteger("height", windowHeight);
+    parameters->setObject(ASCIILiteral("size"), WTFMove(windowSize));
+    m_host->sendCommandToBackend(ASCIILiteral("resizeWindowOfBrowsingContext"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        completionHandler(CommandResult::success());
+    });
+}
+
+RefPtr<InspectorObject> Session::createElement(RefPtr<InspectorValue>&& value)
+{
+    RefPtr<InspectorObject> valueObject;
+    if (!value->asObject(valueObject))
+        return nullptr;
+
+    String elementID;
+    if (!valueObject->getString("session-node-" + m_id, elementID))
+        return nullptr;
+
+    RefPtr<InspectorObject> elementObject = InspectorObject::create();
+    elementObject->setString(webElementIdentifier, elementID);
+    return elementObject;
+}
+
+RefPtr<InspectorObject> Session::createElement(const String& elementID)
+{
+    RefPtr<InspectorObject> elementObject = InspectorObject::create();
+    elementObject->setString("session-node-" + m_id, elementID);
+    return elementObject;
+}
+
+RefPtr<InspectorObject> Session::extractElement(InspectorValue& value)
+{
+    String elementID = extractElementID(value);
+    return !elementID.isEmpty() ? createElement(elementID) : nullptr;
+}
+
+String Session::extractElementID(InspectorValue& value)
+{
+    RefPtr<InspectorObject> valueObject;
+    if (!value.asObject(valueObject))
+        return emptyString();
+
+    String elementID;
+    if (!valueObject->getString(webElementIdentifier, elementID))
+        return emptyString();
+
+    return elementID;
+}
+
+void Session::computeElementLayout(const String& elementID, OptionSet<ElementLayoutOption> options, Function<void (std::optional<Rect>&&, RefPtr<InspectorObject>&&)>&& completionHandler)
+{
+    ASSERT(m_toplevelBrowsingContext.value());
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("browsingContextHandle"), m_toplevelBrowsingContext.value());
+    parameters->setString(ASCIILiteral("frameHandle"), m_browsingContext.value());
+    parameters->setString(ASCIILiteral("nodeHandle"), elementID);
+    parameters->setBoolean(ASCIILiteral("scrollIntoViewIfNeeded"), options.contains(ElementLayoutOption::ScrollIntoViewIfNeeded));
+    parameters->setBoolean(ASCIILiteral("useViewportCoordinates"), options.contains(ElementLayoutOption::UseViewportCoordinates));
+    m_host->sendCommandToBackend(ASCIILiteral("computeElementLayout"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) mutable {
+        if (response.isError || !response.responseObject) {
+            completionHandler(std::nullopt, WTFMove(response.responseObject));
+            return;
+        }
+        RefPtr<InspectorObject> rectObject;
+        if (!response.responseObject->getObject(ASCIILiteral("rect"), rectObject)) {
+            completionHandler(std::nullopt, nullptr);
+            return;
+        }
+        std::optional<int> elementX;
+        std::optional<int> elementY;
+        RefPtr<InspectorObject> elementPosition;
+        if (rectObject->getObject(ASCIILiteral("origin"), elementPosition)) {
+            int x, y;
+            if (elementPosition->getInteger(ASCIILiteral("x"), x) && elementPosition->getInteger(ASCIILiteral("y"), y)) {
+                elementX = x;
+                elementY = y;
+            }
+        }
+        if (!elementX || !elementY) {
+            completionHandler(std::nullopt, nullptr);
+            return;
+        }
+        std::optional<int> elementWidth;
+        std::optional<int> elementHeight;
+        RefPtr<InspectorObject> elementSize;
+        if (rectObject->getObject(ASCIILiteral("size"), elementSize)) {
+            int width, height;
+            if (elementSize->getInteger(ASCIILiteral("width"), width) && elementSize->getInteger(ASCIILiteral("height"), height)) {
+                elementWidth = width;
+                elementHeight = height;
+            }
+        }
+        if (!elementWidth || !elementHeight) {
+            completionHandler(std::nullopt, nullptr);
+            return;
+        }
+        Rect rect = { { elementX.value(), elementY.value() }, { elementWidth.value(), elementHeight.value() } };
+        completionHandler(rect, nullptr);
+    });
+}
+
+void Session::findElements(const String& strategy, const String& selector, FindElementsMode mode, const String& rootElementID, Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    auto implicitWait = m_timeouts.implicit.value_or(0_s);
+    RefPtr<InspectorArray> arguments = InspectorArray::create();
+    arguments->pushString(InspectorValue::create(strategy)->toJSONString());
+    if (rootElementID.isEmpty())
+        arguments->pushString(InspectorValue::null()->toJSONString());
+    else
+        arguments->pushString(createElement(rootElementID)->toJSONString());
+    arguments->pushString(InspectorValue::create(selector)->toJSONString());
+    arguments->pushString(InspectorValue::create(mode == FindElementsMode::Single)->toJSONString());
+    arguments->pushString(InspectorValue::create(implicitWait.milliseconds())->toJSONString());
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("browsingContextHandle"), m_toplevelBrowsingContext.value());
+    if (m_browsingContext)
+        parameters->setString(ASCIILiteral("frameHandle"), m_browsingContext.value());
+    parameters->setString(ASCIILiteral("function"), FindNodesJavaScript);
+    parameters->setArray(ASCIILiteral("arguments"), WTFMove(arguments));
+    parameters->setBoolean(ASCIILiteral("expectsImplicitCallbackArgument"), true);
+    // If there's an implicit wait, use one second more as callback timeout.
+    if (implicitWait)
+        parameters->setInteger(ASCIILiteral("callbackTimeout"), Seconds(implicitWait + 1_s).millisecondsAs<int>());
+
+    m_host->sendCommandToBackend(ASCIILiteral("evaluateJavaScriptFunction"), WTFMove(parameters), [this, protectedThis = makeRef(*this), mode, completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError || !response.responseObject) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        String valueString;
+        if (!response.responseObject->getString(ASCIILiteral("result"), valueString)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        RefPtr<InspectorValue> resultValue;
+        if (!InspectorValue::parseJSON(valueString, resultValue)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+
+        switch (mode) {
+        case FindElementsMode::Single: {
+            RefPtr<InspectorObject> elementObject = createElement(WTFMove(resultValue));
+            if (!elementObject) {
+                completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchElement));
+                return;
+            }
+            completionHandler(CommandResult::success(WTFMove(elementObject)));
+            break;
+        }
+        case FindElementsMode::Multiple: {
+            RefPtr<InspectorArray> elementsArray;
+            if (!resultValue->asArray(elementsArray)) {
+                completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchElement));
+                return;
+            }
+            RefPtr<InspectorArray> elementObjectsArray = InspectorArray::create();
+            unsigned elementsArrayLength = elementsArray->length();
+            for (unsigned i = 0; i < elementsArrayLength; ++i) {
+                if (auto elementObject = createElement(elementsArray->get(i)))
+                    elementObjectsArray->pushObject(WTFMove(elementObject));
+            }
+            completionHandler(CommandResult::success(WTFMove(elementObjectsArray)));
+            break;
+        }
+        }
+    });
+}
+
+void Session::isElementSelected(const String& elementID, Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    RefPtr<InspectorArray> arguments = InspectorArray::create();
+    arguments->pushString(createElement(elementID)->toJSONString());
+    arguments->pushString(InspectorValue::create("selected")->toJSONString());
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("browsingContextHandle"), m_toplevelBrowsingContext.value());
+    if (m_browsingContext)
+        parameters->setString(ASCIILiteral("frameHandle"), m_browsingContext.value());
+    parameters->setString(ASCIILiteral("function"), ElementAttributeJavaScript);
+    parameters->setArray(ASCIILiteral("arguments"), WTFMove(arguments));
+    m_host->sendCommandToBackend(ASCIILiteral("evaluateJavaScriptFunction"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError || !response.responseObject) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        String valueString;
+        if (!response.responseObject->getString(ASCIILiteral("result"), valueString)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        RefPtr<InspectorValue> resultValue;
+        if (!InspectorValue::parseJSON(valueString, resultValue)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        if (resultValue->isNull()) {
+            completionHandler(CommandResult::success(InspectorValue::create(false)));
+            return;
+        }
+        String booleanResult;
+        if (!resultValue->asString(booleanResult) || booleanResult != "true") {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        completionHandler(CommandResult::success(InspectorValue::create(true)));
+    });
+}
+
+void Session::getElementText(const String& elementID, Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    RefPtr<InspectorArray> arguments = InspectorArray::create();
+    arguments->pushString(createElement(elementID)->toJSONString());
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("browsingContextHandle"), m_toplevelBrowsingContext.value());
+    if (m_browsingContext)
+        parameters->setString(ASCIILiteral("frameHandle"), m_browsingContext.value());
+    // FIXME: Add an atom to properly implement this instead of just using innerText.
+    parameters->setString(ASCIILiteral("function"), ASCIILiteral("function(element) { return element.innerText.replace(/^[^\\S\\xa0]+|[^\\S\\xa0]+$/g, '') }"));
+    parameters->setArray(ASCIILiteral("arguments"), WTFMove(arguments));
+    m_host->sendCommandToBackend(ASCIILiteral("evaluateJavaScriptFunction"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError || !response.responseObject) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        String valueString;
+        if (!response.responseObject->getString(ASCIILiteral("result"), valueString)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        RefPtr<InspectorValue> resultValue;
+        if (!InspectorValue::parseJSON(valueString, resultValue)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        completionHandler(CommandResult::success(WTFMove(resultValue)));
+    });
+}
+
+void Session::getElementTagName(const String& elementID, Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    RefPtr<InspectorArray> arguments = InspectorArray::create();
+    arguments->pushString(createElement(elementID)->toJSONString());
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("browsingContextHandle"), m_toplevelBrowsingContext.value());
+    if (m_browsingContext)
+        parameters->setString(ASCIILiteral("frameHandle"), m_browsingContext.value());
+    parameters->setString(ASCIILiteral("function"), ASCIILiteral("function(element) { return element.tagName.toLowerCase() }"));
+    parameters->setArray(ASCIILiteral("arguments"), WTFMove(arguments));
+    m_host->sendCommandToBackend(ASCIILiteral("evaluateJavaScriptFunction"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError || !response.responseObject) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        String valueString;
+        if (!response.responseObject->getString(ASCIILiteral("result"), valueString)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        RefPtr<InspectorValue> resultValue;
+        if (!InspectorValue::parseJSON(valueString, resultValue)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        completionHandler(CommandResult::success(WTFMove(resultValue)));
+    });
+}
+
+void Session::getElementRect(const String& elementID, Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    computeElementLayout(elementID, { }, [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](std::optional<Rect>&& rect, RefPtr<InspectorObject>&& error) {
+        if (!rect || error) {
+            completionHandler(CommandResult::fail(WTFMove(error)));
+            return;
+        }
+        RefPtr<InspectorObject> rectObject = InspectorObject::create();
+        rectObject->setInteger(ASCIILiteral("x"), rect.value().origin.x);
+        rectObject->setInteger(ASCIILiteral("y"), rect.value().origin.y);
+        rectObject->setInteger(ASCIILiteral("width"), rect.value().size.width);
+        rectObject->setInteger(ASCIILiteral("height"), rect.value().size.height);
+        completionHandler(CommandResult::success(WTFMove(rectObject)));
+    });
+}
+
+void Session::isElementEnabled(const String& elementID, Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    RefPtr<InspectorArray> arguments = InspectorArray::create();
+    arguments->pushString(createElement(elementID)->toJSONString());
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("browsingContextHandle"), m_toplevelBrowsingContext.value());
+    if (m_browsingContext)
+        parameters->setString(ASCIILiteral("frameHandle"), m_browsingContext.value());
+    parameters->setString(ASCIILiteral("function"), ASCIILiteral("function(element) { return element.disabled === undefined ? true : !element.disabled }"));
+    parameters->setArray(ASCIILiteral("arguments"), WTFMove(arguments));
+    m_host->sendCommandToBackend(ASCIILiteral("evaluateJavaScriptFunction"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError || !response.responseObject) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        String valueString;
+        if (!response.responseObject->getString(ASCIILiteral("result"), valueString)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        RefPtr<InspectorValue> resultValue;
+        if (!InspectorValue::parseJSON(valueString, resultValue)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        completionHandler(CommandResult::success(WTFMove(resultValue)));
+    });
+}
+
+void Session::isElementDisplayed(const String& elementID, Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    RefPtr<InspectorArray> arguments = InspectorArray::create();
+    arguments->pushString(createElement(elementID)->toJSONString());
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("browsingContextHandle"), m_toplevelBrowsingContext.value());
+    if (m_browsingContext)
+        parameters->setString(ASCIILiteral("frameHandle"), m_browsingContext.value());
+    parameters->setString(ASCIILiteral("function"), ElementDisplayedJavaScript);
+    parameters->setArray(ASCIILiteral("arguments"), WTFMove(arguments));
+    m_host->sendCommandToBackend(ASCIILiteral("evaluateJavaScriptFunction"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError || !response.responseObject) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        String valueString;
+        if (!response.responseObject->getString(ASCIILiteral("result"), valueString)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        RefPtr<InspectorValue> resultValue;
+        if (!InspectorValue::parseJSON(valueString, resultValue)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        completionHandler(CommandResult::success(WTFMove(resultValue)));
+    });
+}
+
+void Session::getElementAttribute(const String& elementID, const String& attribute, Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    RefPtr<InspectorArray> arguments = InspectorArray::create();
+    arguments->pushString(createElement(elementID)->toJSONString());
+    arguments->pushString(InspectorValue::create(attribute)->toJSONString());
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("browsingContextHandle"), m_toplevelBrowsingContext.value());
+    if (m_browsingContext)
+        parameters->setString(ASCIILiteral("frameHandle"), m_browsingContext.value());
+    parameters->setString(ASCIILiteral("function"), ElementAttributeJavaScript);
+    parameters->setArray(ASCIILiteral("arguments"), WTFMove(arguments));
+    m_host->sendCommandToBackend(ASCIILiteral("evaluateJavaScriptFunction"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError || !response.responseObject) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        String valueString;
+        if (!response.responseObject->getString(ASCIILiteral("result"), valueString)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        RefPtr<InspectorValue> resultValue;
+        if (!InspectorValue::parseJSON(valueString, resultValue)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        completionHandler(CommandResult::success(WTFMove(resultValue)));
+    });
+}
+
+void Session::elementClick(const String& elementID, Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    OptionSet<ElementLayoutOption> options = ElementLayoutOption::ScrollIntoViewIfNeeded;
+    options |= ElementLayoutOption::UseViewportCoordinates;
+    computeElementLayout(elementID, options, [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](std::optional<Rect>&& rect, RefPtr<InspectorObject>&& error) mutable {
+        if (!rect || error) {
+            completionHandler(CommandResult::fail(WTFMove(error)));
+            return;
+        }
+
+        // FIXME: the center of the bounding box is not always part of the element.
+        performMouseInteraction(rect.value().origin.x + rect.value().size.width / 2, rect.value().origin.y + rect.value().size.height / 2,
+            MouseButton::Left, MouseInteraction::SingleClick, WTFMove(completionHandler));
+    });
+}
+
+void Session::elementClear(const String& elementID, Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    RefPtr<InspectorArray> arguments = InspectorArray::create();
+    arguments->pushString(createElement(elementID)->toJSONString());
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("browsingContextHandle"), m_toplevelBrowsingContext.value());
+    if (m_browsingContext)
+        parameters->setString(ASCIILiteral("frameHandle"), m_browsingContext.value());
+    parameters->setString(ASCIILiteral("function"), FormElementClearJavaScript);
+    parameters->setArray(ASCIILiteral("arguments"), WTFMove(arguments));
+    m_host->sendCommandToBackend(ASCIILiteral("evaluateJavaScriptFunction"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        completionHandler(CommandResult::success());
+    });
+}
+
+String Session::virtualKeyForKeySequence(const String& keySequence, KeyModifier& modifier)
+{
+    // §17.4.2 Keyboard Actions.
+    // https://www.w3.org/TR/webdriver/#keyboard-actions
+    modifier = KeyModifier::None;
+    switch (keySequence[0]) {
+    case 0xE001U:
+        return ASCIILiteral("Cancel");
+    case 0xE002U:
+        return ASCIILiteral("Help");
+    case 0xE003U:
+        return ASCIILiteral("Backspace");
+    case 0xE004U:
+        return ASCIILiteral("Tab");
+    case 0xE005U:
+        return ASCIILiteral("Clear");
+    case 0xE006U:
+        return ASCIILiteral("Return");
+    case 0xE007U:
+        return ASCIILiteral("Enter");
+    case 0xE008U:
+        modifier = KeyModifier::Shift;
+        return ASCIILiteral("Shift");
+    case 0xE009U:
+        modifier = KeyModifier::Control;
+        return ASCIILiteral("Control");
+    case 0xE00AU:
+        modifier = KeyModifier::Alternate;
+        return ASCIILiteral("Alternate");
+    case 0xE00BU:
+        return ASCIILiteral("Pause");
+    case 0xE00CU:
+        return ASCIILiteral("Escape");
+    case 0xE00DU:
+        return ASCIILiteral("Space");
+    case 0xE00EU:
+        return ASCIILiteral("PageUp");
+    case 0xE00FU:
+        return ASCIILiteral("PageDown");
+    case 0xE010U:
+        return ASCIILiteral("End");
+    case 0xE011U:
+        return ASCIILiteral("Home");
+    case 0xE012U:
+        return ASCIILiteral("LeftArrow");
+    case 0xE013U:
+        return ASCIILiteral("UpArrow");
+    case 0xE014U:
+        return ASCIILiteral("RightArrow");
+    case 0xE015U:
+        return ASCIILiteral("DownArrow");
+    case 0xE016U:
+        return ASCIILiteral("Insert");
+    case 0xE017U:
+        return ASCIILiteral("Delete");
+    case 0xE018U:
+        return ASCIILiteral("Semicolon");
+    case 0xE019U:
+        return ASCIILiteral("Equals");
+    case 0xE01AU:
+        return ASCIILiteral("NumberPad0");
+    case 0xE01BU:
+        return ASCIILiteral("NumberPad1");
+    case 0xE01CU:
+        return ASCIILiteral("NumberPad2");
+    case 0xE01DU:
+        return ASCIILiteral("NumberPad3");
+    case 0xE01EU:
+        return ASCIILiteral("NumberPad4");
+    case 0xE01FU:
+        return ASCIILiteral("NumberPad5");
+    case 0xE020U:
+        return ASCIILiteral("NumberPad6");
+    case 0xE021U:
+        return ASCIILiteral("NumberPad7");
+    case 0xE022U:
+        return ASCIILiteral("NumberPad8");
+    case 0xE023U:
+        return ASCIILiteral("NumberPad9");
+    case 0xE024U:
+        return ASCIILiteral("NumberPadMultiply");
+    case 0xE025U:
+        return ASCIILiteral("NumberPadAdd");
+    case 0xE026U:
+        return ASCIILiteral("NumberPadSeparator");
+    case 0xE027U:
+        return ASCIILiteral("NumberPadSubtract");
+    case 0xE028U:
+        return ASCIILiteral("NumberPadDecimal");
+    case 0xE029U:
+        return ASCIILiteral("NumberPadDivide");
+    case 0xE031U:
+        return ASCIILiteral("Function1");
+    case 0xE032U:
+        return ASCIILiteral("Function2");
+    case 0xE033U:
+        return ASCIILiteral("Function3");
+    case 0xE034U:
+        return ASCIILiteral("Function4");
+    case 0xE035U:
+        return ASCIILiteral("Function5");
+    case 0xE036U:
+        return ASCIILiteral("Function6");
+    case 0xE037U:
+        return ASCIILiteral("Function7");
+    case 0xE038U:
+        return ASCIILiteral("Function8");
+    case 0xE039U:
+        return ASCIILiteral("Function9");
+    case 0xE03AU:
+        return ASCIILiteral("Function10");
+    case 0xE03BU:
+        return ASCIILiteral("Function11");
+    case 0xE03CU:
+        return ASCIILiteral("Function12");
+    case 0xE03DU:
+        modifier = KeyModifier::Meta;
+        return ASCIILiteral("Meta");
+    default:
+        break;
+    }
+    return String();
+}
+
+void Session::elementSendKeys(const String& elementID, Vector<String>&& keys, Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    // FIXME: move this to an atom.
+    static const char focusScript[] =
+        "function focus(element) {"
+        "    var doc = element.ownerDocument || element;"
+        "    var prevActiveElement = doc.activeElement;"
+        "    if (element != prevActiveElement && prevActiveElement)"
+        "        prevActiveElement.blur();"
+        "    element.focus();"
+        "    if (element != prevActiveElement && element.value && element.value.length && element.setSelectionRange)"
+        "        element.setSelectionRange(element.value.length, element.value.length);"
+        "    if (element != doc.activeElement)"
+        "        throw new Error('cannot focus element');"
+        "}";
+
+    RefPtr<InspectorArray> arguments = InspectorArray::create();
+    arguments->pushString(createElement(elementID)->toJSONString());
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("browsingContextHandle"), m_toplevelBrowsingContext.value());
+    if (m_browsingContext)
+        parameters->setString(ASCIILiteral("frameHandle"), m_browsingContext.value());
+    parameters->setString(ASCIILiteral("function"), focusScript);
+    parameters->setArray(ASCIILiteral("arguments"), WTFMove(arguments));
+    m_host->sendCommandToBackend(ASCIILiteral("evaluateJavaScriptFunction"), WTFMove(parameters), [this, protectedThis = makeRef(*this), keys = WTFMove(keys), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) mutable {
+        if (response.isError || !response.responseObject) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+
+        unsigned stickyModifiers = 0;
+        Vector<KeyboardInteraction> interactions;
+        interactions.reserveInitialCapacity(keys.size());
+        for (const auto& key : keys) {
+            KeyboardInteraction interaction;
+            KeyModifier modifier;
+            auto virtualKey = virtualKeyForKeySequence(key, modifier);
+            if (!virtualKey.isNull()) {
+                interaction.key = virtualKey;
+                if (modifier != KeyModifier::None) {
+                    stickyModifiers ^= modifier;
+                    if (stickyModifiers & modifier)
+                        interaction.type = KeyboardInteractionType::KeyPress;
+                    else
+                        interaction.type = KeyboardInteractionType::KeyRelease;
+                }
+            } else
+                interaction.text = key;
+            interactions.uncheckedAppend(WTFMove(interaction));
+        }
+
+        // Reset sticky modifiers if needed.
+        if (stickyModifiers) {
+            if (stickyModifiers & KeyModifier::Shift)
+                interactions.append({ KeyboardInteractionType::KeyRelease, std::nullopt, std::optional<String>(ASCIILiteral("Shift")) });
+            if (stickyModifiers & KeyModifier::Control)
+                interactions.append({ KeyboardInteractionType::KeyRelease, std::nullopt, std::optional<String>(ASCIILiteral("Control")) });
+            if (stickyModifiers & KeyModifier::Alternate)
+                interactions.append({ KeyboardInteractionType::KeyRelease, std::nullopt, std::optional<String>(ASCIILiteral("Alternate")) });
+            if (stickyModifiers & KeyModifier::Meta)
+                interactions.append({ KeyboardInteractionType::KeyRelease, std::nullopt, std::optional<String>(ASCIILiteral("Meta")) });
+        }
+
+        performKeyboardInteractions(WTFMove(interactions), WTFMove(completionHandler));
+    });
+}
+
+void Session::elementSubmit(const String& elementID, Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    RefPtr<InspectorArray> arguments = InspectorArray::create();
+    arguments->pushString(createElement(elementID)->toJSONString());
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("browsingContextHandle"), m_toplevelBrowsingContext.value());
+    if (m_browsingContext)
+        parameters->setString(ASCIILiteral("frameHandle"), m_browsingContext.value());
+    parameters->setString(ASCIILiteral("function"), FormSubmitJavaScript);
+    parameters->setArray(ASCIILiteral("arguments"), WTFMove(arguments));
+    m_host->sendCommandToBackend(ASCIILiteral("evaluateJavaScriptFunction"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        completionHandler(CommandResult::success());
+    });
+}
+
+RefPtr<InspectorValue> Session::handleScriptResult(RefPtr<InspectorValue>&& resultValue)
+{
+    RefPtr<InspectorArray> resultArray;
+    if (resultValue->asArray(resultArray)) {
+        RefPtr<InspectorArray> returnValueArray = InspectorArray::create();
+        unsigned resultArrayLength = resultArray->length();
+        for (unsigned i = 0; i < resultArrayLength; ++i)
+            returnValueArray->pushValue(handleScriptResult(resultArray->get(i)));
+        return returnValueArray;
+    }
+
+    if (auto element = createElement(RefPtr<InspectorValue>(resultValue)))
+        return element;
+
+    RefPtr<InspectorObject> resultObject;
+    if (resultValue->asObject(resultObject)) {
+        RefPtr<InspectorObject> returnValueObject = InspectorObject::create();
+        auto end = resultObject->end();
+        for (auto it = resultObject->begin(); it != end; ++it)
+            returnValueObject->setValue(it->key, handleScriptResult(WTFMove(it->value)));
+        return returnValueObject;
+    }
+
+    return resultValue;
+}
+
+void Session::executeScript(const String& script, RefPtr<InspectorArray>&& argumentsArray, ExecuteScriptMode mode, Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (!m_toplevelBrowsingContext) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::NoSuchWindow));
+        return;
+    }
+
+    RefPtr<InspectorArray> arguments = InspectorArray::create();
+    unsigned argumentsLength = argumentsArray->length();
+    for (unsigned i = 0; i < argumentsLength; ++i) {
+        if (auto argument = argumentsArray->get(i)) {
+            if (auto element = extractElement(*argument))
+                arguments->pushString(element->toJSONString());
+            else
+                arguments->pushString(argument->toJSONString());
+        }
+    }
+
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("browsingContextHandle"), m_toplevelBrowsingContext.value());
+    if (m_browsingContext)
+        parameters->setString(ASCIILiteral("frameHandle"), m_browsingContext.value());
+    parameters->setString(ASCIILiteral("function"), "function(){" + script + '}');
+    parameters->setArray(ASCIILiteral("arguments"), WTFMove(arguments));
+    if (mode == ExecuteScriptMode::Async) {
+        parameters->setBoolean(ASCIILiteral("expectsImplicitCallbackArgument"), true);
+        if (m_timeouts.script)
+            parameters->setInteger(ASCIILiteral("callbackTimeout"), m_timeouts.script.value().millisecondsAs<int>());
+    }
+    m_host->sendCommandToBackend(ASCIILiteral("evaluateJavaScriptFunction"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError || !response.responseObject) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        String valueString;
+        if (!response.responseObject->getString(ASCIILiteral("result"), valueString)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        if (valueString.isEmpty()) {
+            completionHandler(CommandResult::success());
+            return;
+        }
+        RefPtr<InspectorValue> resultValue;
+        if (!InspectorValue::parseJSON(valueString, resultValue)) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError));
+            return;
+        }
+        completionHandler(CommandResult::success(handleScriptResult(WTFMove(resultValue))));
+    });
+}
+
+void Session::performMouseInteraction(int x, int y, MouseButton button, MouseInteraction interaction, Function<void (CommandResult&&)>&& completionHandler)
+{
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("handle"), m_toplevelBrowsingContext.value());
+    RefPtr<InspectorObject> position = InspectorObject::create();
+    position->setInteger(ASCIILiteral("x"), x);
+    position->setInteger(ASCIILiteral("y"), y);
+    parameters->setObject(ASCIILiteral("position"), WTFMove(position));
+    switch (button) {
+    case MouseButton::None:
+        parameters->setString(ASCIILiteral("button"), ASCIILiteral("None"));
+        break;
+    case MouseButton::Left:
+        parameters->setString(ASCIILiteral("button"), ASCIILiteral("Left"));
+        break;
+    case MouseButton::Middle:
+        parameters->setString(ASCIILiteral("button"), ASCIILiteral("Middle"));
+        break;
+    case MouseButton::Right:
+        parameters->setString(ASCIILiteral("button"), ASCIILiteral("Right"));
+        break;
+    }
+    switch (interaction) {
+    case MouseInteraction::Move:
+        parameters->setString(ASCIILiteral("interaction"), ASCIILiteral("Move"));
+        break;
+    case MouseInteraction::Down:
+        parameters->setString(ASCIILiteral("interaction"), ASCIILiteral("Down"));
+        break;
+    case MouseInteraction::Up:
+        parameters->setString(ASCIILiteral("interaction"), ASCIILiteral("Up"));
+        break;
+    case MouseInteraction::SingleClick:
+        parameters->setString(ASCIILiteral("interaction"), ASCIILiteral("SingleClick"));
+        break;
+    case MouseInteraction::DoubleClick:
+        parameters->setString(ASCIILiteral("interaction"), ASCIILiteral("DoubleClick"));
+        break;
+    }
+    parameters->setArray(ASCIILiteral("modifiers"), InspectorArray::create());
+    m_host->sendCommandToBackend(ASCIILiteral("performMouseInteraction"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        completionHandler(CommandResult::success());
+    });
+}
+
+void Session::performKeyboardInteractions(Vector<KeyboardInteraction>&& interactions, Function<void (CommandResult&&)>&& completionHandler)
+{
+    RefPtr<InspectorObject> parameters = InspectorObject::create();
+    parameters->setString(ASCIILiteral("handle"), m_toplevelBrowsingContext.value());
+    RefPtr<InspectorArray> interactionsArray = InspectorArray::create();
+    for (const auto& interaction : interactions) {
+        RefPtr<InspectorObject> interactionObject = InspectorObject::create();
+        switch (interaction.type) {
+        case KeyboardInteractionType::KeyPress:
+            interactionObject->setString(ASCIILiteral("type"), ASCIILiteral("KeyPress"));
+            break;
+        case KeyboardInteractionType::KeyRelease:
+            interactionObject->setString(ASCIILiteral("type"), ASCIILiteral("KeyRelease"));
+            break;
+        case KeyboardInteractionType::InsertByKey:
+            interactionObject->setString(ASCIILiteral("type"), ASCIILiteral("InsertByKey"));
+            break;
+        }
+        if (interaction.key)
+            interactionObject->setString(ASCIILiteral("key"), interaction.key.value());
+        if (interaction.text)
+            interactionObject->setString(ASCIILiteral("text"), interaction.text.value());
+        interactionsArray->pushObject(WTFMove(interactionObject));
+    }
+    parameters->setArray(ASCIILiteral("interactions"), WTFMove(interactionsArray));
+    m_host->sendCommandToBackend(ASCIILiteral("performKeyboardInteractions"), WTFMove(parameters), [this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](SessionHost::CommandResponse&& response) {
+        if (response.isError) {
+            completionHandler(CommandResult::fail(WTFMove(response.responseObject)));
+            return;
+        }
+        completionHandler(CommandResult::success());
+    });
+}
+
+} // namespace WebDriver
diff --git a/Source/WebDriver/Session.h b/Source/WebDriver/Session.h
new file mode 100644 (file)
index 0000000..1b3c29b
--- /dev/null
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2017 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <wtf/Forward.h>
+#include <wtf/Function.h>
+#include <wtf/OptionSet.h>
+#include <wtf/RefCounted.h>
+#include <wtf/Seconds.h>
+#include <wtf/Vector.h>
+#include <wtf/text/WTFString.h>
+
+namespace Inspector {
+class InspectorArray;
+class InspectorObject;
+class InspectorValue;
+}
+
+namespace WebDriver {
+
+class Capabilities;
+class CommandResult;
+class SessionHost;
+
+class Session : public RefCounted<Session> {
+public:
+    static Ref<Session> create(std::unique_ptr<SessionHost>&& host)
+    {
+        return adoptRef(*new Session(WTFMove(host)));
+    }
+    ~Session();
+
+    const String& id() const { return m_id; }
+    const Capabilities& capabilities() const;
+
+    enum class FindElementsMode { Single, Multiple };
+    enum class ExecuteScriptMode { Sync, Async };
+    enum class Timeout { Script, PageLoad, Implicit };
+
+    struct Timeouts {
+        std::optional<Seconds> script;
+        std::optional<Seconds> pageLoad;
+        std::optional<Seconds> implicit;
+    };
+
+    void createTopLevelBrowsingContext(Function<void (CommandResult&&)>&&);
+    void close(Function<void (CommandResult&&)>&&);
+    void getTimeouts(Function<void (CommandResult&&)>&&);
+    void setTimeouts(const Timeouts&, Function<void (CommandResult&&)>&&);
+
+    void go(const String& url, Function<void (CommandResult&&)>&&);
+    void getCurrentURL(Function<void (CommandResult&&)>&&);
+    void back(Function<void (CommandResult&&)>&&);
+    void forward(Function<void (CommandResult&&)>&&);
+    void refresh(Function<void (CommandResult&&)>&&);
+    void getTitle(Function<void (CommandResult&&)>&&);
+    void getWindowHandle(Function<void (CommandResult&&)>&&);
+    void closeWindow(Function<void (CommandResult&&)>&&);
+    void switchToWindow(const String& windowHandle, Function<void (CommandResult&&)>&&);
+    void getWindowHandles(Function<void (CommandResult&&)>&&);
+    void switchToFrame(RefPtr<Inspector::InspectorValue>&&, Function<void (CommandResult&&)>&&);
+    void switchToParentFrame(Function<void (CommandResult&&)>&&);
+    void getWindowPosition(Function<void (CommandResult&&)>&&);
+    void setWindowPosition(int windowX, int windowY, Function<void (CommandResult&&)>&&);
+    void getWindowSize(Function<void (CommandResult&&)>&&);
+    void setWindowSize(int windowWidth, int windowHeight, Function<void (CommandResult&&)>&&);
+    void findElements(const String& strategy, const String& selector, FindElementsMode, const String& rootElementID, Function<void (CommandResult&&)>&&);
+    void isElementSelected(const String& elementID, Function<void (CommandResult&&)>&&);
+    void getElementText(const String& elementID, Function<void (CommandResult&&)>&&);
+    void getElementTagName(const String& elementID, Function<void (CommandResult&&)>&&);
+    void getElementRect(const String& elementID, Function<void (CommandResult&&)>&&);
+    void isElementEnabled(const String& elementID, Function<void (CommandResult&&)>&&);
+    void isElementDisplayed(const String& elementID, Function<void (CommandResult&&)>&&);
+    void getElementAttribute(const String& elementID, const String& attribute, Function<void (CommandResult&&)>&&);
+    void elementClick(const String& elementID, Function<void (CommandResult&&)>&&);
+    void elementClear(const String& elementID, Function<void (CommandResult&&)>&&);
+    void elementSendKeys(const String& elementID, Vector<String>&& keys, Function<void (CommandResult&&)>&&);
+    void elementSubmit(const String& elementID, Function<void (CommandResult&&)>&&);
+    void executeScript(const String& script, RefPtr<Inspector::InspectorArray>&& arguments, ExecuteScriptMode, Function<void (CommandResult&&)>&&);
+
+private:
+    Session(std::unique_ptr<SessionHost>&&);
+
+    RefPtr<Inspector::InspectorObject> createElement(RefPtr<Inspector::InspectorValue>&&);
+    RefPtr<Inspector::InspectorObject> createElement(const String& elementID);
+    RefPtr<Inspector::InspectorObject> extractElement(Inspector::InspectorValue&);
+    String extractElementID(Inspector::InspectorValue&);
+    RefPtr<Inspector::InspectorValue> handleScriptResult(RefPtr<Inspector::InspectorValue>&&);
+
+    struct Point {
+        int x { 0 };
+        int y { 0 };
+    };
+
+    struct Size {
+        int width { 0 };
+        int height { 0 };
+    };
+
+    struct Rect {
+        Point origin;
+        Size size;
+    };
+
+    enum class ElementLayoutOption {
+        ScrollIntoViewIfNeeded = 1 << 0,
+        UseViewportCoordinates = 1 << 1,
+    };
+    void computeElementLayout(const String& elementID, OptionSet<ElementLayoutOption>, Function<void (std::optional<Rect>&&, RefPtr<Inspector::InspectorObject>&&)>&&);
+
+    enum class MouseButton { None, Left, Middle, Right };
+    enum class MouseInteraction { Move, Down, Up, SingleClick, DoubleClick };
+    void performMouseInteraction(int x, int y, MouseButton, MouseInteraction, Function<void (CommandResult&&)>&&);
+
+    enum class KeyboardInteractionType { KeyPress, KeyRelease, InsertByKey };
+    struct KeyboardInteraction {
+        KeyboardInteractionType type { KeyboardInteractionType::InsertByKey };
+        std::optional<String> text;
+        std::optional<String> key;
+    };
+    enum KeyModifier {
+        None = 0,
+        Shift = 1 << 0,
+        Control = 1 << 1,
+        Alternate = 1 << 2,
+        Meta = 1 << 3,
+    };
+    String virtualKeyForKeySequence(const String& keySequence, KeyModifier&);
+    void performKeyboardInteractions(Vector<KeyboardInteraction>&&, Function<void (CommandResult&&)>&&);
+
+    std::unique_ptr<SessionHost> m_host;
+    Timeouts m_timeouts;
+    String m_id;
+    std::optional<String> m_toplevelBrowsingContext;
+    std::optional<String> m_browsingContext;
+};
+
+} // WebDriver
diff --git a/Source/WebDriver/SessionHost.cpp b/Source/WebDriver/SessionHost.cpp
new file mode 100644 (file)
index 0000000..12478b9
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2017 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+#include "SessionHost.h"
+
+#include <inspector/InspectorValues.h>
+#include <wtf/text/StringBuilder.h>
+
+using namespace Inspector;
+
+namespace WebDriver {
+
+void SessionHost::inspectorDisconnected()
+{
+    if (!m_closeMessageID)
+        return;
+
+    // Closing the browsing context made the browser quit.
+    auto responseHandler = m_commandRequests.take(m_closeMessageID);
+    m_closeMessageID = 0;
+    responseHandler({ });
+}
+
+long SessionHost::sendCommandToBackend(const String& command, RefPtr<InspectorObject>&& parameters, Function<void (CommandResponse&&)>&& responseHandler)
+{
+    static long lastSequenceID = 0;
+    long sequenceID = ++lastSequenceID;
+    m_commandRequests.add(sequenceID, WTFMove(responseHandler));
+    if (command == "closeBrowsingContext")
+        m_closeMessageID = sequenceID;
+    StringBuilder messageBuilder;
+    messageBuilder.appendLiteral("{\"id\":");
+    messageBuilder.appendNumber(sequenceID);
+    messageBuilder.appendLiteral(",\"method\":\"Automation.");
+    messageBuilder.append(command);
+    messageBuilder.append('"');
+    if (parameters) {
+        messageBuilder.appendLiteral(",\"params\":");
+        messageBuilder.append(parameters->toJSONString());
+    }
+    messageBuilder.append('}');
+    sendMessageToBackend(messageBuilder.toString());
+
+    return sequenceID;
+}
+
+void SessionHost::dispatchMessage(const String& message)
+{
+    RefPtr<InspectorValue> messageValue;
+    if (!InspectorValue::parseJSON(message, messageValue))
+        return;
+
+    RefPtr<InspectorObject> messageObject;
+    if (!messageValue->asObject(messageObject))
+        return;
+
+    long sequenceID;
+    if (!messageObject->getInteger(ASCIILiteral("id"), sequenceID))
+        return;
+
+    if (m_closeMessageID == sequenceID)
+        m_closeMessageID = 0;
+
+    auto responseHandler = m_commandRequests.take(sequenceID);
+    ASSERT(responseHandler);
+
+    CommandResponse response;
+    RefPtr<InspectorObject> errorObject;
+    if (messageObject->getObject(ASCIILiteral("error"), errorObject)) {
+        response.responseObject = WTFMove(errorObject);
+        response.isError = true;
+    } else {
+        RefPtr<InspectorObject> resultObject;
+        if (messageObject->getObject(ASCIILiteral("result"), resultObject) && resultObject->size())
+            response.responseObject = WTFMove(resultObject);
+    }
+
+    responseHandler(WTFMove(response));
+}
+
+} // namespace WebDriver
diff --git a/Source/WebDriver/SessionHost.h b/Source/WebDriver/SessionHost.h
new file mode 100644 (file)
index 0000000..6a79c7e
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2017 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Capabilities.h"
+#include <wtf/HashMap.h>
+
+#if USE(GLIB)
+#include <wtf/glib/GRefPtr.h>
+typedef struct _GDBusConnection GDBusConnection;
+typedef struct _GDBusInterfaceVTable GDBusInterfaceVTable;
+typedef struct _GSubprocess GSubprocess;
+#endif
+
+namespace Inspector {
+class InspectorObject;
+}
+
+namespace WebDriver {
+
+struct ConnectToBrowserAsyncData;
+
+class SessionHost {
+    WTF_MAKE_FAST_ALLOCATED(SessionHost);
+public:
+    explicit SessionHost(Capabilities&& capabilities)
+        : m_capabilities(WTFMove(capabilities))
+    {
+    }
+    ~SessionHost();
+
+    const Capabilities& capabilities() const { return m_capabilities; }
+
+    enum class Succeeded { No, Yes };
+    void connectToBrowser(Function<void (Succeeded)>&&);
+    void startAutomationSession(const String& sessionID, Function<void ()>&&);
+
+    struct CommandResponse {
+        RefPtr<Inspector::InspectorObject> responseObject;
+        bool isError { false };
+    };
+    long sendCommandToBackend(const String&, RefPtr<Inspector::InspectorObject>&& parameters, Function<void (CommandResponse&&)>&&);
+
+private:
+    struct Target {
+        uint64_t id { 0 };
+        CString name;
+        bool paired { false };
+    };
+
+    void inspectorDisconnected();
+    void sendMessageToBackend(const String&);
+    void dispatchMessage(const String&);
+
+#if USE(GLIB)
+    static void dbusConnectionClosedCallback(SessionHost*);
+    static const GDBusInterfaceVTable s_interfaceVTable;
+    void launchBrowser(Function<void (Succeeded)>&&);
+    void connectToBrowser(std::unique_ptr<ConnectToBrowserAsyncData>&&);
+    void setupConnection(GRefPtr<GDBusConnection>&&, Function<void (Succeeded)>&&);
+    void setTargetList(uint64_t connectionID, Vector<Target>&&);
+    void sendMessageToFrontend(uint64_t connectionID, uint64_t targetID, const char* message);
+#endif
+
+    Capabilities m_capabilities;
+
+    uint64_t m_connectionID { 0 };
+    Target m_target;
+
+    HashMap<long, Function<void (CommandResponse&&)>> m_commandRequests;
+    long m_closeMessageID { 0 };
+
+#if USE(GLIB)
+    Function<void ()> m_startSessionCompletionHandler;
+    GRefPtr<GSubprocess> m_browser;
+    GRefPtr<GDBusConnection> m_dbusConnection;
+    GRefPtr<GCancellable> m_cancellable;
+#endif
+};
+
+} // namespace WebDriver
diff --git a/Source/WebDriver/WebDriverMain.cpp b/Source/WebDriver/WebDriverMain.cpp
new file mode 100644 (file)
index 0000000..525eaec
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+
+#include "WebDriverService.h"
+#include <wtf/MainThread.h>
+#include <wtf/Threading.h>
+
+int main(int argc, char** argv)
+{
+    WTF::initializeMainThread();
+
+    WebDriver::WebDriverService service;
+    return service.run(argc, argv);
+}
diff --git a/Source/WebDriver/WebDriverService.cpp b/Source/WebDriver/WebDriverService.cpp
new file mode 100644 (file)
index 0000000..f7f1f77
--- /dev/null
@@ -0,0 +1,906 @@
+/*
+ * Copyright (C) 2017 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+#include "WebDriverService.h"
+
+#include "Capabilities.h"
+#include "CommandResult.h"
+#include "SessionHost.h"
+#include <inspector/InspectorValues.h>
+#include <wtf/RunLoop.h>
+#include <wtf/text/WTFString.h>
+
+using namespace Inspector;
+
+namespace WebDriver {
+
+WebDriverService::WebDriverService()
+    : m_server(*this)
+{
+}
+
+static void printUsageStatement(const char* programName)
+{
+    printf("Usage: %s options\n", programName);
+    printf("  -h, --help                Prints this help message\n");
+    printf("  -p <port>, --port=<port>  Port number the driver will use\n");
+    printf("\n");
+}
+
+int WebDriverService::run(int argc, char** argv)
+{
+    String portString;
+    for (unsigned i = 1 ; i < argc; ++i) {
+        const char* arg = argv[i];
+        if (!strcmp(arg, "-h") || !strcmp(arg, "--help")) {
+            printUsageStatement(argv[0]);
+            return EXIT_SUCCESS;
+        }
+
+        if (!strcmp(arg, "-p") && portString.isNull()) {
+            if (++i == argc) {
+                printUsageStatement(argv[0]);
+                return EXIT_FAILURE;
+            }
+            portString = argv[i];
+            continue;
+        }
+
+        static const unsigned portStrLength = strlen("--port=");
+        if (!strncmp(arg, "--port=", portStrLength) && portString.isNull()) {
+            portString = String(arg + portStrLength);
+            continue;
+        }
+    }
+
+    if (portString.isNull()) {
+        printUsageStatement(argv[0]);
+        return EXIT_FAILURE;
+    }
+
+    bool ok;
+    unsigned port = portString.toUInt(&ok);
+    if (!ok) {
+        fprintf(stderr, "Invalid port %s provided\n", portString.ascii().data());
+        return EXIT_FAILURE;
+    }
+
+    RunLoop::initializeMainRunLoop();
+
+    if (!m_server.listen(port))
+        return EXIT_FAILURE;
+
+    RunLoop::run();
+
+    return EXIT_SUCCESS;
+}
+
+void WebDriverService::quit()
+{
+    m_server.disconnect();
+    RunLoop::main().stop();
+}
+
+const WebDriverService::Command WebDriverService::s_commands[] = {
+    { HTTPMethod::Post, "/session", &WebDriverService::newSession },
+    { HTTPMethod::Delete, "/session/$sessionId", &WebDriverService::deleteSession },
+    { HTTPMethod::Post, "/session/$sessionId/timeouts", &WebDriverService::setTimeouts },
+
+    { HTTPMethod::Post, "/session/$sessionId/url", &WebDriverService::go },
+    { HTTPMethod::Get, "/session/$sessionId/url", &WebDriverService::getCurrentURL },
+    { HTTPMethod::Post, "/session/$sessionId/back", &WebDriverService::back },
+    { HTTPMethod::Post, "/session/$sessionId/forward", &WebDriverService::forward },
+    { HTTPMethod::Post, "/session/$sessionId/refresh", &WebDriverService::refresh },
+    { HTTPMethod::Get, "/session/$sessionId/title", &WebDriverService::getTitle },
+
+    { HTTPMethod::Get, "/session/$sessionId/window", &WebDriverService::getWindowHandle },
+    { HTTPMethod::Delete, "/session/$sessionId/window", &WebDriverService::closeWindow },
+    { HTTPMethod::Post, "/session/$sessionId/window", &WebDriverService::switchToWindow },
+    { HTTPMethod::Get, "/session/$sessionId/window/handles", &WebDriverService::getWindowHandles },
+    { HTTPMethod::Post, "/session/$sessionId/frame", &WebDriverService::switchToFrame },
+    { HTTPMethod::Post, "/session/$sessionId/frame/parent", &WebDriverService::switchToParentFrame },
+
+    // FIXME: Not in the spec, but still used by Selenium.
+    { HTTPMethod::Get, "/session/$sessionId/window/position", &WebDriverService::getWindowPosition },
+    { HTTPMethod::Post, "/session/$sessionId/window/position", &WebDriverService::setWindowPosition },
+    { HTTPMethod::Get, "/session/$sessionId/window/size", &WebDriverService::getWindowSize },
+    { HTTPMethod::Post, "/session/$sessionId/window/size", &WebDriverService::setWindowSize },
+
+    { HTTPMethod::Post, "/session/$sessionId/element", &WebDriverService::findElement },
+    { HTTPMethod::Post, "/session/$sessionId/elements", &WebDriverService::findElements },
+    { HTTPMethod::Post, "/session/$sessionId/element/$elementId/element", &WebDriverService::findElementFromElement },
+    { HTTPMethod::Post, "/session/$sessionId/element/$elementId/elements", &WebDriverService::findElementsFromElement },
+
+    { HTTPMethod::Get, "/session/$sessionId/element/$elementId/selected", &WebDriverService::isElementSelected },
+    { HTTPMethod::Get, "/session/$sessionId/element/$elementId/attribute/$name", &WebDriverService::getElementAttribute },
+    { HTTPMethod::Get, "/session/$sessionId/element/$elementId/text", &WebDriverService::getElementText },
+    { HTTPMethod::Get, "/session/$sessionId/element/$elementId/name", &WebDriverService::getElementTagName },
+    { HTTPMethod::Get, "/session/$sessionId/element/$elementId/rect", &WebDriverService::getElementRect },
+    { HTTPMethod::Get, "/session/$sessionId/element/$elementId/enabled", &WebDriverService::isElementEnabled },
+
+    { HTTPMethod::Post, "/session/$sessionId/element/$elementId/click", &WebDriverService::elementClick },
+    { HTTPMethod::Post, "/session/$sessionId/element/$elementId/clear", &WebDriverService::elementClear },
+    { HTTPMethod::Post, "/session/$sessionId/element/$elementId/value", &WebDriverService::elementSendKeys },
+
+    // FIXME: Not in the spec, but still used by Selenium.
+    { HTTPMethod::Post, "/session/$sessionId/element/$elementId/submit", &WebDriverService::elementSubmit },
+
+    { HTTPMethod::Post, "/session/$sessionId/execute/sync", &WebDriverService::executeScript },
+    { HTTPMethod::Post, "/session/$sessionId/execute/async", &WebDriverService::executeAsyncScript },
+
+    { HTTPMethod::Get, "/session/$sessionId/element/$elementId/displayed", &WebDriverService::isElementDisplayed },
+};
+
+std::optional<WebDriverService::HTTPMethod> WebDriverService::toCommandHTTPMethod(const String& method)
+{
+    auto lowerCaseMethod = method.convertToASCIILowercase();
+    if (lowerCaseMethod == "get")
+        return WebDriverService::HTTPMethod::Get;
+    if (lowerCaseMethod == "post" || lowerCaseMethod == "put")
+        return WebDriverService::HTTPMethod::Post;
+    if (lowerCaseMethod == "delete")
+        return WebDriverService::HTTPMethod::Delete;
+
+    return std::nullopt;
+}
+
+bool WebDriverService::findCommand(const String& method, const String& path, CommandHandler* handler, HashMap<String, String>& parameters)
+{
+    auto commandMethod = toCommandHTTPMethod(method);
+    if (!commandMethod)
+        return false;
+
+    size_t length = WTF_ARRAY_LENGTH(s_commands);
+    for (size_t i = 0; i < length; ++i) {
+        if (s_commands[i].method != *commandMethod)
+            continue;
+
+        Vector<String> pathTokens;
+        path.split("/", pathTokens);
+        Vector<String> commandTokens;
+        String::fromUTF8(s_commands[i].uriTemplate).split("/", commandTokens);
+        if (pathTokens.size() != commandTokens.size())
+            continue;
+
+        bool allMatched = true;
+        for (size_t j = 0; j < pathTokens.size() && allMatched; ++j) {
+            if (commandTokens[j][0] == '$')
+                parameters.set(commandTokens[j].substring(1), pathTokens[j]);
+            else if (commandTokens[j] != pathTokens[j])
+                allMatched = false;
+        }
+
+        if (allMatched) {
+            *handler = s_commands[i].handler;
+            return true;
+        }
+
+        parameters.clear();
+    }
+
+    return false;
+}
+
+void WebDriverService::handleRequest(HTTPRequestHandler::Request&& request, Function<void (HTTPRequestHandler::Response&&)>&& replyHandler)
+{
+    CommandHandler handler;
+    HashMap<String, String> parameters;
+    if (!findCommand(request.method, request.path, &handler, parameters)) {
+        sendResponse(WTFMove(replyHandler), CommandResult::fail(CommandResult::ErrorCode::UnknownCommand, String("Unknown command: " + request.path)));
+        return;
+    }
+
+    RefPtr<InspectorObject> parametersObject;
+    if (request.dataLength) {
+        RefPtr<InspectorValue> messageValue;
+        if (!InspectorValue::parseJSON(String::fromUTF8(request.data, request.dataLength), messageValue)) {
+            sendResponse(WTFMove(replyHandler), CommandResult::fail(CommandResult::ErrorCode::InvalidArgument));
+            return;
+        }
+
+        if (!messageValue->asObject(parametersObject)) {
+            sendResponse(WTFMove(replyHandler), CommandResult::fail(CommandResult::ErrorCode::InvalidArgument));
+            return;
+        }
+    } else
+        parametersObject = InspectorObject::create();
+    for (const auto& parameter : parameters)
+        parametersObject->setString(parameter.key, parameter.value);
+
+    ((*this).*handler)(WTFMove(parametersObject), [this, replyHandler = WTFMove(replyHandler)](CommandResult&& result) mutable {
+        sendResponse(WTFMove(replyHandler), WTFMove(result));
+    });
+}
+
+void WebDriverService::sendResponse(Function<void (HTTPRequestHandler::Response&&)>&& replyHandler, CommandResult&& result) const
+{
+    RefPtr<InspectorObject> responseObject;
+    if (result.isError()) {
+        responseObject = InspectorObject::create();
+        responseObject->setString(ASCIILiteral("error"), result.errorString());
+        responseObject->setString(ASCIILiteral("message"), result.errorMessage().value_or(emptyString()));
+        responseObject->setString(ASCIILiteral("stacktrace"), emptyString());
+    } else {
+        responseObject = InspectorObject::create();
+        auto resultValue = result.result();
+        responseObject->setValue(ASCIILiteral("value"), resultValue ? WTFMove(resultValue) : InspectorValue::null());
+    }
+    replyHandler({ result.httpStatusCode(), responseObject->toJSONString().utf8(), ASCIILiteral("application/json; charset=utf-8") });
+}
+
+bool WebDriverService::parseCapabilities(InspectorObject& desiredCapabilities, Capabilities& capabilities, Function<void (CommandResult&&)>& completionHandler)
+{
+    RefPtr<InspectorValue> value;
+    if (desiredCapabilities.getValue(ASCIILiteral("browserName"), value) && !value->asString(capabilities.browserName)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::SessionNotCreated, String("browserName parameter is invalid in capabilities")));
+        return false;
+    }
+    if (desiredCapabilities.getValue(ASCIILiteral("version"), value) && !value->asString(capabilities.browserVersion)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::SessionNotCreated, String("version parameter is invalid in capabilities")));
+        return false;
+    }
+    if (desiredCapabilities.getValue(ASCIILiteral("platform"), value) && !value->asString(capabilities.platform)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::SessionNotCreated, String("platform parameter is invalid in capabilities")));
+        return false;
+    }
+    // FIXME: parse all other well-known capabilities: acceptInsecureCerts, pageLoadStrategy, proxy, setWindowRect, timeouts, unhandledPromptBehavior.
+    return platformParseCapabilities(desiredCapabilities, capabilities, completionHandler);
+}
+
+RefPtr<Session> WebDriverService::findSessionOrCompleteWithError(InspectorObject& parameters, Function<void (CommandResult&&)>& completionHandler)
+{
+    String sessionID;
+    if (!parameters.getString(ASCIILiteral("sessionId"), sessionID)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument));
+        return nullptr;
+    }
+
+    auto session = m_sessions.get(sessionID);
+    if (!session) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidSessionID));
+        return nullptr;
+    }
+
+    return session;
+}
+
+void WebDriverService::newSession(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §8.1 New Session.
+    // https://www.w3.org/TR/webdriver/#new-session
+    RefPtr<InspectorObject> capabilitiesObject;
+    if (!parameters->getObject(ASCIILiteral("capabilities"), capabilitiesObject)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::SessionNotCreated));
+        return;
+    }
+    RefPtr<InspectorValue> requiredCapabilitiesValue;
+    RefPtr<InspectorObject> requiredCapabilities;
+    if (!capabilitiesObject->getValue(ASCIILiteral("alwaysMatch"), requiredCapabilitiesValue))
+        requiredCapabilities = InspectorObject::create();
+    else if (!requiredCapabilitiesValue->asObject(requiredCapabilities)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::SessionNotCreated, String("alwaysMatch is invalid in capabilities")));
+        return;
+    }
+    // FIXME: process firstMatch capabilities.
+
+    Capabilities capabilities;
+    if (!parseCapabilities(*requiredCapabilities, capabilities, completionHandler))
+        return;
+
+    auto sessionHost = std::make_unique<SessionHost>(WTFMove(capabilities));
+    auto* sessionHostPtr = sessionHost.get();
+    sessionHostPtr->connectToBrowser([this, sessionHost = WTFMove(sessionHost), completionHandler = WTFMove(completionHandler)](SessionHost::Succeeded succeeded) mutable {
+        if (succeeded == SessionHost::Succeeded::No) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::SessionNotCreated, String("Failed to connect to browser")));
+            return;
+        }
+
+        RefPtr<Session> session = Session::create(WTFMove(sessionHost));
+        session->createTopLevelBrowsingContext([this, session, completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable {
+            if (result.isError()) {
+                completionHandler(CommandResult::fail(CommandResult::ErrorCode::SessionNotCreated, result.errorString()));
+                return;
+            }
+
+            m_activeSession = session.get();
+            m_sessions.add(session->id(), session);
+            RefPtr<InspectorObject> resultObject = InspectorObject::create();
+            resultObject->setString(ASCIILiteral("sessionId"), session->id());
+            RefPtr<InspectorObject> capabilities = InspectorObject::create();
+            capabilities->setString(ASCIILiteral("browserName"), session->capabilities().browserName);
+            capabilities->setString(ASCIILiteral("version"), session->capabilities().browserVersion);
+            capabilities->setString(ASCIILiteral("platform"), session->capabilities().platform);
+            resultObject->setObject(ASCIILiteral("value"), WTFMove(capabilities));
+            completionHandler(CommandResult::success(WTFMove(resultObject)));
+        });
+    });
+}
+
+void WebDriverService::deleteSession(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §8.2 Delete Session.
+    // https://www.w3.org/TR/webdriver/#delete-session
+    String sessionID;
+    if (!parameters->getString(ASCIILiteral("sessionId"), sessionID)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument));
+        return;
+    }
+
+    auto session = m_sessions.take(sessionID);
+    if (!session) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidSessionID));
+        return;
+    }
+
+    if (m_activeSession == session.get())
+        m_activeSession = nullptr;
+
+    session->close(WTFMove(completionHandler));
+}
+
+void WebDriverService::setTimeouts(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §8.5 Set Timeouts.
+    // https://www.w3.org/TR/webdriver/#set-timeouts
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    Session::Timeouts timeouts;
+    auto end = parameters->end();
+    for (auto it = parameters->begin(); it != end; ++it) {
+        if (it->key == "sessionId")
+            continue;
+
+        int timeoutMS;
+        if (!it->value->asInteger(timeoutMS) || timeoutMS < 0 || timeoutMS > INT_MAX) {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument));
+            return;
+        }
+
+        if (it->key == "script")
+            timeouts.script = Seconds::fromMilliseconds(timeoutMS);
+        else if (it->key == "page load")
+            timeouts.pageLoad = Seconds::fromMilliseconds(timeoutMS);
+        else if (it->key == "implicit")
+            timeouts.implicit = Seconds::fromMilliseconds(timeoutMS);
+        else {
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument));
+            return;
+        }
+    }
+
+    session->setTimeouts(timeouts, WTFMove(completionHandler));
+}
+
+void WebDriverService::go(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §9.1 Go.
+    // https://www.w3.org/TR/webdriver/#go
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    String url;
+    if (!parameters->getString(ASCIILiteral("url"), url)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument));
+        return;
+    }
+
+    session->go(url, WTFMove(completionHandler));
+}
+
+void WebDriverService::getCurrentURL(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §9.2 Get Current URL.
+    // https://www.w3.org/TR/webdriver/#get-current-url
+    if (auto session = findSessionOrCompleteWithError(*parameters, completionHandler))
+        session->getCurrentURL(WTFMove(completionHandler));
+}
+
+void WebDriverService::back(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §9.3 Back.
+    // https://www.w3.org/TR/webdriver/#back
+    if (auto session = findSessionOrCompleteWithError(*parameters, completionHandler))
+        session->back(WTFMove(completionHandler));
+}
+
+void WebDriverService::forward(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §9.4 Forward.
+    // https://www.w3.org/TR/webdriver/#forward
+    if (auto session = findSessionOrCompleteWithError(*parameters, completionHandler))
+        session->forward(WTFMove(completionHandler));
+}
+
+void WebDriverService::refresh(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §9.5 Refresh.
+    // https://www.w3.org/TR/webdriver/#refresh
+    if (auto session = findSessionOrCompleteWithError(*parameters, completionHandler))
+        session->refresh(WTFMove(completionHandler));
+}
+
+void WebDriverService::getTitle(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §9.6 Get Title.
+    // https://www.w3.org/TR/webdriver/#get-title
+    if (auto session = findSessionOrCompleteWithError(*parameters, completionHandler))
+        session->getTitle(WTFMove(completionHandler));
+}
+
+void WebDriverService::getWindowHandle(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §10.1 Get Window Handle.
+    // https://www.w3.org/TR/webdriver/#get-window-handle
+    if (auto session = findSessionOrCompleteWithError(*parameters, completionHandler))
+        session->getWindowHandle(WTFMove(completionHandler));
+}
+
+void WebDriverService::getWindowPosition(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (auto session = findSessionOrCompleteWithError(*parameters, completionHandler))
+        session->getWindowPosition(WTFMove(completionHandler));
+}
+
+void WebDriverService::setWindowPosition(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    int windowX;
+    if (!parameters->getInteger(ASCIILiteral("x"), windowX)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument));
+        return;
+    }
+
+    int windowY;
+    if (!parameters->getInteger(ASCIILiteral("y"), windowY)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument));
+        return;
+    }
+
+    session->setWindowPosition(windowX, windowY, WTFMove(completionHandler));
+}
+
+void WebDriverService::getWindowSize(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    if (auto session = findSessionOrCompleteWithError(*parameters, completionHandler))
+        session->getWindowSize(WTFMove(completionHandler));
+}
+
+void WebDriverService::setWindowSize(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    int windowWidth;
+    if (!parameters->getInteger(ASCIILiteral("width"), windowWidth)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument));
+        return;
+    }
+
+    int windowHeight;
+    if (!parameters->getInteger(ASCIILiteral("height"), windowHeight)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument));
+        return;
+    }
+
+    session->setWindowSize(windowWidth, windowHeight, WTFMove(completionHandler));
+}
+
+void WebDriverService::closeWindow(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §10.2 Close Window.
+    // https://www.w3.org/TR/webdriver/#close-window
+    if (auto session = findSessionOrCompleteWithError(*parameters, completionHandler))
+        session->closeWindow(WTFMove(completionHandler));
+}
+
+void WebDriverService::switchToWindow(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §10.3 Switch To Window.
+    // https://www.w3.org/TR/webdriver/#switch-to-window
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    String handle;
+    if (!parameters->getString(ASCIILiteral("handle"), handle)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument));
+        return;
+    }
+
+    session->switchToWindow(handle, WTFMove(completionHandler));
+}
+
+void WebDriverService::getWindowHandles(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §10.4 Get Window Handles.
+    // https://www.w3.org/TR/webdriver/#get-window-handles
+    if (auto session = findSessionOrCompleteWithError(*parameters, completionHandler))
+        session->getWindowHandles(WTFMove(completionHandler));
+}
+
+void WebDriverService::switchToFrame(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §10.5 Switch To Frame.
+    // https://www.w3.org/TR/webdriver/#switch-to-frame
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    RefPtr<InspectorValue> frameID;
+    if (!parameters->getValue(ASCIILiteral("id"), frameID)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument));
+        return;
+    }
+
+    session->switchToFrame(WTFMove(frameID), WTFMove(completionHandler));
+}
+
+void WebDriverService::switchToParentFrame(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §10.6 Switch To Parent Frame.
+    // https://www.w3.org/TR/webdriver/#switch-to-parent-frame
+    if (auto session = findSessionOrCompleteWithError(*parameters, completionHandler))
+        session->switchToParentFrame(WTFMove(completionHandler));
+}
+
+static std::optional<String> findElementOrCompleteWithError(InspectorObject& parameters, Function<void (CommandResult&&)>& completionHandler)
+{
+    String elementID;
+    if (!parameters.getString(ASCIILiteral("elementId"), elementID) || elementID.isEmpty()) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument));
+        return std::nullopt;
+    }
+    return elementID;
+}
+
+static bool findStrategyAndSelectorOrCompleteWithError(InspectorObject& parameters, Function<void (CommandResult&&)>& completionHandler, String& strategy, String& selector)
+{
+    if (!parameters.getString(ASCIILiteral("using"), strategy)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument));
+        return false;
+    }
+    if (!parameters.getString(ASCIILiteral("value"), selector)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument));
+        return false;
+    }
+    return true;
+}
+
+void WebDriverService::findElement(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §12.2 Find Element.
+    // https://www.w3.org/TR/webdriver/#find-element
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    String strategy, selector;
+    if (!findStrategyAndSelectorOrCompleteWithError(*parameters, completionHandler, strategy, selector))
+        return;
+
+    session->findElements(strategy, selector, Session::FindElementsMode::Single, emptyString(), WTFMove(completionHandler));
+}
+
+void WebDriverService::findElements(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §12.3 Find Elements.
+    // https://www.w3.org/TR/webdriver/#find-elements
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    String strategy, selector;
+    if (!findStrategyAndSelectorOrCompleteWithError(*parameters, completionHandler, strategy, selector))
+        return;
+
+    session->findElements(strategy, selector, Session::FindElementsMode::Multiple, emptyString(), WTFMove(completionHandler));
+}
+
+void WebDriverService::findElementFromElement(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §12.4 Find Element From Element.
+    // https://www.w3.org/TR/webdriver/#find-element-from-element
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    auto elementID = findElementOrCompleteWithError(*parameters, completionHandler);
+    if (!elementID)
+        return;
+
+    String strategy, selector;
+    if (!findStrategyAndSelectorOrCompleteWithError(*parameters, completionHandler, strategy, selector))
+        return;
+
+    session->findElements(strategy, selector, Session::FindElementsMode::Single, elementID.value(), WTFMove(completionHandler));
+}
+
+void WebDriverService::findElementsFromElement(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §12.5 Find Elements From Element.
+    // https://www.w3.org/TR/webdriver/#find-elements-from-element
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    auto elementID = findElementOrCompleteWithError(*parameters, completionHandler);
+    if (!elementID)
+        return;
+
+    String strategy, selector;
+    if (!findStrategyAndSelectorOrCompleteWithError(*parameters, completionHandler, strategy, selector))
+        return;
+
+    session->findElements(strategy, selector, Session::FindElementsMode::Multiple, elementID.value(), WTFMove(completionHandler));
+}
+
+void WebDriverService::isElementSelected(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §13.1 Is Element Selected.
+    // https://www.w3.org/TR/webdriver/#is-element-selected
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    auto elementID = findElementOrCompleteWithError(*parameters, completionHandler);
+    if (!elementID)
+        return;
+
+    session->isElementSelected(elementID.value(), WTFMove(completionHandler));
+}
+
+void WebDriverService::getElementAttribute(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §13.2 Get Element Attribute.
+    // https://www.w3.org/TR/webdriver/#get-element-attribute
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    auto elementID = findElementOrCompleteWithError(*parameters, completionHandler);
+    if (!elementID)
+        return;
+
+    String attribute;
+    if (!parameters->getString(ASCIILiteral("name"), attribute)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument));
+        return;
+    }
+
+    session->getElementAttribute(elementID.value(), attribute, WTFMove(completionHandler));
+}
+
+void WebDriverService::getElementText(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §13.5 Get Element Text.
+    // https://www.w3.org/TR/webdriver/#get-element-text
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    auto elementID = findElementOrCompleteWithError(*parameters, completionHandler);
+    if (!elementID)
+        return;
+
+    session->getElementText(elementID.value(), WTFMove(completionHandler));
+}
+
+void WebDriverService::getElementTagName(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §13.6 Get Element Tag Name.
+    // https://www.w3.org/TR/webdriver/#get-element-tag-name
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    auto elementID = findElementOrCompleteWithError(*parameters, completionHandler);
+    if (!elementID)
+        return;
+
+    session->getElementTagName(elementID.value(), WTFMove(completionHandler));
+}
+
+void WebDriverService::getElementRect(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §13.7 Get Element Rect.
+    // https://www.w3.org/TR/webdriver/#get-element-rect
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    auto elementID = findElementOrCompleteWithError(*parameters, completionHandler);
+    if (!elementID)
+        return;
+
+    session->getElementRect(elementID.value(), WTFMove(completionHandler));
+}
+
+void WebDriverService::isElementEnabled(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §13.8 Is Element Enabled.
+    // https://www.w3.org/TR/webdriver/#is-element-enabled
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    auto elementID = findElementOrCompleteWithError(*parameters, completionHandler);
+    if (!elementID)
+        return;
+
+    session->isElementEnabled(elementID.value(), WTFMove(completionHandler));
+}
+
+void WebDriverService::isElementDisplayed(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §C. Element Displayedness.
+    // https://www.w3.org/TR/webdriver/#element-displayedness
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    auto elementID = findElementOrCompleteWithError(*parameters, completionHandler);
+    if (!elementID)
+        return;
+
+    session->isElementDisplayed(elementID.value(), WTFMove(completionHandler));
+}
+
+void WebDriverService::elementClick(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §14.1 Element Click.
+    // https://www.w3.org/TR/webdriver/#element-click
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    auto elementID = findElementOrCompleteWithError(*parameters, completionHandler);
+    if (!elementID)
+        return;
+
+    session->elementClick(elementID.value(), WTFMove(completionHandler));
+}
+
+void WebDriverService::elementClear(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §14.2 Element Clear.
+    // https://www.w3.org/TR/webdriver/#element-clear
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    auto elementID = findElementOrCompleteWithError(*parameters, completionHandler);
+    if (!elementID)
+        return;
+
+    session->elementClear(elementID.value(), WTFMove(completionHandler));
+}
+
+void WebDriverService::elementSendKeys(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §14.3 Element Send Keys.
+    // https://www.w3.org/TR/webdriver/#element-send-keys
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    auto elementID = findElementOrCompleteWithError(*parameters, completionHandler);
+    if (!elementID)
+        return;
+
+    RefPtr<InspectorArray> valueArray;
+    if (!parameters->getArray(ASCIILiteral("value"), valueArray)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument));
+        return;
+    }
+
+    unsigned valueArrayLength = valueArray->length();
+    if (!valueArrayLength) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument));
+        return;
+    }
+    Vector<String> value;
+    value.reserveInitialCapacity(valueArrayLength);
+    for (unsigned i = 0; i < valueArrayLength; ++i) {
+        if (auto keyValue = valueArray->get(i)) {
+            String key;
+            if (keyValue->asString(key))
+                value.uncheckedAppend(WTFMove(key));
+        }
+    }
+    if (!value.size()) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument));
+        return;
+    }
+
+    session->elementSendKeys(elementID.value(), WTFMove(value), WTFMove(completionHandler));
+}
+
+void WebDriverService::elementSubmit(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    auto elementID = findElementOrCompleteWithError(*parameters, completionHandler);
+    if (!elementID)
+        return;
+
+    session->elementSubmit(elementID.value(), WTFMove(completionHandler));
+}
+
+static bool findScriptAndArgumentsOrCompleteWithError(InspectorObject& parameters, Function<void (CommandResult&&)>& completionHandler, String& script, RefPtr<InspectorArray>& arguments)
+{
+    if (!parameters.getString(ASCIILiteral("script"), script)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument));
+        return false;
+    }
+    if (!parameters.getArray(ASCIILiteral("args"), arguments)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument));
+        return false;
+    }
+    return true;
+}
+
+void WebDriverService::executeScript(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §15.2.1 Execute Script.
+    // https://www.w3.org/TR/webdriver/#execute-script
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    String script;
+    RefPtr<InspectorArray> arguments;
+    if (!findScriptAndArgumentsOrCompleteWithError(*parameters, completionHandler, script, arguments))
+        return;
+
+    session->executeScript(script, WTFMove(arguments), Session::ExecuteScriptMode::Sync, WTFMove(completionHandler));
+}
+
+void WebDriverService::executeAsyncScript(RefPtr<InspectorObject>&& parameters, Function<void (CommandResult&&)>&& completionHandler)
+{
+    // §15.2.2 Execute Async Script.
+    // https://www.w3.org/TR/webdriver/#execute-async-script
+    auto session = findSessionOrCompleteWithError(*parameters, completionHandler);
+    if (!session)
+        return;
+
+    String script;
+    RefPtr<InspectorArray> arguments;
+    if (!findScriptAndArgumentsOrCompleteWithError(*parameters, completionHandler, script, arguments))
+        return;
+
+    session->executeScript(script, WTFMove(arguments), Session::ExecuteScriptMode::Async, WTFMove(completionHandler));
+}
+
+} // namespace WebDriver
diff --git a/Source/WebDriver/WebDriverService.h b/Source/WebDriver/WebDriverService.h
new file mode 100644 (file)
index 0000000..6b8b61d
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2017 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "HTTPServer.h"
+#include "Session.h"
+#include <wtf/Forward.h>
+#include <wtf/HashMap.h>
+#include <wtf/text/StringHash.h>
+
+namespace Inspector {
+class InspectorArray;
+class InspectorObject;
+}
+
+namespace WebDriver {
+
+class Capabilities;
+class CommandResult;
+class Session;
+
+class WebDriverService final : public HTTPRequestHandler {
+public:
+    WebDriverService();
+    ~WebDriverService() = default;
+
+    int run(int argc, char** argv);
+    void quit();
+
+private:
+    enum class HTTPMethod { Get, Post, Delete };
+    typedef void (WebDriverService::*CommandHandler)(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    struct Command {
+        HTTPMethod method;
+        const char* uriTemplate;
+        CommandHandler handler;
+    };
+    static const Command s_commands[];
+
+    static std::optional<HTTPMethod> toCommandHTTPMethod(const String& method);
+    static bool findCommand(const String& method, const String& path, CommandHandler*, HashMap<String, String>& parameters);
+
+    void newSession(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void deleteSession(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void setTimeouts(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void go(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void getCurrentURL(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void back(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void forward(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void refresh(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void getTitle(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void getWindowHandle(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void closeWindow(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void switchToWindow(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void getWindowHandles(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void switchToFrame(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void switchToParentFrame(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void getWindowPosition(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void setWindowPosition(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void getWindowSize(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void setWindowSize(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void findElement(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void findElements(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void findElementFromElement(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void findElementsFromElement(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void isElementSelected(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void getElementText(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void getElementTagName(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void getElementRect(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void isElementEnabled(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void isElementDisplayed(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void getElementAttribute(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void elementClick(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void elementClear(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void elementSendKeys(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void elementSubmit(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void executeScript(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+    void executeAsyncScript(RefPtr<Inspector::InspectorObject>&&, Function<void (CommandResult&&)>&&);
+
+    bool parseCapabilities(Inspector::InspectorObject& desiredCapabilities, Capabilities&, Function<void (CommandResult&&)>&);
+    bool platformParseCapabilities(Inspector::InspectorObject& desiredCapabilities, Capabilities&, Function<void (CommandResult&&)>&);
+    RefPtr<Session> findSessionOrCompleteWithError(Inspector::InspectorObject&, Function<void (CommandResult&&)>&);
+
+    void handleRequest(HTTPRequestHandler::Request&&, Function<void (HTTPRequestHandler::Response&&)>&& replyHandler) override;
+    void sendResponse(Function<void (HTTPRequestHandler::Response&&)>&& replyHandler, CommandResult&&) const;
+
+    HTTPServer m_server;
+    HashMap<String, RefPtr<Session>> m_sessions;
+    Session* m_activeSession { nullptr };
+};
+
+} // namespace WebDriver
diff --git a/Source/WebDriver/config.h b/Source/WebDriver/config.h
new file mode 100644 (file)
index 0000000..9c74f96
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#if defined(HAVE_CONFIG_H) && HAVE_CONFIG_H && defined(BUILDING_WITH_CMAKE)
+#include "cmakeconfig.h"
+#endif
+
+#include <wtf/Platform.h>
+
+#include <wtf/DisallowCType.h>
+#include <wtf/ExportMacros.h>
+
+#ifdef __cplusplus
+#undef new
+#undef delete
+#include <wtf/FastMalloc.h>
+
+#endif // __cplusplus
diff --git a/Source/WebDriver/glib/SessionHostGlib.cpp b/Source/WebDriver/glib/SessionHostGlib.cpp
new file mode 100644 (file)
index 0000000..e223918
--- /dev/null
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2017 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+#include "SessionHost.h"
+
+#include <gio/gio.h>
+#include <wtf/RunLoop.h>
+#include <wtf/glib/GUniquePtr.h>
+
+#define REMOTE_INSPECTOR_CLIENT_DBUS_INTERFACE "org.webkit.RemoteInspectorClient"
+#define REMOTE_INSPECTOR_CLIENT_OBJECT_PATH "/org/webkit/RemoteInspectorClient"
+#define INSPECTOR_DBUS_INTERFACE "org.webkit.Inspector"
+#define INSPECTOR_DBUS_OBJECT_PATH "/org/webkit/Inspector"
+
+namespace WebDriver {
+
+SessionHost::~SessionHost()
+{
+    g_cancellable_cancel(m_cancellable.get());
+    if (m_browser)
+        g_subprocess_force_exit(m_browser.get());
+}
+
+static const char introspectionXML[] =
+    "<node>"
+    "  <interface name='" REMOTE_INSPECTOR_CLIENT_DBUS_INTERFACE "'>"
+    "    <method name='SetTargetList'>"
+    "      <arg type='t' name='connectionID' direction='in'/>"
+    "      <arg type='a(tsssb)' name='list' direction='in'/>"
+    "    </method>"
+    "    <method name='SendMessageToFrontend'>"
+    "      <arg type='t' name='connectionID' direction='in'/>"
+    "      <arg type='t' name='target' direction='in'/>"
+    "      <arg type='s' name='message' direction='in'/>"
+    "    </method>"
+    "  </interface>"
+    "</node>";
+
+const GDBusInterfaceVTable SessionHost::s_interfaceVTable = {
+    // method_call
+    [](GDBusConnection* connection, const gchar* sender, const gchar* objectPath, const gchar* interfaceName, const gchar* methodName, GVariant* parameters, GDBusMethodInvocation* invocation, gpointer userData) {
+        auto* sessionHost = static_cast<SessionHost*>(userData);
+        if (!g_strcmp0(methodName, "SetTargetList")) {
+            guint64 connectionID;
+            GUniqueOutPtr<GVariantIter> iter;
+            g_variant_get(parameters, "(ta(tsssb))", &connectionID, &iter.outPtr());
+            size_t targetCount = g_variant_iter_n_children(iter.get());
+            Vector<SessionHost::Target> targetList;
+            targetList.reserveInitialCapacity(targetCount);
+            guint64 targetID;
+            const char* type;
+            const char* name;
+            const char* dummy;
+            gboolean isPaired;
+            while (g_variant_iter_loop(iter.get(), "(t&s&s&sb)", &targetID, &type, &name, &dummy, &isPaired)) {
+                if (!g_strcmp0(type, "Automation"))
+                    targetList.uncheckedAppend({ targetID, name, isPaired });
+            }
+            sessionHost->setTargetList(connectionID, WTFMove(targetList));
+            g_dbus_method_invocation_return_value(invocation, nullptr);
+        } else if (!g_strcmp0(methodName, "SendMessageToFrontend")) {
+            guint64 connectionID, targetID;
+            const char* message;
+            g_variant_get(parameters, "(tt&s)", &connectionID, &targetID, &message);
+            sessionHost->sendMessageToFrontend(connectionID, targetID, message);
+            g_dbus_method_invocation_return_value(invocation, nullptr);
+        }
+    },
+    // get_property
+    nullptr,
+    // set_property
+    nullptr,
+};
+
+void SessionHost::connectToBrowser(Function<void (Succeeded)>&& completionHandler)
+{
+    launchBrowser(WTFMove(completionHandler));
+}
+
+struct ConnectToBrowserAsyncData {
+    ConnectToBrowserAsyncData(SessionHost* sessionHost, GUniquePtr<char>&& dbusAddress, GCancellable* cancellable, Function<void (SessionHost::Succeeded)>&& completionHandler)
+        : sessionHost(sessionHost)
+        , dbusAddress(WTFMove(dbusAddress))
+        , cancellable(cancellable)
+        , completionHandler(WTFMove(completionHandler))
+    {
+    }
+
+    SessionHost* sessionHost;
+    GUniquePtr<char> dbusAddress;
+    GRefPtr<GCancellable> cancellable;
+    Function<void (SessionHost::Succeeded)> completionHandler;
+};
+
+static guint16 freePort()
+{
+    GRefPtr<GSocket> socket = adoptGRef(g_socket_new(G_SOCKET_FAMILY_IPV4, G_SOCKET_TYPE_STREAM, G_SOCKET_PROTOCOL_DEFAULT, nullptr));
+    GRefPtr<GInetAddress> loopbackAdress = adoptGRef(g_inet_address_new_loopback(G_SOCKET_FAMILY_IPV4));
+    GRefPtr<GSocketAddress> address = adoptGRef(g_inet_socket_address_new(loopbackAdress.get(), 0));
+    g_socket_bind(socket.get(), address.get(), FALSE, nullptr);
+    g_socket_listen(socket.get(), nullptr);
+    address = adoptGRef(g_socket_get_local_address(socket.get(), nullptr));
+    g_socket_close(socket.get(), nullptr);
+    return g_inet_socket_address_get_port(G_INET_SOCKET_ADDRESS(address.get()));
+}
+
+void SessionHost::launchBrowser(Function<void (Succeeded)>&& completionHandler)
+{
+    m_cancellable = adoptGRef(g_cancellable_new());
+    GRefPtr<GSubprocessLauncher> launcher = adoptGRef(g_subprocess_launcher_new(G_SUBPROCESS_FLAGS_NONE));
+    guint16 port = freePort();
+    GUniquePtr<char> inspectorAddress(g_strdup_printf("127.0.0.1:%u", port));
+    g_subprocess_launcher_setenv(launcher.get(), "WEBKIT_INSPECTOR_SERVER", inspectorAddress.get(), TRUE);
+#if PLATFORM(GTK)
+    g_subprocess_launcher_setenv(launcher.get(), "GTK_OVERLAY_SCROLLING", m_capabilities.useOverlayScrollbars ? "1" : "0", TRUE);
+#endif
+
+    GUniquePtr<char*> args(g_new0(char*, m_capabilities.browserArguments.size() + 2));
+    args.get()[0] = g_strdup(m_capabilities.browserBinary.utf8().data());
+    for (unsigned i = 0; i < m_capabilities.browserArguments.size(); ++i)
+        args.get()[i + 1] = g_strdup(m_capabilities.browserArguments[i].utf8().data());
+
+    m_browser = adoptGRef(g_subprocess_launcher_spawnv(launcher.get(), args.get(), nullptr));
+    g_subprocess_wait_async(m_browser.get(), m_cancellable.get(), [](GObject* browser, GAsyncResult* result, gpointer userData) {
+        GUniqueOutPtr<GError> error;
+        g_subprocess_wait_finish(G_SUBPROCESS(browser), result, &error.outPtr());
+        if (g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED))
+            return;
+        auto* sessionHost = static_cast<SessionHost*>(userData);
+        sessionHost->m_browser = nullptr;
+    }, this);
+
+    GUniquePtr<char> dbusAddress(g_strdup_printf("tcp:host=%s,port=%u", "127.0.0.1", port));
+    connectToBrowser(std::make_unique<ConnectToBrowserAsyncData>(this, WTFMove(dbusAddress), m_cancellable.get(), WTFMove(completionHandler)));
+}
+
+void SessionHost::connectToBrowser(std::unique_ptr<ConnectToBrowserAsyncData>&& data)
+{
+    if (!m_browser)
+        return;
+
+    RunLoop::main().dispatchAfter(100_ms, [connectToBrowserData = WTFMove(data)]() mutable {
+        auto* data = connectToBrowserData.release();
+        if (g_cancellable_is_cancelled(data->cancellable.get()))
+            return;
+
+        g_dbus_connection_new_for_address(data->dbusAddress.get(), G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT, nullptr, data->cancellable.get(),
+            [](GObject*, GAsyncResult* result, gpointer userData) {
+                auto data = std::unique_ptr<ConnectToBrowserAsyncData>(static_cast<ConnectToBrowserAsyncData*>(userData));
+                GUniqueOutPtr<GError> error;
+                GRefPtr<GDBusConnection> connection = adoptGRef(g_dbus_connection_new_for_address_finish(result, &error.outPtr()));
+                if (!connection) {
+                    if (g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED))
+                        return;
+
+                    if (g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CONNECTION_REFUSED)) {
+                        data->sessionHost->connectToBrowser(WTFMove(data));
+                        return;
+                    }
+
+                    data->completionHandler(Succeeded::No);
+                    return;
+                }
+                data->sessionHost->setupConnection(WTFMove(connection), WTFMove(data->completionHandler));
+        }, data);
+    });
+}
+
+void SessionHost::dbusConnectionClosedCallback(SessionHost* sessionHost)
+{
+    sessionHost->inspectorDisconnected();
+}
+
+static void dbusConnectionCallAsyncReadyCallback(GObject* source, GAsyncResult* result, gpointer)
+{
+    GUniqueOutPtr<GError> error;
+    GRefPtr<GVariant> resultVariant = adoptGRef(g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error.outPtr()));
+    if (!resultVariant && !g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        WTFLogAlways("RemoteInspectorServer failed to send DBus message: %s", error->message);
+}
+
+void SessionHost::setupConnection(GRefPtr<GDBusConnection>&& connection, Function<void (Succeeded)>&& completionHandler)
+{
+    ASSERT(!m_dbusConnection);
+    ASSERT(connection);
+    m_dbusConnection = WTFMove(connection);
+
+    g_signal_connect_swapped(m_dbusConnection.get(), "closed", G_CALLBACK(dbusConnectionClosedCallback), this);
+
+    static GDBusNodeInfo* introspectionData = nullptr;
+    if (!introspectionData)
+        introspectionData = g_dbus_node_info_new_for_xml(introspectionXML, nullptr);
+
+    g_dbus_connection_register_object(m_dbusConnection.get(), REMOTE_INSPECTOR_CLIENT_OBJECT_PATH, introspectionData->interfaces[0], &s_interfaceVTable, this, nullptr, nullptr);
+
+    completionHandler(Succeeded::Yes);
+}
+
+void SessionHost::startAutomationSession(const String& sessionID, Function<void ()>&& completionHandler)
+{
+    ASSERT(m_dbusConnection);
+    ASSERT(!m_startSessionCompletionHandler);
+    m_startSessionCompletionHandler = WTFMove(completionHandler);
+    g_dbus_connection_call(m_dbusConnection.get(), nullptr,
+        INSPECTOR_DBUS_OBJECT_PATH,
+        INSPECTOR_DBUS_INTERFACE,
+        "StartAutomationSession",
+        g_variant_new("(s)", sessionID.utf8().data()),
+        nullptr, G_DBUS_CALL_FLAGS_NO_AUTO_START,
+        -1, m_cancellable.get(), dbusConnectionCallAsyncReadyCallback, nullptr);
+}
+
+void SessionHost::setTargetList(uint64_t connectionID, Vector<Target>&& targetList)
+{
+    // The server notifies all its clients when connection is lost by sending an empty target list.
+    // We only care about automation connection.
+    if (m_connectionID && m_connectionID != connectionID)
+        return;
+
+    ASSERT(targetList.size() <= 1);
+    if (targetList.isEmpty()) {
+        m_target = Target();
+        m_connectionID = 0;
+        return;
+    }
+
+    m_target = targetList[0];
+    if (m_connectionID) {
+        ASSERT(m_connectionID == connectionID);
+        return;
+    }
+
+    m_connectionID = connectionID;
+    g_dbus_connection_call(m_dbusConnection.get(), nullptr,
+        INSPECTOR_DBUS_OBJECT_PATH,
+        INSPECTOR_DBUS_INTERFACE,
+        "Setup",
+        g_variant_new("(tt)", m_connectionID, m_target.id),
+        nullptr, G_DBUS_CALL_FLAGS_NO_AUTO_START,
+        -1, m_cancellable.get(), dbusConnectionCallAsyncReadyCallback, nullptr);
+
+    auto startSessionCompletionHandler = std::exchange(m_startSessionCompletionHandler, nullptr);
+    startSessionCompletionHandler();
+}
+
+void SessionHost::sendMessageToFrontend(uint64_t connectionID, uint64_t targetID, const char* message)
+{
+    if (connectionID != m_connectionID || targetID != m_target.id)
+        return;
+    dispatchMessage(String::fromUTF8(message));
+}
+
+void SessionHost::sendMessageToBackend(const String& message)
+{
+    ASSERT(m_dbusConnection);
+    ASSERT(m_connectionID);
+    ASSERT(m_target.id);
+
+    g_dbus_connection_call(m_dbusConnection.get(), nullptr,
+        INSPECTOR_DBUS_OBJECT_PATH,
+        INSPECTOR_DBUS_INTERFACE,
+        "SendMessageToBackend",
+        g_variant_new("(tts)", m_connectionID, m_target.id, message.utf8().data()),
+        nullptr, G_DBUS_CALL_FLAGS_NO_AUTO_START,
+        -1, m_cancellable.get(), dbusConnectionCallAsyncReadyCallback, nullptr);
+}
+
+} // namespace WebDriver
diff --git a/Source/WebDriver/gtk/WebDriverServiceGtk.cpp b/Source/WebDriver/gtk/WebDriverServiceGtk.cpp
new file mode 100644 (file)
index 0000000..2610658
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2017 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+#include "WebDriverService.h"
+
+#include "Capabilities.h"
+#include "CommandResult.h"
+#include <inspector/InspectorValues.h>
+
+using namespace Inspector;
+
+namespace WebDriver {
+
+bool WebDriverService::platformParseCapabilities(InspectorObject& desiredCapabilities, Capabilities& capabilities, Function<void (CommandResult&&)>& completionHandler)
+{
+    RefPtr<InspectorValue> value;
+    RefPtr<InspectorObject> browserOptions;
+    if (desiredCapabilities.getValue(ASCIILiteral("webkitgtk:browserOptions"), value) && !value->asObject(browserOptions)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::SessionNotCreated, String("webkitgtk:browserOptions is invalid in capabilities")));
+        return false;
+    }
+    if (browserOptions->isNull()) {
+        capabilities.browserBinary = LIBEXECDIR "/webkit2gtk-" WEBKITGTK_API_VERSION_STRING "/MiniBrowser";
+        capabilities.browserArguments = { ASCIILiteral("--automation") };
+        return true;
+    }
+
+    if (!browserOptions->getString(ASCIILiteral("binary"), capabilities.browserBinary)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::SessionNotCreated, String("binary parameter is invalid or missing in webkitgtk:browserOptions")));
+        return false;
+    }
+
+    RefPtr<InspectorArray> browserArguments;
+    if (browserOptions->getValue(ASCIILiteral("args"), value) && !value->asArray(browserArguments)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::SessionNotCreated, String("args parameter is invalid in webkitgtk:browserOptions")));
+        return false;
+    }
+    unsigned browserArgumentsLength = browserArguments->length();
+    if (!browserArgumentsLength)
+        return true;
+    capabilities.browserArguments.reserveInitialCapacity(browserArgumentsLength);
+    for (unsigned i = 0; i < browserArgumentsLength; ++i) {
+        RefPtr<InspectorValue> value = browserArguments->get(i);
+        String argument;
+        if (!value->asString(argument)) {
+            capabilities.browserArguments.clear();
+            completionHandler(CommandResult::fail(CommandResult::ErrorCode::SessionNotCreated, String("Failed to extract arguments from webkitgtk:browserOptions::args")));
+            return false;
+        }
+        capabilities.browserArguments.uncheckedAppend(WTFMove(argument));
+    }
+
+    if (browserOptions->getValue(ASCIILiteral("useOverlayScrollbars"), value) && !value->asBoolean(capabilities.useOverlayScrollbars)) {
+        completionHandler(CommandResult::fail(CommandResult::ErrorCode::SessionNotCreated, String("useOverlayScrollbars parameter is invalid in webkitgtk:browserOptions")));
+        return false;
+    }
+
+    return true;
+}
+
+} // namespace WebDriver
diff --git a/Source/WebDriver/soup/HTTPServerSoup.cpp b/Source/WebDriver/soup/HTTPServerSoup.cpp
new file mode 100644 (file)
index 0000000..7759924
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2017 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+#include "HTTPServer.h"
+
+#include <libsoup/soup.h>
+#include <wtf/Function.h>
+#include <wtf/glib/GUniquePtr.h>
+
+namespace WebDriver {
+
+bool HTTPServer::listen(unsigned port)
+{
+    m_soupServer = adoptGRef(soup_server_new(SOUP_SERVER_SERVER_HEADER, "WebKitWebDriver", nullptr));
+    GUniqueOutPtr<GError> error;
+    if (!soup_server_listen_local(m_soupServer.get(), port, static_cast<SoupServerListenOptions>(0), &error.outPtr())) {
+        WTFLogAlways("Failed to start HTTP server at port %u: %s", port, error->message);
+        return false;
+    }
+
+    soup_server_add_handler(m_soupServer.get(), nullptr, [](SoupServer* server, SoupMessage* message, const char* path, GHashTable*, SoupClientContext*, gpointer userData) {
+        auto* httpServer = static_cast<HTTPServer*>(userData);
+        GRefPtr<SoupMessage> protectedMessage = message;
+        soup_server_pause_message(server, message);
+        httpServer->m_requestHandler.handleRequest({ String::fromUTF8(message->method), String::fromUTF8(path), message->request_body->data, static_cast<size_t>(message->request_body->length) },
+            [server, message = WTFMove(protectedMessage)](HTTPRequestHandler::Response&& response) {
+                soup_message_set_status(message.get(), response.statusCode);
+                if (!response.data.isNull()) {
+                    soup_message_headers_append(message->response_headers, "Content-Type", response.contentType.utf8().data());
+                    soup_message_body_append(message->response_body, SOUP_MEMORY_COPY, response.data.data(), response.data.length());
+                }
+                soup_server_unpause_message(server, message.get());
+        });
+    }, this, nullptr);
+
+    return true;
+}
+
+void HTTPServer::disconnect()
+{
+    soup_server_disconnect(m_soupServer.get());
+    m_soupServer = nullptr;
+}
+
+} // namespace WebDriver
index 492949d..efdb2f8 100644 (file)
@@ -143,6 +143,7 @@ WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_SPELLCHECK PUBLIC ON)
 WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_TOUCH_EVENTS PUBLIC ON)
 WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_VIDEO PUBLIC ON)
 WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_WEB_AUDIO PUBLIC ON)
+WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_WEBDRIVER PUBLIC ON)
 WEBKIT_OPTION_DEFAULT_PORT_VALUE(USE_SYSTEM_MALLOC PUBLIC OFF)
 
 # Private options shared with other WebKit ports. Add options here when
@@ -276,6 +277,16 @@ if (ENABLE_SUBTLE_CRYPTO)
     SET_AND_EXPOSE_TO_BUILD(USE_GCRYPT TRUE)
 endif ()
 
+if (ENABLE_WEBDRIVER)
+    # WebDriver requires newer versions of GLib and Soup.
+    if (PC_GLIB_VERSION VERSION_LESS "2.40")
+        message(FATAL_ERROR "GLib 2.40 is required to enable WebDriver support.")
+    endif ()
+    if (PC_LIBSOUP_VERSION VERSION_LESS "2.48")
+        message(FATAL_ERROR "libsoup 2.48 is required to enable WebDriver support.")
+    endif ()
+endif ()
+
 SET_AND_EXPOSE_TO_BUILD(USE_TEXTURE_MAPPER TRUE)
 
 if (ENABLE_OPENGL)
index 7fe2d91..7cd2c14 100644 (file)
@@ -25,10 +25,14 @@ endif ()
 if (NOT TOOLS_DIR)
     set(TOOLS_DIR "${CMAKE_SOURCE_DIR}/Tools")
 endif ()
+if (NOT WEBDRIVER_DIR)
+    set(WEBDRIVER_DIR "${CMAKE_SOURCE_DIR}/Source/WebDriver")
+endif ()
 
 set(DERIVED_SOURCES_DIR "${CMAKE_BINARY_DIR}/DerivedSources")
 set(DERIVED_SOURCES_JAVASCRIPTCORE_DIR "${CMAKE_BINARY_DIR}/DerivedSources/JavaScriptCore")
 set(DERIVED_SOURCES_WEBCORE_DIR "${CMAKE_BINARY_DIR}/DerivedSources/WebCore")
+set(DERIVED_SOURCES_WEBDRIVER_DIR "${CMAKE_BINARY_DIR}/DerivedSources/WebDriver")
 set(DERIVED_SOURCES_WEBKITLEGACY_DIR "${CMAKE_BINARY_DIR}/DerivedSources/WebKitLegacy")
 set(DERIVED_SOURCES_WEBKIT_DIR "${CMAKE_BINARY_DIR}/DerivedSources/WebKit")
 set(DERIVED_SOURCES_WEBKIT2_DIR "${CMAKE_BINARY_DIR}/DerivedSources/WebKit2")
@@ -56,3 +60,7 @@ if (ENABLE_WEBKIT_LEGACY)
     file(MAKE_DIRECTORY ${DERIVED_SOURCES_WEBKITLEGACY_DIR})
     file(MAKE_DIRECTORY ${DERIVED_SOURCES_WEBKIT_DIR})
 endif ()
+
+if (ENABLE_WEBDRIVER)
+    file(MAKE_DIRECTORY ${DERIVED_SOURCES_WEBDRIVER_DIR})
+endif ()
index 5a6fc43..59c31ee 100644 (file)
@@ -182,6 +182,7 @@ macro(WEBKIT_OPTION_BEGIN)
     WEBKIT_OPTION_DEFINE(ENABLE_VIDEO_TRACK "Toggle Track support for HTML5 video" PRIVATE OFF)
     WEBKIT_OPTION_DEFINE(ENABLE_VIEW_MODE_CSS_MEDIA "Toggle Track support for the view-mode media Feature" PRIVATE ON)
     WEBKIT_OPTION_DEFINE(ENABLE_WEBASSEMBLY "Toggle WebAssembly support" PRIVATE ${ENABLE_FTL_DEFAULT})
+    WEBKIT_OPTION_DEFINE(ENABLE_WEBDRIVER "Whether to enable the WebDriver service process" PRIVATE OFF)
     WEBKIT_OPTION_DEFINE(ENABLE_WEBGL "Toggle WebGL support" PRIVATE OFF)
     WEBKIT_OPTION_DEFINE(ENABLE_WEBGL2 "Toggle WebGL 2.0 support" PRIVATE OFF)
     WEBKIT_OPTION_DEFINE(ENABLE_WEBGPU "Toggle WebGPU support" PRIVATE OFF)