1717#include " src/server/frontend_wayland/keyboard_state_tracker.h"
1818
1919#include < mir/test/doubles/advanceable_clock.h>
20+ #include < mir/events/keyboard_event.h>
21+ #include < mir/input/parameter_keymap.h>
2022
2123#include < xkbcommon/xkbcommon-keysyms.h>
2224
@@ -34,12 +36,13 @@ namespace
3436// / Based on standard US QWERTY layout (PC keyboard / evdev scancodes)
3537
3638constexpr uint32_t key_1_scancode = 2 ;
39+ constexpr uint32_t key_0_scancode = 11 ;
3740constexpr uint32_t a_scancode = 30 ;
3841constexpr uint32_t b_scancode = 48 ;
3942
40- constexpr uint32_t ctrl_l_scancode = 37 ;
41- constexpr uint32_t shift_l_scancode = 50 ;
42- constexpr uint32_t shift_r_scancode = 62 ;
43+ constexpr uint32_t ctrl_l_scancode = 29 ;
44+ constexpr uint32_t shift_l_scancode = 42 ;
45+ constexpr uint32_t shift_r_scancode = 54 ;
4346
4447class KeyboardStateTrackerTest : public Test
4548{
@@ -52,13 +55,15 @@ class KeyboardStateTrackerTest : public Test
5255 if (keysym == XKB_KEY_Shift_R)
5356 mod |= mir_input_event_modifier_shift_right;
5457
55- return mir::events::make_key_event (
58+ auto event = mir::events::make_key_event (
5659 id,
5760 clock.now ().time_since_epoch (),
5861 mir_keyboard_action_down,
5962 keysym,
6063 scancode,
6164 mod);
65+ event->to_input ()->to_keyboard ()->set_keymap (default_keymap);
66+ return event;
6267 }
6368
6469 auto key_up (uint32_t keysym, uint32_t scancode, MirInputDeviceId id = device_id) -> mir::EventUPtr
@@ -69,13 +74,15 @@ class KeyboardStateTrackerTest : public Test
6974 if (keysym == XKB_KEY_Shift_R)
7075 mod &= ~mir_input_event_modifier_shift_right;
7176
72- return mir::events::make_key_event (
77+ auto event = mir::events::make_key_event (
7378 id,
7479 clock.now ().time_since_epoch (),
7580 mir_keyboard_action_up,
7681 keysym,
7782 scancode,
7883 mod);
84+ event->to_input ()->to_keyboard ()->set_keymap (default_keymap);
85+ return event;
7986 }
8087
8188 auto static inline const device_id = MirInputDeviceId{0 };
@@ -84,6 +91,12 @@ class KeyboardStateTrackerTest : public Test
8491 mtd::AdvanceableClock clock;
8592 mf::KeyboardStateTracker tracker;
8693
94+ // Default US QWERTY keymap attached to every test event so that the
95+ // tracker's xkb_state is populated and shift-transition re-derivation
96+ // via xkb_state_key_get_one_sym() works correctly.
97+ std::shared_ptr<mir::input::Keymap> const default_keymap{
98+ std::make_shared<mir::input::ParameterKeymap>()};
99+
87100 // Track modifier state per device to emulate Mir's tracking of modifiers
88101 std::unordered_map<MirInputDeviceId, MirInputEventModifiers> modifier_states;
89102};
@@ -308,6 +321,29 @@ TEST_F(KeyboardStateTrackerTest, shift_release_on_one_device_does_not_demote_key
308321 EXPECT_FALSE (tracker.keysym_is_pressed (other_device_id, XKB_KEY_a));
309322}
310323
324+ TEST_F (KeyboardStateTrackerTest, pressing_shift_after_digit_promotes_to_symbol)
325+ {
326+ tracker.process (*key_down (XKB_KEY_0, key_0_scancode));
327+ EXPECT_TRUE (tracker.keysym_is_pressed (device_id, XKB_KEY_0));
328+
329+ tracker.process (*key_down (XKB_KEY_Shift_L, shift_l_scancode));
330+
331+ EXPECT_TRUE (tracker.keysym_is_pressed (device_id, XKB_KEY_parenright));
332+ EXPECT_FALSE (tracker.keysym_is_pressed (device_id, XKB_KEY_0));
333+ }
334+
335+ TEST_F (KeyboardStateTrackerTest, releasing_shift_after_digit_demotes_symbol_back_to_digit)
336+ {
337+ // Shift held, '0' pressed (which the layout reports as ')'), then Shift
338+ // released: the tracker should revert to '0'.
339+ tracker.process (*key_down (XKB_KEY_Shift_L, shift_l_scancode));
340+ tracker.process (*key_down (XKB_KEY_parenright, key_0_scancode));
341+ tracker.process (*key_up (XKB_KEY_Shift_L, shift_l_scancode));
342+
343+ EXPECT_TRUE (tracker.keysym_is_pressed (device_id, XKB_KEY_0));
344+ EXPECT_FALSE (tracker.keysym_is_pressed (device_id, XKB_KEY_parenright));
345+ }
346+
311347TEST_F (KeyboardStateTrackerTest, key_up_clears_key_when_modifier_changed_while_held)
312348{
313349 // Simulate: '1' pressed (keysym = XKB_KEY_1), then Shift pressed, then '1'
@@ -320,11 +356,13 @@ TEST_F(KeyboardStateTrackerTest, key_up_clears_key_when_modifier_changed_while_h
320356
321357 tracker.process (*key_down (XKB_KEY_Shift_L, shift_l_scancode));
322358
323- // XKB_KEY_1 has no uppercase equivalent so it remains as XKB_KEY_1
324- EXPECT_TRUE (tracker.keysym_is_pressed (device_id, XKB_KEY_1));
359+ // With a real xkb_state, pressing Shift re-derives '1' -> '!' correctly.
360+ EXPECT_TRUE (tracker.keysym_is_pressed (device_id, XKB_KEY_exclam));
361+ EXPECT_FALSE (tracker.keysym_is_pressed (device_id, XKB_KEY_1));
325362 EXPECT_TRUE (tracker.scancode_is_pressed (device_id, key_1_scancode));
326363
327- // Key-up event reports XKB_KEY_exclam because Shift is still held
364+ // Key-up event reports XKB_KEY_exclam because Shift is still held.
365+ // The tracker must clear the entry by scancode, not by keysym.
328366 tracker.process (*key_up (XKB_KEY_exclam, key_1_scancode));
329367
330368 EXPECT_FALSE (tracker.keysym_is_pressed (device_id, XKB_KEY_1));
0 commit comments