Coverage Summary for Class: GoalEditScreenKt (com.georgeci.moneysurfer.feature.goal.edit)

Class Class, % Method, % Branch, % Line, % Instruction, %
GoalEditScreenKt 0% (0/1) 0% (0/18) 0% (0/46) 0% (0/98) 0% (0/1238)


 package com.georgeci.moneysurfer.feature.goal.edit
 
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.OutlinedTextField
 import androidx.compose.material3.Scaffold
 import androidx.compose.material3.Text
 import androidx.compose.material3.TextButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.text.input.KeyboardType
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.georgeci.moneysurfer.domain.primitives.GoalId
 import com.georgeci.moneysurfer.feature.goal.GoalDateField
 import com.georgeci.moneysurfer.feature.goal.GoalDatePickerDialog
 import com.georgeci.moneysurfer.feature.goal.formatGoalDate
 import com.georgeci.moneysurfer.uikit.components.base.SurferSectionLabel
 import com.georgeci.moneysurfer.uikit.components.base.SurferToolbar
 import com.georgeci.moneysurfer.uikit.components.base.SurferToolbarButtonAction
 import com.georgeci.moneysurfer.uikit.components.goal.SurferEmojiPicker
 import com.georgeci.moneysurfer.uikit.components.goal.SurferGoalHueRow
 import com.georgeci.moneysurfer.uikit.components.goal.SurferGoalIcon
 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.preview.SurferComponentPreview
 import com.georgeci.moneysurfer.uikit.theme.AppTheme
 import com.georgeci.moneysurfer.utils.HandleSideEffect
 import kotlinx.datetime.LocalDate
 import moneysurfer.feature.goal.generated.resources.Res
 import moneysurfer.feature.goal.generated.resources.goal_edit_color_label
 import moneysurfer.feature.goal.generated.resources.goal_edit_icon_label
 import moneysurfer.feature.goal.generated.resources.goal_edit_name_error_required
 import moneysurfer.feature.goal.generated.resources.goal_edit_name_label
 import moneysurfer.feature.goal.generated.resources.goal_edit_save
 import moneysurfer.feature.goal.generated.resources.goal_edit_target_date_clear
 import moneysurfer.feature.goal.generated.resources.goal_edit_target_date_label
 import moneysurfer.feature.goal.generated.resources.goal_edit_target_date_placeholder
 import moneysurfer.feature.goal.generated.resources.goal_edit_target_error
 import moneysurfer.feature.goal.generated.resources.goal_edit_target_label
 import moneysurfer.feature.goal.generated.resources.goal_edit_title_create
 import moneysurfer.feature.goal.generated.resources.goal_edit_title_edit
 import org.jetbrains.compose.resources.stringResource
 import org.koin.compose.viewmodel.koinViewModel
 import org.koin.core.parameter.parametersOf
 
 /** Stable selectors for the goal create/edit screen — see docs/testing/testing-strategy.md. */
 object GoalEditTestTags {
     const val Root = "goalEdit:root"
     const val TitleField = "goalEdit:title"
     const val TargetField = "goalEdit:target"
     const val SaveButton = "goalEdit:save"
 }
 
 @Composable
 fun GoalEditScreen(
     goalId: GoalId? = null,
     onNavigateBack: () -> Unit,
     viewModel: GoalEditViewModel = koinViewModel(key = goalId?.value ?: "new") { parametersOf(goalId) },
 ) {
     val state by viewModel.collectAsStateWithLifecycle()
 
     viewModel.HandleSideEffect { effect ->
         when (effect) {
             GoalEditEffect.NavigateBack -> onNavigateBack()
         }
     }
 
     GoalEditContent(state = state, onEvent = viewModel::onEvent)
 }
 
 @Composable
 private fun GoalEditContent(
     state: GoalEditState,
     onEvent: (GoalEditEvent) -> Unit,
 ) {
     Scaffold(
         modifier = Modifier
             .surferSafeInsets()
             .testTag(GoalEditTestTags.Root)
             .surferTestTagAsId(),
         containerColor = AppTheme.materialColors.surface,
         topBar = {
             SurferToolbar(
                 title = stringResource(
                     if (state.isEdit) Res.string.goal_edit_title_edit else Res.string.goal_edit_title_create,
                 ),
                 onBack = { onEvent(GoalEditEvent.OnBackClick) },
                 actions = {
                     SurferToolbarButtonAction(
                         icon = SurferIcons.Check,
                         text = stringResource(Res.string.goal_edit_save),
                         enabled = state.canSave,
                         onClick = { onEvent(GoalEditEvent.OnSaveClick) },
                         modifier = Modifier.testTag(GoalEditTestTags.SaveButton),
                     )
                 },
             )
         },
     ) { padding ->
         Column(
             modifier = Modifier
                 .fillMaxSize()
                 .surferContentContainer()
                 .padding(padding)
                 .verticalScroll(rememberScrollState())
                 .padding(AppTheme.spacing.default),
             verticalArrangement = Arrangement.spacedBy(AppTheme.spacing.default),
         ) {
             Row(
                 verticalAlignment = Alignment.CenterVertically,
                 horizontalArrangement = Arrangement.spacedBy(AppTheme.spacing.medium),
             ) {
                 SurferGoalIcon(emoji = state.emoji, hue = state.hue, size = 56.dp)
                 OutlinedTextField(
                     value = state.title,
                     onValueChange = { onEvent(GoalEditEvent.OnTitleChanged(it)) },
                     label = { Text(stringResource(Res.string.goal_edit_name_label)) },
                     isError = state.titleMissing,
                     supportingText = if (state.titleMissing) {
                         { Text(stringResource(Res.string.goal_edit_name_error_required)) }
                     } else {
                         null
                     },
                     singleLine = true,
                     modifier = Modifier
                         .weight(1f)
                         .testTag(GoalEditTestTags.TitleField),
                 )
             }
 
             OutlinedTextField(
                 value = state.target,
                 onValueChange = { onEvent(GoalEditEvent.OnTargetChanged(it)) },
                 label = { Text(stringResource(Res.string.goal_edit_target_label)) },
                 isError = state.targetMissing,
                 supportingText = if (state.targetMissing) {
                     { Text(stringResource(Res.string.goal_edit_target_error)) }
                 } else {
                     null
                 },
                 keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
                 singleLine = true,
                 modifier = Modifier
                     .fillMaxWidth()
                     .testTag(GoalEditTestTags.TargetField),
             )
 
             GoalDateField(
                 label = stringResource(Res.string.goal_edit_target_date_label),
                 value = state.targetDate?.let(::formatGoalDate)
                     ?: stringResource(Res.string.goal_edit_target_date_placeholder),
                 onClick = { onEvent(GoalEditEvent.OnDatePickerOpen) },
                 trailing = if (state.targetDate == null) {
                     null
                 } else {
                     {
                         TextButton(onClick = { onEvent(GoalEditEvent.OnTargetDateCleared) }) {
                             Text(stringResource(Res.string.goal_edit_target_date_clear))
                         }
                     }
                 },
             )
 
             state.error?.let { error ->
                 Text(
                     text = stringResource(error),
                     style = AppTheme.typography.bodyMedium,
                     color = AppTheme.materialColors.error,
                 )
             }
 
             SurferSectionLabel(stringResource(Res.string.goal_edit_icon_label))
             SurferEmojiPicker(
                 selected = state.emoji,
                 onSelect = { onEvent(GoalEditEvent.OnEmojiSelected(it)) },
             )
 
             SurferSectionLabel(stringResource(Res.string.goal_edit_color_label))
             SurferGoalHueRow(
                 selected = state.hue,
                 onSelect = { onEvent(GoalEditEvent.OnHueSelected(it)) },
             )
         }
     }
 
     if (state.showDatePicker) {
         GoalDatePickerDialog(
             initialDate = state.targetDate,
             onDismiss = { onEvent(GoalEditEvent.OnDatePickerDismiss) },
             onConfirm = {
                 onEvent(GoalEditEvent.OnTargetDateChanged(it))
                 onEvent(GoalEditEvent.OnDatePickerDismiss)
             },
         )
     }
 }
 
 @Preview
 @Composable
 private fun GoalEditScreenPreview() {
     SurferComponentPreview {
         GoalEditContent(
             state = GoalEditState(
                 title = "Lisbon trip",
                 emoji = "🏖️",
                 hue = 30,
                 target = "2400",
                 targetDate = LocalDate(2026, 9, 1),
             ),
             onEvent = {},
         )
     }
 }