diff --git a/Userland/Libraries/LibWeb/WebDriver/Actions.cpp b/Userland/Libraries/LibWeb/WebDriver/Actions.cpp index cd264025f7e..c94f7c0f6fa 100644 --- a/Userland/Libraries/LibWeb/WebDriver/Actions.cpp +++ b/Userland/Libraries/LibWeb/WebDriver/Actions.cpp @@ -5,6 +5,7 @@ */ #include +#include #include #include #include @@ -245,14 +246,17 @@ static ErrorOr 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 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({ + { '`', '~', 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 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 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 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 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(), 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(), global_key_state, browsing_context)); + break; case ActionObject::Subtype::PointerDown: TRY(dispatch_pointer_down_action(action_object.pointer_up_down_fields(), source->get(), global_key_state, browsing_context)); break; @@ -1025,5 +1404,4 @@ JS::NonnullGCPtr 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); } - } diff --git a/Userland/Libraries/LibWeb/WebDriver/Actions.h b/Userland/Libraries/LibWeb/WebDriver/Actions.h index 2f2d0ff12e5..316e7bce159 100644 --- a/Userland/Libraries/LibWeb/WebDriver/Actions.h +++ b/Userland/Libraries/LibWeb/WebDriver/Actions.h @@ -47,7 +47,7 @@ struct ActionObject { }; struct KeyFields { - String value; + u32 value { 0 }; }; struct PointerFields {