#!/bin/bash # Round01 fullscreen overlay QA. It opens a temporary fullscreen host window, # then verifies Quick Switch can appear above the fullscreen Space and close. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" OUTPUT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # shellcheck source=build-output-paths.sh source "$SCRIPT_DIR/build-output-paths.sh" APP="$BUILD_CURRENT_APP" REPORT="$BUILD_REPORT_ROOT/round01-fullscreen-overlay-report.json" WORK_DIR="$(mktemp -d /tmp/aligner-round01-fullscreen-qa.XXXXXX)" READY_FILE="$WORK_DIR/fullscreen-ready.txt" HOST_LOG="$WORK_DIR/fullscreen-host.log" ALIGNER_LOG="$WORK_DIR/aligner.log" TARGET_SCREEN_INDEX="${ALIGNER_ROUND1_FULLSCREEN_SCREEN_INDEX:-0}" MOUSE_SCREEN_INDEX="${ALIGNER_ROUND1_FULLSCREEN_MOUSE_SCREEN_INDEX:-$TARGET_SCREEN_INDEX}" START_WAIT="${ALIGNER_ROUND1_FULLSCREEN_START_WAIT:-1.6}" OPEN_WAIT="${ALIGNER_ROUND1_FULLSCREEN_OPEN_WAIT:-1.0}" CLOSE_WAIT="${ALIGNER_ROUND1_FULLSCREEN_CLOSE_WAIT:-0.8}" fail() { echo "Round01 fullscreen overlay QA failed: $*" >&2 exit 1 } post_key_with_system_events() { local shortcut="$1" case "$shortcut" in escape) osascript -e 'tell application "System Events" to key code 53' ;; *) fail "unsupported System Events shortcut: $shortcut" ;; esac } aligner_pids_for_current_app() { ps -axo pid=,args= | while read -r pid command; do case "$command" in "$APP/Contents/MacOS/Aligner"*) echo "$pid" ;; esac done } stop_current_aligner() { for pid in $(aligner_pids_for_current_app); do kill "$pid" 2>/dev/null || true done for _ in {1..30}; do [ -z "$(aligner_pids_for_current_app)" ] && return sleep 0.1 done fail "current Aligner app did not exit before QA" } wait_for_file() { local path="$1" local timeout="$2" /usr/bin/python3 - "$path" "$timeout" <<'PY' import pathlib import sys import time path = pathlib.Path(sys.argv[1]) timeout = float(sys.argv[2]) deadline = time.monotonic() + timeout while time.monotonic() < deadline: if path.exists() and path.stat().st_size > 0: print(path.read_text(encoding="utf-8"), end="") sys.exit(0) time.sleep(0.1) sys.exit(1) PY } require_fullscreen_ready() { local path="$1" /usr/bin/python3 - "$path" <<'PY' import pathlib import sys text = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8") if "readyReason=windowDidEnterFullScreen" not in text: print(text[-6000:], end="", file=sys.stderr) sys.exit(1) print(text[-6000:], end="") PY } require_fullscreen_target_screen() { local path="$1" /usr/bin/python3 - "$path" <<'PY' import pathlib import re import sys text = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8") def frame(name): match = re.search(rf"^{name}=x:([-0-9]+),y:([-0-9]+),w:([0-9]+),h:([0-9]+)$", text, re.MULTILINE) if not match: print(text[-6000:], end="", file=sys.stderr) print(f"missing {name}", file=sys.stderr) sys.exit(1) return tuple(int(value) for value in match.groups()) requested = frame("requestedScreenFrame") target = frame("targetScreenFrame") if requested != target: print(text[-6000:], end="", file=sys.stderr) print(f"fullscreen host entered wrong screen: requested={requested} target={target}", file=sys.stderr) sys.exit(1) print(f"fullscreen host target screen verified: {target}") PY } wait_for_no_quick_switch() { local timeout="$1" /usr/bin/python3 - "$SCRIPT_DIR" "$timeout" <<'PY' import subprocess import sys import time script_dir = sys.argv[1] timeout = float(sys.argv[2]) deadline = time.monotonic() + timeout last_output = "" while time.monotonic() < deadline: result = subprocess.run( ["swift", f"{script_dir}/window-logic-qa.swift", "--expect-no-quick-switch"], text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) last_output = result.stdout if result.returncode == 0: print(last_output[-6000:], end="") sys.exit(0) time.sleep(0.2) print(last_output[-6000:], end="") sys.exit(1) PY } run_window_logic_check() { swift "$SCRIPT_DIR/window-logic-qa.swift" "$@" 2>&1 | head -c 6000 local status="${PIPESTATUS[0]}" return "$status" } cleanup() { post_key_with_system_events escape 2>/dev/null || true if [ -n "${ALIGNER_PID:-}" ] && kill -0 "$ALIGNER_PID" 2>/dev/null; then kill "$ALIGNER_PID" 2>/dev/null || true wait "$ALIGNER_PID" 2>/dev/null || true fi if [ -n "${HOST_PID:-}" ] && kill -0 "$HOST_PID" 2>/dev/null; then kill "$HOST_PID" 2>/dev/null || true wait "$HOST_PID" 2>/dev/null || true fi rm -rf "$WORK_DIR" } trap cleanup EXIT [ -x "$APP/Contents/MacOS/Aligner" ] || fail "missing built app: $APP" stop_current_aligner rm -f "$REPORT" echo "preparing fullscreen host screen index: $TARGET_SCREEN_INDEX" swift "$SCRIPT_DIR/round1-move-mouse-to-screen-center.swift" "$TARGET_SCREEN_INDEX" 2>&1 | head -c 6000 swift "$SCRIPT_DIR/round1-fullscreen-host.swift" \ --ready-file="$READY_FILE" \ --screen-index="$TARGET_SCREEN_INDEX" >"$HOST_LOG" 2>&1 & HOST_PID=$! if ! wait_for_file "$READY_FILE" 16.0; then tail -c 6000 "$HOST_LOG" >&2 || true fail "fullscreen host did not become ready" fi if ! require_fullscreen_ready "$READY_FILE"; then fail "fullscreen host did not report windowDidEnterFullScreen" fi if ! require_fullscreen_target_screen "$READY_FILE"; then fail "fullscreen host did not enter requested screen" fi echo "fullscreen host screen index: $TARGET_SCREEN_INDEX" echo "quick switch mouse screen index: $MOUSE_SCREEN_INDEX" swift "$SCRIPT_DIR/round1-move-mouse-to-screen-center.swift" "$MOUSE_SCREEN_INDEX" 2>&1 | head -c 6000 "$APP/Contents/MacOS/Aligner" \ --round0-skip-permissions \ --round01-open-quick-switch \ --round01-quick-switch-report="$REPORT" >"$ALIGNER_LOG" 2>&1 & ALIGNER_PID=$! sleep "$START_WAIT" sleep "$OPEN_WAIT" run_window_logic_check \ --expect-quick-switch \ --expect-single-aligner-overlay \ --expect-overlay-on-mouse-screen \ --expect-overlay-uses-screen-frame post_key_with_system_events escape sleep "$CLOSE_WAIT" wait_for_no_quick_switch 5.0 kill "$ALIGNER_PID" 2>/dev/null || true wait "$ALIGNER_PID" 2>/dev/null || true kill "$HOST_PID" 2>/dev/null || true wait "$HOST_PID" 2>/dev/null || true trap - EXIT rm -rf "$WORK_DIR" echo "Round01 fullscreen overlay QA passed"