Coverage Summary for Class: GoalsScreenKt (com.georgeci.moneysurfer.feature.goal.list)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| GoalsScreenKt |
0%
(0/12)
|
0%
(0/54)
|
0%
(0/53)
|
0%
(0/906)
|
| GoalsScreenKt$GoalsList$1$1$2$1$2$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/11)
|
| GoalsScreenKt$GoalsList$lambda$0$0$$inlined$items$default$1 |
0%
(0/1)
|
|
| GoalsScreenKt$GoalsList$lambda$0$0$$inlined$items$default$2 |
0%
(0/1)
|
|
| GoalsScreenKt$GoalsList$lambda$0$0$$inlined$items$default$3 |
0%
(0/1)
|
|
| GoalsScreenKt$GoalsList$lambda$0$0$$inlined$items$default$4 |
0%
(0/1)
|
|
| Total |
0%
(0/17)
|
0%
(0/54)
|
0%
(0/54)
|
0%
(0/917)
|
package com.georgeci.moneysurfer.feature.goal.list
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Scaffold
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.tooling.preview.Preview
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.georgeci.moneysurfer.domain.model.GoalStatus
import com.georgeci.moneysurfer.domain.primitives.GoalId
import com.georgeci.moneysurfer.feature.goal.formatGoalDate
import com.georgeci.moneysurfer.feature.goal.labelRes
import com.georgeci.moneysurfer.feature.goal.toUi
import com.georgeci.moneysurfer.uikit.components.SurferEmptyState
import com.georgeci.moneysurfer.uikit.components.base.SurferAddFab
import com.georgeci.moneysurfer.uikit.components.base.SurferToolbar
import com.georgeci.moneysurfer.uikit.components.goal.SurferGoalCard
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.AsyncState
import com.georgeci.moneysurfer.utils.HandleSideEffect
import kotlinx.datetime.LocalDate
import moneysurfer.feature.goal.generated.resources.Res
import moneysurfer.feature.goal.generated.resources.goals_add
import moneysurfer.feature.goal.generated.resources.goals_card_deadline
import moneysurfer.feature.goal.generated.resources.goals_empty_subtitle
import moneysurfer.feature.goal.generated.resources.goals_empty_title
import moneysurfer.feature.goal.generated.resources.goals_title
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
/**
* Stable selectors for the Goals list — see docs/testing/testing-strategy.md.
*
* The FAB is always composed, so on an empty list it sits alongside the empty-state CTA that
* fires the same event. [AddButton] names the FAB, which is the one reachable in both states;
* the empty-state CTA carries no tag because no flow needs to single it out. Add one — through
* `SurferEmptyState`'s `modifier` — if that changes.
*/
object GoalsTestTags {
const val Root = "goals:root"
const val AddButton = "goals:add"
}
@Composable
fun GoalsScreen(
onNavigateBack: () -> Unit,
onNavigateToGoalCreation: () -> Unit,
onNavigateToGoalDetails: (GoalId) -> Unit,
viewModel: GoalsViewModel = koinViewModel(),
) {
val state by viewModel.collectAsStateWithLifecycle()
viewModel.HandleSideEffect { effect ->
when (effect) {
GoalsEffect.NavigateBack -> onNavigateBack()
GoalsEffect.NavigateToGoalCreation -> onNavigateToGoalCreation()
is GoalsEffect.NavigateToGoalDetails -> onNavigateToGoalDetails(effect.goalId)
}
}
GoalsContent(
goals = (state as? AsyncState.Content)?.value?.goals.orEmpty(),
isLoading = state is AsyncState.Loading,
onEvent = viewModel::onEvent,
)
}
@Composable
private fun GoalsContent(
goals: List<GoalListItem>,
isLoading: Boolean,
onEvent: (GoalsEvent) -> Unit,
) {
Scaffold(
modifier = Modifier
.surferSafeInsets()
.testTag(GoalsTestTags.Root)
.surferTestTagAsId(),
containerColor = AppTheme.materialColors.surface,
topBar = {
SurferToolbar(
title = stringResource(Res.string.goals_title),
onBack = { onEvent(GoalsEvent.OnBackClick) },
)
},
floatingActionButton = {
val addLabel = stringResource(Res.string.goals_add)
SurferAddFab(
label = addLabel,
onClick = { onEvent(GoalsEvent.OnAddGoalClick) },
modifier = Modifier.testTag(GoalsTestTags.AddButton),
)
},
) { padding ->
when {
isLoading -> Box(modifier = Modifier.fillMaxSize().padding(padding))
goals.isEmpty() -> Box(
modifier = Modifier.fillMaxSize().surferContentContainer().padding(padding),
contentAlignment = Alignment.Center,
) {
SurferEmptyState(
icon = SurferIcons.Savings,
title = stringResource(Res.string.goals_empty_title),
subtitle = stringResource(Res.string.goals_empty_subtitle),
actionLabel = stringResource(Res.string.goals_add),
onActionClick = { onEvent(GoalsEvent.OnAddGoalClick) },
)
}
else -> GoalsList(goals = goals, padding = padding, onEvent = onEvent)
}
}
}
@Composable
private fun GoalsList(
goals: List<GoalListItem>,
padding: PaddingValues,
onEvent: (GoalsEvent) -> Unit,
) {
LazyColumn(
modifier = Modifier.fillMaxSize().surferContentContainer().padding(padding),
contentPadding = PaddingValues(
start = AppTheme.spacing.default,
end = AppTheme.spacing.default,
top = AppTheme.spacing.small,
bottom = AppTheme.spacing.xxLarge,
),
) {
items(goals, key = { it.id.value }) { goal ->
Box(modifier = Modifier.padding(bottom = AppTheme.spacing.medium)) {
SurferGoalCard(
title = goal.title,
emoji = goal.emoji,
hue = goal.hue,
status = goal.status.toUi(),
statusLabel = stringResource(goal.status.labelRes()),
progress = goal.progress,
savedLabel = goal.savedLabel,
targetLabel = goal.targetLabel,
percentLabel = goal.percentLabel,
caption = goal.targetDate?.let {
stringResource(Res.string.goals_card_deadline, formatGoalDate(it))
},
onClick = { onEvent(GoalsEvent.OnGoalClick(goal.id)) },
)
}
}
}
}
@Preview
@Composable
private fun GoalsScreenPreview() {
SurferComponentPreview {
GoalsContent(
goals = listOf(
GoalListItem(
id = GoalId("1"),
title = "Emergency fund",
emoji = "☂️",
hue = 210,
status = GoalStatus.ACTIVE,
targetDate = LocalDate(2026, 8, 1),
progress = 0.74f,
percentLabel = "74%",
savedLabel = "€8,915",
targetLabel = "€12,000",
),
),
isLoading = false,
onEvent = {},
)
}
}