05cc026d75
The picker blocks on user input for however long they take. In that window niri can destroy a dynamic workspace (its last window closed) and renumber every later one down, so the pre-pick current["idx"] can point at the wrong workspace by the time move-window-to-workspace runs — restoring to the wrong place. The stashed window itself can also have closed in the meantime, in which case the move/focus calls would silently no-op. main() now re-reads workspaces/windows after pick() returns, recomputes the current workspace, and confirms the chosen window still exists before acting; each bails with a notify and the documented exit code otherwise. Adds window_exists() as a pure, tested helper alongside the existing pure functions. Also: clarify the focus=false comment in niri/config.kdl (it read as if focus=false caused the problem it actually prevents), and note in the design doc's Out of scope section that the stash's index-1 pinning is per-output, so a second monitor would need its own analysis of the Mod+1..9 offset.
94 lines
4.1 KiB
Python
94 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Unit tests for window-restore's pure logic (no niri, no wofi, no subprocess).
|
|
Loads the hyphenated script by path and drives the pure functions with the JSON
|
|
shapes `niri msg -j workspaces` / `windows` actually return."""
|
|
import importlib.util
|
|
import os
|
|
import sys
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
MOD_PATH = os.path.join(HERE, "..", "window-restore.py")
|
|
spec = importlib.util.spec_from_file_location("window_restore", MOD_PATH)
|
|
wr = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(wr)
|
|
|
|
fails = 0
|
|
|
|
|
|
def check(desc, got, want):
|
|
global fails
|
|
if got == want:
|
|
print(f"ok - {desc}")
|
|
else:
|
|
print(f"FAIL - {desc} (got {got!r} want {want!r})")
|
|
fails = 1
|
|
|
|
|
|
# Shapes mirror real `niri msg -j` output: the stash is always idx 1.
|
|
WS_WITH_STASH = [
|
|
{"id": 1, "idx": 1, "name": "minimized", "is_focused": False},
|
|
{"id": 2, "idx": 2, "name": None, "is_focused": True},
|
|
{"id": 3, "idx": 3, "name": None, "is_focused": False},
|
|
]
|
|
WS_NO_STASH = [
|
|
{"id": 2, "idx": 1, "name": None, "is_focused": True},
|
|
{"id": 3, "idx": 2, "name": None, "is_focused": False},
|
|
]
|
|
WS_ON_STASH = [
|
|
{"id": 1, "idx": 1, "name": "minimized", "is_focused": True},
|
|
{"id": 2, "idx": 2, "name": None, "is_focused": False},
|
|
]
|
|
|
|
WINDOWS = [
|
|
{"id": 10, "title": "Balatro", "app_id": "steam_app_2379780", "workspace_id": 1},
|
|
{"id": 11, "title": "Pi-hole", "app_id": "firefox", "workspace_id": 1},
|
|
{"id": 12, "title": "notes", "app_id": "Alacritty", "workspace_id": 2},
|
|
]
|
|
|
|
# --- stash_workspace / current_workspace -----------------------------------
|
|
check("stash found when declared", wr.stash_workspace(WS_WITH_STASH)["id"], 1)
|
|
check("stash is None when undeclared", wr.stash_workspace(WS_NO_STASH), None)
|
|
check("stash is None for empty list", wr.stash_workspace([]), None)
|
|
check("current workspace is the focused one", wr.current_workspace(WS_WITH_STASH)["id"], 2)
|
|
check("current is None when none focused", wr.current_workspace([]), None)
|
|
check("focused-on-stash is detectable",
|
|
wr.current_workspace(WS_ON_STASH)["id"] == wr.stash_workspace(WS_ON_STASH)["id"], True)
|
|
|
|
# --- stashed_windows -------------------------------------------------------
|
|
check("only windows on the stash", [w["id"] for w in wr.stashed_windows(WINDOWS, 1)], [10, 11])
|
|
check("empty stash yields nothing", wr.stashed_windows(WINDOWS, 99), [])
|
|
|
|
# --- label -----------------------------------------------------------------
|
|
check("label joins app_id and title", wr.label(WINDOWS[1]), "firefox — Pi-hole")
|
|
check("label falls back to title when app_id missing",
|
|
wr.label({"id": 5, "app_id": None, "title": "Bare Title"}), "Bare Title")
|
|
check("label falls back to app_id when title empty",
|
|
wr.label({"id": 5, "app_id": "firefox", "title": ""}), "firefox")
|
|
check("label never renders empty",
|
|
wr.label({"id": 5, "app_id": None, "title": None}), "window 5")
|
|
|
|
# --- resolve ---------------------------------------------------------------
|
|
check("resolve maps a label back to its window",
|
|
wr.resolve(WINDOWS, "firefox — Pi-hole")["id"], 11)
|
|
check("resolve returns None for no match", wr.resolve(WINDOWS, "nope"), None)
|
|
|
|
DUPES = [
|
|
{"id": 20, "title": "same", "app_id": "Alacritty", "workspace_id": 1},
|
|
{"id": 21, "title": "same", "app_id": "Alacritty", "workspace_id": 1},
|
|
]
|
|
check("duplicate labels resolve to the first", wr.resolve(DUPES, "Alacritty — same")["id"], 20)
|
|
|
|
# --- window_exists -----------------------------------------------------------
|
|
check("window_exists is True for a present window", wr.window_exists(WINDOWS, 11), True)
|
|
check("window_exists is False once the window has closed", wr.window_exists(WINDOWS, 999), False)
|
|
check("window_exists is False for an empty list", wr.window_exists([], 11), False)
|
|
|
|
# --- pick ------------------------------------------------------------------
|
|
check("pick returns the chooser's selection",
|
|
wr.pick(["a", "b"], lambda text: "b"), "b")
|
|
check("pick passes newline-joined labels to the chooser",
|
|
wr.pick(["a", "b"], lambda text: text), "a\nb")
|
|
check("cancelled pick returns empty", wr.pick(["a"], lambda text: ""), "")
|
|
|
|
sys.exit(fails)
|