* Fix command palette shortcuts to stay window-scoped * Fix cross-window command palette typing focus lock
219 lines
7.4 KiB
Python
219 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Regression test: command palette should open only in the active window.
|
|
|
|
Why: if command-palette toggle is broadcast to all windows, inactive windows can
|
|
end up with an open palette that steals focus once they become key.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
from cmux import cmux, cmuxError
|
|
|
|
|
|
SOCKET_PATH = os.environ.get("CMUX_SOCKET", "/tmp/cmux-debug.sock")
|
|
|
|
|
|
def _wait_until(predicate, timeout_s: float = 5.0, interval_s: float = 0.05, message: str = "timeout") -> None:
|
|
start = time.time()
|
|
while time.time() - start < timeout_s:
|
|
if predicate():
|
|
return
|
|
time.sleep(interval_s)
|
|
raise cmuxError(message)
|
|
|
|
|
|
def _palette_visible(client: cmux, window_id: str) -> bool:
|
|
res = client._call("debug.command_palette.visible", {"window_id": window_id}) or {}
|
|
return bool(res.get("visible"))
|
|
|
|
|
|
def _palette_results(client: cmux, window_id: str, limit: int = 20) -> dict:
|
|
return client.command_palette_results(window_id=window_id, limit=limit)
|
|
|
|
|
|
def _set_palette_visible(client: cmux, window_id: str, visible: bool) -> None:
|
|
if _palette_visible(client, window_id) == visible:
|
|
return
|
|
client._call("debug.command_palette.toggle", {"window_id": window_id})
|
|
_wait_until(
|
|
lambda: _palette_visible(client, window_id) == visible,
|
|
timeout_s=3.0,
|
|
message=f"palette in {window_id} did not become {visible}",
|
|
)
|
|
|
|
|
|
def _focus_window(client: cmux, window_id: str) -> None:
|
|
client.focus_window(window_id)
|
|
client.activate_app()
|
|
_wait_until(
|
|
lambda: client.current_window().lower() == window_id.lower(),
|
|
timeout_s=3.0,
|
|
message=f"failed to focus window {window_id}",
|
|
)
|
|
time.sleep(0.15)
|
|
|
|
|
|
def _assert_shortcut_window_scoped(client: cmux, shortcut: str, w1: str, w2: str) -> None:
|
|
_set_palette_visible(client, w1, False)
|
|
_set_palette_visible(client, w2, False)
|
|
|
|
_focus_window(client, w1)
|
|
client.simulate_shortcut(shortcut)
|
|
_wait_until(
|
|
lambda: _palette_visible(client, w1),
|
|
timeout_s=3.0,
|
|
message=f"{shortcut} did not open palette in window1",
|
|
)
|
|
if _palette_visible(client, w2):
|
|
raise cmuxError(f"{shortcut} in window1 incorrectly opened palette in window2")
|
|
|
|
_focus_window(client, w2)
|
|
client.simulate_shortcut(shortcut)
|
|
_wait_until(
|
|
lambda: _palette_visible(client, w2),
|
|
timeout_s=3.0,
|
|
message=f"{shortcut} did not open palette in window2",
|
|
)
|
|
if not _palette_visible(client, w1):
|
|
raise cmuxError(
|
|
f"{shortcut} in window2 incorrectly toggled window1 palette off "
|
|
"(cross-window routing regression)"
|
|
)
|
|
|
|
client.simulate_shortcut(shortcut)
|
|
_wait_until(
|
|
lambda: not _palette_visible(client, w2),
|
|
timeout_s=3.0,
|
|
message=f"second {shortcut} did not close palette in window2",
|
|
)
|
|
if not _palette_visible(client, w1):
|
|
raise cmuxError(
|
|
f"second {shortcut} in window2 incorrectly changed window1 palette visibility"
|
|
)
|
|
|
|
_focus_window(client, w1)
|
|
client.simulate_shortcut(shortcut)
|
|
_wait_until(
|
|
lambda: not _palette_visible(client, w1),
|
|
timeout_s=3.0,
|
|
message=f"second {shortcut} did not close palette in window1",
|
|
)
|
|
|
|
|
|
def _assert_cross_window_typing_after_mixed_shortcuts(client: cmux, w1: str, w2: str) -> None:
|
|
_set_palette_visible(client, w1, False)
|
|
_set_palette_visible(client, w2, False)
|
|
|
|
_focus_window(client, w1)
|
|
client.simulate_shortcut("cmd+shift+p")
|
|
_wait_until(
|
|
lambda: _palette_visible(client, w1),
|
|
timeout_s=3.0,
|
|
message="cmd+shift+p did not open palette in window1",
|
|
)
|
|
_wait_until(
|
|
lambda: str(_palette_results(client, w1).get("mode") or "") == "commands",
|
|
timeout_s=3.0,
|
|
message="window1 palette did not enter commands mode",
|
|
)
|
|
window1_query_before = str(_palette_results(client, w1).get("query") or "")
|
|
|
|
_focus_window(client, w2)
|
|
client.simulate_shortcut("cmd+p")
|
|
_wait_until(
|
|
lambda: _palette_visible(client, w2),
|
|
timeout_s=3.0,
|
|
message="cmd+p did not open palette in window2",
|
|
)
|
|
_wait_until(
|
|
lambda: str(_palette_results(client, w2).get("mode") or "") == "switcher",
|
|
timeout_s=3.0,
|
|
message="window2 palette did not enter switcher mode",
|
|
)
|
|
|
|
typed = ""
|
|
for ch in "crosswindow":
|
|
typed += ch
|
|
client.simulate_type(ch)
|
|
_wait_until(
|
|
lambda expected=typed: str(_palette_results(client, w2).get("query") or "").lower() == expected,
|
|
timeout_s=1.8,
|
|
message=(
|
|
"typing into window2 palette did not accumulate query text "
|
|
f"(expected {typed!r})"
|
|
),
|
|
)
|
|
|
|
window1_query_now = str(_palette_results(client, w1).get("query") or "")
|
|
if window1_query_now != window1_query_before:
|
|
raise cmuxError(
|
|
"typing in window2 changed window1 command-palette query "
|
|
f"(before={window1_query_before!r}, now={window1_query_now!r})"
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
with cmux(SOCKET_PATH) as client:
|
|
client.activate_app()
|
|
time.sleep(0.2)
|
|
w1 = client.current_window()
|
|
w2 = client.new_window()
|
|
time.sleep(0.25)
|
|
|
|
_ = client.new_workspace(window_id=w1)
|
|
_ = client.new_workspace(window_id=w2)
|
|
time.sleep(0.25)
|
|
_set_palette_visible(client, w1, False)
|
|
_set_palette_visible(client, w2, False)
|
|
|
|
# Open palette in window1 and verify window2 remains untouched.
|
|
client._call("debug.command_palette.toggle", {"window_id": w1})
|
|
_wait_until(
|
|
lambda: _palette_visible(client, w1),
|
|
timeout_s=3.0,
|
|
message="window1 command palette did not open",
|
|
)
|
|
if _palette_visible(client, w2):
|
|
raise cmuxError("window2 palette became visible when toggling window1")
|
|
|
|
# Closing window1 palette should not affect window2.
|
|
client._call("debug.command_palette.toggle", {"window_id": w1})
|
|
_wait_until(
|
|
lambda: not _palette_visible(client, w1),
|
|
timeout_s=3.0,
|
|
message="window1 command palette did not close",
|
|
)
|
|
|
|
# Mirror the same check in the other direction.
|
|
client._call("debug.command_palette.toggle", {"window_id": w2})
|
|
_wait_until(
|
|
lambda: _palette_visible(client, w2),
|
|
timeout_s=3.0,
|
|
message="window2 command palette did not open",
|
|
)
|
|
if _palette_visible(client, w1):
|
|
raise cmuxError("window1 palette became visible when toggling window2")
|
|
client._call("debug.command_palette.toggle", {"window_id": w2})
|
|
_wait_until(
|
|
lambda: not _palette_visible(client, w2),
|
|
timeout_s=3.0,
|
|
message="window2 command palette did not close",
|
|
)
|
|
|
|
# Reproduce keyboard-shortcut window-scoping path:
|
|
# opening from window2 must not jump back and toggle window1.
|
|
_assert_shortcut_window_scoped(client, "cmd+shift+p", w1, w2)
|
|
_assert_shortcut_window_scoped(client, "cmd+p", w1, w2)
|
|
_assert_cross_window_typing_after_mixed_shortcuts(client, w1, w2)
|
|
|
|
print("PASS: command palette is scoped to active window")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|