#!/bin/bash
|
# Round01.1 theme toggle fixture QA. It verifies the 0.1.1 Quick Switch
|
# light/dark skin toggle, persistence, top-right placement, and dark gradient
|
# backdrop evidence without changing the 0.1 interaction model.
|
|
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"
|
DOMAIN="com.ar.Aligner"
|
THEME_KEY="com.ar.Aligner.preferences.appearance.theme"
|
REPORT_WAIT="${ALIGNER_ROUND1_THEME_TOGGLE_REPORT_WAIT:-6.0}"
|
REPORT_DEFAULT="$BUILD_REPORT_ROOT/round01-theme-toggle-default-report.json"
|
REPORT_DARK="$BUILD_REPORT_ROOT/round01-theme-toggle-dark-report.json"
|
REPORT_DARK_REOPEN="$BUILD_REPORT_ROOT/round01-theme-toggle-dark-reopen-report.json"
|
REPORT_LIGHT="$BUILD_REPORT_ROOT/round01-theme-toggle-light-report.json"
|
OLD_THEME_SET=0
|
OLD_THEME=""
|
|
fail() {
|
echo "Round01.1 theme toggle 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"
|
}
|
|
preserve_user_theme() {
|
if OLD_THEME="$(/usr/bin/defaults read "$DOMAIN" "$THEME_KEY" 2>/dev/null)"; then
|
OLD_THEME_SET=1
|
else
|
OLD_THEME_SET=0
|
OLD_THEME=""
|
fi
|
}
|
|
restore_user_theme() {
|
if [ "$OLD_THEME_SET" -eq 1 ]; then
|
/usr/bin/defaults write "$DOMAIN" "$THEME_KEY" -string "$OLD_THEME"
|
else
|
/usr/bin/defaults delete "$DOMAIN" "$THEME_KEY" >/dev/null 2>&1 || true
|
fi
|
}
|
|
wait_for_theme_report() {
|
local report="$1"
|
local expected_theme="$2"
|
local expected_mouse_command="${3:-}"
|
|
/usr/bin/python3 - "$report" "$expected_theme" "$expected_mouse_command" "$REPORT_WAIT" <<'PY'
|
import json
|
import sys
|
import time
|
|
path = sys.argv[1]
|
expected_theme = sys.argv[2]
|
expected_mouse_command = sys.argv[3]
|
timeout = float(sys.argv[4])
|
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", {})
|
if (
|
report.get("snapshotLoaded") is True
|
and report.get("quickSwitchVisible") is True
|
and report.get("resolvedQuickSwitchTheme") == expected_theme
|
and root.get("themeResolved") == expected_theme
|
and (not expected_mouse_command or root.get("lastMouseCommand") == expected_mouse_command)
|
):
|
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"theme report did not reach {expected_theme} within {timeout:.1f}s", file=sys.stderr)
|
sys.exit(1)
|
PY
|
}
|
|
assert_theme_report() {
|
local report="$1"
|
local expected_theme="$2"
|
local expected_next_theme="$3"
|
local expected_mouse_command="${4:-}"
|
|
/usr/bin/python3 - "$report" "$expected_theme" "$expected_next_theme" "$expected_mouse_command" <<'PY'
|
import json
|
import sys
|
|
path = sys.argv[1]
|
expected_theme = sys.argv[2]
|
expected_next_theme = sys.argv[3]
|
expected_mouse_command = sys.argv[4]
|
|
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", {})
|
theme_frame = root.get("themeToggleButtonFrame", {})
|
view_frame = root.get("viewModeToggleButtonFrame", {})
|
bounds = root.get("bounds", {})
|
gradient_colors = root.get("darkBackdropBackgroundGradientColors", [])
|
|
require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true")
|
require(report.get("quickSwitchVisible") is True, "Quick Switch must remain visible during theme QA")
|
require(report.get("resolvedQuickSwitchTheme") == expected_theme, "app-level resolved Quick Switch theme must match")
|
require(root.get("themeCurrentPreference") == expected_theme, "root theme preference must match expected theme")
|
require(root.get("themeResolved") == expected_theme, "root resolved theme must match expected theme")
|
require(root.get("themeNextPreference") == expected_next_theme, "theme next preference must point to the opposite theme")
|
require(root.get("themeToggleButtonVisible") is True, "theme toggle button must be visible")
|
require(root.get("themeToggleIconKind") == "halfDisk", "theme toggle must use the approved half-disk icon")
|
require(root.get("themeToggleHitSize") == 34, "theme toggle must keep a 34pt hit target")
|
require(theme_frame.get("width") == 30 and theme_frame.get("height") == 30, "theme toggle visual size must be 30pt")
|
require(view_frame.get("width") == 30 and view_frame.get("height") == 30, "view toggle visual size must remain 30pt")
|
require(theme_frame.get("y") == view_frame.get("y"), "theme toggle must align vertically with view toggle")
|
require(theme_frame.get("x", 0) < view_frame.get("x", 0), "theme toggle must sit to the left of the view-mode toggle")
|
require(view_frame.get("x", 0) + view_frame.get("width", 0) > bounds.get("width", 0) * 0.90, "top-right toggle cluster must remain near the right edge")
|
if expected_mouse_command:
|
require(root.get("lastMouseCommand") == expected_mouse_command, "debug mouse command must be recorded")
|
|
if expected_theme == "dark":
|
require(root.get("darkBackdropLayersVisible") is True, "dark backdrop ambient layers must be visible in dark theme")
|
require(root.get("darkBackdropLayerCount") == 4, "dark theme must expose three ambient layers plus vignette")
|
require(len(gradient_colors) >= 4, "dark background must expose a multi-stop gradient")
|
reds = {round(color.get("red", 0), 3) for color in gradient_colors}
|
greens = {round(color.get("green", 0), 3) for color in gradient_colors}
|
blues = {round(color.get("blue", 0), 3) for color in gradient_colors}
|
require(len(reds) > 1 and len(greens) > 1 and len(blues) > 1, "dark gradient must not collapse to a single color")
|
first = gradient_colors[0]
|
require(first.get("blue", 0) > first.get("red", 1), "dark gradient must start in a blue-family tone")
|
else:
|
require(root.get("darkBackdropLayersVisible") is False, "dark backdrop layers must be hidden in light theme")
|
require(len(gradient_colors) == 2, "light background should keep the existing two-stop baseline gradient")
|
|
print(json.dumps({
|
"theme": expected_theme,
|
"nextTheme": root.get("themeNextPreference"),
|
"themeToggleButtonFrame": theme_frame,
|
"viewModeToggleButtonFrame": view_frame,
|
"darkBackdropLayerCount": root.get("darkBackdropLayerCount"),
|
"darkBackdropLayersVisible": root.get("darkBackdropLayersVisible")
|
}, indent=2, ensure_ascii=False))
|
PY
|
}
|
|
run_fixture() {
|
local report="$1"
|
shift
|
stop_current_aligner
|
rm -f "$report"
|
"$APP/Contents/MacOS/Aligner" \
|
--round0-skip-permissions \
|
--round01-open-quick-switch \
|
--round01-fixture-app-count=4 \
|
--round01-fixture-windows-per-app=2 \
|
--round01-disable-screenshot-refresh \
|
--round01-quick-switch-report="$report" \
|
--round01-quick-switch-auto-hide-after=1.2 \
|
--round01-quick-switch-quit-after=1.8 \
|
"$@" >/dev/null 2>&1 &
|
}
|
|
cleanup() {
|
stop_current_aligner || true
|
restore_user_theme
|
}
|
|
preserve_user_theme
|
trap cleanup EXIT
|
stop_current_aligner
|
|
/usr/bin/defaults delete "$DOMAIN" "$THEME_KEY" >/dev/null 2>&1 || true
|
run_fixture "$REPORT_DEFAULT"
|
wait_for_theme_report "$REPORT_DEFAULT" "light"
|
assert_theme_report "$REPORT_DEFAULT" "light" "dark"
|
|
run_fixture "$REPORT_DARK" --round01-debug-mouse-sequence="click-theme-toggle"
|
wait_for_theme_report "$REPORT_DARK" "dark" "click-theme-toggle"
|
assert_theme_report "$REPORT_DARK" "dark" "light" "click-theme-toggle"
|
|
run_fixture "$REPORT_DARK_REOPEN"
|
wait_for_theme_report "$REPORT_DARK_REOPEN" "dark"
|
assert_theme_report "$REPORT_DARK_REOPEN" "dark" "light"
|
|
run_fixture "$REPORT_LIGHT" --round01-debug-mouse-sequence="click-theme-toggle"
|
wait_for_theme_report "$REPORT_LIGHT" "light" "click-theme-toggle"
|
assert_theme_report "$REPORT_LIGHT" "light" "dark" "click-theme-toggle"
|
|
stop_current_aligner
|
restore_user_theme
|
trap - EXIT
|
|
cat <<EOF
|
Round01.1 theme toggle QA passed
|
Reports:
|
- $REPORT_DEFAULT
|
- $REPORT_DARK
|
- $REPORT_DARK_REOPEN
|
- $REPORT_LIGHT
|
EOF
|