diff --git a/Userland/Services/WebContent/WebDriverClient.ipc b/Userland/Services/WebContent/WebDriverClient.ipc index 78852f2da9..3862aa2892 100644 --- a/Userland/Services/WebContent/WebDriverClient.ipc +++ b/Userland/Services/WebContent/WebDriverClient.ipc @@ -22,6 +22,7 @@ endpoint WebDriverClient { get_element_rect(String element_id) => (Web::WebDriver::Response response) is_element_enabled(String element_id) => (Web::WebDriver::Response response) get_source() => (Web::WebDriver::Response response) + execute_script(JsonValue payload) => (Web::WebDriver::Response response) take_screenshot() => (Web::WebDriver::Response response) take_element_screenshot(String element_id) => (Web::WebDriver::Response response) } diff --git a/Userland/Services/WebContent/WebDriverConnection.cpp b/Userland/Services/WebContent/WebDriverConnection.cpp index f2b6a450b2..192694729a 100644 --- a/Userland/Services/WebContent/WebDriverConnection.cpp +++ b/Userland/Services/WebContent/WebDriverConnection.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -155,7 +156,8 @@ static void scroll_element_into_view(Web::DOM::Element& element) element.scroll_into_view(options); } -static ErrorOr get_property(JsonValue const& payload, StringView key) +template +static ErrorOr get_property(JsonValue const& payload, StringView key) { if (!payload.is_object()) return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, "Payload is not a JSON object"); @@ -164,10 +166,19 @@ static ErrorOr get_property(JsonValue const& payl if (!property) return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, String::formatted("No property called '{}' present", key)); - if (!property->is_string()) - return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, String::formatted("Property '{}' is not a String", key)); - return property->as_string(); + if constexpr (IsSame) { + if (!property->is_string()) + return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, String::formatted("Property '{}' is not a String", key)); + return property->as_string(); + } else if constexpr (IsSame) { + if (!property->is_array()) + return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, String::formatted("Property '{}' is not an Array", key)); + return &property->as_array(); + } else { + static_assert(DependentFalse, "get_property invoked with unknown property type"); + VERIFY_NOT_REACHED(); + } } ErrorOr> WebDriverConnection::connect(ConnectionFromClient& web_content_client, PageHost& page_host, String const& webdriver_ipc_path) @@ -754,6 +765,38 @@ Messages::WebDriverClient::GetSourceResponse WebDriverConnection::get_source() return make_success_response(source.release_value()); } +// 13.2.1 Execute Script, https://w3c.github.io/webdriver/#dfn-execute-script +Messages::WebDriverClient::ExecuteScriptResponse WebDriverConnection::execute_script(JsonValue const& payload) +{ + // 1. Let body and arguments be the result of trying to extract the script arguments from a request with argument parameters. + auto const& [body, arguments] = TRY(extract_the_script_arguments_from_a_request(payload)); + + // 2. If the current browsing context is no longer open, return error with error code no such window. + TRY(ensure_open_top_level_browsing_context()); + + // FIXME: 3. Handle any user prompts, and return its value if it is an error. + + // 4., 5.1-5.3. + // FIXME: Move timeouts from WebDriver to WebContent and pass the script timeout through here. + auto result = Web::WebDriver::execute_script(m_page_host.page(), body, move(arguments), {}); + dbgln_if(WEBDRIVER_DEBUG, "Executing script returned: {}", result.value); + + switch (result.type) { + // 6. If promise is still pending and the session script timeout is reached, return error with error code script timeout. + case Web::WebDriver::ExecuteScriptResultType::Timeout: + return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::ScriptTimeoutError, "Script timed out"); + // 7. Upon fulfillment of promise with value v, let result be a JSON clone of v, and return success with data result. + case Web::WebDriver::ExecuteScriptResultType::PromiseResolved: + return make_success_response(move(result.value)); + // 8. Upon rejection of promise with reason r, let result be a JSON clone of r, and return error with error code javascript error and data result. + case Web::WebDriver::ExecuteScriptResultType::PromiseRejected: + case Web::WebDriver::ExecuteScriptResultType::JavaScriptError: + return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::JavascriptError, "Script returned an error", move(result.value)); + } + + VERIFY_NOT_REACHED(); +} + // 17.1 Take Screenshot, https://w3c.github.io/webdriver/#take-screenshot Messages::WebDriverClient::TakeScreenshotResponse WebDriverConnection::take_screenshot() { @@ -892,4 +935,29 @@ ErrorOr WebDriverConnection::find(Web::DOM::Pa return result; } +// https://w3c.github.io/webdriver/#dfn-extract-the-script-arguments-from-a-request +ErrorOr WebDriverConnection::extract_the_script_arguments_from_a_request(JsonValue const& payload) +{ + auto* window = m_page_host.page().top_level_browsing_context().active_window(); + auto& vm = window->vm(); + + // 1. Let script be the result of getting a property named script from the parameters. + // 2. If script is not a String, return error with error code invalid argument. + auto script = TRY(get_property(payload, "script"sv)); + + // 3. Let args be the result of getting a property named args from the parameters. + // 4. If args is not an Array return error with error code invalid argument. + auto const& args = *TRY(get_property(payload, "args"sv)); + + // 5. Let arguments be the result of calling the JSON deserialize algorithm with arguments args. + auto arguments = JS::MarkedVector { vm.heap() }; + + args.for_each([&](auto const& arg) { + arguments.append(JS::JSONObject::parse_json_value(vm, arg)); + }); + + // 6. Return success with data script and arguments. + return ScriptArguments { move(script), move(arguments) }; +} + } diff --git a/Userland/Services/WebContent/WebDriverConnection.h b/Userland/Services/WebContent/WebDriverConnection.h index bce9e68265..52f6455681 100644 --- a/Userland/Services/WebContent/WebDriverConnection.h +++ b/Userland/Services/WebContent/WebDriverConnection.h @@ -9,6 +9,8 @@ #pragma once #include +#include +#include #include #include #include @@ -52,6 +54,7 @@ private: virtual Messages::WebDriverClient::GetElementRectResponse get_element_rect(String const& element_id) override; virtual Messages::WebDriverClient::IsElementEnabledResponse is_element_enabled(String const& element_id) override; virtual Messages::WebDriverClient::GetSourceResponse get_source() override; + virtual Messages::WebDriverClient::ExecuteScriptResponse execute_script(JsonValue const& payload) override; virtual Messages::WebDriverClient::TakeScreenshotResponse take_screenshot() override; virtual Messages::WebDriverClient::TakeElementScreenshotResponse take_element_screenshot(String const& element_id) override; @@ -61,6 +64,12 @@ private: Gfx::IntRect iconify_the_window(); ErrorOr find(Web::DOM::ParentNode& start_node, Web::WebDriver::LocationStrategy using_, StringView value); + struct ScriptArguments { + String script; + JS::MarkedVector arguments; + }; + ErrorOr extract_the_script_arguments_from_a_request(JsonValue const& payload); + ConnectionFromClient& m_web_content_client; PageHost& m_page_host; }; diff --git a/Userland/Services/WebDriver/Client.cpp b/Userland/Services/WebDriver/Client.cpp index be69280e2f..b18e5213e2 100644 --- a/Userland/Services/WebDriver/Client.cpp +++ b/Userland/Services/WebDriver/Client.cpp @@ -735,8 +735,7 @@ Web::WebDriver::Response Client::handle_execute_script(Vector const& { dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session//execute/sync"); auto* session = TRY(find_session_with_id(parameters[0])); - auto result = TRY(session->execute_script(payload)); - return make_json_value(result); + return session->web_content_connection().execute_script(payload); } // 13.2.2 Execute Async Script, https://w3c.github.io/webdriver/#dfn-execute-async-script diff --git a/Userland/Services/WebDriver/Session.cpp b/Userland/Services/WebDriver/Session.cpp index 6a677dcc5a..c0510a44a2 100644 --- a/Userland/Services/WebDriver/Session.cpp +++ b/Userland/Services/WebDriver/Session.cpp @@ -329,47 +329,6 @@ static ErrorOr extract_the_script_argume return ScriptArguments { script, args }; } -// 13.2.1 Execute Script, https://w3c.github.io/webdriver/#dfn-execute-script -Web::WebDriver::Response Session::execute_script(JsonValue const& payload) -{ - // 1. Let body and arguments be the result of trying to extract the script arguments from a request with argument parameters. - auto const& [body, arguments] = TRY(extract_the_script_arguments_from_a_request(payload)); - - // 2. If the current browsing context is no longer open, return error with error code no such window. - TRY(check_for_open_top_level_browsing_context_or_return_error()); - - // FIXME: 3. Handle any user prompts, and return its value if it is an error. - - // 4., 5.1-5.3. - Vector json_arguments; - arguments.for_each([&](JsonValue const& json_value) { - // NOTE: serialized() instead of to_string() ensures proper quoting. - json_arguments.append(json_value.serialized()); - }); - - dbgln("Executing script with 'args': [{}] / 'body':\n{}", String::join(", "sv, json_arguments), body); - auto execute_script_response = m_browser_connection->execute_script(body, json_arguments, m_timeouts_configuration.script_timeout, false); - dbgln("Executing script returned: {}", execute_script_response.json_result()); - - // NOTE: This is assumed to be a valid JSON value. - auto result = MUST(JsonValue::from_string(execute_script_response.json_result())); - - switch (execute_script_response.result_type()) { - // 6. If promise is still pending and the session script timeout is reached, return error with error code script timeout. - case Web::WebDriver::ExecuteScriptResultType::Timeout: - return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::ScriptTimeoutError, "Script timed out"); - // 7. Upon fulfillment of promise with value v, let result be a JSON clone of v, and return success with data result. - case Web::WebDriver::ExecuteScriptResultType::PromiseResolved: - return result; - // 8. Upon rejection of promise with reason r, let result be a JSON clone of r, and return error with error code javascript error and data result. - case Web::WebDriver::ExecuteScriptResultType::PromiseRejected: - case Web::WebDriver::ExecuteScriptResultType::JavaScriptError: - return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::JavascriptError, "Script returned an error", move(result)); - default: - VERIFY_NOT_REACHED(); - } -} - // 13.2.2 Execute Async Script, https://w3c.github.io/webdriver/#dfn-execute-async-script Web::WebDriver::Response Session::execute_async_script(JsonValue const& parameters) { diff --git a/Userland/Services/WebDriver/Session.h b/Userland/Services/WebDriver/Session.h index bfe53e0d6e..d21b680e61 100644 --- a/Userland/Services/WebDriver/Session.h +++ b/Userland/Services/WebDriver/Session.h @@ -58,7 +58,6 @@ public: Web::WebDriver::Response get_window_handle(); ErrorOr> close_window(); Web::WebDriver::Response get_window_handles() const; - Web::WebDriver::Response execute_script(JsonValue const& payload); Web::WebDriver::Response execute_async_script(JsonValue const& payload); Web::WebDriver::Response get_all_cookies(); Web::WebDriver::Response get_named_cookie(String const& name);