Files
Classroom_Desktop/tests/test-classroom-chrome-windows.sh
T
ekklesiaandClaude Opus 5 0e91b4cbd3 fix: keep the dock's Close button reachable
The Close button followed focus, and moving the pointer down to the dock
moves focus off the popup. So the button disappeared somewhere on the
journey to it, and a secondary Chrome window could not be closed with a
mouse at all. Found on the endpoint; my fault.

The target is remembered instead. Once a secondary window has been
focused the dock keeps offering to close it, and losing focus — to the
dock, the main browser, a print dialog, any layer-shell surface —
changes nothing. It moves on only when that window closes or another
secondary takes over, and it falls back to another open secondary rather
than to nothing, so cancelling a print dialog leaves the render window
still closeable. It is chosen from the secondary list, which never
contains the first window, so it can never name the main browser.

Restoring the floating role to a window that already existed no longer
moves or focuses it. Moving it would undo wherever the teacher dragged
it; focusing it would raise the render window over the print dialog
sitting above it, which is one way the print flow was getting disturbed.

Verified by driving a real pointer through the exact path that was
broken: pointer over the popup, click to focus, then travel down to the
dock in steps, screenshotting throughout. The button is still there on
arrival, at 992,1041. Then the print flow: main browser, render window
and a separate Print dialog, pointer to the dock, button still there,
and killing the dialog leaves the render window as the new target with
the main browser untouched.

The button's own action could not be actuated in that harness, because
synthetic pointer input does not reach Wayland clients under headless
sway — the Browser button does not respond to it either. The action was
run directly instead, closing exactly the remembered container and
leaving main Chrome alive.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-07 20:05:42 -05:00

228 lines
8.2 KiB
Bash
Executable File

