import sys import types import unittest from pathlib import Path from unittest import mock sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) from cthulhu import compositor_state_i3 from cthulhu import compositor_state_types class FakeNode: def __init__( self, *, node_type="con", name="", window=None, window_title=None, focused=False, children=None, ): self.type = node_type self.name = name self.window = window self.window_title = window_title self.focused = focused self.nodes = list(children or []) self.floating_nodes = [] self.parent = None for child in self.nodes: child.parent = self def __iter__(self): pending = list(self.nodes) + list(self.floating_nodes) while pending: node = pending.pop(0) yield node pending.extend(node.nodes) pending.extend(node.floating_nodes) def workspaces(self): return [node for node in self if node.type == "workspace"] def find_focused(self): if self.focused: return self return next((node for node in self if node.focused), None) def workspace(self): node = self while node is not None and node.type != "workspace": node = node.parent return node class FakeWorkspaceReply: def __init__(self, name, focused): self.name = name self.focused = focused class FakeConnection: def __init__(self, tree, workspaces): self.tree = tree self.workspaceReplies = workspaces def get_tree(self): return self.tree def get_workspaces(self): return self.workspaceReplies class I3WorkspaceBackendRegressionTests(unittest.TestCase): def test_query_reports_focused_window_id_title_and_nonempty_workspace(self): focusedWindow = FakeNode( name="AT-SPI misses this title", window=0x200000C, window_title="Accurate i3 title", focused=True, ) workspace = FakeNode( node_type="workspace", name="8", children=[focusedWindow], ) root = FakeNode(node_type="root", children=[workspace]) connection = FakeConnection(root, [FakeWorkspaceReply("8", True)]) backend = compositor_state_i3.I3WorkspaceBackend(i3ipc_module=types.SimpleNamespace()) backend._connection = connection workspaceIds, payload = backend._query_context() self.assertEqual(workspaceIds, {"8"}) self.assertEqual(payload["focused_window_id"], 0x200000C) self.assertEqual(payload["focused_window_title"], "Accurate i3 title") self.assertFalse(payload["focused_workspace_empty"]) def test_query_reports_confirmed_empty_focused_workspace(self): workspace = FakeNode( node_type="workspace", name="9", focused=True, ) root = FakeNode(node_type="root", children=[workspace]) connection = FakeConnection(root, [FakeWorkspaceReply("9", True)]) backend = compositor_state_i3.I3WorkspaceBackend(i3ipc_module=types.SimpleNamespace()) backend._connection = connection workspaceIds, payload = backend._query_context() self.assertEqual(workspaceIds, {"9"}) self.assertIsNone(payload["focused_window_id"]) self.assertEqual(payload["focused_window_title"], "") self.assertTrue(payload["focused_workspace_empty"]) def test_refresh_emits_started_state_and_finished_with_foreground_payload(self): backend = compositor_state_i3.I3WorkspaceBackend(i3ipc_module=types.SimpleNamespace()) backend._active = True backend._emitSignal = mock.Mock() payload = { "focused_window_id": None, "focused_window_title": "", "focused_workspace_empty": True, } with mock.patch.object(backend, "_query_context", return_value=({"9"}, payload)): self.assertFalse(backend._refresh_context("workspace-focus", transition=True)) self.assertEqual( backend._emitSignal.call_args_list, [ mock.call( compositor_state_types.DESKTOP_TRANSITION_STARTED, {"9"}, "workspace-focus", payload, ), mock.call( compositor_state_types.WORKSPACE_STATE_CHANGED, {"9"}, "workspace-focus", payload, ), mock.call( compositor_state_types.DESKTOP_TRANSITION_FINISHED, {"9"}, "workspace-focus", payload, ), ], ) def test_query_failure_emits_unknown_instead_of_preserving_stale_i3_context(self): backend = compositor_state_i3.I3WorkspaceBackend(i3ipc_module=types.SimpleNamespace()) backend._active = True backend._emitSignal = mock.Mock() backend._lastContext = (frozenset({"8"}), 0x200000C, "Browser", False) with ( mock.patch.object(backend, "_query_context", side_effect=OSError("IPC disconnected")), mock.patch.object(compositor_state_i3.debug, "printMessage"), ): self.assertFalse(backend._refresh_context("window-focus", transition=True)) backend._emitSignal.assert_called_once_with( compositor_state_types.WORKSPACE_STATE_CHANGED, set(), "window-focus: i3-context-unknown", { "focused_window_id": None, "focused_window_title": "", "focused_workspace_empty": None, }, ) if __name__ == "__main__": unittest.main()