[PM-12595] add notification badge to settings item in settings tab screen (#3976)

This commit is contained in:
Dave Severns 2024-09-27 11:37:30 -04:00 committed by GitHub
parent 70e75b910c
commit 853f25bf57
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 246 additions and 59 deletions

View file

@ -1,15 +1,18 @@
package com.x8bit.bitwarden.ui.platform.feature.settings package com.x8bit.bitwarden.ui.platform.feature.settings
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -20,6 +23,7 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.ripple import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -29,12 +33,14 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.bottomDivider import com.x8bit.bitwarden.ui.platform.base.util.bottomDivider
import com.x8bit.bitwarden.ui.platform.base.util.mirrorIfRtl import com.x8bit.bitwarden.ui.platform.base.util.mirrorIfRtl
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenMediumTopAppBar import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenMediumTopAppBar
import com.x8bit.bitwarden.ui.platform.components.badge.NotificationBadge
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@ -53,6 +59,7 @@ fun SettingsScreen(
onNavigateToVault: () -> Unit, onNavigateToVault: () -> Unit,
viewModel: SettingsViewModel = hiltViewModel(), viewModel: SettingsViewModel = hiltViewModel(),
) { ) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
EventsEffect(viewModel = viewModel) { event -> EventsEffect(viewModel = viewModel) { event ->
when (event) { when (event) {
SettingsEvent.NavigateAbout -> onNavigateToAbout() SettingsEvent.NavigateAbout -> onNavigateToAbout()
@ -82,16 +89,20 @@ fun SettingsScreen(
.fillMaxSize() .fillMaxSize()
.verticalScroll(state = rememberScrollState()), .verticalScroll(state = rememberScrollState()),
) { ) {
Settings.entries.forEach { Settings.entries.forEach { settingEntry ->
SettingsRow( SettingsRow(
text = it.text, text = settingEntry.text,
onClick = remember(viewModel) { onClick = remember(viewModel) {
{ viewModel.trySendAction(SettingsAction.SettingsClick(it)) } { viewModel.trySendAction(SettingsAction.SettingsClick(settingEntry)) }
}, },
modifier = Modifier modifier = Modifier
.testTag(it.testTag) .testTag(settingEntry.testTag)
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.fillMaxWidth(), .fillMaxWidth(),
notificationCount = state.notificationBadgeCountMap.getOrDefault(
key = settingEntry,
defaultValue = 0,
),
) )
} }
} }
@ -103,6 +114,7 @@ private fun SettingsRow(
text: Text, text: Text,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
notificationCount: Int,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@ -126,6 +138,27 @@ private fun SettingsRow(
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
) )
TrailingContent(notificationCount = notificationCount)
}
}
@Composable
private fun TrailingContent(
notificationCount: Int,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
val notificationBadgeVisible = notificationCount > 0
NotificationBadge(
notificationCount = notificationCount,
isVisible = notificationBadgeVisible,
)
if (notificationBadgeVisible) {
Spacer(modifier = Modifier.width(12.dp))
}
Icon( Icon(
painter = rememberVectorPainter(id = R.drawable.ic_navigate_next), painter = rememberVectorPainter(id = R.drawable.ic_navigate_next),
contentDescription = null, contentDescription = null,
@ -138,18 +171,21 @@ private fun SettingsRow(
} }
@Preview @Preview
@Preview(name = "Right-To-Left", locale = "ar")
@Composable @Composable
private fun SettingsRows_preview() { private fun SettingsRows_preview() {
BitwardenTheme { BitwardenTheme {
Column( Column(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.padding(16.dp) .padding(16.dp)
.fillMaxSize(), .fillMaxSize(),
) { ) {
Settings.entries.forEach { Settings.entries.forEachIndexed { index, it ->
SettingsRow( SettingsRow(
text = it.text, text = it.text,
onClick = { }, onClick = { },
notificationCount = index % 3,
) )
} }
} }

View file

@ -1,22 +1,62 @@
package com.x8bit.bitwarden.ui.platform.feature.settings package com.x8bit.bitwarden.ui.platform.feature.settings
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import javax.inject.Inject import javax.inject.Inject
/** /**
* View model for the settings screen. * View model for the settings screen.
*/ */
@HiltViewModel @HiltViewModel
class SettingsViewModel @Inject constructor() : BaseViewModel<Unit, SettingsEvent, SettingsAction>( class SettingsViewModel @Inject constructor(
initialState = Unit, settingsRepository: SettingsRepository,
) : BaseViewModel<SettingsState, SettingsEvent, SettingsAction>(
initialState = SettingsState(
securityCount = settingsRepository.allSecuritySettingsBadgeCountFlow.value,
autoFillCount = settingsRepository.allAutofillSettingsBadgeCountFlow.value,
),
) { ) {
init {
combine(
settingsRepository.allSecuritySettingsBadgeCountFlow,
settingsRepository.allAutofillSettingsBadgeCountFlow,
) { securityCount, autofillCount ->
SettingsAction.Internal.SettingsNotificationCountUpdate(
securityCount = securityCount,
autoFillCount = autofillCount,
)
}
.onEach(::sendAction)
.launchIn(viewModelScope)
}
override fun handleAction(action: SettingsAction): Unit = when (action) { override fun handleAction(action: SettingsAction): Unit = when (action) {
is SettingsAction.SettingsClick -> handleAccountSecurityClick(action) is SettingsAction.SettingsClick -> handleAccountSecurityClick(action)
is SettingsAction.Internal.SettingsNotificationCountUpdate -> {
handleSettingsNotificationCountUpdate(action)
}
}
private fun handleSettingsNotificationCountUpdate(
action: SettingsAction.Internal.SettingsNotificationCountUpdate,
) {
mutableStateFlow.update {
it.copy(
autoFillCount = action.autoFillCount,
securityCount = action.securityCount,
)
}
} }
private fun handleAccountSecurityClick(action: SettingsAction.SettingsClick) { private fun handleAccountSecurityClick(action: SettingsAction.SettingsClick) {
@ -48,6 +88,19 @@ class SettingsViewModel @Inject constructor() : BaseViewModel<Unit, SettingsEven
} }
} }
/**
* Models the state of the settings screen.
*/
data class SettingsState(
private val autoFillCount: Int,
private val securityCount: Int,
) {
val notificationBadgeCountMap: Map<Settings, Int> = mapOf(
Settings.ACCOUNT_SECURITY to autoFillCount,
Settings.AUTO_FILL to securityCount,
)
}
/** /**
* Models events for the settings screen. * Models events for the settings screen.
*/ */
@ -93,6 +146,19 @@ sealed class SettingsAction {
data class SettingsClick( data class SettingsClick(
val settings: Settings, val settings: Settings,
) : SettingsAction() ) : SettingsAction()
/**
* Models internal actions for the settings screen.
*/
sealed class Internal : SettingsAction() {
/**
* Update the notification count for the settings rows.
*/
data class SettingsNotificationCountUpdate(
val autoFillCount: Int,
val securityCount: Int,
) : Internal()
}
} }
/** /**

View file

@ -1,26 +1,33 @@
package com.x8bit.bitwarden.ui.platform.feature.settings package com.x8bit.bitwarden.ui.platform.feature.settings
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.runs import io.mockk.runs
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.update
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
class SettingsScreenTest : BaseComposeTest() { class SettingsScreenTest : BaseComposeTest() {
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val mutableEventFlow = bufferedMutableSharedFlow<SettingsEvent>()
private val viewModel = mockk<SettingsViewModel> {
every { stateFlow } returns mutableStateFlow
every { eventFlow } returns mutableEventFlow
}
@Test @Test
fun `on about row click should emit SettingsClick`() { fun `on about row click should emit SettingsClick`() {
val viewModel = mockk<SettingsViewModel> {
every { eventFlow } returns emptyFlow() every { viewModel.trySendAction(SettingsAction.SettingsClick(Settings.ABOUT)) } just runs
every { trySendAction(SettingsAction.SettingsClick(Settings.ABOUT)) } just runs
}
composeTestRule.setContent { composeTestRule.setContent {
SettingsScreen( SettingsScreen(
viewModel = viewModel, viewModel = viewModel,
@ -38,12 +45,9 @@ class SettingsScreenTest : BaseComposeTest() {
@Test @Test
fun `on account security row click should emit SettingsClick`() { fun `on account security row click should emit SettingsClick`() {
val viewModel = mockk<SettingsViewModel> { every {
every { eventFlow } returns emptyFlow() viewModel.trySendAction(SettingsAction.SettingsClick(Settings.ACCOUNT_SECURITY))
every { } just runs
trySendAction(SettingsAction.SettingsClick(Settings.ACCOUNT_SECURITY))
} just runs
}
composeTestRule.setContent { composeTestRule.setContent {
SettingsScreen( SettingsScreen(
viewModel = viewModel, viewModel = viewModel,
@ -61,10 +65,9 @@ class SettingsScreenTest : BaseComposeTest() {
@Test @Test
fun `on appearance row click should emit SettingsClick`() { fun `on appearance row click should emit SettingsClick`() {
val viewModel = mockk<SettingsViewModel> { every {
every { eventFlow } returns emptyFlow() viewModel.trySendAction(SettingsAction.SettingsClick(Settings.APPEARANCE))
every { trySendAction(SettingsAction.SettingsClick(Settings.APPEARANCE)) } just runs } just runs
}
composeTestRule.setContent { composeTestRule.setContent {
SettingsScreen( SettingsScreen(
viewModel = viewModel, viewModel = viewModel,
@ -82,10 +85,10 @@ class SettingsScreenTest : BaseComposeTest() {
@Test @Test
fun `on auto-fill row click should emit SettingsClick`() { fun `on auto-fill row click should emit SettingsClick`() {
val viewModel = mockk<SettingsViewModel> {
every { eventFlow } returns emptyFlow() every {
every { trySendAction(SettingsAction.SettingsClick(Settings.AUTO_FILL)) } just runs viewModel.trySendAction(SettingsAction.SettingsClick(Settings.AUTO_FILL))
} } just runs
composeTestRule.setContent { composeTestRule.setContent {
SettingsScreen( SettingsScreen(
viewModel = viewModel, viewModel = viewModel,
@ -103,10 +106,8 @@ class SettingsScreenTest : BaseComposeTest() {
@Test @Test
fun `on other row click should emit SettingsClick`() { fun `on other row click should emit SettingsClick`() {
val viewModel = mockk<SettingsViewModel> {
every { eventFlow } returns emptyFlow() every { viewModel.trySendAction(SettingsAction.SettingsClick(Settings.OTHER)) } just runs
every { trySendAction(SettingsAction.SettingsClick(Settings.OTHER)) } just runs
}
composeTestRule.setContent { composeTestRule.setContent {
SettingsScreen( SettingsScreen(
viewModel = viewModel, viewModel = viewModel,
@ -124,10 +125,8 @@ class SettingsScreenTest : BaseComposeTest() {
@Test @Test
fun `on vault row click should emit SettingsClick`() { fun `on vault row click should emit SettingsClick`() {
val viewModel = mockk<SettingsViewModel> {
every { eventFlow } returns emptyFlow() every { viewModel.trySendAction(SettingsAction.SettingsClick(Settings.VAULT)) } just runs
every { trySendAction(SettingsAction.SettingsClick(Settings.VAULT)) } just runs
}
composeTestRule.setContent { composeTestRule.setContent {
SettingsScreen( SettingsScreen(
viewModel = viewModel, viewModel = viewModel,
@ -146,9 +145,6 @@ class SettingsScreenTest : BaseComposeTest() {
@Test @Test
fun `on NavigateAbout should call onNavigateToAbout`() { fun `on NavigateAbout should call onNavigateToAbout`() {
var haveCalledNavigateToAbout = false var haveCalledNavigateToAbout = false
val viewModel = mockk<SettingsViewModel> {
every { eventFlow } returns flowOf(SettingsEvent.NavigateAbout)
}
composeTestRule.setContent { composeTestRule.setContent {
SettingsScreen( SettingsScreen(
viewModel = viewModel, viewModel = viewModel,
@ -162,15 +158,13 @@ class SettingsScreenTest : BaseComposeTest() {
onNavigateToVault = { }, onNavigateToVault = { },
) )
} }
mutableEventFlow.tryEmit(SettingsEvent.NavigateAbout)
assertTrue(haveCalledNavigateToAbout) assertTrue(haveCalledNavigateToAbout)
} }
@Test @Test
fun `on NavigateAccountSecurity should call onNavigateToAccountSecurity`() { fun `on NavigateAccountSecurity should call onNavigateToAccountSecurity`() {
var haveCalledNavigateToAccountSecurity = false var haveCalledNavigateToAccountSecurity = false
val viewModel = mockk<SettingsViewModel> {
every { eventFlow } returns flowOf(SettingsEvent.NavigateAccountSecurity)
}
composeTestRule.setContent { composeTestRule.setContent {
SettingsScreen( SettingsScreen(
viewModel = viewModel, viewModel = viewModel,
@ -184,15 +178,13 @@ class SettingsScreenTest : BaseComposeTest() {
onNavigateToVault = { }, onNavigateToVault = { },
) )
} }
mutableEventFlow.tryEmit(SettingsEvent.NavigateAccountSecurity)
assertTrue(haveCalledNavigateToAccountSecurity) assertTrue(haveCalledNavigateToAccountSecurity)
} }
@Test @Test
fun `on NavigateAccountSecurity should call NavigateAppearance`() { fun `on NavigateAccountSecurity should call NavigateAppearance`() {
var haveCalledNavigateToAppearance = false var haveCalledNavigateToAppearance = false
val viewModel = mockk<SettingsViewModel> {
every { eventFlow } returns flowOf(SettingsEvent.NavigateAppearance)
}
composeTestRule.setContent { composeTestRule.setContent {
SettingsScreen( SettingsScreen(
viewModel = viewModel, viewModel = viewModel,
@ -204,15 +196,13 @@ class SettingsScreenTest : BaseComposeTest() {
onNavigateToVault = { }, onNavigateToVault = { },
) )
} }
mutableEventFlow.tryEmit(SettingsEvent.NavigateAppearance)
assertTrue(haveCalledNavigateToAppearance) assertTrue(haveCalledNavigateToAppearance)
} }
@Test @Test
fun `on NavigateAccountSecurity should call onNavigateToAutoFill`() { fun `on NavigateAccountSecurity should call onNavigateToAutoFill`() {
var haveCalledNavigateToAutoFill = false var haveCalledNavigateToAutoFill = false
val viewModel = mockk<SettingsViewModel> {
every { eventFlow } returns flowOf(SettingsEvent.NavigateAutoFill)
}
composeTestRule.setContent { composeTestRule.setContent {
SettingsScreen( SettingsScreen(
viewModel = viewModel, viewModel = viewModel,
@ -226,15 +216,13 @@ class SettingsScreenTest : BaseComposeTest() {
onNavigateToVault = { }, onNavigateToVault = { },
) )
} }
mutableEventFlow.tryEmit(SettingsEvent.NavigateAutoFill)
assertTrue(haveCalledNavigateToAutoFill) assertTrue(haveCalledNavigateToAutoFill)
} }
@Test @Test
fun `on NavigateAccountSecurity should call onNavigateToOther`() { fun `on NavigateAccountSecurity should call onNavigateToOther`() {
var haveCalledNavigateToOther = false var haveCalledNavigateToOther = false
val viewModel = mockk<SettingsViewModel> {
every { eventFlow } returns flowOf(SettingsEvent.NavigateOther)
}
composeTestRule.setContent { composeTestRule.setContent {
SettingsScreen( SettingsScreen(
viewModel = viewModel, viewModel = viewModel,
@ -248,15 +236,13 @@ class SettingsScreenTest : BaseComposeTest() {
onNavigateToVault = { }, onNavigateToVault = { },
) )
} }
mutableEventFlow.tryEmit(SettingsEvent.NavigateOther)
assertTrue(haveCalledNavigateToOther) assertTrue(haveCalledNavigateToOther)
} }
@Test @Test
fun `on NavigateAccountSecurity should call NavigateVault`() { fun `on NavigateAccountSecurity should call NavigateVault`() {
var haveCalledNavigateToVault = false var haveCalledNavigateToVault = false
val viewModel = mockk<SettingsViewModel> {
every { eventFlow } returns flowOf(SettingsEvent.NavigateVault)
}
composeTestRule.setContent { composeTestRule.setContent {
SettingsScreen( SettingsScreen(
viewModel = viewModel, viewModel = viewModel,
@ -270,6 +256,47 @@ class SettingsScreenTest : BaseComposeTest() {
}, },
) )
} }
mutableEventFlow.tryEmit(SettingsEvent.NavigateVault)
assertTrue(haveCalledNavigateToVault) assertTrue(haveCalledNavigateToVault)
} }
@Test
fun `Settings screen should show correct number of notification badges based on state`() {
composeTestRule.setContent {
SettingsScreen(
viewModel = viewModel,
onNavigateToAbout = {},
onNavigateToAppearance = {},
onNavigateToAutoFill = {},
onNavigateToOther = {},
onNavigateToVault = {},
onNavigateToAccountSecurity = {},
)
}
composeTestRule
.onNodeWithText(text = "1", useUnmergedTree = true)
.assertDoesNotExist()
mutableStateFlow.update { it.copy(autoFillCount = 1) }
composeTestRule
.onNodeWithText(text = "1", useUnmergedTree = true)
.assertExists()
mutableStateFlow.update { it.copy(securityCount = 1) }
composeTestRule
.onAllNodesWithText(text = "1", useUnmergedTree = true)[0]
.assertExists()
composeTestRule
.onAllNodesWithText(text = "1", useUnmergedTree = true)[1]
.assertExists()
}
} }
private val DEFAULT_STATE = SettingsState(
securityCount = 0,
autoFillCount = 0,
)

View file

@ -1,16 +1,28 @@
package com.x8bit.bitwarden.ui.platform.feature.settings package com.x8bit.bitwarden.ui.platform.feature.settings
import app.cash.turbine.test import app.cash.turbine.test
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class SettingsViewModelTest : BaseViewModelTest() { class SettingsViewModelTest : BaseViewModelTest() {
private val mutableAutofillBadgeCountFlow = MutableStateFlow(0)
private val mutableSecurityBadgeCountFlow = MutableStateFlow(0)
private val settingsRepository = mockk<SettingsRepository> {
every { allSecuritySettingsBadgeCountFlow } returns mutableSecurityBadgeCountFlow
every { allAutofillSettingsBadgeCountFlow } returns mutableAutofillBadgeCountFlow
}
@Test @Test
fun `on SettingsClick with ABOUT should emit NavigateAbout`() = runTest { fun `on SettingsClick with ABOUT should emit NavigateAbout`() = runTest {
val viewModel = SettingsViewModel() val viewModel = SettingsViewModel(settingsRepository = settingsRepository)
viewModel.eventFlow.test { viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.ABOUT)) viewModel.trySendAction(SettingsAction.SettingsClick(Settings.ABOUT))
assertEquals(SettingsEvent.NavigateAbout, awaitItem()) assertEquals(SettingsEvent.NavigateAbout, awaitItem())
@ -19,7 +31,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
@Test @Test
fun `on SettingsClick with ACCOUNT_SECURITY should emit NavigateAccountSecurity`() = runTest { fun `on SettingsClick with ACCOUNT_SECURITY should emit NavigateAccountSecurity`() = runTest {
val viewModel = SettingsViewModel() val viewModel = SettingsViewModel(settingsRepository = settingsRepository)
viewModel.eventFlow.test { viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.ACCOUNT_SECURITY)) viewModel.trySendAction(SettingsAction.SettingsClick(Settings.ACCOUNT_SECURITY))
assertEquals(SettingsEvent.NavigateAccountSecurity, awaitItem()) assertEquals(SettingsEvent.NavigateAccountSecurity, awaitItem())
@ -28,7 +40,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
@Test @Test
fun `on SettingsClick with APPEARANCE should emit NavigateAppearance`() = runTest { fun `on SettingsClick with APPEARANCE should emit NavigateAppearance`() = runTest {
val viewModel = SettingsViewModel() val viewModel = SettingsViewModel(settingsRepository = settingsRepository)
viewModel.eventFlow.test { viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.APPEARANCE)) viewModel.trySendAction(SettingsAction.SettingsClick(Settings.APPEARANCE))
assertEquals(SettingsEvent.NavigateAppearance, awaitItem()) assertEquals(SettingsEvent.NavigateAppearance, awaitItem())
@ -37,7 +49,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
@Test @Test
fun `on SettingsClick with AUTO_FILL should emit NavigateAutoFill`() = runTest { fun `on SettingsClick with AUTO_FILL should emit NavigateAutoFill`() = runTest {
val viewModel = SettingsViewModel() val viewModel = SettingsViewModel(settingsRepository = settingsRepository)
viewModel.eventFlow.test { viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.AUTO_FILL)) viewModel.trySendAction(SettingsAction.SettingsClick(Settings.AUTO_FILL))
assertEquals(SettingsEvent.NavigateAutoFill, awaitItem()) assertEquals(SettingsEvent.NavigateAutoFill, awaitItem())
@ -46,7 +58,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
@Test @Test
fun `on SettingsClick with OTHER should emit NavigateOther`() = runTest { fun `on SettingsClick with OTHER should emit NavigateOther`() = runTest {
val viewModel = SettingsViewModel() val viewModel = SettingsViewModel(settingsRepository = settingsRepository)
viewModel.eventFlow.test { viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.OTHER)) viewModel.trySendAction(SettingsAction.SettingsClick(Settings.OTHER))
assertEquals(SettingsEvent.NavigateOther, awaitItem()) assertEquals(SettingsEvent.NavigateOther, awaitItem())
@ -55,10 +67,56 @@ class SettingsViewModelTest : BaseViewModelTest() {
@Test @Test
fun `on SettingsClick with VAULT should emit NavigateVault`() = runTest { fun `on SettingsClick with VAULT should emit NavigateVault`() = runTest {
val viewModel = SettingsViewModel() val viewModel = SettingsViewModel(settingsRepository = settingsRepository)
viewModel.eventFlow.test { viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.VAULT)) viewModel.trySendAction(SettingsAction.SettingsClick(Settings.VAULT))
assertEquals(SettingsEvent.NavigateVault, awaitItem()) assertEquals(SettingsEvent.NavigateVault, awaitItem())
} }
} }
@Test
fun `initial state reflects the current state of the repository`() {
mutableAutofillBadgeCountFlow.update { 1 }
mutableSecurityBadgeCountFlow.update { 2 }
val viewModel = SettingsViewModel(settingsRepository = settingsRepository)
assertEquals(
SettingsState(
autoFillCount = 1,
securityCount = 2,
),
viewModel.stateFlow.value,
)
}
@Test
fun `State updates when repository emits new values for badge counts`() = runTest {
val viewModel = SettingsViewModel(settingsRepository = settingsRepository)
viewModel.stateFlow.test {
assertEquals(
SettingsState(
autoFillCount = 0,
securityCount = 0,
),
awaitItem(),
)
mutableSecurityBadgeCountFlow.update { 2 }
assertEquals(
SettingsState(
autoFillCount = 0,
securityCount = 2,
),
awaitItem(),
)
mutableAutofillBadgeCountFlow.update { 1 }
assertEquals(
SettingsState(
autoFillCount = 1,
securityCount = 2,
),
awaitItem(),
)
}
}
} }