LibWeb: Implement dispatching WebDriver key down and key up actions
Some checks are pending
CI / Lagom (false, FUZZ, ubuntu-24.04, Linux, Clang) (push) Waiting to run
CI / Lagom (false, NO_FUZZ, macos-14, macOS, Clang) (push) Waiting to run
CI / Lagom (false, NO_FUZZ, ubuntu-24.04, Linux, GNU) (push) Waiting to run
CI / Lagom (true, NO_FUZZ, ubuntu-24.04, Linux, Clang) (push) Waiting to run
Package the js repl as a binary artifact / build-and-package (macos-14, macOS, macOS-universal2) (push) Waiting to run
Package the js repl as a binary artifact / build-and-package (ubuntu-24.04, Linux, Linux-x86_64) (push) Waiting to run
Run test262 and test-wasm / run_and_update_results (push) Waiting to run
Lint Code / lint (push) Waiting to run
Push notes / build (push) Waiting to run

This commit is contained in:
Timothy Flynn 2024-10-07 10:21:02 -04:00 committed by Andreas Kling
parent 5b2633d90f
commit d6a8fc00c3
Notes: github-actions[bot] 2024-10-10 08:42:10 +00:00
2 changed files with 385 additions and 7 deletions

View file

@ -5,6 +5,7 @@
*/ */
#include <AK/Enumerate.h> #include <AK/Enumerate.h>
#include <AK/Find.h>
#include <AK/GenericShorthands.h> #include <AK/GenericShorthands.h>
#include <AK/JsonArray.h> #include <AK/JsonArray.h>
#include <AK/JsonObject.h> #include <AK/JsonObject.h>
@ -245,14 +246,17 @@ static ErrorOr<ActionObject, WebDriver::Error> process_key_action(String id, Jso
// 6. If key is not a String containing a single unicode code point [or grapheme cluster?] return error with error // 6. If key is not a String containing a single unicode code point [or grapheme cluster?] return error with error
// code invalid argument. // code invalid argument.
if (Utf8View { key }.length() != 1) { 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 // 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. // 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"); 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. // 7. Set the value property on action to key.
action.key_fields().value = MUST(String::from_byte_string(key)); action.key_fields().value = *utf8_key.begin();
// 8. Return success with data action. // 8. Return success with data action.
return action; return action;
@ -626,6 +630,379 @@ static void dispatch_pause_action()
// 1. Return success with data null. // 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;
}
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.
// FIXME: Add `repeat` support to Page::handle_keydown.
// 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);
// 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);
// 13. Return success with data null.
return {};
}
// https://w3c.github.io/webdriver/#dfn-dispatch-a-pointerdown-action // 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) 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)
{ {
@ -973,9 +1350,11 @@ ErrorOr<void, WebDriver::Error> dispatch_tick_actions(InputState& input_state, R
dispatch_pause_action(); dispatch_pause_action();
break; break;
case ActionObject::Subtype::KeyDown: case ActionObject::Subtype::KeyDown:
return WebDriver::Error::from_code(WebDriver::ErrorCode::UnsupportedOperation, "Key down events not implemented"sv); TRY(dispatch_key_down_action(action_object.key_fields(), source->get<KeyInputSource>(), global_key_state, browsing_context));
break;
case ActionObject::Subtype::KeyUp: case ActionObject::Subtype::KeyUp:
return WebDriver::Error::from_code(WebDriver::ErrorCode::UnsupportedOperation, "Key up events not implemented"sv); TRY(dispatch_key_up_action(action_object.key_fields(), source->get<KeyInputSource>(), global_key_state, browsing_context));
break;
case ActionObject::Subtype::PointerDown: case ActionObject::Subtype::PointerDown:
TRY(dispatch_pointer_down_action(action_object.pointer_up_down_fields(), source->get<PointerInputSource>(), global_key_state, browsing_context)); TRY(dispatch_pointer_down_action(action_object.pointer_up_down_fields(), source->get<PointerInputSource>(), global_key_state, browsing_context));
break; break;
@ -1025,5 +1404,4 @@ JS::NonnullGCPtr<JS::Cell> dispatch_list_of_actions(InputState& input_state, Vec
// 3. Return the result of dispatch actions with input state, actions by tick, browsing context, and actions options. // 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); return dispatch_actions(input_state, move(actions_by_tick), browsing_context, move(actions_options), on_complete);
} }
} }

View file

@ -47,7 +47,7 @@ struct ActionObject {
}; };
struct KeyFields { struct KeyFields {
String value; u32 value { 0 };
}; };
struct PointerFields { struct PointerFields {