#!/bin/bash # Round01 multi-page identity fixture QA. It verifies that Quick Switch keeps # same-App/same-title pages distinct for activation and close requests. 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" BUNDLE_ID="com.ar.Aligner" CONFIRM_DEFAULTS_KEY="QuickSwitchConfirmBeforeClose" REPORT_WAIT="${ALIGNER_ROUND1_MULTI_PAGE_REPORT_WAIT:-7.0}" ACTIVATION_REPORT="$BUILD_REPORT_ROOT/round01-multi-page-activation-report.json" CLOSE_REPORT="$BUILD_REPORT_ROOT/round01-multi-page-close-report.json" fail() { echo "Round01 multi-page identity fixture QA failed: $*" >&2 exit 1 } 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" } reset_close_confirmation_preference() { defaults delete "$BUNDLE_ID" "$CONFIRM_DEFAULTS_KEY" >/dev/null 2>&1 || true } wait_for_report() { local report="$1" local mode="$2" /usr/bin/python3 - "$report" "$REPORT_WAIT" "$mode" <<'PY' import json import sys import time path = sys.argv[1] timeout = float(sys.argv[2]) mode = sys.argv[3] deadline = time.monotonic() + timeout last_report = None def ready(report): root = report.get("rootView", {}) if report.get("snapshotLoaded") is not True: return False if mode == "activated": return ( report.get("quickSwitchVisible") is False and report.get("lastActivationResult") is not None ) if mode == "verifiedFailure": return ( report.get("quickSwitchVisible") is True and report.get("lastCloseResult") is not None and root.get("closeFeedbackKind") == "failure" ) return False while time.monotonic() < deadline: try: with open(path, "r", encoding="utf-8") as file: report = json.load(file) last_report = report if ready(report): sys.exit(0) except FileNotFoundError: pass except json.JSONDecodeError: pass time.sleep(0.2) if last_report is not None: print(json.dumps(last_report, indent=2, ensure_ascii=False), file=sys.stderr) print(f"multi-page identity report {path} did not reach mode={mode} within {timeout:.1f}s", file=sys.stderr) sys.exit(1) PY } run_case() { local report="$1" local sequence="$2" local mode="$3" local activation_flag="${4:-}" local close_flag="${5:-}" stop_current_aligner reset_close_confirmation_preference rm -f "$report" local args=( --round0-skip-permissions --round01-open-quick-switch --round01-waterfall-view-mode=vertical --round01-fixture-multi-page-identity --round01-disable-screenshot-refresh --round01-debug-mouse-sequence="$sequence" --round01-quick-switch-report="$report" ) if [ -n "$activation_flag" ]; then args+=(--round01-debug-window-activation) fi if [ -n "$close_flag" ]; then args+=(--round01-debug-window-close) fi "$APP/Contents/MacOS/Aligner" "${args[@]}" & APP_PID=$! cleanup() { kill "$APP_PID" 2>/dev/null || true wait "$APP_PID" 2>/dev/null || true stop_current_aligner } trap cleanup EXIT wait_for_report "$report" "$mode" kill "$APP_PID" 2>/dev/null || true wait "$APP_PID" 2>/dev/null || true trap - EXIT stop_current_aligner } assert_activation_report() { /usr/bin/python3 - "$ACTIVATION_REPORT" <<'PY' import json import sys path = sys.argv[1] with open(path, "r", encoding="utf-8") as file: report = json.load(file) def require(condition, message): if not condition: print(message, file=sys.stderr) print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr) sys.exit(1) root = report.get("rootView", {}) columns = root.get("waterfallColumns", []) cards = columns[0].get("cards", []) if columns else [] ids = [card.get("windowID") for card in cards] require(report.get("snapshotLoaded") is True, "activation fixture must load") require(report.get("quickSwitchVisible") is False, "activation must close Quick Switch") require(report.get("appCount") == 1, "multi-page fixture must expose one App") require(report.get("windowCount") == 3, "multi-page fixture must expose three pages") require(ids == [70101, 70102, 70103], "same-title pages must keep stable windowID order") require(report.get("lastCommittedAppGroupIndex") == 0, "activation must target App group 0") require(report.get("lastCommittedWindowIndex") == 1, "activation must target second page index") require(report.get("lastCommittedWindowID") == 70102, "activation commit must target page windowID 70102") require(report.get("lastActivationWindowID") == 70102, "activation service must receive exact page windowID 70102") require(report.get("lastActivationResult") == "activated", "debug activation must report activated") require(root.get("selectedWindowID") == 70102, "selectedWindowID must track exact activated page") print(json.dumps({ "case": "multiPageActivation", "windowIDs": ids, "lastActivationWindowID": report.get("lastActivationWindowID") }, indent=2, ensure_ascii=False)) PY } assert_close_report() { /usr/bin/python3 - "$CLOSE_REPORT" <<'PY' import json import sys path = sys.argv[1] with open(path, "r", encoding="utf-8") as file: report = json.load(file) def require(condition, message): if not condition: print(message, file=sys.stderr) print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr) sys.exit(1) root = report.get("rootView", {}) columns = root.get("waterfallColumns", []) cards = columns[0].get("cards", []) if columns else [] ids = [card.get("windowID") for card in cards] require(report.get("snapshotLoaded") is True, "close fixture must load") require(report.get("quickSwitchVisible") is True, "close verification failure must keep Quick Switch visible") require(report.get("appCount") == 1, "multi-page close fixture must expose one App") require(report.get("windowCount") == 3, "fixture must still expose all three pages when debug close does not remove them") require(ids == [70101, 70102, 70103], "close must not reorder or fake-remove same-title pages") require(report.get("lastCloseTargetKind") == "window", "close must target a window/page") require(report.get("lastCloseAppGroupIndex") == 0, "close must target App group 0") require(report.get("lastCloseWindowID") == 70103, "close service must receive exact page windowID 70103") require(report.get("lastCloseResult") == "requested", "debug close must report requested") require(report.get("suppressedCloseTargetCount") == 0, "requested-but-still-present must not suppress the card") require(root.get("closeFeedbackKind") == "failure", "still-present debug close must show verification failure") print(json.dumps({ "case": "multiPageClose", "windowIDs": ids, "lastCloseWindowID": report.get("lastCloseWindowID"), "closeFeedbackKind": root.get("closeFeedbackKind") }, indent=2, ensure_ascii=False)) PY } stop_current_aligner "$SCRIPT_DIR/package-app.sh" >&2 run_case "$ACTIVATION_REPORT" "click-card:0:1" "activated" "activation" "" assert_activation_report run_case "$CLOSE_REPORT" "click-card-close:0:2,confirm-close" "verifiedFailure" "" "close" assert_close_report echo "Round01 multi-page identity fixture QA passed."