diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index c90f1540dca..1f107b1212c 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -737,6 +737,8 @@ set(SOURCES WebDriver/ElementReference.cpp WebDriver/Error.cpp WebDriver/ExecuteScript.cpp + WebDriver/InputSource.cpp + WebDriver/InputState.cpp WebDriver/Response.cpp WebDriver/Screenshot.cpp WebDriver/TimeoutsConfiguration.cpp diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h index a3875d41bf1..eff5ee9cc41 100644 --- a/Userland/Libraries/LibWeb/Forward.h +++ b/Userland/Libraries/LibWeb/Forward.h @@ -786,6 +786,11 @@ class ExceptionOr; using Promise = JS::PromiseCapability; } +namespace Web::WebDriver { +struct ActionObject; +struct InputState; +}; + namespace Web::WebSockets { class WebSocket; } diff --git a/Userland/Libraries/LibWeb/WebDriver/InputSource.cpp b/Userland/Libraries/LibWeb/WebDriver/InputSource.cpp new file mode 100644 index 00000000000..b0357d0a4f5 --- /dev/null +++ b/Userland/Libraries/LibWeb/WebDriver/InputSource.cpp @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace Web::WebDriver { + +static InputSourceType input_source_type(InputSource const& input_source) +{ + return input_source.visit( + [](NullInputSource const&) { return InputSourceType::None; }, + [](KeyInputSource const&) { return InputSourceType::Key; }, + [](PointerInputSource const&) { return InputSourceType::Pointer; }, + [](WheelInputSource const&) { return InputSourceType::Wheel; }); +} + +// https://w3c.github.io/webdriver/#dfn-get-a-pointer-id +static u32 get_pointer_id(InputState const& input_state, PointerInputSource::Subtype subtype) +{ + // 1. Let minimum id be 0 if subtype is "mouse", or 2 otherwise. + auto minimum_id = subtype == PointerInputSource::Subtype::Mouse ? 0u : 2u; + + // 2. Let pointer ids be an empty set. + HashTable pointer_ids; + + // 3. Let sources be the result of getting the values with input state's input state map. + // 4. For each source in sources: + for (auto const& source : input_state.input_state_map) { + // 1. If source is a pointer input source, append source's pointerId to pointer ids. + if (auto const* pointer_input_source = source.value.get_pointer()) + pointer_ids.set(pointer_input_source->pointer_id); + } + + // 5. Return the smallest integer that is greater than or equal to minimum id and that is not contained in pointer ids. + for (u32 integer = minimum_id; integer < NumericLimits::max(); ++integer) { + if (!pointer_ids.contains(integer)) + return integer; + } + + VERIFY_NOT_REACHED(); +} + +// https://w3c.github.io/webdriver/#dfn-create-a-pointer-input-source +PointerInputSource::PointerInputSource(InputState const& input_state, PointerInputSource::Subtype subtype) + : subtype(subtype) + , pointer_id(get_pointer_id(input_state, subtype)) +{ + // To create a pointer input source object given input state, and subtype, return a new pointer input source with + // subtype set to subtype, pointerId set to get a pointer id with input state and subtype, and the other items set + // to their default values. +} + +UIEvents::KeyModifier GlobalKeyState::modifiers() const +{ + auto modifiers = UIEvents::KeyModifier::Mod_None; + + if (ctrl_key) + modifiers |= UIEvents::KeyModifier::Mod_Ctrl; + if (shift_key) + modifiers |= UIEvents::KeyModifier::Mod_Shift; + if (alt_key) + modifiers |= UIEvents::KeyModifier::Mod_Alt; + if (meta_key) + modifiers |= UIEvents::KeyModifier::Mod_Super; + + return modifiers; +} + +Optional input_source_type_from_string(StringView input_source_type) +{ + if (input_source_type == "none"sv) + return InputSourceType::None; + if (input_source_type == "key"sv) + return InputSourceType::Key; + if (input_source_type == "pointer"sv) + return InputSourceType::Pointer; + if (input_source_type == "wheel"sv) + return InputSourceType::Wheel; + return {}; +} + +Optional pointer_input_source_subtype_from_string(StringView pointer_type) +{ + if (pointer_type == "mouse"sv) + return PointerInputSource::Subtype::Mouse; + if (pointer_type == "pen"sv) + return PointerInputSource::Subtype::Pen; + if (pointer_type == "touch"sv) + return PointerInputSource::Subtype::Touch; + return {}; +} + +// https://w3c.github.io/webdriver/#dfn-create-an-input-source +InputSource create_input_source(InputState const& input_state, InputSourceType type, Optional subtype) +{ + // Run the substeps matching the first matching value of type: + switch (type) { + // "none" + case InputSourceType::None: + // Let source be the result of create a null input source. + return NullInputSource {}; + + // "key" + case InputSourceType::Key: + // Let source be the result of create a key input source. + return KeyInputSource {}; + + // "pointer" + case InputSourceType::Pointer: + // Let source be the result of create a pointer input source with input state and subtype. + return PointerInputSource { input_state, *subtype }; + + // "wheel" + case InputSourceType::Wheel: + // Let source be the result of create a wheel input source. + return WheelInputSource {}; + } + + // Otherwise: + // Return error with error code invalid argument. + + // NOTE: We know this cannot be reached because the only caller will have already thrown an invalid argument error + // if the `type` parameter was not valid. + VERIFY_NOT_REACHED(); +} + +// https://w3c.github.io/webdriver/#dfn-get-an-input-source +Optional get_input_source(InputState& input_state, StringView id) +{ + // 1. Let input state map be input state's input state map. + // 2. If input state map[input id] exists, return input state map[input id]. + // 3. Return undefined. + return input_state.input_state_map.get(id); +} + +// https://w3c.github.io/webdriver/#dfn-get-or-create-an-input-source +ErrorOr get_or_create_input_source(InputState& input_state, InputSourceType type, StringView id, Optional subtype) +{ + // 1. Let source be get an input source with input state and input id. + auto source = get_input_source(input_state, id); + + // 2. If source is not undefined and source's type is not equal to type, or source is a pointer input source, + // return error with error code invalid argument. + if (source.has_value() && input_source_type(*source) != type) { + // FIXME: Spec issue: It does not make sense to check if "source is a pointer input source". This would errantly + // prevent the ability to perform two pointer actions in a row. + // https://github.com/w3c/webdriver/issues/1810 + return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, "Property 'type' does not match existing input source type"); + } + + // 3. If source is undefined, set source to the result of trying to create an input source with input state and type. + if (!source.has_value()) { + // FIXME: Spec issue: The spec doesn't say to add the source to the input state map, but it is explicitly + // expected when we reach the `dispatch tick actions` AO. + // https://github.com/w3c/webdriver/issues/1810 + input_state.input_state_map.set(MUST(String::from_utf8(id)), create_input_source(input_state, type, subtype)); + source = get_input_source(input_state, id); + } + + // 4. Return success with data source. + return &source.value(); +} + +// https://w3c.github.io/webdriver/#dfn-get-the-global-key-state +GlobalKeyState get_global_key_state(InputState const& input_state) +{ + // 1. Let input state map be input state's input state map. + auto const& input_state_map = input_state.input_state_map; + + // 2. Let sources be the result of getting the values with input state map. + + // 3. Let key state be a new global key state with pressed set to an empty set, altKey, ctrlKey, metaKey, and + // shiftKey set to false. + GlobalKeyState key_state {}; + + // 4. For each source in sources: + for (auto const& source : input_state_map) { + // 1. If source is not a key input source, continue to the first step of this loop. + auto const* key_input_source = source.value.get_pointer(); + if (!key_input_source) + continue; + + // 2. Set key state's pressed item to the union of its current value and source's pressed item. + for (auto const& pressed : key_input_source->pressed) + key_state.pressed.set(pressed); + + // 3. If source's alt item is true, set key state's altKey item to true. + key_state.alt_key |= key_input_source->alt; + + // 4. If source's ctrl item is true, set key state's ctrlKey item to true. + key_state.ctrl_key |= key_input_source->ctrl; + + // 5. If source's meta item is true, set key state's metaKey item to true. + key_state.meta_key |= key_input_source->meta; + + // 6. If source's shift item is true, set key state's shiftKey item to true. + key_state.shift_key |= key_input_source->shift; + } + + // 5. Return key state. + return key_state; +} + +} diff --git a/Userland/Libraries/LibWeb/WebDriver/InputSource.h b/Userland/Libraries/LibWeb/WebDriver/InputSource.h new file mode 100644 index 00000000000..cf466759182 --- /dev/null +++ b/Userland/Libraries/LibWeb/WebDriver/InputSource.h @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::WebDriver { + +enum class InputSourceType { + None, + Key, + Pointer, + Wheel, +}; + +// https://w3c.github.io/webdriver/#dfn-null-input-source +struct NullInputSource { +}; + +// https://w3c.github.io/webdriver/#dfn-key-input-source +struct KeyInputSource { + HashTable pressed; + bool alt { false }; + bool ctrl { false }; + bool meta { false }; + bool shift { false }; +}; + +// https://w3c.github.io/webdriver/#dfn-pointer-input-source +struct PointerInputSource { + enum class Subtype { + Mouse, + Pen, + Touch, + }; + + PointerInputSource(InputState const&, Subtype); + + Subtype subtype { Subtype::Mouse }; + u32 pointer_id { 0 }; + UIEvents::MouseButton pressed { UIEvents::MouseButton::None }; + CSSPixelPoint position; +}; + +// https://w3c.github.io/webdriver/#dfn-wheel-input-source +struct WheelInputSource { +}; + +// https://w3c.github.io/webdriver/#dfn-input-source +using InputSource = Variant; + +// https://w3c.github.io/webdriver/#dfn-global-key-state +struct GlobalKeyState { + UIEvents::KeyModifier modifiers() const; + + HashTable pressed; + bool alt_key { false }; + bool ctrl_key { false }; + bool meta_key { false }; + bool shift_key { false }; +}; + +Optional input_source_type_from_string(StringView); +Optional pointer_input_source_subtype_from_string(StringView); + +InputSource create_input_source(InputState const&, InputSourceType, Optional); +Optional get_input_source(InputState&, StringView id); +ErrorOr get_or_create_input_source(InputState&, InputSourceType, StringView id, Optional); + +GlobalKeyState get_global_key_state(InputState const&); + +} diff --git a/Userland/Libraries/LibWeb/WebDriver/InputState.cpp b/Userland/Libraries/LibWeb/WebDriver/InputState.cpp new file mode 100644 index 00000000000..8316ee0490d --- /dev/null +++ b/Userland/Libraries/LibWeb/WebDriver/InputState.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace Web::WebDriver { + +// https://w3c.github.io/webdriver/#dfn-browsing-context-input-state-map +static HashMap, InputState> s_browsing_context_input_state_map; + +InputState::InputState() = default; +InputState::~InputState() = default; + +// https://w3c.github.io/webdriver/#dfn-get-the-input-state +InputState& get_input_state(HTML::BrowsingContext& browsing_context) +{ + // 1. Assert: browsing context is a top-level browsing context. + VERIFY(browsing_context.is_top_level()); + + // 2. Let input state map be session's browsing context input state map. + // 3. If input state map does not contain browsing context, set input state map[browsing context] to create an input state. + auto& input_state = s_browsing_context_input_state_map.ensure(browsing_context); + + // 4. Return input state map[browsing context]. + return input_state; +} + +// https://w3c.github.io/webdriver/#dfn-reset-the-input-state +void reset_input_state(HTML::BrowsingContext& browsing_context) +{ + // 1. Assert: browsing context is a top-level browsing context. + VERIFY(browsing_context.is_top_level()); + + // 2. Let input state map be session's browsing context input state map. + // 3. If input state map[browsing context] exists, then remove input state map[browsing context]. + s_browsing_context_input_state_map.remove(browsing_context); +} + +} diff --git a/Userland/Libraries/LibWeb/WebDriver/InputState.h b/Userland/Libraries/LibWeb/WebDriver/InputState.h new file mode 100644 index 00000000000..1e5d1f1e980 --- /dev/null +++ b/Userland/Libraries/LibWeb/WebDriver/InputState.h @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace Web::WebDriver { + +// https://w3c.github.io/webdriver/#dfn-input-state +struct InputState { + InputState(); + ~InputState(); + + // https://w3c.github.io/webdriver/#dfn-input-state-map + HashMap input_state_map; + + // https://w3c.github.io/webdriver/#dfn-input-cancel-list + Vector input_cancel_list; + + // https://w3c.github.io/webdriver/#dfn-actions-queue + Vector actions_queue; +}; + +InputState& get_input_state(HTML::BrowsingContext&); +void reset_input_state(HTML::BrowsingContext&); + +}