* chore: bump minimum Python from 3.8 to 3.9 and adopt PEP 585 builtins - Update requires-python, classifiers, mypy target, and black target in pyproject.toml - Replace Python 3.8 with 3.9 in CI matrix and nightly workflow - Replace typing.List/Dict/Tuple/Set with builtin list/dict/tuple/set (PEP 585) - Update all 3.8 version references in README, docs, website, and install.sh - Fix test_wayland_no_tools to mock IBus availability - Remove List from common_types re-exports and update test * style: reformat test files for Black 26.x and pin version in CI Black 26.x introduced parenthesized context manager formatting. Reformat 7 test files and pin Black>=26,<27 in CI to prevent future drift from unpinned version upgrades.
518 lines
21 KiB
Python
518 lines
21 KiB
Python
"""
|
|
Additional coverage tests for tray_indicator.py module.
|
|
|
|
Tests for TrayIndicator class methods including initialization, menu creation,
|
|
state handling, and UI updates.
|
|
"""
|
|
|
|
import os
|
|
import signal
|
|
import sys
|
|
import unittest
|
|
from unittest.mock import MagicMock, call, patch
|
|
|
|
import pytest
|
|
|
|
# Create mock modules for GTK/GI BEFORE importing anything
|
|
mock_gi = MagicMock()
|
|
mock_gi.require_version = MagicMock()
|
|
|
|
mock_gtk = MagicMock()
|
|
mock_glib = MagicMock()
|
|
mock_gobject = MagicMock()
|
|
mock_gdkpixbuf = MagicMock()
|
|
mock_appindicator = MagicMock()
|
|
|
|
# Create mock for gi.repository
|
|
mock_gi_repository = MagicMock()
|
|
mock_gi_repository.Gtk = mock_gtk
|
|
mock_gi_repository.GLib = mock_glib
|
|
mock_gi_repository.GObject = mock_gobject
|
|
mock_gi_repository.GdkPixbuf = mock_gdkpixbuf
|
|
mock_gi_repository.AppIndicator3 = mock_appindicator
|
|
|
|
# Inject mocks into sys.modules BEFORE any imports
|
|
sys.modules["gi"] = mock_gi
|
|
sys.modules["gi.repository"] = mock_gi_repository
|
|
|
|
|
|
class TestTrayIndicatorInitialization(unittest.TestCase):
|
|
"""Tests for TrayIndicator initialization."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
"""Set up class fixtures."""
|
|
# Ensure mocks are in place for the entire test class
|
|
sys.modules["gi"] = mock_gi
|
|
sys.modules["gi.repository"] = mock_gi_repository
|
|
|
|
def setUp(self):
|
|
"""Set up test environment before each test."""
|
|
# Reset all GTK mocks to avoid test pollution
|
|
mock_gtk.reset_mock()
|
|
mock_glib.reset_mock()
|
|
mock_gobject.reset_mock()
|
|
mock_gdkpixbuf.reset_mock()
|
|
mock_appindicator.reset_mock()
|
|
|
|
# Configure idle_add to execute the function directly
|
|
mock_glib.idle_add.side_effect = lambda func, *args: func(*args) or False
|
|
|
|
# Clear any cached imports of tray_indicator
|
|
modules_to_remove = [k for k in list(sys.modules.keys()) if "tray_indicator" in k]
|
|
for mod in modules_to_remove:
|
|
del sys.modules[mod]
|
|
|
|
def test_tray_indicator_init(self):
|
|
"""Test TrayIndicator initialization."""
|
|
# Import BEFORE patching to ensure mocks are already in place
|
|
from vocalinux.common_types import RecognitionState
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
with (
|
|
patch("vocalinux.ui.tray_indicator.logging"),
|
|
patch("vocalinux.ui.tray_indicator._resource_manager") as mock_resource_manager,
|
|
patch("vocalinux.ui.tray_indicator.ConfigManager") as mock_config_manager,
|
|
patch("vocalinux.ui.tray_indicator.KeyboardShortcutManager") as mock_keyboard_manager,
|
|
):
|
|
|
|
# Set up mocks
|
|
mock_speech_engine = MagicMock()
|
|
mock_speech_engine.state = RecognitionState.IDLE
|
|
mock_text_injector = MagicMock()
|
|
|
|
# Mock config manager
|
|
mock_config_inst = MagicMock()
|
|
mock_config_inst.get.return_value = "ctrl+ctrl"
|
|
mock_config_manager.return_value = mock_config_inst
|
|
|
|
# Mock keyboard shortcut manager
|
|
mock_keyboard_inst = MagicMock()
|
|
mock_keyboard_inst.active = False
|
|
mock_keyboard_manager.return_value = mock_keyboard_inst
|
|
|
|
# Mock resource manager
|
|
mock_resource_manager.icons_dir = "/tmp/icons"
|
|
mock_resource_manager.get_icon_path.return_value = "/tmp/icons/test.png"
|
|
mock_resource_manager.ensure_directories_exist.return_value = None
|
|
mock_resource_manager.validate_resources.return_value = {
|
|
"resources_dir_exists": True,
|
|
"missing_icons": [],
|
|
"missing_sounds": [],
|
|
}
|
|
|
|
# Initialize
|
|
indicator = TrayIndicator(
|
|
speech_engine=mock_speech_engine,
|
|
text_injector=mock_text_injector,
|
|
)
|
|
|
|
# Verify initialization
|
|
assert indicator.speech_engine == mock_speech_engine
|
|
assert indicator.text_injector == mock_text_injector
|
|
assert indicator.config_manager == mock_config_inst
|
|
assert indicator.shortcut_manager == mock_keyboard_inst
|
|
assert indicator._syncing_autostart_menu is False
|
|
|
|
# Verify that idle_add was called to initialize the indicator
|
|
mock_glib.idle_add.assert_called()
|
|
|
|
def test_tray_indicator_registers_state_callback(self):
|
|
"""Test that TrayIndicator registers state callback on speech engine."""
|
|
# Import BEFORE patching to ensure mocks are already in place
|
|
from vocalinux.common_types import RecognitionState
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
with (
|
|
patch("vocalinux.ui.tray_indicator.logging"),
|
|
patch("vocalinux.ui.tray_indicator._resource_manager") as mock_resource_manager,
|
|
patch("vocalinux.ui.tray_indicator.ConfigManager") as mock_config_manager,
|
|
patch("vocalinux.ui.tray_indicator.KeyboardShortcutManager") as mock_keyboard_manager,
|
|
):
|
|
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
# Set up mocks
|
|
mock_speech_engine = MagicMock()
|
|
mock_text_injector = MagicMock()
|
|
|
|
mock_config_inst = MagicMock()
|
|
mock_config_inst.get.return_value = "ctrl+ctrl"
|
|
mock_config_manager.return_value = mock_config_inst
|
|
|
|
mock_keyboard_inst = MagicMock()
|
|
mock_keyboard_inst.active = False
|
|
mock_keyboard_manager.return_value = mock_keyboard_inst
|
|
|
|
mock_resource_manager.icons_dir = "/tmp/icons"
|
|
mock_resource_manager.get_icon_path.return_value = "/tmp/icons/test.png"
|
|
mock_resource_manager.ensure_directories_exist.return_value = None
|
|
mock_resource_manager.validate_resources.return_value = {
|
|
"resources_dir_exists": True,
|
|
"missing_icons": [],
|
|
"missing_sounds": [],
|
|
}
|
|
|
|
# Initialize
|
|
TrayIndicator(
|
|
speech_engine=mock_speech_engine,
|
|
text_injector=mock_text_injector,
|
|
)
|
|
|
|
# Verify callback was registered
|
|
mock_speech_engine.register_state_callback.assert_called_once()
|
|
|
|
|
|
class TestTrayIndicatorMenuOperations(unittest.TestCase):
|
|
"""Tests for TrayIndicator menu operations."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
"""Set up class fixtures."""
|
|
sys.modules["gi"] = mock_gi
|
|
sys.modules["gi.repository"] = mock_gi_repository
|
|
|
|
def setUp(self):
|
|
"""Set up test environment before each test."""
|
|
mock_gtk.reset_mock()
|
|
mock_glib.reset_mock()
|
|
mock_gobject.reset_mock()
|
|
mock_gdkpixbuf.reset_mock()
|
|
mock_appindicator.reset_mock()
|
|
mock_glib.idle_add.side_effect = lambda func, *args: func(*args) or False
|
|
modules_to_remove = [k for k in list(sys.modules.keys()) if "tray_indicator" in k]
|
|
for mod in modules_to_remove:
|
|
del sys.modules[mod]
|
|
|
|
def test_add_menu_item(self):
|
|
"""Test adding a menu item."""
|
|
# Import BEFORE patching to ensure mocks are already in place
|
|
from vocalinux.common_types import RecognitionState
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
with (
|
|
patch("vocalinux.ui.tray_indicator.logging"),
|
|
patch("vocalinux.ui.tray_indicator._resource_manager") as mock_resource_manager,
|
|
patch("vocalinux.ui.tray_indicator.ConfigManager") as mock_config_manager,
|
|
patch("vocalinux.ui.tray_indicator.KeyboardShortcutManager") as mock_keyboard_manager,
|
|
):
|
|
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
mock_speech_engine = MagicMock()
|
|
mock_text_injector = MagicMock()
|
|
mock_config_inst = MagicMock()
|
|
mock_config_inst.get.return_value = "ctrl+ctrl"
|
|
mock_config_manager.return_value = mock_config_inst
|
|
mock_keyboard_inst = MagicMock()
|
|
mock_keyboard_inst.active = False
|
|
mock_keyboard_manager.return_value = mock_keyboard_inst
|
|
mock_resource_manager.icons_dir = "/tmp/icons"
|
|
mock_resource_manager.get_icon_path.return_value = "/tmp/icons/test.png"
|
|
mock_resource_manager.ensure_directories_exist.return_value = None
|
|
mock_resource_manager.validate_resources.return_value = {
|
|
"resources_dir_exists": True,
|
|
"missing_icons": [],
|
|
"missing_sounds": [],
|
|
}
|
|
|
|
indicator = TrayIndicator(
|
|
speech_engine=mock_speech_engine,
|
|
text_injector=mock_text_injector,
|
|
)
|
|
|
|
assert indicator is not None
|
|
|
|
def test_add_menu_separator(self):
|
|
"""Test adding a menu separator."""
|
|
# Import BEFORE patching to ensure mocks are already in place
|
|
from vocalinux.common_types import RecognitionState
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
with (
|
|
patch("vocalinux.ui.tray_indicator.logging"),
|
|
patch("vocalinux.ui.tray_indicator._resource_manager") as mock_resource_manager,
|
|
patch("vocalinux.ui.tray_indicator.ConfigManager") as mock_config_manager,
|
|
patch("vocalinux.ui.tray_indicator.KeyboardShortcutManager") as mock_keyboard_manager,
|
|
):
|
|
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
mock_speech_engine = MagicMock()
|
|
mock_text_injector = MagicMock()
|
|
mock_config_inst = MagicMock()
|
|
mock_config_inst.get.return_value = "ctrl+ctrl"
|
|
mock_config_manager.return_value = mock_config_inst
|
|
mock_keyboard_inst = MagicMock()
|
|
mock_keyboard_inst.active = False
|
|
mock_keyboard_manager.return_value = mock_keyboard_inst
|
|
mock_resource_manager.icons_dir = "/tmp/icons"
|
|
mock_resource_manager.get_icon_path.return_value = "/tmp/icons/test.png"
|
|
mock_resource_manager.ensure_directories_exist.return_value = None
|
|
mock_resource_manager.validate_resources.return_value = {
|
|
"resources_dir_exists": True,
|
|
"missing_icons": [],
|
|
"missing_sounds": [],
|
|
}
|
|
|
|
indicator = TrayIndicator(
|
|
speech_engine=mock_speech_engine,
|
|
text_injector=mock_text_injector,
|
|
)
|
|
|
|
assert indicator is not None
|
|
|
|
|
|
class TestTrayIndicatorStateHandling(unittest.TestCase):
|
|
"""Tests for TrayIndicator state handling."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
"""Set up class fixtures."""
|
|
sys.modules["gi"] = mock_gi
|
|
sys.modules["gi.repository"] = mock_gi_repository
|
|
|
|
def setUp(self):
|
|
"""Set up test environment before each test."""
|
|
mock_gtk.reset_mock()
|
|
mock_glib.reset_mock()
|
|
mock_gobject.reset_mock()
|
|
mock_gdkpixbuf.reset_mock()
|
|
mock_appindicator.reset_mock()
|
|
mock_glib.idle_add.side_effect = lambda func, *args: func(*args) or False
|
|
modules_to_remove = [k for k in list(sys.modules.keys()) if "tray_indicator" in k]
|
|
for mod in modules_to_remove:
|
|
del sys.modules[mod]
|
|
|
|
def test_on_recognition_state_changed(self):
|
|
"""Test state change callback."""
|
|
# Import BEFORE patching to ensure mocks are already in place
|
|
from vocalinux.common_types import RecognitionState
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
with (
|
|
patch("vocalinux.ui.tray_indicator.logging"),
|
|
patch("vocalinux.ui.tray_indicator._resource_manager") as mock_resource_manager,
|
|
patch("vocalinux.ui.tray_indicator.ConfigManager") as mock_config_manager,
|
|
patch("vocalinux.ui.tray_indicator.KeyboardShortcutManager") as mock_keyboard_manager,
|
|
):
|
|
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
mock_speech_engine = MagicMock()
|
|
mock_text_injector = MagicMock()
|
|
mock_config_inst = MagicMock()
|
|
mock_config_inst.get.return_value = "ctrl+ctrl"
|
|
mock_config_manager.return_value = mock_config_inst
|
|
mock_keyboard_inst = MagicMock()
|
|
mock_keyboard_inst.active = False
|
|
mock_keyboard_manager.return_value = mock_keyboard_inst
|
|
mock_resource_manager.icons_dir = "/tmp/icons"
|
|
mock_resource_manager.get_icon_path.return_value = "/tmp/icons/test.png"
|
|
mock_resource_manager.ensure_directories_exist.return_value = None
|
|
mock_resource_manager.validate_resources.return_value = {
|
|
"resources_dir_exists": True,
|
|
"missing_icons": [],
|
|
"missing_sounds": [],
|
|
}
|
|
|
|
indicator = TrayIndicator(
|
|
speech_engine=mock_speech_engine,
|
|
text_injector=mock_text_injector,
|
|
)
|
|
|
|
assert indicator is not None
|
|
|
|
def test_update_ui_idle_state(self):
|
|
"""Test UI update for idle state."""
|
|
# Import BEFORE patching to ensure mocks are already in place
|
|
from vocalinux.common_types import RecognitionState
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
with (
|
|
patch("vocalinux.ui.tray_indicator.logging"),
|
|
patch("vocalinux.ui.tray_indicator._resource_manager") as mock_resource_manager,
|
|
patch("vocalinux.ui.tray_indicator.ConfigManager") as mock_config_manager,
|
|
patch("vocalinux.ui.tray_indicator.KeyboardShortcutManager") as mock_keyboard_manager,
|
|
):
|
|
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
mock_speech_engine = MagicMock()
|
|
mock_text_injector = MagicMock()
|
|
mock_config_inst = MagicMock()
|
|
mock_config_inst.get.return_value = "ctrl+ctrl"
|
|
mock_config_manager.return_value = mock_config_inst
|
|
mock_keyboard_inst = MagicMock()
|
|
mock_keyboard_inst.active = False
|
|
mock_keyboard_manager.return_value = mock_keyboard_inst
|
|
mock_resource_manager.icons_dir = "/tmp/icons"
|
|
mock_resource_manager.get_icon_path.return_value = "/tmp/icons/test.png"
|
|
mock_resource_manager.ensure_directories_exist.return_value = None
|
|
mock_resource_manager.validate_resources.return_value = {
|
|
"resources_dir_exists": True,
|
|
"missing_icons": [],
|
|
"missing_sounds": [],
|
|
}
|
|
|
|
indicator = TrayIndicator(
|
|
speech_engine=mock_speech_engine,
|
|
text_injector=mock_text_injector,
|
|
)
|
|
|
|
assert indicator is not None
|
|
|
|
def test_update_ui_listening_state(self):
|
|
"""Test UI update for listening state."""
|
|
# Import BEFORE patching to ensure mocks are already in place
|
|
from vocalinux.common_types import RecognitionState
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
with (
|
|
patch("vocalinux.ui.tray_indicator.logging"),
|
|
patch("vocalinux.ui.tray_indicator._resource_manager") as mock_resource_manager,
|
|
patch("vocalinux.ui.tray_indicator.ConfigManager") as mock_config_manager,
|
|
patch("vocalinux.ui.tray_indicator.KeyboardShortcutManager") as mock_keyboard_manager,
|
|
):
|
|
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
mock_speech_engine = MagicMock()
|
|
mock_text_injector = MagicMock()
|
|
mock_config_inst = MagicMock()
|
|
mock_config_inst.get.return_value = "ctrl+ctrl"
|
|
mock_config_manager.return_value = mock_config_inst
|
|
mock_keyboard_inst = MagicMock()
|
|
mock_keyboard_inst.active = False
|
|
mock_keyboard_manager.return_value = mock_keyboard_inst
|
|
mock_resource_manager.icons_dir = "/tmp/icons"
|
|
mock_resource_manager.get_icon_path.return_value = "/tmp/icons/test.png"
|
|
mock_resource_manager.ensure_directories_exist.return_value = None
|
|
mock_resource_manager.validate_resources.return_value = {
|
|
"resources_dir_exists": True,
|
|
"missing_icons": [],
|
|
"missing_sounds": [],
|
|
}
|
|
|
|
indicator = TrayIndicator(
|
|
speech_engine=mock_speech_engine,
|
|
text_injector=mock_text_injector,
|
|
)
|
|
|
|
assert indicator is not None
|
|
|
|
|
|
class TestTrayIndicatorSignalHandling(unittest.TestCase):
|
|
"""Tests for TrayIndicator signal handling."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
"""Set up class fixtures."""
|
|
sys.modules["gi"] = mock_gi
|
|
sys.modules["gi.repository"] = mock_gi_repository
|
|
|
|
def setUp(self):
|
|
"""Set up test environment before each test."""
|
|
mock_gtk.reset_mock()
|
|
mock_glib.reset_mock()
|
|
mock_gobject.reset_mock()
|
|
mock_gdkpixbuf.reset_mock()
|
|
mock_appindicator.reset_mock()
|
|
mock_glib.idle_add.side_effect = lambda func, *args: func(*args) or False
|
|
modules_to_remove = [k for k in list(sys.modules.keys()) if "tray_indicator" in k]
|
|
for mod in modules_to_remove:
|
|
del sys.modules[mod]
|
|
|
|
def test_signal_handler(self):
|
|
"""Test signal handler for graceful termination."""
|
|
# Import BEFORE patching to ensure mocks are already in place
|
|
from vocalinux.common_types import RecognitionState
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
with (
|
|
patch("vocalinux.ui.tray_indicator.logging"),
|
|
patch("vocalinux.ui.tray_indicator._resource_manager") as mock_resource_manager,
|
|
patch("vocalinux.ui.tray_indicator.ConfigManager") as mock_config_manager,
|
|
patch("vocalinux.ui.tray_indicator.KeyboardShortcutManager") as mock_keyboard_manager,
|
|
):
|
|
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
mock_speech_engine = MagicMock()
|
|
mock_text_injector = MagicMock()
|
|
mock_config_inst = MagicMock()
|
|
mock_config_inst.get.return_value = "ctrl+ctrl"
|
|
mock_config_manager.return_value = mock_config_inst
|
|
mock_keyboard_inst = MagicMock()
|
|
mock_keyboard_inst.active = False
|
|
mock_keyboard_manager.return_value = mock_keyboard_inst
|
|
mock_resource_manager.icons_dir = "/tmp/icons"
|
|
mock_resource_manager.get_icon_path.return_value = "/tmp/icons/test.png"
|
|
mock_resource_manager.ensure_directories_exist.return_value = None
|
|
mock_resource_manager.validate_resources.return_value = {
|
|
"resources_dir_exists": True,
|
|
"missing_icons": [],
|
|
"missing_sounds": [],
|
|
}
|
|
|
|
indicator = TrayIndicator(
|
|
speech_engine=mock_speech_engine,
|
|
text_injector=mock_text_injector,
|
|
)
|
|
|
|
# Test signal handler
|
|
indicator._signal_handler(signal.SIGINT, None)
|
|
|
|
# Verify idle_add was called to quit
|
|
mock_glib.idle_add.assert_called()
|
|
|
|
def test_update_shortcut(self):
|
|
"""Test updating keyboard shortcut."""
|
|
# Import BEFORE patching to ensure mocks are already in place
|
|
from vocalinux.common_types import RecognitionState
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
with (
|
|
patch("vocalinux.ui.tray_indicator.logging"),
|
|
patch("vocalinux.ui.tray_indicator._resource_manager") as mock_resource_manager,
|
|
patch("vocalinux.ui.tray_indicator.ConfigManager") as mock_config_manager,
|
|
patch("vocalinux.ui.tray_indicator.KeyboardShortcutManager") as mock_keyboard_manager,
|
|
):
|
|
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
mock_speech_engine = MagicMock()
|
|
mock_text_injector = MagicMock()
|
|
mock_config_inst = MagicMock()
|
|
mock_config_inst.get.return_value = "ctrl+ctrl"
|
|
mock_config_manager.return_value = mock_config_inst
|
|
mock_keyboard_inst = MagicMock()
|
|
mock_keyboard_inst.active = True
|
|
mock_keyboard_inst.shortcut = "ctrl+ctrl"
|
|
mock_keyboard_inst.mode = "toggle"
|
|
mock_keyboard_inst.set_shortcut.return_value = True
|
|
mock_keyboard_manager.return_value = mock_keyboard_inst
|
|
mock_resource_manager.icons_dir = "/tmp/icons"
|
|
mock_resource_manager.get_icon_path.return_value = "/tmp/icons/test.png"
|
|
mock_resource_manager.ensure_directories_exist.return_value = None
|
|
mock_resource_manager.validate_resources.return_value = {
|
|
"resources_dir_exists": True,
|
|
"missing_icons": [],
|
|
"missing_sounds": [],
|
|
}
|
|
|
|
indicator = TrayIndicator(
|
|
speech_engine=mock_speech_engine,
|
|
text_injector=mock_text_injector,
|
|
)
|
|
|
|
# Test updating shortcut
|
|
result = indicator.update_shortcut("alt+alt")
|
|
|
|
# Verify the update succeeded
|
|
assert isinstance(result, bool)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|