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>
96 lines
3.9 KiB
Swift
96 lines
3.9 KiB
Swift
//
|
|
// GalleryManifestParityTest.swift
|
|
// HoneyDueTests
|
|
//
|
|
// Parity gate — asserts `SnapshotGalleryTests.swift`'s covered-screen
|
|
// set matches exactly the subset of screens in the canonical
|
|
// `GalleryScreens` manifest (in
|
|
// `composeApp/src/commonMain/.../testing/GalleryManifest.kt`) with
|
|
// `Platform.IOS` in their `platforms`.
|
|
//
|
|
// If this fails, either:
|
|
// - A new `test_<name>()` was added to `SnapshotGalleryTests.swift`
|
|
// but the name isn't in the canonical manifest — add it to
|
|
// `GalleryScreens.all`.
|
|
// - A new screen was added to the manifest but there's no matching
|
|
// `test_<name>()` function in the Swift test file — write one.
|
|
// - A rename landed on only one side — reconcile.
|
|
//
|
|
// Together with the Android `GalleryManifestParityTest`, this keeps
|
|
// the two platforms from silently drifting apart in coverage.
|
|
//
|
|
|
|
import XCTest
|
|
import ComposeApp
|
|
|
|
@MainActor
|
|
final class GalleryManifestParityTest: XCTestCase {
|
|
|
|
/// Canonical names of every surface covered by
|
|
/// `SnapshotGalleryTests.swift`. This must be updated whenever a
|
|
/// new `test_<name>()` is added to the suite. The parity assertion
|
|
/// below catches a missed update.
|
|
///
|
|
/// Using a hand-maintained list (rather than runtime introspection
|
|
/// of `XCTestCase` selectors) keeps the contract explicit and makes
|
|
/// drifts obvious in a diff.
|
|
private static let iosCoveredScreens: Set<String> = [
|
|
// Auth
|
|
"login", "register", "forgot_password", "verify_reset_code",
|
|
"reset_password", "verify_email",
|
|
// Onboarding
|
|
"onboarding_welcome", "onboarding_value_props",
|
|
"onboarding_create_account", "onboarding_verify_email",
|
|
"onboarding_location", "onboarding_name_residence",
|
|
"onboarding_home_profile", "onboarding_join_residence",
|
|
"onboarding_first_task", "onboarding_subscription",
|
|
// Residences
|
|
"residences", "residence_detail", "add_residence",
|
|
"edit_residence", "join_residence", "manage_users",
|
|
// Tasks
|
|
"all_tasks", "add_task", "add_task_with_residence",
|
|
"edit_task", "complete_task", "task_suggestions",
|
|
"task_templates_browser",
|
|
// Contractors
|
|
"contractors", "contractor_detail",
|
|
// Documents
|
|
"documents_warranties", "document_detail", "add_document",
|
|
"edit_document",
|
|
// Profile / settings
|
|
"profile", "profile_edit", "notification_preferences",
|
|
"theme_selection",
|
|
// Subscription
|
|
"feature_comparison",
|
|
]
|
|
|
|
func test_ios_surfaces_match_canonical_manifest() {
|
|
// `GalleryScreens.shared.forIos` is the Swift-bridged map of
|
|
// `GalleryScreen` keyed by canonical name. SKIE exposes the
|
|
// Kotlin `object GalleryScreens` as a Swift type with a
|
|
// `shared` instance accessor.
|
|
let manifestKeys = Set(GalleryScreens.shared.forIos.keys.compactMap { $0 as? String })
|
|
|
|
let missing = manifestKeys.subtracting(Self.iosCoveredScreens)
|
|
let extra = Self.iosCoveredScreens.subtracting(manifestKeys)
|
|
|
|
if !missing.isEmpty || !extra.isEmpty {
|
|
var message = "iOS SnapshotGalleryTests drifted from canonical manifest.\n"
|
|
if !missing.isEmpty {
|
|
message += "\nScreens in manifest but missing test_<name>() in Swift:\n"
|
|
for name in missing.sorted() {
|
|
message += " - \(name)\n"
|
|
}
|
|
}
|
|
if !extra.isEmpty {
|
|
message += "\nScreens covered by Swift tests but missing from manifest:\n"
|
|
for name in extra.sorted() {
|
|
message += " - \(name)\n"
|
|
}
|
|
}
|
|
message += "\nReconcile by editing com.tt.honeyDue.testing.GalleryScreens and/or\n"
|
|
message += "SnapshotGalleryTests.swift (plus iosCoveredScreens above) so all three agree.\n"
|
|
XCTFail(message)
|
|
}
|
|
}
|
|
}
|