#include "test_harness.h" #include "../src/keybind_manager.h" // Build a SDL_KEYDOWN event for the given key + modifier set. static SDL_Event keyDownEvent(SDL_Keycode key, SDL_Keymod mod) { SDL_Event ev{}; ev.type = SDL_EVENT_KEY_DOWN; ev.key.key = key; ev.key.mod = mod; return ev; } TEST(bare_bind_fires_without_modifiers) { KeybindManager kb; int calls = 0; kb.bind(SDLK_RETURN, "enter", [&calls]() { calls++; }); ASSERT_TRUE(kb.handleEvent(keyDownEvent(SDLK_RETURN, SDL_KMOD_NONE))); ASSERT_EQ(calls, 1); } TEST(bare_bind_ignores_modifiers) { KeybindManager kb; int calls = 0; kb.bind(SDLK_RETURN, "enter", [&calls]() { calls++; }); // A plain bind fires even while a modifier is held. ASSERT_TRUE(kb.handleEvent(keyDownEvent(SDLK_RETURN, SDL_KMOD_CTRL))); ASSERT_EQ(calls, 1); } TEST(combo_requires_modifier) { KeybindManager kb; int calls = 0; kb.bind(SDLK_D, SDL_KMOD_CTRL, "clear", [&calls]() { calls++; }); // Without Ctrl held the combo must not fire. ASSERT_FALSE(kb.handleEvent(keyDownEvent(SDLK_D, SDL_KMOD_NONE))); ASSERT_EQ(calls, 0); // With Ctrl held it fires. ASSERT_TRUE(kb.handleEvent(keyDownEvent(SDLK_D, SDL_KMOD_CTRL))); ASSERT_EQ(calls, 1); } // Regression: a physical press only sets ONE side of a paired modifier // (e.g. SDL_KMOD_LCTRL), never the combined SDL_KMOD_CTRL mask. The raw // bitwise check (pressed & required) == required therefore never matched and // Ctrl+D silently did nothing on real hardware. TEST(combo_fires_with_single_ctrl_key) { KeybindManager kb; int calls = 0; kb.bind(SDLK_D, SDL_KMOD_CTRL, "clear", [&calls]() { calls++; }); ASSERT_TRUE(kb.handleEvent(keyDownEvent(SDLK_D, SDL_KMOD_LCTRL))); ASSERT_EQ(calls, 1); ASSERT_TRUE(kb.handleEvent(keyDownEvent(SDLK_D, SDL_KMOD_RCTRL))); ASSERT_EQ(calls, 2); } TEST(combo_tolerates_extra_modifiers) { KeybindManager kb; int calls = 0; kb.bind(SDLK_D, SDL_KMOD_CTRL, "clear", [&calls]() { calls++; }); // CapsLock being stuck on should not block the combo. const SDL_Keymod extra = static_cast(SDL_KMOD_CTRL | SDL_KMOD_CAPS); ASSERT_TRUE(kb.handleEvent(keyDownEvent(SDLK_D, extra))); ASSERT_EQ(calls, 1); } TEST(combo_partial_modifiers_do_not_fire) { KeybindManager kb; int calls = 0; kb.bind(SDLK_D, static_cast(SDL_KMOD_CTRL | SDL_KMOD_SHIFT), "clear", [&calls]() { calls++; }); // Only Ctrl held, Shift missing: must not fire. ASSERT_FALSE(kb.handleEvent(keyDownEvent(SDLK_D, SDL_KMOD_CTRL))); ASSERT_EQ(calls, 0); // Both held: fires. ASSERT_TRUE(kb.handleEvent(keyDownEvent(SDLK_D, static_cast(SDL_KMOD_CTRL | SDL_KMOD_SHIFT)))); ASSERT_EQ(calls, 1); } TEST(unbind_removes_binding) { KeybindManager kb; int calls = 0; kb.bind(SDLK_D, SDL_KMOD_CTRL, "clear", [&calls]() { calls++; }); kb.unbind(SDLK_D); ASSERT_FALSE(kb.isBound(SDLK_D)); ASSERT_FALSE(kb.handleEvent(keyDownEvent(SDLK_D, SDL_KMOD_CTRL))); ASSERT_EQ(calls, 0); } TEST(unbind_all_removes_bindings) { KeybindManager kb; int calls = 0; kb.bind(SDLK_D, SDL_KMOD_CTRL, "clear", [&calls]() { calls++; }); kb.bind(SDLK_UP, "up", [&calls]() { calls++; }); kb.unbindAll(); ASSERT_FALSE(kb.isBound(SDLK_D)); ASSERT_FALSE(kb.isBound(SDLK_UP)); ASSERT_FALSE(kb.handleEvent(keyDownEvent(SDLK_D, SDL_KMOD_CTRL))); ASSERT_EQ(calls, 0); } TEST(is_bound_reflects_bindings) { KeybindManager kb; kb.bind(SDLK_D, SDL_KMOD_CTRL, "clear", []() {}); ASSERT_TRUE(kb.isBound(SDLK_D)); ASSERT_FALSE(kb.isBound(SDLK_UP)); } TEST(non_keydown_event_is_ignored) { KeybindManager kb; int calls = 0; kb.bind(SDLK_D, SDL_KMOD_CTRL, "clear", [&calls]() { calls++; }); SDL_Event ev{}; ev.type = SDL_EVENT_KEY_UP; ev.key.key = SDLK_D; ev.key.mod = SDL_KMOD_CTRL; ASSERT_FALSE(kb.handleEvent(ev)); ASSERT_EQ(calls, 0); }