From 8a8674ac9b125fbc00137ff283f062b0b3ea953e Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Wed, 28 Oct 2026 19:33:26 -0700 Subject: [PATCH] feat(e2e): sync the neutral core from kfde bf5da0e6 --- e2e/lib/komorebi_vm/core/platform.py | 10 ++- .../komorebi_vm/core/scenarios/placement.py | 65 +++++++++++++++++++ e2e/lib/tests/fake_platform.py | 33 ++++++++++ e2e/lib/tests/test_focus_moving.py | 21 ++++++ e2e/lib/tests/test_tiling.py | 3 + 5 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 e2e/lib/komorebi_vm/core/scenarios/placement.py diff --git a/e2e/lib/komorebi_vm/core/platform.py b/e2e/lib/komorebi_vm/core/platform.py index 5a037907..976cf125 100644 --- a/e2e/lib/komorebi_vm/core/platform.py +++ b/e2e/lib/komorebi_vm/core/platform.py @@ -10,7 +10,7 @@ and takes the shape both platforms can give. """ from dataclasses import dataclass -from typing import Protocol +from typing import Any, Protocol from PIL import Image @@ -93,6 +93,14 @@ class Platform(Protocol): """Starts the daemon from a clean state and waits until it answers.""" ... + def restart_with(self, config: dict[str, Any]) -> None: + """Stops the daemon if it runs, and starts it from a clean state with `config` as its + `komorebi.json`, and waits until it answers. + + Windows that are open stay open, and the new daemon takes up the ones that are in view. + """ + ... + def komorebic(self, *args: str) -> str: """Runs `komorebic` with `args` and returns what it printed. Fails if it does.""" ... diff --git a/e2e/lib/komorebi_vm/core/scenarios/placement.py b/e2e/lib/komorebi_vm/core/scenarios/placement.py new file mode 100644 index 00000000..f285f3bf --- /dev/null +++ b/e2e/lib/komorebi_vm/core/scenarios/placement.py @@ -0,0 +1,65 @@ +"""Where in the layout a window that opens goes, as the placement rules of the config say.""" + +from typing import Any + +from komorebi_vm.core.checks import Checks +from komorebi_vm.core.platform import Client, Platform + +CASES: list[tuple[str, Any, list[str], list[str]]] = [ + # (what it says, the placement rules, the titles in the order they open, the order they end in) + ("no rules: after the one in focus", None, ["a", "b", "c"], ["a", "b", "c"]), + ("every window to the primary tile", "Primary", ["a", "b", "c"], ["c", "b", "a"]), + ("by the number of the place, counted from 1", 2, ["a", "b", "c"], ["a", "c", "b"]), + ("text that is a number is the same place", "2", ["a", "b", "c"], ["a", "c", "b"]), + ( + "by the title: first, last, and after the one in focus", + { + "Primary": {"kind": "Title", "id": "special", "matching_strategy": "Equals"}, + "Last": [{"kind": "Title", "id": "chat", "matching_strategy": "Contains"}], + }, + ["one", "two", "special", "a chat room", "four"], + ["special", "one", "two", "a chat room", "four"], + ), +] + + +def _config(rules: Any) -> dict[str, Any]: + workspace: dict[str, Any] = {"name": "a", "layout": "BSP"} + if rules is not None: + workspace["initial_window_placement_rules"] = rules + return { + "default_workspace_padding": 10, + "default_container_padding": 10, + "monitors": [{"workspaces": [workspace]}], + } + + +def _order(platform: Platform) -> list[str]: + return [titles[0] for titles in platform.state().containers()] + + +def _settled(platform: Platform, title: str, count: int) -> None: + """Waits until `count` windows are managed and the last one to open has the focus.""" + platform.wait_for(lambda: platform.state().managed() == count, timeout=20) + platform.wait_for(lambda: platform.state().focused_title() == title, timeout=20) + + +def run(platform: Platform, checks: Checks) -> None: + """Starts the daemon with each case's rules, opens its windows one after the other, and checks + the order they are in. + + Nothing else may be open: the daemon takes up every window in view, and the order would + count it. The windows of a case are closed before the next one. A window is opened only after + the one before is managed and has the focus, because "after the one in focus" depends on it. + """ + for description, rules, titles, want in CASES: + platform.restart_with(_config(rules)) + clients: list[Client] = [] + for title in titles: + clients.append(platform.spawn(title)) + _settled(platform, title, len(clients)) + got = _order(platform) + checks.check(f"{description}: {', '.join(want)} ({got})", got == want) + for client in clients: + platform.close(client) + platform.wait_for(lambda: platform.state().managed() == 0, timeout=20) diff --git a/e2e/lib/tests/fake_platform.py b/e2e/lib/tests/fake_platform.py index 2c728707..3ee24cca 100644 --- a/e2e/lib/tests/fake_platform.py +++ b/e2e/lib/tests/fake_platform.py @@ -17,6 +17,11 @@ from komorebi_vm.core.snapshot import Snapshot, parse TILES = [Rect(0, 0, 640, 1080), Rect(640, 0, 640, 1080), Rect(1280, 0, 640, 1080)] +def _place_index(place: str, count: int) -> int: + """The index a placement rule's place stands for: `Primary`, `Last`, or a number from 1.""" + return int(place) - 1 if place.isdigit() else {"Primary": 0, "Last": count}[place] + + class FakePointer: """A pointer that, dragged from just outside the right edge of a tile, moves that edge.""" @@ -62,11 +67,13 @@ class FakePlatform: self.initial_rules: dict[str, int] = {} self.rules: dict[str, int] = {} self.ignored: set[str] = set() + self.placing = False self.unmanaged: list[str] = [] self.display_count = 2 self.display_of: dict[str, int] = {} self.forgets_displays = False self.running = True + self.placement: Any = None self.moves = moves self.focus_follows_move = focus_follows_move @@ -126,12 +133,38 @@ class FakePlatform: self.spaces[self.initial_rules.pop(title)].append([title]) elif title in self.rules: self.spaces[self.rules[title]].append([title]) + elif self.placing: + self._place(title) else: self.containers.append([title]) if len(self.containers) > len(self.tiles): self._relayout() return Client(title) + def _place(self, title: str) -> None: + """A window goes where the placement rules say, or after the one in focus, and has the + focus.""" + n = len(self.containers) + index = min(self.focus + 1, n) if n else 0 + rules = self.placement + if isinstance(rules, dict): + for place, matches in rules.items(): + for match in matches if isinstance(matches, list) else [matches]: + equals = match["matching_strategy"] == "Equals" + if (title == match["id"]) if equals else (match["id"] in title): + index = _place_index(place, n) + elif rules is not None: + index = 0 if rules == "Primary" else int(rules) - 1 + index = max(0, min(index, n)) + self.containers.insert(index, [title]) + self.focus = index + + def restart_with(self, config: dict[str, Any]) -> None: + self.start_komorebi() + workspace = config["monitors"][0]["workspaces"][0] + self.placement = workspace.get("initial_window_placement_rules") + self.placing = True + def _find(self, title: str) -> tuple[int, int]: for s, containers in enumerate(self.spaces): for i, stack in enumerate(containers): diff --git a/e2e/lib/tests/test_focus_moving.py b/e2e/lib/tests/test_focus_moving.py index 40600839..cc892fce 100644 --- a/e2e/lib/tests/test_focus_moving.py +++ b/e2e/lib/tests/test_focus_moving.py @@ -9,6 +9,7 @@ from komorebi_vm.core.scenarios import ( lifecycle, monocle, moving, + placement, resize, rules, stacks, @@ -279,3 +280,23 @@ def test_a_known_failure_does_not_fail_the_suite_until_it_is_fixed() -> None: clients = opened(fixed, checks) commands.unknown_monitor(fixed, checks, clients, known_failure=reason) assert checks.failed == 2 and checks.known == [] + + +def test_a_daemon_that_follows_the_placement_rules_passes() -> None: + platform = FakePlatform() + checks = Checks() + placement.run(platform, checks) + assert checks.failed == 0, checks.failures + assert checks.passed == len(placement.CASES) + + +def test_a_daemon_that_ignores_the_placement_rules_fails() -> None: + class Ignores(FakePlatform): + def restart_with(self, config: dict) -> None: + super().restart_with(config) + self.placement = None + + platform = Ignores() + checks = Checks() + placement.run(platform, checks) + assert checks.failed == len(placement.CASES) - 1, checks.failures diff --git a/e2e/lib/tests/test_tiling.py b/e2e/lib/tests/test_tiling.py index a5c0583b..bacc2f74 100644 --- a/e2e/lib/tests/test_tiling.py +++ b/e2e/lib/tests/test_tiling.py @@ -63,6 +63,9 @@ class FakePlatform: def start_komorebi(self) -> None: pass + def restart_with(self, config: dict) -> None: + pass + def komorebic(self, *args: str) -> str: return ""