Ariver
2026-06-14 4d4294f8c375a06ad87e12a2c73a98a19eb7ed7e
C3.tools/round1-view-mode-entry-qa.sh
@@ -1,7 +1,7 @@
#!/bin/bash
# Round01.1 view-mode entry QA. It verifies that the installed-app path exposes
# a discoverable status-menu entry for switching Quick Switch Waterfall view
# modes and reports visible About version/build metadata.
# menu entries plus the in-panel Quick Switch view-mode toggle for switching
# Waterfall view modes, and reports visible About version/build metadata.
set -euo pipefail
@@ -12,7 +12,8 @@
APP="$BUILD_CURRENT_APP"
REPORT_VERTICAL="$BUILD_REPORT_ROOT/round01-view-mode-entry-vertical-report.json"
REPORT_HORIZONTAL="$BUILD_REPORT_ROOT/round01-view-mode-entry-horizontal-report.json"
REPORT_SETTINGS="$BUILD_REPORT_ROOT/round01-view-mode-entry-settings-report.json"
REPORT_TOGGLE_TO_HORIZONTAL="$BUILD_REPORT_ROOT/round01-view-mode-entry-toggle-to-horizontal-report.json"
REPORT_TOGGLE_TO_VERTICAL="$BUILD_REPORT_ROOT/round01-view-mode-entry-toggle-to-vertical-report.json"
DOMAIN="com.ar.Aligner"
WATERFALL_MODE_KEY="com.ar.Aligner.preferences.quickSwitch.waterfallViewMode"
REPORT_WAIT="${ALIGNER_ROUND1_VIEW_MODE_ENTRY_REPORT_WAIT:-6.0}"
@@ -135,6 +136,9 @@
main_menu = report.get("mainMenu", {})
settings_window = report.get("settingsWindow", {})
root = report.get("rootView", {})
expected_target_mode = "verticalColumns" if expected_mode == "horizontalMasonry" else "horizontalMasonry"
expected_icon_kind = "verticalColumnsTarget" if expected_target_mode == "verticalColumns" else "horizontalMasonryTarget"
expected_accessibility_label = "切换到纵栏瀑布" if expected_target_mode == "verticalColumns" else "切换到横栏瀑布"
require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true")
require(report.get("quickSwitchVisible") is True, "Quick Switch must be visible during entry QA")
@@ -154,10 +158,15 @@
require(main_menu.get("settingsKeyEquivalent") == ",", "Settings must use Cmd+, key equivalent")
require(main_menu.get("hasQuickSwitchViewSubmenuInAppMenu") is True, "app menu must expose Quick Switch View submenu")
require(main_menu.get("hasQuickSwitchViewSubmenuInViewMenu") is True, "View menu must expose Quick Switch View submenu")
require(root.get("settingsButtonVisible") is True, "Quick Switch must expose a top-right Settings button")
settings_frame = root.get("settingsButtonFrame", {})
require(settings_frame.get("width", 0) >= 30 and settings_frame.get("height", 0) >= 30, "Settings button must have tappable size")
require(settings_frame.get("x", 0) + settings_frame.get("width", 0) > root.get("bounds", {}).get("width", 0) * 0.90, "Settings button must sit near the top-right edge")
require(root.get("settingsButtonVisible") is False, "Quick Switch must not expose the old top-right Settings button")
require(root.get("viewModeToggleButtonVisible") is True, "Quick Switch must expose a top-right view-mode toggle button")
toggle_frame = root.get("viewModeToggleButtonFrame", {})
require(toggle_frame.get("width", 0) >= 30 and toggle_frame.get("height", 0) >= 30, "view-mode toggle button must have tappable size")
require(toggle_frame.get("x", 0) + toggle_frame.get("width", 0) > root.get("bounds", {}).get("width", 0) * 0.90, "view-mode toggle button must sit near the top-right edge")
require(root.get("viewModeToggleCurrentMode") == expected_mode, "view-mode toggle must report the current mode")
require(root.get("viewModeToggleTargetMode") == expected_target_mode, "view-mode toggle must report the next target mode")
require(root.get("viewModeToggleIconKind") == expected_icon_kind, "view-mode toggle icon kind must represent the target mode")
require(root.get("viewModeToggleAccessibilityLabel") == expected_accessibility_label, "view-mode toggle label must name the target mode")
require(settings_window.get("visible") is False, "Settings window should not be open in passive view-mode entry cases")
require(report.get("aboutVersion") == app_version, "About version must match app Info.plist")
require(report.get("aboutBuild") == app_build, "About build must match app Info.plist")
@@ -170,21 +179,25 @@
    "aboutBuild": report.get("aboutBuild"),
    "statusMenu": status_menu,
    "mainMenu": main_menu,
    "settingsButtonFrame": settings_frame
    "viewModeToggleButtonFrame": toggle_frame,
    "viewModeToggleTargetMode": root.get("viewModeToggleTargetMode"),
    "viewModeToggleIconKind": root.get("viewModeToggleIconKind")
}, indent=2, ensure_ascii=False))
PY
}
wait_for_settings_report() {
wait_for_toggle_report() {
  local report="$1"
  local expected_mode="$2"
  /usr/bin/python3 - "$report" "$REPORT_WAIT" <<'PY'
  /usr/bin/python3 - "$report" "$expected_mode" "$REPORT_WAIT" <<'PY'
import json
import sys
import time
path = sys.argv[1]
timeout = float(sys.argv[2])
expected_mode = sys.argv[2]
timeout = float(sys.argv[3])
deadline = time.monotonic() + timeout
last_report = None
@@ -193,7 +206,14 @@
        with open(path, "r", encoding="utf-8") as file:
            report = json.load(file)
        last_report = report
        if report.get("settingsWindow", {}).get("visible") is True:
        root = report.get("rootView", {})
        if (
            report.get("snapshotLoaded") is True
            and report.get("quickSwitchVisible") is True
            and report.get("resolvedWaterfallViewMode") == expected_mode
            and root.get("waterfallViewMode") == expected_mode
            and root.get("lastMouseCommand") == "click-view-toggle"
        ):
            sys.exit(0)
    except FileNotFoundError:
        pass
@@ -203,23 +223,31 @@
if last_report is not None:
    print(json.dumps(last_report, indent=2, ensure_ascii=False), file=sys.stderr)
print(f"settings report did not become visible within {timeout:.1f}s", file=sys.stderr)
print(f"view-mode toggle report did not reach {expected_mode} within {timeout:.1f}s", file=sys.stderr)
sys.exit(1)
PY
}
assert_settings_report() {
assert_toggle_report() {
  local report="$1"
  local app_version="$2"
  local app_build="$3"
  local initial_mode="$2"
  local expected_mode="$3"
  local expected_vertical_state="$4"
  local expected_horizontal_state="$5"
  local app_version="$6"
  local app_build="$7"
  /usr/bin/python3 - "$report" "$app_version" "$app_build" <<'PY'
  /usr/bin/python3 - "$report" "$initial_mode" "$expected_mode" "$expected_vertical_state" "$expected_horizontal_state" "$app_version" "$app_build" <<'PY'
import json
import sys
path = sys.argv[1]
app_version = sys.argv[2]
app_build = sys.argv[3]
initial_mode = sys.argv[2]
expected_mode = sys.argv[3]
expected_vertical_state = sys.argv[4]
expected_horizontal_state = sys.argv[5]
app_version = sys.argv[6]
app_build = sys.argv[7]
with open(path, "r", encoding="utf-8") as file:
    report = json.load(file)
@@ -231,30 +259,40 @@
        sys.exit(1)
root = report.get("rootView", {})
status_menu = report.get("statusMenu", {})
main_menu = report.get("mainMenu", {})
settings_window = report.get("settingsWindow", {})
expected_icon_kind = "verticalColumnsTarget" if initial_mode == "verticalColumns" else "horizontalMasonryTarget"
expected_label = "切换到纵栏瀑布" if initial_mode == "verticalColumns" else "切换到横栏瀑布"
require(report.get("snapshotLoaded") is True, "settings case must load the snapshot before clicking the gear")
require(report.get("quickSwitchVisible") is False, "clicking the Settings gear must close Quick Switch")
require(root.get("lastMouseCommand") == "click-settings", "debug mouse sequence must click the Settings gear")
require(root.get("settingsButtonVisible") is True, "Quick Switch root must report the Settings gear")
require(report.get("snapshotLoaded") is True, "toggle case must load the snapshot before clicking the view-mode toggle")
require(report.get("quickSwitchVisible") is True, "clicking the view-mode toggle must keep Quick Switch visible")
require(report.get("preferenceWaterfallViewMode") == expected_mode, "preference Waterfall mode must switch after toggle")
require(report.get("resolvedWaterfallViewMode") == expected_mode, "resolved Waterfall mode must switch after toggle")
require(root.get("waterfallViewMode") == expected_mode, "root view Waterfall mode must switch after toggle")
require(root.get("lastMouseCommand") == "click-view-toggle", "debug mouse sequence must click the view-mode toggle")
require(root.get("settingsButtonVisible") is False, "old Settings button must stay hidden")
require(root.get("viewModeToggleButtonVisible") is True, "Quick Switch root must report the view-mode toggle")
require(root.get("viewModeToggleCurrentMode") == expected_mode, "toggle current mode must update after click")
require(root.get("viewModeToggleTargetMode") == initial_mode, "toggle target mode must flip back to the initial mode after click")
require(root.get("viewModeToggleIconKind") == expected_icon_kind, "toggle icon must represent the next target mode after click")
require(root.get("viewModeToggleAccessibilityLabel") == expected_label, "toggle label must represent the next target mode after click")
require(status_menu.get("verticalColumnsState") == expected_vertical_state, "Vertical Columns menu state must update after toggle")
require(status_menu.get("horizontalWaterfallState") == expected_horizontal_state, "Horizontal Waterfall menu state must update after toggle")
require(main_menu.get("hasSettingsItem") is True, "main menu must expose Settings")
require(main_menu.get("settingsKeyEquivalent") == ",", "Settings must use Cmd+, key equivalent")
require(settings_window.get("exists") is True, "Settings window controller must be created")
require(settings_window.get("visible") is True, "Settings window must be visible after clicking gear")
require(settings_window.get("selectedTabLabel") == "视图", "Settings must open on the View tab")
require(settings_window.get("selectedTabIdentifier") == "view", "Settings View tab identifier must be stable")
require(settings_window.get("hasViewTab") is True, "Settings window must expose View tab")
require(settings_window.get("usesSwiftUI") is False, "Settings window must not use SwiftUI")
require(settings_window.get("waterfallViewMode") == "verticalColumns", "Settings must read the current Waterfall view mode")
require(settings_window.get("visible") is False, "view-mode toggle must not open Settings")
require(report.get("aboutVersion") == app_version, "About version must match app Info.plist")
require(report.get("aboutBuild") == app_build, "About build must match app Info.plist")
print(json.dumps({
    "case": "settings-entry",
    "case": "view-mode-toggle",
    "initialMode": initial_mode,
    "expectedMode": expected_mode,
    "quickSwitchVisible": report.get("quickSwitchVisible"),
    "lastMouseCommand": root.get("lastMouseCommand"),
    "settingsWindow": settings_window,
    "viewModeToggleIconKind": root.get("viewModeToggleIconKind"),
    "statusMenu": status_menu,
    "mainMenu": main_menu
}, indent=2, ensure_ascii=False))
PY
@@ -288,14 +326,18 @@
  stop_current_aligner
}
run_settings_case() {
  local report="$1"
  local app_version="$2"
  local app_build="$3"
run_toggle_case() {
  local initial_mode="$1"
  local expected_mode="$2"
  local report="$3"
  local vertical_state="$4"
  local horizontal_state="$5"
  local app_version="$6"
  local app_build="$7"
  stop_current_aligner
  rm -f "$report"
  /usr/bin/defaults write "$DOMAIN" "$WATERFALL_MODE_KEY" -string "verticalColumns"
  /usr/bin/defaults write "$DOMAIN" "$WATERFALL_MODE_KEY" -string "$initial_mode"
  "$APP/Contents/MacOS/Aligner" \
    --round0-skip-permissions \
@@ -303,12 +345,12 @@
    --round01-fixture-app-count=4 \
    --round01-fixture-windows-per-app=2 \
    --round01-disable-screenshot-refresh \
    --round01-debug-mouse-sequence="click-settings" \
    --round01-debug-mouse-sequence="click-view-toggle" \
    --round01-quick-switch-report="$report" \
    --round01-quick-switch-quit-after=1.2 &
  wait_for_settings_report "$report"
  assert_settings_report "$report" "$app_version" "$app_build"
  wait_for_toggle_report "$report" "$expected_mode"
  assert_toggle_report "$report" "$initial_mode" "$expected_mode" "$vertical_state" "$horizontal_state" "$app_version" "$app_build"
  wait "$!" || true
  stop_current_aligner
}
@@ -325,6 +367,7 @@
run_case "verticalColumns" "$REPORT_VERTICAL" "on" "off" "$APP_VERSION" "$APP_BUILD"
run_case "horizontalMasonry" "$REPORT_HORIZONTAL" "off" "on" "$APP_VERSION" "$APP_BUILD"
run_settings_case "$REPORT_SETTINGS" "$APP_VERSION" "$APP_BUILD"
run_toggle_case "verticalColumns" "horizontalMasonry" "$REPORT_TOGGLE_TO_HORIZONTAL" "off" "on" "$APP_VERSION" "$APP_BUILD"
run_toggle_case "horizontalMasonry" "verticalColumns" "$REPORT_TOGGLE_TO_VERTICAL" "on" "off" "$APP_VERSION" "$APP_BUILD"
echo "Round01.1 view-mode entry QA passed"