Ariver
2026-06-13 18a0fe3977175f3f7a945b300524fd2cd37fef28
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#!/usr/bin/env bash
set -euo pipefail
 
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CONTENT_VIEW_SWIFT="$ROOT_DIR/Apptag/ContentView.swift"
APP_GRID_SWIFT="$ROOT_DIR/Apptag/AppGridCollectionView.swift"
PREFERENCES_VIEW_SWIFT="$ROOT_DIR/Apptag/PreferencesView.swift"
APP_DEFAULTS_SWIFT="$ROOT_DIR/Apptag/AppDefaults.swift"
LOCALIZATION_DIR="$ROOT_DIR/Apptag/Localization"
 
fail() {
  printf 'FAIL: %s\n' "$*" >&2
  exit 1
}
 
[[ -f "$CONTENT_VIEW_SWIFT" ]] || fail "missing ContentView.swift"
[[ -f "$APP_GRID_SWIFT" ]] || fail "missing AppGridCollectionView.swift"
[[ -f "$PREFERENCES_VIEW_SWIFT" ]] || fail "missing PreferencesView.swift"
[[ -f "$APP_DEFAULTS_SWIFT" ]] || fail "missing AppDefaults.swift"
[[ -d "$LOCALIZATION_DIR" ]] || fail "missing Localization directory"
 
python3 - "$CONTENT_VIEW_SWIFT" "$APP_GRID_SWIFT" "$PREFERENCES_VIEW_SWIFT" "$APP_DEFAULTS_SWIFT" "$LOCALIZATION_DIR" <<'PY'
import json
import pathlib
import re
import sys
 
content_view = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")
app_grid = pathlib.Path(sys.argv[2]).read_text(encoding="utf-8")
preferences = pathlib.Path(sys.argv[3]).read_text(encoding="utf-8")
defaults = pathlib.Path(sys.argv[4]).read_text(encoding="utf-8")
localization_dir = pathlib.Path(sys.argv[5])
 
required_keys = [
    "settings.hideUsageTips",
    "settings.hideUsageTipsDesc",
    "usageTips.previous",
    "usageTips.next",
]
for index in range(1, 9):
    required_keys.append(f"usageTips.tip{index}.title")
    required_keys.append(f"usageTips.tip{index}.detail")
 
 
def fail(message: str) -> None:
    print(f"FAIL: {message}", file=sys.stderr)
    sys.exit(1)
 
 
def require(pattern: str, source: str, message: str) -> None:
    if not re.search(pattern, source, re.S):
        fail(message)
 
 
require(
    r"static\s+let\s+hideUsageTips\s*=\s*false",
    defaults,
    "usage tips must default to visible for existing and new users",
)
require(
    r'"hideUsageTips"\s*:\s*hideUsageTips',
    defaults,
    "AppDefaults.register must register hideUsageTips",
)
require(
    r'@AppStorage\("hideUsageTips"\)\s+private\s+var\s+hideUsageTips\s*=\s*AppDefaults\.hideUsageTips',
    content_view,
    "ContentView must observe hideUsageTips with AppStorage",
)
require(
    r'@AppStorage\("hideUsageTips"\)\s+private\s+var\s+hideUsageTips\s*=\s*AppDefaults\.hideUsageTips',
    preferences,
    "PreferencesView must persist hideUsageTips with AppStorage",
)
require(
    r'Toggle\s*\(\s*tr\("settings\.hideUsageTips"\)\s*,\s*isOn\s*:\s*\$hideUsageTips\s*\)',
    preferences,
    "General settings must expose a Hide usage tips toggle",
)
require(
    r"\bprivate\s+struct\s+AppGridUsageTipsBar\s*:\s*View\b",
    content_view,
    "ContentView must define the app grid usage tips overlay",
)
require(
    r"static\s+let\s+reservedHeight\s*:\s*CGFloat\s*=\s*64",
    content_view,
    "Usage tips overlay must reserve stable bottom space",
)
require(
    r"ScrollView\s*\(\s*\.horizontal\s*,\s*showsIndicators\s*:\s*textHovered\s*\)",
    content_view,
    "Long localized tip text must be horizontally scrollable on hover",
)
require(
    r"\.onHover\s*\{\s*textHovered\s*=\s*\$0\s*\}",
    content_view,
    "Usage tip text must react to hover so long text can be inspected",
)
require(
    r"bottomContentPadding\s*:\s*shouldShowUsageTips\s*\?\s*AppGridUsageTipsBar\.reservedHeight\s*:\s*0",
    content_view,
    "AppGrid must reserve bottom content space when tips are visible",
)
require(
    r"\.allowsHitTesting\s*\(\s*!dragModeActive\s*\)",
    content_view,
    "Usage tips overlay must stop intercepting drag/drop while app drag mode is active",
)
require(
    r"let\s+bottomContentPadding\s*:\s*CGFloat",
    app_grid,
    "AppGridCollectionView must accept bottom content padding",
)
if '"bottom=\\(Int(bottomContentPadding.rounded()))"' not in app_grid:
    fail("AppGrid layout signature must include bottom content padding")
require(
    r"\+\s*max\s*\(\s*0\s*,\s*bottomContentPadding\s*\)",
    app_grid,
    "AppGrid layout plans must add bottom content padding to content height",
)
 
tip_ids = [int(value) for value in re.findall(r"AppGridUsageTip\s*\(\s*id\s*:\s*(\d+)", content_view)]
if tip_ids != list(range(1, 9)):
    fail(f"usage tip IDs must be exactly 1 through 8, got {tip_ids}")
if "usageTips.tip0" in content_view:
    fail("usage tips must not expose a zero-based tip number")
 
for source_name, source in {
    "ContentView.swift": content_view,
    "AppGridCollectionView.swift": app_grid,
    "PreferencesView.swift": preferences,
}.items():
    if re.search(r"#available\s*\(\s*macOS\s+(?:15|26)", source):
        fail(f"{source_name} must not rely on macOS 15/26 availability for usage tips")
    if "scrollClipDisabled" in source:
        fail(f"{source_name} must avoid newer scrollClipDisabled behavior for macOS 14 compatibility")
 
json_files = sorted(localization_dir.glob("*.json"))
if len(json_files) != 29:
    fail(f"expected 29 localization files, found {len(json_files)}")
 
for path in json_files:
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except Exception as exc:
        fail(f"{path.name} is not valid JSON: {exc}")
    missing = [key for key in required_keys if not data.get(key)]
    if missing:
        fail(f"{path.name} missing usage tip localization keys: {', '.join(missing)}")
 
print("PASS usage tips QA: overlay, hide setting, macOS 14-safe implementation, and 29-language localizations are present")
PY