Coverage Summary for Class: AppearanceTestTags (com.georgeci.moneysurfer.feature.settings.appearance)

Class Class, % Method, % Branch, % Line, % Instruction, %
AppearanceTestTags 0% (0/1) 0% (0/2) 0% (0/2) 0% (0/16)


 package com.georgeci.moneysurfer.feature.settings.appearance
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.aspectRatio
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.Icon
 import androidx.compose.material3.Scaffold
 import androidx.compose.material3.Text
 import androidx.compose.material3.TopAppBarDefaults
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.georgeci.moneysurfer.domain.preferences.AccentSeed
 import com.georgeci.moneysurfer.domain.preferences.ContainerStyle
 import com.georgeci.moneysurfer.domain.preferences.PaletteSource
 import com.georgeci.moneysurfer.domain.preferences.ThemeMode
 import com.georgeci.moneysurfer.uikit.atom.SurferCard
 import com.georgeci.moneysurfer.uikit.components.base.SurferPaneTopBar
 import com.georgeci.moneysurfer.uikit.components.settings.SurferSettingsGroup
 import com.georgeci.moneysurfer.uikit.components.settings.SurferSettingsRadio
 import com.georgeci.moneysurfer.uikit.components.settings.SurferSettingsRow
 import com.georgeci.moneysurfer.uikit.icons.SurferIcons
 import com.georgeci.moneysurfer.uikit.modifier.surferContentContainer
 import com.georgeci.moneysurfer.uikit.modifier.surferSafeInsets
 import com.georgeci.moneysurfer.uikit.modifier.surferTestTagAsId
 import com.georgeci.moneysurfer.uikit.semantics.SurferSemantics
 import com.georgeci.moneysurfer.uikit.theme.AppTheme
 import com.georgeci.moneysurfer.uikit.tokens.AccentSeeds
 import com.georgeci.moneysurfer.uikit.tokens.AppColors
 import moneysurfer.feature.settings.generated.resources.Res
 import moneysurfer.feature.settings.generated.resources.settings_appearance_accent_footnote
 import moneysurfer.feature.settings.generated.resources.settings_appearance_color_source
 import moneysurfer.feature.settings.generated.resources.settings_appearance_dynamic_supporting
 import moneysurfer.feature.settings.generated.resources.settings_appearance_dynamic_title
 import moneysurfer.feature.settings.generated.resources.settings_appearance_live_preview_supporting
 import moneysurfer.feature.settings.generated.resources.settings_appearance_live_preview_title
 import moneysurfer.feature.settings.generated.resources.settings_appearance_swatch_brand
 import moneysurfer.feature.settings.generated.resources.settings_appearance_swatch_mint
 import moneysurfer.feature.settings.generated.resources.settings_appearance_swatch_plum
 import moneysurfer.feature.settings.generated.resources.settings_appearance_swatch_rose
 import moneysurfer.feature.settings.generated.resources.settings_appearance_swatch_sky
 import moneysurfer.feature.settings.generated.resources.settings_appearance_theme_dark
 import moneysurfer.feature.settings.generated.resources.settings_appearance_theme_dark_supporting
 import moneysurfer.feature.settings.generated.resources.settings_appearance_theme_light
 import moneysurfer.feature.settings.generated.resources.settings_appearance_theme_light_supporting
 import moneysurfer.feature.settings.generated.resources.settings_appearance_theme_section
 import moneysurfer.feature.settings.generated.resources.settings_appearance_theme_system
 import moneysurfer.feature.settings.generated.resources.settings_appearance_theme_system_supporting
 import moneysurfer.feature.settings.generated.resources.settings_appearance_title
 import org.jetbrains.compose.resources.StringResource
 import org.jetbrains.compose.resources.stringResource
 import org.koin.compose.viewmodel.koinViewModel
 
 private sealed interface SwatchSpec {
     val labelRes: StringResource
     val color: Color
 
     data class Brand(override val color: Color) : SwatchSpec {
         override val labelRes: StringResource = Res.string.settings_appearance_swatch_brand
     }
 
     data class Preset(
         val seed: AccentSeed,
         override val labelRes: StringResource,
         override val color: Color,
     ) : SwatchSpec
 }
 
 private val FixedSwatches: List<SwatchSpec> = listOf(
     SwatchSpec.Brand(AppColors.Primary),
     SwatchSpec.Preset(AccentSeed.Plum, Res.string.settings_appearance_swatch_plum, AccentSeeds.Plum),
     SwatchSpec.Preset(AccentSeed.Sky, Res.string.settings_appearance_swatch_sky, AccentSeeds.Sky),
     SwatchSpec.Preset(AccentSeed.Mint, Res.string.settings_appearance_swatch_mint, AccentSeeds.Mint),
     SwatchSpec.Preset(AccentSeed.Rose, Res.string.settings_appearance_swatch_rose, AccentSeeds.Rose),
 )
 
 /**
  * Stable selectors for the Appearance screen — see docs/testing/testing-strategy.md.
  *
  * The option rows are named after the enum entry they set, not the localized label, so a
  * renamed row title never breaks a flow: `appearance:theme:system|light|dark` and
  * `appearance:cardStyle:filled|outlined|card`.
  */
 object AppearanceTestTags {
     const val Root = "appearance:root"
 
     /** Row that selects a [ThemeMode]. */
     fun theme(mode: ThemeMode): String = "appearance:theme:${mode.name.lowercase()}"
 
     /** Row that selects a [ContainerStyle] — the "Card style" section. */
     fun cardStyle(style: ContainerStyle): String = "appearance:cardStyle:${style.name.lowercase()}"
 }
 
 @Composable
 fun AppearanceScreen(
     onNavigateBack: () -> Unit,
     viewModel: AppearanceViewModel = koinViewModel(),
 ) {
     val state by viewModel.collectAsStateWithLifecycle()
     AppearanceContent(
         state = state,
         onNavigateBack = onNavigateBack,
         onDynamicSelect = { viewModel.onEvent(AppearanceEvent.OnDynamicSelect) },
         onBrandSelect = { viewModel.onEvent(AppearanceEvent.OnBrandSelect) },
         onPresetSelect = { seed -> viewModel.onEvent(AppearanceEvent.OnPresetSelect(seed)) },
         onThemeModeSelect = { mode -> viewModel.onEvent(AppearanceEvent.OnThemeModeSelect(mode)) },
         onContainerStyleSelect = { style ->
             viewModel.onEvent(AppearanceEvent.OnContainerStyleSelect(style))
         },
     )
 }
 
 @Composable
 private fun AppearanceContent(
     state: AppearanceState,
     onNavigateBack: () -> Unit,
     onDynamicSelect: () -> Unit,
     onBrandSelect: () -> Unit,
     onPresetSelect: (AccentSeed) -> Unit,
     onThemeModeSelect: (ThemeMode) -> Unit,
     onContainerStyleSelect: (ContainerStyle) -> Unit,
 ) {
     Scaffold(
         modifier = Modifier
             .surferSafeInsets()
             .testTag(AppearanceTestTags.Root)
             .surferTestTagAsId(),
         containerColor = AppTheme.materialColors.surface,
         topBar = {
             SurferPaneTopBar(
                 title = stringResource(Res.string.settings_appearance_title),
                 onBack = onNavigateBack,
                 colors = TopAppBarDefaults.topAppBarColors(
                     containerColor = AppTheme.materialColors.surface,
                     titleContentColor = AppTheme.materialColors.onSurface,
                 ),
             )
         },
     ) { padding ->
         Column(
             modifier = Modifier
                 .fillMaxSize()
                 .surferContentContainer()
                 .padding(top = padding.calculateTopPadding())
                 .verticalScroll(rememberScrollState()),
         ) {
             Box(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
                 LiveSchemePreview()
             }
 
             SurferSettingsGroup(title = stringResource(Res.string.settings_appearance_theme_section)) {
                 SurferSettingsRow(
                     icon = SurferIcons.Sparkle,
                     title = stringResource(Res.string.settings_appearance_theme_system),
                     supportingText = stringResource(Res.string.settings_appearance_theme_system_supporting),
                     onClick = { onThemeModeSelect(ThemeMode.System) },
                     trailing = { SurferSettingsRadio(selected = state.themeMode == ThemeMode.System) },
                     modifier = Modifier.testTag(AppearanceTestTags.theme(ThemeMode.System)),
                 )
                 SurferSettingsRow(
                     icon = SurferIcons.LightMode,
                     title = stringResource(Res.string.settings_appearance_theme_light),
                     supportingText = stringResource(Res.string.settings_appearance_theme_light_supporting),
                     onClick = { onThemeModeSelect(ThemeMode.Light) },
                     trailing = { SurferSettingsRadio(selected = state.themeMode == ThemeMode.Light) },
                     modifier = Modifier.testTag(AppearanceTestTags.theme(ThemeMode.Light)),
                 )
                 SurferSettingsRow(
                     icon = SurferIcons.DarkMode,
                     title = stringResource(Res.string.settings_appearance_theme_dark),
                     supportingText = stringResource(Res.string.settings_appearance_theme_dark_supporting),
                     onClick = { onThemeModeSelect(ThemeMode.Dark) },
                     trailing = { SurferSettingsRadio(selected = state.themeMode == ThemeMode.Dark) },
                     modifier = Modifier.testTag(AppearanceTestTags.theme(ThemeMode.Dark)),
                 )
             }
 
             SurferSettingsGroup(
                 title = stringResource(Res.string.settings_appearance_color_source),
                 footnote = stringResource(Res.string.settings_appearance_accent_footnote),
             ) {
                 if (state.isDynamicColorAvailable) {
                     SurferSettingsRow(
                         icon = SurferIcons.Sparkle,
                         title = stringResource(Res.string.settings_appearance_dynamic_title),
                         supportingText = stringResource(Res.string.settings_appearance_dynamic_supporting),
                         onClick = onDynamicSelect,
                         trailing = { SurferSettingsRadio(selected = state.isDynamicColorEnabled) },
                     )
                 }
                 SeedPickerCard(
                     swatches = FixedSwatches,
                     isBrandSelected = state.isBrandSelected,
                     selectedSeed = state.selectedSeed,
                     onPickBrand = onBrandSelect,
                     onPickPreset = onPresetSelect,
                 )
             }
 
             SurferSettingsGroup(title = "Card style") {
                 ContainerStyle.entries.forEach { style ->
                     SurferSettingsRow(
                         icon = containerStyleIcon(style),
                         title = style.displayLabel(),
                         supportingText = style.displaySupporting(),
                         onClick = { onContainerStyleSelect(style) },
                         trailing = { SurferSettingsRadio(selected = state.containerStyle == style) },
                         modifier = Modifier.testTag(AppearanceTestTags.cardStyle(style)),
                     )
                 }
             }
             Spacer(Modifier.height(padding.calculateBottomPadding() + 24.dp))
         }
     }
 }
 
 private fun ContainerStyle.displayLabel(): String = when (this) {
     ContainerStyle.Filled -> "Filled"
     ContainerStyle.Outlined -> "Outlined"
     ContainerStyle.Card -> "Card"
 }
 
 private fun ContainerStyle.displaySupporting(): String = when (this) {
     ContainerStyle.Filled -> "Solid surfaces, no border"
     ContainerStyle.Outlined -> "Bordered surfaces, no fill"
     ContainerStyle.Card -> "Elevated surfaces with shadow"
 }
 
 private fun containerStyleIcon(style: ContainerStyle) = when (style) {
     ContainerStyle.Filled -> SurferIcons.Palette
     ContainerStyle.Outlined -> SurferIcons.Code
     ContainerStyle.Card -> SurferIcons.Receipt
 }
 
 @Composable
 private fun LiveSchemePreview() {
     val colors = AppTheme.materialColors
     SurferCard(modifier = Modifier.fillMaxWidth()) {
         Row(
             modifier = Modifier
                 .fillMaxWidth()
                 .padding(18.dp),
             verticalAlignment = Alignment.CenterVertically,
             horizontalArrangement = Arrangement.spacedBy(14.dp),
         ) {
             Box(
                 modifier = Modifier
                     .size(48.dp)
                     .clip(CircleShape)
                     .background(colors.primary),
                 contentAlignment = Alignment.Center,
             ) {
                 Icon(
                     imageVector = SurferIcons.Palette,
                     contentDescription = SurferSemantics.Decorative,
                     tint = colors.onPrimary,
                     modifier = Modifier.size(22.dp),
                 )
             }
             Column(modifier = Modifier.weight(1f)) {
                 Text(
                     text = stringResource(Res.string.settings_appearance_live_preview_title),
                     style = AppTheme.typography.titleSmall,
                     color = colors.onSurface,
                 )
                 Text(
                     text = stringResource(Res.string.settings_appearance_live_preview_supporting),
                     style = AppTheme.typography.bodySmall,
                     color = colors.onSurfaceVariant,
                     modifier = Modifier.padding(top = 2.dp),
                 )
             }
             Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
                 listOf(colors.primary, colors.secondary, colors.tertiary).forEach { c ->
                     Box(
                         modifier = Modifier
                             .size(18.dp)
                             .clip(CircleShape)
                             .background(c),
                     )
                 }
             }
         }
     }
 }
 
 @Composable
 private fun SeedPickerCard(
     swatches: List<SwatchSpec>,
     isBrandSelected: Boolean,
     selectedSeed: AccentSeed?,
     onPickBrand: () -> Unit,
     onPickPreset: (AccentSeed) -> Unit,
 ) {
     val colors = AppTheme.materialColors
     SurferCard(modifier = Modifier.fillMaxWidth()) {
         Row(
             horizontalArrangement = Arrangement.spacedBy(10.dp),
             modifier = Modifier.fillMaxWidth(),
         ) {
             swatches.forEach { swatch ->
                 val selected = when (swatch) {
                     is SwatchSpec.Brand -> isBrandSelected
                     is SwatchSpec.Preset -> swatch.seed == selectedSeed
                 }
                 SwatchCell(
                     label = stringResource(swatch.labelRes),
                     color = swatch.color,
                     selected = selected,
                     labelColor = if (selected) colors.onSurface else colors.onSurfaceVariant,
                     onClick = {
                         when (swatch) {
                             is SwatchSpec.Brand -> onPickBrand()
                             is SwatchSpec.Preset -> onPickPreset(swatch.seed)
                         }
                     },
                     modifier = Modifier.weight(1f),
                 )
             }
         }
     }
 }
 
 @Composable
 private fun SwatchCell(
     label: String,
     color: Color,
     selected: Boolean,
     labelColor: Color,
     onClick: () -> Unit,
     modifier: Modifier = Modifier,
 ) {
     Column(
         modifier = modifier,
         horizontalAlignment = Alignment.CenterHorizontally,
         verticalArrangement = Arrangement.spacedBy(6.dp),
     ) {
         Box(
             modifier = Modifier
                 .fillMaxWidth()
                 .aspectRatio(1f)
                 .clip(RoundedCornerShape(16.dp))
                 .background(color)
                 .clickable { onClick() },
             contentAlignment = Alignment.Center,
         ) {
             if (selected) {
                 Box(
                     modifier = Modifier
                         .size(28.dp)
                         .clip(CircleShape)
                         .background(Color.White.copy(alpha = 0.25f)),
                     contentAlignment = Alignment.Center,
                 ) {
                     Icon(
                         imageVector = SurferIcons.Check,
                         contentDescription = SurferSemantics.Decorative,
                         tint = Color.White,
                         modifier = Modifier.size(18.dp),
                     )
                 }
             }
         }
         Text(
             text = label,
             style = AppTheme.typography.labelMedium,
             color = labelColor,
         )
     }
 }
 
 @Preview()
 @Composable
 private fun AppearanceScreenPreview() {
     AppTheme {
         AppearanceContent(
             state = AppearanceState(isDynamicColorAvailable = true),
             onNavigateBack = {},
             onDynamicSelect = {},
             onBrandSelect = {},
             onPresetSelect = {},
             onThemeModeSelect = {},
             onContainerStyleSelect = {},
         )
     }
 }
 
 @Preview
 @Composable
 private fun AppearanceScreenDynamicOnPreview() {
     AppTheme {
         AppearanceContent(
             state = AppearanceState(
                 isDynamicColorAvailable = true,
                 paletteSource = PaletteSource.Dynamic,
             ),
             onNavigateBack = {},
             onDynamicSelect = {},
             onBrandSelect = {},
             onPresetSelect = {},
             onThemeModeSelect = {},
             onContainerStyleSelect = {},
         )
     }
 }