Files
voculinux/tests/test_tray_indicator_ext.py
T
Jatin K MalikandGitHub fa17b88f74 chore: bump minimum Python from 3.8 to 3.9 (#336)
* 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.
2026-03-21 12:15:10 -07:00

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()