#!/bin/bash # Round01.1 horizontal Waterfall fixture QA. It verifies the P1 masonry view: # no horizontal scroll, centered equal-width fixed-height cards, 150pt vertical # placement steps, App Shelf anchoring, horizontal-mode Tab navigation, and # horizontal arrow focus across App Shelf icons plus window cards. 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" LAYOUT_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-layout-report.json" KEYBOARD_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-keyboard-report.json" APP_ENTER_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-app-enter-report.json" LETTER_INDEX_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-letter-index-report.json" ARROW_WRAP_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-arrow-wrap-report.json" FILTER_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-filter-report.json" FIXTURE_APP_COUNT="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_FIXTURE_APP_COUNT:-8}" FIXTURE_WINDOWS_PER_APP="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_FIXTURE_WINDOWS_PER_APP:-6}" HOVER_APP_INDEX="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_HOVER_APP_INDEX:-7}" KEY_SEQUENCE="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_KEY_SEQUENCE:-tab}" APP_ENTER_KEY_SEQUENCE="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_APP_ENTER_KEY_SEQUENCE:-physical-index:3,enter}" APP_ENTER_TARGET_APP_INDEX="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_APP_ENTER_TARGET_APP_INDEX:-2}" LETTER_INDEX_FIXTURE_APP_COUNT="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_LETTER_INDEX_FIXTURE_APP_COUNT:-12}" LETTER_INDEX_KEY_SEQUENCE="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_LETTER_INDEX_KEY_SEQUENCE:-physical-index:A}" LETTER_INDEX_TARGET_APP_INDEX="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_LETTER_INDEX_TARGET_APP_INDEX:-10}" ARROW_WRAP_KEY_SEQUENCE="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_ARROW_WRAP_KEY_SEQUENCE:-app:1,down,up,left,down,enter}" REPORT_WAIT="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_REPORT_WAIT:-6.0}" fail() { echo "Round01.1 horizontal Waterfall 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" } wait_for_loaded_report() { local report="$1" /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 root = report.get("rootView", {}) if report.get("snapshotLoaded") is True and root.get("waterfallViewMode") == "horizontalMasonry": 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"horizontal Waterfall report did not become loaded within {timeout:.1f}s", file=sys.stderr) sys.exit(1) PY } assert_layout_report() { /usr/bin/python3 - "$LAYOUT_REPORT" "$FIXTURE_APP_COUNT" "$FIXTURE_WINDOWS_PER_APP" "$HOVER_APP_INDEX" <<'PY' import json import sys path = sys.argv[1] fixture_app_count = int(sys.argv[2]) fixture_windows_per_app = int(sys.argv[3]) hover_app_index = int(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", {}) columns = root.get("waterfallColumns", []) items = root.get("appShelfItems", []) waterfall_frame = root.get("waterfallFrame", {}) waterfall_height = waterfall_frame.get("height", 0) waterfall_width = root.get("waterfallVisibleWidth", 0) 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, "layout run must keep Quick Switch visible") require(root.get("waterfallViewMode") == "horizontalMasonry", "Waterfall view mode must be horizontalMasonry") require(report.get("appCount") == fixture_app_count, "fixture appCount must match requested count") require(report.get("windowCount") == fixture_app_count * fixture_windows_per_app, "fixture windowCount must match requested shape") require(root.get("waterfallColumnCount") == fixture_app_count, "Waterfall app section count must match app count") require(len(columns) == fixture_app_count, "Waterfall column reports must match app count") require(len(items) == fixture_app_count, "App Shelf item reports must match app count") require(root.get("waterfallColumnNames") == root.get("appShelfNames"), "Waterfall app order must match App Shelf order") require(root.get("waterfallScrollable") is False, "horizontal masonry must not expose horizontal scroll") require(root.get("waterfallMaxScrollOffset") == 0, "horizontal masonry horizontal max scroll offset must be zero") require(abs(root.get("waterfallContentWidth", 0) - waterfall_width) <= 1.0, "horizontal masonry content width must equal visible width") require(root.get("horizontalMasonryScrollable") is True, "fixture must make horizontal masonry vertically scrollable") require(root.get("horizontalMasonryMaxScrollOffset", 0) > 0, "horizontal masonry must expose positive vertical max scroll offset") require(root.get("horizontalMasonryScrollOffset", 0) > 0, "hovering a later App must anchor masonry vertically") require(root.get("appShelfHoveredIndex") == hover_app_index, "debug hover must mark the requested App Shelf item") require(root.get("hoveredAppGroupIndex") == hover_app_index, "horizontal masonry must keep the hovered App group available to Waterfall visuals") require(cards, "horizontal masonry fixture must expose cards") widths = [round(card.get("bounds", {}).get("width", 0), 1) for card in cards] require(len(set(widths)) == 1, "horizontal masonry cards must be equal width") heights = [round(card.get("bounds", {}).get("height", 0), 1) for card in cards] expected_card_height = 138.0 expected_thumbnail_height = 106.0 expected_card_step = 150.0 require(len(set(heights)) == 1, "horizontal masonry cards must use one fixed height") require(abs(heights[0] - expected_card_height) <= 1.0, "horizontal masonry card height must match vertical Waterfall 138pt height") require(all(card.get("frame", {}).get("x", -1) >= -1 for card in cards), "cards must not overflow left") require(all(card.get("frame", {}).get("x", 0) + card.get("frame", {}).get("width", 0) <= waterfall_width + 1 for card in cards), "cards must not overflow right") thumbnail_heights = [round(card.get("thumbnailFrame", {}).get("height", 0), 1) for card in cards] require(all(abs(height - expected_thumbnail_height) <= 1.0 for height in thumbnail_heights), "horizontal masonry thumbnails must use the same fixed card template height as vertical cards") require(all(card.get("titleBarFrame", {}).get("y", 0) >= card.get("thumbnailFrame", {}).get("y", 0) + card.get("thumbnailFrame", {}).get("height", 0) for card in cards), "title bars must remain above thumbnails") cards_by_lane = {} for card in cards: lane_x = round(card.get("frame", {}).get("x", 0), 1) cards_by_lane.setdefault(lane_x, []).append(card) lane_steps = [] for lane_cards in cards_by_lane.values(): ordered = sorted(lane_cards, key=lambda card: card.get("frame", {}).get("y", 0), reverse=True) for upper, lower in zip(ordered, ordered[1:]): lane_steps.append(round(upper.get("frame", {}).get("y", 0) - lower.get("frame", {}).get("y", 0), 1)) require(lane_steps, "fixture must place multiple cards in at least one horizontal masonry lane") require(all(abs(step - expected_card_step) <= 1.0 for step in lane_steps), "horizontal masonry vertical placement step must be 150pt") for expected_index, column in enumerate(columns): require(column.get("appGroupIndex") == expected_index, "appGroupIndex must remain sequential") require(column.get("windowCount") == fixture_windows_per_app, "each fixture App must keep its windows") column_cards = column.get("cards", []) require([card.get("windowIndex") for card in column_cards] == list(range(fixture_windows_per_app)), "window order must remain stable within each App") hover_column = columns[hover_app_index] hover_first_card = hover_column.get("cards", [])[0] hover_visible = hover_first_card.get("visibleFrame", {}) require(hover_visible.get("y", -9999) < waterfall_height + 1, "hovered App first card should be brought into vertical viewport") require(hover_visible.get("y", 0) + hover_visible.get("height", 0) > -1, "hovered App first card should not be fully below viewport") hover_cards = hover_column.get("cards", []) non_hover_cards = [ card for column in columns if column.get("appGroupIndex") != hover_app_index for card in column.get("cards", []) ] blue_reference = next( ( color for segment in root.get("spaceLaneSegments", []) for color in segment.get("windowBlockColors", []) if color.get("alpha", 0) > 0 ), None ) require(blue_reference is not None, "fixture must expose a Space Lane blue reference color") require(all("appLinked" in card.get("visualStates", []) for card in hover_cards), "all cards for the hovered App must expose appLinked visual state") require(all("appLinked" not in card.get("visualStates", []) for card in non_hover_cards), "non-hovered App cards must not expose appLinked visual state") require(root.get("selectedAppGroupIndex") == hover_app_index, "hovered App must select its first window as the default submit target") require(root.get("selectedWindowIndex") == 0, "hovered App default submit target must be its first window") require(root.get("horizontalAppDefaultWindowID") == hover_first_card.get("windowID"), "hovered App default window ID must match the first card") require(hover_first_card.get("isSelected") is True, "hovered App first card must become the effective selection") require(hover_first_card.get("isHorizontalAppDefault") is True, "hovered App first card must expose app default state") require("appDefault" in hover_first_card.get("visualStates", []), "hovered App first card must report appDefault visual state") require("selected" in hover_first_card.get("visualStates", []), "hovered App first card must get selected visual state") require(all(card.get("shadowOpacity", 0) >= 0.15 for card in hover_cards), "hovered App cards must float with visible shadow opacity") require(all(card.get("shadowRadius", 0) >= 18 for card in hover_cards), "hovered App cards must float with visible shadow radius") require(all(card.get("shadowOpacity", 1) <= 0.01 for card in non_hover_cards if "spaceFocused" not in card.get("visualStates", []) and "hover" not in card.get("visualStates", []) and "selected" not in card.get("visualStates", [])), "normal non-hovered cards must not inherit App hover shadow") for card in hover_cards: title_fill = card.get("titleBarBackgroundColor", {}) require(title_fill.get("alpha", 0) >= 0.16, "hovered App card title bar must get a visible blue fill") for channel in ("red", "green", "blue"): require( abs(title_fill.get(channel, -1) - blue_reference.get(channel, -2)) <= 0.002, f"hovered App card title bar must use the Space Lane blue reference for {channel}" ) require(all(card.get("titleBarBackgroundColor", {}).get("alpha", 1) <= 0.01 for card in non_hover_cards), "non-hovered App card title bars must stay clear") print(json.dumps({ "mode": root.get("waterfallViewMode"), "appCount": report.get("appCount"), "windowCount": report.get("windowCount"), "cardWidth": widths[0], "cardHeight": heights[0], "thumbnailHeight": thumbnail_heights[0], "cardStep": expected_card_step, "hoveredAppGroupIndex": root.get("hoveredAppGroupIndex"), "hoveredAppLinkedCardCount": len(hover_cards), "hoveredAppTitleBarFill": hover_cards[0].get("titleBarBackgroundColor"), "horizontalMasonryScrollOffset": root.get("horizontalMasonryScrollOffset"), "horizontalMasonryMaxScrollOffset": root.get("horizontalMasonryMaxScrollOffset") }, indent=2, ensure_ascii=False)) PY } assert_keyboard_report() { /usr/bin/python3 - "$KEYBOARD_REPORT" "$KEY_SEQUENCE" <<'PY' import json import sys path = sys.argv[1] key_sequence = [part for part in sys.argv[2].split(",") if part] 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", []) require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true") require(root.get("waterfallViewMode") == "horizontalMasonry", "keyboard run must use horizontalMasonry") require(root.get("keyboardCommandsApplied") == key_sequence, "horizontal keyboard commands must be applied") require(root.get("tabIgnoredCount") == 0, "Tab must navigate in horizontal masonry mode") require(root.get("selectedAppGroupIndex") == 0, "single Tab from first card must stay in first App") require(root.get("selectedWindowIndex") == 1, "single Tab from first card must select the second laid card") selected = columns[0].get("cards", [])[1] require(root.get("selectedWindowID") == selected.get("windowID"), "selectedWindowID must match the second laid card") print(json.dumps({ "commands": root.get("keyboardCommandsApplied"), "tabIgnoredCount": root.get("tabIgnoredCount"), "selectedAppGroupIndex": root.get("selectedAppGroupIndex"), "selectedWindowIndex": root.get("selectedWindowIndex") }, indent=2, ensure_ascii=False)) PY } assert_app_enter_report() { /usr/bin/python3 - "$APP_ENTER_REPORT" "$APP_ENTER_KEY_SEQUENCE" "$APP_ENTER_TARGET_APP_INDEX" <<'PY' import json import sys path = sys.argv[1] raw_key_sequence = [part for part in sys.argv[2].split(",") if part] target_app_index = int(sys.argv[3]) with open(path, "r", encoding="utf-8") as file: report = json.load(file) def normalize_key_sequence(sequence): normalized = [] physical_symbols = [] for part in sequence: lowered = part.lower() matched_prefix = None for prefix in ("physical-index:", "physical-index-", "keydown:", "keydown-"): if lowered.startswith(prefix): matched_prefix = prefix break if matched_prefix is None: normalized.append(part) continue symbol = part[len(matched_prefix):].strip().upper() physical_symbols.append(symbol) normalized.append(f"app:{symbol}") return normalized, physical_symbols key_sequence, physical_symbols = normalize_key_sequence(raw_key_sequence) 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", []) require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true") require(root.get("waterfallViewMode") == "horizontalMasonry", "App Enter run must use horizontalMasonry") require(root.get("keyboardCommandsApplied") == key_sequence, "App Enter keyboard commands must be applied") require(root.get("appShelfIndexKeyDownCount") == len(physical_symbols), "physical App index keyDown path must be exercised") if physical_symbols: require(root.get("lastAppShelfIndexKeySymbol") == physical_symbols[-1], "last physical App index symbol must be reported") require(target_app_index < len(columns), "target App index must exist") target_card = columns[target_app_index].get("cards", [])[0] require(root.get("hoverTargetKind") == "app", "App index focus must leave hover target on the App icon") require(root.get("keyboardFocusedAppGroupIndex") == target_app_index, "keyboard App focus must target requested App") require(root.get("keyboardFocusedWindowID") is None, "App icon focus must not keep stale window keyboard focus") require(root.get("selectedAppGroupIndex") == target_app_index, "App icon focus must select the target App") require(root.get("selectedWindowIndex") == 0, "App icon focus must select the first window") require(root.get("horizontalAppDefaultWindowID") == target_card.get("windowID"), "App default window ID must match the first window") require(root.get("lastCommittedAppGroupIndex") == target_app_index, "Enter on App icon must commit the target App") require(root.get("lastCommittedWindowIndex") == 0, "Enter on App icon must commit the first window") require(root.get("lastCommittedWindowID") == target_card.get("windowID"), "Enter on App icon must commit the first window ID") require(root.get("lastCommitSource") == "keyboard", "App Enter commit must be keyboard sourced") require(target_card.get("isHorizontalAppDefault") is True, "target first card must report app default state") require("appDefault" in target_card.get("visualStates", []), "target first card must expose appDefault visual state") require("selected" in target_card.get("visualStates", []), "target first card must expose selected visual state") print(json.dumps({ "commands": root.get("keyboardCommandsApplied"), "hoverTargetKind": root.get("hoverTargetKind"), "targetAppGroupIndex": target_app_index, "lastCommittedWindowID": root.get("lastCommittedWindowID") }, indent=2, ensure_ascii=False)) PY } assert_letter_index_report() { /usr/bin/python3 - "$LETTER_INDEX_REPORT" "$LETTER_INDEX_KEY_SEQUENCE" "$LETTER_INDEX_TARGET_APP_INDEX" <<'PY' import json import sys path = sys.argv[1] raw_key_sequence = [part for part in sys.argv[2].split(",") if part] target_app_index = int(sys.argv[3]) 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) def normalize_key_sequence(sequence): normalized = [] physical_symbols = [] for part in sequence: lowered = part.lower() matched_prefix = None for prefix in ("physical-index:", "physical-index-", "keydown:", "keydown-"): if lowered.startswith(prefix): matched_prefix = prefix break if matched_prefix is None: normalized.append(part) continue symbol = part[len(matched_prefix):].strip().upper() physical_symbols.append(symbol) normalized.append(f"app:{symbol}") return normalized, physical_symbols key_sequence, physical_symbols = normalize_key_sequence(raw_key_sequence) root = report.get("rootView", {}) columns = root.get("waterfallColumns", []) require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true") require(root.get("waterfallViewMode") == "horizontalMasonry", "letter App index run must use horizontalMasonry") require(root.get("keyboardCommandsApplied") == key_sequence, "letter App index keyboard commands must be applied") require(root.get("appShelfIndexKeyDownCount") == len(physical_symbols), "letter App index must exercise physical keyDown path") require(physical_symbols, "letter App index test must contain a physical key") require(root.get("lastAppShelfIndexKeySymbol") == physical_symbols[-1], "last letter App index symbol must be reported") require(target_app_index < len(columns), "target letter App index must exist") target_card = columns[target_app_index].get("cards", [])[0] require(root.get("hoverTargetKind") == "app", "letter App index focus must leave hover target on App icon") require(root.get("hoverTargetSource") == "keyboard", "letter App index focus must report keyboard source") require(root.get("keyboardFocusedAppGroupIndex") == target_app_index, "letter App index must focus the expected App") require(root.get("keyboardFocusedWindowID") is None, "letter App icon focus must not keep stale window keyboard focus") require(root.get("selectedAppGroupIndex") == target_app_index, "letter App icon focus must select the target App") require(root.get("selectedWindowIndex") == 0, "letter App icon focus must select the first window") require(root.get("horizontalAppDefaultWindowID") == target_card.get("windowID"), "letter App default window must match the first card") require(root.get("lastCommittedWindowID") is None, "letter App index focus without Enter must not commit") require(root.get("lastCommitSource") is None, "letter App index focus without Enter must not report commit source") require(target_card.get("isHorizontalAppDefault") is True, "letter target first card must report app default state") require("appDefault" in target_card.get("visualStates", []), "letter target first card must expose appDefault visual state") require("selected" in target_card.get("visualStates", []), "letter target first card must expose selected visual state") print(json.dumps({ "commands": root.get("keyboardCommandsApplied"), "physicalKeyDownCount": root.get("appShelfIndexKeyDownCount"), "lastSymbol": root.get("lastAppShelfIndexKeySymbol"), "targetAppGroupIndex": target_app_index, "defaultWindowID": root.get("horizontalAppDefaultWindowID") }, indent=2, ensure_ascii=False)) PY } assert_arrow_wrap_report() { /usr/bin/python3 - "$ARROW_WRAP_REPORT" "$ARROW_WRAP_KEY_SEQUENCE" "$FIXTURE_APP_COUNT" <<'PY' import json import sys path = sys.argv[1] key_sequence = [part for part in sys.argv[2].split(",") if part] fixture_app_count = int(sys.argv[3]) 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", []) target_app_index = fixture_app_count - 1 require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true") require(root.get("waterfallViewMode") == "horizontalMasonry", "arrow wrap run must use horizontalMasonry") require(root.get("keyboardCommandsApplied") == key_sequence, "arrow wrap keyboard commands must be applied") require(len(columns) == fixture_app_count, "fixture App count must match") target_card = columns[target_app_index].get("cards", [])[0] require(root.get("lastCommittedAppGroupIndex") == target_app_index, "left arrow from first App icon must wrap to the last App") require(root.get("lastCommittedWindowIndex") == 0, "down arrow from App icon must land on the first window") require(root.get("lastCommittedWindowID") == target_card.get("windowID"), "Enter after arrow focus must commit the focused first window") require(root.get("selectedAppGroupIndex") == target_app_index, "selected App must match wrapped App") require(root.get("selectedWindowIndex") == 0, "selected window must be the wrapped App first window") require(root.get("lastCommitSource") == "keyboard", "arrow focus Enter commit must be keyboard sourced") print(json.dumps({ "commands": root.get("keyboardCommandsApplied"), "wrappedAppGroupIndex": target_app_index, "lastCommittedWindowID": root.get("lastCommittedWindowID") }, indent=2, ensure_ascii=False)) PY } assert_filter_report() { /usr/bin/python3 - "$FILTER_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 = [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, "Space filter run must keep Quick Switch visible") require(root.get("waterfallViewMode") == "horizontalMasonry", "Space filter run must use horizontalMasonry") require(root.get("spaceFilterActive") is True, "clicking Space A1 must lock the filter") require(root.get("spaceFilterLockedSpaceID") == 1, "Space A1 must be the locked Space") require(root.get("appShelfNames") == ["Alpha Space App", "Beta Space App"], "horizontal Space filter must filter App Shelf") require(root.get("waterfallColumnNames") == ["Alpha Space App", "Beta Space App"], "horizontal Space filter must filter Waterfall sections") require([card.get("windowID") for card in cards] == [50101, 50102, 50201], "horizontal Space filter must expose only Space A1 windows") require(all(card.get("primarySpaceID") == 1 for card in cards), "all filtered horizontal cards must belong to Space A1") require(root.get("waterfallScrollable") is False, "horizontal Space filter must not expose horizontal scroll") require(root.get("projectionTransitionFadeOutLayerCount", 0) > 0, "horizontal Space filter must animate removed cards/apps") require(root.get("projectionTransitionDurationMilliseconds", 0) >= 120, "horizontal Space filter must report projection duration") widths = [round(card.get("bounds", {}).get("width", 0), 1) for card in cards] require(len(set(widths)) == 1, "filtered horizontal cards must remain equal width") heights = [round(card.get("bounds", {}).get("height", 0), 1) for card in cards] require(len(set(heights)) == 1 and abs(heights[0] - 138.0) <= 1.0, "filtered horizontal cards must keep the fixed 138pt height") visible_width = root.get("waterfallVisibleWidth", 0) require(all(card.get("frame", {}).get("x", -1) >= -1 for card in cards), "filtered cards must not overflow left") require(all(card.get("frame", {}).get("x", 0) + card.get("frame", {}).get("width", 0) <= visible_width + 1 for card in cards), "filtered cards must not overflow right") print(json.dumps({ "mode": root.get("waterfallViewMode"), "lockedSpaceID": root.get("spaceFilterLockedSpaceID"), "apps": root.get("appShelfNames"), "windowIDs": [card.get("windowID") for card in cards], "fadeOutLayerCount": root.get("projectionTransitionFadeOutLayerCount"), "transitionDurationMilliseconds": root.get("projectionTransitionDurationMilliseconds") }, indent=2, ensure_ascii=False)) PY } run_fixture() { local report="$1" shift run_fixture_with_app_count "$report" "$FIXTURE_APP_COUNT" "$@" } run_fixture_with_app_count() { local report="$1" local app_count="$2" shift 2 rm -f "$report" "$APP/Contents/MacOS/Aligner" \ --round0-skip-permissions \ --round01-open-quick-switch \ --round01-fixture-app-count="$app_count" \ --round01-fixture-windows-per-app="$FIXTURE_WINDOWS_PER_APP" \ --round01-disable-screenshot-refresh \ --round01-waterfall-view-mode=horizontal \ --round01-quick-switch-report="$report" \ "$@" & APP_PID=$! } run_space_filter_fixture() { local report="$1" rm -f "$report" "$APP/Contents/MacOS/Aligner" \ --round0-skip-permissions \ --round01-open-quick-switch \ --round01-fixture-space-filter \ --round01-disable-screenshot-refresh \ --round01-waterfall-view-mode=horizontal \ --round01-debug-mouse-sequence="click-space:1" \ --round01-quick-switch-report="$report" & APP_PID=$! } if [ "$FIXTURE_APP_COUNT" -le "$HOVER_APP_INDEX" ]; then fail "hover App index must be inside fixture app count" fi if [ "$LETTER_INDEX_FIXTURE_APP_COUNT" -le "$LETTER_INDEX_TARGET_APP_INDEX" ]; then fail "letter index target App must be inside letter fixture app count" fi if [ "$FIXTURE_WINDOWS_PER_APP" -lt 2 ]; then fail "fixture must include at least two windows per App for Tab navigation" fi stop_current_aligner "$SCRIPT_DIR/package-app.sh" >&2 APP_PID="" cleanup() { if [ -n "${APP_PID:-}" ]; then kill "$APP_PID" 2>/dev/null || true wait "$APP_PID" 2>/dev/null || true fi stop_current_aligner } trap cleanup EXIT run_fixture "$LAYOUT_REPORT" --round01-debug-mouse-sequence="hover-app:$HOVER_APP_INDEX" wait_for_loaded_report "$LAYOUT_REPORT" assert_layout_report kill "$APP_PID" 2>/dev/null || true wait "$APP_PID" 2>/dev/null || true stop_current_aligner run_fixture "$KEYBOARD_REPORT" --round01-debug-key-sequence="$KEY_SEQUENCE" wait_for_loaded_report "$KEYBOARD_REPORT" assert_keyboard_report kill "$APP_PID" 2>/dev/null || true wait "$APP_PID" 2>/dev/null || true stop_current_aligner run_fixture "$APP_ENTER_REPORT" --round01-debug-key-sequence="$APP_ENTER_KEY_SEQUENCE" wait_for_loaded_report "$APP_ENTER_REPORT" assert_app_enter_report kill "$APP_PID" 2>/dev/null || true wait "$APP_PID" 2>/dev/null || true stop_current_aligner run_fixture_with_app_count "$LETTER_INDEX_REPORT" "$LETTER_INDEX_FIXTURE_APP_COUNT" --round01-debug-key-sequence="$LETTER_INDEX_KEY_SEQUENCE" wait_for_loaded_report "$LETTER_INDEX_REPORT" assert_letter_index_report kill "$APP_PID" 2>/dev/null || true wait "$APP_PID" 2>/dev/null || true stop_current_aligner run_fixture "$ARROW_WRAP_REPORT" --round01-debug-key-sequence="$ARROW_WRAP_KEY_SEQUENCE" wait_for_loaded_report "$ARROW_WRAP_REPORT" assert_arrow_wrap_report kill "$APP_PID" 2>/dev/null || true wait "$APP_PID" 2>/dev/null || true stop_current_aligner run_space_filter_fixture "$FILTER_REPORT" wait_for_loaded_report "$FILTER_REPORT" assert_filter_report kill "$APP_PID" 2>/dev/null || true wait "$APP_PID" 2>/dev/null || true trap - EXIT stop_current_aligner echo "Round01.1 horizontal Waterfall fixture QA passed"