# Unit tests for command_manager.py methods. # # Copyright 2025 Igalia, S.L. # Author: Joanmarie Diggs # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # pylint: disable=wrong-import-position # pylint: disable=import-outside-toplevel # pylint: disable=protected-access # pylint: disable=too-many-lines # pylint: disable=too-many-arguments # pylint: disable=too-many-positional-arguments """Unit tests for command_manager.py methods.""" from __future__ import annotations from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from unittest.mock import Mock from cthulhu_test_context import CthulhuTestContext @pytest.mark.unit class TestCommand: """Test Command base class methods.""" def _setup_dependencies(self, test_context: CthulhuTestContext) -> dict[str, Mock]: """Returns dependencies for command_manager module testing.""" essential_modules = test_context.setup_shared_dependencies([]) input_event_mock = essential_modules["cthulhu.input_event"] input_event_mock.InputEventHandler = test_context.Mock keybindings_mock = essential_modules["cthulhu.keybindings"] keybindings_mock.KeyBinding = test_context.Mock keybindings_mock.KeyBindings = test_context.Mock return essential_modules def _create_mock_function(self, test_context: CthulhuTestContext) -> Mock: """Creates a mock function for a Command.""" function = test_context.Mock() function.return_value = True return function def test_init_minimal(self, test_context: CthulhuTestContext) -> None: """Test Command.__init__ with minimal arguments.""" self._setup_dependencies(test_context) from cthulhu.command_manager import Command function = self._create_mock_function(test_context) command = Command("testCommand", function, "Test Group") assert command.get_name() == "testCommand" assert command.get_function() == function assert command.get_group_label() == "Test Group" assert command.get_description() == "" def test_init_full(self, test_context: CthulhuTestContext) -> None: """Test Command.__init__ with all arguments.""" self._setup_dependencies(test_context) from cthulhu.command_manager import Command function = self._create_mock_function(test_context) command = Command( "fullCommand", function, "Full Group", "Full description", enabled=False, suspended=True, ) assert command.get_name() == "fullCommand" assert command.get_function() == function assert command.get_group_label() == "Full Group" assert command.get_description() == "Full description" assert command.is_enabled() is False assert command.is_suspended() is True def test_set_group_label(self, test_context: CthulhuTestContext) -> None: """Test Command.set_group_label.""" self._setup_dependencies(test_context) from cthulhu.command_manager import Command function = self._create_mock_function(test_context) command = Command("testCommand", function, "Original Group") assert command.get_group_label() == "Original Group" command.set_group_label("New Group") assert command.get_group_label() == "New Group" def test_init_enabled_suspended_defaults(self, test_context: CthulhuTestContext) -> None: """Test that enabled defaults to True and suspended defaults to False.""" self._setup_dependencies(test_context) from cthulhu.command_manager import Command function = self._create_mock_function(test_context) command = Command("testCommand", function, "Test Group") assert command.is_enabled() is True assert command.is_suspended() is False def test_init_enabled_suspended_explicit(self, test_context: CthulhuTestContext) -> None: """Test Command.__init__ with explicit enabled and suspended values.""" self._setup_dependencies(test_context) from cthulhu.command_manager import Command function = self._create_mock_function(test_context) command = Command("testCommand", function, "Test Group", enabled=False, suspended=True) assert command.is_enabled() is False assert command.is_suspended() is True def test_set_enabled(self, test_context: CthulhuTestContext) -> None: """Test Command.set_enabled.""" self._setup_dependencies(test_context) from cthulhu.command_manager import Command function = self._create_mock_function(test_context) command = Command("testCommand", function, "Test Group") assert command.is_enabled() is True command.set_enabled(False) assert command.is_enabled() is False command.set_enabled(True) assert command.is_enabled() is True def test_set_suspended(self, test_context: CthulhuTestContext) -> None: """Test Command.set_suspended.""" self._setup_dependencies(test_context) from cthulhu.command_manager import Command function = self._create_mock_function(test_context) command = Command("testCommand", function, "Test Group") assert command.is_suspended() is False command.set_suspended(True) assert command.is_suspended() is True command.set_suspended(False) assert command.is_suspended() is False def test_execute_calls_function(self, test_context: CthulhuTestContext) -> None: """Test that execute calls the command's function.""" self._setup_dependencies(test_context) from cthulhu.command_manager import Command function = self._create_mock_function(test_context) function.return_value = True command = Command("testCommand", function, "Test Group") mock_script = test_context.Mock() mock_event = test_context.Mock() result = command.execute(mock_script, mock_event) function.assert_called_once_with(mock_script, mock_event) assert result is True def test_execute_returns_function_result(self, test_context: CthulhuTestContext) -> None: """Test that execute returns the function's return value.""" self._setup_dependencies(test_context) from cthulhu.command_manager import Command function = self._create_mock_function(test_context) function.return_value = False command = Command("testCommand", function, "Test Group") mock_script = test_context.Mock() result = command.execute(mock_script, None) assert result is False @pytest.mark.unit class TestKeyboardCommand: """Test KeyboardCommand class methods.""" def _setup_dependencies(self, test_context: CthulhuTestContext) -> dict[str, Mock]: """Returns dependencies for command_manager module testing.""" essential_modules = test_context.setup_shared_dependencies([]) input_event_mock = essential_modules["cthulhu.input_event"] input_event_mock.InputEventHandler = test_context.Mock keybindings_mock = essential_modules["cthulhu.keybindings"] keybindings_mock.KeyBinding = test_context.Mock keybindings_mock.KeyBindings = test_context.Mock return essential_modules def _create_mock_function(self, test_context: CthulhuTestContext) -> Mock: """Creates a mock function for a Command.""" function = test_context.Mock() function.return_value = True return function def _create_mock_keybinding( self, test_context: CthulhuTestContext, keyval: int = 65, keycode: int = 38, ) -> Mock: """Creates a mock KeyBinding with default keyval/keycode for indexing.""" kb = test_context.Mock() kb.keyval = keyval kb.keycode = keycode return kb def test_init_minimal(self, test_context: CthulhuTestContext) -> None: """Test KeyboardCommand.__init__ with minimal arguments.""" self._setup_dependencies(test_context) from cthulhu.command_manager import KeyboardCommand function = self._create_mock_function(test_context) command = KeyboardCommand("testCommand", function, "Test Group") assert command.get_name() == "testCommand" assert command.get_function() == function assert command.get_group_label() == "Test Group" assert command.get_description() == "" assert command.get_keybinding() is None def test_init_with_keybindings(self, test_context: CthulhuTestContext) -> None: """Test KeyboardCommand.__init__ with keybindings.""" self._setup_dependencies(test_context) from cthulhu.command_manager import KeyboardCommand function = self._create_mock_function(test_context) desktop_kb = self._create_mock_keybinding(test_context) laptop_kb = self._create_mock_keybinding(test_context) command = KeyboardCommand( "fullCommand", function, "Full Group", "Full description", desktop_keybinding=desktop_kb, laptop_keybinding=laptop_kb, ) assert command.get_name() == "fullCommand" assert command.get_default_keybinding(is_desktop=True) == desktop_kb assert command.get_default_keybinding(is_desktop=False) == laptop_kb def test_set_keybinding(self, test_context: CthulhuTestContext) -> None: """Test KeyboardCommand.set_keybinding.""" self._setup_dependencies(test_context) from cthulhu.command_manager import KeyboardCommand function = self._create_mock_function(test_context) command = KeyboardCommand("testCommand", function, "Test Group") assert command.get_keybinding() is None new_kb = self._create_mock_keybinding(test_context) command.set_keybinding(new_kb) assert command.get_keybinding() == new_kb command.set_keybinding(None) assert command.get_keybinding() is None def test_is_active_enabled_not_suspended_with_keybinding( self, test_context: CthulhuTestContext, ) -> None: """Test is_active returns True when enabled, not suspended, and has keybinding.""" self._setup_dependencies(test_context) from cthulhu.command_manager import KeyboardCommand function = self._create_mock_function(test_context) keybinding = self._create_mock_keybinding(test_context) command = KeyboardCommand( "testCommand", function, "Test Group", desktop_keybinding=keybinding, ) command.set_keybinding(keybinding) assert command.is_active() is True def test_is_active_disabled(self, test_context: CthulhuTestContext) -> None: """Test is_active returns False when disabled.""" self._setup_dependencies(test_context) from cthulhu.command_manager import KeyboardCommand function = self._create_mock_function(test_context) keybinding = self._create_mock_keybinding(test_context) command = KeyboardCommand( "testCommand", function, "Test Group", desktop_keybinding=keybinding, enabled=False, ) command.set_keybinding(keybinding) assert command.is_active() is False def test_is_active_suspended(self, test_context: CthulhuTestContext) -> None: """Test is_active returns False when suspended.""" self._setup_dependencies(test_context) from cthulhu.command_manager import KeyboardCommand function = self._create_mock_function(test_context) keybinding = self._create_mock_keybinding(test_context) command = KeyboardCommand( "testCommand", function, "Test Group", desktop_keybinding=keybinding, suspended=True, ) command.set_keybinding(keybinding) assert command.is_active() is False def test_is_active_no_keybinding(self, test_context: CthulhuTestContext) -> None: """Test is_active returns False when no keybinding is assigned.""" self._setup_dependencies(test_context) from cthulhu.command_manager import KeyboardCommand function = self._create_mock_function(test_context) command = KeyboardCommand("testCommand", function, "Test Group") assert command.is_active() is False def test_is_group_toggle_init(self, test_context: CthulhuTestContext) -> None: """Test KeyboardCommand initializes is_group_toggle correctly.""" self._setup_dependencies(test_context) from cthulhu.command_manager import KeyboardCommand function = self._create_mock_function(test_context) # Default is False cmd_default = KeyboardCommand("cmd1", function, "Test Group") assert cmd_default.is_group_toggle() is False # Explicit True cmd_toggle = KeyboardCommand("cmd2", function, "Test Group", is_group_toggle=True) assert cmd_toggle.is_group_toggle() is True @pytest.mark.unit class TestBrailleCommand: """Test BrailleCommand class methods.""" def _setup_dependencies(self, test_context: CthulhuTestContext) -> dict[str, Mock]: """Returns dependencies for command_manager module testing.""" essential_modules = test_context.setup_shared_dependencies([]) input_event_mock = essential_modules["cthulhu.input_event"] input_event_mock.InputEventHandler = test_context.Mock keybindings_mock = essential_modules["cthulhu.keybindings"] keybindings_mock.KeyBinding = test_context.Mock keybindings_mock.KeyBindings = test_context.Mock return essential_modules def _create_mock_function(self, test_context: CthulhuTestContext) -> Mock: """Creates a mock function for a Command.""" function = test_context.Mock() function.return_value = True return function def test_braille_bindings_default_empty(self, test_context: CthulhuTestContext) -> None: """Test that braille bindings default to empty tuple.""" self._setup_dependencies(test_context) from cthulhu.command_manager import BrailleCommand function = self._create_mock_function(test_context) command = BrailleCommand("testCommand", function, "Test Group") assert command.get_braille_bindings() == () def test_init_with_braille_bindings(self, test_context: CthulhuTestContext) -> None: """Test BrailleCommand.__init__ with braille bindings.""" self._setup_dependencies(test_context) from cthulhu.command_manager import BrailleCommand function = self._create_mock_function(test_context) command = BrailleCommand( "testCommand", function, "Test Group", "Test description", braille_bindings=(1, 2, 3), ) assert command.get_braille_bindings() == (1, 2, 3) @pytest.mark.unit class TestCommandManager: """Test CommandManager class methods.""" def _setup_dependencies(self, test_context: CthulhuTestContext) -> dict[str, Mock]: """Returns dependencies for command_manager module testing.""" essential_modules = test_context.setup_shared_dependencies([]) input_event_mock = essential_modules["cthulhu.input_event"] input_event_mock.InputEventHandler = test_context.Mock keybindings_mock = essential_modules["cthulhu.keybindings"] keybindings_mock.KeyBinding = test_context.Mock keybindings_mock.KeyBindings = test_context.Mock from cthulhu import gsettings_registry gsettings_registry.get_registry().clear_runtime_values() return essential_modules def _create_mock_function( self, test_context: CthulhuTestContext, _description: str = "Test function", ) -> Mock: """Creates a mock function for a Command.""" function = test_context.Mock() function.return_value = True return function def _create_mock_keybinding( self, test_context: CthulhuTestContext, keyval: int = 65, keycode: int = 38, ) -> Mock: """Creates a mock KeyBinding with default keyval/keycode for indexing.""" kb = test_context.Mock() kb.keyval = keyval kb.keycode = keycode kb.keysymstring = "a" return kb def test_init(self, test_context: CthulhuTestContext) -> None: """Test CommandManager.__init__.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager manager = CommandManager() assert not manager.get_all_keyboard_commands() assert not manager.get_all_braille_commands() def test_add_and_get_keyboard_command(self, test_context: CthulhuTestContext) -> None: """Test CommandManager.add_command and get_keyboard_command.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) command = KeyboardCommand("testCommand", function, "Test Group") manager.add_command(command) retrieved = manager.get_keyboard_command("testCommand") assert retrieved == command assert manager.get_keyboard_command("nonexistent") is None def test_get_all_keyboard_commands(self, test_context: CthulhuTestContext) -> None: """Test CommandManager.get_all_keyboard_commands.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function1 = self._create_mock_function(test_context) function2 = self._create_mock_function(test_context) function3 = self._create_mock_function(test_context) cmd1 = KeyboardCommand("cmd1", function1, "Group A") cmd2 = KeyboardCommand("cmd2", function2, "Group B") cmd3 = KeyboardCommand("cmd3", function3, "Group A") manager.add_command(cmd1) manager.add_command(cmd2) manager.add_command(cmd3) all_commands = manager.get_all_keyboard_commands() assert len(all_commands) == 3 assert cmd1 in all_commands assert cmd2 in all_commands assert cmd3 in all_commands def test_get_command_for_event_finds_match(self, test_context: CthulhuTestContext) -> None: """Test get_command_for_event returns matching command.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) kb = self._create_mock_keybinding(test_context) kb.matches.return_value = True kb.modifiers = 4 kb.click_count = 1 cmd = KeyboardCommand("cmd", function, "Test Group", desktop_keybinding=kb) cmd.set_keybinding(kb) manager.add_command(cmd) event = test_context.Mock() event.get_click_count.return_value = 1 event.id = 65 event.hw_code = 38 event.modifiers = 4 event.is_keypad_key_with_numlock_on.return_value = False result = manager.get_command_for_event(event) assert result == cmd def test_get_command_for_event_no_match(self, test_context: CthulhuTestContext) -> None: """Test get_command_for_event returns None when no match.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) kb = self._create_mock_keybinding(test_context) kb.matches.return_value = False cmd = KeyboardCommand("cmd", function, "Test Group", desktop_keybinding=kb) cmd.set_keybinding(kb) manager.add_command(cmd) event = test_context.Mock() event.get_click_count.return_value = 1 event.id = 65 event.hw_code = 38 event.modifiers = 4 result = manager.get_command_for_event(event) assert result is None def test_get_command_for_event_skips_inactive(self, test_context: CthulhuTestContext) -> None: """Test get_command_for_event skips inactive commands by default.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) kb = self._create_mock_keybinding(test_context) kb.matches.return_value = True kb.modifiers = 4 kb.click_count = 1 cmd = KeyboardCommand("cmd", function, "Test Group", desktop_keybinding=kb, suspended=True) cmd.set_keybinding(kb) manager.add_command(cmd) event = test_context.Mock() event.get_click_count.return_value = 1 event.id = 65 event.hw_code = 38 event.modifiers = 4 result = manager.get_command_for_event(event) assert result is None def test_get_command_for_event_includes_inactive(self, test_context: CthulhuTestContext) -> None: """Test get_command_for_event can include inactive commands.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) kb = self._create_mock_keybinding(test_context) kb.matches.return_value = True kb.modifiers = 4 kb.click_count = 1 cmd = KeyboardCommand("cmd", function, "Test Group", desktop_keybinding=kb, suspended=True) cmd.set_keybinding(kb) manager.add_command(cmd) event = test_context.Mock() event.get_click_count.return_value = 1 event.id = 65 event.hw_code = 38 event.modifiers = 4 event.is_keypad_key_with_numlock_on.return_value = False result = manager.get_command_for_event(event, active_only=False) assert result == cmd def test_get_command_for_braille_event_finds_match(self, test_context: CthulhuTestContext) -> None: """Test get_command_for_braille_event returns matching command.""" self._setup_dependencies(test_context) from cthulhu.command_manager import BrailleCommand, CommandManager manager = CommandManager() function = self._create_mock_function(test_context) cmd = BrailleCommand("cmd", function, "Test Group", braille_bindings=(100, 200, 300)) manager.add_command(cmd) result = manager.get_command_for_braille_event(200) assert result == cmd def test_get_command_for_braille_event_no_match(self, test_context: CthulhuTestContext) -> None: """Test get_command_for_braille_event returns None when no match.""" self._setup_dependencies(test_context) from cthulhu.command_manager import BrailleCommand, CommandManager manager = CommandManager() function = self._create_mock_function(test_context) cmd = BrailleCommand("cmd", function, "Test Group", braille_bindings=(100, 200, 300)) manager.add_command(cmd) result = manager.get_command_for_braille_event(999) assert result is None def test_get_command_for_braille_event_empty_bindings( self, test_context: CthulhuTestContext, ) -> None: """Test get_command_for_braille_event returns None when command has no braille bindings.""" self._setup_dependencies(test_context) from cthulhu.command_manager import BrailleCommand, CommandManager manager = CommandManager() function = self._create_mock_function(test_context) cmd = BrailleCommand("cmd", function, "Test Group") manager.add_command(cmd) result = manager.get_command_for_braille_event(100) assert result is None def test_set_group_enabled(self, test_context: CthulhuTestContext) -> None: """Test set_group_enabled sets enabled state for all commands in group.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function1 = self._create_mock_function(test_context) function2 = self._create_mock_function(test_context) function3 = self._create_mock_function(test_context) cmd1 = KeyboardCommand("cmd1", function1, "Group A") cmd2 = KeyboardCommand("cmd2", function2, "Group A") cmd3 = KeyboardCommand("cmd3", function3, "Group B") manager.add_command(cmd1) manager.add_command(cmd2) manager.add_command(cmd3) manager.set_group_enabled("Group A", False) assert cmd1.is_enabled() is False assert cmd2.is_enabled() is False assert cmd3.is_enabled() is True def test_set_group_suspended(self, test_context: CthulhuTestContext) -> None: """Test set_group_suspended sets suspended state for all commands in group.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function1 = self._create_mock_function(test_context) function2 = self._create_mock_function(test_context) function3 = self._create_mock_function(test_context) cmd1 = KeyboardCommand("cmd1", function1, "Group A") cmd2 = KeyboardCommand("cmd2", function2, "Group A") cmd3 = KeyboardCommand("cmd3", function3, "Group B") manager.add_command(cmd1) manager.add_command(cmd2) manager.add_command(cmd3) manager.set_group_suspended("Group A", True) assert cmd1.is_suspended() is True assert cmd2.is_suspended() is True assert cmd3.is_suspended() is False def test_has_multi_click_bindings_true(self, test_context: CthulhuTestContext) -> None: """Test has_multi_click_bindings returns True when multi-click binding exists.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) # Single-click binding (KP_Up keyval = 65431, keycode = 80) kb1 = self._create_mock_keybinding(test_context, keyval=65431, keycode=80) kb1.matches.return_value = True kb1.click_count = 1 cmd1 = KeyboardCommand("readLine", function, "Flat Review", desktop_keybinding=kb1) cmd1.set_keybinding(kb1) manager.add_command(cmd1) # Double-click binding for same key kb2 = self._create_mock_keybinding(test_context, keyval=65431, keycode=80) kb2.matches.return_value = True kb2.click_count = 2 cmd2 = KeyboardCommand("spellLine", function, "Flat Review", desktop_keybinding=kb2) cmd2.set_keybinding(kb2) manager.add_command(cmd2) assert manager.has_multi_click_bindings(65431, 80, 0) is True def test_has_multi_click_bindings_false_single_only( self, test_context: CthulhuTestContext, ) -> None: """Test has_multi_click_bindings returns False when only single-click binding exists.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) # Only single-click binding (KP_Home keyval = 65429, keycode = 79) kb = self._create_mock_keybinding(test_context, keyval=65429, keycode=79) kb.matches.return_value = True kb.click_count = 1 cmd = KeyboardCommand("previousLine", function, "Flat Review", desktop_keybinding=kb) cmd.set_keybinding(kb) manager.add_command(cmd) assert manager.has_multi_click_bindings(65429, 79, 0) is False def test_has_multi_click_bindings_false_no_bindings( self, test_context: CthulhuTestContext, ) -> None: """Test has_multi_click_bindings returns False when no bindings exist for key.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager manager = CommandManager() # KP_Home keyval = 65429, keycode = 79 assert manager.has_multi_click_bindings(65429, 79, 0) is False def test_has_multi_click_bindings_respects_modifiers( self, test_context: CthulhuTestContext, ) -> None: """Test has_multi_click_bindings distinguishes by modifiers.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) # Double-click binding with Cthulhu modifier (KP_Up keyval = 65431, keycode = 80) kb = self._create_mock_keybinding(test_context, keyval=65431, keycode=80) # matches() returns True only when modifiers=256 kb.matches.side_effect = lambda kv, kc, mods: mods == 256 kb.click_count = 2 cmd = KeyboardCommand("someCommand", function, "Test", desktop_keybinding=kb) cmd.set_keybinding(kb) manager.add_command(cmd) # With modifier: has multi-click assert manager.has_multi_click_bindings(65431, 80, 256) is True # Without modifier: no multi-click bindings assert manager.has_multi_click_bindings(65431, 80, 0) is False def test_get_command_for_event_shifted_key(self, test_context: CthulhuTestContext) -> None: """Test get_command_for_event finds command via keycode when keyval differs (shifted).""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) # Command bound to 'h' (keyval=104, keycode=38) kb = self._create_mock_keybinding(test_context, keyval=104, keycode=38) kb.matches.return_value = True kb.click_count = 1 cmd = KeyboardCommand("prevHeading", function, "Structural Nav", desktop_keybinding=kb) cmd.set_keybinding(kb) manager.add_command(cmd) # Event has 'H' keyval (72) due to Shift, but same keycode (38) event = test_context.Mock() event.get_click_count.return_value = 1 event.id = 72 # 'H' keyval (shifted) event.hw_code = 38 # Same keycode as 'h' event.modifiers = 1 # Shift event.is_keypad_key_with_numlock_on.return_value = False result = manager.get_command_for_event(event) assert result == cmd def test_get_command_for_event_unshifted_key(self, test_context: CthulhuTestContext) -> None: """Test get_command_for_event finds command via keyval for unshifted key.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) # Command bound to 'h' (keyval=104, keycode=38) kb = self._create_mock_keybinding(test_context, keyval=104, keycode=38) kb.matches.return_value = True kb.click_count = 1 cmd = KeyboardCommand("nextHeading", function, "Structural Nav", desktop_keybinding=kb) cmd.set_keybinding(kb) manager.add_command(cmd) # Event has 'h' keyval (104), no shift event = test_context.Mock() event.get_click_count.return_value = 1 event.id = 104 # 'h' keyval (unshifted) event.hw_code = 38 event.modifiers = 0 event.is_keypad_key_with_numlock_on.return_value = False result = manager.get_command_for_event(event) assert result == cmd def test_has_multi_click_bindings_shifted_key(self, test_context: CthulhuTestContext) -> None: """Test has_multi_click_bindings finds binding via keycode when keyval differs.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) # Single-click binding for 'h' (keyval=104, keycode=38) kb1 = self._create_mock_keybinding(test_context, keyval=104, keycode=38) kb1.matches.return_value = True kb1.click_count = 1 cmd1 = KeyboardCommand("nextHeading", function, "Structural Nav", desktop_keybinding=kb1) cmd1.set_keybinding(kb1) manager.add_command(cmd1) # Double-click binding for same key kb2 = self._create_mock_keybinding(test_context, keyval=104, keycode=38) kb2.matches.return_value = True kb2.click_count = 2 cmd2 = KeyboardCommand("listHeadings", function, "Structural Nav", desktop_keybinding=kb2) cmd2.set_keybinding(kb2) manager.add_command(cmd2) # Query with 'H' keyval (72) due to Shift, but same keycode (38) # Should find multi-click binding via keycode assert manager.has_multi_click_bindings(72, 38, 1) is True def test_has_multi_click_bindings_unshifted_key(self, test_context: CthulhuTestContext) -> None: """Test has_multi_click_bindings finds binding via keyval for unshifted key.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) # Single-click binding for 'h' (keyval=104, keycode=38) kb1 = self._create_mock_keybinding(test_context, keyval=104, keycode=38) kb1.matches.return_value = True kb1.click_count = 1 cmd1 = KeyboardCommand("nextHeading", function, "Structural Nav", desktop_keybinding=kb1) cmd1.set_keybinding(kb1) manager.add_command(cmd1) # Double-click binding for same key kb2 = self._create_mock_keybinding(test_context, keyval=104, keycode=38) kb2.matches.return_value = True kb2.click_count = 2 cmd2 = KeyboardCommand("listHeadings", function, "Structural Nav", desktop_keybinding=kb2) cmd2.set_keybinding(kb2) manager.add_command(cmd2) # Query with 'h' keyval (104), unshifted - should find via keyval assert manager.has_multi_click_bindings(104, 38, 0) is True @pytest.mark.unit class TestGetManager: """Test the get_manager singleton function.""" def _setup_dependencies(self, test_context: CthulhuTestContext) -> dict[str, Mock]: """Returns dependencies for command_manager module testing.""" essential_modules = test_context.setup_shared_dependencies([]) input_event_mock = essential_modules["cthulhu.input_event"] input_event_mock.InputEventHandler = test_context.Mock keybindings_mock = essential_modules["cthulhu.keybindings"] keybindings_mock.KeyBinding = test_context.Mock keybindings_mock.KeyBindings = test_context.Mock from cthulhu import gsettings_registry gsettings_registry.get_registry().clear_runtime_values() return essential_modules def test_get_manager_returns_singleton(self, test_context: CthulhuTestContext) -> None: """Test that get_manager returns the same instance.""" self._setup_dependencies(test_context) from cthulhu.command_manager import get_manager manager1 = get_manager() manager2 = get_manager() assert manager1 is manager2 def test_get_manager_returns_command_manager(self, test_context: CthulhuTestContext) -> None: """Test that get_manager returns a CommandManager instance.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, get_manager manager = get_manager() assert isinstance(manager, CommandManager) @pytest.mark.unit class TestDiffBasedGrabUpdates: """Test diff-based grab updates in CommandManager.""" def _setup_dependencies(self, test_context: CthulhuTestContext) -> dict[str, Mock]: """Returns dependencies for command_manager module testing.""" essential_modules = test_context.setup_shared_dependencies(["cthulhu.cthulhu_modifier_manager"]) input_event_mock = essential_modules["cthulhu.input_event"] input_event_mock.InputEventHandler = test_context.Mock keybindings_mock = essential_modules["cthulhu.keybindings"] keybindings_mock.KeyBinding = test_context.Mock keybindings_mock.KeyBindings = test_context.Mock cthulhu_modifier_manager_mock = essential_modules["cthulhu.cthulhu_modifier_manager"] modifier_manager_instance = test_context.Mock() modifier_manager_instance.refresh_cthulhu_modifiers = test_context.Mock() cthulhu_modifier_manager_mock.get_manager = test_context.Mock( return_value=modifier_manager_instance, ) from cthulhu import gsettings_registry gsettings_registry.get_registry().clear_runtime_values() essential_modules["modifier_manager_instance"] = modifier_manager_instance return essential_modules def _create_mock_function(self, test_context: CthulhuTestContext) -> Mock: """Creates a mock function for a Command.""" function = test_context.Mock() function.return_value = True return function def _create_mock_keybinding( self, test_context: CthulhuTestContext, keysymstring: str = "a", modifiers: int = 0, click_count: int = 1, keyval: int = 65, keycode: int = 38, ) -> Mock: """Creates a mock KeyBinding with specified properties.""" kb = test_context.Mock() kb.keysymstring = keysymstring kb.modifiers = modifiers kb.click_count = click_count kb.keyval = keyval kb.keycode = keycode kb.has_grabs = test_context.Mock(return_value=False) kb.add_grabs = test_context.Mock() kb.remove_grabs = test_context.Mock() kb.get_grab_ids = test_context.Mock(return_value=[]) kb.set_grab_ids = test_context.Mock() return kb def test_binding_key_returns_tuple(self, test_context: CthulhuTestContext) -> None: """Test _binding_key returns correct tuple.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager manager = CommandManager() kb = self._create_mock_keybinding( test_context, keysymstring="h", modifiers=256, click_count=2, ) result = manager._binding_key(kb) assert result == ("h", 256, 2) def test_binding_key_returns_none_for_none(self, test_context: CthulhuTestContext) -> None: """Test _binding_key returns None for None keybinding.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager manager = CommandManager() assert manager._binding_key(None) is None def test_binding_key_returns_none_for_empty_keysymstring( self, test_context: CthulhuTestContext, ) -> None: """Test _binding_key returns None when keysymstring is empty.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager manager = CommandManager() kb = self._create_mock_keybinding(test_context, keysymstring="") assert manager._binding_key(kb) is None def test_diff_transfers_grab_ids_for_matching_bindings( self, test_context: CthulhuTestContext, ) -> None: """Test that grab IDs are transferred when bindings match.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) # Old command with grabs old_kb = self._create_mock_keybinding(test_context, keysymstring="h", modifiers=256) old_kb.has_grabs.return_value = True old_kb.get_grab_ids.return_value = [42, 43] old_cmd = KeyboardCommand("cmd", function, "Group", desktop_keybinding=old_kb) old_cmd.set_keybinding(old_kb) old_commands = {"cmd": old_cmd} # New command with same binding but no grabs yet new_kb = self._create_mock_keybinding(test_context, keysymstring="h", modifiers=256) new_kb.has_grabs.return_value = False new_cmd = KeyboardCommand("cmd", function, "Group", desktop_keybinding=new_kb) new_cmd.set_keybinding(new_kb) new_commands = {"cmd": new_cmd} manager._keyboard_commands = old_commands manager._diff_and_update_grabs(new_commands, "test") # Verify grab IDs were transferred new_kb.set_grab_ids.assert_called_once_with([42, 43]) old_kb.set_grab_ids.assert_called_once_with([]) # Neither add nor remove should be called new_kb.add_grabs.assert_not_called() old_kb.remove_grabs.assert_not_called() def test_diff_removes_grabs_for_old_only_bindings(self, test_context: CthulhuTestContext) -> None: """Test that grabs are removed for bindings only in old commands.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) # Old command with grabs old_kb = self._create_mock_keybinding(test_context, keysymstring="h", modifiers=256) old_kb.has_grabs.return_value = True old_cmd = KeyboardCommand("old_cmd", function, "Group", desktop_keybinding=old_kb) old_cmd.set_keybinding(old_kb) old_commands = {"old_cmd": old_cmd} # Empty new commands new_commands: dict = {} manager._keyboard_commands = old_commands manager._diff_and_update_grabs(new_commands, "test") # Verify grabs were removed old_kb.remove_grabs.assert_called_once() def test_diff_adds_grabs_for_new_only_bindings(self, test_context: CthulhuTestContext) -> None: """Test that grabs are added for bindings only in new commands.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) # Empty old commands old_commands: dict = {} # New command without grabs new_kb = self._create_mock_keybinding(test_context, keysymstring="h", modifiers=256) new_kb.has_grabs.return_value = False new_cmd = KeyboardCommand("new_cmd", function, "Group", desktop_keybinding=new_kb) new_cmd.set_keybinding(new_kb) new_commands = {"new_cmd": new_cmd} manager._keyboard_commands = old_commands manager._diff_and_update_grabs(new_commands, "test") # Verify grabs were added new_kb.add_grabs.assert_called_once() def test_set_active_commands_uses_diff(self, test_context: CthulhuTestContext) -> None: """Test set_active_commands uses diff-based updates.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) # Add initial command with grabs old_kb = self._create_mock_keybinding(test_context, keysymstring="a") old_kb.has_grabs.return_value = True old_cmd = KeyboardCommand("old_cmd", function, "Group", desktop_keybinding=old_kb) old_cmd.set_keybinding(old_kb) manager.add_command(old_cmd) # Set new commands new_kb = self._create_mock_keybinding(test_context, keysymstring="b") new_kb.has_grabs.return_value = False new_cmd = KeyboardCommand("new_cmd", function, "Group", desktop_keybinding=new_kb) new_cmd.set_keybinding(new_kb) manager.set_active_commands({"new_cmd": new_cmd}, "test") # Old binding should have grabs removed, new binding should have grabs added old_kb.remove_grabs.assert_called_once() new_kb.add_grabs.assert_called_once() def test_activate_commands_applies_overrides(self, test_context: CthulhuTestContext) -> None: """Test activate_commands applies user overrides.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) # Add a command kb = self._create_mock_keybinding(test_context, keysymstring="a") cmd = KeyboardCommand("test_cmd", function, "Group", desktop_keybinding=kb) cmd.set_keybinding(kb) manager.add_command(cmd) # activate_commands reads overrides via layered_lookup on the registry. # With no user values in the memory backend, no overrides are applied. manager.activate_commands("test") # The command should still have its original keybinding unchanged. assert cmd.get_keybinding() is kb def test_diff_skips_inactive_commands(self, test_context: CthulhuTestContext) -> None: """Test that diff skips inactive commands.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) # Old command that is disabled (inactive) old_kb = self._create_mock_keybinding(test_context, keysymstring="h") old_kb.has_grabs.return_value = True old_cmd = KeyboardCommand( "cmd", function, "Group", desktop_keybinding=old_kb, enabled=False, ) old_cmd.set_keybinding(old_kb) old_commands = {"cmd": old_cmd} # New command that is also disabled new_kb = self._create_mock_keybinding(test_context, keysymstring="h") new_cmd = KeyboardCommand( "cmd", function, "Group", desktop_keybinding=new_kb, enabled=False, ) new_cmd.set_keybinding(new_kb) new_commands = {"cmd": new_cmd} manager._keyboard_commands = old_commands manager._diff_and_update_grabs(new_commands, "test") # Neither should have grabs modified since both are inactive old_kb.remove_grabs.assert_not_called() new_kb.add_grabs.assert_not_called() new_kb.set_grab_ids.assert_not_called() def test_set_group_suspended_removes_grabs_when_suspending( self, test_context: CthulhuTestContext, ) -> None: """Test set_group_suspended removes grabs when suspending active commands.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) # Active command with grabs kb = self._create_mock_keybinding(test_context, keysymstring="h") kb.has_grabs.return_value = True cmd = KeyboardCommand("cmd", function, "Test Group", desktop_keybinding=kb) cmd.set_keybinding(kb) manager.add_command(cmd) # Suspend the group manager.set_group_suspended("Test Group", True) # Grabs should be removed kb.remove_grabs.assert_called_once() kb.add_grabs.assert_not_called() def test_set_group_suspended_adds_grabs_when_unsuspending( self, test_context: CthulhuTestContext, ) -> None: """Test set_group_suspended adds grabs when unsuspending commands.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) # Suspended command without grabs kb = self._create_mock_keybinding(test_context, keysymstring="h") kb.has_grabs.return_value = False cmd = KeyboardCommand("cmd", function, "Test Group", desktop_keybinding=kb, suspended=True) cmd.set_keybinding(kb) manager.add_command(cmd) # Unsuspend the group manager.set_group_suspended("Test Group", False) # Grabs should be added kb.add_grabs.assert_called_once() kb.remove_grabs.assert_not_called() def test_set_group_suspended_no_change_when_already_in_state( self, test_context: CthulhuTestContext, ) -> None: """Test set_group_suspended does nothing when commands already in desired state.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) # Already suspended command kb = self._create_mock_keybinding(test_context, keysymstring="h") kb.has_grabs.return_value = False cmd = KeyboardCommand("cmd", function, "Test Group", desktop_keybinding=kb, suspended=True) cmd.set_keybinding(kb) manager.add_command(cmd) # Suspend again (no change) manager.set_group_suspended("Test Group", True) # No grabs should be modified kb.add_grabs.assert_not_called() kb.remove_grabs.assert_not_called() def test_set_group_enabled_removes_grabs_when_disabling( self, test_context: CthulhuTestContext, ) -> None: """Test set_group_enabled removes grabs when disabling active commands.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) # Active command with grabs kb = self._create_mock_keybinding(test_context, keysymstring="h") kb.has_grabs.return_value = True cmd = KeyboardCommand("cmd", function, "Test Group", desktop_keybinding=kb) cmd.set_keybinding(kb) manager.add_command(cmd) # Disable the group manager.set_group_enabled("Test Group", False) # Grabs should be removed kb.remove_grabs.assert_called_once() kb.add_grabs.assert_not_called() def test_set_group_enabled_skips_group_toggle_commands( self, test_context: CthulhuTestContext, ) -> None: """Test set_group_enabled skips group toggle commands.""" self._setup_dependencies(test_context) from cthulhu.command_manager import CommandManager, KeyboardCommand manager = CommandManager() function = self._create_mock_function(test_context) # Group toggle command with grabs kb = self._create_mock_keybinding(test_context, keysymstring="z") kb.has_grabs.return_value = True cmd = KeyboardCommand( "toggle_cmd", function, "Test Group", desktop_keybinding=kb, is_group_toggle=True, ) cmd.set_keybinding(kb) manager.add_command(cmd) # Disable the group manager.set_group_enabled("Test Group", False) # Group toggle should not have grabs removed (it stays active) kb.remove_grabs.assert_not_called() # Command should still be enabled assert cmd.is_enabled() is True