فهرست منبع

LibWeb+WebDriver+WebContent: Implement the Element Click endpoint

Timothy Flynn 9 ماه پیش
والد
کامیت
5aa50bff8b

+ 12 - 0
Userland/Libraries/LibWeb/WebDriver/Actions.cpp

@@ -1014,4 +1014,16 @@ ErrorOr<void, WebDriver::Error> dispatch_tick_actions(InputState& input_state, R
     return {};
 }
 
+// https://w3c.github.io/webdriver/#dfn-dispatch-a-list-of-actions
+JS::NonnullGCPtr<JS::Cell> dispatch_list_of_actions(InputState& input_state, Vector<ActionObject> actions, HTML::BrowsingContext& browsing_context, ActionsOptions actions_options, OnActionsComplete on_complete)
+{
+    // 1. Let tick actions be the list «actions»
+    // 2. Let actions by tick be the list «tick actions».
+    Vector<Vector<ActionObject>> actions_by_tick;
+    actions_by_tick.append(move(actions));
+
+    // 3. Return the result of dispatch actions with input state, actions by tick, browsing context, and actions options.
+    return dispatch_actions(input_state, move(actions_by_tick), browsing_context, move(actions_options), on_complete);
+}
+
 }

+ 1 - 0
Userland/Libraries/LibWeb/WebDriver/Actions.h

@@ -128,5 +128,6 @@ ErrorOr<Vector<Vector<ActionObject>>, WebDriver::Error> extract_an_action_sequen
 
 JS::NonnullGCPtr<JS::Cell> dispatch_actions(InputState&, Vector<Vector<ActionObject>>, HTML::BrowsingContext&, ActionsOptions, OnActionsComplete);
 ErrorOr<void, WebDriver::Error> dispatch_tick_actions(InputState&, ReadonlySpan<ActionObject>, AK::Duration, HTML::BrowsingContext&, ActionsOptions const&);
+JS::NonnullGCPtr<JS::Cell> dispatch_list_of_actions(InputState&, Vector<ActionObject>, HTML::BrowsingContext&, ActionsOptions, OnActionsComplete);
 
 }

+ 27 - 0
Userland/Libraries/LibWeb/WebDriver/InputSource.cpp

@@ -4,6 +4,7 @@
  * SPDX-License-Identifier: BSD-2-Clause
  */
 
+#include <LibWeb/WebDriver/Actions.h>
 #include <LibWeb/WebDriver/InputSource.h>
 #include <LibWeb/WebDriver/InputState.h>
 
@@ -128,6 +129,32 @@ InputSource create_input_source(InputState const& input_state, InputSourceType t
     VERIFY_NOT_REACHED();
 }
 
+// https://w3c.github.io/webdriver/#dfn-remove-an-input-source
+void add_input_source(InputState& input_state, String id, InputSource source)
+{
+    // 1. Let input state map be input state's input state map.
+    // 2. Set input state map[input id] to source.
+    input_state.input_state_map.set(move(id), move(source));
+}
+
+// https://w3c.github.io/webdriver/#dfn-remove-an-input-source
+void remove_input_source(InputState& input_state, StringView id)
+{
+    // 1. Assert: None of the items in input state's input cancel list has id equal to input id.
+    // FIXME: Spec issue: This assertion cannot be correct. For example, when Element Click is executed, the initial
+    //        pointer down action will append a pointer up action to the input cancel list, and the input cancel list
+    //        is never subsequently cleared. So instead of performing this assertion, we remove any action from the
+    //        input cancel list with the provided input ID.
+    //        https://github.com/w3c/webdriver/issues/1809
+    input_state.input_cancel_list.remove_all_matching([&](ActionObject const& action) {
+        return action.id == id;
+    });
+
+    // 2. Let input state map be input state's input state map.
+    // 3. Remove input state map[input id].
+    input_state.input_state_map.remove(id);
+}
+
 // https://w3c.github.io/webdriver/#dfn-get-an-input-source
 Optional<InputSource&> get_input_source(InputState& input_state, StringView id)
 {

+ 2 - 0
Userland/Libraries/LibWeb/WebDriver/InputSource.h

@@ -77,6 +77,8 @@ Optional<InputSourceType> input_source_type_from_string(StringView);
 Optional<PointerInputSource::Subtype> pointer_input_source_subtype_from_string(StringView);
 
 InputSource create_input_source(InputState const&, InputSourceType, Optional<PointerInputSource::Subtype>);
+void add_input_source(InputState&, String id, InputSource);
+void remove_input_source(InputState&, StringView id);
 Optional<InputSource&> get_input_source(InputState&, StringView id);
 ErrorOr<InputSource*, WebDriver::Error> get_or_create_input_source(InputState&, InputSourceType, StringView id, Optional<PointerInputSource::Subtype>);
 

+ 84 - 25
Userland/Services/WebContent/WebDriverConnection.cpp

@@ -19,6 +19,7 @@
 #include <LibWeb/CSS/StyleProperties.h>
 #include <LibWeb/Cookie/Cookie.h>
 #include <LibWeb/Cookie/ParsedCookie.h>
+#include <LibWeb/Crypto/Crypto.h>
 #include <LibWeb/DOM/Document.h>
 #include <LibWeb/DOM/Element.h>
 #include <LibWeb/DOM/Event.h>
@@ -1318,6 +1319,24 @@ Messages::WebDriverClient::ElementClickResponse WebDriverConnection::element_cli
     // FIXME: 6. If element’s container is still not in view, return error with error code element not interactable.
     // FIXME: 7. If element’s container is obscured by another element, return error with error code element click intercepted.
 
+    auto on_complete = JS::create_heap_function(current_browsing_context().heap(), [this](Web::WebDriver::Response result) {
+        // 9. Wait until the user agent event loop has spun enough times to process the DOM events generated by the
+        //    previous step.
+        m_action_executor = nullptr;
+
+        // FIXME: 10. Perform implementation-defined steps to allow any navigations triggered by the click to start.
+
+        // 11. Try to wait for navigation to complete.
+        if (auto navigation_result = wait_for_navigation_to_complete(); navigation_result.is_error()) {
+            async_actions_performed(navigation_result.release_error());
+            return;
+        }
+
+        // FIXME: 12. Try to run the post-navigation checks.
+
+        async_actions_performed(move(result));
+    });
+
     // 8. Matching on element:
     // -> option element
     if (is<Web::HTML::HTMLOptionElement>(*element)) {
@@ -1361,42 +1380,82 @@ Messages::WebDriverClient::ElementClickResponse WebDriverConnection::element_cli
                 fire_an_event<Web::DOM::Event>(Web::HTML::EventNames::change, parent_node);
             }
         }
