feat(settings): implement SettingsScreen with ViewModel, UI and persistence
Implement the full Settings tab with household size and daily kcal/person input fields, persisted via Room through the existing SettingsRepository. The DashboardViewModel now reads settings reactively and passes them to CalculateSupplyRangeUseCase instead of using hardcoded defaults. Changes: - Add observeValue(key) Flow method to SettingsDao and SettingsRepository - Create SettingsViewModel with load/save logic and input validation - Create SettingsUiState data class - Replace SettingsScreen placeholder with full Compose UI (household size, kcal/day fields, save button, export/import placeholders) - Integrate settings into DashboardViewModel via combine() with 4 flows - Add SettingsViewModelTest (6 tests covering defaults, persistence, validation) - Update DashboardViewModelTest and test fakes for new constructor parameter Closes #35
This commit is contained in:
parent
34bd1f603f
commit
e85b151cd5
11 changed files with 443 additions and 15 deletions
|
|
@ -15,6 +15,9 @@ internal interface SettingsDao {
|
|||
@Query("SELECT value FROM settings WHERE `key` = :key")
|
||||
suspend fun getValue(key: String): String?
|
||||
|
||||
@Query("SELECT value FROM settings WHERE `key` = :key")
|
||||
fun observeValue(key: String): Flow<String?>
|
||||
|
||||
@Query("SELECT * FROM settings")
|
||||
fun getAll(): Flow<List<SettingsEntity>>
|
||||
|
||||
|
|
|
|||
|
|
@ -18,5 +18,7 @@ internal class SettingsRepositoryImpl @Inject constructor(
|
|||
override suspend fun setValue(key: String, value: String) =
|
||||
withContext(Dispatchers.IO) { dao.upsert(SettingsEntity(key = key, value = value)) }
|
||||
|
||||
override fun observeValue(key: String): Flow<String?> = dao.observeValue(key)
|
||||
|
||||
override fun getAll(): Flow<List<SettingsEntity>> = dao.getAll()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,5 +6,6 @@ import kotlinx.coroutines.flow.Flow
|
|||
internal interface SettingsRepository {
|
||||
suspend fun getValue(key: String): String?
|
||||
suspend fun setValue(key: String, value: String)
|
||||
fun observeValue(key: String): Flow<String?>
|
||||
fun getAll(): Flow<List<SettingsEntity>>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ import androidx.lifecycle.viewModelScope
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import de.krisenvorrat.app.domain.repository.CategoryRepository
|
||||
import de.krisenvorrat.app.domain.repository.ItemRepository
|
||||
import de.krisenvorrat.app.domain.repository.SettingsRepository
|
||||
import de.krisenvorrat.app.domain.usecase.CalculateCategorySummaryUseCase
|
||||
import de.krisenvorrat.app.domain.usecase.CalculateSupplyRangeUseCase
|
||||
import de.krisenvorrat.app.domain.usecase.CalculateTotalValueUseCase
|
||||
import de.krisenvorrat.app.domain.usecase.GetExpiryWarningsUseCase
|
||||
import de.krisenvorrat.app.domain.usecase.GetMinStockWarningsUseCase
|
||||
import de.krisenvorrat.app.ui.settings.SettingsViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
|
@ -22,6 +24,7 @@ import javax.inject.Inject
|
|||
internal class DashboardViewModel @Inject constructor(
|
||||
private val itemRepository: ItemRepository,
|
||||
private val categoryRepository: CategoryRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val calculateCategorySummary: CalculateCategorySummaryUseCase,
|
||||
private val calculateTotalValue: CalculateTotalValueUseCase,
|
||||
private val calculateSupplyRange: CalculateSupplyRangeUseCase,
|
||||
|
|
@ -36,12 +39,19 @@ internal class DashboardViewModel @Inject constructor(
|
|||
viewModelScope.launch {
|
||||
combine(
|
||||
itemRepository.getAll(),
|
||||
categoryRepository.getAll()
|
||||
) { items, categories ->
|
||||
categoryRepository.getAll(),
|
||||
settingsRepository.observeValue(SettingsViewModel.KEY_HOUSEHOLD_SIZE),
|
||||
settingsRepository.observeValue(SettingsViewModel.KEY_DAILY_KCAL_PER_PERSON)
|
||||
) { items, categories, householdSizeStr, dailyKcalStr ->
|
||||
val householdSize = householdSizeStr?.toIntOrNull()
|
||||
?: CalculateSupplyRangeUseCase.DEFAULT_HOUSEHOLD_SIZE
|
||||
val dailyKcal = dailyKcalStr?.toIntOrNull()
|
||||
?: CalculateSupplyRangeUseCase.DEFAULT_DAILY_KCAL_PER_PERSON
|
||||
|
||||
DashboardUiState(
|
||||
categorySummaries = calculateCategorySummary(items, categories),
|
||||
totalValue = calculateTotalValue(items),
|
||||
supplyRangeDays = calculateSupplyRange(items),
|
||||
supplyRangeDays = calculateSupplyRange(items, householdSize, dailyKcal),
|
||||
expiryWarnings = getExpiryWarnings(items),
|
||||
minStockWarnings = getMinStockWarnings(items),
|
||||
isLoading = false
|
||||
|
|
|
|||
|
|
@ -1,34 +1,144 @@
|
|||
package de.krisenvorrat.app.ui.settings
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
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.LocalContext
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun SettingsScreen() {
|
||||
internal fun SettingsScreen(
|
||||
viewModel: SettingsViewModel = hiltViewModel()
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
TopAppBar(title = { Text("Einstellungen") })
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Einstellungen werden in einem späteren Schritt implementiert.",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
|
||||
if (uiState.isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
)
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Haushalt",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = uiState.householdSize,
|
||||
onValueChange = viewModel::onHouseholdSizeChanged,
|
||||
label = { Text("Personenzahl") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = uiState.dailyKcalPerPerson,
|
||||
onValueChange = viewModel::onDailyKcalChanged,
|
||||
label = { Text("kcal pro Tag pro Person") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(
|
||||
onClick = viewModel::saveSettings,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Speichern")
|
||||
}
|
||||
|
||||
if (uiState.isSaved) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Einstellungen gespeichert",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "Daten",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Export wird in einem späteren Update verfügbar.",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Daten exportieren")
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Import wird in einem späteren Update verfügbar.",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Daten importieren")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package de.krisenvorrat.app.ui.settings
|
||||
|
||||
internal data class SettingsUiState(
|
||||
val householdSize: String = "",
|
||||
val dailyKcalPerPerson: String = "",
|
||||
val isLoading: Boolean = true,
|
||||
val isSaved: Boolean = false
|
||||
)
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package de.krisenvorrat.app.ui.settings
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import de.krisenvorrat.app.domain.repository.SettingsRepository
|
||||
import de.krisenvorrat.app.domain.usecase.CalculateSupplyRangeUseCase
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class SettingsViewModel @Inject constructor(
|
||||
private val settingsRepository: SettingsRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(SettingsUiState())
|
||||
val uiState: StateFlow<SettingsUiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
loadSettings()
|
||||
}
|
||||
|
||||
private fun loadSettings() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val householdSize = settingsRepository.getValue(KEY_HOUSEHOLD_SIZE)
|
||||
?: CalculateSupplyRangeUseCase.DEFAULT_HOUSEHOLD_SIZE.toString()
|
||||
val dailyKcal = settingsRepository.getValue(KEY_DAILY_KCAL_PER_PERSON)
|
||||
?: CalculateSupplyRangeUseCase.DEFAULT_DAILY_KCAL_PER_PERSON.toString()
|
||||
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
householdSize = householdSize,
|
||||
dailyKcalPerPerson = dailyKcal,
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
householdSize = CalculateSupplyRangeUseCase.DEFAULT_HOUSEHOLD_SIZE.toString(),
|
||||
dailyKcalPerPerson = CalculateSupplyRangeUseCase.DEFAULT_DAILY_KCAL_PER_PERSON.toString(),
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onHouseholdSizeChanged(value: String) {
|
||||
_uiState.update { it.copy(householdSize = value, isSaved = false) }
|
||||
}
|
||||
|
||||
fun onDailyKcalChanged(value: String) {
|
||||
_uiState.update { it.copy(dailyKcalPerPerson = value, isSaved = false) }
|
||||
}
|
||||
|
||||
fun saveSettings() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val householdSize = _uiState.value.householdSize
|
||||
val dailyKcal = _uiState.value.dailyKcalPerPerson
|
||||
|
||||
if (householdSize.toIntOrNull() != null && householdSize.toInt() > 0) {
|
||||
settingsRepository.setValue(KEY_HOUSEHOLD_SIZE, householdSize)
|
||||
}
|
||||
if (dailyKcal.toIntOrNull() != null && dailyKcal.toInt() > 0) {
|
||||
settingsRepository.setValue(KEY_DAILY_KCAL_PER_PERSON, dailyKcal)
|
||||
}
|
||||
|
||||
_uiState.update { it.copy(isSaved = true) }
|
||||
} catch (e: Exception) {
|
||||
// Fehler beim Speichern – UI zeigt keinen Saved-Status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_HOUSEHOLD_SIZE = "household_size"
|
||||
const val KEY_DAILY_KCAL_PER_PERSON = "daily_kcal_per_person"
|
||||
}
|
||||
}
|
||||
|
|
@ -92,6 +92,7 @@ internal class FakeSettingsDao : SettingsDao {
|
|||
|
||||
override suspend fun upsert(setting: SettingsEntity) = throw UnsupportedOperationException()
|
||||
override suspend fun getValue(key: String): String? = throw UnsupportedOperationException()
|
||||
override fun observeValue(key: String): Flow<String?> = throw UnsupportedOperationException()
|
||||
override fun getAll(): Flow<List<SettingsEntity>> = flow
|
||||
|
||||
override suspend fun upsertAll(settings: List<SettingsEntity>) {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ private class FakeSettingsDao : SettingsDao {
|
|||
|
||||
override suspend fun getValue(key: String): String? = store[key]
|
||||
|
||||
override fun observeValue(key: String): Flow<String?> =
|
||||
MutableStateFlow(store[key])
|
||||
|
||||
override fun getAll(): Flow<List<SettingsEntity>> = flow
|
||||
|
||||
override suspend fun upsertAll(settings: List<SettingsEntity>) {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ package de.krisenvorrat.app.ui.dashboard
|
|||
|
||||
import de.krisenvorrat.app.data.db.entity.CategoryEntity
|
||||
import de.krisenvorrat.app.data.db.entity.ItemEntity
|
||||
import de.krisenvorrat.app.data.db.entity.SettingsEntity
|
||||
import de.krisenvorrat.app.domain.repository.CategoryRepository
|
||||
import de.krisenvorrat.app.domain.repository.ItemRepository
|
||||
import de.krisenvorrat.app.domain.repository.SettingsRepository
|
||||
import de.krisenvorrat.app.domain.usecase.CalculateCategorySummaryUseCase
|
||||
import de.krisenvorrat.app.domain.usecase.CalculateSupplyRangeUseCase
|
||||
import de.krisenvorrat.app.domain.usecase.CalculateTotalValueUseCase
|
||||
|
|
@ -32,6 +34,7 @@ class DashboardViewModelTest {
|
|||
private val testDispatcher = StandardTestDispatcher()
|
||||
private lateinit var fakeItemRepository: FakeItemRepository
|
||||
private lateinit var fakeCategoryRepository: FakeCategoryRepository
|
||||
private lateinit var fakeSettingsRepository: FakeSettingsRepository
|
||||
private lateinit var viewModel: DashboardViewModel
|
||||
|
||||
@Before
|
||||
|
|
@ -39,6 +42,7 @@ class DashboardViewModelTest {
|
|||
Dispatchers.setMain(testDispatcher)
|
||||
fakeItemRepository = FakeItemRepository()
|
||||
fakeCategoryRepository = FakeCategoryRepository()
|
||||
fakeSettingsRepository = FakeSettingsRepository()
|
||||
}
|
||||
|
||||
@After
|
||||
|
|
@ -49,6 +53,7 @@ class DashboardViewModelTest {
|
|||
private fun createViewModel() = DashboardViewModel(
|
||||
itemRepository = fakeItemRepository,
|
||||
categoryRepository = fakeCategoryRepository,
|
||||
settingsRepository = fakeSettingsRepository,
|
||||
calculateCategorySummary = CalculateCategorySummaryUseCase(),
|
||||
calculateTotalValue = CalculateTotalValueUseCase(),
|
||||
calculateSupplyRange = CalculateSupplyRangeUseCase(),
|
||||
|
|
@ -318,4 +323,23 @@ private class FakeCategoryRepository : CategoryRepository {
|
|||
}
|
||||
}
|
||||
|
||||
private class FakeSettingsRepository : SettingsRepository {
|
||||
private val store = mutableMapOf<String, String>()
|
||||
private val observeFlows = mutableMapOf<String, MutableStateFlow<String?>>()
|
||||
private val allFlow = MutableStateFlow<List<SettingsEntity>>(emptyList())
|
||||
|
||||
override suspend fun getValue(key: String): String? = store[key]
|
||||
|
||||
override suspend fun setValue(key: String, value: String) {
|
||||
store[key] = value
|
||||
observeFlows[key]?.value = value
|
||||
allFlow.value = store.map { SettingsEntity(key = it.key, value = it.value) }
|
||||
}
|
||||
|
||||
override fun observeValue(key: String): Flow<String?> =
|
||||
observeFlows.getOrPut(key) { MutableStateFlow(store[key]) }
|
||||
|
||||
override fun getAll(): Flow<List<SettingsEntity>> = allFlow
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
|
|
|||
|
|
@ -0,0 +1,180 @@
|
|||
package de.krisenvorrat.app.ui.settings
|
||||
|
||||
import de.krisenvorrat.app.data.db.entity.SettingsEntity
|
||||
import de.krisenvorrat.app.domain.repository.SettingsRepository
|
||||
import de.krisenvorrat.app.domain.usecase.CalculateSupplyRangeUseCase
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class SettingsViewModelTest {
|
||||
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
private lateinit var fakeSettingsRepository: FakeSettingsRepository
|
||||
private lateinit var viewModel: SettingsViewModel
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
fakeSettingsRepository = FakeSettingsRepository()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
private fun createViewModel() = SettingsViewModel(
|
||||
settingsRepository = fakeSettingsRepository
|
||||
)
|
||||
|
||||
@Test
|
||||
fun test_init_withNoStoredValues_showsDefaults() = runTest(testDispatcher) {
|
||||
// Given – no stored settings
|
||||
viewModel = createViewModel()
|
||||
|
||||
// When
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
val state = viewModel.uiState.value
|
||||
assertFalse(state.isLoading)
|
||||
assertEquals(
|
||||
CalculateSupplyRangeUseCase.DEFAULT_HOUSEHOLD_SIZE.toString(),
|
||||
state.householdSize
|
||||
)
|
||||
assertEquals(
|
||||
CalculateSupplyRangeUseCase.DEFAULT_DAILY_KCAL_PER_PERSON.toString(),
|
||||
state.dailyKcalPerPerson
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_init_withStoredValues_showsStoredValues() = runTest(testDispatcher) {
|
||||
// Given
|
||||
fakeSettingsRepository.store[SettingsViewModel.KEY_HOUSEHOLD_SIZE] = "4"
|
||||
fakeSettingsRepository.store[SettingsViewModel.KEY_DAILY_KCAL_PER_PERSON] = "2500"
|
||||
|
||||
viewModel = createViewModel()
|
||||
|
||||
// When
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
val state = viewModel.uiState.value
|
||||
assertEquals("4", state.householdSize)
|
||||
assertEquals("2500", state.dailyKcalPerPerson)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_onHouseholdSizeChanged_updatesState() = runTest(testDispatcher) {
|
||||
// Given
|
||||
viewModel = createViewModel()
|
||||
advanceUntilIdle()
|
||||
|
||||
// When
|
||||
viewModel.onHouseholdSizeChanged("5")
|
||||
|
||||
// Then
|
||||
assertEquals("5", viewModel.uiState.value.householdSize)
|
||||
assertFalse(viewModel.uiState.value.isSaved)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_onDailyKcalChanged_updatesState() = runTest(testDispatcher) {
|
||||
// Given
|
||||
viewModel = createViewModel()
|
||||
advanceUntilIdle()
|
||||
|
||||
// When
|
||||
viewModel.onDailyKcalChanged("1800")
|
||||
|
||||
// Then
|
||||
assertEquals("1800", viewModel.uiState.value.dailyKcalPerPerson)
|
||||
assertFalse(viewModel.uiState.value.isSaved)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_saveSettings_withValidValues_persistsAndShowsSaved() = runTest(testDispatcher) {
|
||||
// Given
|
||||
viewModel = createViewModel()
|
||||
advanceUntilIdle()
|
||||
viewModel.onHouseholdSizeChanged("3")
|
||||
viewModel.onDailyKcalChanged("2200")
|
||||
|
||||
// When
|
||||
viewModel.saveSettings()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
assertTrue(viewModel.uiState.value.isSaved)
|
||||
assertEquals("3", fakeSettingsRepository.store[SettingsViewModel.KEY_HOUSEHOLD_SIZE])
|
||||
assertEquals("2200", fakeSettingsRepository.store[SettingsViewModel.KEY_DAILY_KCAL_PER_PERSON])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_saveSettings_withInvalidHouseholdSize_doesNotPersistInvalid() = runTest(testDispatcher) {
|
||||
// Given
|
||||
viewModel = createViewModel()
|
||||
advanceUntilIdle()
|
||||
viewModel.onHouseholdSizeChanged("abc")
|
||||
viewModel.onDailyKcalChanged("2200")
|
||||
|
||||
// When
|
||||
viewModel.saveSettings()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then – household_size not stored, but dailyKcal is
|
||||
assertFalse(fakeSettingsRepository.store.containsKey(SettingsViewModel.KEY_HOUSEHOLD_SIZE))
|
||||
assertEquals("2200", fakeSettingsRepository.store[SettingsViewModel.KEY_DAILY_KCAL_PER_PERSON])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_saveSettings_withZeroHouseholdSize_doesNotPersist() = runTest(testDispatcher) {
|
||||
// Given
|
||||
viewModel = createViewModel()
|
||||
advanceUntilIdle()
|
||||
viewModel.onHouseholdSizeChanged("0")
|
||||
|
||||
// When
|
||||
viewModel.saveSettings()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then – zero is invalid, not persisted
|
||||
assertFalse(fakeSettingsRepository.store.containsKey(SettingsViewModel.KEY_HOUSEHOLD_SIZE))
|
||||
}
|
||||
}
|
||||
|
||||
// region Test Helpers
|
||||
|
||||
private class FakeSettingsRepository : SettingsRepository {
|
||||
val store = mutableMapOf<String, String>()
|
||||
private val allFlow = MutableStateFlow<List<SettingsEntity>>(emptyList())
|
||||
|
||||
override suspend fun getValue(key: String): String? = store[key]
|
||||
|
||||
override suspend fun setValue(key: String, value: String) {
|
||||
store[key] = value
|
||||
allFlow.value = store.map { SettingsEntity(key = it.key, value = it.value) }
|
||||
}
|
||||
|
||||
override fun observeValue(key: String): Flow<String?> =
|
||||
MutableStateFlow(store[key])
|
||||
|
||||
override fun getAll(): Flow<List<SettingsEntity>> = allFlow
|
||||
}
|
||||
|
||||
// endregion
|
||||
Loading…
Reference in a new issue