Files
voculinux/tests/test_tray_indicator_ext.py
soubarnak ea7be5f111 feat(identity): rename user-visible identity to voculinux with one-time legacy import
The fork takes its own name everywhere a user can see it: binaries
(voculinux, voculinux-gui), desktop entry, autostart, tray, D-Bus
namespace (com.voculinux.Voculinux, no alias for the old name),
config/data/cache dirs, single-instance lock, IBus engine identity,
tray icons, resource prefixes, VOCULINUX_* environment names, and the
update-checker user agent. The internal Python module stays vocalinux
and the distribution metadata keeps the upstream URLs as provenance.

On first run a previous Vocalinux install's config.json, custom
dictionary, and portal restore token are copied once into the new
locations (new config always wins, no merge, no write-back); models
re-resolve through the pinned-checksum download path. An offline
naming-sweep test proves no old-name strings remain on user-visible
surfaces outside the allowlist.

Refs #18
2026-11-06 01:29:39 +05:30

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