#!/bin/bash
set -uo pipefail
# Secondary Chrome windows: which window is which, how a popup is placed,
# and which one the dock offers to close.
#
# tests/test-classroom-chrome-windows.sh
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PYTHONPATH="$ROOT/lib" python3 - "$ROOT" <<'PY'
import importlib.machinery
import importlib.util
import json
import os
import sys
import tempfile
from pathlib import Path
from classroom_chrome import ChromeRoles, chrome_windows
root = Path(sys.argv[1])
path = root / "scripts/classroom-chrome-windows"
loader = importlib.machinery.SourceFileLoader("classroom_chrome_windows", str(path))
spec = importlib.util.spec_from_loader(loader.name, loader)
script = importlib.util.module_from_spec(spec)
loader.exec_module(script)
source = path.read_text()
dock_source = (root / "scripts/classroom-dock").read_text()
passed = failed = 0
def check(label, condition, detail=""):
global passed, failed
if condition:
print(f" ok {label}")
passed += 1
else:
print(f" FAIL {label}")
if detail:
print(f" {detail}")
failed += 1
def window(identifier, title, floating=False, hidden=False, focused=False):
return {
"id": identifier,
"app_id": "google-chrome",
"class": "",
"instance": "",
"pid": 4242,
"name": title,
"focused": focused,
"floating": floating,
"hidden": hidden,
}
print("1. creation order decides which window is the browser")
roles = ChromeRoles()
main = window(5, "Main browser - Google Chrome")
popup = window(8, "about:blank - Google Chrome")
change = roles.update([main])
check("the first Chrome window is the browser", change["primary"] == 5)
check("it is not a popup", change["new_secondary"] == [])
change = roles.update([main, popup])
check("a later Chrome window is a popup", change["new_secondary"] == [8])
check("the browser stays the browser", change["primary"] == 5)
renamed_main = window(5, "Something else entirely")
renamed_popup = window(8, "A different page title")
change = roles.update([renamed_main, renamed_popup])
check("titles never change a role",
change["primary"] == 5 and not change["new_secondary"])
check("a shared PID never confuses roles",
len(chrome_windows([renamed_main, renamed_popup])) == 2)
print()
print("2. several popups at once")
third = window(9, "Third window")
change = roles.update([main, popup, third])
check("a third top-level is also a popup", change["new_secondary"] == [9])
change = roles.update([main, popup])
check("closing one leaves the other a popup", roles.secondary == [8])
check("the closed one is reported", change["removed"] == [9])
print()
print("3. how a popup is placed")
commands = []
script.command = lambda argument: commands.append(argument)
script.SETTLE_SECONDS = 0
script.configure_secondary(8)
check("floats exactly one container", commands[0] == "[con_id=8] floating enable")
check("borderless, like the main browser", commands[1] == "[con_id=8] border none")
check("about three quarters of the output",
"width 76 ppt height 76 ppt" in commands[2], commands)
check("centred", commands[3] == "[con_id=8] move position center")
check("focused", commands[4] == "[con_id=8] focus")
# Chrome resizes a popup after the fact, so the placement is repeated
# once the size has settled or the window ends up off to one side.
check("placed again after Chrome settles",
commands[-1] == "[con_id=8] move position center"
and "width 76 ppt" in commands[-2], commands)
check("placed exactly twice, never on a loop",
commands.count("[con_id=8] move position center") == 2, commands)
check("never matches all Chrome windows at once",
all("app_id" not in item for item in commands))
commands.clear()
script.configure_secondary(8, arriving=False)
check("restoring the role never moves the window",
not any("move position" in item for item in commands), commands)
check("restoring the role never raises it over a dialog",
not any("focus" in item for item in commands), commands)
check("it only puts the window back to floating and borderless",
commands == ["[con_id=8] floating enable", "[con_id=8] border none"],
commands)
commands.clear()
script.configure_primary(8)
check("a promoted window goes back to tiled",
commands[0] == "[con_id=8] floating disable")
check("and borderless", commands[1] == "[con_id=8] border none")
print()
print("4. the dock's Close target survives losing focus")
# The whole point: the pointer has to travel from the popup down to the
# dock, and that moves focus. If the target followed focus the button
# would vanish before anyone could reach it.
active = script.ActiveSecondary()
chrome = {5: window(5, "main", focused=True)}
check("nothing to close with only the main browser",
active.update([], chrome) is None)
chrome = {5: window(5, "main"), 8: window(8, "popup", focused=True)}
check("a focused popup becomes the target", active.update([8], chrome) == 8)
chrome = {5: window(5, "main"), 8: window(8, "popup")}
check("it stays the target when nothing is focused",
active.update([8], chrome) == 8)
chrome = {5: window(5, "main", focused=True), 8: window(8, "popup")}
check("it stays when focus moves to the main browser",
active.update([8], chrome) == 8)
check("the main browser is never the target", active.target != 5)
chrome = {5: window(5, "main"), 8: window(8, "popup"), 9: window(9, "dialog", focused=True)}
check("a newer focused popup takes over", active.update([8, 9], chrome) == 9)
chrome = {5: window(5, "main"), 8: window(8, "popup"), 9: window(9, "dialog")}
check("and keeps it after focus moves to the dock",
active.update([8, 9], chrome) == 9)
chrome = {5: window(5, "main"), 8: window(8, "popup")}
check("closing the dialog falls back to the render window",
active.update([8], chrome) == 8)
chrome = {5: window(5, "main")}
check("closing the last popup clears it", active.update([], chrome) is None)
fresh = script.ActiveSecondary()
chrome = {5: window(5, "main", focused=True), 8: window(8, "popup")}
check("an unfocused popup is still closeable",
fresh.update([8], chrome) == 8)
hidden = script.ActiveSecondary()
chrome = {8: window(8, "one", hidden=True)}
check("a minimised popup is not adopted",
hidden.update([8], chrome) is None)
print()
print("5. the state the dock reads")
work = Path(tempfile.mkdtemp())
state = work / "chrome-close-target"
script.publish(9, state)
check("publishes the container id", state.read_text().strip() == "9")
check("published by rename, not left half written",
not (work / "chrome-close-target.tmp").exists())
script.publish(None, state)
check("empty when there is nothing to close", state.read_text().strip() == "")
print()
print("6. the dock owns the button, not the window")
check("no layer-shell surface in the window helper",
"GtkLayerShell" not in source)
check("no second GTK process spawned for a button",
"Gtk.main" not in source and "show_close_button" not in source)
check("no geometry maths chasing a window",
"output_rect" not in source and "button_margins" not in source)
check("the dock has a close action", '"close"' in dock_source)
check("the dock closes exactly one container",
'[con_id={self.close_target}] kill' in dock_source)
check("the dock never kills Chrome processes",
"pkill" not in dock_source and "killall" not in dock_source)
check("the dock watches rather than polls for it",
"monitor_directory" in dock_source)
print()
print("7. driven by Sway events, not a poll")
check("subscribes to window events", '"-t", "subscribe"' in source)
check("ignores title churn",
"title" not in script.INTERESTING and "new" in script.INTERESTING)
check("reacts to focus and movement",
{"focus", "move", "close"} <= set(script.INTERESTING))
check("no polling loop constant", "POLL_SECONDS" not in source)
print()
print("8. the session still starts it")
config = (root / "config/sway/config").read_text()
check("started with the session",
"exec --no-startup-id /usr/local/bin/classroom-chrome-windows" in config)
check("no broad Chrome floating rule",
not any("floating enable" in line and "google-chrome" in line
for line in config.splitlines()))
print()
print(f"passed: {passed} failed: {failed}")
sys.exit(1 if failed else 0)
PY