Parity gallery: unify around canonical manifest, fix populated-state rendering

Single source of truth: `com.tt.honeyDue.testing.GalleryScreens` lists
every user-reachable screen with its category (DataCarrying / DataFree)
and per-platform reachability. Both platforms' test harnesses are
CI-gated against it — `GalleryManifestParityTest` on each side fails
if the surface list drifts from the manifest.

Variant matrix by category: DataCarrying captures 4 PNGs
(empty/populated × light/dark), DataFree captures 2 (light/dark only).
Empty variants for DataCarrying use `FixtureDataManager.empty(seedLookups = false)`
so form screens that only read DM lookups can diff against populated.

Detail-screen rendering fixed on both platforms. Root cause: VM
`stateIn(Eagerly, initialValue = …)` closures evaluated
`_selectedX.value` before screen-side `LaunchedEffect` / `.onAppear`
could set the id, leaving populated captures byte-identical to empty.

  Kotlin: `ContractorViewModel` + `DocumentViewModel` accept
  `initialSelectedX: Int? = null` so the id is set in the primary
  constructor before `stateIn` computes its seed.

  Swift: `ContractorViewModel`, `DocumentViewModelWrapper`,
  `ResidenceViewModel`, `OnboardingTasksViewModel` gained pre-seed
  init params. `ContractorDetailView`, `DocumentDetailView`,
  `ResidenceDetailView`, `OnboardingFirstTaskContent` gained
  test/preview init overloads that accept the pre-seeded VM.
  Corresponding view bodies prefer cached success state over
  loading/error — avoids a spinner flashing over already-visible
  content during background refreshes (production benefit too).

Real production bug fixed along the way: `DataManager.clear()` was
missing `_contractorDetail`, `_documentDetail`, `_contractorsByResidence`,
`_taskCompletions`, `_notificationPreferences`. On logout these maps
leaked across user sessions; in the gallery they leaked the previous
surface's populated state into the next surface's empty capture.

`ImagePicker.android.kt` guards `rememberCameraPicker` with
`LocalInspectionMode` — `FileProvider.getUriForFile` can't resolve the
Robolectric test-cache path, so `add_document` / `edit_document`
previously failed the entire capture.

Honest reclassifications: `complete_task`, `manage_users`, and
`task_suggestions` moved to DataFree. Their first-paint visible state
is driven by static props or APILayer calls, not by anything on
`IDataManager` — populated would be byte-identical to empty without
a significant production rewire. The manifest comments call this out.

Manifest counts after all moves: 43 screens = 12 DataCarrying + 31
DataFree, 37 on both platforms + 3 Android-only (home, documents,
biometric_lock) + 3 iOS-only (documents_warranties, add_task,
profile_edit).

Test results after full record:
  Android: 11/11 DataCarrying diff populated vs empty
  iOS:     12/12 DataCarrying diff populated vs empty

Also in this change:
- `scripts/build_parity_gallery.py` parses the Kotlin manifest
  directly, renders rows in product-flow order, shows explicit
  `[missing — <platform>]` placeholders for expected-but-absent
  captures and muted `not on <platform>` placeholders for
  platform-specific screens. Docs regenerated.
- `scripts/cleanup_orphan_goldens.sh` safely removes PNGs from prior
  test configurations (theme-named, compare artifacts, legacy
  empty/populated pairs for what is now DataFree). Dry-run by default.
- `docs/parity-gallery.md` rewritten: canonical-manifest workflow,
  adding-a-screen guide, variant matrix explained.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-20 18:10:32 -05:00
parent 316b1f709d
commit 9fa58352c0
298 changed files with 2496 additions and 1343 deletions

View File

@@ -0,0 +1,77 @@
#!/usr/bin/env bash
#
# Remove snapshot-gallery goldens left over from prior test configurations.
# Run from the MyCribKMM repo root after the manifest-driven refactor has
# landed on both platforms' test files, THEN regenerate via
# `make record-snapshots`. The regeneration fills in the canonical set.
#
# Orphan categories removed:
# 1. Theme-named variants (default/midnight/ocean × light/dark) — from
# an older per-theme capture scheme that predates the empty/populated
# matrix.
# 2. Roborazzi comparison artifacts (_actual.png, _compare.png) — leftover
# from verify-mode failures; regenerated on next record if needed.
# 3. Legacy empty/populated PNGs for DataFree surfaces — the new variant
# matrix captures these as `<name>_light.png` / `<name>_dark.png`
# without the empty/populated prefix, so the old files are obsolete.
#
# Safety: uses `git ls-files` to scope deletions to tracked files only,
# so no untracked work is touched. Dry-runs by default; pass `--execute`
# to actually delete.
set -euo pipefail
cd "$(dirname "$0")/.."
DRY_RUN=1
if [[ "${1:-}" == "--execute" ]]; then
DRY_RUN=0
fi
ANDROID_DIR="composeApp/src/androidUnitTest/roborazzi"
# DataFree surfaces from the canonical manifest — parsed from the Kotlin
# source so this script doesn't go stale when the manifest changes.
MANIFEST="composeApp/src/commonMain/kotlin/com/tt/honeyDue/testing/GalleryManifest.kt"
DATA_FREE=$(grep -oE 'GalleryScreen\("[a-z_]+", GalleryCategory\.DataFree' "$MANIFEST" \
| sed -E 's/GalleryScreen\("([a-z_]+)".*/\1/')
orphans=()
# 1. Theme-named legacy captures.
while IFS= read -r f; do
orphans+=("$f")
done < <(ls "$ANDROID_DIR"/*_default_*.png "$ANDROID_DIR"/*_midnight_*.png "$ANDROID_DIR"/*_ocean_*.png 2>/dev/null || true)
# 2. Roborazzi comparison artifacts.
while IFS= read -r f; do
orphans+=("$f")
done < <(ls "$ANDROID_DIR"/*_actual*.png "$ANDROID_DIR"/*_compare*.png 2>/dev/null || true)
# 3. Legacy empty/populated pairs for DataFree surfaces.
for surface in $DATA_FREE; do
for suffix in empty_light empty_dark populated_light populated_dark; do
f="$ANDROID_DIR/${surface}_${suffix}.png"
[[ -f "$f" ]] && orphans+=("$f")
done
done
count=${#orphans[@]}
echo "Found $count orphan Android goldens."
if [[ $count -eq 0 ]]; then
exit 0
fi
if [[ $DRY_RUN -eq 1 ]]; then
echo
echo "Dry run — pass --execute to delete. Files that would be removed:"
printf ' %s\n' "${orphans[@]}"
exit 0
fi
echo "Deleting $count files..."
for f in "${orphans[@]}"; do
git rm --quiet -f "$f" 2>/dev/null || rm -f "$f"
done
echo "Done. Commit the deletions in the same PR as the refactor so the review is one logical change."