LibWeb: Begin implementing the WebDriver Input State and Input Source

These concepts are rather tightly coupled. This implements the types and
AOs needed to handle the Perform Actions endoint.
This commit is contained in:
Timothy Flynn 2024-09-26 13:17:18 -04:00 committed by Andreas Kling
parent 94c243acd6
commit 0c98c7637e
Notes: github-actions[bot] 2024-10-01 09:03:37 +00:00
6 changed files with 379 additions and 0 deletions

View file

@ -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

View file

@ -786,6 +786,11 @@ class ExceptionOr;
using Promise = JS::PromiseCapability;
}
namespace Web::WebDriver {
struct ActionObject;
struct InputState;
};
namespace Web::WebSockets {
class WebSocket;
}

View file

@ -0,0 +1,208 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/WebDriver/InputSource.h>
#include <LibWeb/WebDriver/InputState.h>
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<u32> 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<PointerInputSource>())
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<u32>::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<InputSourceType> 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<PointerInputSource::Subtype> 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<PointerInputSource::Subtype> 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<InputSource&> 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<InputSource*, WebDriver::Error> get_or_create_input_source(InputState& input_state, InputSourceType type, StringView id, Optional<PointerInputSource::Subtype> 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<KeyInputSource>();
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;
}
}

View file

@ -0,0 +1,85 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/HashTable.h>
#include <AK/Optional.h>
#include <AK/String.h>
#include <AK/Variant.h>
#include <AK/Vector.h>
#include <LibWeb/Forward.h>
#include <LibWeb/PixelUnits.h>
#include <LibWeb/UIEvents/KeyCode.h>
#include <LibWeb/UIEvents/MouseButton.h>
#include <LibWeb/WebDriver/Error.h>
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<String> 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<NullInputSource, KeyInputSource, PointerInputSource, WheelInputSource>;
// https://w3c.github.io/webdriver/#dfn-global-key-state
struct GlobalKeyState {
UIEvents::KeyModifier modifiers() const;
HashTable<String> pressed;
bool alt_key { false };
bool ctrl_key { false };
bool meta_key { false };
bool shift_key { false };
};
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>);
Optional<InputSource&> get_input_source(InputState&, StringView id);
ErrorOr<InputSource*, WebDriver::Error> get_or_create_input_source(InputState&, InputSourceType, StringView id, Optional<PointerInputSource::Subtype>);
GlobalKeyState get_global_key_state(InputState const&);
}

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/WebDriver/Actions.h>
#include <LibWeb/WebDriver/InputState.h>
namespace Web::WebDriver {
// https://w3c.github.io/webdriver/#dfn-browsing-context-input-state-map
static HashMap<JS::RawGCPtr<HTML::BrowsingContext>, 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);
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/HashMap.h>
#include <AK/String.h>
#include <AK/Vector.h>
#include <LibWeb/Forward.h>
#include <LibWeb/WebDriver/InputSource.h>
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<String, InputSource> input_state_map;
// https://w3c.github.io/webdriver/#dfn-input-cancel-list
Vector<ActionObject> input_cancel_list;
// https://w3c.github.io/webdriver/#dfn-actions-queue
Vector<String> actions_queue;
};
InputState& get_input_state(HTML::BrowsingContext&);
void reset_input_state(HTML::BrowsingContext&);
}