From 8598ed86fe9b07e55655d36fcce7ec1dfdbc4491 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Fri, 11 Oct 2024 09:53:52 -0400 Subject: [PATCH] LibWeb+WebContent: Implement the Element Clear endpoint --- .../LibWeb/WebDriver/ElementReference.cpp | 97 +++++++++++++ .../LibWeb/WebDriver/ElementReference.h | 6 + .../WebContent/WebDriverConnection.cpp | 136 +++++++++++------- 3 files changed, 191 insertions(+), 48 deletions(-) diff --git a/Userland/Libraries/LibWeb/WebDriver/ElementReference.cpp b/Userland/Libraries/LibWeb/WebDriver/ElementReference.cpp index 868b637fc6d..b927d0a61b5 100644 --- a/Userland/Libraries/LibWeb/WebDriver/ElementReference.cpp +++ b/Userland/Libraries/LibWeb/WebDriver/ElementReference.cpp @@ -10,8 +10,13 @@ #include #include #include +#include #include #include +#include +#include +#include +#include #include namespace Web::WebDriver { @@ -125,6 +130,36 @@ bool is_element_stale(Web::DOM::Node const& element) return !element.document().is_active() || !element.is_connected(); } +// https://w3c.github.io/webdriver/#dfn-interactable +bool is_element_interactable(Web::HTML::BrowsingContext const& browsing_context, Web::DOM::Element const& element) +{ + // An interactable element is an element which is either pointer-interactable or keyboard-interactable. + return is_element_keyboard_interactable(element) || is_element_pointer_interactable(browsing_context, element); +} + +// https://w3c.github.io/webdriver/#dfn-pointer-interactable +bool is_element_pointer_interactable(Web::HTML::BrowsingContext const& browsing_context, Web::DOM::Element const& element) +{ + // A pointer-interactable element is defined to be the first element, defined by the paint order found at the center + // point of its rectangle that is inside the viewport, excluding the size of any rendered scrollbars. + auto const* document = browsing_context.active_document(); + if (!document) + return false; + + auto const* paint_root = document->paintable_box(); + if (!paint_root) + return false; + + auto viewport = browsing_context.page().top_level_traversable()->viewport_rect(); + auto center_point = in_view_center_point(element, viewport); + + auto result = paint_root->hit_test(center_point, Painting::HitTestType::TextCursor); + if (!result.has_value()) + return false; + + return result->dom_node() == &element; +} + // https://w3c.github.io/webdriver/#dfn-keyboard-interactable bool is_element_keyboard_interactable(Web::DOM::Element const& element) { @@ -132,6 +167,68 @@ bool is_element_keyboard_interactable(Web::DOM::Element const& element) return element.is_focusable() || is(element) || element.is_document_element(); } +// https://w3c.github.io/webdriver/#dfn-editable +bool is_element_editable(Web::DOM::Element const& element) +{ + // Editable elements are those that can be used for typing and clearing, and they fall into two subcategories: + // "Mutable form control elements" and "Mutable elements". + return is_element_mutable_form_control(element) || is_element_mutable(element); +} + +// https://w3c.github.io/webdriver/#dfn-mutable-element +bool is_element_mutable(Web::DOM::Element const& element) +{ + // Denotes elements that are editing hosts or content editable. + if (!is(element)) + return false; + + auto const& html_element = static_cast(element); + return html_element.is_editable(); +} + +// https://w3c.github.io/webdriver/#dfn-mutable-form-control-element +bool is_element_mutable_form_control(Web::DOM::Element const& element) +{ + // Denotes input elements that are mutable (e.g. that are not read only or disabled) and whose type attribute is + // in one of the following states: + if (is(element)) { + auto const& input_element = static_cast(element); + if (!input_element.is_mutable() || !input_element.enabled()) + return false; + + // Text and Search, URL, Telephone, Email, Password, Date, Month, Week, Time, Local Date and Time, Number, + // Range, Color, File Upload + switch (input_element.type_state()) { + case HTML::HTMLInputElement::TypeAttributeState::Text: + case HTML::HTMLInputElement::TypeAttributeState::Search: + case HTML::HTMLInputElement::TypeAttributeState::URL: + case HTML::HTMLInputElement::TypeAttributeState::Telephone: + case HTML::HTMLInputElement::TypeAttributeState::Email: + case HTML::HTMLInputElement::TypeAttributeState::Password: + case HTML::HTMLInputElement::TypeAttributeState::Date: + case HTML::HTMLInputElement::TypeAttributeState::Month: + case HTML::HTMLInputElement::TypeAttributeState::Week: + case HTML::HTMLInputElement::TypeAttributeState::Time: + case HTML::HTMLInputElement::TypeAttributeState::LocalDateAndTime: + case HTML::HTMLInputElement::TypeAttributeState::Number: + case HTML::HTMLInputElement::TypeAttributeState::Range: + case HTML::HTMLInputElement::TypeAttributeState::Color: + case HTML::HTMLInputElement::TypeAttributeState::FileUpload: + return true; + default: + return false; + } + } + + // And the textarea element. + if (is(element)) { + auto const& text_area = static_cast(element); + return text_area.enabled(); + } + + return false; +} + // https://w3c.github.io/webdriver/#dfn-non-typeable-form-control bool is_element_non_typeable_form_control(Web::DOM::Element const& element) { diff --git a/Userland/Libraries/LibWeb/WebDriver/ElementReference.h b/Userland/Libraries/LibWeb/WebDriver/ElementReference.h index f267f9237f1..04f4b0fec85 100644 --- a/Userland/Libraries/LibWeb/WebDriver/ElementReference.h +++ b/Userland/Libraries/LibWeb/WebDriver/ElementReference.h @@ -25,7 +25,13 @@ ErrorOr, Web::WebDriver::Error> get_web_elem ErrorOr get_known_connected_element(StringView element_id); bool is_element_stale(Web::DOM::Node const& element); +bool is_element_interactable(Web::HTML::BrowsingContext const&, Web::DOM::Element const&); +bool is_element_pointer_interactable(Web::HTML::BrowsingContext const&, Web::DOM::Element const&); bool is_element_keyboard_interactable(Web::DOM::Element const&); + +bool is_element_editable(Web::DOM::Element const&); +bool is_element_mutable(Web::DOM::Element const&); +bool is_element_mutable_form_control(Web::DOM::Element const&); bool is_element_non_typeable_form_control(Web::DOM::Element const&); ByteString get_or_create_a_shadow_root_reference(Web::DOM::ShadowRoot const& shadow_root); diff --git a/Userland/Services/WebContent/WebDriverConnection.cpp b/Userland/Services/WebContent/WebDriverConnection.cpp index 51c4c434604..4b81678acf7 100644 --- a/Userland/Services/WebContent/WebDriverConnection.cpp +++ b/Userland/Services/WebContent/WebDriverConnection.cpp @@ -1475,40 +1475,75 @@ Messages::WebDriverClient::ElementClickResponse WebDriverConnection::element_cli // 12.5.2 Element Clear, https://w3c.github.io/webdriver/#dfn-element-clear Messages::WebDriverClient::ElementClearResponse WebDriverConnection::element_clear(String const& element_id) { - dbgln("FIXME: WebDriverConnection::element_clear({})", element_id); + // https://w3c.github.io/webdriver/#dfn-clear-a-content-editable-element + auto clear_content_editable_element = [&](Web::DOM::Element& element) { + // 1. If element's innerHTML IDL attribute is an empty string do nothing and return. + if (auto result = element.inner_html(); result.is_error() || result.value().is_empty()) + return; - // To clear a content editable element: - { - // FIXME: 1. If element's innerHTML IDL attribute is an empty string do nothing and return. - // FIXME: 2. Run the focusing steps for element. - // FIXME: 3. Set element's innerHTML IDL attribute to an empty string. - // FIXME: 4. Run the unfocusing steps for the element. - } + // 2. Run the focusing steps for element. + Web::HTML::run_focusing_steps(&element); - // To clear a resettable element: - { - // FIXME: 1. Let empty be the result of the first matching condition: - { + // 3. Set element's innerHTML IDL attribute to an empty string. + (void)element.set_inner_html({}); + + // 4. Run the unfocusing steps for the element. + Web::HTML::run_unfocusing_steps(&element); + }; + + // https://w3c.github.io/webdriver/#dfn-clear-a-resettable-element + auto clear_resettable_element = [&](Web::DOM::Element& element) { + VERIFY(is(element)); + auto& form_associated_element = dynamic_cast(element); + + // 1. Let empty be the result of the first matching condition: + auto empty = [&]() { // -> element is an input element whose type attribute is in the File Upload state - { - // True if the list of selected files has a length of 0, and false otherwise. - } - // -> otherwise - { - // True if its value IDL attribute is an empty string, and false otherwise. - } - } - // FIXME: 2. If element is a candidate for constraint validation it satisfies its constraints, and empty is true, abort these substeps. - // FIXME: 3. Invoke the focusing steps for element. - // FIXME: 4. Invoke the clear algorithm for element. - // FIXME: 5. Invoke the unfocusing steps for the element. - } + // True if the list of selected files has a length of 0, and false otherwise + if (is(element)) { + auto& input_element = static_cast(element); + + if (input_element.type_state() == Web::HTML::HTMLInputElement::TypeAttributeState::FileUpload) + return input_element.files()->length() == 0; + } + + // -> otherwise + // True if its value IDL attribute is an empty string, and false otherwise. + return form_associated_element.value().is_empty(); + }(); + + // 2. If element is a candidate for constraint validation it satisfies its constraints, and empty is true, + // abort these substeps. + // FIXME: Implement constraint validation. + if (empty) + return; + + // 3. Invoke the focusing steps for element. + Web::HTML::run_focusing_steps(&element); + + // 4. Invoke the clear algorithm for element. + form_associated_element.clear_algorithm(); + + // 5. Invoke the unfocusing steps for the element. + Web::HTML::run_unfocusing_steps(&element); + }; + + // 1. If session's current browsing context is no longer open, return error with error code no such window. + TRY(ensure_current_browsing_context_is_open()); + + // 2. Try to handle any user prompts with session. + TRY(handle_any_user_prompts()); + + // 3. Let element be the result of trying to get a known element with session and element id. + auto* element = TRY(Web::WebDriver::get_known_connected_element(element_id)); + + // 4. If element is not editable, return an error with error code invalid element state. + if (!Web::WebDriver::is_element_editable(*element)) + return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidElementState, "Element is not editable"sv); + + // 5. Scroll into view the element. + TRY(scroll_element_into_view(*element)); - // FIXME: 1. If session's current browsing context is no longer open, return error with error code no such window. - // FIXME: 2. Try to handle any user prompts with session. - // FIXME: 3. Let element be the result of trying to get a known element with session and element id. - // FIXME: 4. If element is not editable, return an error with error code invalid element state. - // FIXME: 5. Scroll into view the element. // FIXME: 6. Let timeout be session's session timeouts' implicit wait timeout. // FIXME: 7. Let timer be a new timer. // FIXME: 8. If timeout is not null: @@ -1516,25 +1551,30 @@ Messages::WebDriverClient::ElementClearResponse WebDriverConnection::element_cle // FIXME: 1. Start the timer with timer and timeout. } // FIXME: 9. Wait for element to become interactable, or timer's timeout fired flag to be set, whichever occurs first. - // FIXME: 10. If element is not interactable, return error with error code element not interactable. - // FIXME: 11. Run the substeps of the first matching statement: - { - // -> element is a mutable form control element - { - // Invoke the steps to clear a resettable element. - } - // -> element is a mutable element - { - // Invoke the steps to clear a content editable element. - } - // -> otherwise - { - // Return error with error code invalid element state. - } - } - // FIXME: 12. Return success with data null. - return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::UnsupportedOperation, "element clear not implemented"sv); + // 10. If element is not interactable, return error with error code element not interactable. + if (!Web::WebDriver::is_element_interactable(current_browsing_context(), *element)) + return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::ElementNotInteractable, "Element is not interactable"sv); + + // 11. Run the substeps of the first matching statement: + // -> element is a mutable form control element + if (Web::WebDriver::is_element_mutable_form_control(*element)) { + // Invoke the steps to clear a resettable element. + clear_resettable_element(*element); + } + // -> element is a mutable element + else if (Web::WebDriver::is_element_mutable(*element)) { + // Invoke the steps to clear a content editable element. + clear_content_editable_element(*element); + } + // -> otherwise + else { + // Return error with error code invalid element state. + return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidElementState, "Element is not editable"sv); + } + + // 12. Return success with data null. + return JsonValue {}; } // 12.5.3 Element Send Keys, https://w3c.github.io/webdriver/#dfn-element-send-keys