ladybird/Libraries/LibWeb/WebDriver/Actions.cpp

1612 lines
81 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Enumerate.h>
#include <AK/Find.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/Crypto/Crypto.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/PaintableBox.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://pr-preview.s3.amazonaws.com/w3c/webdriver/pull/1847.html#dfn-get-parent-offset
static CSSPixelPoint get_parent_offset(HTML::BrowsingContext const& browsing_context)
{
// 1. Let offsetLeft equal to 0 and offsetTop equal to 0.
CSSPixelPoint offset;
// 2. Let navigable be context's active document's parent.
auto navigable = browsing_context.active_document()->navigable();
if (!navigable)
return offset;
// 3. Let parent navigable be navigable's parent.
auto parent_navigable = navigable->parent();
// 4. If parent navigable is not null:
if (parent_navigable && parent_navigable->active_document() && parent_navigable->active_document()->browsing_context()) {
// 1. Let parent context be parent navigable's document's browsing context.
auto* parent_context = parent_navigable->active_document()->browsing_context();
// 2. Let (parentOffsetLeft, parentOffsetTop) be result of get parent offset of parent context.
auto parent_offset = get_parent_offset(*parent_context);
// 3. Add parentOffsetLeft to offsetLeft.
// 4. Add parentOffsetTop to offsetTop.
offset.translate_by(parent_offset);
// 5. Let containerElement be an element which navigable container presents parent navigable.
auto container_element = parent_navigable->container();
if (!container_element)
return offset;
// 6. Let containerRect be the result of calling getBoundingClientRect() of containerElement.
auto container_rect = container_element->get_bounding_client_rect();
CSSPixels border_left_width = 0;
CSSPixels border_top_width = 0;
if (auto* paintable_box = container_element->paintable_box()) {
// 7. Let borderLeftWidth be the computed border-left-width of containerElement in CSS pixels.
border_left_width = paintable_box->computed_values().border_left().width;
// 8. Let borderTopWidth be the computed border-top-width of containerElement in CSS pixels.
border_top_width = paintable_box->computed_values().border_top().width;
}
// 9. Add containerRect.left + borderLeftWidth to offsetLeft.
// 10. Add containerRect.top + borderTopWidth to offsetTop.
offset.translate_by(
CSSPixels { container_rect->left() } + border_left_width,
CSSPixels { container_rect->top() } + border_top_width);
}
// 5. Return (offsetLeft, offsetTop).
return offset;
}
// https://w3c.github.io/webdriver/#dfn-get-coordinates-relative-to-an-origin
static ErrorOr<CSSPixelPoint, WebDriver::Error> get_coordinates_relative_to_origin(PointerInputSource const& source, HTML::BrowsingContext const& browsing_context, CSSPixelPoint offset, CSSPixelRect viewport, ActionObject::Origin const& origin, ActionsOptions const& actions_options)
{
// FIXME: Spec-issue: If the browsing context is that of a subframe, we need to get its offset relative to the top
// frame, rather than its own frame.
// https://github.com/w3c/webdriver/issues/1840
// 1. Let (parentOffsetLeft, parentOffsetTop) be the result of get parent offset of browsing context.
auto parent_offset = get_parent_offset(browsing_context);
// 2. Run the substeps of the first matching value of origin
auto coordinates = TRY(origin.visit(
[&](ActionObject::OriginType origin) -> ErrorOr<CSSPixelPoint, WebDriver::Error> {
switch (origin) {
// "viewport"
case ActionObject::OriginType::Viewport:
// 1. Let x equal x offset and y equal y offset.
return offset;
// "pointer"
case ActionObject::OriginType::Pointer:
// 1. Let start x be equal to the x property of source.
// 2. Let start y be equal to the y property of source.
// 3. Let x equal start x + x offset and y equal start y + y offset.
return source.position.translated(offset);
}
VERIFY_NOT_REACHED();
},
[&](String const& origin) -> ErrorOr<CSSPixelPoint, WebDriver::Error> {
// Otherwise
// 1. Let element be the result of trying to run actions options' get element origin steps with origin and
// browsing context.
// 2. If element is null, return error with error code no such element.
auto element = TRY(actions_options.get_element_origin(browsing_context, origin));
// 3. Let x element and y element be the result of calculating the in-view center point of element.
auto position = in_view_center_point(element, viewport);
// 4. Let x equal x element + x offset, and y equal y element + y offset.
return position.translated(offset);
}));
// 2. Return (x + parentOffsetLeft, y + parentOffsetTop)
return coordinates.translated(parent_offset);
}
// 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.
Utf8View utf8_key { key };
if (utf8_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. We would also need to update Page's key event
// handlers to support multi-code point events.
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 = *utf8_key.begin();
// 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;
}
// https://w3c.github.io/webdriver/#dfn-computing-the-tick-duration
static AK::Duration compute_tick_duration(ReadonlySpan<ActionObject> tick_actions)
{
// 1. Let max duration be 0.
auto max_duration = AK::Duration::zero();
// 2. For each action object in tick actions:
for (auto const& action_object : tick_actions) {
// 1. let duration be undefined.
Optional<AK::Duration> duration;
// 2. If action object has subtype property set to "pause" or action object has type property set to "pointer"
// and subtype property set to "pointerMove", or action object has type property set to "wheel" and subtype
// property set to "scroll", let duration be equal to the duration property of action object.
action_object.fields.visit(
[&](OneOf<ActionObject::PauseFields, ActionObject::PointerMoveFields, ActionObject::ScrollFields> auto const& fields) {
duration = fields.duration;
},
[](auto const&) {});
// 3. If duration is not undefined, and duration is greater than max duration, let max duration be equal to duration.
if (duration.has_value())
max_duration = max(max_duration, *duration);
}
// 3. Return max duration.
return max_duration;
}
// https://w3c.github.io/webdriver/#dfn-dispatch-a-pause-action
static void dispatch_pause_action()
{
// 1. Return success with data null.
}
// https://w3c.github.io/webdriver/#dfn-normalized-key-value
static String normalized_key_value(u32 key)
{
// The normalized key value for a raw key key is, if key appears in the table below, the string value in the second
// column on the row containing key's unicode code point in the first column, otherwise it is key.
// clang-format off
switch (key) {
case 0xE000: return "Unidentified"_string;
case 0xE001: return "Cancel"_string;
case 0xE002: return "Help"_string;
case 0xE003: return "Backspace"_string;
case 0xE004: return "Tab"_string;
case 0xE005: return "Clear"_string;
case 0xE006: return "Return"_string;
case 0xE007: return "Enter"_string;
case 0xE008: return "Shift"_string;
case 0xE009: return "Control"_string;
case 0xE00A: return "Alt"_string;
case 0xE00B: return "Pause"_string;
case 0xE00C: return "Escape"_string;
case 0xE00D: return " "_string;
case 0xE00E: return "PageUp"_string;
case 0xE00F: return "PageDown"_string;
case 0xE010: return "End"_string;
case 0xE011: return "Home"_string;
case 0xE012: return "ArrowLeft"_string;
case 0xE013: return "ArrowUp"_string;
case 0xE014: return "ArrowRight"_string;
case 0xE015: return "ArrowDown"_string;
case 0xE016: return "Insert"_string;
case 0xE017: return "Delete"_string;
case 0xE018: return ";"_string;
case 0xE019: return "="_string;
case 0xE01A: return "0"_string;
case 0xE01B: return "1"_string;
case 0xE01C: return "2"_string;
case 0xE01D: return "3"_string;
case 0xE01E: return "4"_string;
case 0xE01F: return "5"_string;
case 0xE020: return "6"_string;
case 0xE021: return "7"_string;
case 0xE022: return "8"_string;
case 0xE023: return "9"_string;
case 0xE024: return "*"_string;
case 0xE025: return "+"_string;
case 0xE026: return ","_string;
case 0xE027: return "-"_string;
case 0xE028: return "."_string;
case 0xE029: return "/"_string;
case 0xE031: return "F1"_string;
case 0xE032: return "F2"_string;
case 0xE033: return "F3"_string;
case 0xE034: return "F4"_string;
case 0xE035: return "F5"_string;
case 0xE036: return "F6"_string;
case 0xE037: return "F7"_string;
case 0xE038: return "F8"_string;
case 0xE039: return "F9"_string;
case 0xE03A: return "F10"_string;
case 0xE03B: return "F11"_string;
case 0xE03C: return "F12"_string;
case 0xE03D: return "Meta"_string;
case 0xE040: return "ZenkakuHankaku"_string;
case 0xE050: return "Shift"_string;
case 0xE051: return "Control"_string;
case 0xE052: return "Alt"_string;
case 0xE053: return "Meta"_string;
case 0xE054: return "PageUp"_string;
case 0xE055: return "PageDown"_string;
case 0xE056: return "End"_string;
case 0xE057: return "Home"_string;
case 0xE058: return "ArrowLeft"_string;
case 0xE059: return "ArrowUp"_string;
case 0xE05A: return "ArrowRight"_string;
case 0xE05B: return "ArrowDown"_string;
case 0xE05C: return "Insert"_string;
case 0xE05D: return "Delete"_string;
}
// clang-format on
return String::from_code_point(key);
}
struct KeyCodeData {
u32 key { 0 };
Optional<u32> alternate_key {};
UIEvents::KeyCode code { UIEvents::KeyCode::Key_Invalid };
UIEvents::KeyModifier modifiers { UIEvents::KeyModifier::Mod_None };
};
// https://w3c.github.io/webdriver/#dfn-code
static KeyCodeData key_code_data(u32 code_point)
{
// The code for key is the value in the last column of the following table on the row with key in either the first
// or second column, if any such row exists, otherwise it is undefined.
static auto key_code_data = to_array<KeyCodeData>({
{ '`', '~', UIEvents::KeyCode::Key_Backtick },
{ '\\', '|', UIEvents::KeyCode::Key_Backslash },
{ 0xE003, {}, UIEvents::KeyCode::Key_Backspace },
{ '[', '{', UIEvents::KeyCode::Key_LeftBracket },
{ ']', '}', UIEvents::KeyCode::Key_RightBracket },
{ ',', '<', UIEvents::KeyCode::Key_Comma },
{ '0', ')', UIEvents::KeyCode::Key_0 },
{ '1', '!', UIEvents::KeyCode::Key_1 },
{ '2', '@', UIEvents::KeyCode::Key_2 },
{ '3', '#', UIEvents::KeyCode::Key_3 },
{ '4', '$', UIEvents::KeyCode::Key_4 },
{ '5', '%', UIEvents::KeyCode::Key_5 },
{ '6', '^', UIEvents::KeyCode::Key_6 },
{ '7', '&', UIEvents::KeyCode::Key_7 },
{ '8', '*', UIEvents::KeyCode::Key_8 },
{ '9', '(', UIEvents::KeyCode::Key_9 },
{ '=', '+', UIEvents::KeyCode::Key_Equal },
// FIXME: "IntlBackslash"
{ 'a', 'A', UIEvents::KeyCode::Key_A },
{ 'b', 'B', UIEvents::KeyCode::Key_B },
{ 'c', 'C', UIEvents::KeyCode::Key_C },
{ 'd', 'D', UIEvents::KeyCode::Key_D },
{ 'e', 'E', UIEvents::KeyCode::Key_E },
{ 'f', 'F', UIEvents::KeyCode::Key_F },
{ 'g', 'G', UIEvents::KeyCode::Key_G },
{ 'h', 'H', UIEvents::KeyCode::Key_H },
{ 'i', 'I', UIEvents::KeyCode::Key_I },
{ 'j', 'J', UIEvents::KeyCode::Key_J },
{ 'k', 'K', UIEvents::KeyCode::Key_K },
{ 'l', 'L', UIEvents::KeyCode::Key_L },
{ 'm', 'M', UIEvents::KeyCode::Key_M },
{ 'n', 'N', UIEvents::KeyCode::Key_N },
{ 'o', 'O', UIEvents::KeyCode::Key_O },
{ 'p', 'P', UIEvents::KeyCode::Key_P },
{ 'q', 'Q', UIEvents::KeyCode::Key_Q },
{ 'r', 'R', UIEvents::KeyCode::Key_R },
{ 's', 'S', UIEvents::KeyCode::Key_S },
{ 't', 'T', UIEvents::KeyCode::Key_T },
{ 'u', 'U', UIEvents::KeyCode::Key_U },
{ 'v', 'V', UIEvents::KeyCode::Key_V },
{ 'w', 'W', UIEvents::KeyCode::Key_W },
{ 'x', 'X', UIEvents::KeyCode::Key_X },
{ 'y', 'Y', UIEvents::KeyCode::Key_Y },
{ 'z', 'Z', UIEvents::KeyCode::Key_Z },
{ '-', '_', UIEvents::KeyCode::Key_Minus },
{ '.', '>', UIEvents::KeyCode::Key_Period },
{ '\'', '"', UIEvents::KeyCode::Key_Apostrophe },
{ ';', ':', UIEvents::KeyCode::Key_Semicolon },
{ '/', '?', UIEvents::KeyCode::Key_Slash },
{ ' ', {}, UIEvents::KeyCode::Key_Space },
{ 0xE00A, {}, UIEvents::KeyCode::Key_LeftAlt },
{ 0xE052, {}, UIEvents::KeyCode::Key_RightAlt },
{ 0xE009, {}, UIEvents::KeyCode::Key_LeftControl },
{ 0xE051, {}, UIEvents::KeyCode::Key_RightControl },
{ 0xE006, {}, UIEvents::KeyCode::Key_Return },
{ 0xE00B, {}, UIEvents::KeyCode::Key_PauseBreak },
{ 0xE03D, {}, UIEvents::KeyCode::Key_LeftSuper },
{ 0xE053, {}, UIEvents::KeyCode::Key_RightSuper },
{ 0xE008, {}, UIEvents::KeyCode::Key_LeftShift },
{ 0xE050, {}, UIEvents::KeyCode::Key_RightShift },
{ 0xE00D, {}, UIEvents::KeyCode::Key_Space },
{ 0xE004, {}, UIEvents::KeyCode::Key_Tab },
{ 0xE017, {}, UIEvents::KeyCode::Key_Delete },
{ 0xE010, {}, UIEvents::KeyCode::Key_End },
// FIXME: "Help"
{ 0xE011, {}, UIEvents::KeyCode::Key_Home },
{ 0xE016, {}, UIEvents::KeyCode::Key_Insert },
{ 0xE00F, {}, UIEvents::KeyCode::Key_PageDown },
{ 0xE00E, {}, UIEvents::KeyCode::Key_PageUp },
{ 0xE015, {}, UIEvents::KeyCode::Key_Down },
{ 0xE012, {}, UIEvents::KeyCode::Key_Left },
{ 0xE014, {}, UIEvents::KeyCode::Key_Right },
{ 0xE013, {}, UIEvents::KeyCode::Key_Up },
{ 0xE00C, {}, UIEvents::KeyCode::Key_Escape },
{ 0xE031, {}, UIEvents::KeyCode::Key_F1 },
{ 0xE032, {}, UIEvents::KeyCode::Key_F2 },
{ 0xE033, {}, UIEvents::KeyCode::Key_F3 },
{ 0xE034, {}, UIEvents::KeyCode::Key_F4 },
{ 0xE035, {}, UIEvents::KeyCode::Key_F5 },
{ 0xE036, {}, UIEvents::KeyCode::Key_F6 },
{ 0xE037, {}, UIEvents::KeyCode::Key_F7 },
{ 0xE038, {}, UIEvents::KeyCode::Key_F8 },
{ 0xE039, {}, UIEvents::KeyCode::Key_F9 },
{ 0xE03A, {}, UIEvents::KeyCode::Key_F10 },
{ 0xE03B, {}, UIEvents::KeyCode::Key_F11 },
{ 0xE03C, {}, UIEvents::KeyCode::Key_F12 },
{ 0xE019, {}, UIEvents::KeyCode::Key_Equal, UIEvents::KeyModifier::Mod_Keypad },
{ 0xE01A, 0xE05C, UIEvents::KeyCode::Key_0, UIEvents::KeyModifier::Mod_Keypad },
{ 0xE01B, 0xE056, UIEvents::KeyCode::Key_1, UIEvents::KeyModifier::Mod_Keypad },
{ 0xE01C, 0xE05B, UIEvents::KeyCode::Key_2, UIEvents::KeyModifier::Mod_Keypad },
{ 0xE01D, 0xE055, UIEvents::KeyCode::Key_3, UIEvents::KeyModifier::Mod_Keypad },
{ 0xE01E, 0xE058, UIEvents::KeyCode::Key_4, UIEvents::KeyModifier::Mod_Keypad },
{ 0xE01F, {}, UIEvents::KeyCode::Key_5, UIEvents::KeyModifier::Mod_Keypad },
{ 0xE020, 0xE05A, UIEvents::KeyCode::Key_6, UIEvents::KeyModifier::Mod_Keypad },
{ 0xE021, 0xE057, UIEvents::KeyCode::Key_7, UIEvents::KeyModifier::Mod_Keypad },
{ 0xE022, 0xE059, UIEvents::KeyCode::Key_8, UIEvents::KeyModifier::Mod_Keypad },
{ 0xE023, 0xE054, UIEvents::KeyCode::Key_9, UIEvents::KeyModifier::Mod_Keypad },
{ 0xE025, {}, UIEvents::KeyCode::Key_Plus, UIEvents::KeyModifier::Mod_Keypad },
{ 0xE026, {}, UIEvents::KeyCode::Key_Comma, UIEvents::KeyModifier::Mod_Keypad },
{ 0xE028, 0xE05D, UIEvents::KeyCode::Key_Period, UIEvents::KeyModifier::Mod_Keypad },
{ 0xE029, {}, UIEvents::KeyCode::Key_Slash, UIEvents::KeyModifier::Mod_Keypad },
{ 0xE007, {}, UIEvents::KeyCode::Key_Return, UIEvents::KeyModifier::Mod_Keypad },
{ 0xE024, {}, UIEvents::KeyCode::Key_Asterisk, UIEvents::KeyModifier::Mod_Keypad },
{ 0xE027, {}, UIEvents::KeyCode::Key_Minus, UIEvents::KeyModifier::Mod_Keypad },
});
auto it = find_if(key_code_data.begin(), key_code_data.end(), [&](auto const& data) {
return data.key == code_point || data.alternate_key == code_point;
});
if (it == key_code_data.end())
return { .key = code_point };
return *it;
}
// https://w3c.github.io/webdriver/#dfn-shifted-character
static bool is_shifted_character(u32 code_point)
{
// A shifted character is one that appears in the second column of the following table.
auto code = key_code_data(code_point);
return code.alternate_key == code_point;
}
struct KeyEvent {
u32 code_point { 0 };
UIEvents::KeyModifier modifiers { UIEvents::KeyModifier::Mod_None };
};
static KeyEvent key_code_to_page_event(u32 code_point, UIEvents::KeyModifier modifiers, KeyCodeData const& code)
{
if (code_point >= 0xE000 && code_point <= 0xE05D) {
code_point = [&]() -> u32 {
// clang-format off
switch (code_point) {
case 0xE00D: return ' ';
case 0xE018: return ';';
case 0xE019: return '=';
case 0xE01A: return '0';
case 0xE01B: return '1';
case 0xE01C: return '2';
case 0xE01D: return '3';
case 0xE01E: return '4';
case 0xE01F: return '5';
case 0xE020: return '6';
case 0xE021: return '7';
case 0xE022: return '8';
case 0xE023: return '9';
case 0xE024: return '*';
case 0xE025: return '+';
case 0xE026: return ',';
case 0xE027: return '-';
case 0xE028: return '.';
case 0xE029: return '/';
default: return 0;
}
// clang-format on
}();
}
modifiers |= code.modifiers;
if (has_flag(modifiers, UIEvents::KeyModifier::Mod_Shift))
code_point = code.alternate_key.value_or(code_point);
return { code_point, modifiers };
}
// https://w3c.github.io/webdriver/#dfn-dispatch-a-keydown-action
static ErrorOr<void, WebDriver::Error> dispatch_key_down_action(ActionObject::KeyFields const& action_object, KeyInputSource& source, GlobalKeyState const& global_key_state, HTML::BrowsingContext& browsing_context)
{
// 1. Let raw key be equal to the action object's value property.
auto raw_key = action_object.value;
// 2. Let key be equal to the normalized key value for raw key.
auto key = normalized_key_value(raw_key);
// 3. If the source's pressed property contains key, let repeat be true, otherwise let repeat be false.
bool repeat = source.pressed.contains(key);
// 4. Let code be the code for raw key.
auto code = key_code_data(raw_key);
// 5. Let location be the key location for raw key.
// 6. Let charCode, keyCode and which be the implementation-specific values of the charCode, keyCode and which
// properties appropriate for a key with key key and location location on a 102 key US keyboard, following the
// guidelines in [UI-EVENTS].
auto modifiers = global_key_state.modifiers();
// 7. If key is "Alt", let source's alt property be true.
if (key == "Alt"sv) {
modifiers |= UIEvents::KeyModifier::Mod_Alt;
source.alt = true;
}
// 8. If key is "Shift", let source's shift property be true.
else if (key == "Shift"sv) {
modifiers |= UIEvents::KeyModifier::Mod_Shift;
source.shift = true;
}
// 9. If key is "Control", let source's ctrl property be true.
else if (key == "Control"sv) {
modifiers |= UIEvents::KeyModifier::Mod_Ctrl;
source.ctrl = true;
}
// 10. If key is "Meta", let source's meta property be true.
else if (key == "Meta"sv) {
modifiers |= UIEvents::KeyModifier::Mod_Super;
source.meta = true;
}
// 11. Add key to source's pressed property.
source.pressed.set(key);
// 12. Perform implementation-specific action dispatch steps on browsing context equivalent to pressing a key on the
// keyboard in accordance with the requirements of [UI-EVENTS], and producing the following events, as appropriate,
// with the specified properties. This will always produce events including at least a keyDown event.
auto event = key_code_to_page_event(raw_key, modifiers, code);
browsing_context.page().handle_keydown(code.code, event.modifiers, event.code_point, repeat);
// 13. Return success with data null.
return {};
}
// https://w3c.github.io/webdriver/#dfn-dispatch-a-keyup-action
static ErrorOr<void, WebDriver::Error> dispatch_key_up_action(ActionObject::KeyFields const& action_object, KeyInputSource& source, GlobalKeyState const& global_key_state, HTML::BrowsingContext& browsing_context)
{
// 1. Let raw key be equal to action object's value property.
auto raw_key = action_object.value;
// 2. Let key be equal to the normalized key value for raw key.
auto key = normalized_key_value(raw_key);
// 3. If the source's pressed item does not contain key, return.
if (!source.pressed.contains(key))
return {};
// 4. Let code be the code for raw key.
auto code = key_code_data(raw_key);
// 5. Let location be the key location for raw key.
// 6. Let charCode, keyCode and which be the implementation-specific values of the charCode, keyCode and which
// properties appropriate for a key with key key and location location on a 102 key US keyboard, following the
// guidelines in [UI-EVENTS].
auto modifiers = global_key_state.modifiers();
// 7. If key is "Alt", let source's alt property be false.
if (key == "Alt"sv) {
modifiers &= ~UIEvents::KeyModifier::Mod_Alt;
source.alt = false;
}
// 8. If key is "Shift", let source's shift property be false.
else if (key == "Shift"sv) {
modifiers &= ~UIEvents::KeyModifier::Mod_Shift;
source.shift = false;
}
// 9. If key is "Control", let source's ctrl property be false.
else if (key == "Control"sv) {
modifiers &= ~UIEvents::KeyModifier::Mod_Ctrl;
source.ctrl = false;
}
// 10. If key is "Meta", let source's meta property be false.
else if (key == "Meta"sv) {
modifiers &= ~UIEvents::KeyModifier::Mod_Super;
source.meta = false;
}
// 11. Remove key from sources's pressed property.
source.pressed.remove(key);
// 12. Perform implementation-specific action dispatch steps on browsing context equivalent to releasing a key on the
// keyboard in accordance with the requirements of [UI-EVENTS], and producing at least the following events with
// the specified properties:
auto event = key_code_to_page_event(raw_key, modifiers, code);
browsing_context.page().handle_keyup(code.code, event.modifiers, event.code_point, false);
// 13. Return success with data null.
return {};
}
// https://w3c.github.io/webdriver/#dfn-dispatch-a-pointerdown-action
static ErrorOr<void, WebDriver::Error> dispatch_pointer_down_action(ActionObject::PointerUpDownFields const& action_object, PointerInputSource& source, GlobalKeyState const& global_key_state, HTML::BrowsingContext& browsing_context)
{
// 1. Let pointerType be equal to action object's pointerType property.
auto pointer_type = action_object.pointer_type;
// 2. Let button be equal to action object's button property.
auto button = action_object.button;
// 3. If the source's pressed property contains button return success with data null.
if (has_flag(source.pressed, button))
return {};
// 4. Let x be equal to source's x property.
// 5. Let y be equal to source's y property.
auto position = browsing_context.page().css_to_device_point(source.position);
// 6. Add button to the set corresponding to source's pressed property, and let buttons be the resulting value of
// that property.
auto buttons = (source.pressed |= button);
// 7. Let width be equal to action object's width property.
// 8. Let height be equal to action object's height property.
// 9. Let pressure be equal to action object's pressure property.
// 10. Let tangentialPressure be equal to action object's tangentialPressure property.
// 11. Let tiltX be equal to action object's tiltX property.
// 12. Let tiltY be equal to action object's tiltY property.
// 13. Let twist be equal to action object's twist property.
// 14. Let altitudeAngle be equal to action object's altitudeAngle property.
// 15. Let azimuthAngle be equal to action object's azimuthAngle property.
// 16. Perform implementation-specific action dispatch steps on browsing context equivalent to pressing the button
// numbered button on the pointer with pointerId equal to source's pointerId, having type pointerType at viewport
// x coordinate x, viewport y coordinate y, width, height, pressure, tangentialPressure, tiltX, tiltY, twist,
// altitudeAngle, azimuthAngle, with buttons buttons depressed in accordance with the requirements of [UI-EVENTS]
// and [POINTER-EVENTS]. set ctrlKey, shiftKey, altKey, and metaKey equal to the corresponding items in global
// key state. Type specific properties for the pointer that are not exposed through the webdriver API must be
// set to the default value specified for hardware that doesn't support that property.
switch (pointer_type) {
case PointerInputSource::Subtype::Mouse:
browsing_context.page().handle_mousedown(position, position, button, buttons, global_key_state.modifiers());
break;
case PointerInputSource::Subtype::Pen:
return WebDriver::Error::from_code(WebDriver::ErrorCode::UnsupportedOperation, "Pen events not implemented"sv);
case PointerInputSource::Subtype::Touch:
return WebDriver::Error::from_code(WebDriver::ErrorCode::UnsupportedOperation, "Touch events not implemented"sv);
}
// 17. Return success with data null.
return {};
}
// https://w3c.github.io/webdriver/#dfn-dispatch-a-pointerup-action
static ErrorOr<void, WebDriver::Error> dispatch_pointer_up_action(ActionObject::PointerUpDownFields const& action_object, PointerInputSource& source, GlobalKeyState const& global_key_state, HTML::BrowsingContext& browsing_context)
{
// 1. Let pointerType be equal to action object's pointerType property.
auto pointer_type = action_object.pointer_type;
// 2. Let button be equal to action object's button property.
auto button = action_object.button;
// 3. If the source's pressed property does not contain button, return success with data null.
if (!has_flag(source.pressed, button))
return {};
// 4. Let x be equal to source's x property.
// 5. Let y be equal to source's y property.
auto position = browsing_context.page().css_to_device_point(source.position);
// 6. Remove button from the set corresponding to source's pressed property, and let buttons be the resulting value
// of that property.
auto buttons = (source.pressed &= ~button);
// 7. Perform implementation-specific action dispatch steps on browsing context equivalent to releasing the button
// numbered button on the pointer with pointerId equal to input source's pointerId, having type pointerType at
// viewport x coordinate x, viewport y coordinate y, with buttons buttons depressed, in accordance with the
// requirements of [UI-EVENTS] and [POINTER-EVENTS]. The generated events must set ctrlKey, shiftKey, altKey,
// and metaKey equal to the corresponding items in global key state. Type specific properties for the pointer
// that are not exposed through the webdriver API must be set to the default value specified for hardware that
// doesn't support that property.
switch (pointer_type) {
case PointerInputSource::Subtype::Mouse:
browsing_context.page().handle_mouseup(position, position, button, buttons, global_key_state.modifiers());
break;
case PointerInputSource::Subtype::Pen:
return WebDriver::Error::from_code(WebDriver::ErrorCode::UnsupportedOperation, "Pen events not implemented"sv);
case PointerInputSource::Subtype::Touch:
return WebDriver::Error::from_code(WebDriver::ErrorCode::UnsupportedOperation, "Touch events not implemented"sv);
}
// 8. Return success with data null.
return {};
}
// https://w3c.github.io/webdriver/#dfn-perform-a-pointer-move
static ErrorOr<void, WebDriver::Error> perform_pointer_move(ActionObject::PointerMoveFields const& action_object, PointerInputSource& source, GlobalKeyState const& global_key_state, HTML::BrowsingContext& browsing_context, AK::Duration, CSSPixelPoint coordinates)
{
// FIXME: 1. Let time delta be the time since the beginning of the current tick, measured in milliseconds on a monotonic clock.
// FIXME: 2. Let duration ratio be the ratio of time delta and duration, if duration is greater than 0, or 1 otherwise.
// FIXME: 3. If duration ratio is 1, or close enough to 1 that the implementation will not further subdivide the move action,
// let last be true. Otherwise let last be false.
// FIXME: 4. If last is true, let x equal target x and y equal target y.
// FIXME: 5. Otherwise let x equal an approximation to duration ratio × (target x - start x) + start x, and y equal an
// approximation to duration ratio × (target y - start y) + start y.
// 6. Let current x equal the x property of input state.
// 7. Let current y equal the y property of input state.
auto current = source.position;
// 8. If x is not equal to current x or y is not equal to current y, run the following steps:
if (current != coordinates) {
// 1. Let buttons be equal to input state's buttons property.
auto buttons = source.pressed;
// 2. Perform implementation-specific action dispatch steps on browsing context equivalent to moving the pointer
// with pointerId equal to input source's pointerId, having type pointerType from viewport x coordinate current
// x, viewport y coordinate current y to viewport x coordinate x and viewport y coordinate y, width, height,
// pressure, tangentialPressure, tiltX, tiltY, twist, altitudeAngle, azimuthAngle, with buttons buttons
// depressed, in accordance with the requirements of [UI-EVENTS] and [POINTER-EVENTS]. The generated events
// must set ctrlKey, shiftKey, altKey, and metaKey equal to the corresponding items in global key state. Type
// specific properties for the pointer that are not exposed through the WebDriver API must be set to the
// default value specified for hardware that doesn't support that property. In the case where the pointerType
// is "pen" or "touch", and buttons is empty, this may be a no-op. For a pointer of type "mouse" this will
// always produce events including at least a pointerMove event.
auto position = browsing_context.page().css_to_device_point(coordinates);
switch (action_object.pointer_type) {
case PointerInputSource::Subtype::Mouse:
browsing_context.page().handle_mousemove(position, position, buttons, global_key_state.modifiers());
break;
case PointerInputSource::Subtype::Pen:
return WebDriver::Error::from_code(WebDriver::ErrorCode::UnsupportedOperation, "Pen events not implemented"sv);
case PointerInputSource::Subtype::Touch:
return WebDriver::Error::from_code(WebDriver::ErrorCode::UnsupportedOperation, "Touch events not implemented"sv);
}
// 3. Let input state's x property equal x and y property equal y.
source.position = coordinates;
}
// FIXME: 9. If last is true, return.
// FIXME: 10. Run the following substeps in parallel:
{
// FIXME: 1. Asynchronously wait for an implementation defined amount of time to pass.
// FIXME: 2. Perform a pointer move with arguments input state, duration, start x, start y, target x, target y.
}
return {};
}
// https://w3c.github.io/webdriver/#dfn-dispatch-a-pointermove-action
static ErrorOr<void, WebDriver::Error> dispatch_pointer_move_action(ActionObject::PointerMoveFields const& action_object, PointerInputSource& source, GlobalKeyState const& global_key_state, AK::Duration tick_duration, HTML::BrowsingContext& browsing_context, ActionsOptions const& actions_options)
{
auto viewport = browsing_context.page().top_level_traversable()->viewport_rect();
// 1. Let x offset be equal to the x property of action object.
// 2. Let y offset be equal to the y property of action object.
// 3. Let origin be equal to the origin property of action object.
// 4. Let (x, y) be the result of trying to get coordinates relative to an origin with source, x offset, y offset,
// origin, browsing context, and actions options.
auto coordinates = TRY(get_coordinates_relative_to_origin(source, browsing_context, action_object.position, viewport, action_object.origin, actions_options));
// 5. If x is less than 0 or greater than the width of the viewport in CSS pixels, then return error with error code move target out of bounds.
if (coordinates.x() < 0 || coordinates.x() > viewport.width())
return WebDriver::Error::from_code(WebDriver::ErrorCode::MoveTargetOutOfBounds, ByteString::formatted("Coordinates {} are out of bounds", coordinates));
// 6. If y is less than 0 or greater than the height of the viewport in CSS pixels, then return error with error code move target out of bounds.
if (coordinates.y() < 0 || coordinates.y() > viewport.height())
return WebDriver::Error::from_code(WebDriver::ErrorCode::MoveTargetOutOfBounds, ByteString::formatted("Coordinates {} are out of bounds", coordinates));
// 7. Let duration be equal to action object's duration property if it is not undefined, or tick duration otherwise.
[[maybe_unused]] auto duration = action_object.duration.value_or(tick_duration);
// FIXME: 8. If duration is greater than 0 and inside any implementation-defined bounds, asynchronously wait for an
// implementation defined amount of time to pass.
// 9. Let width be equal to action object's width property.
// 10. Let height be equal to action object's height property.
// 11. Let pressure be equal to action object's pressure property.
// 12. Let tangentialPressure be equal to action object's tangentialPressure property.
// 13. Let tiltX be equal to action object's tiltX property.
// 14. Let tiltY be equal to action object's tiltY property.
// 15. Let twist be equal to action object's twist property.
// 16. Let altitudeAngle be equal to action object's altitudeAngle property.
// 17. Let azimuthAngle be equal to action object's azimuthAngle property.
// 18. Perform a pointer move with arguments source, global key state, duration, start x, start y, x, y, width,
// height, pressure, tangentialPressure, tiltX, tiltY, twist, altitudeAngle, azimuthAngle.
TRY(perform_pointer_move(action_object, source, global_key_state, browsing_context, duration, coordinates));
// 19. Return success with data null.
return {};
}
// https://w3c.github.io/webdriver/#dfn-dispatch-actions-inner
class ActionExecutor final : public JS::Cell {
GC_CELL(ActionExecutor, JS::Cell);
GC_DECLARE_ALLOCATOR(ActionExecutor);
public:
ActionExecutor(InputState& input_state, Vector<Vector<ActionObject>> actions_by_tick, HTML::BrowsingContext& browsing_context, ActionsOptions actions_options, OnActionsComplete on_complete)
: m_browsing_context(browsing_context)
, m_input_state(input_state)
, m_actions_options(move(actions_options))
, m_actions_by_tick(move(actions_by_tick))
, m_on_complete(on_complete)
{
}
// 1. For each item tick actions in actions by tick:
void process_next_tick()
{
if (m_current_tick >= m_actions_by_tick.size()) {
m_on_complete->function()(JsonValue {});
return;
}
auto const& tick_actions = m_actions_by_tick[m_current_tick++];
// 1. Let tick duration be the result of computing the tick duration with argument tick actions.
auto tick_duration = compute_tick_duration(tick_actions);
// 2. Try to dispatch tick actions with input state, tick actions, tick duration, browsing context, and actions options.
if (auto result = dispatch_tick_actions(m_input_state, tick_actions, tick_duration, m_browsing_context, m_actions_options); result.is_error()) {
m_on_complete->function()(result.release_error());
return;
}
// 3. Wait until the following conditions are all met:
// * There are no pending asynchronous waits arising from the last invocation of the dispatch tick actions
// steps.
// * The user agent event loop has spun enough times to process the DOM events generated by the last
// invocation of the dispatch tick actions steps.
// * At least tick duration milliseconds have passed.
// FIXME: We currently do not implement any asynchronous waits. And we assume that Page will generally fire the
// events of interest synchronously. So we simply wait for the tick duration to pass, and then let the
// event loop spin a single time.
m_timer = Core::Timer::create_single_shot(static_cast<int>(tick_duration.to_milliseconds()), [this]() {
m_timer = nullptr;
HTML::queue_a_task(HTML::Task::Source::Unspecified, nullptr, nullptr, GC::create_function(heap(), [this]() {
process_next_tick();
}));
});
m_timer->start();
}
private:
virtual void visit_edges(Cell::Visitor& visitor) override
{
Base::visit_edges(visitor);
visitor.visit(m_browsing_context);
visitor.visit(m_on_complete);
}
GC::Ref<HTML::BrowsingContext> m_browsing_context;
InputState& m_input_state;
ActionsOptions m_actions_options;
Vector<Vector<ActionObject>> m_actions_by_tick;
size_t m_current_tick { 0 };
OnActionsComplete m_on_complete;
RefPtr<Core::Timer> m_timer;
};
GC_DEFINE_ALLOCATOR(ActionExecutor);
// https://w3c.github.io/webdriver/#dfn-wait-for-an-action-queue-token
void wait_for_an_action_queue_token(InputState& input_state)
{
// 1. Let token be a new unique identifier.
auto token = MUST(Crypto::generate_random_uuid());
// 2. Enqueue token in input state's actions queue.
input_state.actions_queue.append(token);
// 3. Wait for token to be the first item in input state's actions queue.
// FIXME: We should probably do this, but our WebDriver currently blocks until a given action is complete anyways,
// so we should never arrive here with an ongoing action (which we verify for now).
VERIFY(input_state.actions_queue.size() == 1);
}
// https://w3c.github.io/webdriver/#dfn-dispatch-actions
GC::Ref<JS::Cell> dispatch_actions(InputState& input_state, Vector<Vector<ActionObject>> actions_by_tick, HTML::BrowsingContext& browsing_context, ActionsOptions actions_options, OnActionsComplete on_complete)
{
// 1. Wait for an action queue token with input state.
wait_for_an_action_queue_token(input_state);
// 2. Let actions result be the result of dispatch actions inner with input state, actions by tick, browsing
// context, and actions options.
auto action_executor = browsing_context.heap().allocate<ActionExecutor>(input_state, move(actions_by_tick), browsing_context, move(actions_options), on_complete);
action_executor->process_next_tick();
// 3. Dequeue input state's actions queue.
// Assert: this returns token
// NOTE: We can't assert because `token` isn't defined here, it exists in `wait_for_an_action_queue_token()` instead.
auto executed_token = input_state.actions_queue.take_first();
// 4. Return actions result.
return action_executor;
}
// https://w3c.github.io/webdriver/#dfn-dispatch-tick-actions
ErrorOr<void, WebDriver::Error> dispatch_tick_actions(InputState& input_state, ReadonlySpan<ActionObject> tick_actions, AK::Duration tick_duration, HTML::BrowsingContext& browsing_context, ActionsOptions const& actions_options)
{
// 1. For each action object in tick actions:
for (auto const& action_object : tick_actions) {
// 1. Let input id be equal to the value of action object's id property.
auto const& input_id = action_object.id;
// 2. Let source type be equal to the value of action object's type property.
// NOTE: We don't actually need this, we can determine the event to fire based on the subtype.
// 3. Let source be the result of get an input source given input state and input id.
auto source = get_input_source(input_state, input_id);
// 4. Assert: source is not undefined.
VERIFY(source.has_value());
// 5. Let global key state be the result of get the global key state with input state.
auto global_key_state = get_global_key_state(input_state);
// 6. Let subtype be action object's subtype.
auto subtype = action_object.subtype;
// 7. Let algorithm be the value of the column dispatch action algorithm from the following table where the
// source type column is source type and the subtype column is equal to subtype.
//
// source type | subtype | Dispatch action algorithm
// ---------------------------------------------------------------
// "none" | "pause" | Dispatch a pause action
// "key" | "pause" | Dispatch a pause action
// "key" | "keyDown" | Dispatch a keyDown action
// "key" | "keyUp" | Dispatch a keyUp action
// "pointer" | "pause" | Dispatch a pause action
// "pointer" | "pointerDown" | Dispatch a pointerDown action
// "pointer" | "pointerUp" | Dispatch a pointerUp action
// "pointer" | "pointerMove" | Dispatch a pointerMove action
// "pointer" | "pointerCancel" | Dispatch a pointerCancel action
// "wheel" | "pause" | Dispatch a pause action
// "wheel" | "scroll" | Dispatch a scroll action
// 8. Try to run algorithm with arguments action object, source, global key state, tick duration, browsing
// context, and actions options.
switch (subtype) {
case ActionObject::Subtype::Pause:
dispatch_pause_action();
break;
case ActionObject::Subtype::KeyDown:
TRY(dispatch_key_down_action(action_object.key_fields(), source->get<KeyInputSource>(), global_key_state, browsing_context));
break;
case ActionObject::Subtype::KeyUp:
TRY(dispatch_key_up_action(action_object.key_fields(), source->get<KeyInputSource>(), global_key_state, browsing_context));
break;
case ActionObject::Subtype::PointerDown:
TRY(dispatch_pointer_down_action(action_object.pointer_up_down_fields(), source->get<PointerInputSource>(), global_key_state, browsing_context));
break;
case ActionObject::Subtype::PointerUp:
TRY(dispatch_pointer_up_action(action_object.pointer_up_down_fields(), source->get<PointerInputSource>(), global_key_state, browsing_context));
break;
case ActionObject::Subtype::PointerMove:
TRY(dispatch_pointer_move_action(action_object.pointer_move_fields(), source->get<PointerInputSource>(), global_key_state, tick_duration, browsing_context, actions_options));
break;
case ActionObject::Subtype::PointerCancel:
return WebDriver::Error::from_code(WebDriver::ErrorCode::UnsupportedOperation, "Pointer cancel events not implemented"sv);
case ActionObject::Subtype::Scroll:
return WebDriver::Error::from_code(WebDriver::ErrorCode::UnsupportedOperation, "Scroll events not implemented"sv);
}
// 9. If subtype is "keyDown", append a copy of action object with the subtype property changed to "keyUp" to
// input state's input cancel list.
if (subtype == ActionObject::Subtype::KeyDown) {
auto action_copy = action_object;
action_copy.subtype = ActionObject::Subtype::KeyUp;
input_state.input_cancel_list.append(move(action_copy));
}
// 10. If subtype is "pointerDown", append a copy of action object with the subtype property changed to
// "pointerUp" to input state's input cancel list.
if (subtype == ActionObject::Subtype::PointerDown) {
auto action_copy = action_object;
action_copy.subtype = ActionObject::Subtype::PointerUp;
input_state.input_cancel_list.append(move(action_copy));
}
}
// 2. Return success with data null.
return {};
}
// https://w3c.github.io/webdriver/#dfn-dispatch-a-list-of-actions
GC::Ref<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);
}
// https://w3c.github.io/webdriver/#dfn-dispatch-the-events-for-a-typeable-string
static GC::Ref<JS::Cell> dispatch_the_events_for_a_typeable_string(Web::WebDriver::InputState& input_state, String const& input_id, Web::WebDriver::InputSource& source, StringView text, Web::HTML::BrowsingContext& browsing_context, Web::WebDriver::OnActionsComplete on_complete)
{
auto& input_source = source.get<Web::WebDriver::KeyInputSource>();
// NOTE: Rather than dispatching each action list individually below, we collect a list of "actions by tick" to
// dispatch, to make handling the asynchronous nature of actions simpler.
Vector<Vector<Web::WebDriver::ActionObject>> actions_by_tick;
// 1. 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,
};
// 2. For each char of text:
for (auto code_point : Utf8View { text }) {
auto char_is_shifted = Web::WebDriver::is_shifted_character(code_point);
// 1. Let global key state be the result of get the global key state with input state.
auto global_key_state = Web::WebDriver::get_global_key_state(input_state);
// 2. If char is a shifted character, and the shifted state of source is false:
if (char_is_shifted && !input_source.shift) {
// 1. Let action be an action object constructed with input id, "key", and "keyDown", and set its value
// property to U+E008 ("left shift").
Web::WebDriver::ActionObject action { input_id, Web::WebDriver::InputSourceType::Key, Web::WebDriver::ActionObject::Subtype::KeyDown };
action.key_fields().value = 0xE008;
// 2. Let actions be the list «action».
Vector actions { move(action) };
// 3. Dispatch a list of actions with input state, actions, and browsing context.
actions_by_tick.append(move(actions));
input_source.shift = true;
}
// 3. If char is not a shifted character and the shifted state of source is true:
if (!char_is_shifted && input_source.shift) {
// 1. Let action be an action object constructed with input id, "key", and "keyUp", and set its value
// property to U+E008 ("left shift").
Web::WebDriver::ActionObject action { input_id, Web::WebDriver::InputSourceType::Key, Web::WebDriver::ActionObject::Subtype::KeyUp };
action.key_fields().value = 0xE008;
// 2. Let tick actions be the list «action».
Vector actions { move(action) };
// 3. Dispatch a list of actions with input state, actions, browsing context, and actions options.
actions_by_tick.append(move(actions));
input_source.shift = false;
}
// 4. Let keydown action be an action object constructed with arguments input id, "key", and "keyDown".
Web::WebDriver::ActionObject keydown_action { input_id, Web::WebDriver::InputSourceType::Key, Web::WebDriver::ActionObject::Subtype::KeyDown };
// 5. Set the value property of keydown action to char.
keydown_action.key_fields().value = code_point;
// 6. Let keyup action be a copy of keydown action with the subtype property changed to "keyUp".
auto keyup_action = keydown_action;
keyup_action.subtype = Web::WebDriver::ActionObject::Subtype::KeyUp;
// 7. Let actions be the list «keydown action, keyup action».
Vector actions { move(keydown_action), move(keyup_action) };
// 8. Dispatch a list of actions with input state, actions, browsing context, and actions options.
actions_by_tick.append(move(actions));
}
return dispatch_actions(input_state, move(actions_by_tick), browsing_context, move(actions_options), on_complete);
}
// https://w3c.github.io/webdriver/#dfn-dispatch-actions-for-a-string
GC::Ref<JS::Cell> dispatch_actions_for_a_string(Web::WebDriver::InputState& input_state, String const& input_id, Web::WebDriver::InputSource& source, StringView text, Web::HTML::BrowsingContext& browsing_context, Web::WebDriver::OnActionsComplete on_complete)
{
// FIXME: 1. Let clusters be an array created by breaking text into extended grapheme clusters.
// FIXME: 2. Let undo actions be an empty map.
// FIXME: 3. Let current typeable text be an empty list.
// FIXME: 4. For each cluster corresponding to an indexed property in clusters run the substeps of the first matching statement:
{
// -> cluster is the null key
{
// FIXME: 1. Dispatch the events for a typeable string with input state, input id, source, current typeable text, and browsing context. Empty current typeable text.
// FIXME: 2. Try to clear the modifier key state with input state, input id, source, undo actions and browsing context.
// FIXME: 3. Clear undo actions.
}
// -> cluster is a modifier key
{
// FIXME: 1. Dispatch the events for a typeable string with input state, input id, source, current typeable text, and browsing context.
// FIXME: 2. Empty current typeable text.
// FIXME: 3. Let keydown action be an action object constructed with arguments input id, "key", and "keyDown".
// FIXME: 4. Set the value property of keydown action to cluster.
// FIXME: 5. Let actions be the list «keydown action»
// FIXME: 6. Dispatch a list of actions with input state, actions, browsing context, and actions options.
// FIXME: 7. Add an entry to undo actions with key cluster and value being a copy of keydown action with the subtype property modified to "keyUp".
}
// -> cluster is typeable
{
// FIXME: Append cluster to current typeable text.
}
// -> Otherwise
{
// FIXME: 1. Dispatch the events for a typeable string with input state, input id, source, current typeable text, and browsing context.
// FIXME: 2. Empty current typeable text.
// FIXME: 3. Dispatch a composition event with arguments "compositionstart", undefined, and browsing context.
// FIXME: 4. Dispatch a composition event with arguments "compositionupdate", cluster, and browsing context.
// FIXME: 5. Dispatch a composition event with arguments "compositionend", cluster, and browsing context.
}
}
// FIXME: We currently only support sending single code points to Page. Much of the above loop would break the the
// text into segments, broken by graphemes / modifier keys / null keys. Until we need such support, we take
// the easy road here and dispatch the string as a single list of actions. When we do implement the above
// steps, we will likely need to implement a completely asynchronous driver (like ActionExecutor above).
// 5. Dispatch the events for a typeable string with input state, input id and source, current typeable text, and
// browsing context.
return dispatch_the_events_for_a_typeable_string(input_state, input_id, source, text, browsing_context, GC::create_function(browsing_context.heap(), [on_complete](Web::WebDriver::Response result) {
// FIXME: 6. Try to clear the modifier key state with input state, input id, source, undo actions, and browsing context.
on_complete->function()(move(result));
}));
}
}