#!/bin/bash
|
set -euo pipefail
|
|
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
APP_DIR="$ROOT_DIR/Apptag"
|
|
python3 - "$APP_DIR" <<'PY'
|
import json
|
import re
|
import sys
|
from pathlib import Path
|
|
app_dir = Path(sys.argv[1])
|
theme_file = app_dir / "AppGridTheme.swift"
|
preferences_file = app_dir / "PreferencesView.swift"
|
defaults_file = app_dir / "AppDefaults.swift"
|
content_file = app_dir / "ContentView.swift"
|
grid_file = app_dir / "AppGridCollectionView.swift"
|
localization_dir = app_dir / "Localization"
|
|
theme_source = theme_file.read_text(encoding="utf-8")
|
preferences_source = preferences_file.read_text(encoding="utf-8")
|
defaults_source = defaults_file.read_text(encoding="utf-8")
|
content_source = content_file.read_text(encoding="utf-8")
|
grid_source = grid_file.read_text(encoding="utf-8")
|
edit_views_source = (app_dir / "EditModeViews.swift").read_text(encoding="utf-8")
|
|
expected_cases = [
|
"defaultLight",
|
"deepBlue",
|
"black",
|
"pink",
|
"purple",
|
"green",
|
"blue",
|
"colorful",
|
]
|
for case in expected_cases:
|
if not re.search(rf"case\s+{re.escape(case)}\b", theme_source):
|
raise SystemExit(f"missing AppGridTheme case: {case}")
|
|
if "case theme" not in preferences_source:
|
raise SystemExit("SettingsTab is missing the theme tab")
|
if "settings.darkAppGrid" in preferences_source:
|
raise SystemExit("legacy dark grid copy is still referenced by PreferencesView")
|
if "Toggle(tr(\"settings.darkAppGrid\")" in preferences_source:
|
raise SystemExit("legacy dark grid toggle is still visible")
|
if "useDarkAppGrid" in content_source or "useDarkAppGrid" in grid_source:
|
raise SystemExit("legacy dark grid boolean still drives AppGrid rendering")
|
if "CAGradientLayer" in grid_source:
|
raise SystemExit("AppGridCollectionView still owns a background gradient layer")
|
if "useDarkAppGrid" not in defaults_source or "AppGridTheme.deepBlue.rawValue" not in defaults_source:
|
raise SystemExit("legacy dark preference migration is missing")
|
if "var usesVisualEffectBackdrop: Bool" not in theme_source:
|
raise SystemExit("theme visual-effect backdrop policy is missing")
|
if "static let settingsDisplayOrder: [AppGridTheme]" not in theme_source:
|
raise SystemExit("settings theme display order is missing")
|
if "var supportsColorTuning: Bool" not in theme_source:
|
raise SystemExit("colorful theme tuning policy is missing")
|
if "func backgroundBaseColors(tuning: Double?)" not in theme_source or "func backgroundAccentColors(tuning: Double?)" not in theme_source:
|
raise SystemExit("tunable AppGrid background color functions are missing")
|
if "AppGridTheme.settingsDisplayOrder" not in preferences_source:
|
raise SystemExit("Preferences theme grid does not use the curated theme order")
|
if "themeColorTuningSection" not in preferences_source or "Slider(value: themeTuningBinding" not in preferences_source:
|
raise SystemExit("Preferences theme tuning slider is missing")
|
if 'Text(tr("settings.theme"))\n .font(.headline)' in preferences_source:
|
raise SystemExit("Theme content area must not repeat the Theme heading")
|
if 'Text(tr("settings.themeScopeDesc"))' in preferences_source:
|
raise SystemExit("Theme scope copy must be merged into the tuning description, not rendered as a standalone bottom line")
|
if not re.search(r"case\s+\.colorful:\s*return\s+0\b", theme_source):
|
raise SystemExit("Light Colorful default tuning must start at the far left of the slider")
|
if "AppGridTheme.deepBlueTuningStorageKey" not in defaults_source or "AppGridTheme.colorfulTuningStorageKey" not in defaults_source:
|
raise SystemExit("theme tuning defaults are missing")
|
if "static let iconSize: Double = 56" not in defaults_source:
|
raise SystemExit("new-install default icon size must be 56")
|
if 'static let displayMode = "gridContainer"' not in defaults_source:
|
raise SystemExit("new-install default display mode must remain a free mode")
|
if "var isPureBlackBackground: Bool" not in theme_source or "self == .black" not in theme_source:
|
raise SystemExit("pure black background policy is missing")
|
if "case .deepBlue, .black:" not in theme_source or "return true" not in theme_source:
|
raise SystemExit("dark glass must be limited to deepBlue and black")
|
if "case .defaultLight, .pink, .purple, .green, .blue, .colorful:" not in theme_source or "return false" not in theme_source:
|
raise SystemExit("bright themes must keep light glass")
|
if "backgroundDimmingOpacity" not in theme_source or "case .deepBlue:" not in theme_source:
|
raise SystemExit("theme background dimming policy is missing")
|
if "theme.usesVisualEffectBackdrop" not in content_source or "theme.isPureBlackBackground" not in content_source:
|
raise SystemExit("ContentView does not apply backdrop/pure-black theme policy")
|
required_edit_tokens = {
|
"editPrimaryTextColor",
|
"editSecondaryTextColor",
|
"editTertiaryTextColor",
|
"editDividerColor",
|
"editToolbarSurfaceColor",
|
"editControlSurfaceColor",
|
"editControlStrokeColor",
|
"editInactiveIndicatorColor",
|
"editDisabledTextColor",
|
"editDisabledSurfaceColor",
|
"editButtonShadowColor",
|
"editAccentColor",
|
"editConfirmForegroundColor",
|
}
|
missing_tokens = [token for token in sorted(required_edit_tokens) if token not in theme_source]
|
if missing_tokens:
|
raise SystemExit(f"AppGridTheme missing edit-mode contrast tokens: {missing_tokens}")
|
if "private var renderedAppGridTheme: AppGridTheme" not in content_source:
|
raise SystemExit("ContentView is missing the runtime theme override")
|
if not re.search(
|
r"private\s+var\s+renderedAppGridTheme\s*:\s*AppGridTheme\s*\{.*?"
|
r"editPhase\s*==\s*\.none.*?"
|
r"ProEntitlementPolicy\.effectiveTheme\(for:\s*appGridTheme\).*?"
|
r":\s*\.defaultLight",
|
content_source,
|
re.S,
|
):
|
raise SystemExit("edit mode must temporarily render with the default light AppGrid theme")
|
if "let theme = renderedAppGridTheme" not in content_source:
|
raise SystemExit("AppGrid background does not use the runtime rendered theme")
|
if "let tuning = renderedAppGridThemeTuning" not in content_source:
|
raise SystemExit("AppGrid background does not use runtime theme tuning")
|
if "AppGridCollectionView(" in content_source and "appGridTheme: renderedAppGridTheme" not in content_source:
|
raise SystemExit("AppGridCollectionView is not passed the runtime rendered theme")
|
if "EditAppsHeaderView(" in content_source and "theme: renderedAppGridTheme" not in content_source:
|
raise SystemExit("edit apps header is not passed the runtime rendered theme")
|
if "theme: renderedAppGridTheme" not in content_source:
|
raise SystemExit("edit mode selectable controls are not passed the runtime rendered theme")
|
for view_name in ["EditAppsHeaderView", "EditOperationPicker", "EditConfirmButton", "EditAppsSidebarIntroView", "EditSelectableTagItem", "EditableAppSelectionItem"]:
|
if f"let theme: AppGridTheme" not in edit_views_source:
|
raise SystemExit("edit mode views do not accept AppGridTheme")
|
if view_name not in edit_views_source:
|
raise SystemExit(f"missing edit mode view: {view_name}")
|
for token in ["theme.editPrimaryTextColor", "theme.editAccentColor", "theme.editControlSurfaceColor", "theme.editDisabledSurfaceColor"]:
|
if token not in edit_views_source:
|
raise SystemExit(f"edit mode views do not use contrast token: {token}")
|
if ".buttonStyle(.bordered)" in edit_views_source:
|
raise SystemExit("edit mode views still rely on default bordered button styling")
|
if ".buttonStyle(.bordered)" in content_source and "editTagsView" in content_source:
|
raise SystemExit("ContentView still relies on default bordered button styling in edit mode")
|
|
required_keys = {
|
"settings.theme",
|
"settings.themeDesc",
|
"settings.themeScopeDesc",
|
"theme.tuning.dark",
|
"theme.tuning.light",
|
"theme.tuning.desc",
|
"theme.default",
|
"theme.deepBlue",
|
"theme.black",
|
"theme.pink",
|
"theme.purple",
|
"theme.green",
|
"theme.blue",
|
"theme.colorful",
|
}
|
files = sorted(localization_dir.glob("*.json"))
|
if len(files) != 29:
|
raise SystemExit(f"expected 29 localization files, found {len(files)}")
|
for path in files:
|
data = json.loads(path.read_text(encoding="utf-8"))
|
legacy = {"settings.darkAppGrid", "settings.darkAppGridDesc"} & set(data)
|
if legacy:
|
raise SystemExit(f"{path.name} still has legacy dark grid keys: {sorted(legacy)}")
|
missing = required_keys - set(data)
|
if missing:
|
raise SystemExit(f"{path.name} missing keys: {sorted(missing)}")
|
raw_values = [
|
key for key in required_keys
|
if str(data.get(key, "")).startswith("settings.") or str(data.get(key, "")).startswith("theme.")
|
]
|
if raw_values:
|
raise SystemExit(f"{path.name} has raw key values: {raw_values}")
|
if data["settings.themeScopeDesc"].strip() not in data["theme.tuning.desc"]:
|
raise SystemExit(f"{path.name} theme tuning description must include the theme scope copy")
|
|
print("PASS theme settings QA: 8 themes, Theme tab, migration, AppGrid-only rendering, glass policy, edit-mode default-theme override, and 29-language keys are present")
|
PY
|