P2: Android parity gallery — real-screen captures (partial, 17/40 surfaces)
Replaces the synthetic theme-showcase ScreenshotTests with real screens rendered against FixtureDataManager.empty() / .populated() via LocalDataManager. GallerySurfaces.kt manifest declares 40 screens. Landed: 68 goldens covering 17 surfaces (login, register, password-reset chain, 10 onboarding screens, home, residences-list). Missing: 23 detail/edit screens that need a specific fixture model passed via GallerySurfaces.kt — tracked as follow-up in docs/parity-gallery.md. Non-blocking: these render silently as blank and don't fail the suite. Android total: 2.5 MB, avg 41 KB, max 113 KB — well under the 150 KB per-file budget enforced by the CI size gate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
@file:OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)
|
||||
package com.tt.honeyDue.screenshot
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
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.TasksScreen
|
||||
import com.tt.honeyDue.ui.screens.VerifyEmailScreen
|
||||
import com.tt.honeyDue.ui.screens.VerifyResetCodeScreen
|
||||
import com.tt.honeyDue.ui.screens.dev.AnimationTestingScreen
|
||||
import com.tt.honeyDue.ui.screens.onboarding.OnboardingCreateAccountContent
|
||||
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.TaskSuggestionsScreen
|
||||
import com.tt.honeyDue.ui.screens.task.TaskTemplatesBrowserScreen
|
||||
import com.tt.honeyDue.ui.screens.theme.ThemeSelectionScreen
|
||||
import com.tt.honeyDue.viewmodel.OnboardingViewModel
|
||||
import com.tt.honeyDue.viewmodel.PasswordResetViewModel
|
||||
|
||||
/**
|
||||
* Declarative manifest of every primary screen in the app that the parity
|
||||
* gallery captures. Each entry renders the production composable directly —
|
||||
* the screen reads its data from [com.tt.honeyDue.data.LocalDataManager],
|
||||
* which the capture driver overrides with a [com.tt.honeyDue.testing.FixtureDataManager]
|
||||
* (empty or populated) per variant.
|
||||
*
|
||||
* 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 listed under `category: shared` in
|
||||
* docs/ios-parity/screens.json (loaders, error rows, thumbnails — they
|
||||
* only appear as part of a parent screen).
|
||||
*
|
||||
* 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<GallerySurface> = 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_subscription") {
|
||||
OnboardingSubscriptionContent(
|
||||
onSubscribe = {},
|
||||
onSkip = {},
|
||||
)
|
||||
},
|
||||
|
||||
// ---------- Home / main navigation ----------
|
||||
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("tasks") {
|
||||
TasksScreen(onNavigateBack = {})
|
||||
},
|
||||
GallerySurface("all_tasks") {
|
||||
AllTasksScreen(onNavigateToEditTask = {})
|
||||
},
|
||||
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") {
|
||||
ContractorDetailScreen(
|
||||
contractorId = Fixtures.contractors.first().id,
|
||||
onNavigateBack = {},
|
||||
)
|
||||
},
|
||||
|
||||
// ---------- Documents ----------
|
||||
GallerySurface("documents") {
|
||||
DocumentsScreen(
|
||||
onNavigateBack = {},
|
||||
residenceId = Fixtures.primaryHome.id,
|
||||
)
|
||||
},
|
||||
GallerySurface("document_detail") {
|
||||
DocumentDetailScreen(
|
||||
documentId = Fixtures.documents.first().id ?: 0,
|
||||
onNavigateBack = {},
|
||||
onNavigateToEdit = {},
|
||||
)
|
||||
},
|
||||
GallerySurface("add_document") {
|
||||
AddDocumentScreen(
|
||||
residenceId = Fixtures.primaryHome.id,
|
||||
onNavigateBack = {},
|
||||
onDocumentCreated = {},
|
||||
)
|
||||
},
|
||||
GallerySurface("edit_document") {
|
||||
EditDocumentScreen(
|
||||
documentId = Fixtures.documents.first().id ?: 0,
|
||||
onNavigateBack = {},
|
||||
)
|
||||
},
|
||||
|
||||
// ---------- Profile / settings ----------
|
||||
GallerySurface("profile") {
|
||||
ProfileScreen(
|
||||
onNavigateBack = {},
|
||||
onLogout = {},
|
||||
)
|
||||
},
|
||||
GallerySurface("theme_selection") {
|
||||
ThemeSelectionScreen(onNavigateBack = {})
|
||||
},
|
||||
GallerySurface("notification_preferences") {
|
||||
NotificationPreferencesScreen(onNavigateBack = {})
|
||||
},
|
||||
GallerySurface("animation_testing") {
|
||||
AnimationTestingScreen(onNavigateBack = {})
|
||||
},
|
||||
GallerySurface("biometric_lock") {
|
||||
BiometricLockScreen(onUnlocked = {})
|
||||
},
|
||||
|
||||
// ---------- Subscription ----------
|
||||
GallerySurface("feature_comparison") {
|
||||
FeatureComparisonScreen(
|
||||
onNavigateBack = {},
|
||||
onNavigateToUpgrade = {},
|
||||
)
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user