@file:OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class) package com.tt.honeyDue.screenshot import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.tt.honeyDue.testing.Fixtures import com.tt.honeyDue.ui.screens.AddDocumentScreen import com.tt.honeyDue.ui.screens.AddResidenceScreen import com.tt.honeyDue.ui.screens.AllTasksScreen import com.tt.honeyDue.ui.screens.BiometricLockScreen import com.tt.honeyDue.ui.screens.CompleteTaskScreen import com.tt.honeyDue.ui.screens.ContractorDetailScreen import com.tt.honeyDue.ui.screens.ContractorsScreen import com.tt.honeyDue.ui.screens.DocumentDetailScreen import com.tt.honeyDue.ui.screens.DocumentsScreen import com.tt.honeyDue.ui.screens.EditDocumentScreen import com.tt.honeyDue.ui.screens.EditResidenceScreen import com.tt.honeyDue.ui.screens.EditTaskScreen import com.tt.honeyDue.ui.screens.ForgotPasswordScreen import com.tt.honeyDue.ui.screens.HomeScreen import com.tt.honeyDue.ui.screens.LoginScreen import com.tt.honeyDue.ui.screens.ManageUsersScreen import com.tt.honeyDue.ui.screens.NotificationPreferencesScreen import com.tt.honeyDue.ui.screens.ProfileScreen import com.tt.honeyDue.ui.screens.RegisterScreen import com.tt.honeyDue.ui.screens.ResetPasswordScreen import com.tt.honeyDue.ui.screens.ResidenceDetailScreen import com.tt.honeyDue.ui.screens.ResidencesScreen import com.tt.honeyDue.ui.screens.VerifyEmailScreen import com.tt.honeyDue.ui.screens.VerifyResetCodeScreen import com.tt.honeyDue.ui.screens.onboarding.OnboardingCreateAccountContent import com.tt.honeyDue.ui.screens.onboarding.OnboardingFirstTaskContent import com.tt.honeyDue.ui.screens.onboarding.OnboardingHomeProfileContent import com.tt.honeyDue.ui.screens.onboarding.OnboardingJoinResidenceContent import com.tt.honeyDue.ui.screens.onboarding.OnboardingLocationContent import com.tt.honeyDue.ui.screens.onboarding.OnboardingNameResidenceContent import com.tt.honeyDue.ui.screens.onboarding.OnboardingSubscriptionContent import com.tt.honeyDue.ui.screens.onboarding.OnboardingValuePropsContent import com.tt.honeyDue.ui.screens.onboarding.OnboardingVerifyEmailContent import com.tt.honeyDue.ui.screens.onboarding.OnboardingWelcomeContent import com.tt.honeyDue.ui.screens.residence.JoinResidenceScreen import com.tt.honeyDue.ui.screens.subscription.FeatureComparisonScreen import com.tt.honeyDue.ui.screens.task.AddTaskWithResidenceScreen import com.tt.honeyDue.ui.screens.task.TaskSuggestionsScreen import com.tt.honeyDue.ui.screens.task.TaskTemplatesBrowserScreen import com.tt.honeyDue.ui.screens.theme.ThemeSelectionScreen import com.tt.honeyDue.viewmodel.ContractorViewModel import com.tt.honeyDue.viewmodel.DocumentViewModel import com.tt.honeyDue.viewmodel.OnboardingViewModel import com.tt.honeyDue.viewmodel.PasswordResetViewModel /** * Declarative manifest of every Android gallery surface. Must stay in sync * with the canonical [com.tt.honeyDue.testing.GalleryScreens] manifest — * [GalleryManifestParityTest] fails CI if the two drift. * * Scope: the screens users land on. We deliberately skip: * - dialogs that live inside a host screen (already captured on the host), * - animation sub-views / decorative components in AnimationTesting/, * - widget views (Android Glance / iOS WidgetKit — separate surface), * - shared helper composables (loaders, error rows, thumbnails — they * only appear as part of a parent screen). * * Detail-VM pattern (contractor_detail, document_detail, edit_document): * the VM is created with the fixture id already pre-selected, so * `stateIn(SharingStarted.Eagerly, initialValue = dataManager.x[id])` * emits `Success(entity)` on first composition. Without this pre-select, * the screens' own `LaunchedEffect(id) { vm.loadX(id) }` dispatches the id * assignment to a coroutine that runs *after* Roborazzi captures the * frame — so both empty and populated captures would render the `Idle` * state and be byte-identical. * * Screens that require a construction-time ViewModel * ([OnboardingViewModel], [PasswordResetViewModel]) instantiate it inline * here. The production code paths start the viewmodel's own * `launch { APILayer.xxx() }` on first composition — those calls fail fast * in the hermetic Robolectric environment, but the composition itself * renders the surface from the injected * [com.tt.honeyDue.data.LocalDataManager] before any network result * arrives, which is exactly what we want to compare against iOS. */ data class GallerySurface( /** Snake-case identifier; used as the golden file-name prefix. */ val name: String, val content: @Composable () -> Unit, ) { /** * ParameterizedRobolectricTestRunner uses `toString()` in the test * display name when the `{0}` pattern is set. The default data-class * toString includes the composable lambda hash — not useful. Override * so test reports show `ScreenshotTests[login]` instead of * `ScreenshotTests[GallerySurface(name=login, content=...@abc123)]`. */ override fun toString(): String = name } val gallerySurfaces: List = listOf( // ---------- Auth ---------- GallerySurface("login") { LoginScreen( onLoginSuccess = {}, onNavigateToRegister = {}, onNavigateToForgotPassword = {}, ) }, GallerySurface("register") { RegisterScreen( onRegisterSuccess = {}, onNavigateBack = {}, ) }, GallerySurface("forgot_password") { ForgotPasswordScreen( onNavigateBack = {}, onNavigateToVerify = {}, onNavigateToReset = {}, viewModel = PasswordResetViewModel(), ) }, GallerySurface("verify_reset_code") { VerifyResetCodeScreen( onNavigateBack = {}, onNavigateToReset = {}, viewModel = PasswordResetViewModel(), ) }, GallerySurface("reset_password") { ResetPasswordScreen( onPasswordResetSuccess = {}, onNavigateBack = {}, viewModel = PasswordResetViewModel(), ) }, GallerySurface("verify_email") { VerifyEmailScreen( onVerifySuccess = {}, onLogout = {}, ) }, // ---------- Onboarding ---------- GallerySurface("onboarding_welcome") { OnboardingWelcomeContent( onStartFresh = {}, onJoinExisting = {}, onLogin = {}, ) }, GallerySurface("onboarding_value_props") { OnboardingValuePropsContent(onContinue = {}) }, GallerySurface("onboarding_create_account") { OnboardingCreateAccountContent( viewModel = OnboardingViewModel(), onAccountCreated = {}, ) }, GallerySurface("onboarding_verify_email") { OnboardingVerifyEmailContent( viewModel = OnboardingViewModel(), onVerified = {}, ) }, GallerySurface("onboarding_location") { OnboardingLocationContent( viewModel = OnboardingViewModel(), onLocationDetected = {}, onSkip = {}, ) }, GallerySurface("onboarding_name_residence") { OnboardingNameResidenceContent( viewModel = OnboardingViewModel(), onContinue = {}, ) }, GallerySurface("onboarding_home_profile") { OnboardingHomeProfileContent( viewModel = OnboardingViewModel(), onContinue = {}, onSkip = {}, ) }, GallerySurface("onboarding_join_residence") { OnboardingJoinResidenceContent( viewModel = OnboardingViewModel(), onJoined = {}, ) }, GallerySurface("onboarding_first_task") { OnboardingFirstTaskContent( viewModel = OnboardingViewModel(), onTasksAdded = {}, ) }, GallerySurface("onboarding_subscription") { OnboardingSubscriptionContent( onSubscribe = {}, onSkip = {}, ) }, // ---------- Home (Android-only dashboard) ---------- GallerySurface("home") { HomeScreen( onNavigateToResidences = {}, onNavigateToTasks = {}, onLogout = {}, ) }, // ---------- Residences ---------- GallerySurface("residences") { ResidencesScreen( onResidenceClick = {}, onAddResidence = {}, onJoinResidence = {}, onLogout = {}, ) }, GallerySurface("residence_detail") { ResidenceDetailScreen( residenceId = Fixtures.primaryHome.id, onNavigateBack = {}, onNavigateToEditResidence = {}, onNavigateToEditTask = {}, ) }, GallerySurface("add_residence") { AddResidenceScreen( onNavigateBack = {}, onResidenceCreated = {}, ) }, GallerySurface("edit_residence") { EditResidenceScreen( residence = Fixtures.primaryHome, onNavigateBack = {}, onResidenceUpdated = {}, ) }, GallerySurface("join_residence") { JoinResidenceScreen( onNavigateBack = {}, onJoined = {}, ) }, GallerySurface("manage_users") { ManageUsersScreen( residenceId = Fixtures.primaryHome.id, residenceName = Fixtures.primaryHome.name, isPrimaryOwner = true, residenceOwnerId = Fixtures.primaryHome.ownerId, onNavigateBack = {}, ) }, // ---------- Tasks ---------- GallerySurface("all_tasks") { AllTasksScreen(onNavigateToEditTask = {}) }, GallerySurface("add_task_with_residence") { AddTaskWithResidenceScreen( residenceId = Fixtures.primaryHome.id, onNavigateBack = {}, onCreated = {}, ) }, GallerySurface("edit_task") { EditTaskScreen( task = Fixtures.tasks.first(), onNavigateBack = {}, onTaskUpdated = {}, ) }, GallerySurface("complete_task") { val task = Fixtures.tasks.first() CompleteTaskScreen( taskId = task.id, taskTitle = task.title, residenceName = Fixtures.primaryHome.name, onNavigateBack = {}, onComplete = { _, _ -> }, ) }, GallerySurface("task_suggestions") { TaskSuggestionsScreen( residenceId = Fixtures.primaryHome.id, onNavigateBack = {}, onSuggestionAccepted = {}, ) }, GallerySurface("task_templates_browser") { TaskTemplatesBrowserScreen( residenceId = Fixtures.primaryHome.id, onNavigateBack = {}, ) }, // ---------- Contractors ---------- GallerySurface("contractors") { ContractorsScreen( onNavigateBack = {}, onNavigateToContractorDetail = {}, ) }, GallerySurface("contractor_detail") { val id = Fixtures.contractors.first().id // Pass `initialSelectedContractorId` at VM construction so the // synchronous `stateIn` initial-value closure observes both the // id AND the fixture-seeded `dataManager.contractorDetail[id]`, // emitting `Success(contractor)` on first composition. Without // this the screen's own `LaunchedEffect(id) { vm.loadContractorDetail(id) }` // dispatches the id assignment to a coroutine that runs after // the frame is captured, leaving both empty and populated // captures byte-identical on the `Idle` branch. val vm = remember { ContractorViewModel(initialSelectedContractorId = id) } ContractorDetailScreen( contractorId = id, onNavigateBack = {}, viewModel = vm, ) }, // ---------- Documents ---------- GallerySurface("documents") { DocumentsScreen( onNavigateBack = {}, residenceId = Fixtures.primaryHome.id, ) }, GallerySurface("document_detail") { val id = Fixtures.documents.first().id ?: 0 val vm = remember { DocumentViewModel(initialSelectedDocumentId = id) } DocumentDetailScreen( documentId = id, onNavigateBack = {}, onNavigateToEdit = { _ -> }, documentViewModel = vm, ) }, GallerySurface("add_document") { AddDocumentScreen( residenceId = Fixtures.primaryHome.id, onNavigateBack = {}, onDocumentCreated = {}, ) }, GallerySurface("edit_document") { val id = Fixtures.documents.first().id ?: 0 val vm = remember { DocumentViewModel(initialSelectedDocumentId = id) } EditDocumentScreen( documentId = id, onNavigateBack = {}, documentViewModel = vm, ) }, // ---------- Profile / settings ---------- GallerySurface("profile") { ProfileScreen( onNavigateBack = {}, onLogout = {}, ) }, GallerySurface("notification_preferences") { NotificationPreferencesScreen(onNavigateBack = {}) }, GallerySurface("theme_selection") { ThemeSelectionScreen(onNavigateBack = {}) }, GallerySurface("biometric_lock") { BiometricLockScreen(onUnlocked = {}) }, // ---------- Subscription ---------- GallerySurface("feature_comparison") { FeatureComparisonScreen( onNavigateBack = {}, onNavigateToUpgrade = {}, ) }, )