diff --git a/Userland/Libraries/LibWeb/WebDriver/Actions.cpp b/Userland/Libraries/LibWeb/WebDriver/Actions.cpp index f9c8f22b16..cd264025f7 100644 --- a/Userland/Libraries/LibWeb/WebDriver/Actions.cpp +++ b/Userland/Libraries/LibWeb/WebDriver/Actions.cpp @@ -1014,4 +1014,16 @@ ErrorOr dispatch_tick_actions(InputState& input_state, R return {}; } +// https://w3c.github.io/webdriver/#dfn-dispatch-a-list-of-actions +JS::NonnullGCPtr dispatch_list_of_actions(InputState& input_state, Vector 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> 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); +} + } diff --git a/Userland/Libraries/LibWeb/WebDriver/Actions.h b/Userland/Libraries/LibWeb/WebDriver/Actions.h index 2853117564..2f2d0ff12e 100644 --- a/Userland/Libraries/LibWeb/WebDriver/Actions.h +++ b/Userland/Libraries/LibWeb/WebDriver/Actions.h @@ -128,5 +128,6 @@ ErrorOr>, WebDriver::Error> extract_an_action_sequen JS::NonnullGCPtr dispatch_actions(InputState&, Vector>, HTML::BrowsingContext&, ActionsOptions, OnActionsComplete); ErrorOr dispatch_tick_actions(InputState&, ReadonlySpan, AK::Duration, HTML::BrowsingContext&, ActionsOptions const&); +JS::NonnullGCPtr dispatch_list_of_actions(InputState&, Vector, HTML::BrowsingContext&, ActionsOptions, OnActionsComplete); } diff --git a/Userland/Libraries/LibWeb/WebDriver/InputSource.cpp b/Userland/Libraries/LibWeb/WebDriver/InputSource.cpp index b0357d0a4f..5506b988b0 100644 --- a/Userland/Libraries/LibWeb/WebDriver/InputSource.cpp +++ b/Userland/Libraries/LibWeb/WebDriver/InputSource.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include @@ -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 get_input_source(InputState& input_state, StringView id) { diff --git a/Userland/Libraries/LibWeb/WebDriver/InputSource.h b/Userland/Libraries/LibWeb/WebDriver/InputSource.h index cf46675918..97a7df8d94 100644 --- a/Userland/Libraries/LibWeb/WebDriver/InputSource.h +++ b/Userland/Libraries/LibWeb/WebDriver/InputSource.h @@ -77,6 +77,8 @@ Optional input_source_type_from_string(StringView); Optional pointer_input_source_subtype_from_string(StringView); InputSource create_input_source(InputState const&, InputSourceType, Optional); +void add_input_source(InputState&, String id, InputSource); +void remove_input_source(InputState&, StringView id); Optional get_input_source(InputState&, StringView id); ErrorOr get_or_create_input_source(InputState&, InputSourceType, StringView id, Optional); diff --git a/Userland/Services/WebContent/WebDriverConnection.cpp b/Userland/Services/WebContent/WebDriverConnection.cpp index 87ac26f3ef..df8103fdc8 100644 --- a/Userland/Services/WebContent/WebDriverConnection.cpp +++ b/Userland/Services/WebContent/WebDriverConnection.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -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(*element)) { @@ -1361,42 +1380,82 @@ Messages::WebDriverClient::ElementClickResponse WebDriverConnection::element_cli fire_an_event(Web::HTML::EventNames::change, parent_node); } } + // 7. Fire a mouseUp event at parent node. fire_an_event(Web::UIEvents::EventNames::mouseup, parent_node); // 8. Fire a click event at parent node. fire_an_event(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()); - // 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. + // 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, + }; + + // 3. Let input id be a the result of generating a UUID. + auto input_id = MUST(Web::Crypto::generate_random_uuid()); + + // 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); + + // 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)); + })); } - // 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. - - return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::UnsupportedOperation, "Click not implemented"sv); + // 13. Return success with data null. + return JsonValue {}; } // 12.5.2 Element Clear, https://w3c.github.io/webdriver/#dfn-element-clear diff --git a/Userland/Services/WebDriver/Client.cpp b/Userland/Services/WebDriver/Client.cpp index 9934f0f814..ea576f1cd6 100644 --- a/Userland/Services/WebDriver/Client.cpp +++ b/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//element//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 diff --git a/Userland/Services/WebDriver/Session.cpp b/Userland/Services/WebDriver/Session.cpp index daf94d9ee4..e9cca25808 100644 --- a/Userland/Services/WebDriver/Session.cpp +++ b/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 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; } }; diff --git a/Userland/Services/WebDriver/Session.h b/Userland/Services/WebDriver/Session.h index 846d8f3d38..9219c0b689 100644 --- a/Userland/Services/WebDriver/Session.h +++ b/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: