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:
@@ -28,12 +28,20 @@ object FixtureDataManager {
|
||||
|
||||
/**
|
||||
* Data-free fixture — represents a freshly-signed-in user with no
|
||||
* residences, no tasks, no contractors, no documents. Lookups
|
||||
* (priorities, categories, frequencies) are still populated because
|
||||
* empty-state form pickers render them even before the user has any
|
||||
* entities of their own.
|
||||
* residences, no tasks, no contractors, no documents.
|
||||
*
|
||||
* @param seedLookups When `true` (the default), lookups (priorities,
|
||||
* categories, frequencies, residence types, contractor specialties,
|
||||
* task templates) are populated. This matches product behaviour —
|
||||
* a user with zero residences still sees the priority picker in
|
||||
* every form.
|
||||
*
|
||||
* When `false`, lookups are empty too. Use this for snapshot tests
|
||||
* that want the `empty` variant of a form to render empty dropdowns
|
||||
* (so populated vs. empty PNGs diff for form screens). The parity
|
||||
* gallery's empty variant passes `seedLookups = false`.
|
||||
*/
|
||||
fun empty(): IDataManager = InMemoryDataManager(
|
||||
fun empty(seedLookups: Boolean = true): IDataManager = InMemoryDataManager(
|
||||
currentUser = null,
|
||||
residences = emptyList(),
|
||||
myResidencesResponse = null,
|
||||
@@ -48,13 +56,13 @@ object FixtureDataManager {
|
||||
upgradeTriggers = emptyMap(),
|
||||
featureBenefits = Fixtures.featureBenefits,
|
||||
promotions = emptyList(),
|
||||
residenceTypes = Fixtures.residenceTypes,
|
||||
taskFrequencies = Fixtures.taskFrequencies,
|
||||
taskPriorities = Fixtures.taskPriorities,
|
||||
taskCategories = Fixtures.taskCategories,
|
||||
contractorSpecialties = Fixtures.contractorSpecialties,
|
||||
taskTemplates = Fixtures.taskTemplates,
|
||||
taskTemplatesGrouped = Fixtures.taskTemplatesGrouped,
|
||||
residenceTypes = if (seedLookups) Fixtures.residenceTypes else emptyList(),
|
||||
taskFrequencies = if (seedLookups) Fixtures.taskFrequencies else emptyList(),
|
||||
taskPriorities = if (seedLookups) Fixtures.taskPriorities else emptyList(),
|
||||
taskCategories = if (seedLookups) Fixtures.taskCategories else emptyList(),
|
||||
contractorSpecialties = if (seedLookups) Fixtures.contractorSpecialties else emptyList(),
|
||||
taskTemplates = if (seedLookups) Fixtures.taskTemplates else emptyList(),
|
||||
taskTemplatesGrouped = if (seedLookups) Fixtures.taskTemplatesGrouped else null,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user