mirror of
https://github.com/bitwarden/android.git
synced 2024-12-18 07:11:51 +03:00
[PM-12595] add notification badge to settings item in settings tab screen (#3976)
This commit is contained in:
parent
70e75b910c
commit
853f25bf57
4 changed files with 246 additions and 59 deletions
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
|
@ -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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue