From 61de50c7fd11136d088545abab8c2bb814ef005d Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Wed, 9 Nov 2022 15:02:38 -0500 Subject: [PATCH] WebContent+WebDriver: Move Find Element to WebContent Note that this does nothing to "fix" how element references are created. We continue to return the element ID because, otherwise, all other element WebDriver endpoints would break. On the bright side, we avoid several IPC round trips now that we perform the entire 'find' operation in the WebContent process; and we are able to work directly on DOM nodes. --- .../Services/WebContent/WebDriverClient.ipc | 1 + .../WebContent/WebDriverConnection.cpp | 114 ++++++++++++++++++ .../Services/WebContent/WebDriverConnection.h | 4 + Userland/Services/WebDriver/Client.cpp | 3 +- Userland/Services/WebDriver/Session.cpp | 55 --------- Userland/Services/WebDriver/Session.h | 1 - 6 files changed, 120 insertions(+), 58 deletions(-) diff --git a/Userland/Services/WebContent/WebDriverClient.ipc b/Userland/Services/WebContent/WebDriverClient.ipc index 9ca42eddc7..68fcd0043b 100644 --- a/Userland/Services/WebContent/WebDriverClient.ipc +++ b/Userland/Services/WebContent/WebDriverClient.ipc @@ -9,4 +9,5 @@ endpoint WebDriverClient { set_window_rect(JsonValue payload) => (Web::WebDriver::Response response) maximize_window() => (Web::WebDriver::Response response) minimize_window() => (Web::WebDriver::Response response) + find_element(JsonValue payload) => (Web::WebDriver::Response response) } diff --git a/Userland/Services/WebContent/WebDriverConnection.cpp b/Userland/Services/WebContent/WebDriverConnection.cpp index 5f52ed7b6f..57ab7f2f86 100644 --- a/Userland/Services/WebContent/WebDriverConnection.cpp +++ b/Userland/Services/WebContent/WebDriverConnection.cpp @@ -50,6 +50,51 @@ static Gfx::IntRect compute_window_rect(Web::Page const& page) }; } +// https://w3c.github.io/webdriver/#dfn-get-or-create-a-web-element-reference +static String get_or_create_a_web_element_reference(Web::DOM::Node const& element) +{ + // FIXME: 1. For each known element of the current browsing context’s list of known elements: + // FIXME: 1. If known element equals element, return success with known element’s web element reference. + // FIXME: 2. Add element to the list of known elements of the current browsing context. + // FIXME: 3. Return success with the element’s web element reference. + + return String::number(element.id()); +} + +// https://w3c.github.io/webdriver/#dfn-web-element-reference-object +static JsonObject web_element_reference_object(Web::DOM::Node const& element) +{ + // https://w3c.github.io/webdriver/#dfn-web-element-identifier + static String const web_element_identifier = "element-6066-11e4-a52e-4f735466cecf"sv; + + // 1. Let identifier be the web element identifier. + auto identifier = web_element_identifier; + + // 2. Let reference be the result of get or create a web element reference given element. + auto reference = get_or_create_a_web_element_reference(element); + + // 3. Return a JSON Object initialized with a property with name identifier and value reference. + JsonObject object; + object.set("name"sv, identifier); + object.set("value"sv, reference); + return object; +} + +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"); + + auto const* property = payload.as_object().get_ptr(key); + + 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(); +} + ErrorOr> WebDriverConnection::connect(ConnectionFromClient& web_content_client, PageHost& page_host, String const& webdriver_ipc_path) { dbgln_if(WEBDRIVER_DEBUG, "Trying to connect to {}", webdriver_ipc_path); @@ -261,6 +306,43 @@ Messages::WebDriverClient::MinimizeWindowResponse WebDriverConnection::minimize_ return serialize_rect(window_rect); } +// 12.3.2 Find Element, https://w3c.github.io/webdriver/#dfn-find-element +Messages::WebDriverClient::FindElementResponse WebDriverConnection::find_element(JsonValue const& payload) +{ + // 1. Let location strategy be the result of getting a property called "using". + auto location_strategy_string = TRY(get_property(payload, "using"sv)); + auto location_strategy = Web::WebDriver::location_strategy_from_string(location_strategy_string); + + // 2. If location strategy is not present as a keyword in the table of location strategies, return error with error code invalid argument. + if (!location_strategy.has_value()) + return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, String::formatted("Location strategy '{}' is invalid", location_strategy_string)); + + // 3. Let selector be the result of getting a property called "value". + // 4. If selector is undefined, return error with error code invalid argument. + auto selector = TRY(get_property(payload, "value"sv)); + + // 5. 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: 6. Handle any user prompts and return its value if it is an error. + + // 7. Let start node be the current browsing context’s document element. + auto* start_node = m_page_host.page().top_level_browsing_context().active_document(); + + // 8. If start node is null, return error with error code no such element. + if (!start_node) + return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchElement, "document element does not exist"sv); + + // 9. Let result be the result of trying to Find with start node, location strategy, and selector. + auto result = TRY(find(*start_node, *location_strategy, selector)); + + // 10. If result is empty, return error with error code no such element. Otherwise, return the first element of result. + if (result.is_empty()) + return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchElement, "The requested element does not exist"sv); + + return make_success_response(result.at(0)); +} + // https://w3c.github.io/webdriver/#dfn-no-longer-open ErrorOr WebDriverConnection::ensure_open_top_level_browsing_context() { @@ -310,4 +392,36 @@ Gfx::IntRect WebDriverConnection::iconify_the_window() return rect; } +// https://w3c.github.io/webdriver/#dfn-find +ErrorOr WebDriverConnection::find(Web::DOM::ParentNode& start_node, Web::WebDriver::LocationStrategy using_, StringView value) +{ + // FIXME: 1. Let end time be the current time plus the session implicit wait timeout. + + // 2. Let location strategy be equal to using. + auto location_strategy = using_; + + // 3. Let selector be equal to value. + auto selector = value; + + // 4. Let elements returned be the result of trying to call the relevant element location strategy with arguments start node, and selector. + auto elements = Web::WebDriver::invoke_location_strategy(location_strategy, start_node, selector); + + // 5. If a DOMException, SyntaxError, XPathException, or other error occurs during the execution of the element location strategy, return error invalid selector. + if (elements.is_error()) + return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidSelector, String::formatted("The location strategy could not finish: {}", elements.error().message)); + + // FIXME: 6. If elements returned is empty and the current time is less than end time return to step 4. Otherwise, continue to the next step. + + // 7. Let result be an empty JSON List. + JsonArray result; + result.ensure_capacity(elements.value()->length()); + + // 8. For each element in elements returned, append the web element reference object for element, to result. + for (size_t i = 0; i < elements.value()->length(); ++i) + result.append(web_element_reference_object(*elements.value()->item(i))); + + // 9. Return success with data result. + return result; +} + } diff --git a/Userland/Services/WebContent/WebDriverConnection.h b/Userland/Services/WebContent/WebDriverConnection.h index 369ef20363..3475c73d42 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 @@ -37,11 +39,13 @@ private: virtual Messages::WebDriverClient::SetWindowRectResponse set_window_rect(JsonValue const& payload) override; virtual Messages::WebDriverClient::MaximizeWindowResponse maximize_window() override; virtual Messages::WebDriverClient::MinimizeWindowResponse minimize_window() override; + virtual Messages::WebDriverClient::FindElementResponse find_element(JsonValue const& payload) override; ErrorOr ensure_open_top_level_browsing_context(); void restore_the_window(); Gfx::IntRect maximize_the_window(); Gfx::IntRect iconify_the_window(); + ErrorOr find(Web::DOM::ParentNode& start_node, Web::WebDriver::LocationStrategy using_, StringView value); ConnectionFromClient& m_web_content_client; PageHost& m_page_host; diff --git a/Userland/Services/WebDriver/Client.cpp b/Userland/Services/WebDriver/Client.cpp index ba782e8510..5843173b36 100644 --- a/Userland/Services/WebDriver/Client.cpp +++ b/Userland/Services/WebDriver/Client.cpp @@ -618,8 +618,7 @@ Web::WebDriver::Response Client::handle_find_element(Vector const& p { dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session//element"); auto* session = TRY(find_session_with_id(parameters[0])); - auto result = TRY(session->find_element(payload)); - return make_json_value(result); + return session->web_content_connection().find_element(payload); } // 12.3.3 Find Elements, https://w3c.github.io/webdriver/#dfn-find-elements diff --git a/Userland/Services/WebDriver/Session.cpp b/Userland/Services/WebDriver/Session.cpp index ef70dcf297..64b5696bcc 100644 --- a/Userland/Services/WebDriver/Session.cpp +++ b/Userland/Services/WebDriver/Session.cpp @@ -447,61 +447,6 @@ ErrorOr, Web::WebDriver::Error> Session::locator_s return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::UnsupportedOperation, "Not implemented: locator strategy XPath"); } -// 12.3.2 Find Element, https://w3c.github.io/webdriver/#dfn-find-element -Web::WebDriver::Response Session::find_element(JsonValue const& payload) -{ - if (!payload.is_object()) - return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, "Payload is not a JSON object"); - - auto const& properties = payload.as_object(); - // 1. Let location strategy be the result of getting a property called "using". - if (!properties.has("using"sv)) - return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, "No property called 'using' present"); - auto const& maybe_location_strategy = properties.get("using"sv); - if (!maybe_location_strategy.is_string()) - return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, "Property 'using' is not a String"); - - auto location_strategy = maybe_location_strategy.to_string(); - - // 2. If location strategy is not present as a keyword in the table of location strategies, return error with error code invalid argument. - if (!s_locator_strategies.first_matching([&](LocatorStrategy const& match) { return match.name == location_strategy; }).has_value()) - return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, "No valid location strategy"); - - // 3. Let selector be the result of getting a property called "value". - // 4. If selector is undefined, return error with error code invalid argument. - if (!properties.has("value"sv)) - return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, "No property called 'value' present"); - auto const& maybe_selector = properties.get("value"sv); - if (!maybe_selector.is_string()) - return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, "Property 'value' is not a String"); - - auto selector = maybe_selector.to_string(); - - // 5. 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: 6. Handle any user prompts and return its value if it is an error. - - // 7. Let start node be the current browsing context’s document element. - auto maybe_start_node_id = m_browser_connection->get_document_element(); - - // 8. If start node is null, return error with error code no such element. - if (!maybe_start_node_id.has_value()) - return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchElement, "document element does not exist"); - - auto start_node_id = maybe_start_node_id.release_value(); - LocalElement start_node = { start_node_id }; - - // 9. Let result be the result of trying to Find with start node, location strategy, and selector. - auto result = TRY(find(start_node, location_strategy, selector)); - - // 10. If result is empty, return error with error code no such element. Otherwise, return the first element of result. - if (result.is_empty()) - return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchElement, "The requested element does not exist"); - - return JsonValue(result.at(0)); -} - // 12.3.3 Find Elements, https://w3c.github.io/webdriver/#dfn-find-elements Web::WebDriver::Response Session::find_elements(JsonValue const& payload) { diff --git a/Userland/Services/WebDriver/Session.h b/Userland/Services/WebDriver/Session.h index 5acf1b6892..05377f5b7f 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 find_element(JsonValue const& payload); Web::WebDriver::Response find_elements(JsonValue const& payload); Web::WebDriver::Response find_element_from_element(JsonValue const& payload, StringView parameter_element_id); Web::WebDriver::Response find_elements_from_element(JsonValue const& payload, StringView parameter_element_id);