package com.tt.honeyDue.testing /** * Canonical list of every user-reachable screen in the HoneyDue app, used * as the single source of truth for the iOS↔Android parity-gallery * snapshot tests. * * Both platforms' snapshot harnesses are CI-gated against this manifest: * - Android: `GalleryManifestParityTest` fails if the entries in * `GallerySurfaces.kt` don't match the subset of screens with * [Platform.ANDROID] in [platforms]. * - iOS: `GalleryManifestParityTest.swift` performs the equivalent check * against `SnapshotGalleryTests.swift`. * * This prevents the two platforms from silently drifting apart in * coverage — adding a screen to one side without updating this manifest * (and therefore the other side) fails CI. * * When a screen is reachable on only one platform (e.g. [GalleryScreens.home] * on Android, [GalleryScreens.documentsWarranties] on iOS), mark it with * the relevant [Platform] set. The gallery HTML renders a visible * `[missing — ]` placeholder for the absent side so the gap is * obvious rather than silently omitted. */ /** Category of a gallery screen — drives the capture-variant matrix. */ enum class GalleryCategory { /** * Screen renders data from [com.tt.honeyDue.data.IDataManager] (lists, * detail views, dashboards). Captures 4 variants: empty+populated x * light+dark — so the test proves the fixture actually reaches the UI. */ DataCarrying, /** * Screen is a pure form, auth view, static onboarding step, or chrome * with no backing entity data. Captures 2 variants: light+dark only. * Skipping the populated variant prevents ~50 byte-identical-to-empty * goldens that add no signal. */ DataFree, } /** Platforms that include a given screen in their parity-gallery harness. */ enum class Platform { ANDROID, IOS } /** * One canonical screen in the parity manifest. * * @property name Snake-case identifier; doubles as the golden-PNG filename * prefix on both platforms. * @property category Drives the capture-variant matrix (see [GalleryCategory]). * @property platforms Platforms that capture this screen. Screens captured * on both have a paired row in the gallery; screens on only one show a * `[missing]` placeholder for the absent platform. */ data class GalleryScreen( val name: String, val category: GalleryCategory, val platforms: Set, ) /** * Canonical manifest — 43 screens, ordered by product flow. * * Breakdown: * - 12 [GalleryCategory.DataCarrying] screens — 4 captures each. * - 31 [GalleryCategory.DataFree] screens — 2 captures each. * - 37 screens captured on both platforms. * - 3 Android-only: `home`, `documents`, `biometric_lock`. * - 3 iOS-only: `documents_warranties`, `add_task`, `profile_edit`. */ object GalleryScreens { private val both = setOf(Platform.ANDROID, Platform.IOS) private val androidOnly = setOf(Platform.ANDROID) private val iosOnly = setOf(Platform.IOS) val all: List = listOf( // ---------- Auth ---------- GalleryScreen("login", GalleryCategory.DataFree, both), GalleryScreen("register", GalleryCategory.DataFree, both), GalleryScreen("forgot_password", GalleryCategory.DataFree, both), GalleryScreen("verify_reset_code", GalleryCategory.DataFree, both), GalleryScreen("reset_password", GalleryCategory.DataFree, both), GalleryScreen("verify_email", GalleryCategory.DataFree, both), // ---------- Onboarding ---------- GalleryScreen("onboarding_welcome", GalleryCategory.DataFree, both), GalleryScreen("onboarding_value_props", GalleryCategory.DataFree, both), GalleryScreen("onboarding_create_account", GalleryCategory.DataFree, both), GalleryScreen("onboarding_verify_email", GalleryCategory.DataFree, both), GalleryScreen("onboarding_location", GalleryCategory.DataFree, both), GalleryScreen("onboarding_name_residence", GalleryCategory.DataFree, both), GalleryScreen("onboarding_home_profile", GalleryCategory.DataFree, both), GalleryScreen("onboarding_join_residence", GalleryCategory.DataFree, both), GalleryScreen("onboarding_first_task", GalleryCategory.DataCarrying, both), GalleryScreen("onboarding_subscription", GalleryCategory.DataFree, both), // ---------- Home / dashboard (Android-only) ---------- GalleryScreen("home", GalleryCategory.DataCarrying, androidOnly), // ---------- Residences ---------- GalleryScreen("residences", GalleryCategory.DataCarrying, both), GalleryScreen("residence_detail", GalleryCategory.DataCarrying, both), GalleryScreen("add_residence", GalleryCategory.DataFree, both), GalleryScreen("edit_residence", GalleryCategory.DataFree, both), GalleryScreen("join_residence", GalleryCategory.DataFree, both), // `manage_users` is DataFree: both platforms render a loading / // error state on first paint because residence-users data is // fetched via APILayer directly (no fixture seam). Populating // it would require a new `usersByResidence` field on // `IDataManager` plus fixture+screen wiring — deferred as a // production improvement rather than a snapshot-test-only // shim. GalleryScreen("manage_users", GalleryCategory.DataFree, both), // ---------- Tasks ---------- GalleryScreen("all_tasks", GalleryCategory.DataCarrying, both), // `add_task` is iOS-only: iOS presents an "Add task" sheet from a // residence-scoped context. Android adds tasks via an inline dialog // inside `residence_detail`, with no standalone destination. GalleryScreen("add_task", GalleryCategory.DataFree, iosOnly), GalleryScreen("add_task_with_residence", GalleryCategory.DataFree, both), GalleryScreen("edit_task", GalleryCategory.DataFree, both), // `complete_task` is DataFree: the task and residence-name are // passed as static props, completion form fields default-render // the same regardless of fixture state, and the contractor // picker is collapsed on first paint. Nothing visible diffs // between empty and populated. GalleryScreen("complete_task", GalleryCategory.DataFree, both), // `task_suggestions` is DataFree in snapshot terms: the visible // first-paint state is driven by an `APILayer.getTaskSuggestions` // call (which fails hermetically), not by anything on // `IDataManager`. The populated templates stored on DM are only // surfaced after the API resolves, so both variants render the // same loading/error frame. Treating as DataFree is honest. GalleryScreen("task_suggestions", GalleryCategory.DataFree, both), GalleryScreen("task_templates_browser", GalleryCategory.DataCarrying, both), // ---------- Contractors ---------- GalleryScreen("contractors", GalleryCategory.DataCarrying, both), GalleryScreen("contractor_detail", GalleryCategory.DataCarrying, both), // ---------- Documents ---------- // Android has a single `documents` screen; iOS has a tabbed // `documents_warranties` view that unifies docs + warranties under // a segmented control. They're structurally distinct enough to // list as separate rows so the gallery makes the divergence // visible rather than pretending they're the same screen. GalleryScreen("documents", GalleryCategory.DataCarrying, androidOnly), GalleryScreen("documents_warranties", GalleryCategory.DataCarrying, iosOnly), GalleryScreen("document_detail", GalleryCategory.DataCarrying, both), GalleryScreen("add_document", GalleryCategory.DataFree, both), GalleryScreen("edit_document", GalleryCategory.DataFree, both), // ---------- Profile / settings ---------- GalleryScreen("profile", GalleryCategory.DataCarrying, both), // `profile_edit` is iOS-only: iOS has a standalone edit-profile view. // On Android, profile editing is folded into `profile` (inline form). GalleryScreen("profile_edit", GalleryCategory.DataFree, iosOnly), GalleryScreen("notification_preferences", GalleryCategory.DataFree, both), GalleryScreen("theme_selection", GalleryCategory.DataFree, both), GalleryScreen("biometric_lock", GalleryCategory.DataFree, androidOnly), // ---------- Subscription ---------- GalleryScreen("feature_comparison", GalleryCategory.DataFree, both), ) /** Screens captured on Android, keyed by canonical name. */ val forAndroid: Map = all.filter { Platform.ANDROID in it.platforms }.associateBy { it.name } /** Screens captured on iOS, keyed by canonical name. */ val forIos: Map = all.filter { Platform.IOS in it.platforms }.associateBy { it.name } }