* feat(tray): remove Start on Login and About from the tray menu
Both entries already live in Settings: the autostart switch is in the
general page and About is a dedicated sidebar page, so the tray items
are redundant. View Logs stays.
Removes the Start on Login checkbox item along with its
_update_autostart_checkbox/_on_autostart_toggled wiring and the now
unused _add_menu_checkbox helper, plus the About item and
_on_about_clicked. _show_settings_page("about") stays for the Update
Available item. autostart_manager is untouched since Settings uses it.
Fixes #654
* fix(settings): sync autostart switch with the real desktop entry
The tray's removed _update_autostart_checkbox also reconciled
general.autostart with the actual login desktop entry at startup. Move
that check into the settings load so the Start on Login switch still
reflects reality when the entry changes outside the app.
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
719 lines
30 KiB
Python
719 lines
30 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.get_shared_config_manager") 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
|
|
|
|
# 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.get_shared_config_manager") 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.get_shared_config_manager") 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.get_shared_config_manager") 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.get_shared_config_manager") 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.get_shared_config_manager") 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.get_shared_config_manager") 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.get_shared_config_manager") 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.get_shared_config_manager") 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)
|
|
|
|
|
|
class TestExternalActivationGate(unittest.TestCase):
|
|
"""Tests for the D-Bus external-activation config gate.
|
|
|
|
These live here (and not in test_dbus_activation.py) so that the real
|
|
tray_indicator module is imported late in the session -- after the tests
|
|
that swap sys.modules["gi.repository"] at runtime. Importing it earlier
|
|
would freeze tray_indicator.GLib against a stale mock and break unrelated
|
|
GLib-identity assertions (e.g. in test_suspend_handler.py).
|
|
"""
|
|
|
|
@staticmethod
|
|
def _fake_indicator(active: bool) -> MagicMock:
|
|
"""Minimal stand-in for calling _setup_keyboard_shortcuts in isolation.
|
|
|
|
``active`` stubs _external_activation_active() directly rather than
|
|
the config it normally derives from -- that derivation (including the
|
|
once-failed-stays-off persistence) has its own tests below.
|
|
"""
|
|
fake = MagicMock()
|
|
fake.shortcut_manager.active = False
|
|
fake.config_manager.get_str.return_value = "toggle"
|
|
fake._external_activation_active.return_value = active
|
|
return fake
|
|
|
|
def test_external_mode_skips_listener_start(self):
|
|
"""With the option enabled, the evdev/pynput listener is not started."""
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
fake = self._fake_indicator(active=True)
|
|
TrayIndicator._setup_keyboard_shortcuts(fake)
|
|
fake.shortcut_manager.start.assert_not_called()
|
|
fake.shortcut_manager.register_toggle_callback.assert_called_once_with(None)
|
|
|
|
def test_default_mode_starts_listener(self):
|
|
"""With the option disabled (default), the listener starts as before."""
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
fake = self._fake_indicator(active=False)
|
|
TrayIndicator._setup_keyboard_shortcuts(fake)
|
|
fake.shortcut_manager.start.assert_called_once()
|
|
|
|
def test_reconfigure_stops_active_recognition_first(self):
|
|
"""An active push-to-talk session is stopped before the listener that
|
|
would have delivered its release callback is torn down, so it cannot
|
|
be left running with no way back to idle."""
|
|
from vocalinux.common_types import RecognitionState
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
fake = self._fake_indicator(active=True)
|
|
fake.speech_engine.state = RecognitionState.LISTENING
|
|
TrayIndicator._setup_keyboard_shortcuts(fake)
|
|
fake._stop_recognition.assert_called_once_with()
|
|
|
|
def test_reconfigure_leaves_idle_recognition_alone(self):
|
|
"""No spurious stop is issued when nothing is active."""
|
|
from vocalinux.common_types import RecognitionState
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
fake = self._fake_indicator(active=False)
|
|
fake.speech_engine.state = RecognitionState.IDLE
|
|
TrayIndicator._setup_keyboard_shortcuts(fake)
|
|
fake._stop_recognition.assert_not_called()
|
|
|
|
|
|
class TestExternalActivationActiveGate(unittest.TestCase):
|
|
"""Tests for _external_activation_active(): the saved setting, unless the
|
|
D-Bus service has already failed to register this run."""
|
|
|
|
@staticmethod
|
|
def _fake_indicator(disable_internal_hotkey: bool, unavailable: bool) -> MagicMock:
|
|
fake = MagicMock()
|
|
fake.config_manager.get_bool.return_value = disable_internal_hotkey
|
|
fake._external_activation_unavailable = unavailable
|
|
return fake
|
|
|
|
def test_true_when_configured_and_available(self):
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
fake = self._fake_indicator(disable_internal_hotkey=True, unavailable=False)
|
|
assert TrayIndicator._external_activation_active(fake) is True
|
|
|
|
def test_false_when_not_configured(self):
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
fake = self._fake_indicator(disable_internal_hotkey=False, unavailable=False)
|
|
assert TrayIndicator._external_activation_active(fake) is False
|
|
|
|
def test_false_once_unavailable_even_if_still_configured(self):
|
|
"""Once the service has failed, the saved setting is overridden for
|
|
the rest of the process -- it does not retry."""
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
fake = self._fake_indicator(disable_internal_hotkey=True, unavailable=True)
|
|
assert TrayIndicator._external_activation_active(fake) is False
|
|
|
|
|
|
class TestDBusRegistrationFailedFallback(unittest.TestCase):
|
|
"""Tests for falling back to the internal listener when the D-Bus service
|
|
could not register, so the user is never left with no activation path."""
|
|
|
|
@staticmethod
|
|
def _fake_indicator(disable_internal_hotkey: bool) -> MagicMock:
|
|
fake = MagicMock()
|
|
fake.config_manager.get_bool.return_value = disable_internal_hotkey
|
|
return fake
|
|
|
|
@patch("vocalinux.ui.tray_indicator.notifications")
|
|
def test_falls_back_when_external_activation_configured(self, mock_notifications):
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
fake = self._fake_indicator(disable_internal_hotkey=True)
|
|
TrayIndicator._on_dbus_registration_failed(fake)
|
|
assert fake._external_activation_unavailable is True
|
|
fake._setup_keyboard_shortcuts.assert_called_once_with()
|
|
mock_notifications.notify.assert_called_once()
|
|
|
|
@patch("vocalinux.ui.tray_indicator.notifications")
|
|
def test_fallback_persists_across_a_later_reconfigure(self, mock_notifications):
|
|
"""The exact regression this fixes: a reconfigure after the fallback
|
|
(mode change, settings toggle, resume) must not re-disable the only
|
|
working activation path just because the saved setting still asks
|
|
for external activation."""
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
with patch.object(TrayIndicator, "__init__", lambda self: None):
|
|
indicator = TrayIndicator.__new__(TrayIndicator)
|
|
indicator._external_activation_unavailable = False
|
|
indicator.config_manager = MagicMock()
|
|
indicator.config_manager.get_bool.return_value = True
|
|
indicator.config_manager.get_str.return_value = "toggle"
|
|
indicator.speech_engine = MagicMock()
|
|
indicator.shortcut_manager = MagicMock()
|
|
indicator.shortcut_manager.active = False
|
|
|
|
indicator._on_dbus_registration_failed()
|
|
indicator.shortcut_manager.start.assert_called_once()
|
|
|
|
# A later reconfigure -- e.g. the mode combo changing in Settings --
|
|
# calls this with no knowledge of the earlier failure.
|
|
indicator.shortcut_manager.reset_mock()
|
|
indicator._setup_keyboard_shortcuts()
|
|
indicator.shortcut_manager.start.assert_called_once()
|
|
|
|
def test_noop_when_internal_listener_already_active(self):
|
|
"""Nothing to fall back to/from if the internal listener was already
|
|
the active mode."""
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
fake = self._fake_indicator(disable_internal_hotkey=False)
|
|
TrayIndicator._on_dbus_registration_failed(fake)
|
|
fake._setup_keyboard_shortcuts.assert_not_called()
|
|
|
|
|
|
class TestExternalTriggerHandlers(unittest.TestCase):
|
|
"""Tests for the D-Bus external Start/Stop handlers on TrayIndicator.
|
|
|
|
These use normal (non push-to-talk) start semantics and guard on the
|
|
current recognition state, so repeated `vocalinux --start`/`--stop` calls
|
|
are idempotent.
|
|
"""
|
|
|
|
def test_external_start_starts_when_idle(self):
|
|
"""_external_start starts recognition only when currently idle."""
|
|
from vocalinux.common_types import RecognitionState
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
fake = MagicMock()
|
|
fake.speech_engine.state = RecognitionState.IDLE
|
|
TrayIndicator._external_start(fake)
|
|
fake.speech_engine.start_recognition.assert_called_once_with()
|
|
|
|
def test_external_start_noop_when_already_running(self):
|
|
"""_external_start does nothing if recognition is already active."""
|
|
from vocalinux.common_types import RecognitionState
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
fake = MagicMock()
|
|
fake.speech_engine.state = RecognitionState.LISTENING
|
|
TrayIndicator._external_start(fake)
|
|
fake.speech_engine.start_recognition.assert_not_called()
|
|
|
|
def test_external_stop_stops_when_active(self):
|
|
"""_external_stop stops recognition when it is not idle."""
|
|
from vocalinux.common_types import RecognitionState
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
fake = MagicMock()
|
|
fake.speech_engine.state = RecognitionState.LISTENING
|
|
TrayIndicator._external_stop(fake)
|
|
fake.speech_engine.stop_recognition.assert_called_once_with()
|
|
|
|
def test_external_stop_noop_when_idle(self):
|
|
"""_external_stop does nothing if recognition is already idle."""
|
|
from vocalinux.common_types import RecognitionState
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
fake = MagicMock()
|
|
fake.speech_engine.state = RecognitionState.IDLE
|
|
TrayIndicator._external_stop(fake)
|
|
fake.speech_engine.stop_recognition.assert_not_called()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|