Files
cthulhu/tests/test_command_manager.py

1418 lines
52 KiB
Python

# Unit tests for command_manager.py methods.
#
# Copyright 2025 Igalia, S.L.
# Author: Joanmarie Diggs <jdiggs@igalia.com>
#
# 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