#!/bin/bash
|
# Round01 Quick Switch screenshot refill QA. It opens the real Quick Switch
|
# session and waits until screenshot-eligible cards are resolved to either real
|
# screenshots or explicit skeleton fallbacks.
|
|
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-screenshot-session-report.json"
|
OPEN_WAIT="${ALIGNER_ROUND1_SCREENSHOT_SESSION_OPEN_WAIT:-1.2}"
|
REPORT_WAIT="${ALIGNER_ROUND1_SCREENSHOT_SESSION_REPORT_WAIT:-8.0}"
|
SCREENSHOT_WAIT="${ALIGNER_ROUND1_SCREENSHOT_SESSION_WAIT:-45.0}"
|
SKIP_OVERLAY_FRAME_CHECK="${ALIGNER_ROUND1_SCREENSHOT_SESSION_SKIP_OVERLAY_FRAME_CHECK:-0}"
|
REQUIRE_REAL_SCREENSHOT="${ALIGNER_ROUND1_SCREENSHOT_SESSION_REQUIRE_REAL:-1}"
|
|
fail() {
|
echo "Round01 screenshot session 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"
|
}
|
|
wait_for_loaded_report() {
|
/usr/bin/python3 - "$REPORT" "$REPORT_WAIT" <<'PY'
|
import json
|
import sys
|
import time
|
|
path = sys.argv[1]
|
timeout = float(sys.argv[2])
|
deadline = time.monotonic() + timeout
|
last_report = None
|
|
while time.monotonic() < deadline:
|
try:
|
with open(path, "r", encoding="utf-8") as file:
|
report = json.load(file)
|
last_report = report
|
if report.get("snapshotLoaded") is True:
|
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"snapshot report did not become loaded within {timeout:.1f}s", file=sys.stderr)
|
sys.exit(1)
|
PY
|
}
|
|
wait_for_screenshots() {
|
/usr/bin/python3 - "$REPORT" "$SCREENSHOT_WAIT" <<'PY'
|
import json
|
import sys
|
import time
|
|
path = sys.argv[1]
|
timeout = float(sys.argv[2])
|
deadline = time.monotonic() + timeout
|
last_report = None
|
|
while time.monotonic() < deadline:
|
try:
|
with open(path, "r", encoding="utf-8") as file:
|
report = json.load(file)
|
last_report = report
|
root = report.get("rootView", {})
|
eligible = root.get("screenshotEligibleCount", 0)
|
resolved = root.get("screenshotResolvedCount", 0)
|
pending = root.get("screenshotPendingCount", 0)
|
if report.get("snapshotLoaded") is True and eligible > 0 and resolved == eligible and pending == 0:
|
sys.exit(0)
|
except FileNotFoundError:
|
pass
|
except json.JSONDecodeError:
|
pass
|
time.sleep(0.25)
|
|
if last_report is not None:
|
root = last_report.get("rootView", {})
|
print(json.dumps({
|
"snapshotLoaded": last_report.get("snapshotLoaded"),
|
"windowCount": last_report.get("windowCount"),
|
"screenshotEligibleCount": root.get("screenshotEligibleCount"),
|
"screenshotResolvedCount": root.get("screenshotResolvedCount"),
|
"screenshotPendingCount": root.get("screenshotPendingCount"),
|
"realScreenshotCount": root.get("realScreenshotCount"),
|
"skeletonFallbackCount": root.get("skeletonFallbackCount"),
|
}, indent=2, ensure_ascii=False), file=sys.stderr)
|
print(f"screenshot refill did not resolve eligible cards within {timeout:.1f}s", file=sys.stderr)
|
sys.exit(1)
|
PY
|
}
|
|
assert_report() {
|
/usr/bin/python3 - "$REPORT" "$REQUIRE_REAL_SCREENSHOT" <<'PY'
|
import json
|
import os
|
import sys
|
|
path = sys.argv[1]
|
require_real_screenshot = sys.argv[2] == "1"
|
with open(path, "r", encoding="utf-8") as file:
|
report = json.load(file)
|
|
def stable_fingerprint(value):
|
hash_value = 0xCBF29CE484222325
|
for byte in (value or "").encode("utf-8"):
|
hash_value ^= byte
|
hash_value = (hash_value * 0x100000001B3) & 0xFFFFFFFFFFFFFFFF
|
return f"{hash_value:016x}"
|
|
def summary():
|
root = report.get("rootView", {})
|
return {
|
"reportBasename": os.path.basename(path),
|
"reportPathHash": stable_fingerprint(path),
|
"snapshotLoaded": report.get("snapshotLoaded"),
|
"quickSwitchVisible": report.get("quickSwitchVisible"),
|
"windowCount": report.get("windowCount"),
|
"eligible": root.get("screenshotEligibleCount"),
|
"resolved": root.get("screenshotResolvedCount"),
|
"pending": root.get("screenshotPendingCount"),
|
"real": root.get("realScreenshotCount"),
|
"fallback": root.get("skeletonFallbackCount"),
|
"notRequested": root.get("screenshotNotRequestedCount"),
|
}
|
|
def require(condition, message):
|
if not condition:
|
print(message, file=sys.stderr)
|
print(json.dumps(summary(), indent=2, ensure_ascii=False), file=sys.stderr)
|
sys.exit(1)
|
|
root = report.get("rootView", {})
|
columns = root.get("waterfallColumns", [])
|
cards = [card for column in columns for card in column.get("cards", [])]
|
eligible = root.get("screenshotEligibleCount")
|
resolved = root.get("screenshotResolvedCount")
|
pending = root.get("screenshotPendingCount")
|
not_requested = root.get("screenshotNotRequestedCount")
|
real = root.get("realScreenshotCount")
|
fallback = root.get("skeletonFallbackCount")
|
|
require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true")
|
require(report.get("quickSwitchVisible") is True, "Quick Switch must stay visible during screenshot QA")
|
require(cards, "screenshot QA requires at least one Window Card")
|
require(isinstance(eligible, int) and eligible > 0, "screenshot QA requires at least one eligible card")
|
require(isinstance(resolved, int) and resolved == eligible, "all eligible screenshots must be resolved")
|
require(pending == 0, "no screenshot-eligible card may remain pending")
|
require(isinstance(not_requested, int), "root view must expose screenshotNotRequestedCount")
|
require(eligible + not_requested == len(cards), "eligible + not-requested cards must cover all cards")
|
if require_real_screenshot:
|
require(isinstance(real, int) and real >= 1, "Screen Recording authorized QA must produce at least one real screenshot")
|
else:
|
require(isinstance(real, int), "root view must expose realScreenshotCount")
|
require(isinstance(fallback, int), "root view must expose skeletonFallbackCount")
|
require(real + fallback == resolved, "real + fallback screenshots must equal resolved screenshots")
|
|
for card in cards:
|
source = card.get("screenshotSource")
|
strategy = card.get("thumbnailStrategy")
|
require(source in {"realScreenshot", "skeletonFallback", "notRequested"}, "screenshot source must be resolved or explicitly not requested")
|
require(source != "pending", "no card may remain pending after refill wait")
|
if source == "realScreenshot":
|
require(strategy == "realScreenshot", "real screenshot cards must expose realScreenshot thumbnail strategy")
|
if source == "skeletonFallback":
|
reason = card.get("screenshotFallbackReason")
|
require(reason is not None, "skeleton fallback cards must expose fallback reason")
|
if card.get("identifierSource") == "syntheticAX":
|
require("syntheticWindowID" in reason, "synthetic AX cards must fallback with syntheticWindowID reason")
|
if source == "notRequested":
|
reason = card.get("screenshotNotRequestedReason")
|
require(reason is not None, "not-requested cards must expose a not-requested reason")
|
require(strategy in {"skeleton", "skeletonPreferred"}, "not-requested cards must remain skeleton based")
|
|
print(json.dumps({
|
"windowCount": report.get("windowCount"),
|
"eligible": eligible,
|
"resolved": resolved,
|
"real": real,
|
"fallback": fallback,
|
"notRequested": not_requested,
|
}, indent=2, ensure_ascii=False))
|
PY
|
}
|
|
stop_current_aligner
|
"$SCRIPT_DIR/package-app.sh" >&2
|
rm -f "$REPORT"
|
|
"$APP/Contents/MacOS/Aligner" \
|
--round0-skip-permissions \
|
--round01-open-quick-switch \
|
--round01-quick-switch-report="$REPORT" &
|
|
APP_PID=$!
|
|
cleanup() {
|
if kill -0 "$APP_PID" 2>/dev/null; then
|
kill "$APP_PID" 2>/dev/null || true
|
wait "$APP_PID" 2>/dev/null || true
|
fi
|
stop_current_aligner
|
}
|
trap cleanup EXIT
|
|
sleep "$OPEN_WAIT"
|
wait_for_loaded_report
|
|
if [ "$SKIP_OVERLAY_FRAME_CHECK" = "1" ]; then
|
swift "$SCRIPT_DIR/window-logic-qa.swift" \
|
--expect-quick-switch \
|
--expect-single-aligner-overlay
|
else
|
swift "$SCRIPT_DIR/window-logic-qa.swift" \
|
--expect-quick-switch \
|
--expect-single-aligner-overlay \
|
--expect-overlay-on-mouse-screen \
|
--expect-overlay-uses-screen-frame
|
fi
|
|
wait_for_screenshots
|
assert_report
|
|
cleanup
|
trap - EXIT
|
|
swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-no-quick-switch
|