Coverage Summary for Class: SurferErrorStateKt (com.georgeci.moneysurfer.uikit.components)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| SurferErrorStateKt |
100%
(1/1)
|
33.3%
(1/3)
|
55.6%
(10/18)
|
93.8%
(45/48)
|
93.3%
(470/504)
|
package com.georgeci.moneysurfer.uikit.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.georgeci.moneysurfer.uikit.icons.SurferIcons
import com.georgeci.moneysurfer.uikit.preview.SurferComponentPreview
import com.georgeci.moneysurfer.uikit.semantics.SurferSemantics
import com.georgeci.moneysurfer.uikit.theme.AppTheme
/**
* Centered error-state block: an error-tinted icon medallion, a title, an optional subtitle
* and an optional retry action. Mirrors [SurferEmptyState] but signals a failed load rather
* than absent content — use [SurferEmptyState] when the load succeeded but produced nothing.
*
* @param onRetry when non-null, renders a retry button; override [retryLabel] to relabel it.
*/
@Composable
fun SurferErrorState(
title: String,
modifier: Modifier = Modifier,
subtitle: String? = null,
icon: ImageVector = SurferIcons.Info,
retryLabel: String = "Retry",
onRetry: (() -> Unit)? = null,
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(AppTheme.spacing.large),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppTheme.spacing.small),
) {
Box(
modifier = Modifier
.size(64.dp)
.clip(CircleShape)
.background(AppTheme.materialColors.errorContainer),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = icon,
contentDescription = SurferSemantics.Decorative,
tint = AppTheme.materialColors.onErrorContainer,
modifier = Modifier.size(28.dp),
)
}
Text(
text = title,
style = AppTheme.typography.titleMedium,
color = AppTheme.materialColors.onSurface,
textAlign = TextAlign.Center,
)
if (subtitle != null) {
Text(
text = subtitle,
style = AppTheme.typography.bodyMedium,
color = AppTheme.materialColors.onSurfaceVariant,
textAlign = TextAlign.Center,
)
}
if (onRetry != null) {
SurferButton(
text = retryLabel,
onClick = onRetry,
style = SurferButtonStyle.Tonal,
modifier = Modifier
.padding(top = AppTheme.spacing.xSmall)
.testTag(SurferErrorStateTestTags.RetryButton),
)
}
}
}
object SurferErrorStateTestTags {
const val RetryButton = "errorState:retry"
}
@Preview
@Composable
private fun SurferErrorStatePreview() {
SurferComponentPreview {
SurferErrorState(
title = "Something went wrong",
subtitle = "We couldn't load your accounts. Check your connection and try again.",
onRetry = {},
)
}
}
@Preview
@Composable
private fun SurferErrorStateNoRetryPreview() {
SurferComponentPreview {
SurferErrorState(title = "Failed to sync")
}
}