فهرست منبع

LibWeb: Implement dispatching WebDriver key down and key up actions

Timothy Flynn 9 ماه پیش
والد
کامیت
d6a8fc00c3
2فایلهای تغییر یافته به همراه385 افزوده شده و 7 حذف شده
  1. 384 6
      Userland/Libraries/LibWeb/WebDriver/Actions.cpp
  2. 1 1
      Userland/Libraries/LibWeb/WebDriver/Actions.h

+ 384 - 6
Userland/Libraries/LibWeb/WebDriver/Actions.cpp

@@ -5,6 +5,7 @@
  */
 
 #include <AK/Enumerate.h>
+#include <AK/Find.h>
 #include <AK/GenericShorthands.h>
 #include <AK/JsonArray.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
     //    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
-        //        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");
     }
 
     // 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.
     return action;
@@ -626,6 +630,379 @@ 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;
+}
+
+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
 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();
             break;
         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:
-            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:
             TRY(dispatch_pointer_down_action(action_object.pointer_up_down_fields(), source->get<PointerInputSource>(), global_key_state, browsing_context));
             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.
     return dispatch_actions(input_state, move(actions_by_tick), browsing_context, move(actions_options), on_complete);
 }
-
 }

+ 1 - 1
Userland/Libraries/LibWeb/WebDriver/Actions.h

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