Coverage Summary for Class: DashboardInsightsWidgetKt (com.georgeci.moneysurfer.feature.dashboard)

Class Method, % Branch, % Line, % Instruction, %
DashboardInsightsWidgetKt 100% (4/4) 86.2% (25/29) 95% (38/40) 97% (509/525)
DashboardInsightsWidgetKt$WhenMappings
Total 100% (4/4) 86.2% (25/29) 95% (38/40) 97% (509/525)


 package com.georgeci.moneysurfer.feature.dashboard
 
 import androidx.compose.foundation.layout.defaultMinSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import com.georgeci.moneysurfer.domain.insight.InsightTone
 import com.georgeci.moneysurfer.uikit.icons.SurferIcons
 import com.georgeci.moneysurfer.uikit.widgets.SurferInsightItem
 import com.georgeci.moneysurfer.uikit.widgets.SurferInsightTone
 import com.georgeci.moneysurfer.uikit.widgets.SurferInsightsVariant
 import com.georgeci.moneysurfer.uikit.widgets.SurferInsightsWidget
 import moneysurfer.feature.dashboard.generated.resources.Res
 import moneysurfer.feature.dashboard.generated.resources.dashboard_insights_category_down_title
 import moneysurfer.feature.dashboard.generated.resources.dashboard_insights_category_up_title
 import moneysurfer.feature.dashboard.generated.resources.dashboard_insights_comparison_body
 import moneysurfer.feature.dashboard.generated.resources.dashboard_insights_empty_text
 import moneysurfer.feature.dashboard.generated.resources.dashboard_insights_period_down_title
 import moneysurfer.feature.dashboard.generated.resources.dashboard_insights_period_flat_title
 import moneysurfer.feature.dashboard.generated.resources.dashboard_insights_period_up_title
 import moneysurfer.feature.dashboard.generated.resources.dashboard_insights_subscriptions_body
 import moneysurfer.feature.dashboard.generated.resources.dashboard_insights_subscriptions_title
 import moneysurfer.feature.dashboard.generated.resources.dashboard_insights_title
 import moneysurfer.feature.dashboard.generated.resources.dashboard_insights_uncategorized
 import org.jetbrains.compose.resources.pluralStringResource
 import org.jetbrains.compose.resources.stringResource
 
 /**
  * The generated insights, and the copy that turns each rule's finding into a sentence.
  *
  * Lives beside `DashboardScreen` rather than in it: six sentence shapes is more copy than any other
  * widget on the dashboard needs, and the screen's job is to route a widget type to one call.
  *
  * Draws its own empty state rather than standing down the way the quick-actions row does —
  * "nothing notable this period" is an answer, and a widget the user switched on should not vanish
  * because the engine had a quiet month.
  */
 @Composable
 internal fun InsightsWidget(state: DashboardState.Content, variant: String?) {
     SurferInsightsWidget(
         items = state.insights.map { it.toWidgetItem() },
         title = stringResource(Res.string.dashboard_insights_title),
         variant = SurferInsightsVariant.fromKey(variant),
         emptyText = stringResource(Res.string.dashboard_insights_empty_text),
         modifier = Modifier
             .fillMaxWidth()
             .padding(horizontal = 16.dp)
             // Only the empty state needs a floor to centre itself in; cards size themselves, and
             // a compact carousel showing one of them would otherwise hang dead space underneath.
             .defaultMinSize(minHeight = if (state.insights.isEmpty()) DASHBOARD_WIDGET_MIN_HEIGHT else Dp.Unspecified)
             .padding(vertical = 8.dp)
             .testTag(DashboardTestTags.Insights),
     )
 }
 
 /**
  * The card an insight draws. The rule that produced it already chose the tone in `domain`; this
  * side picks the words and the icon that go with it.
  */
 @Composable
 private fun InsightUi.toWidgetItem(): SurferInsightItem = SurferInsightItem(
     id = id,
     tone = when (tone) {
         InsightTone.Good -> SurferInsightTone.Good
         InsightTone.Warn -> SurferInsightTone.Warn
         InsightTone.Neutral -> SurferInsightTone.Neutral
     },
     icon = when (kind) {
         InsightKind.CategoryUp, InsightKind.PeriodUp -> SurferIcons.ArrowUp
         InsightKind.CategoryDown, InsightKind.PeriodDown -> SurferIcons.ArrowDown
         InsightKind.PeriodFlat -> SurferIcons.Calendar
         InsightKind.Subscriptions -> SurferIcons.Sync
     },
     title = insightTitle(),
     body = insightBody(),
 )
 
 @Composable
 private fun InsightUi.insightTitle(): String {
     // A slice with no category is a real slice, so it gets a name rather than being skipped.
     val subject = label ?: stringResource(Res.string.dashboard_insights_uncategorized)
     return when (kind) {
         InsightKind.CategoryUp ->
             stringResource(Res.string.dashboard_insights_category_up_title, subject, percent)
         InsightKind.CategoryDown ->
             stringResource(Res.string.dashboard_insights_category_down_title, subject, percent)
         InsightKind.PeriodUp -> stringResource(Res.string.dashboard_insights_period_up_title, percent)
         InsightKind.PeriodDown -> stringResource(Res.string.dashboard_insights_period_down_title, percent)
         InsightKind.PeriodFlat -> stringResource(Res.string.dashboard_insights_period_flat_title)
         InsightKind.Subscriptions ->
             pluralStringResource(Res.plurals.dashboard_insights_subscriptions_title, count, count)
     }
 }
 
 @Composable
 private fun InsightUi.insightBody(): String = when (kind) {
     InsightKind.CategoryUp,
     InsightKind.CategoryDown,
     InsightKind.PeriodUp,
     InsightKind.PeriodDown,
     InsightKind.PeriodFlat,
     -> stringResource(Res.string.dashboard_insights_comparison_body, amount, comparison)
     InsightKind.Subscriptions ->
         stringResource(Res.string.dashboard_insights_subscriptions_body, amount)
 }