+
         // 7. Fire a mouseUp event at parent node.
         fire_an_event<Web::UIEvents::MouseEvent>(Web::UIEvents::EventNames::mouseup, parent_node);
 
         // 8. Fire a click event at parent node.
         fire_an_event<Web::UIEvents::MouseEvent>(Web::UIEvents::EventNames::click, parent_node);
+
+        Web::HTML::queue_a_task(Web::HTML::Task::Source::Unspecified, nullptr, nullptr, JS::create_heap_function(current_browsing_context().heap(), [on_complete]() {
+            on_complete->function()(JsonValue {});
+        }));
     }
     // -> Otherwise
     else {
-        dbgln("FIXME: WebDriverConnection::element_click({})", element->class_name());
+        // 1. Let input state be the result of get the input state given current session and current top-level
+        //    browsing context.
+        auto& input_state = Web::WebDriver::get_input_state(*current_top_level_browsing_context());
+
+        // 2. Let actions options be a new actions options with the is element origin steps set to represents a web
+        //    element, and the get element origin steps set to get a WebElement origin.
+        Web::WebDriver::ActionsOptions actions_options {
+            .is_element_origin = &Web::WebDriver::represents_a_web_element,
+            .get_element_origin = &Web::WebDriver::get_web_element_origin,
+        };
 
-        // FIXME: 1. Let input state be the result of get the input state given current session and current top-level browsing context.
-        // FIXME: 2. Let actions options be a new actions options with the is element origin steps set to represents a web element, and the get element origin steps set to get a WebElement origin.
-        // FIXME: 3. Let input id be a the result of generating a UUID.
-        // FIXME: 4. Let source be the result of create an input source with input state, and "pointer".
-        // FIXME: 5. Add an input source with input state, input id and source.
-        // FIXME: 6. Let click point be the element’s in-view center point.
-        // FIXME: 7. Let pointer move action be an action object constructed with arguments input id, "pointer", and "pointerMove".
-        // FIXME: 8. Set a property x to 0 on pointer move action.
-        // FIXME: 9. Set a property y to 0 on pointer move action.
-        // FIXME: 10. Set a property origin to element on pointer move action.
-        // FIXME: 11. Let pointer down action be an action object constructed with arguments input id, "pointer", and "pointerDown".
-        // FIXME: 12. Set a property button to 0 on pointer down action.
-        // FIXME: 13. Let pointer up action be an action object constructed with arguments input id, "mouse", and "pointerUp" as arguments.
-        // FIXME: 14. Set a property button to 0 on pointer up action.
-        // FIXME: 15. Let actions be the list «pointer move action, pointer down action, pointer move action».
-        // FIXME: 16. Dispatch a list of actions with input state, actions, current browsing context, and actions options.
-        // FIXME: 17. Remove an input source with input state and input id.
-    }
+        // 3. Let input id be a the result of generating a UUID.
+        auto input_id = MUST(Web::Crypto::generate_random_uuid());
 
-    // FIXME: 9. Wait until the user agent event loop has spun enough times to process the DOM events generated by the previous step.
-    // FIXME: 10. Perform implementation-defined steps to allow any navigations triggered by the click to start.
-    // FIXME: 11. Try to wait for navigation to complete.
-    // FIXME: 12. Try to run the post-navigation checks.
-    // FIXME: 13. Return success with data null.
+        // 4. Let source be the result of create an input source with input state, and "pointer".
+        auto source = Web::WebDriver::create_input_source(input_state, Web::WebDriver::InputSourceType::Pointer, Web::WebDriver::PointerInputSource::Subtype::Mouse);
 
