Files
cthulhu/tests/test_dbus_service_native_interfaces.py

362 lines
14 KiB
Python

import unittest
from unittest import mock
from cthulhu import dbus_service
class SampleModule:
def __init__(self):
self.enabled = False
self.calls = []
@dbus_service.command
def toggle_feature(self, script=None, event=None, notify_user=True):
"""Toggles the test feature."""
self.calls.append((script, event, notify_user))
return True
@dbus_service.command
def command_without_notify(self, script=None, event=None):
"""Runs a command that does not accept notify_user."""
self.calls.append((script, event))
return True
@dbus_service.command
def list_items(self) -> "list[str]":
"""Returns a list from a command."""
return ["alpha", "beta"]
@dbus_service.parameterized_command
def speak_text(
self,
text: "str",
count: "int" = 1,
script=None,
event=None,
notify_user=True,
) -> "str":
"""Speaks text a number of times."""
self.calls.append((text, count, script, event, notify_user))
return text * count
@dbus_service.getter
def get_enabled(self) -> "bool":
"""Returns whether the feature is enabled."""
return self.enabled
@dbus_service.setter
def set_enabled(self, value: "bool"):
"""Sets whether the feature is enabled."""
self.enabled = value
class NativeInterfaceBuilderTest(unittest.TestCase):
def test_registration_groups_decorated_members_by_native_names(self):
registration = dbus_service._ModuleRegistration.from_module_instance(
"TestModule", SampleModule()
)
self.assertIn("ToggleFeature", registration.get_commands())
self.assertIn("SpeakText", registration.get_parameterized_commands())
self.assertIn("Enabled", registration.get_getters())
self.assertIn("Enabled", registration.get_setters())
self.assertEqual(registration.total_member_count(), 6)
def test_interface_builder_exposes_methods_and_properties(self):
registration = dbus_service._ModuleRegistration.from_module_instance(
"TestModule", SampleModule()
)
interface_class = dbus_service._InterfaceBuilder.build(registration)
self.assertTrue(hasattr(interface_class, "ToggleFeature"))
self.assertTrue(hasattr(interface_class, "CommandWithoutNotify"))
self.assertTrue(hasattr(interface_class, "ListItems"))
self.assertTrue(hasattr(interface_class, "SpeakText"))
self.assertIsInstance(interface_class.Enabled, property)
self.assertFalse(hasattr(interface_class, "ExecuteCommand"))
self.assertIn("org.stormux.Cthulhu1.TestModule", interface_class.__dbus_xml__)
def test_generated_command_method_uses_active_or_default_script(self):
module = SampleModule()
registration = dbus_service._ModuleRegistration.from_module_instance("TestModule", module)
interface = dbus_service._InterfaceBuilder.build(registration)()
script = mock.Mock()
manager = mock.Mock()
manager.get_active_script.return_value = None
manager.get_default_script.return_value = script
event_manager = mock.Mock()
event_manager_module = mock.Mock()
event_manager_module.get_manager.return_value = event_manager
with (
mock.patch.object(dbus_service.script_manager, "get_manager", return_value=manager),
mock.patch.object(
dbus_service, "_get_input_event_manager", return_value=event_manager_module
),
):
self.assertTrue(interface.ToggleFeature(False))
self.assertTrue(interface.CommandWithoutNotify(False))
self.assertEqual(interface.ListItems(False), ["alpha", "beta"])
self.assertEqual(module.calls[0][0], script)
self.assertFalse(module.calls[0][2])
self.assertEqual(module.calls[1][0], script)
self.assertEqual(event_manager.process_remote_controller_event.call_count, 3)
def test_generated_properties_call_getters_and_setters(self):
module = SampleModule()
registration = dbus_service._ModuleRegistration.from_module_instance("TestModule", module)
interface = dbus_service._InterfaceBuilder.build(registration)()
self.assertFalse(interface.Enabled)
interface.Enabled = True
self.assertTrue(module.enabled)
self.assertTrue(interface.Enabled)
class NativeRemoteControllerTest(unittest.TestCase):
def test_controller_uses_versioned_service_name_and_object_path(self):
self.assertEqual(dbus_service.CthulhuRemoteController.SERVICE_NAME, "org.stormux.Cthulhu1.Service")
self.assertEqual(dbus_service.CthulhuRemoteController.OBJECT_PATH, "/org/stormux/Cthulhu1/Service")
@mock.patch("cthulhu.speech.speak")
def test_speak_text_uses_direct_speech_output(self, speak):
interface = dbus_service.CthulhuDBusServiceInterface()
self.assertTrue(interface.SpeakText("hello", True))
speak.assert_called_once_with("hello", interrupt=True)
@mock.patch("cthulhu.braille.displayMessage")
def test_braille_message_uses_persistent_flash(self, display_message):
interface = dbus_service.CthulhuDBusServiceInterface()
self.assertTrue(interface.BrailleMessage("hello"))
display_message.assert_called_once_with("hello", flashTime=-1)
@mock.patch("cthulhu.speech.stop")
def test_cancel_speech_stops_output(self, stop):
interface = dbus_service.CthulhuDBusServiceInterface()
self.assertTrue(interface.CancelSpeech())
stop.assert_called_once_with()
def test_ssml_fails_closed(self):
interface = dbus_service.CthulhuDBusServiceInterface()
self.assertEqual(interface.SpeakSsml("request", "<speak>Hello</speak>"), 50)
self.assertEqual(interface.SpeakSsml("request", "not xml"), 87)
@mock.patch.object(dbus_service.CthulhuDBusServiceInterface, "PresentMessage")
def test_accessible_event_normalizes_useful_fields(self, present_message):
present_message.return_value = True
interface = dbus_service.CthulhuDBusServiceInterface()
result = interface.PresentAccessibleEvent(
"source", "focus", "Open", "button", "", "focused", "", 2, 4, "Settings"
)
self.assertTrue(result)
present_message.assert_not_called()
interface._present_pending_accessible_focus()
present_message.assert_called_once_with("Open, button, 2 of 4, Settings")
@mock.patch.object(dbus_service.CthulhuDBusServiceInterface, "PresentMessage")
def test_accessible_event_ignores_changes_from_unfocused_objects(self, present_message):
present_message.return_value = True
interface = dbus_service.CthulhuDBusServiceInterface()
interface.PresentAccessibleEvent(
"control-1", "focus", "Open", "button", "", "", "", 0, 0, "Settings"
)
present_message.reset_mock()
result = interface.PresentAccessibleEvent(
"control-2", "value", "Cancel", "button", "", "", "", 0, 0, "Settings"
)
self.assertFalse(result)
present_message.assert_not_called()
@mock.patch.object(dbus_service.CthulhuDBusServiceInterface, "PresentMessage")
def test_accessible_event_presents_focused_value_changes_without_repeating_title(
self, present_message
):
present_message.return_value = True
interface = dbus_service.CthulhuDBusServiceInterface()
interface.PresentAccessibleEvent(
"control-1", "focus", "Version", "combo box", "Windows 10", "", "", 0, 0,
"Wine configuration"
)
interface._present_pending_accessible_focus()
present_message.reset_mock()
result = interface.PresentAccessibleEvent(
"control-1", "value", "Version", "combo box", "Windows 11", "", "", 0, 0,
"Wine configuration"
)
self.assertTrue(result)
present_message.assert_called_once_with("Version, combo box, Windows 11")
@mock.patch.object(dbus_service.CthulhuDBusServiceInterface, "PresentMessage")
def test_accessible_event_suppresses_immediate_duplicates(self, present_message):
present_message.return_value = True
interface = dbus_service.CthulhuDBusServiceInterface()
first = interface.PresentAccessibleEvent(
"control-1", "focus", "OK", "button", "", "", "", 0, 0, "Settings"
)
interface._present_pending_accessible_focus()
second = interface.PresentAccessibleEvent(
"control-1", "focus", "OK", "button", "", "", "", 0, 0, "Settings"
)
self.assertTrue(first)
self.assertFalse(second)
present_message.assert_called_once_with("OK, button, Settings")
@mock.patch.object(dbus_service.CthulhuDBusServiceInterface, "PresentMessage")
def test_accessible_focus_burst_presents_only_final_control(self, present_message):
present_message.return_value = True
interface = dbus_service.CthulhuDBusServiceInterface()
interface.PresentAccessibleEvent(
"control-1", "focus", "Add application", "button", "", "", "", 0, 0,
"Wine configuration"
)
interface.PresentAccessibleEvent(
"control-2", "focus", "OK", "button", "", "", "", 0, 0,
"Wine configuration"
)
interface.PresentAccessibleEvent(
"control-1", "focus", "Add application", "button", "", "", "", 0, 0,
"Wine configuration"
)
present_message.assert_not_called()
interface._present_pending_accessible_focus()
present_message.assert_called_once_with(
"Add application, button, Wine configuration"
)
@mock.patch.object(dbus_service.CthulhuDBusServiceInterface, "PresentMessage")
def test_accessible_focus_duplicate_clears_stale_pending_focus(self, present_message):
present_message.return_value = True
interface = dbus_service.CthulhuDBusServiceInterface()
interface.PresentAccessibleEvent(
"control-1", "focus", "Open", "button", "", "", "", 0, 0,
"Settings"
)
interface._present_pending_accessible_focus()
present_message.reset_mock()
interface.PresentAccessibleEvent(
"control-2", "focus", "Cancel", "button", "", "", "", 0, 0,
"Settings"
)
duplicate = interface.PresentAccessibleEvent(
"control-1", "focus", "Open", "button", "", "", "", 0, 0,
"Settings"
)
interface._present_pending_accessible_focus()
self.assertFalse(duplicate)
self.assertEqual(interface._wineAccessibleFocusSource, "control-1")
present_message.assert_not_called()
@mock.patch.object(dbus_service.CthulhuDBusServiceInterface, "PresentMessage")
def test_accessible_focus_presents_new_window_title_without_control_fields(
self, present_message
):
present_message.return_value = True
interface = dbus_service.CthulhuDBusServiceInterface()
result = interface.PresentAccessibleEvent(
"window", "focus", "", "", "", "", "", 0, 0, "Wine configuration"
)
interface._present_pending_accessible_focus()
self.assertTrue(result)
present_message.assert_called_once_with("Wine configuration")
@mock.patch.object(dbus_service.CthulhuDBusServiceInterface, "PresentMessage")
def test_accessible_event_omits_generic_client_role(self, present_message):
present_message.return_value = True
interface = dbus_service.CthulhuDBusServiceInterface()
interface.PresentAccessibleEvent(
"control-1", "focus", "OK", "client", "", "", "", 0, 0, "Settings"
)
interface._present_pending_accessible_focus()
present_message.assert_called_once_with("OK, Settings")
@mock.patch.object(dbus_service.CthulhuDBusServiceInterface, "PresentMessage")
def test_accessible_event_ignores_name_and_state_noise_on_focused_object(
self, present_message
):
present_message.return_value = True
interface = dbus_service.CthulhuDBusServiceInterface()
interface.PresentAccessibleEvent(
"control-1", "focus", "Windows 10", "client", "", "", "", 0, 0,
"Wine configuration"
)
interface._present_pending_accessible_focus()
present_message.reset_mock()
name_result = interface.PresentAccessibleEvent(
"control-1", "name", "OK", "client", "", "", "", 0, 0,
"Wine configuration"
)
state_result = interface.PresentAccessibleEvent(
"control-1", "state", "OK", "client", "", "focused", "", 0, 0,
"Wine configuration"
)
self.assertFalse(name_result)
self.assertFalse(state_result)
present_message.assert_not_called()
@mock.patch.object(dbus_service.CthulhuDBusServiceInterface, "PresentMessage")
def test_accessible_event_accepts_selection_from_focused_combo_child(
self, present_message
):
present_message.return_value = True
interface = dbus_service.CthulhuDBusServiceInterface()
interface.PresentAccessibleEvent(
"combo", "focus", "Windows 10", "client", "", "", "", 0, 0,
"Wine configuration"
)
interface._present_pending_accessible_focus()
present_message.reset_mock()
result = interface.PresentAccessibleEvent(
"combo-item-2", "selection", "Windows 8.1", "client", "", "", "", 0, 0,
"Wine configuration"
)
self.assertTrue(result)
present_message.assert_called_once_with("Windows 8.1")
@mock.patch.object(dbus_service.CthulhuDBusServiceInterface, "PresentMessage")
def test_accessible_event_rejects_selection_from_other_window(self, present_message):
present_message.return_value = True
interface = dbus_service.CthulhuDBusServiceInterface()
interface.PresentAccessibleEvent(
"combo", "focus", "Windows 10", "client", "", "", "", 0, 0,
"Wine configuration"
)
interface._present_pending_accessible_focus()
present_message.reset_mock()
result = interface.PresentAccessibleEvent(
"other-item", "selection", "Unrelated", "client", "", "", "", 0, 0,
"Other window"
)
self.assertFalse(result)
present_message.assert_not_called()
if __name__ == "__main__":
unittest.main()