362 lines
14 KiB
Python
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()
|