import sys import unittest from pathlib import Path from unittest import mock sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) from cthulhu import cthulhu_state from cthulhu import cthulhu from cthulhu import compositor_state_adapter from cthulhu import compositor_state_types class FakeWorkspaceBackend: def __init__(self, available: bool, name: str) -> None: self.available = available self.name = name self.activate_calls = [] self.deactivate_calls = [] def is_available(self, session_type: str | None = None) -> bool: return self.available def activate(self, adapter=None) -> None: self.activate_calls.append(adapter) def deactivate(self, adapter=None) -> None: self.deactivate_calls.append(adapter) class CompositorStateAdapterRegressionTests(unittest.TestCase): def setUp(self) -> None: cthulhu_state.compositorSnapshot = None cthulhu_state.pauseAtspiChurn = False cthulhu_state.prioritizedDesktopContextToken = None def test_activate_selects_first_available_backend(self) -> None: unavailableBackend = FakeWorkspaceBackend(False, "unavailable") selectedBackend = FakeWorkspaceBackend(True, "selected") adapter = compositor_state_adapter.CompositorStateAdapter( workspace_backends=[unavailableBackend, selectedBackend] ) adapter.activate() self.assertEqual(unavailableBackend.activate_calls, []) self.assertEqual(selectedBackend.activate_calls, [adapter]) self.assertEqual(adapter.get_snapshot().backend_name, "selected") def test_activate_is_idempotent_and_deactivates_previous_backend(self) -> None: backend = FakeWorkspaceBackend(True, "selected") adapter = compositor_state_adapter.CompositorStateAdapter(workspace_backends=[backend]) adapter.activate() adapter.activate() self.assertEqual(backend.activate_calls, [adapter, adapter]) self.assertEqual(backend.deactivate_calls, [adapter]) self.assertEqual(adapter.get_snapshot().backend_name, "selected") def test_sync_accessible_context_emits_focus_context_changed_when_active_window_token_changes(self) -> None: adapter = compositor_state_adapter.CompositorStateAdapter() events = [] adapter.add_listener(events.append) firstWindow = object() secondWindow = object() def get_process_id(obj): return 111 if obj is firstWindow else 222 def get_name(obj): return "Terminal" with ( mock.patch.object(compositor_state_adapter.AXObject, "get_process_id", side_effect=get_process_id), mock.patch.object(compositor_state_adapter.AXObject, "get_name", side_effect=get_name), ): cthulhu_state.activeWindow = firstWindow cthulhu_state.locusOfFocus = firstWindow adapter.sync_accessible_context("startup") events.clear() cthulhu_state.activeWindow = secondWindow cthulhu_state.locusOfFocus = secondWindow adapter.sync_accessible_context("workspace transition") self.assertIn( compositor_state_types.DESKTOP_FOCUS_CONTEXT_CHANGED, [event.type for event in events], ) self.assertIn( compositor_state_types.PRIORITIZE_FOCUS, [event.type for event in events], ) def test_sync_accessible_context_builds_stable_active_window_tokens(self) -> None: adapter = compositor_state_adapter.CompositorStateAdapter() window = object() with ( mock.patch.object(compositor_state_adapter.AXObject, "get_process_id", return_value=4242), mock.patch.object(compositor_state_adapter.AXObject, "get_name", return_value="Terminal"), ): cthulhu_state.activeWindow = window cthulhu_state.locusOfFocus = window snapshot = adapter.sync_accessible_context("startup") self.assertEqual(snapshot.active_window_token, "4242:Terminal") self.assertEqual(cthulhu_state.compositorSnapshot.active_window_token, "4242:Terminal") def test_event_manager_startup_resyncs_adapter_after_focus_recovery(self) -> None: adapter = mock.Mock() adapter.sync_accessible_context = mock.Mock(return_value=None) listener = mock.Mock() window = object() focusedObject = object() with ( mock.patch.object(cthulhu.event_manager.Atspi.EventListener, "new", return_value=listener), mock.patch.object(cthulhu.event_manager.AXUtilities, "can_be_active_window", return_value=False), mock.patch.object(cthulhu.event_manager.AXUtilities, "find_active_window", return_value=window), mock.patch.object(cthulhu.event_manager.AXUtilities, "get_focused_object", return_value=focusedObject), mock.patch.object(cthulhu.event_manager.cthulhu, "setActiveWindow") as setActiveWindow, mock.patch.object(cthulhu.event_manager.cthulhu, "setLocusOfFocus") as setLocusOfFocus, ): manager = cthulhu.event_manager.EventManager(mock.Mock()) manager.set_compositor_state_adapter(adapter) manager._sync_focus_on_startup() setActiveWindow.assert_called_once_with(window, alsoSetLocusOfFocus=True, notifyScript=False) setLocusOfFocus.assert_called_once_with(None, focusedObject, notifyScript=True, force=True) adapter.sync_accessible_context.assert_called_once_with("event-manager-startup") if __name__ == "__main__": unittest.main()