From 6a49b26ff3ef989844673cb77d2700404242c424 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Tue, 09 Jun 2026 20:58:01 +0800
Subject: [PATCH] Release 0.0.48 verification candidate
---
C3.tools/round1-three-zone-linkage-fixture-qa.sh | 289 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 files changed, 274 insertions(+), 15 deletions(-)
diff --git a/C3.tools/round1-three-zone-linkage-fixture-qa.sh b/C3.tools/round1-three-zone-linkage-fixture-qa.sh
index 4fcd850..93a8bc2 100755
--- a/C3.tools/round1-three-zone-linkage-fixture-qa.sh
+++ b/C3.tools/round1-three-zone-linkage-fixture-qa.sh
@@ -122,26 +122,34 @@
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-window-activation \
- --round01-debug-mouse-sequence="$sequence" \
- --round01-quick-switch-report="$report" &
+ "$APP/Contents/MacOS/Aligner" "${args[@]}" &
APP_PID=$!
wait_for_report "$report" "$expected_last_mouse"
if [ "$quick_switch_expectation" = "hidden" ]; then
swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-no-quick-switch >&2
- else
+ 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
@@ -186,10 +194,16 @@
require(len(focused_segments) == 1, "exactly one Space Lane segment must be focused")
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")
@@ -201,10 +215,28 @@
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))
@@ -234,7 +266,8 @@
last_index = fixture_app_count - 1
require(report.get("quickSwitchVisible") is False, "remote App click must close Quick Switch after committing its first window")
-require(root.get("activeSpaceFocusID") == 1, "Space hover preview should still be reflected in the pre-commit report")
+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")
@@ -245,12 +278,14 @@
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"),
@@ -307,6 +342,128 @@
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
+}
+
assert_app_shelf_scroll_report() {
/usr/bin/python3 - "$1" "$1" <<'PY'
import json
@@ -347,6 +504,96 @@
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
+ )
+
+root = report.get("rootView", {})
+groups = root.get("spaceLaneDisplayGroups", [])
+segments = root.get("spaceLaneSegments", [])
+
+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")
+
+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", [])
+ 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")
+
+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")
+
+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
@@ -354,12 +601,21 @@
"$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" "hover-space:1" "hover-space:1"
assert_focus_report "$FOCUS_REPORT"
+
+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" hidden
assert_space_click_report "$SPACE_CLICK_REPORT"
@@ -370,6 +626,9 @@
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" 6 3 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
--
Gitblit v1.9.3