-    return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::UnsupportedOperation, "Click not implemented"sv);
+        // 5. Add an input source with input state, input id and source.
+        Web::WebDriver::add_input_source(input_state, input_id, move(source));
+
+        // 6. Let click point be the element’s in-view center point.
+        // FIXME: Spec-issue: This parameter is unused. Note that it would not correct to set the mouse move action
+        //        position to this click point. The [0,0] specified below is ultimately interpreted as an offset from
+        //        the element's center position.
+        //        https://github.com/w3c/webdriver/issues/1563
+
+        // 7. Let pointer move action be an action object constructed with arguments input id, "pointer", and "pointerMove".
+        Web::WebDriver::ActionObject pointer_move_action { input_id, Web::WebDriver::InputSourceType::Pointer, Web::WebDriver::ActionObject::Subtype::PointerMove };
+
+        // 8. Set a property x to 0 on pointer move action.
+        // 9. Set a property y to 0 on pointer move action.
+        pointer_move_action.pointer_move_fields().position = { 0, 0 };
+
+        // 10. Set a property origin to element on pointer move action.
+        auto origin = Web::WebDriver::get_or_create_a_web_element_reference(*element);
+        pointer_move_action.pointer_move_fields().origin = MUST(String::from_byte_string(origin));
+
+        // 11. Let pointer down action be an action object constructed with arguments input id, "pointer", and "pointerDown".
+        Web::WebDriver::ActionObject pointer_down_action { input_id, Web::WebDriver::InputSourceType::Pointer, Web::WebDriver::ActionObject::Subtype::PointerDown };
+
+        // 12. Set a property button to 0 on pointer down action.
+        pointer_down_action.pointer_up_down_fields().button = Web::UIEvents::button_code_to_mouse_button(0);
+
+        // 13. Let pointer up action be an action object constructed with arguments input id, "pointer", and "pointerUp" as arguments.
+        Web::WebDriver::ActionObject pointer_up_action { input_id, Web::WebDriver::InputSourceType::Pointer, Web::WebDriver::ActionObject::Subtype::PointerUp };
+
+        // 14. Set a property button to 0 on pointer up action.
+        pointer_up_action.pointer_up_down_fields().button = Web::UIEvents::button_code_to_mouse_button(0);
+
+        // 15. Let actions be the list «pointer move action, pointer down action, pointer up action».
+        Vector actions { move(pointer_move_action), move(pointer_down_action), move(pointer_up_action) };
+
+        // 16. Dispatch a list of actions with input state, actions, current browsing context, and actions options.
+        m_action_executor = Web::WebDriver::dispatch_list_of_actions(input_state, move(actions), current_browsing_context(), move(actions_options), JS::create_heap_function(current_browsing_context().heap(), [on_complete, &input_state, input_id = move(input_id)](Web::WebDriver::Response result) {
+            // 17. Remove an input source with input state and input id.
+            Web::WebDriver::remove_input_source(input_state, input_id);
+
+            on_complete->function()(move(result));
+        }));
+    }
+
+    // 13. Return success with data null.
+    return JsonValue {};
 }
 
 // 12.5.2 Element Clear, https://w3c.github.io/webdriver/#dfn-element-clear

+ 1 - 1
Userland/Services/WebDriver/Client.cpp

@@ -588,7 +588,7 @@ Web::WebDriver::Response Client::element_click(Web::WebDriver::Parameters parame
 {
     dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/element/<element_id>/click");
     auto session = TRY(find_session_with_id(parameters[0]));
-    return session->web_content_connection().element_click(move(parameters[1]));
+    return session->element_click(move(parameters[1]));
 }
 
 // 12.5.2 Element Clear, https://w3c.github.io/webdriver/#dfn-element-clear

+ 18 - 0
Userland/Services/WebDriver/Session.cpp

@@ -201,6 +201,24 @@ Web::WebDriver::Response Session::execute_script(JsonValue payload, ScriptMode m
     return response.release_value();
 }
 
+Web::WebDriver::Response Session::element_click(String element_id) const
+{
+    ScopeGuard guard { [&]() { web_content_connection().on_actions_performed = nullptr; } };
+
+    Optional<Web::WebDriver::Response> response;
+    web_content_connection().on_actions_performed = [&](auto result) {
+        response = move(result);
+    };
+
+    TRY(web_content_connection().element_click(move(element_id)));
+
+    Core::EventLoop::current().spin_until([&]() {
+        return response.has_value();
+    });
+
+    return response.release_value();
+}
+
 Web::WebDriver::Response Session::perform_actions(JsonValue payload) const
 {
     ScopeGuard guard { [&]() { web_content_connection().on_actions_performed = nullptr; } };

+ 1 - 0
Userland/Services/WebDriver/Session.h

@@ -61,6 +61,7 @@ public:
     };
     Web::WebDriver::Response execute_script(JsonValue, ScriptMode) const;
 
+    Web::WebDriver::Response element_click(String) const;
     Web::WebDriver::Response perform_actions(JsonValue) const;
 
 private: