#!/bin/bash
|
# Round01 window index progressive filter fixture QA. It verifies that the
|
# first Option window-index key dims only non-target Waterfall cards, and that
|
# the second key commits immediately while preserving the pre-commit snapshot.
|
|
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_WINDOW_INDEX_FILTER_REPORT_WAIT:-8.0}"
|
FIXTURE_APP_COUNT="${ALIGNER_ROUND1_WINDOW_INDEX_FILTER_APP_COUNT:-8}"
|
FIXTURE_WINDOWS_PER_APP="${ALIGNER_ROUND1_WINDOW_INDEX_FILTER_WINDOWS_PER_APP:-6}"
|
TARGET_APP_INDEX="${ALIGNER_ROUND1_WINDOW_INDEX_FILTER_TARGET_APP_INDEX:-2}"
|
TARGET_WINDOW_INDEX="${ALIGNER_ROUND1_WINDOW_INDEX_FILTER_TARGET_WINDOW_INDEX:-5}"
|
APP_SYMBOL="${ALIGNER_ROUND1_WINDOW_INDEX_FILTER_APP_SYMBOL:-3}"
|
WINDOW_SYMBOL="${ALIGNER_ROUND1_WINDOW_INDEX_FILTER_WINDOW_SYMBOL:-6}"
|
TARGET_CODE="${APP_SYMBOL}${WINDOW_SYMBOL}"
|
|
REPORT_HORIZONTAL_LIGHT="$BUILD_REPORT_ROOT/round01-window-index-progressive-filter-horizontal-light-report.json"
|
REPORT_VERTICAL_LIGHT="$BUILD_REPORT_ROOT/round01-window-index-progressive-filter-vertical-light-report.json"
|
REPORT_HORIZONTAL_DARK="$BUILD_REPORT_ROOT/round01-window-index-progressive-filter-horizontal-dark-report.json"
|
REPORT_COMMIT="$BUILD_REPORT_ROOT/round01-window-index-progressive-filter-commit-report.json"
|
REPORT_INVALID="$BUILD_REPORT_ROOT/round01-window-index-progressive-filter-invalid-report.json"
|
REPORT_SINGLE_WINDOW="$BUILD_REPORT_ROOT/round01-window-index-progressive-filter-single-window-report.json"
|
SUMMARY_DIR="$BUILD_TMP_ROOT/round01-window-index-progressive-filter-fixture-qa"
|
|
APP_PID=""
|
OLD_THEME_SET=0
|
OLD_THEME=""
|
SUMMARY_FILES=()
|
|
fail() {
|
echo "Round01 window index progressive filter fixture QA failed: $*" >&2
|
exit 1
|
}
|
|
aligner_pids_for_current_app() {
|
ps -axo pid=,args= | while read -r pid command; do
|
if [[ "$command" == "$APP/Contents/MacOS/Aligner"* ]] \
|
|| [[ "$command" == "/Applications/Aligner.app/Contents/MacOS/Aligner"* ]] \
|
|| [[ "$command" == "$HOME/Applications/Aligner.app/Contents/MacOS/Aligner"* ]]; then
|
echo "$pid"
|
fi
|
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"
|
}
|
|
stop_current_aligner_safely() {
|
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
|
|
echo "warning: current Aligner app did not exit during cleanup" >&2
|
}
|
|
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
|
}
|
|
cleanup() {
|
if [ -n "${APP_PID:-}" ]; then
|
kill "$APP_PID" 2>/dev/null || true
|
wait "$APP_PID" 2>/dev/null || true
|
APP_PID=""
|
fi
|
stop_current_aligner_safely
|
restore_user_theme
|
}
|
|
set_theme() {
|
local theme="$1"
|
/usr/bin/defaults write "$DOMAIN" "$THEME_KEY" -string "$theme"
|
}
|
|
mode_report_value() {
|
local mode="$1"
|
case "$mode" in
|
horizontal) echo "horizontalMasonry" ;;
|
vertical) echo "verticalColumns" ;;
|
*) fail "unsupported waterfall mode $mode" ;;
|
esac
|
}
|
|
pending_key_sequence() {
|
echo "physical-index:$APP_SYMBOL"
|
}
|
|
commit_key_sequence() {
|
echo "physical-index:$APP_SYMBOL,physical-index:$WINDOW_SYMBOL"
|
}
|
|
pending_expected_sequence() {
|
echo "app:$APP_SYMBOL"
|
}
|
|
commit_expected_sequence() {
|
echo "app:$APP_SYMBOL,window:$TARGET_CODE"
|
}
|
|
invalid_key_sequence() {
|
echo "physical-index:$APP_SYMBOL,physical-index:Z"
|
}
|
|
invalid_expected_sequence() {
|
echo "app:$APP_SYMBOL,windowInvalid:${APP_SYMBOL}Z"
|
}
|
|
single_window_expected_sequence() {
|
echo "app:$APP_SYMBOL,windowSingle:$APP_SYMBOL"
|
}
|
|
run_fixture() {
|
local report="$1"
|
local theme="$2"
|
local mode="$3"
|
local key_sequence="$4"
|
local windows_per_app="${5:-$FIXTURE_WINDOWS_PER_APP}"
|
|
stop_current_aligner
|
set_theme "$theme"
|
rm -f "$report"
|
|
"$APP/Contents/MacOS/Aligner" \
|
--round0-skip-permissions \
|
--round01-open-quick-switch \
|
--round01-fixture-app-count="$FIXTURE_APP_COUNT" \
|
--round01-fixture-windows-per-app="$windows_per_app" \
|
--round01-disable-screenshot-refresh \
|
--round01-waterfall-view-mode="$mode" \
|
--round01-debug-key-sequence="$key_sequence" \
|
--round01-quick-switch-report="$report" >/dev/null 2>&1 &
|
|
APP_PID=$!
|
}
|
|
finish_case() {
|
if [ -n "${APP_PID:-}" ]; then
|
kill "$APP_PID" 2>/dev/null || true
|
wait "$APP_PID" 2>/dev/null || true
|
APP_PID=""
|
fi
|
stop_current_aligner
|
}
|
|
wait_for_pending_report() {
|
local report="$1"
|
local expected_mode="$2"
|
local expected_sequence="$3"
|
local case_name="$4"
|
|
/usr/bin/python3 - "$report" "$REPORT_WAIT" "$expected_mode" "$expected_sequence" "$case_name" <<'PY'
|
import json
|
import sys
|
import time
|
|
path = sys.argv[1]
|
timeout = float(sys.argv[2])
|
expected_mode = sys.argv[3]
|
expected_sequence = [part for part in sys.argv[4].split(",") if part]
|
case_name = sys.argv[5]
|
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 root.get("waterfallViewMode") == expected_mode
|
and root.get("keyboardCommandsApplied") == expected_sequence
|
and root.get("windowShortcutFilterPhase") == "appPending"
|
):
|
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"{case_name}: pending filter report did not reach appPending within {timeout:.1f}s", file=sys.stderr)
|
sys.exit(1)
|
PY
|
}
|
|
wait_for_commit_report() {
|
local report="$1"
|
local expected_mode="$2"
|
local expected_sequence="$3"
|
|
/usr/bin/python3 - "$report" "$REPORT_WAIT" "$expected_mode" "$expected_sequence" "$TARGET_CODE" <<'PY'
|
import json
|
import sys
|
import time
|
|
path = sys.argv[1]
|
timeout = float(sys.argv[2])
|
expected_mode = sys.argv[3]
|
expected_sequence = [part for part in sys.argv[4].split(",") if part]
|
target_code = sys.argv[5]
|
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", {})
|
snapshot = root.get("lastWindowShortcutPreCommitFilterSnapshot")
|
if (
|
report.get("snapshotLoaded") is True
|
and root.get("waterfallViewMode") == expected_mode
|
and root.get("keyboardCommandsApplied") == expected_sequence
|
and root.get("lastWindowIndexKeyCommitCode") == target_code
|
and isinstance(snapshot, dict)
|
):
|
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"commit report did not reach pre-commit snapshot within {timeout:.1f}s", file=sys.stderr)
|
sys.exit(1)
|
PY
|
}
|
|
wait_for_invalid_report() {
|
local report="$1"
|
local expected_mode="$2"
|
local expected_sequence="$3"
|
|
/usr/bin/python3 - "$report" "$REPORT_WAIT" "$expected_mode" "$expected_sequence" "${APP_SYMBOL}Z" <<'PY'
|
import json
|
import sys
|
import time
|
|
path = sys.argv[1]
|
timeout = float(sys.argv[2])
|
expected_mode = sys.argv[3]
|
expected_sequence = [part for part in sys.argv[4].split(",") if part]
|
invalid_code = sys.argv[5]
|
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 root.get("waterfallViewMode") == expected_mode
|
and root.get("keyboardCommandsApplied") == expected_sequence
|
and root.get("lastWindowIndexKeyCommand") == f"windowInvalid:{invalid_code}"
|
and root.get("windowShortcutFilterPhase") == "invalid"
|
):
|
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"invalid report did not reach invalid state within {timeout:.1f}s", file=sys.stderr)
|
sys.exit(1)
|
PY
|
}
|
|
wait_for_single_window_report() {
|
local report="$1"
|
local expected_mode="$2"
|
local expected_sequence="$3"
|
|
/usr/bin/python3 - "$report" "$REPORT_WAIT" "$expected_mode" "$expected_sequence" "$APP_SYMBOL" <<'PY'
|
import json
|
import sys
|
import time
|
|
path = sys.argv[1]
|
timeout = float(sys.argv[2])
|
expected_mode = sys.argv[3]
|
expected_sequence = [part for part in sys.argv[4].split(",") if part]
|
app_symbol = sys.argv[5]
|
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 root.get("waterfallViewMode") == expected_mode
|
and root.get("keyboardCommandsApplied") == expected_sequence
|
and root.get("lastWindowIndexKeyCommand") == f"windowSingle:{app_symbol}"
|
and root.get("lastCommitSource") == "keyboard"
|
):
|
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"single-window report did not reach direct commit within {timeout:.1f}s", file=sys.stderr)
|
sys.exit(1)
|
PY
|
}
|
|
assert_pending_report() {
|
local report="$1"
|
local case_name="$2"
|
local theme="$3"
|
local expected_mode="$4"
|
local summary_file="$SUMMARY_DIR/$case_name.json"
|
|
/usr/bin/python3 - \
|
"$report" \
|
"$case_name" \
|
"$theme" \
|
"$expected_mode" \
|
"$(pending_expected_sequence)" \
|
"$FIXTURE_APP_COUNT" \
|
"$FIXTURE_WINDOWS_PER_APP" \
|
"$TARGET_APP_INDEX" \
|
"$APP_SYMBOL" \
|
"$summary_file" <<'PY'
|
import json
|
import numbers
|
import sys
|
|
path = sys.argv[1]
|
case_name = sys.argv[2]
|
theme = sys.argv[3]
|
expected_mode = sys.argv[4]
|
expected_sequence = [part for part in sys.argv[5].split(",") if part]
|
fixture_app_count = int(sys.argv[6])
|
fixture_windows_per_app = int(sys.argv[7])
|
target_app_index = int(sys.argv[8])
|
app_symbol = sys.argv[9]
|
summary_file = sys.argv[10]
|
|
with open(path, "r", encoding="utf-8") as file:
|
report = json.load(file)
|
|
def require(condition, message):
|
if not condition:
|
print(f"{case_name}: {message}", file=sys.stderr)
|
print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr)
|
sys.exit(1)
|
|
def sorted_ints(values):
|
require(isinstance(values, list), "window ID field must be a list")
|
return sorted(int(value) for value in values)
|
|
def is_number(value):
|
return isinstance(value, numbers.Real) and not isinstance(value, bool)
|
|
root = report.get("rootView", {})
|
columns = root.get("waterfallColumns", [])
|
cards = [card for column in columns for card in column.get("cards", [])]
|
target_column = next((column for column in columns if column.get("appGroupIndex") == target_app_index), None)
|
|
require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true")
|
require(report.get("quickSwitchVisible") is True, "pending filter must keep Quick Switch visible")
|
require(report.get("appCount") == fixture_app_count, "fixture appCount must match")
|
require(report.get("windowCount") == fixture_app_count * fixture_windows_per_app, "fixture windowCount must match")
|
require(root.get("waterfallViewMode") == expected_mode, "Waterfall view mode must match case")
|
require(root.get("keyboardCommandsApplied") == expected_sequence, "pending key sequence must be applied")
|
require(root.get("appShelfIndexKeyDownCount") == 1, "pending filter must exercise one physical keyDown")
|
require(root.get("lastAppShelfIndexKeySymbol") == app_symbol, "pending physical symbol must be reported")
|
require(root.get("lastCommitSource") is None, "pending filter must not commit")
|
require(root.get("lastCommittedWindowID") is None, "pending filter must not commit a window")
|
require(root.get("lastWindowIndexKeyCommitCode") is None, "pending filter must not expose a commit code")
|
|
require(root.get("windowShortcutFilterActive") is True, "windowShortcutFilterActive must be true")
|
require(root.get("windowShortcutFilterPhase") == "appPending", "filter phase must be appPending")
|
require(root.get("windowShortcutFilterPendingAppGroupIndex") == target_app_index, "pending App group must match first key")
|
require(root.get("windowShortcutFilterPrefix") == app_symbol, "filter prefix must match first physical key")
|
require(root.get("lastWindowShortcutPreCommitFilterSnapshot") is None, "pending run must not expose a pre-commit snapshot")
|
|
require(target_column is not None, "target App column must exist")
|
target_cards = target_column.get("cards", [])
|
target_ids = sorted(int(card.get("windowID")) for card in target_cards)
|
dimmed_cards = [card for card in cards if int(card.get("windowID")) not in set(target_ids)]
|
dimmed_ids = sorted(int(card.get("windowID")) for card in dimmed_cards)
|
|
require(len(target_cards) == fixture_windows_per_app, "target App must keep all fixture windows")
|
require(root.get("windowShortcutFilterMatchedCount") == len(target_ids), "matched count must equal target App window count")
|
require(root.get("windowShortcutFilterDimmedCount") == len(dimmed_ids), "dimmed count must equal non-target Waterfall cards")
|
require(sorted_ints(root.get("windowShortcutFilterMatchedWindowIDs")) == target_ids, "matched window IDs must be exactly target App windows")
|
require(sorted_ints(root.get("windowShortcutFilterDimmedWindowIDs")) == dimmed_ids, "dimmed window IDs must be exactly non-target Waterfall windows")
|
|
matched_opacities = []
|
dimmed_opacities = []
|
for card in target_cards:
|
states = card.get("visualStates", [])
|
opacity = card.get("windowShortcutFilterOpacity")
|
require(card.get("windowShortcutFilterState") == "matched", "target App cards must report matched filter state")
|
require(is_number(opacity), "matched cards must expose numeric filter opacity")
|
require(0 <= opacity <= 1.01, "matched card opacity must be normalized")
|
require("shortcutMatched" in states, "matched cards must expose shortcutMatched visual state")
|
require("shortcutDimmed" not in states, "matched cards must not expose shortcutDimmed")
|
require("shortcutTarget" not in states, "pending matched cards must not expose shortcutTarget")
|
matched_opacities.append(float(opacity))
|
|
for card in dimmed_cards:
|
states = card.get("visualStates", [])
|
opacity = card.get("windowShortcutFilterOpacity")
|
require(card.get("windowShortcutFilterState") == "dimmed", "non-target Waterfall cards must report dimmed filter state")
|
require(is_number(opacity), "dimmed cards must expose numeric filter opacity")
|
require(0 <= opacity <= 1.01, "dimmed card opacity must be normalized")
|
require("shortcutDimmed" in states, "dimmed cards must expose shortcutDimmed visual state")
|
require("shortcutMatched" not in states, "dimmed cards must not expose shortcutMatched")
|
require("shortcutTarget" not in states, "pending dimmed cards must not expose shortcutTarget")
|
dimmed_opacities.append(float(opacity))
|
|
require(matched_opacities, "fixture must have matched cards")
|
require(dimmed_opacities, "fixture must have dimmed cards")
|
require(min(matched_opacities) > max(dimmed_opacities), "matched cards must stay visually brighter than dimmed cards")
|
|
for node in root.get("appShelfItems", []) + root.get("spaceLaneSegments", []):
|
states = node.get("visualStates", [])
|
require("shortcutDimmed" not in states, "shortcut dim visual state must not leak to Space Lane or App Shelf")
|
if "windowShortcutFilterState" in node:
|
require(node.get("windowShortcutFilterState") != "dimmed", "Space Lane/App Shelf must not report dimmed filter state")
|
if "windowShortcutFilterOpacity" in node:
|
opacity = node.get("windowShortcutFilterOpacity")
|
require(is_number(opacity), "Space Lane/App Shelf filter opacity must be numeric if present")
|
require(opacity >= 0.99, "Space Lane/App Shelf must not be dimmed by window shortcut filter")
|
|
summary = {
|
"case": case_name,
|
"status": "passed",
|
"theme": theme,
|
"mode": root.get("waterfallViewMode"),
|
"report": path,
|
"phase": root.get("windowShortcutFilterPhase"),
|
"prefix": root.get("windowShortcutFilterPrefix"),
|
"pendingAppGroupIndex": root.get("windowShortcutFilterPendingAppGroupIndex"),
|
"matchedCount": root.get("windowShortcutFilterMatchedCount"),
|
"dimmedCount": root.get("windowShortcutFilterDimmedCount"),
|
"matchedWindowIDs": target_ids,
|
"dimmedWindowIDs": dimmed_ids,
|
"matchedOpacityMin": min(matched_opacities),
|
"dimmedOpacityMax": max(dimmed_opacities),
|
"quickSwitchVisible": report.get("quickSwitchVisible"),
|
"commitSource": root.get("lastCommitSource"),
|
}
|
with open(summary_file, "w", encoding="utf-8") as file:
|
json.dump(summary, file, indent=2, ensure_ascii=False)
|
PY
|
|
SUMMARY_FILES+=("$summary_file")
|
}
|
|
assert_commit_report() {
|
local report="$1"
|
local case_name="valid-commit-horizontal"
|
local summary_file="$SUMMARY_DIR/$case_name.json"
|
|
/usr/bin/python3 - \
|
"$report" \
|
"$case_name" \
|
"light" \
|
"horizontalMasonry" \
|
"$(commit_expected_sequence)" \
|
"$FIXTURE_APP_COUNT" \
|
"$FIXTURE_WINDOWS_PER_APP" \
|
"$TARGET_APP_INDEX" \
|
"$TARGET_WINDOW_INDEX" \
|
"$APP_SYMBOL" \
|
"$WINDOW_SYMBOL" \
|
"$TARGET_CODE" \
|
"$summary_file" <<'PY'
|
import json
|
import numbers
|
import sys
|
|
path = sys.argv[1]
|
case_name = sys.argv[2]
|
theme = sys.argv[3]
|
expected_mode = sys.argv[4]
|
expected_sequence = [part for part in sys.argv[5].split(",") if part]
|
fixture_app_count = int(sys.argv[6])
|
fixture_windows_per_app = int(sys.argv[7])
|
target_app_index = int(sys.argv[8])
|
target_window_index = int(sys.argv[9])
|
app_symbol = sys.argv[10]
|
window_symbol = sys.argv[11]
|
target_code = sys.argv[12]
|
summary_file = sys.argv[13]
|
|
with open(path, "r", encoding="utf-8") as file:
|
report = json.load(file)
|
|
def require(condition, message):
|
if not condition:
|
print(f"{case_name}: {message}", file=sys.stderr)
|
print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr)
|
sys.exit(1)
|
|
def sorted_ints(values):
|
require(isinstance(values, list), "window ID field must be a list")
|
return sorted(int(value) for value in values)
|
|
def is_number(value):
|
return isinstance(value, numbers.Real) and not isinstance(value, bool)
|
|
root = report.get("rootView", {})
|
columns = root.get("waterfallColumns", [])
|
cards = [card for column in columns for card in column.get("cards", [])]
|
all_window_ids = sorted(int(card.get("windowID")) for card in cards)
|
committed_window_id = root.get("lastCommittedWindowID")
|
require(isinstance(committed_window_id, int), "commit must report a committed window ID")
|
target_window_id = int(committed_window_id)
|
|
require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true")
|
require(report.get("appCount") == fixture_app_count, "multi-window fixture must keep target App visible after suppressing one window")
|
require(report.get("windowCount") == fixture_app_count * fixture_windows_per_app - 1, "commit run must suppress the activated target window from visible cards")
|
require(root.get("waterfallViewMode") == expected_mode, "commit run must use horizontal Waterfall")
|
require(root.get("keyboardCommandsApplied") == expected_sequence, "commit key sequence must be applied")
|
require(root.get("appShelfIndexKeyDownCount") == 2, "commit must exercise two physical keyDown events")
|
require(root.get("lastAppShelfIndexKeySymbol") == window_symbol, "last physical symbol must be the window key")
|
require(root.get("lastWindowIndexKeyCommand") == f"window:{target_code}", "last window shortcut command must be reported")
|
require(root.get("lastWindowIndexKeyCommitCode") == target_code, "last window shortcut commit code must be reported")
|
require(root.get("windowIndexKeyPendingAppGroupIndex") is None, "commit must clear legacy pending App group")
|
require(root.get("windowIndexKeyPendingAppSymbol") is None, "commit must clear legacy pending App symbol")
|
require(root.get("lastCommitSource") == "keyboard", "valid shortcut commit must be keyboard sourced")
|
|
require(root.get("lastCommittedAppGroupIndex") == target_app_index, "commit must target expected App")
|
require(root.get("lastCommittedWindowIndex") == target_window_index, "commit must target expected window index")
|
require(root.get("lastCommittedWindowID") == target_window_id, "commit must target expected window ID")
|
require(report.get("suppressedActivationWindowIDs") == [target_window_id], "commit must suppress the activated target window")
|
require(report.get("suppressedActivationWindowCount") == 1, "commit must report one suppressed activation window")
|
require(target_window_id not in all_window_ids, "committed target window must not remain in visible cards after activation")
|
|
snapshot = root.get("lastWindowShortcutPreCommitFilterSnapshot")
|
require(isinstance(snapshot, dict), "pre-commit filter snapshot must be an object")
|
require(snapshot.get("phase") == "targetPreCommit", "pre-commit snapshot phase must be targetPreCommit")
|
require(snapshot.get("targetCode") == target_code, "pre-commit snapshot targetCode must match committed shortcut")
|
require(int(snapshot.get("targetWindowID")) == target_window_id, "pre-commit snapshot targetWindowID must match committed window")
|
require(sorted_ints(snapshot.get("matchedWindowIDs")) == [target_window_id], "pre-commit snapshot must match only the target window")
|
require(sorted_ints(snapshot.get("dimmedWindowIDs")) == all_window_ids, "pre-commit snapshot must dim all non-target visible Waterfall windows")
|
require(root.get("windowShortcutFilterActive") is True, "commit must preserve target pre-commit filter state for QA evidence")
|
require(root.get("windowShortcutFilterPhase") == "targetPreCommit", "commit must preserve target pre-commit filter phase")
|
require(root.get("windowShortcutFilterPrefix") == target_code, "commit filter prefix must match target code")
|
require(sorted_ints(root.get("windowShortcutFilterMatchedWindowIDs")) == [target_window_id], "commit filter must keep target as matched evidence")
|
require(sorted_ints(root.get("windowShortcutFilterDimmedWindowIDs")) == all_window_ids, "commit filter must dim all non-target visible windows")
|
require(root.get("windowShortcutFilterMatchedCount") == 1, "commit filter must report one matched window")
|
require(root.get("windowShortcutFilterDimmedCount") == len(all_window_ids), "commit filter dimmed count must match visible non-target windows")
|
|
for card in cards:
|
card_states = card.get("visualStates", [])
|
card_opacity = card.get("windowShortcutFilterOpacity")
|
require(card.get("windowShortcutFilterState") == "dimmed", "visible non-target cards must remain dimmed in target pre-commit state")
|
require(is_number(card_opacity), "non-target cards must expose numeric filter opacity")
|
require(0 <= float(card_opacity) <= 1.01, "non-target card opacity must be normalized")
|
require("shortcutDimmed" in card_states, "visible non-target cards must expose shortcutDimmed in target pre-commit state")
|
require("shortcutTarget" not in card_states, "non-target cards must not expose shortcutTarget")
|
|
for node in root.get("appShelfItems", []) + root.get("spaceLaneSegments", []):
|
states = node.get("visualStates", [])
|
require("shortcutDimmed" not in states, "shortcut dim visual state must not leak to Space Lane or App Shelf during commit")
|
if "windowShortcutFilterState" in node:
|
require(node.get("windowShortcutFilterState") != "dimmed", "Space Lane/App Shelf must not report dimmed filter state during commit")
|
|
summary = {
|
"case": case_name,
|
"status": "passed",
|
"theme": theme,
|
"mode": root.get("waterfallViewMode"),
|
"report": path,
|
"commands": root.get("keyboardCommandsApplied"),
|
"targetCode": target_code,
|
"targetWindowID": target_window_id,
|
"matchedWindowIDs": snapshot.get("matchedWindowIDs"),
|
"dimmedCount": len(snapshot.get("dimmedWindowIDs", [])),
|
"lastCommitSource": root.get("lastCommitSource"),
|
"lastWindowIndexKeyCommitCode": root.get("lastWindowIndexKeyCommitCode"),
|
"suppressedActivationWindowIDs": report.get("suppressedActivationWindowIDs"),
|
}
|
with open(summary_file, "w", encoding="utf-8") as file:
|
json.dump(summary, file, indent=2, ensure_ascii=False)
|
PY
|
|
SUMMARY_FILES+=("$summary_file")
|
}
|
|
assert_invalid_report() {
|
local report="$1"
|
local case_name="invalid-second-key-horizontal"
|
local summary_file="$SUMMARY_DIR/$case_name.json"
|
|
/usr/bin/python3 - \
|
"$report" \
|
"$case_name" \
|
"horizontalMasonry" \
|
"$(invalid_expected_sequence)" \
|
"$APP_SYMBOL" \
|
"${APP_SYMBOL}Z" \
|
"$summary_file" <<'PY'
|
import json
|
import numbers
|
import sys
|
|
path = sys.argv[1]
|
case_name = sys.argv[2]
|
expected_mode = sys.argv[3]
|
expected_sequence = [part for part in sys.argv[4].split(",") if part]
|
app_symbol = sys.argv[5]
|
invalid_code = sys.argv[6]
|
summary_file = sys.argv[7]
|
|
with open(path, "r", encoding="utf-8") as file:
|
report = json.load(file)
|
|
def require(condition, message):
|
if not condition:
|
print(f"{case_name}: {message}", file=sys.stderr)
|
print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr)
|
sys.exit(1)
|
|
def is_number(value):
|
return isinstance(value, numbers.Real) and not isinstance(value, bool)
|
|
root = report.get("rootView", {})
|
columns = root.get("waterfallColumns", [])
|
cards = [card for column in columns for card in column.get("cards", [])]
|
|
require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true")
|
require(report.get("quickSwitchVisible") is True, "invalid shortcut must keep Quick Switch visible")
|
require(root.get("waterfallViewMode") == expected_mode, "invalid run must use horizontal Waterfall")
|
require(root.get("keyboardCommandsApplied") == expected_sequence, "invalid key sequence must be applied")
|
require(root.get("appShelfIndexKeyDownCount") == 2, "invalid shortcut must exercise two physical keyDown events")
|
require(root.get("lastAppShelfIndexKeySymbol") == "Z", "last physical symbol must be invalid window key")
|
require(root.get("lastWindowIndexKeyCommand") == f"windowInvalid:{invalid_code}", "invalid command must be reported")
|
require(root.get("lastWindowIndexKeyCommitCode") is None, "invalid shortcut must not expose commit code")
|
require(root.get("lastCommitSource") is None, "invalid shortcut must not commit")
|
require(root.get("lastCommittedWindowID") is None, "invalid shortcut must not commit a window")
|
require(root.get("windowIndexKeyPendingAppGroupIndex") is None, "invalid shortcut must clear legacy pending App group")
|
require(root.get("windowIndexKeyPendingAppSymbol") is None, "invalid shortcut must clear legacy pending App symbol")
|
require(root.get("windowShortcutFilterActive") is False, "invalid shortcut must not leave active filter")
|
require(root.get("windowShortcutFilterPhase") == "invalid", "invalid shortcut must report invalid phase")
|
require(root.get("windowShortcutFilterPendingAppGroupIndex") is None, "invalid shortcut must clear pending App group")
|
require(root.get("windowShortcutFilterPrefix") is None, "invalid shortcut must clear filter prefix")
|
require(root.get("windowShortcutFilterMatchedCount") == 0, "invalid shortcut must clear matched count")
|
require(root.get("windowShortcutFilterDimmedCount") == 0, "invalid shortcut must clear dimmed count")
|
require(root.get("windowShortcutFilterMatchedWindowIDs") == [], "invalid shortcut must clear matched IDs")
|
require(root.get("windowShortcutFilterDimmedWindowIDs") == [], "invalid shortcut must clear dimmed IDs")
|
require(root.get("lastWindowShortcutPreCommitFilterSnapshot") is None, "invalid shortcut must not expose pre-commit snapshot")
|
|
for card in cards:
|
opacity = card.get("windowShortcutFilterOpacity")
|
states = card.get("visualStates", [])
|
require(card.get("windowShortcutFilterState") == "none", "invalid shortcut must restore card filter state")
|
require(is_number(opacity), "cards must expose numeric filter opacity")
|
require(float(opacity) >= 0.99, "invalid shortcut must restore card opacity")
|
require("shortcutDimmed" not in states, "invalid shortcut must not leave shortcutDimmed state")
|
require("shortcutMatched" not in states, "invalid shortcut must not leave shortcutMatched state")
|
require("shortcutTarget" not in states, "invalid shortcut must not leave shortcutTarget state")
|
|
summary = {
|
"case": case_name,
|
"status": "passed",
|
"mode": root.get("waterfallViewMode"),
|
"report": path,
|
"commands": root.get("keyboardCommandsApplied"),
|
"firstSymbol": app_symbol,
|
"invalidCode": invalid_code,
|
"phase": root.get("windowShortcutFilterPhase"),
|
"quickSwitchVisible": report.get("quickSwitchVisible"),
|
"cardCount": len(cards)
|
}
|
with open(summary_file, "w", encoding="utf-8") as file:
|
json.dump(summary, file, indent=2, ensure_ascii=False)
|
PY
|
|
SUMMARY_FILES+=("$summary_file")
|
}
|
|
assert_single_window_report() {
|
local report="$1"
|
local case_name="single-window-first-key-commit-horizontal"
|
local summary_file="$SUMMARY_DIR/$case_name.json"
|
|
/usr/bin/python3 - \
|
"$report" \
|
"$case_name" \
|
"horizontalMasonry" \
|
"$(single_window_expected_sequence)" \
|
"$FIXTURE_APP_COUNT" \
|
"$TARGET_APP_INDEX" \
|
"$APP_SYMBOL" \
|
"$summary_file" <<'PY'
|
import json
|
import numbers
|
import sys
|
|
path = sys.argv[1]
|
case_name = sys.argv[2]
|
expected_mode = sys.argv[3]
|
expected_sequence = [part for part in sys.argv[4].split(",") if part]
|
fixture_app_count = int(sys.argv[5])
|
target_app_index = int(sys.argv[6])
|
app_symbol = sys.argv[7]
|
summary_file = sys.argv[8]
|
|
with open(path, "r", encoding="utf-8") as file:
|
report = json.load(file)
|
|
def require(condition, message):
|
if not condition:
|
print(f"{case_name}: {message}", file=sys.stderr)
|
print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr)
|
sys.exit(1)
|
|
def is_number(value):
|
return isinstance(value, numbers.Real) and not isinstance(value, bool)
|
|
root = report.get("rootView", {})
|
columns = root.get("waterfallColumns", [])
|
cards = [card for column in columns for card in column.get("cards", [])]
|
all_window_ids = sorted(int(card.get("windowID")) for card in cards)
|
committed_window_id = root.get("lastCommittedWindowID")
|
require(isinstance(committed_window_id, int), "direct commit must report a committed window ID")
|
target_window_id = int(committed_window_id)
|
|
require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true")
|
require(report.get("appCount") == fixture_app_count - 1, "single-window fixture must hide the committed target App after activation")
|
require(report.get("windowCount") == fixture_app_count - 1, "single-window fixture must hide the committed target window after activation")
|
require(root.get("waterfallViewMode") == expected_mode, "single-window run must use horizontal Waterfall")
|
require(root.get("keyboardCommandsApplied") == expected_sequence, "single-window key sequence must direct-commit")
|
require(root.get("appShelfIndexKeyDownCount") == 1, "single-window direct commit must use one physical keyDown")
|
require(root.get("lastAppShelfIndexKeySymbol") == app_symbol, "last physical symbol must be the App key")
|
require(root.get("lastWindowIndexKeyCommand") == f"windowSingle:{app_symbol}", "single-window direct command must be reported")
|
require(root.get("lastWindowIndexKeyCommitCode") == app_symbol, "single-window commit code must be the one-key shortcut")
|
require(root.get("lastCommitSource") == "keyboard", "single-window direct commit must be keyboard sourced")
|
require(root.get("windowIndexKeyPendingAppGroupIndex") is None, "single-window direct commit must not leave pending App group")
|
require(root.get("windowIndexKeyPendingAppSymbol") is None, "single-window direct commit must not leave pending App symbol")
|
require(root.get("windowShortcutFilterActive") is False, "single-window direct commit must not enter progressive filter")
|
require(root.get("windowShortcutFilterPhase") == "none", "single-window direct commit must keep filter phase none")
|
require(root.get("windowShortcutFilterMatchedCount") == 0, "single-window direct commit must not leave matched filter count")
|
require(root.get("windowShortcutFilterDimmedCount") == 0, "single-window direct commit must not leave dimmed filter count")
|
require(root.get("lastWindowShortcutPreCommitFilterSnapshot") is None, "single-window direct commit must not expose two-key pre-commit snapshot")
|
|
require(root.get("lastCommittedAppGroupIndex") == target_app_index, "direct commit must target expected App")
|
require(root.get("lastCommittedWindowIndex") == 0, "direct commit must target the only window")
|
require(root.get("lastCommittedWindowID") == target_window_id, "direct commit must target expected window ID")
|
require(report.get("suppressedActivationWindowIDs") == [target_window_id], "direct commit must suppress the activated target window")
|
require(report.get("suppressedActivationWindowCount") == 1, "direct commit must report one suppressed activation window")
|
require(target_window_id not in all_window_ids, "direct-committed target window must not remain in visible cards after activation")
|
require(target_app_index not in [column.get("appGroupIndex") for column in columns], "single-window target App must not remain visible after activation")
|
|
for card in cards:
|
opacity = card.get("windowShortcutFilterOpacity")
|
states = card.get("visualStates", [])
|
require(card.get("windowShortcutFilterState") == "none", "single-window direct commit must not leave card filter state")
|
require(is_number(opacity), "cards must expose numeric filter opacity")
|
require(float(opacity) >= 0.99, "single-window direct commit must not dim cards")
|
require("shortcutDimmed" not in states, "single-window direct commit must not leave shortcutDimmed state")
|
require("shortcutMatched" not in states, "single-window direct commit must not leave shortcutMatched state")
|
require("shortcutTarget" not in states, "single-window direct commit must not leave shortcutTarget state")
|
|
summary = {
|
"case": case_name,
|
"status": "passed",
|
"mode": root.get("waterfallViewMode"),
|
"report": path,
|
"commands": root.get("keyboardCommandsApplied"),
|
"targetWindowID": target_window_id,
|
"lastCommitSource": root.get("lastCommitSource"),
|
"lastWindowIndexKeyCommitCode": root.get("lastWindowIndexKeyCommitCode"),
|
"suppressedActivationWindowIDs": report.get("suppressedActivationWindowIDs"),
|
"cardCount": len(cards)
|
}
|
with open(summary_file, "w", encoding="utf-8") as file:
|
json.dump(summary, file, indent=2, ensure_ascii=False)
|
PY
|
|
SUMMARY_FILES+=("$summary_file")
|
}
|
|
assert_pending_summaries_consistent() {
|
local baseline="$1"
|
local candidate="$2"
|
local label="$3"
|
|
/usr/bin/python3 - "$baseline" "$candidate" "$label" <<'PY'
|
import json
|
import sys
|
|
baseline_path = sys.argv[1]
|
candidate_path = sys.argv[2]
|
label = sys.argv[3]
|
|
with open(baseline_path, "r", encoding="utf-8") as file:
|
baseline = json.load(file)
|
with open(candidate_path, "r", encoding="utf-8") as file:
|
candidate = json.load(file)
|
|
fields = [
|
"phase",
|
"prefix",
|
"pendingAppGroupIndex",
|
"matchedCount",
|
"dimmedCount",
|
"matchedWindowIDs",
|
"dimmedWindowIDs",
|
]
|
|
for field in fields:
|
if baseline.get(field) != candidate.get(field):
|
print(f"{label}: pending field {field} differs between baseline and candidate", file=sys.stderr)
|
print(json.dumps({"baseline": baseline, "candidate": candidate}, indent=2, ensure_ascii=False), file=sys.stderr)
|
sys.exit(1)
|
PY
|
}
|
|
emit_summary() {
|
/usr/bin/python3 - "$APP" "$FIXTURE_APP_COUNT" "$FIXTURE_WINDOWS_PER_APP" "${SUMMARY_FILES[@]}" <<'PY'
|
import json
|
import sys
|
|
app = sys.argv[1]
|
fixture_app_count = int(sys.argv[2])
|
fixture_windows_per_app = int(sys.argv[3])
|
case_paths = sys.argv[4:]
|
cases = []
|
for path in case_paths:
|
with open(path, "r", encoding="utf-8") as file:
|
cases.append(json.load(file))
|
|
print(json.dumps({
|
"status": "passed",
|
"script": "round1-window-index-progressive-filter-fixture-qa.sh",
|
"app": app,
|
"fixture": {
|
"appCount": fixture_app_count,
|
"windowsPerApp": fixture_windows_per_app
|
},
|
"cases": cases,
|
"timeoutCleanup": {
|
"status": "skipped",
|
"reason": "Current --round01-debug-key-sequence has no stable wait token for the 0.9s expiry path; no sleep injection was added."
|
}
|
}, indent=2, ensure_ascii=False))
|
PY
|
}
|
|
if [ ! -x "$APP/Contents/MacOS/Aligner" ]; then
|
fail "current build app executable not found at $APP/Contents/MacOS/Aligner"
|
fi
|
if [ "$FIXTURE_APP_COUNT" -le "$TARGET_APP_INDEX" ]; then
|
fail "fixture app count must include target App index $TARGET_APP_INDEX"
|
fi
|
if [ "$FIXTURE_WINDOWS_PER_APP" -le "$TARGET_WINDOW_INDEX" ]; then
|
fail "fixture windows per App must include target window index $TARGET_WINDOW_INDEX"
|
fi
|
|
mkdir -p "$SUMMARY_DIR"
|
rm -f "$SUMMARY_DIR"/*.json
|
|
stop_current_aligner
|
"$SCRIPT_DIR/package-app.sh" >&2
|
preserve_user_theme
|
trap cleanup EXIT
|
stop_current_aligner
|
|
run_fixture "$REPORT_SINGLE_WINDOW" "light" "horizontal" "$(pending_key_sequence)" 1
|
wait_for_single_window_report "$REPORT_SINGLE_WINDOW" "$(mode_report_value horizontal)" "$(single_window_expected_sequence)"
|
assert_single_window_report "$REPORT_SINGLE_WINDOW"
|
finish_case
|
|
run_fixture "$REPORT_HORIZONTAL_LIGHT" "light" "horizontal" "$(pending_key_sequence)"
|
wait_for_pending_report "$REPORT_HORIZONTAL_LIGHT" "$(mode_report_value horizontal)" "$(pending_expected_sequence)" "pending-horizontal-light"
|
assert_pending_report "$REPORT_HORIZONTAL_LIGHT" "pending-horizontal-light" "light" "$(mode_report_value horizontal)"
|
finish_case
|
|
run_fixture "$REPORT_VERTICAL_LIGHT" "light" "vertical" "$(pending_key_sequence)"
|
wait_for_pending_report "$REPORT_VERTICAL_LIGHT" "$(mode_report_value vertical)" "$(pending_expected_sequence)" "pending-vertical-light"
|
assert_pending_report "$REPORT_VERTICAL_LIGHT" "pending-vertical-light" "light" "$(mode_report_value vertical)"
|
finish_case
|
|
run_fixture "$REPORT_HORIZONTAL_DARK" "dark" "horizontal" "$(pending_key_sequence)"
|
wait_for_pending_report "$REPORT_HORIZONTAL_DARK" "$(mode_report_value horizontal)" "$(pending_expected_sequence)" "pending-horizontal-dark"
|
assert_pending_report "$REPORT_HORIZONTAL_DARK" "pending-horizontal-dark" "dark" "$(mode_report_value horizontal)"
|
assert_pending_summaries_consistent \
|
"$SUMMARY_DIR/pending-horizontal-light.json" \
|
"$SUMMARY_DIR/pending-horizontal-dark.json" \
|
"pending-horizontal-dark"
|
finish_case
|
|
run_fixture "$REPORT_COMMIT" "light" "horizontal" "$(commit_key_sequence)"
|
wait_for_commit_report "$REPORT_COMMIT" "$(mode_report_value horizontal)" "$(commit_expected_sequence)"
|
assert_commit_report "$REPORT_COMMIT"
|
finish_case
|
|
run_fixture "$REPORT_INVALID" "light" "horizontal" "$(invalid_key_sequence)"
|
wait_for_invalid_report "$REPORT_INVALID" "$(mode_report_value horizontal)" "$(invalid_expected_sequence)"
|
assert_invalid_report "$REPORT_INVALID"
|
finish_case
|
|
cleanup
|
trap - EXIT
|
emit_summary
|