Browse Source

LibWeb: Implement WebDriver action extraction

There's a lot of parsing involved in action extraction. So this patch
implements only that - actually executing actions will be a future
patch.
Timothy Flynn 10 months ago
parent
commit
6cf7d07a98

+ 1 - 0
Userland/Libraries/LibWeb/CMakeLists.txt

@@ -730,6 +730,7 @@ set(SOURCES
     WebAudio/OfflineAudioContext.cpp
     WebAudio/OscillatorNode.cpp
     WebAudio/PeriodicWave.cpp
+    WebDriver/Actions.cpp
     WebDriver/Capabilities.cpp
     WebDriver/Client.cpp
     WebDriver/Contexts.cpp

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

@@ -0,0 +1,549 @@
+/*
+ * Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <AK/Enumerate.h>
+#include <AK/GenericShorthands.h>
+#include <AK/JsonArray.h>
+#include <AK/JsonObject.h>
+#include <AK/JsonValue.h>
+#include <AK/Math.h>
+#include <AK/Utf8View.h>
+#include <LibWeb/WebDriver/Actions.h>
+#include <LibWeb/WebDriver/ElementReference.h>
+#include <LibWeb/WebDriver/InputState.h>
+#include <LibWeb/WebDriver/Properties.h>
+
+namespace Web::WebDriver {
+
+static Optional<ActionObject::Subtype> action_object_subtype_from_string(StringView action_subtype)
+{
+    if (action_subtype == "pause"sv)
+        return ActionObject::Subtype::Pause;
+    if (action_subtype == "keyUp"sv)
+        return ActionObject::Subtype::KeyUp;
+    if (action_subtype == "keyDown"sv)
+        return ActionObject::Subtype::KeyDown;
+    if (action_subtype == "pointerUp"sv)
+        return ActionObject::Subtype::PointerUp;
+    if (action_subtype == "pointerDown"sv)
+        return ActionObject::Subtype::PointerDown;
+    if (action_subtype == "pointerMove"sv)
+        return ActionObject::Subtype::PointerMove;
+    if (action_subtype == "pointerCancel"sv)
+        return ActionObject::Subtype::PointerCancel;
+    if (action_subtype == "scroll"sv)
+        return ActionObject::Subtype::Scroll;
+    return {};
+}
+
+static ActionObject::Fields fields_from_subtype(ActionObject::Subtype subtype)
+{
+    switch (subtype) {
+    case ActionObject::Subtype::Pause:
+        return ActionObject::PauseFields {};
+
+    case ActionObject::Subtype::KeyUp:
+    case ActionObject::Subtype::KeyDown:
+        return ActionObject::KeyFields {};
+
+    case ActionObject::Subtype::PointerUp:
+    case ActionObject::Subtype::PointerDown:
+        return ActionObject::PointerUpDownFields {};
+
+    case ActionObject::Subtype::PointerMove:
+        return ActionObject::PointerMoveFields {};
+
+    case ActionObject::Subtype::PointerCancel:
+        return ActionObject::PointerCancelFields {};
+
+    case ActionObject::Subtype::Scroll:
+        return ActionObject::ScrollFields {};
+    }
+
+    VERIFY_NOT_REACHED();
+}
+
+ActionObject::ActionObject(String id, InputSourceType type, Subtype subtype)
+    : id(move(id))
+    , type(type)
+    , subtype(subtype)
+    , fields(fields_from_subtype(subtype))
+{
+}
+
+void ActionObject::set_pointer_type(PointerInputSource::Subtype pointer_type)
+{
+    fields.visit(
+        [&](OneOf<PointerUpDownFields, PointerMoveFields, PointerCancelFields> auto& fields) {
+            fields.pointer_type = pointer_type;
+        },
+        [](auto const&) { VERIFY_NOT_REACHED(); });
+}
+
+static Optional<ActionObject::Origin> determine_origin(ActionsOptions const& actions_options, Optional<JsonValue const&> const& origin)
+{
+    if (!origin.has_value())
+        return ActionObject::OriginType::Viewport;
+
+    if (origin->is_string()) {
+        if (origin->as_string() == "viewport"sv)
+            return ActionObject::OriginType::Viewport;
+        if (origin->as_string() == "pointer"sv)
+            return ActionObject::OriginType::Pointer;
+    }
+
+    if (origin->is_object()) {
+        if (actions_options.is_element_origin(origin->as_object()))
+            return MUST(String::from_byte_string(extract_web_element_reference(origin->as_object())));
+    }
+
+    return {};
+}
+
+// https://w3c.github.io/webdriver/#dfn-process-pointer-parameters
+struct PointerParameters {
+    PointerInputSource::Subtype pointer_type { PointerInputSource::Subtype::Mouse };
+};
+static ErrorOr<PointerParameters, WebDriver::Error> process_pointer_parameters(Optional<JsonValue const&> const& parameters_data)
+{
+    // 1. Let parameters be the default pointer parameters.
+    PointerParameters parameters;
+
+    // 2. If parameters data is undefined, return success with data parameters.
+    if (!parameters_data.has_value())
+        return parameters;
+
+    // 3. If parameters data is not an Object, return error with error code invalid argument.
+    if (!parameters_data->is_object())
+        return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, "Property 'parameters' is not an Object");
+
+    // 4. Let pointer type be the result of getting a property named "pointerType" from parameters data.
+    auto pointer_type = TRY(get_optional_property(parameters_data->as_object(), "pointerType"sv));
+
+    // 5. If pointer type is not undefined:
+    if (pointer_type.has_value()) {
+        // 1. If pointer type does not have one of the values "mouse", "pen", or "touch", return error with error code
+        //    invalid argument.
+        auto parsed_pointer_type = pointer_input_source_subtype_from_string(*pointer_type);
+
+        if (!parsed_pointer_type.has_value())
+            return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, "Property 'pointerType' must be one of 'mouse', 'pen', or 'touch'");
+
+        // 2. Set the pointerType property of parameters to pointer type.
+        parameters.pointer_type = *parsed_pointer_type;
+    }
+
+    // 6. Return success with data parameters.
+    return parameters;
+}
+
+// https://w3c.github.io/webdriver/#dfn-process-a-pause-action
+static ErrorOr<void, WebDriver::Error> process_pause_action(JsonObject const& action_item, ActionObject& action)
+{
+    // 1. Let duration be the result of getting the property "duration" from action item.
+    // 2. If duration is not undefined and duration is not an Integer greater than or equal to 0, return error with error code invalid argument.
+    // 3. Set the duration property of action to duration.
+    if (auto duration = TRY(get_optional_property_with_limits<i64>(action_item, "duration"sv, 0, {})); duration.has_value())
+        action.pause_fields().duration = AK::Duration::from_milliseconds(*duration);
+
+    // 4. Return success with data action.
+    return {};
+}
+
+// https://w3c.github.io/webdriver/#dfn-process-a-null-action
+static ErrorOr<ActionObject, WebDriver::Error> process_null_action(String id, JsonObject const& action_item)
+{
+    // 1. Let subtype be the result of getting a property named "type" from action item.
+    auto subtype = action_object_subtype_from_string(TRY(get_property(action_item, "type"sv)));
+
+    // 2. If subtype is not "pause", return error with error code invalid argument.
+    if (subtype != ActionObject::Subtype::Pause)
+        return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, "Property 'type' must be 'pause'");
+
+    // 3. Let action be an action object constructed with arguments id, "none", and subtype.
+    ActionObject action { move(id), InputSourceType::None, *subtype };
+
+    // 4. Let result be the result of trying to process a pause action with arguments action item and action.
+    TRY(process_pause_action(action_item, action));
+
+    // 5. Return result.
+    return action;
+}
+
+// https://w3c.github.io/webdriver/#dfn-process-a-key-action
+static ErrorOr<ActionObject, WebDriver::Error> process_key_action(String id, JsonObject const& action_item)
+{
+    using enum ActionObject::Subtype;
+
+    // 1. Let subtype be the result of getting a property named "type" from action item.
+    auto subtype = action_object_subtype_from_string(TRY(get_property(action_item, "type"sv)));
+
+    // 2. If subtype is not one of the values "keyUp", "keyDown", or "pause", return an error with error code invalid argument.
+    if (!first_is_one_of(subtype, KeyUp, KeyDown, Pause))
+        return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, "Property 'type' must be one of 'keyUp', 'keyDown', or 'pause'");
+
+    // 3. Let action be an action object constructed with arguments id, "key", and subtype.
+    ActionObject action { move(id), InputSourceType::Key, *subtype };
+
+    // 4. If subtype is "pause", let result be the result of trying to process a pause action with arguments action
+    //    item and action, and return result.
+    if (subtype == Pause) {
+        TRY(process_pause_action(action_item, action));
+        return action;
+    }
+
+    // 5. Let key be the result of getting a property named "value" from action item.
+    auto key = TRY(get_property(action_item, "value"sv));
+
+    // 6. If key is not a String containing a single unicode code point [or grapheme cluster?] return error with error
+    //    code invalid argument.
+    if (Utf8View { key }.length() != 1) {
+        // FIXME: The spec seems undecided on whether grapheme clusters should be supported. Update this step to check
+        //        for graphemes if we end up needing to support them.
+        return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, "Property 'value' must be a single code point");
+    }
+
+    // 7. Set the value property on action to key.
+    action.key_fields().value = MUST(String::from_byte_string(key));
+
+    // 8. Return success with data action.
+    return action;
+}
+
+// Common steps between:
+// https://w3c.github.io/webdriver/#dfn-process-a-pointer-up-or-pointer-down-action
+// https://w3c.github.io/webdriver/#dfn-process-a-pointer-move-action
+static ErrorOr<void, WebDriver::Error> process_pointer_action_common(JsonObject const& action_item, ActionObject::PointerFields& fields)
+{
+    // 4. Let width be the result of getting the property width from action item.
+    // 5. If width is not undefined and width is not a Number greater than or equal to 0 return error with error code invalid argument.
+    // 6. Set the width property of action to width.
+    fields.width = TRY(get_optional_property_with_limits<double>(action_item, "width"sv, 0.0, {}));
+
+    // 7. Let height be the result of getting the property height from action item.
+    // 8. If height is not undefined and height is not a Number greater than or equal to 0 return error with error code invalid argument.
+    // 9. Set the height property of action to height.
+    fields.height = TRY(get_optional_property_with_limits<double>(action_item, "height"sv, 0.0, {}));
+
+    // 10. Let pressure be the result of getting the property pressure from action item.
+    // 11. If pressure is not undefined and pressure is not a Number greater than or equal to 0 and less than or equal to 1 return error with error code invalid argument.
+    // 12. Set the pressure property of action to pressure.
+    fields.pressure = TRY(get_optional_property_with_limits<double>(action_item, "pressure"sv, 0.0, 1.0));
+
+    // 13. Let tangentialPressure be the result of getting the property tangentialPressure from action item.
+    // 14. If tangentialPressure is not undefined and tangentialPressure is not a Number greater than or equal to -1 and less than or equal to 1 return error with error code invalid argument.
+    // 15. Set the tangentialPressure property of action to tangentialPressure.
+    fields.tangential_pressure = TRY(get_optional_property_with_limits<double>(action_item, "tangentialPressure"sv, -1.0, 1.0));
+
+    // 16. Let tiltX be the result of getting the property tiltX from action item.
+    // 17. If tiltX is not undefined and tiltX is not an Integer greater than or equal to -90 and less than or equal to 90 return error with error code invalid argument.
+    // 18. Set the tiltX property of action to tiltX.
+    fields.tilt_x = TRY(get_optional_property_with_limits<i32>(action_item, "tiltX"sv, -90, 90));
+
+    // 19. Let tiltY be the result of getting the property tiltY from action item.
+    // 20. If tiltY is not undefined and tiltY is not an Integer greater than or equal to -90 and less than or equal to 90 return error with error code invalid argument.
+    // 21. Set the tiltY property of action to tiltY.
+    fields.tilt_y = TRY(get_optional_property_with_limits<i32>(action_item, "tiltY"sv, -90, 90));
+
+    // 22. Let twist be the result of getting the property twist from action item.
+    // 23. If twist is not undefined and twist is not an Integer greater than or equal to 0 and less than or equal to 359 return error with error code invalid argument.
+    // 24. Set the twist property of action to twist.
+    fields.twist = TRY(get_optional_property_with_limits<u32>(action_item, "twist"sv, 0, 359));
+
+    // 25. Let altitudeAngle be the result of getting the property altitudeAngle from action item.
+    // 26. If altitudeAngle is not undefined and altitudeAngle is not a Number greater than or equal to 0 and less than or equal to π/2 return error with error code invalid argument.
+    // 27. Set the altitudeAngle property of action to altitudeAngle.
+    fields.altitude_angle = TRY(get_optional_property_with_limits<double>(action_item, "altitudeAngle"sv, 0.0, AK::Pi<double> / 2.0));
+
+    // 28. Let azimuthAngle be the result of getting the property azimuthAngle from action item.
+    // 29. If azimuthAngle is not undefined and azimuthAngle is not a Number greater than or equal to 0 and less than or equal to 2π return error with error code invalid argument.
+    // 30. Set the azimuthAngle property of action to azimuthAngle.
+    fields.azimuth_angle = TRY(get_optional_property_with_limits<double>(action_item, "azimuthAngle"sv, 0.0, AK::Pi<double> * 2.0));
+
+    // 31. Return success with data null.
+    return {};
+}
+
+// https://w3c.github.io/webdriver/#dfn-process-a-pointer-up-or-pointer-down-action
+static ErrorOr<void, WebDriver::Error> process_pointer_up_or_down_action(JsonObject const& action_item, ActionObject& action)
+{
+    auto& fields = action.pointer_up_down_fields();
+
+    // 1. Let button be the result of getting the property button from action item.
+    // 2. If button is not an Integer greater than or equal to 0 return error with error code invalid argument.
+    // 3. Set the button property of action to button.
+    fields.button = UIEvents::button_code_to_mouse_button(TRY(get_property_with_limits<i16>(action_item, "button"sv, 0, {})));
+
+    return process_pointer_action_common(action_item, fields);
+}
+
+// https://w3c.github.io/webdriver/#dfn-process-a-pointer-move-action
+static ErrorOr<void, WebDriver::Error> process_pointer_move_action(JsonObject const& action_item, ActionObject& action, ActionsOptions const& actions_options)
+{
+    auto& fields = action.pointer_move_fields();
+
+    // 1. Let duration be the result of getting the property duration from action item.
+    // 2. If duration is not undefined and duration is not an Integer greater than or equal to 0, return error with error code invalid argument.
+    // 3. Set the duration property of action to duration.
+    if (auto duration = TRY(get_optional_property_with_limits<i64>(action_item, "duration"sv, 0, {})); duration.has_value())
+        fields.duration = AK::Duration::from_milliseconds(*duration);
+
+    // 4. Let origin be the result of getting the property origin from action item.
+    // 5. If origin is undefined let origin equal "viewport".
+    auto origin = determine_origin(actions_options, action_item.get("origin"sv));
+
+    // 6. If origin is not equal to "viewport" or "pointer", and actions options is element origin steps given origin
+    //    return false, return error with error code invalid argument.
+    if (!origin.has_value())
+        return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, "Property 'origin' must be 'viewport', 'pointer', or an element origin");
+
+    // 7. Set the origin property of action to origin.
+    fields.origin = origin.release_value();
+
+    // 8. Let x be the result of getting the property x from action item.
+    // 9. If x is not an Integer, return error with error code invalid argument.
+    // 10. Set the x property of action to x.
+    fields.position.set_x(TRY(get_property<i32>(action_item, "x"sv)));
+
+    // 11. Let y be the result of getting the property y from action item.
+    // 12. If y is not an Integer, return error with error code invalid argument.
+    // 13. Set the y property of action to y.
+    fields.position.set_y(TRY(get_property<i32>(action_item, "y"sv)));
+
+    return process_pointer_action_common(action_item, fields);
+}
+
+// https://w3c.github.io/webdriver/#dfn-process-a-pointer-action
+static ErrorOr<ActionObject, WebDriver::Error> process_pointer_action(String id, PointerParameters const& parameters, JsonObject const& action_item, ActionsOptions const& actions_options)
+{
+    using enum ActionObject::Subtype;
+
+    // 1. Let subtype be the result of getting a property named "type" from action item.
+    auto subtype = action_object_subtype_from_string(TRY(get_property(action_item, "type"sv)));
+
+    // 2. If subtype is not one of the values "pause", "pointerUp", "pointerDown", "pointerMove", or "pointerCancel", return an error with error code invalid argument.
+    if (!first_is_one_of(subtype, Pause, PointerUp, PointerDown, PointerMove, PointerCancel))
+        return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, "Property 'type' must be one of 'pause', 'pointerUp', 'pointerDown', 'pointerMove', or 'pointerCancel'");
+
+    // 3. Let action be an action object constructed with arguments id, "pointer", and subtype.
+    ActionObject action { move(id), InputSourceType::Pointer, *subtype };
+
+    // 4. If subtype is "pause", let result be the result of trying to process a pause action with arguments action
+    //    item, action, and actions options, and return result.
+    if (subtype == Pause) {
+        TRY(process_pause_action(action_item, action));
+        return action;
+    }
+
+    // 5. Set the pointerType property of action equal to the pointerType property of parameters.
+    action.set_pointer_type(parameters.pointer_type);
+
+    // 6. If subtype is "pointerUp" or "pointerDown", process a pointer up or pointer down action with arguments action
+    //    item and action. If doing so results in an error, return that error.
+    if (subtype == PointerUp || subtype == PointerDown) {
+        TRY(process_pointer_up_or_down_action(action_item, action));
+    }
+
+    // 7. If subtype is "pointerMove" process a pointer move action with arguments action item, action, and actions
+    //    options. If doing so results in an error, return that error.
+    else if (subtype == PointerMove) {
+        TRY(process_pointer_move_action(action_item, action, actions_options));
+    }
+
+    // 8. If subtype is "pointerCancel" process a pointer cancel action. If doing so results in an error, return that error.
+    else if (subtype == PointerCancel) {
+        // FIXME: There are no spec steps to "process a pointer cancel action" yet.
+        return WebDriver::Error::from_code(WebDriver::ErrorCode::UnsupportedOperation, "pointerCancel events not implemented"sv);
+    }
+
+    // 9. Return success with data action.
+    return action;
+}
+
+// https://w3c.github.io/webdriver/#dfn-process-a-wheel-action
+static ErrorOr<ActionObject, WebDriver::Error> process_wheel_action(String id, JsonObject const& action_item, ActionsOptions const& actions_options)
+{
+    using enum ActionObject::Subtype;
+
+    // 1. Let subtype be the result of getting a property named "type" from action item.
+    auto subtype = action_object_subtype_from_string(TRY(get_property(action_item, "type"sv)));
+
+    // 2. If subtype is not the value "pause", or "scroll", return an error with error code invalid argument.
+    if (!first_is_one_of(subtype, Pause, Scroll))
+        return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, "Property 'type' must be one of 'pause' or 'scroll'");
+
+    // 3. Let action be an action object constructed with arguments id, "wheel", and subtype.
+    ActionObject action { move(id), InputSourceType::Wheel, *subtype };
+
+    // 4. If subtype is "pause", let result be the result of trying to process a pause action with arguments action
+    //    item and action, and return result.
+    if (subtype == Pause) {
+        TRY(process_pause_action(action_item, action));
+        return action;
+    }
+
+    auto& fields = action.scroll_fields();
+
+    // 5. Let duration be the result of getting a property named "duration" from action item.
+    // 6. If duration is not undefined and duration is not an Integer greater than or equal to 0, return error with error code invalid argument.
+    // 7. Set the duration property of action to duration.
+    if (auto duration = TRY(get_optional_property_with_limits<i64>(action_item, "duration"sv, 0, {})); duration.has_value())
+        fields.duration = AK::Duration::from_milliseconds(*duration);
+
+    // 8. Let origin be the result of getting the property origin from action item.
+    // 9. If origin is undefined let origin equal "viewport".
+    auto origin = determine_origin(actions_options, action_item.get("origin"sv));
+
+    // 10. If origin is not equal to "viewport", or actions options' is element origin steps given origin return false,
+    //     return error with error code invalid argument.
+    if (!origin.has_value() || origin == ActionObject::OriginType::Pointer)
+        return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, "Property 'origin' must be 'viewport' or an element origin");
+
+    // 11. Set the origin property of action to origin.
+    fields.origin = origin.release_value();
+
+    // 12. Let x be the result of getting the property x from action item.
+    // 13. If x is not an Integer, return error with error code invalid argument.
+    // 14. Set the x property of action to x.
+    fields.x = TRY(get_property<i64>(action_item, "x"sv));
+
+    // 15. Let y be the result of getting the property y from action item.
+    // 16. If y is not an Integer, return error with error code invalid argument.
+    // 17. Set the y property of action to y.
+    fields.y = TRY(get_property<i64>(action_item, "y"sv));
+
+    // 18. Let deltaX be the result of getting the property deltaX from action item.
+    // 19. If deltaX is not an Integer, return error with error code invalid argument.
+    // 20. Set the deltaX property of action to deltaX.
+    fields.delta_x = TRY(get_property<i64>(action_item, "deltaX"sv));
+
+    // 21. Let deltaY be the result of getting the property deltaY from action item.
+    // 22. If deltaY is not an Integer, return error with error code invalid argument.
+    // 23. Set the deltaY property of action to deltaY.
+    fields.delta_y = TRY(get_property<i64>(action_item, "deltaY"sv));
+
+    // 24. Return success with data action.
+    return action;
+}
+
+// https://w3c.github.io/webdriver/#dfn-process-an-input-source-action-sequence
+static ErrorOr<Vector<ActionObject>, WebDriver::Error> process_input_source_action_sequence(InputState& input_state, JsonValue const& action_sequence, ActionsOptions const& actions_options)
+{
+    // 1. Let type be the result of getting a property named "type" from action sequence.
+    auto type = input_source_type_from_string(TRY(get_property(action_sequence, "type"sv)));
+
+    // 2. If type is not "key", "pointer", "wheel", or "none", return an error with error code invalid argument.
+    if (!type.has_value())
+        return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, "Property 'type' must be one of 'key', 'pointer', 'wheel', or 'none'");
+
+    // 3. Let id be the result of getting the property "id" from action sequence.
+    // 4. If id is undefined or is not a String, return error with error code invalid argument.
+    auto const id = MUST(String::from_byte_string(TRY(get_property(action_sequence, "id"sv))));
+
+    // 5. If type is equal to "pointer", let parameters data be the result of getting the property "parameters" from
+    //    action sequence. Then let parameters be the result of trying to process pointer parameters with argument
+    //    parameters data.
+    Optional<PointerParameters> parameters;
+    Optional<PointerInputSource::Subtype> subtype;
+
+    if (type == InputSourceType::Pointer) {
+        parameters = TRY(process_pointer_parameters(action_sequence.as_object().get("parameters"sv)));
+        subtype = parameters->pointer_type;
+    }
+
+    // 6. Let source be the result of trying to get or create an input source given input state, type and id.
+    auto const& source = *TRY(get_or_create_input_source(input_state, *type, id, subtype));
+
+    // 7. If parameters is not undefined, then if its pointerType property is not equal to source's subtype property,
+    //    return an error with error code invalid argument.
+    if (auto const* pointer_input_source = source.get_pointer<PointerInputSource>(); pointer_input_source && parameters.has_value()) {
+        if (parameters->pointer_type != pointer_input_source->subtype)
+            return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, "Invalid 'pointerType' property");
+    }
+
+    // 8. Let action items be the result of getting a property named "actions" from action sequence.
+    // 9. If action items is not an Array, return error with error code invalid argument.
+    auto const& action_items = *TRY(get_property<JsonArray const*>(action_sequence, "actions"sv));
+
+    // 10. Let actions be a new list.
+    Vector<ActionObject> actions;
+
+    // 11. For each action item in action items:
+    TRY(action_items.try_for_each([&](auto const& action_item) -> ErrorOr<void, WebDriver::Error> {
+        // 1. If action item is not an Object return error with error code invalid argument.
+        if (!action_item.is_object())
+            return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, "Property 'actions' item is not an Object");
+
+        auto action = TRY([&]() {
+            switch (*type) {
+            // 2. If type is "none" let action be the result of trying to process a null action with parameters id, and
+            //    action item.
+            case InputSourceType::None:
+                return process_null_action(id, action_item.as_object());
+
+            // 3. Otherwise, if type is "key" let action be the result of trying to process a key action with parameters
+            //    id, and action item.
+            case InputSourceType::Key:
+                return process_key_action(id, action_item.as_object());
+
+            // 4. Otherwise, if type is "pointer" let action be the result of trying to process a pointer action with
+            //    parameters id, parameters, action item, and actions options.
+            case InputSourceType::Pointer:
+                return process_pointer_action(id, *parameters, action_item.as_object(), actions_options);
+
+            // 5. Otherwise, if type is "wheel" let action be the result of trying to process a wheel action with
+            //    parameters id, and action item, and actions options.
+            case InputSourceType::Wheel:
+                return process_wheel_action(id, action_item.as_object(), actions_options);
+            }
+
+            VERIFY_NOT_REACHED();
+        }());
+
+        // 6. Append action to actions.
+        actions.append(move(action));
+        return {};
+    }));
+
+    // 12. Return success with data actions.
+    return actions;
+}
+
+// https://w3c.github.io/webdriver/#dfn-extract-an-action-sequence
+ErrorOr<Vector<Vector<ActionObject>>, WebDriver::Error> extract_an_action_sequence(InputState& input_state, JsonValue const& parameters, ActionsOptions const& actions_options)
+{
+    // 1. Let actions be the result of getting a property named "actions" from parameters.
+    // 2. If actions is undefined or is not an Array, return error with error code invalid argument.
+    auto const& actions = *TRY(get_property<JsonArray const*>(parameters, "actions"sv));
+
+    // 3. Let actions by tick be an empty List.
+    Vector<Vector<ActionObject>> actions_by_tick;
+
+    // 4. For each value action sequence corresponding to an indexed property in actions:
+    TRY(actions.try_for_each([&](auto const& action_sequence) -> ErrorOr<void, WebDriver::Error> {
+        // 1. Let source actions be the result of trying to process an input source action sequence given input state,
+        //    action sequence, and actions options.
+        auto source_actions = TRY(process_input_source_action_sequence(input_state, action_sequence, actions_options));
+
+        // 2. For each action in source actions:
+        for (auto [i, action] : enumerate(source_actions)) {
+            // 1. Let i be the zero-based index of action in source actions.
+            // 2. If the length of actions by tick is less than i + 1, append a new List to actions by tick.
+            if (actions_by_tick.size() < (i + 1))
+                actions_by_tick.resize(i + 1);
+
+            // 3. Append action to the List at index i in actions by tick.
+            actions_by_tick[i].append(move(action));
+        }
+
+        return {};
+    }));
+
+    // 5. Return success with data actions by tick.
+    return actions_by_tick;
+}
+
+}

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

@@ -0,0 +1,125 @@
+/*
+ * Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/Function.h>
+#include <AK/Optional.h>
+#include <AK/String.h>
+#include <AK/Time.h>
+#include <AK/Variant.h>
+#include <AK/Vector.h>
+#include <LibJS/Heap/GCPtr.h>
+#include <LibWeb/Forward.h>
+#include <LibWeb/PixelUnits.h>
+#include <LibWeb/UIEvents/MouseButton.h>
+#include <LibWeb/WebDriver/Error.h>
+#include <LibWeb/WebDriver/InputSource.h>
+
+namespace Web::WebDriver {
+
+// https://w3c.github.io/webdriver/#dfn-action-object
+struct ActionObject {
+    enum class Subtype {
+        Pause,
+        KeyUp,
+        KeyDown,
+        PointerUp,
+        PointerDown,
+        PointerMove,
+        PointerCancel,
+        Scroll,
+    };
+
+    enum class OriginType {
+        Viewport,
+        Pointer,
+    };
+    using Origin = Variant<OriginType, String>;
+
+    struct PauseFields {
+        Optional<AK::Duration> duration;
+    };
+
+    struct KeyFields {
+        String value;
+    };
+
+    struct PointerFields {
+        PointerInputSource::Subtype pointer_type { PointerInputSource::Subtype::Mouse };
+        Optional<double> width;
+        Optional<double> height;
+        Optional<double> pressure;
+        Optional<double> tangential_pressure;
+        Optional<i32> tilt_x;
+        Optional<i32> tilt_y;
+        Optional<u32> twist;
+        Optional<double> altitude_angle;
+        Optional<double> azimuth_angle;
+    };
+
+    struct PointerUpDownFields : public PointerFields {
+        UIEvents::MouseButton button { UIEvents::MouseButton::None };
+    };
+
+    struct PointerMoveFields : public PointerFields {
+        Optional<AK::Duration> duration;
+        Origin origin { OriginType::Viewport };
+        CSSPixelPoint position;
+    };
+
+    struct PointerCancelFields {
+        PointerInputSource::Subtype pointer_type { PointerInputSource::Subtype::Mouse };
+    };
+
+    struct ScrollFields {
+        Origin origin { OriginType::Viewport };
+        Optional<AK::Duration> duration;
+        i64 x { 0 };
+        i64 y { 0 };
+        i64 delta_x { 0 };
+        i64 delta_y { 0 };
+    };
+
+    ActionObject(String id, InputSourceType type, Subtype subtype);
+
+    void set_pointer_type(PointerInputSource::Subtype);
+
+    PauseFields& pause_fields() { return fields.get<PauseFields>(); }
+    PauseFields const& pause_fields() const { return fields.get<PauseFields>(); }
+
+    KeyFields& key_fields() { return fields.get<KeyFields>(); }
+    KeyFields const& key_fields() const { return fields.get<KeyFields>(); }
+
+    PointerUpDownFields& pointer_up_down_fields() { return fields.get<PointerUpDownFields>(); }
+    PointerUpDownFields const& pointer_up_down_fields() const { return fields.get<PointerUpDownFields>(); }
+
+    PointerMoveFields& pointer_move_fields() { return fields.get<PointerMoveFields>(); }
+    PointerMoveFields const& pointer_move_fields() const { return fields.get<PointerMoveFields>(); }
+
+    PointerCancelFields& pointer_cancel_fields() { return fields.get<PointerCancelFields>(); }
+    PointerCancelFields const& pointer_cancel_fields() const { return fields.get<PointerCancelFields>(); }
+
+    ScrollFields& scroll_fields() { return fields.get<ScrollFields>(); }
+    ScrollFields const& scroll_fields() const { return fields.get<ScrollFields>(); }
+
+    String id;
+    InputSourceType type;
+    Subtype subtype;
+
+    using Fields = Variant<PauseFields, KeyFields, PointerUpDownFields, PointerMoveFields, PointerCancelFields, ScrollFields>;
+    Fields fields;
+};
+
+// https://w3c.github.io/webdriver/#dfn-actions-options
+struct ActionsOptions {
+    Function<bool(JsonObject const&)> is_element_origin;
+    Function<ErrorOr<JS::NonnullGCPtr<DOM::Element>, WebDriver::Error>(StringView)> get_element_origin;
+};
+
+ErrorOr<Vector<Vector<ActionObject>>, WebDriver::Error> extract_an_action_sequence(InputState&, JsonValue const&, ActionsOptions const&);
+
+}