| | |
| | | #!/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 |
| | | |
| | |
| | | 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}" |
| | |
| | | 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") |
| | |
| | | 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") |
| | |
| | | "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 |
| | | |
| | |
| | | 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 |
| | |
| | | |
| | | 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) |
| | |
| | | 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 |
| | |
| | | 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 \ |
| | |
| | | --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 |
| | | } |
| | |
| | | |
| | | 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" |