182 lines
5.8 KiB
Python
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()
|