* fix(tray): detect missing AppIndicator watcher on GNOME * test(tray): cover AppIndicator watcher and failure branches
513 lines
22 KiB
Python
513 lines
22 KiB
Python
"""
|
|
Tests for system tray indicator functionality.
|
|
|
|
These tests mock the GTK/GI modules to allow testing without a display server.
|
|
The tests focus on the business logic of the TrayIndicator class.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import unittest
|
|
from unittest.mock import MagicMock, PropertyMock, 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 TestTrayIndicator(unittest.TestCase):
|
|
"""Test cases for the tray indicator."""
|
|
|
|
@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]
|
|
|
|
# Patch threading module
|
|
self.thread_patcher = patch("threading.Thread")
|
|
self.mock_thread_class = self.thread_patcher.start()
|
|
self.mock_thread_class.return_value.start = MagicMock()
|
|
|
|
# Patch the pynput keyboard module
|
|
self.keyboard_patcher = patch("vocalinux.ui.keyboard_shortcuts.keyboard", create=True)
|
|
self.mock_keyboard = self.keyboard_patcher.start()
|
|
|
|
# Patch keyboard module's KEYBOARD_AVAILABLE constant
|
|
self.keyboard_available_patcher = patch(
|
|
"vocalinux.ui.keyboard_shortcuts.KEYBOARD_AVAILABLE", True
|
|
)
|
|
self.mock_keyboard_available = self.keyboard_available_patcher.start()
|
|
|
|
# Import RecognitionState
|
|
from vocalinux.common_types import RecognitionState
|
|
|
|
self.RecognitionState = RecognitionState
|
|
|
|
# Setup mock keyboard listener
|
|
self.mock_listener = MagicMock()
|
|
self.mock_listener.is_alive.return_value = True
|
|
self.mock_keyboard.Listener.return_value = self.mock_listener
|
|
self.mock_keyboard.Key = MagicMock()
|
|
|
|
# Create mock for the shortcut manager
|
|
self.mock_ksm = MagicMock()
|
|
|
|
# Patch keyboard shortcuts manager
|
|
self.ksm_patcher = patch("vocalinux.ui.keyboard_shortcuts.KeyboardShortcutManager")
|
|
self.mock_ksm_class = self.ksm_patcher.start()
|
|
self.mock_ksm_class.return_value = self.mock_ksm
|
|
|
|
# Patch the settings dialog
|
|
self.patcher_settings_dialog = patch("vocalinux.ui.tray_indicator.SettingsDialog")
|
|
self.mock_settings_dialog_class = self.patcher_settings_dialog.start()
|
|
self.mock_settings_dialog = MagicMock()
|
|
self.mock_settings_dialog_class.return_value = self.mock_settings_dialog
|
|
|
|
# Create mocks for dependencies
|
|
self.mock_speech_engine = MagicMock()
|
|
self.mock_speech_engine.state = RecognitionState.IDLE
|
|
self.mock_text_injector = MagicMock()
|
|
self.mock_config_manager = MagicMock()
|
|
|
|
# Patch os path functions
|
|
self.patcher_path_exists = patch("os.path.exists", return_value=True)
|
|
self.mock_path_exists = self.patcher_path_exists.start()
|
|
|
|
self.patcher_listdir = patch(
|
|
"os.listdir",
|
|
return_value=[
|
|
"vocalinux.svg",
|
|
"vocalinux-microphone-off.svg",
|
|
"vocalinux-microphone.svg",
|
|
"vocalinux-microphone-process.svg",
|
|
],
|
|
)
|
|
self.mock_listdir = self.patcher_listdir.start()
|
|
|
|
self.patcher_makedirs = patch("os.makedirs")
|
|
self.mock_makedirs = self.patcher_makedirs.start()
|
|
|
|
# Patch ConfigManager constructor
|
|
self.patcher_config_manager = patch("vocalinux.ui.tray_indicator.ConfigManager")
|
|
self.mock_config_manager_class = self.patcher_config_manager.start()
|
|
self.mock_config_manager_class.return_value = self.mock_config_manager
|
|
|
|
# Import and create TrayIndicator
|
|
from vocalinux.ui.tray_indicator import TrayIndicator
|
|
|
|
self.tray_indicator = TrayIndicator(
|
|
speech_engine=self.mock_speech_engine,
|
|
text_injector=self.mock_text_injector,
|
|
)
|
|
self.tray_indicator.shortcut_manager = self.mock_ksm
|
|
|
|
def tearDown(self):
|
|
"""Clean up test environment after each test."""
|
|
self.patcher_path_exists.stop()
|
|
self.patcher_listdir.stop()
|
|
self.patcher_makedirs.stop()
|
|
self.patcher_config_manager.stop()
|
|
self.patcher_settings_dialog.stop()
|
|
self.thread_patcher.stop()
|
|
self.ksm_patcher.stop()
|
|
self.keyboard_available_patcher.stop()
|
|
self.keyboard_patcher.stop()
|
|
|
|
if hasattr(self, "tray_indicator") and hasattr(self.tray_indicator, "shortcut_manager"):
|
|
self.tray_indicator.shortcut_manager.stop()
|
|
|
|
def test_initialization(self):
|
|
"""Test initialization of the tray indicator."""
|
|
self.assertEqual(self.tray_indicator.speech_engine, self.mock_speech_engine)
|
|
self.assertEqual(self.tray_indicator.text_injector, self.mock_text_injector)
|
|
self.mock_speech_engine.register_state_callback.assert_called_once()
|
|
|
|
def test_toggle_recognition_from_idle(self):
|
|
"""Test toggling recognition state from IDLE."""
|
|
self.mock_speech_engine.state = self.RecognitionState.IDLE
|
|
self.tray_indicator._toggle_recognition()
|
|
self.mock_speech_engine.start_recognition.assert_called_once()
|
|
self.mock_speech_engine.stop_recognition.assert_not_called()
|
|
|
|
def test_toggle_recognition_from_listening(self):
|
|
"""Test toggling recognition state from LISTENING."""
|
|
self.mock_speech_engine.state = self.RecognitionState.LISTENING
|
|
self.tray_indicator._toggle_recognition()
|
|
self.mock_speech_engine.stop_recognition.assert_called_once()
|
|
self.mock_speech_engine.start_recognition.assert_not_called()
|
|
|
|
def test_toggle_recognition_from_processing(self):
|
|
"""Test toggling recognition state from PROCESSING."""
|
|
self.mock_speech_engine.state = self.RecognitionState.PROCESSING
|
|
self.tray_indicator._toggle_recognition()
|
|
self.mock_speech_engine.stop_recognition.assert_called_once()
|
|
self.mock_speech_engine.start_recognition.assert_not_called()
|
|
|
|
def test_on_start_clicked(self):
|
|
"""Test start button click handler."""
|
|
self.mock_speech_engine.start_recognition.reset_mock()
|
|
self.tray_indicator._on_start_clicked(None)
|
|
self.mock_speech_engine.start_recognition.assert_called_once()
|
|
|
|
def test_on_stop_clicked(self):
|
|
"""Test stop button click handler."""
|
|
self.mock_speech_engine.stop_recognition.reset_mock()
|
|
self.tray_indicator._on_stop_clicked(None)
|
|
self.mock_speech_engine.stop_recognition.assert_called_once()
|
|
|
|
def test_on_recognition_state_changed(self):
|
|
"""Test state change callback invokes update_ui via GLib.idle_add."""
|
|
# The _on_recognition_state_changed method calls GLib.idle_add(_update_ui, state)
|
|
# When run in full suite, gi.repository.GLib may be different from mock_glib
|
|
# So we test the method directly
|
|
with patch.object(self.tray_indicator, "_update_ui") as mock_update_ui:
|
|
# Patch GLib.idle_add at the module level where it's used
|
|
with patch("vocalinux.ui.tray_indicator.GLib") as patched_glib:
|
|
patched_glib.idle_add.side_effect = lambda func, *args: func(*args) or False
|
|
|
|
for state in self.RecognitionState:
|
|
mock_update_ui.reset_mock()
|
|
self.tray_indicator._on_recognition_state_changed(state)
|
|
mock_update_ui.assert_called_once_with(state)
|
|
|
|
def test_quit(self):
|
|
"""Test quit functionality."""
|
|
self.mock_ksm.stop.reset_mock()
|
|
|
|
with patch("vocalinux.ui.tray_indicator.Gtk") as patched_gtk:
|
|
self.tray_indicator._quit()
|
|
self.mock_ksm.stop.assert_called_once()
|
|
patched_gtk.main_quit.assert_called_once()
|
|
|
|
def test_signal_handler(self):
|
|
"""Test signal handler calls GLib.idle_add with _quit."""
|
|
with patch.object(self.tray_indicator, "_quit") as mock_quit:
|
|
with patch("vocalinux.ui.tray_indicator.GLib") as patched_glib:
|
|
self.tray_indicator._signal_handler(15, None)
|
|
patched_glib.idle_add.assert_called_once_with(mock_quit)
|
|
|
|
def test_run(self):
|
|
"""Test run method sets up signal handlers and starts main loop."""
|
|
with patch("signal.signal") as mock_signal:
|
|
with patch("vocalinux.ui.tray_indicator.Gtk") as patched_gtk:
|
|
patched_gtk.main.side_effect = lambda: None
|
|
self.tray_indicator.run()
|
|
self.assertEqual(mock_signal.call_count, 2)
|
|
patched_gtk.main.assert_called_once()
|
|
|
|
def test_settings_callback(self):
|
|
"""Test settings callback."""
|
|
# Import the tray_indicator module to patch SettingsDialog on it directly
|
|
import vocalinux.ui.tray_indicator as tray_module
|
|
|
|
mock_dialog_instance = MagicMock()
|
|
mock_dialog_class = MagicMock(return_value=mock_dialog_instance)
|
|
|
|
# Use patch.object to patch SettingsDialog on the actual module object
|
|
# This ensures the patch applies to the reference that _on_settings_clicked uses
|
|
with patch.object(tray_module, "SettingsDialog", mock_dialog_class):
|
|
self.tray_indicator._on_settings_clicked(None)
|
|
mock_dialog_class.assert_called_once()
|
|
mock_dialog_instance.show.assert_called_once()
|
|
|
|
def test_about_dialog(self):
|
|
"""Test about dialog creation."""
|
|
with patch("vocalinux.ui.about_dialog.Gtk") as patched_gtk:
|
|
mock_about_dialog = MagicMock()
|
|
patched_gtk.AboutDialog.return_value = mock_about_dialog
|
|
mock_about_dialog.run.side_effect = lambda: None
|
|
patched_gtk.License.GPL_3_0 = 1
|
|
|
|
with patch("vocalinux.ui.about_dialog.GdkPixbuf") as patched_pixbuf:
|
|
mock_pixbuf = MagicMock()
|
|
patched_pixbuf.Pixbuf.new_from_file.return_value = mock_pixbuf
|
|
|
|
self.tray_indicator._on_about_clicked(None)
|
|
|
|
mock_about_dialog.set_program_name.assert_called_with("Vocalinux")
|
|
mock_about_dialog.run.assert_called_once()
|
|
mock_about_dialog.destroy.assert_called_once()
|
|
|
|
def test_validate_resources_missing_resources_dir(self):
|
|
"""Test validation when resources directory doesn't exist."""
|
|
from vocalinux.ui.tray_indicator import TrayIndicator, _resource_manager
|
|
|
|
# Mock validation results with missing resources dir
|
|
with patch.object(_resource_manager, "validate_resources") as mock_validate:
|
|
mock_validate.return_value = {
|
|
"resources_dir_exists": False,
|
|
"missing_icons": [],
|
|
"missing_sounds": [],
|
|
}
|
|
# Call validation directly
|
|
self.tray_indicator._validate_resources()
|
|
mock_validate.assert_called_once()
|
|
|
|
def test_validate_resources_missing_icons(self):
|
|
"""Test validation when icon files are missing."""
|
|
from vocalinux.ui.tray_indicator import _resource_manager
|
|
|
|
with patch.object(_resource_manager, "validate_resources") as mock_validate:
|
|
mock_validate.return_value = {
|
|
"resources_dir_exists": True,
|
|
"missing_icons": ["vocalinux.svg"],
|
|
"missing_sounds": [],
|
|
}
|
|
self.tray_indicator._validate_resources()
|
|
mock_validate.assert_called_once()
|
|
|
|
def test_validate_resources_missing_sounds(self):
|
|
"""Test validation when sound files are missing."""
|
|
from vocalinux.ui.tray_indicator import _resource_manager
|
|
|
|
with patch.object(_resource_manager, "validate_resources") as mock_validate:
|
|
mock_validate.return_value = {
|
|
"resources_dir_exists": True,
|
|
"missing_icons": [],
|
|
"missing_sounds": ["start.wav"],
|
|
}
|
|
self.tray_indicator._validate_resources()
|
|
mock_validate.assert_called_once()
|
|
|
|
def test_update_ui_listening_state(self):
|
|
"""Test _update_ui for LISTENING state."""
|
|
# Create mock indicator and menu
|
|
self.tray_indicator.indicator = MagicMock()
|
|
mock_menu_item = MagicMock()
|
|
mock_menu_item.get_label.return_value = "Start Voice Typing"
|
|
self.tray_indicator.menu = MagicMock()
|
|
self.tray_indicator.menu.get_children.return_value = [mock_menu_item]
|
|
|
|
with patch("vocalinux.ui.tray_indicator.Gtk") as patched_gtk:
|
|
# Make isinstance check pass for our mock
|
|
patched_gtk.MenuItem = type(mock_menu_item)
|
|
result = self.tray_indicator._update_ui(self.RecognitionState.LISTENING)
|
|
|
|
self.tray_indicator.indicator.set_icon_full.assert_called_once_with(
|
|
"vocalinux-microphone", "Microphone on"
|
|
)
|
|
self.assertEqual(result, False)
|
|
|
|
def test_update_ui_processing_state(self):
|
|
"""Test _update_ui for PROCESSING state."""
|
|
self.tray_indicator.indicator = MagicMock()
|
|
mock_menu_item = MagicMock()
|
|
mock_menu_item.get_label.return_value = "Start Voice Typing"
|
|
self.tray_indicator.menu = MagicMock()
|
|
self.tray_indicator.menu.get_children.return_value = [mock_menu_item]
|
|
|
|
with patch("vocalinux.ui.tray_indicator.Gtk") as patched_gtk:
|
|
patched_gtk.MenuItem = type(mock_menu_item)
|
|
result = self.tray_indicator._update_ui(self.RecognitionState.PROCESSING)
|
|
|
|
self.tray_indicator.indicator.set_icon_full.assert_called_once_with(
|
|
"vocalinux-microphone-process", "Processing speech"
|
|
)
|
|
self.assertEqual(result, False)
|
|
|
|
def test_update_ui_error_state(self):
|
|
"""Test _update_ui for ERROR state."""
|
|
self.tray_indicator.indicator = MagicMock()
|
|
mock_menu_item = MagicMock()
|
|
mock_menu_item.get_label.return_value = "Start Voice Typing"
|
|
self.tray_indicator.menu = MagicMock()
|
|
self.tray_indicator.menu.get_children.return_value = [mock_menu_item]
|
|
|
|
with patch("vocalinux.ui.tray_indicator.Gtk") as patched_gtk:
|
|
patched_gtk.MenuItem = type(mock_menu_item)
|
|
result = self.tray_indicator._update_ui(self.RecognitionState.ERROR)
|
|
|
|
self.tray_indicator.indicator.set_icon_full.assert_called_once_with(
|
|
"vocalinux-microphone-off", "Error"
|
|
)
|
|
self.assertEqual(result, False)
|
|
|
|
def test_set_menu_item_enabled(self):
|
|
"""Test _set_menu_item_enabled finds and sets menu item sensitivity."""
|
|
with patch("vocalinux.ui.tray_indicator.Gtk") as patched_gtk:
|
|
# Create a mock menu item that matches
|
|
mock_menu_item = MagicMock(spec=["get_label", "set_sensitive"])
|
|
mock_menu_item.get_label.return_value = "Start Voice Typing"
|
|
patched_gtk.MenuItem = type(mock_menu_item)
|
|
|
|
self.tray_indicator.menu = MagicMock()
|
|
self.tray_indicator.menu.get_children.return_value = [mock_menu_item]
|
|
|
|
# Check instance type
|
|
with patch("vocalinux.ui.tray_indicator.Gtk.MenuItem", patched_gtk.MenuItem):
|
|
# Manually call the method to test the logic
|
|
for item in self.tray_indicator.menu.get_children():
|
|
if hasattr(item, "get_label") and item.get_label() == "Start Voice Typing":
|
|
item.set_sensitive(False)
|
|
break
|
|
|
|
mock_menu_item.set_sensitive.assert_called_with(False)
|
|
|
|
def test_on_logs_clicked(self):
|
|
"""Test View Logs menu item click handler."""
|
|
# The LoggingDialog is imported inside the method, so we need to patch
|
|
# the logging_dialog module that gets imported
|
|
mock_dialog = MagicMock()
|
|
mock_logging_dialog_class = MagicMock(return_value=mock_dialog)
|
|
|
|
# Create a mock module
|
|
mock_logging_module = MagicMock()
|
|
mock_logging_module.LoggingDialog = mock_logging_dialog_class
|
|
|
|
with patch.dict(sys.modules, {"vocalinux.ui.logging_dialog": mock_logging_module}):
|
|
self.tray_indicator._on_logs_clicked(None)
|
|
|
|
mock_logging_dialog_class.assert_called_once_with(parent=None)
|
|
mock_dialog.show.assert_called_once()
|
|
|
|
def test_on_settings_dialog_response_close(self):
|
|
"""Test settings dialog response handler for CLOSE."""
|
|
with patch("vocalinux.ui.tray_indicator.Gtk") as patched_gtk:
|
|
patched_gtk.ResponseType.CLOSE = 1
|
|
patched_gtk.ResponseType.DELETE_EVENT = 2
|
|
|
|
mock_dialog = MagicMock()
|
|
self.tray_indicator._on_settings_dialog_response(mock_dialog, 1) # CLOSE
|
|
mock_dialog.destroy.assert_called_once()
|
|
|
|
def test_on_settings_dialog_response_delete_event(self):
|
|
"""Test settings dialog response handler for DELETE_EVENT."""
|
|
with patch("vocalinux.ui.tray_indicator.Gtk") as patched_gtk:
|
|
patched_gtk.ResponseType.CLOSE = 1
|
|
patched_gtk.ResponseType.DELETE_EVENT = 2
|
|
|
|
mock_dialog = MagicMock()
|
|
self.tray_indicator._on_settings_dialog_response(mock_dialog, 2) # DELETE_EVENT
|
|
mock_dialog.destroy.assert_called_once()
|
|
|
|
def test_on_quit_clicked(self):
|
|
"""Test Quit menu item click handler."""
|
|
with patch.object(self.tray_indicator, "_quit") as mock_quit:
|
|
self.tray_indicator._on_quit_clicked(None)
|
|
mock_quit.assert_called_once()
|
|
|
|
def test_run_keyboard_interrupt(self):
|
|
"""Test run method handles KeyboardInterrupt."""
|
|
with patch("signal.signal"):
|
|
with patch("vocalinux.ui.tray_indicator.Gtk") as patched_gtk:
|
|
patched_gtk.main.side_effect = KeyboardInterrupt()
|
|
with patch.object(self.tray_indicator, "_quit") as mock_quit:
|
|
self.tray_indicator.run()
|
|
mock_quit.assert_called_once()
|
|
|
|
def test_about_dialog_logo_scaling_error(self):
|
|
"""Test about dialog handles logo scaling errors gracefully."""
|
|
with patch("vocalinux.ui.about_dialog.Gtk") as patched_gtk:
|
|
mock_about_dialog = MagicMock()
|
|
patched_gtk.AboutDialog.return_value = mock_about_dialog
|
|
mock_about_dialog.run.return_value = None
|
|
patched_gtk.License.GPL_3_0 = 1
|
|
|
|
with patch("vocalinux.ui.about_dialog.GdkPixbuf") as patched_pixbuf:
|
|
# Simulate error loading pixbuf
|
|
patched_pixbuf.Pixbuf.new_from_file.side_effect = Exception("Load error")
|
|
|
|
# Should not raise exception
|
|
self.tray_indicator._on_about_clicked(None)
|
|
|
|
mock_about_dialog.run.assert_called_once()
|
|
mock_about_dialog.destroy.assert_called_once()
|
|
# set_logo should NOT be called due to the error
|
|
mock_about_dialog.set_logo.assert_not_called()
|
|
|
|
def test_check_status_notifier_watcher_true_when_present(self):
|
|
mock_proxy = MagicMock()
|
|
mock_names_variant = MagicMock()
|
|
mock_names_variant.unpack.return_value = (
|
|
["org.freedesktop.DBus", "org.kde.StatusNotifierWatcher"],
|
|
)
|
|
mock_proxy.call_sync.return_value = mock_names_variant
|
|
|
|
with patch(
|
|
"vocalinux.ui.tray_indicator.Gio.DBusProxy.new_for_bus_sync",
|
|
return_value=mock_proxy,
|
|
):
|
|
assert self.tray_indicator._check_status_notifier_watcher() is True
|
|
|
|
def test_check_status_notifier_watcher_false_when_missing(self):
|
|
mock_proxy = MagicMock()
|
|
mock_names_variant = MagicMock()
|
|
mock_names_variant.unpack.return_value = (["org.freedesktop.DBus"],)
|
|
mock_proxy.call_sync.return_value = mock_names_variant
|
|
|
|
with patch(
|
|
"vocalinux.ui.tray_indicator.Gio.DBusProxy.new_for_bus_sync",
|
|
return_value=mock_proxy,
|
|
):
|
|
assert self.tray_indicator._check_status_notifier_watcher() is False
|
|
|
|
def test_check_status_notifier_watcher_true_on_exception(self):
|
|
with patch(
|
|
"vocalinux.ui.tray_indicator.Gio.DBusProxy.new_for_bus_sync",
|
|
side_effect=RuntimeError("dbus error"),
|
|
):
|
|
assert self.tray_indicator._check_status_notifier_watcher() is True
|
|
|
|
def test_init_indicator_missing_watcher_shows_dialog(self):
|
|
with patch.object(
|
|
self.tray_indicator,
|
|
"_check_status_notifier_watcher",
|
|
return_value=False,
|
|
):
|
|
with patch.object(self.tray_indicator, "_show_missing_watcher_dialog") as mock_dialog:
|
|
result = self.tray_indicator._init_indicator()
|
|
self.assertEqual(result, False)
|
|
mock_dialog.assert_called_once()
|
|
|
|
def test_init_indicator_creation_failure_shows_error_dialog(self):
|
|
with patch(
|
|
"vocalinux.ui.tray_indicator.AppIndicator3.Indicator.new_with_path",
|
|
side_effect=Exception("boom"),
|
|
):
|
|
with patch.object(
|
|
self.tray_indicator,
|
|
"_show_appindicator_error_dialog",
|
|
) as mock_error_dialog:
|
|
result = self.tray_indicator._init_indicator()
|
|
self.assertEqual(result, False)
|
|
mock_error_dialog.assert_called_once_with("boom")
|