| | |
| | | local expected_last_mouse="$3" |
| | | local app_count="${4:-$FIXTURE_APP_COUNT}" |
| | | local windows_per_app="${5:-$FIXTURE_WINDOWS_PER_APP}" |
| | | local quick_switch_expectation="${6:-visible}" |
| | | local fixture_shape="${7:-single-display}" |
| | | local args=( |
| | | --round0-skip-permissions |
| | | --round01-open-quick-switch |
| | | --round01-fixture-app-count="$app_count" |
| | | --round01-fixture-windows-per-app="$windows_per_app" |
| | | --round01-debug-overlay-width="$OVERLAY_WIDTH" |
| | | --round01-disable-screenshot-refresh |
| | | --round01-debug-window-activation |
| | | --round01-debug-mouse-sequence="$sequence" |
| | | --round01-quick-switch-report="$report" |
| | | ) |
| | | if [ "$fixture_shape" = "multi-display" ]; then |
| | | args+=(--round01-fixture-multi-display) |
| | | fi |
| | | |
| | | 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="$windows_per_app" \ |
| | | --round01-debug-overlay-width="$OVERLAY_WIDTH" \ |
| | | --round01-disable-screenshot-refresh \ |
| | | --round01-debug-mouse-sequence="$sequence" \ |
| | | --round01-quick-switch-report="$report" & |
| | | "$APP/Contents/MacOS/Aligner" "${args[@]}" & |
| | | |
| | | APP_PID=$! |
| | | |
| | | wait_for_report "$report" "$expected_last_mouse" |
| | | swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-quick-switch >&2 |
| | | if [ "$quick_switch_expectation" = "hidden" ]; then |
| | | swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-no-quick-switch >&2 |
| | | elif [ "$quick_switch_expectation" = "visible" ]; then |
| | | swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-quick-switch >&2 |
| | | elif [ "$quick_switch_expectation" != "report-only" ]; then |
| | | fail "unknown quick switch expectation: $quick_switch_expectation" |
| | | fi |
| | | |
| | | kill "$APP_PID" 2>/dev/null || true |
| | | wait "$APP_PID" 2>/dev/null || true |
| | |
| | | segments = root.get("spaceLaneSegments", []) |
| | | |
| | | require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true") |
| | | require(root.get("activeSpaceFocusID") == 1, "Space Lane focus must set activeSpaceFocusID") |
| | | require(root.get("spaceLaneFocusedSpaceID") == 1, "click-space must persist focused Space") |
| | | require(root.get("lastSpaceLaneClickSpaceID") == 1, "last clicked Space must be recorded") |
| | | require(root.get("activeSpaceFocusID") == 1, "Space Lane hover must set activeSpaceFocusID") |
| | | require(root.get("spaceLaneHoveredSpaceID") == 1, "hover-space must expose hovered Space") |
| | | require(root.get("spaceLaneFocusedSpaceID") is None, "hover-space must not persist clicked Space focus") |
| | | require(root.get("lastSpaceLaneClickSpaceID") is None, "hover-space must not record a Space click") |
| | | require(root.get("lastSpaceFocusedAppGroupIndex") == 0, "first related App column must be appGroupIndex 0") |
| | | require(root.get("spaceFocusAppGroupIndexes") == list(range(fixture_app_count)), "all fixture apps must be marked related to focused Space") |
| | | require(root.get("waterfallScrollOffset") == 0, "first related App column should remain visible at offset zero") |
| | | |
| | | focused_segments = [segment for segment in segments if segment.get("isFocused") is True] |
| | | require(len(focused_segments) == 1, "exactly one Space Lane segment must be focused") |
| | | require(focused_segments[0].get("label") == "A1", "focused Space should be A1") |
| | | require(focused_segments[0].get("label") == "01", "focused Space should be 01") |
| | | require("focused" in focused_segments[0].get("visualStates", []), "focused Space segment must expose focused visual state") |
| | | require(focused_segments[0].get("shadowRadius", 0) >= 24, "focused Space segment shadow radius must use the stronger float treatment") |
| | | require(focused_segments[0].get("shadowOpacity", 0) >= 0.25, "focused Space segment shadow opacity must use the stronger float treatment") |
| | | |
| | | require(len(items) == fixture_app_count, "App Shelf fixture item count must match app count") |
| | | require(all(item.get("isSpaceFocused") is True for item in items), "all fixture apps must be highlighted for Space A1") |
| | | require(all("spaceFocused" in item.get("visualStates", []) for item in items), "App Shelf items must expose spaceFocused visual state") |
| | | selected_items = [item for item in items if item.get("isSelected") is True] |
| | | require(len(selected_items) == 1, "App Shelf must keep one functional selection while Space focus is active") |
| | | require("selected" not in selected_items[0].get("visualStates", []), "Space focus must suppress App Shelf selected visual state") |
| | | require("selectedVisualSuppressed" in selected_items[0].get("visualStates", []), "suppressed App Shelf selected visual must be explicit") |
| | | require(all(item.get("spaceFocusWindowCount", 0) > 0 for item in items), "focused Space badge count must be positive for every fixture app") |
| | | require(all(item.get("badgeVisible") is True for item in items), "focused Space badges must be visible") |
| | | require(items[0].get("spaceFocusDirection") == "visible", "first related App must be visible") |
| | |
| | | require(columns[0].get("spaceFocusDirection") == "visible", "first Waterfall column must be visible") |
| | | require(columns[-1].get("isSpaceFocused") is True, "last Waterfall column must be marked focused for Space") |
| | | require(columns[-1].get("spaceFocusDirection") == "right", "last Waterfall column must report right direction before remote click") |
| | | selected_cards = [card for column in columns for card in column.get("cards", []) if card.get("isSelected") is True] |
| | | require(len(selected_cards) == 1, "Waterfall must keep one functional selection while Space focus is active") |
| | | require("selected" not in selected_cards[0].get("visualStates", []), "Space focus must suppress Waterfall selected visual state") |
| | | require("selectedVisualSuppressed" in selected_cards[0].get("visualStates", []), "suppressed Waterfall selected visual must be explicit") |
| | | require(selected_cards[0].get("shadowRadius", 0) >= 24, "space-focused Waterfall card shadow radius must use the stronger float treatment") |
| | | require(selected_cards[0].get("shadowOpacity", 0) >= 0.19, "space-focused Waterfall card shadow opacity must use the stronger float treatment") |
| | | |
| | | space_fill = focused_segments[0].get("backgroundColor", {}) |
| | | card_fill = selected_cards[0].get("backgroundColor", {}) |
| | | for channel in ("red", "green", "blue", "alpha"): |
| | | require( |
| | | abs(space_fill.get(channel, -1) - card_fill.get(channel, -2)) <= 0.002, |
| | | f"focused Space fill must match space-focused card fill for {channel}" |
| | | ) |
| | | |
| | | print(json.dumps({ |
| | | "activeSpaceFocusID": root.get("activeSpaceFocusID"), |
| | | "focusedApps": root.get("spaceFocusAppGroupIndexes"), |
| | | "focusedSpaceFill": space_fill, |
| | | "spaceFocusedCardFill": card_fill, |
| | | "focusedSpaceShadowRadius": focused_segments[0].get("shadowRadius"), |
| | | "spaceFocusedCardShadowRadius": selected_cards[0].get("shadowRadius"), |
| | | "lastRemoteDirection": items[-1].get("spaceFocusDirection"), |
| | | "lastRemoteBadgeText": items[-1].get("badgeText") |
| | | }, indent=2, ensure_ascii=False)) |
| | |
| | | columns = root.get("waterfallColumns", []) |
| | | last_index = fixture_app_count - 1 |
| | | |
| | | require(root.get("activeSpaceFocusID") == 1, "Space focus must persist after clicking App Shelf item") |
| | | require(report.get("quickSwitchVisible") is False, "remote App click must close Quick Switch after committing its first window") |
| | | require(root.get("activeSpaceFocusID") is None, "App click must clear Space hover focus before the pre-commit report") |
| | | require(root.get("hoverTargetKind") == "app", "App click must leave the hover target on the clicked App, not on Space") |
| | | require(root.get("lastClickedAppGroupIndex") == last_index, "remote App click must be recorded") |
| | | require(root.get("lastAppShelfClickChangedSelection") is True, "App Shelf click must move keyboard selection to the remote App column") |
| | | require(root.get("selectedAppGroupIndex") == last_index, "remote App click must focus the remote App column") |
| | | require(root.get("lastCommittedWindowID") is None, "App Shelf click must not commit a window") |
| | | expected_window_id = columns[last_index].get("cards", [])[0].get("windowID") |
| | | require(root.get("lastCommittedWindowID") == expected_window_id, "App Shelf click must commit the first remote window") |
| | | require(report.get("lastCommittedWindowID") == expected_window_id, "session must surface the remote App commit") |
| | | require(report.get("lastActivationWindowID") == expected_window_id, "activation window must be the first remote window") |
| | | require(report.get("lastActivationResult") == "activated", "debug activation must activate the first remote window") |
| | | require(report.get("lastCommitSource") == "mouse", "remote App commit source must be mouse") |
| | | require(root.get("waterfallScrollOffset", 0) > 0, "remote App click must scroll Waterfall horizontally") |
| | | require(items[last_index].get("spaceFocusDirection") == "visible", "remote App must become visible after click") |
| | | require(columns[last_index].get("spaceFocusDirection") == "visible", "remote Waterfall column must become visible after click") |
| | | require(items[last_index].get("badgeVisible") is True, "focused Space badge must remain visible after remote click") |
| | | require(items[last_index].get("spaceFocusDirection") == "none", "App click must clear Space focus direction after leaving Space Lane") |
| | | require(columns[last_index].get("spaceFocusDirection") == "none", "Waterfall column must not keep Space focus after App click") |
| | | require(all(item.get("isSpaceFocused") is False for item in items), "App click must clear all App Shelf spaceFocused states") |
| | | require(all(column.get("isSpaceFocused") is False for column in columns), "App click must clear all Waterfall column spaceFocused states") |
| | | |
| | | print(json.dumps({ |
| | | "activeSpaceFocusID": root.get("activeSpaceFocusID"), |
| | | "hoverTargetKind": root.get("hoverTargetKind"), |
| | | "lastClickedAppGroupIndex": root.get("lastClickedAppGroupIndex"), |
| | | "lastCommittedWindowID": root.get("lastCommittedWindowID"), |
| | | "lastActivationResult": report.get("lastActivationResult"), |
| | | "waterfallScrollOffset": root.get("waterfallScrollOffset"), |
| | | "remoteDirectionAfterClick": items[last_index].get("spaceFocusDirection") |
| | | }, indent=2, ensure_ascii=False)) |
| | | PY |
| | | } |
| | | |
| | | assert_space_click_filter_report() { |
| | | /usr/bin/python3 - "$1" <<'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 = sorted(root.get("waterfallColumns", []), key=lambda column: column.get("appGroupIndex", 0)) |
| | | target_space_id = 1 |
| | | cards = [card for column in columns for card in column.get("cards", [])] |
| | | segments = root.get("spaceLaneSegments", []) |
| | | locked_segments = [segment for segment in segments if "locked" in segment.get("visualStates", [])] |
| | | |
| | | require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true") |
| | | require(report.get("quickSwitchVisible") is True, "click-space must keep Quick Switch visible after locking Space filter") |
| | | require(report.get("spaceFilterActive") is True, "click-space must enable session Space filter") |
| | | require(report.get("spaceFilterLockedSpaceID") == target_space_id, "click-space must lock the clicked Space") |
| | | require(report.get("lastSpaceFilterAction") == "lock", "click-space must record a lock action") |
| | | require(root.get("spaceFilterActive") is True, "root report must expose active Space filter") |
| | | require(root.get("spaceFilterLockedSpaceID") == target_space_id, "root report must expose the locked Space") |
| | | require(root.get("lastSpaceLaneClickSpaceID") == target_space_id, "click-space must record the clicked Space") |
| | | require(root.get("spaceLaneFocusedSpaceID") is None, "click-space must not reuse persistent hover focus as its behavior") |
| | | require(report.get("lastCommittedWindowID") is None, "ordinary click-space must not commit a window") |
| | | require(report.get("lastActivationWindowID") is None, "ordinary click-space must not activate a window") |
| | | require(report.get("lastActivationResult") is None, "ordinary click-space must not report activation") |
| | | require(report.get("lastCommitSource") is None, "ordinary click-space must not report a commit source") |
| | | require(cards, "fixture must include filtered cards for Space A1") |
| | | require(all(card.get("primarySpaceID") == target_space_id for card in cards), "click-space filter must only keep windows in clicked Space") |
| | | require(len(locked_segments) == 1, "exactly one Space Lane segment must expose locked") |
| | | require(locked_segments[0].get("spaceID") == target_space_id, "locked segment must be the clicked Space") |
| | | |
| | | print(json.dumps({ |
| | | "clickedSpaceID": root.get("lastSpaceLaneClickSpaceID"), |
| | | "lockedSpaceID": root.get("spaceFilterLockedSpaceID"), |
| | | "visibleCardSpaceIDs": sorted({card.get("primarySpaceID") for card in cards}), |
| | | "quickSwitchVisible": report.get("quickSwitchVisible") |
| | | }, indent=2, ensure_ascii=False)) |
| | | PY |
| | | } |
| | | |
| | | assert_space_focus_clear_report() { |
| | | /usr/bin/python3 - "$1" "$FIXTURE_APP_COUNT" <<'PY' |
| | | import json |
| | | import sys |
| | | |
| | | path = sys.argv[1] |
| | | fixture_app_count = int(sys.argv[2]) |
| | | |
| | | 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", {}) |
| | | items = root.get("appShelfItems", []) |
| | | columns = root.get("waterfallColumns", []) |
| | | segments = root.get("spaceLaneSegments", []) |
| | | cards = [card for column in columns for card in column.get("cards", [])] |
| | | hovered_window = next( |
| | | ( |
| | | card |
| | | for column in columns |
| | | for card in column.get("cards", []) |
| | | if card.get("windowID") == root.get("hoveredWindowID") |
| | | ), |
| | | None |
| | | ) |
| | | |
| | | require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true") |
| | | require(root.get("hoverTargetKind") == "window", "hover-card after hover-space must set the hover target to window") |
| | | require(root.get("activeSpaceFocusID") is None, "hover-card must clear activeSpaceFocusID after leaving Space Lane") |
| | | require(root.get("spaceLaneHoveredSpaceID") is None, "hover-card must clear Space Lane hovered Space ID") |
| | | require(root.get("spaceFocusAppGroupIndexes") == [], "hover-card must clear all Space focused App indexes") |
| | | require(root.get("hoveredWindowID") is not None, "hover-card must still expose hoveredWindowID") |
| | | require(hovered_window is not None, "hover-card report must contain the hovered window card") |
| | | require(root.get("windowHoverAssociatedSpaceID") == hovered_window.get("primarySpaceID"), "hover-card must expose the hovered window Space ID") |
| | | require(len(items) == fixture_app_count, "App Shelf fixture item count must match app count") |
| | | require(all(item.get("isSpaceFocused") is False for item in items), "hover-card must not leave App Shelf items spaceFocused") |
| | | require(all(column.get("isSpaceFocused") is False for column in columns), "hover-card must not leave Waterfall columns spaceFocused") |
| | | require(all("spaceFocused" not in card.get("visualStates", []) for card in cards), "hover-card must not leave Waterfall cards spaceFocused") |
| | | associated_segments = [segment for segment in segments if segment.get("isWindowAssociated") is True] |
| | | require(len(associated_segments) == 1, "hover-card must highlight exactly one associated Space segment") |
| | | require(associated_segments[0].get("spaceID") == hovered_window.get("primarySpaceID"), "window-associated Space segment must match hovered card Space") |
| | | require("windowAssociated" in associated_segments[0].get("visualStates", []), "window-associated Space segment must expose windowAssociated visual state") |
| | | require("focused" not in associated_segments[0].get("visualStates", []), "window-associated Space segment must not expose focused") |
| | | require("hover" not in associated_segments[0].get("visualStates", []), "window-associated Space segment must not expose Space hover") |
| | | |
| | | print(json.dumps({ |
| | | "hoverTargetKind": root.get("hoverTargetKind"), |
| | | "activeSpaceFocusID": root.get("activeSpaceFocusID"), |
| | | "hoveredWindowID": root.get("hoveredWindowID"), |
| | | "windowHoverAssociatedSpaceID": root.get("windowHoverAssociatedSpaceID"), |
| | | "spaceFocusAppGroupIndexes": root.get("spaceFocusAppGroupIndexes") |
| | | }, indent=2, ensure_ascii=False)) |
| | | PY |
| | | } |
| | | |
| | | assert_app_hover_space_lane_report() { |
| | | /usr/bin/python3 - "$1" <<'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", {}) |
| | | items = root.get("appShelfItems", []) |
| | | columns = root.get("waterfallColumns", []) |
| | | segments = root.get("spaceLaneSegments", []) |
| | | cards = [card for column in columns for card in column.get("cards", [])] |
| | | |
| | | target_column = next((column for column in columns if column.get("appGroupIndex") == 0), None) |
| | | require(target_column is not None, "fixture must include App group 0") |
| | | |
| | | expected_space_ids = sorted({ |
| | | card.get("primarySpaceID") |
| | | for card in target_column.get("cards", []) |
| | | if isinstance(card.get("primarySpaceID"), int) |
| | | }) |
| | | reported_space_ids = sorted(root.get("appHoverAssociatedSpaceIDs", [])) |
| | | associated_segments = [segment for segment in segments if segment.get("isAppAssociated") is True] |
| | | associated_segment_ids = sorted(segment.get("spaceID") for segment in associated_segments) |
| | | |
| | | require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true") |
| | | require(root.get("hoverTargetKind") == "app", "hover-app must set the hover target to app") |
| | | require(root.get("hoveredAppGroupIndex") == 0, "hover-app must expose the hovered App group") |
| | | require(root.get("activeSpaceFocusID") is None, "hover-app must not set activeSpaceFocusID") |
| | | require(root.get("spaceLaneHoveredSpaceID") is None, "hover-app must not masquerade as Space Lane hover") |
| | | require(root.get("spaceFocusAppGroupIndexes") == [], "hover-app must not activate Space focus") |
| | | require(reported_space_ids == expected_space_ids, "hover-app must report only the hovered App's Spaces") |
| | | require(associated_segment_ids == expected_space_ids, "Space Lane appAssociated segments must match hovered App Spaces") |
| | | require(len(associated_segments) > 0, "hover-app must visually associate at least one Space") |
| | | require(all("appAssociated" in segment.get("visualStates", []) for segment in associated_segments), "associated Space segments must expose appAssociated") |
| | | require(all("focused" not in segment.get("visualStates", []) for segment in associated_segments), "app-associated Spaces must not expose focused") |
| | | require(all("hover" not in segment.get("visualStates", []) for segment in associated_segments), "app-associated Spaces must not expose Space hover") |
| | | require(all(segment.get("shadowRadius", 0) >= 18 for segment in associated_segments), "app-associated Spaces must get a visible float shadow") |
| | | require(all(item.get("isSpaceFocused") is False for item in items), "hover-app must not mark App Shelf items spaceFocused") |
| | | require(all(column.get("isSpaceFocused") is False for column in columns), "hover-app must not mark Waterfall columns spaceFocused") |
| | | require(all("spaceFocused" not in card.get("visualStates", []) for card in cards), "hover-app must not mark Waterfall cards spaceFocused") |
| | | |
| | | print(json.dumps({ |
| | | "hoverTargetKind": root.get("hoverTargetKind"), |
| | | "hoveredAppGroupIndex": root.get("hoveredAppGroupIndex"), |
| | | "appHoverAssociatedSpaceIDs": reported_space_ids, |
| | | "associatedSegmentIDs": associated_segment_ids, |
| | | "activeSpaceFocusID": root.get("activeSpaceFocusID") |
| | | }, indent=2, ensure_ascii=False)) |
| | | PY |
| | | } |
| | |
| | | PY |
| | | } |
| | | |
| | | assert_display_group_report() { |
| | | /usr/bin/python3 - "$1" <<'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) |
| | | |
| | | def right_edge(frame): |
| | | return frame.get("x", 0) + frame.get("width", 0) |
| | | |
| | | def contains(outer, inner, tolerance=1.0): |
| | | return ( |
| | | inner.get("x", 0) >= outer.get("x", 0) - tolerance |
| | | and inner.get("y", 0) >= outer.get("y", 0) - tolerance |
| | | and right_edge(inner) <= right_edge(outer) + tolerance |
| | | and inner.get("y", 0) + inner.get("height", 0) <= outer.get("y", 0) + outer.get("height", 0) + tolerance |
| | | ) |
| | | |
| | | def is_light_blue(color): |
| | | return ( |
| | | color.get("alpha", 0) >= 0.98 |
| | | and color.get("blue", 0) >= 0.98 |
| | | and color.get("green", 0) >= 0.90 |
| | | and color.get("red", 0) >= 0.80 |
| | | and color.get("blue", 0) > color.get("red", 0) + 0.04 |
| | | ) |
| | | |
| | | def is_white(color): |
| | | return ( |
| | | color.get("alpha", 0) >= 0.98 |
| | | and color.get("red", 0) >= 0.98 |
| | | and color.get("green", 0) >= 0.98 |
| | | and color.get("blue", 0) >= 0.98 |
| | | ) |
| | | |
| | | root = report.get("rootView", {}) |
| | | groups = root.get("spaceLaneDisplayGroups", []) |
| | | segments = root.get("spaceLaneSegments", []) |
| | | lane_background = root.get("spaceLaneBackgroundColor", {}) |
| | | |
| | | require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true") |
| | | require(report.get("quickSwitchVisible") is True, "multi-display report must keep Quick Switch visible") |
| | | require(root.get("activeSpaceFocusID") == 4, "multi-display hover must focus Space B1") |
| | | require(root.get("spaceLaneDisplayGroupCount") == 2, "multi-display fixture must expose two Display groups") |
| | | require(len(groups) == 2, "Display group reports must contain two groups") |
| | | require(root.get("spaceLaneSegmentCount") == 5, "multi-display fixture must expose five Space segments") |
| | | require(len(segments) == 5, "Space segment reports must contain five segments") |
| | | require(groups[0].get("spaceLabels") == ["A1", "A2", "A3"], "first Display group must contain A spaces") |
| | | require(groups[1].get("spaceLabels") == ["B1", "B2"], "second Display group must contain B spaces") |
| | | require(lane_background.get("alpha", 1) <= 0.01, "Space Lane outer background must be transparent") |
| | | require(root.get("spaceLaneCornerRadius") == 0, "Space Lane outer layer must not draw a rounded white rectangle") |
| | | |
| | | first_group_frame = groups[0].get("visibleFrame", {}) |
| | | second_group_frame = groups[1].get("visibleFrame", {}) |
| | | group_gap = groups[0].get("groupGap", 0) |
| | | actual_group_gap = second_group_frame.get("x", 0) - right_edge(first_group_frame) |
| | | require(actual_group_gap >= group_gap - 1.0, "Display group gap must be at least the reported group gap") |
| | | require(actual_group_gap > groups[0].get("segmentGap", 0) * 2, "Display group gap must be visibly larger than inner segment gap") |
| | | |
| | | segments_by_label = {segment.get("label"): segment for segment in segments} |
| | | for group in groups: |
| | | group_frame = group.get("visibleFrame", {}) |
| | | labels = group.get("spaceLabels", []) |
| | | require(group.get("borderWidth") == 0, "Display group container must be borderless") |
| | | require(group.get("shadowOpacity") == 0, "Display group container must not add a floating shadow") |
| | | require(group.get("backgroundColor", {}).get("alpha", 0) >= 0.98, "Display group container must use an opaque system surface") |
| | | require(group.get("groupHorizontalPadding", 0) >= 24, "Display group horizontal padding must be widened") |
| | | for label in labels: |
| | | require(label in segments_by_label, f"segment report must include {label}") |
| | | require(contains(group_frame, segments_by_label[label].get("visibleFrame", {})), f"{label} must be inside its Display group") |
| | | first_segment = segments_by_label[labels[0]].get("visibleFrame", {}) |
| | | last_segment = segments_by_label[labels[-1]].get("visibleFrame", {}) |
| | | require( |
| | | first_segment.get("x", 0) - group_frame.get("x", 0) >= group.get("groupHorizontalPadding", 0) - 1.0, |
| | | "Display group leading padding must match widened padding" |
| | | ) |
| | | require( |
| | | right_edge(group_frame) - right_edge(last_segment) >= group.get("groupHorizontalPadding", 0) - 1.0, |
| | | "Display group trailing padding must match widened padding" |
| | | ) |
| | | |
| | | for group in groups: |
| | | labels = group.get("spaceLabels", []) |
| | | if len(labels) < 2: |
| | | continue |
| | | previous = segments_by_label[labels[0]].get("visibleFrame", {}) |
| | | for label in labels[1:]: |
| | | current = segments_by_label[label].get("visibleFrame", {}) |
| | | actual_segment_gap = current.get("x", 0) - right_edge(previous) |
| | | require( |
| | | abs(actual_segment_gap - group.get("segmentGap", 0)) <= 1.0, |
| | | "inner Space segment gap must match reported segment gap" |
| | | ) |
| | | previous = current |
| | | |
| | | focused = [segment for segment in segments if segment.get("isFocused") is True] |
| | | require(len(focused) == 1, "exactly one Space segment must be focused") |
| | | require(focused[0].get("label") == "B1", "hovered multi-display Space must be B1") |
| | | require("focused" in focused[0].get("visualStates", []), "focused B1 segment must expose focused visual state") |
| | | |
| | | empty_segments = [ |
| | | segment for segment in segments |
| | | if segment.get("windowCount") == 0 |
| | | ] |
| | | require(empty_segments, "multi-display visual fixture must include at least one empty Space") |
| | | require( |
| | | {"A3", "B1"}.issubset({segment.get("label") for segment in empty_segments}), |
| | | "multi-display visual fixture must include the A3/B1 empty Space regression pair" |
| | | ) |
| | | require( |
| | | all(segment.get("backgroundKind") == "clear" for segment in empty_segments), |
| | | "empty Spaces must not keep a dirty gray fill, even when focused/current/associated" |
| | | ) |
| | | require( |
| | | all(not segment.get("gradientColors", []) for segment in empty_segments), |
| | | "empty Spaces must clear stale gradient colors" |
| | | ) |
| | | require( |
| | | all(segment.get("backgroundColor", {}).get("alpha", 1) <= 0.01 for segment in empty_segments), |
| | | "empty Spaces must keep transparent fill" |
| | | ) |
| | | require( |
| | | all(segment.get("shadowOpacity", 1) <= 0.01 for segment in empty_segments), |
| | | "empty Spaces must not draw a shadow that reads as an interior gray fill" |
| | | ) |
| | | idle_occupied_segments = [ |
| | | segment for segment in segments |
| | | if segment.get("windowCount", 0) > 0 |
| | | and segment.get("isFocused") is not True |
| | | and segment.get("isCurrent") is not True |
| | | and segment.get("isAppAssociated") is not True |
| | | and segment.get("isWindowAssociated") is not True |
| | | ] |
| | | require(idle_occupied_segments, "multi-display visual fixture must include an idle occupied Space") |
| | | for segment in idle_occupied_segments: |
| | | gradient_colors = segment.get("gradientColors", []) |
| | | require(segment.get("backgroundKind") == "gradient", "idle occupied Spaces must use blue-white gradient fill") |
| | | require(len(gradient_colors) == 2, "idle occupied Space gradient must expose two color stops") |
| | | require(is_light_blue(gradient_colors[0]), "idle occupied Space gradient must start with pure light blue") |
| | | require(is_white(gradient_colors[1]), "idle occupied Space gradient must end with pure white") |
| | | fullscreen_segments = [segment for segment in segments if segment.get("type") == "fullscreen"] |
| | | require(fullscreen_segments, "multi-display visual fixture must include a fullscreen Space") |
| | | for segment in fullscreen_segments: |
| | | marker_frame = segment.get("fullscreenMarkerFrame", {}) |
| | | app_frame = segment.get("appFrame", {}) |
| | | marker_center_ratio = segment.get("fullscreenMarkerCenterRatio", 0) |
| | | marker_mid_y = marker_frame.get("y", 0) + marker_frame.get("height", 0) / 2 |
| | | app_mid_y = app_frame.get("y", 0) + app_frame.get("height", 0) / 2 |
| | | require(0.36 <= marker_center_ratio <= 0.44, "fullscreen Space marker must use adaptive vertical centering") |
| | | require(abs(marker_mid_y - app_mid_y) <= 2, "fullscreen App label must remain centered in the marker") |
| | | |
| | | print(json.dumps({ |
| | | "displayGroups": [ |
| | | { |
| | | "label": group.get("displayLabel"), |
| | | "spaces": group.get("spaceLabels"), |
| | | "visibleFrame": group.get("visibleFrame") |
| | | } |
| | | for group in groups |
| | | ], |
| | | "actualGroupGap": actual_group_gap, |
| | | "focusedSpace": focused[0].get("label") |
| | | }, indent=2, ensure_ascii=False)) |
| | | PY |
| | | } |
| | | |
| | | trap cleanup EXIT |
| | | |
| | | acquire_lock |
| | |
| | | "$SCRIPT_DIR/package-app.sh" >&2 |
| | | |
| | | FOCUS_REPORT="$REPORT_DIR/round01-three-zone-linkage-focus-report.json" |
| | | APP_HOVER_REPORT="$REPORT_DIR/round01-three-zone-linkage-app-hover-report.json" |
| | | CLEAR_REPORT="$REPORT_DIR/round01-three-zone-linkage-clear-report.json" |
| | | SPACE_CLICK_REPORT="$REPORT_DIR/round01-three-zone-linkage-space-click-report.json" |
| | | REMOTE_REPORT="$REPORT_DIR/round01-three-zone-linkage-remote-report.json" |
| | | SCROLL_REPORT="$REPORT_DIR/round01-three-zone-linkage-scroll-report.json" |
| | | DISPLAY_GROUP_REPORT="$REPORT_DIR/round01-three-zone-linkage-display-group-report.json" |
| | | |
| | | run_report "$FOCUS_REPORT" "click-space:1" "click-space:1" |
| | | run_report "$FOCUS_REPORT" "hover-space:1" "hover-space:1" |
| | | assert_focus_report "$FOCUS_REPORT" |
| | | |
| | | run_report "$REMOTE_REPORT" "click-space:1,click-app:$REMOTE_APP_INDEX" "click-app:$REMOTE_APP_INDEX" |
| | | run_report "$APP_HOVER_REPORT" "hover-app:0" "hover-app:0" |
| | | assert_app_hover_space_lane_report "$APP_HOVER_REPORT" |
| | | |
| | | run_report "$CLEAR_REPORT" "hover-space:1,hover-card:0:0" "hover-card:0:0" |
| | | assert_space_focus_clear_report "$CLEAR_REPORT" |
| | | |
| | | run_report "$SPACE_CLICK_REPORT" "click-space:1" "click-space:1" "$FIXTURE_APP_COUNT" "$FIXTURE_WINDOWS_PER_APP" visible |
| | | assert_space_click_filter_report "$SPACE_CLICK_REPORT" |
| | | |
| | | run_report "$REMOTE_REPORT" "hover-space:1,click-app:$REMOTE_APP_INDEX" "click-app:$REMOTE_APP_INDEX" "$FIXTURE_APP_COUNT" "$FIXTURE_WINDOWS_PER_APP" hidden |
| | | assert_remote_click_report "$REMOTE_REPORT" |
| | | |
| | | run_report "$SCROLL_REPORT" "scroll-app-shelf:99999" "scroll-app-shelf:99999" 60 1 |
| | | assert_app_shelf_scroll_report "$SCROLL_REPORT" |
| | | |
| | | run_report "$DISPLAY_GROUP_REPORT" "hover-space:4" "hover-space:4" 1 1 report-only multi-display |
| | | assert_display_group_report "$DISPLAY_GROUP_REPORT" |
| | | |
| | | swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-no-quick-switch >&2 |
| | | stop_current_aligner |
| | | release_lock |