Files
cthulhu/tests/test_compositor_state_i3_regressions.py
2026-07-30 18:53:56 -04:00

182 lines
5.8 KiB
Python

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()