mirror of
https://github.com/bitwarden/android.git
synced 2024-11-29 06:18:52 +03:00
BIT-1158: Add No Network states to Vault Screen (#391)
This commit is contained in:
parent
36e913b680
commit
0655f74479
5 changed files with 380 additions and 13 deletions
|
@ -0,0 +1,49 @@
|
||||||
|
package com.x8bit.bitwarden.ui.platform.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Bitwarden-themed, re-usable error state.
|
||||||
|
*
|
||||||
|
* Note that when [onTryAgainClick] is absent, there will be no "Try again" button displayed.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun BitwardenErrorContent(
|
||||||
|
message: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onTryAgainClick: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = message,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
onTryAgainClick?.let {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
BitwardenTextButton(
|
||||||
|
label = stringResource(id = R.string.try_again),
|
||||||
|
onClick = it,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,11 +25,15 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
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.components.BasicDialogState
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountActionItem
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountActionItem
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountSwitcher
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountSwitcher
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenMediumTopAppBar
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenMediumTopAppBar
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||||
|
@ -125,6 +129,12 @@ fun VaultScreen(
|
||||||
trashClick = remember(viewModel) {
|
trashClick = remember(viewModel) {
|
||||||
{ viewModel.trySendAction(VaultAction.TrashClick) }
|
{ viewModel.trySendAction(VaultAction.TrashClick) }
|
||||||
},
|
},
|
||||||
|
tryAgainClick = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(VaultAction.TryAgainClick) }
|
||||||
|
},
|
||||||
|
dialogDismiss = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(VaultAction.DialogDismiss) }
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,6 +161,8 @@ private fun VaultScreenScaffold(
|
||||||
identityGroupClick: () -> Unit,
|
identityGroupClick: () -> Unit,
|
||||||
secureNoteGroupClick: () -> Unit,
|
secureNoteGroupClick: () -> Unit,
|
||||||
trashClick: () -> Unit,
|
trashClick: () -> Unit,
|
||||||
|
tryAgainClick: () -> Unit,
|
||||||
|
dialogDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
var accountMenuVisible by rememberSaveable {
|
var accountMenuVisible by rememberSaveable {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
|
@ -165,6 +177,20 @@ private fun VaultScreenScaffold(
|
||||||
canScroll = { !accountMenuVisible },
|
canScroll = { !accountMenuVisible },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
when (val dialog = state.dialog) {
|
||||||
|
is VaultState.DialogState.Error -> {
|
||||||
|
BitwardenBasicDialog(
|
||||||
|
visibilityState = BasicDialogState.Shown(
|
||||||
|
title = dialog.title,
|
||||||
|
message = dialog.message,
|
||||||
|
),
|
||||||
|
onDismissRequest = dialogDismiss,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> Unit
|
||||||
|
}
|
||||||
|
|
||||||
BitwardenScaffold(
|
BitwardenScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
BitwardenMediumTopAppBar(
|
BitwardenMediumTopAppBar(
|
||||||
|
@ -229,9 +255,15 @@ private fun VaultScreenScaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
addItemClickAction = addItemClickAction,
|
addItemClickAction = addItemClickAction,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is VaultState.ViewState.Error -> BitwardenErrorContent(
|
||||||
|
message = viewState.message(),
|
||||||
|
onTryAgainClick = tryAgainClick,
|
||||||
|
modifier = modifier
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val context = LocalContext.current
|
|
||||||
BitwardenAccountSwitcher(
|
BitwardenAccountSwitcher(
|
||||||
isVisible = accountMenuVisible,
|
isVisible = accountMenuVisible,
|
||||||
accountSummaries = state.accountSummaries.toImmutableList(),
|
accountSummaries = state.accountSummaries.toImmutableList(),
|
||||||
|
|
|
@ -95,6 +95,8 @@ class VaultViewModel @Inject constructor(
|
||||||
is VaultAction.SecureNoteGroupClick -> handleSecureNoteClick()
|
is VaultAction.SecureNoteGroupClick -> handleSecureNoteClick()
|
||||||
is VaultAction.TrashClick -> handleTrashClick()
|
is VaultAction.TrashClick -> handleTrashClick()
|
||||||
is VaultAction.VaultItemClick -> handleVaultItemClick(action)
|
is VaultAction.VaultItemClick -> handleVaultItemClick(action)
|
||||||
|
is VaultAction.TryAgainClick -> handleTryAgainClick()
|
||||||
|
is VaultAction.DialogDismiss -> handleDialogDismiss()
|
||||||
is VaultAction.Internal.UserStateUpdateReceive -> handleUserStateUpdateReceive(action)
|
is VaultAction.Internal.UserStateUpdateReceive -> handleUserStateUpdateReceive(action)
|
||||||
is VaultAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
|
is VaultAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
|
||||||
}
|
}
|
||||||
|
@ -176,6 +178,16 @@ class VaultViewModel @Inject constructor(
|
||||||
sendEvent(VaultEvent.NavigateToVaultItem(action.vaultItem.id))
|
sendEvent(VaultEvent.NavigateToVaultItem(action.vaultItem.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleTryAgainClick() {
|
||||||
|
vaultRepository.sync()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDialogDismiss() {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(dialog = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleUserStateUpdateReceive(action: VaultAction.Internal.UserStateUpdateReceive) {
|
private fun handleUserStateUpdateReceive(action: VaultAction.Internal.UserStateUpdateReceive) {
|
||||||
// Leave the current data alone if there is no UserState; we are in the process of logging
|
// Leave the current data alone if there is no UserState; we are in the process of logging
|
||||||
// out.
|
// out.
|
||||||
|
@ -225,9 +237,27 @@ class VaultViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun vaultNoNetworkReceive(vaultData: DataState.NoNetwork<VaultData>) {
|
private fun vaultNoNetworkReceive(vaultData: DataState.NoNetwork<VaultData>) {
|
||||||
// TODO update state to no network state BIT-1158
|
val title = R.string.internet_connection_required_title.asText()
|
||||||
mutableStateFlow.update { it.copy(viewState = VaultState.ViewState.NoItems) }
|
val message = R.string.internet_connection_required_message.asText()
|
||||||
sendEvent(VaultEvent.ShowToast(message = "Vault no network state not yet implemented"))
|
if (vaultData.data != null) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
viewState = vaultData.data.toViewState(),
|
||||||
|
dialog = VaultState.DialogState.Error(
|
||||||
|
title = title,
|
||||||
|
message = message,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
viewState = VaultState.ViewState.Error(
|
||||||
|
message = message,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun vaultPendingReceive(vaultData: DataState.Pending<VaultData>) {
|
private fun vaultPendingReceive(vaultData: DataState.Pending<VaultData>) {
|
||||||
|
@ -245,6 +275,7 @@ class VaultViewModel @Inject constructor(
|
||||||
* @property initials The initials to be displayed on the avatar.
|
* @property initials The initials to be displayed on the avatar.
|
||||||
* @property accountSummaries List of all the current accounts.
|
* @property accountSummaries List of all the current accounts.
|
||||||
* @property viewState The specific view state representing loading, no items, or content state.
|
* @property viewState The specific view state representing loading, no items, or content state.
|
||||||
|
* @property dialog Information about any dialogs that may need to be displayed.
|
||||||
* @property isSwitchingAccounts Whether or not we are actively switching accounts.
|
* @property isSwitchingAccounts Whether or not we are actively switching accounts.
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@ -253,6 +284,7 @@ data class VaultState(
|
||||||
val initials: String,
|
val initials: String,
|
||||||
val accountSummaries: List<AccountSummary>,
|
val accountSummaries: List<AccountSummary>,
|
||||||
val viewState: ViewState,
|
val viewState: ViewState,
|
||||||
|
val dialog: DialogState? = null,
|
||||||
// Internal-use properties
|
// Internal-use properties
|
||||||
val isSwitchingAccounts: Boolean = false,
|
val isSwitchingAccounts: Boolean = false,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
@ -280,6 +312,15 @@ data class VaultState(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object NoItems : ViewState()
|
data object NoItems : ViewState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a state where the [VaultScreen] is unable to display data due to an error
|
||||||
|
* retrieving it. The given [message] should be displayed.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class Error(
|
||||||
|
val message: Text,
|
||||||
|
) : ViewState()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Content state for the [VaultScreen] showing the actual content or items.
|
* Content state for the [VaultScreen] showing the actual content or items.
|
||||||
*
|
*
|
||||||
|
@ -433,6 +474,21 @@ data class VaultState(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about a dialog to display.
|
||||||
|
*/
|
||||||
|
sealed class DialogState : Parcelable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an error dialog with the given [title] and [message].
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class Error(
|
||||||
|
val title: Text,
|
||||||
|
val message: Text,
|
||||||
|
) : DialogState()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* The maximum number of no folder items that can be displayed before the UI creates a
|
* The maximum number of no folder items that can be displayed before the UI creates a
|
||||||
|
@ -572,6 +628,16 @@ sealed class VaultAction {
|
||||||
*/
|
*/
|
||||||
data object TrashClick : VaultAction()
|
data object TrashClick : VaultAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user has requested that any visible dialogs are dismissed.
|
||||||
|
*/
|
||||||
|
data object DialogDismiss : VaultAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User clicked the Try Again button when there is an error displayed.
|
||||||
|
*/
|
||||||
|
data object TryAgainClick : VaultAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Models actions that the [VaultViewModel] itself might send.
|
* Models actions that the [VaultViewModel] itself might send.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -3,9 +3,12 @@ package com.x8bit.bitwarden.ui.vault.feature.vault
|
||||||
import androidx.compose.ui.test.assertIsDisplayed
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
import androidx.compose.ui.test.assertTextEquals
|
import androidx.compose.ui.test.assertTextEquals
|
||||||
import androidx.compose.ui.test.filterToOne
|
import androidx.compose.ui.test.filterToOne
|
||||||
|
import androidx.compose.ui.test.hasAnyAncestor
|
||||||
import androidx.compose.ui.test.hasClickAction
|
import androidx.compose.ui.test.hasClickAction
|
||||||
import androidx.compose.ui.test.hasScrollToNodeAction
|
import androidx.compose.ui.test.hasScrollToNodeAction
|
||||||
import androidx.compose.ui.test.hasText
|
import androidx.compose.ui.test.hasText
|
||||||
|
import androidx.compose.ui.test.isDialog
|
||||||
|
import androidx.compose.ui.test.onAllNodesWithText
|
||||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
|
@ -176,6 +179,104 @@ class VaultScreenTest : BaseComposeTest() {
|
||||||
composeTestRule.assertNoDialogExists()
|
composeTestRule.assertNoDialogExists()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `error dialog should be shown or hidden according to the state`() {
|
||||||
|
val errorTitle = "Error title"
|
||||||
|
val errorMessage = "Error message"
|
||||||
|
composeTestRule.assertNoDialogExists()
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(errorTitle)
|
||||||
|
.assertDoesNotExist()
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(errorMessage)
|
||||||
|
.assertDoesNotExist()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialog = VaultState.DialogState.Error(
|
||||||
|
title = errorTitle.asText(),
|
||||||
|
message = errorMessage.asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(errorTitle)
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertIsDisplayed()
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(errorMessage)
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `OK button click in error dialog should send DialogDismiss`() {
|
||||||
|
val errorTitle = "Error title"
|
||||||
|
val errorMessage = "Error message"
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialog = VaultState.DialogState.Error(
|
||||||
|
title = errorTitle.asText(),
|
||||||
|
message = errorMessage.asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText("Ok")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify { viewModel.trySendAction(VaultAction.DialogDismiss) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Error screen should be shown according to the state`() {
|
||||||
|
val errorMessage = "Error message"
|
||||||
|
val tryAgainButtonText = "Try again"
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(errorMessage)
|
||||||
|
.assertDoesNotExist()
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(tryAgainButtonText)
|
||||||
|
.assertDoesNotExist()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
viewState = VaultState.ViewState.Error(
|
||||||
|
message = errorMessage.asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(errorMessage)
|
||||||
|
.assertIsDisplayed()
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(tryAgainButtonText)
|
||||||
|
.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `try again button click on the Error screen should send TryAgainClick`() {
|
||||||
|
val errorMessage = "Error message"
|
||||||
|
val tryAgainButtonText = "Try again"
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
viewState = VaultState.ViewState.Error(
|
||||||
|
message = errorMessage.asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(tryAgainButtonText)
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify { viewModel.trySendAction(VaultAction.TryAgainClick) }
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `search icon click should send SearchIconClick action`() {
|
fun `search icon click should send SearchIconClick action`() {
|
||||||
mutableStateFlow.update { it.copy(viewState = VaultState.ViewState.NoItems) }
|
mutableStateFlow.update { it.copy(viewState = VaultState.ViewState.NoItems) }
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.x8bit.bitwarden.ui.vault.feature.vault
|
package com.x8bit.bitwarden.ui.vault.feature.vault
|
||||||
|
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||||
|
@ -352,25 +353,99 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `vaultDataStateFlow NoNetwork should show toast and update state to NoItems`() = runTest {
|
fun `vaultDataStateFlow NoNetwork without data should update state to Error`() = runTest {
|
||||||
mutableVaultDataStateFlow.tryEmit(
|
mutableVaultDataStateFlow.tryEmit(
|
||||||
value = DataState.NoNetwork(),
|
value = DataState.NoNetwork(),
|
||||||
)
|
)
|
||||||
|
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
viewModel.eventFlow.test {
|
|
||||||
assertEquals(
|
|
||||||
VaultEvent.ShowToast("Vault no network state not yet implemented"),
|
|
||||||
awaitItem(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
createMockVaultState(viewState = VaultState.ViewState.NoItems),
|
createMockVaultState(
|
||||||
|
viewState = VaultState.ViewState.Error(
|
||||||
|
message = R.string.internet_connection_required_message.asText(),
|
||||||
|
),
|
||||||
|
),
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `vaultDataStateFlow NoNetwork with items should update state to Content and show an error dialog`() =
|
||||||
|
runTest {
|
||||||
|
mutableVaultDataStateFlow.tryEmit(
|
||||||
|
value = DataState.NoNetwork(
|
||||||
|
data = VaultData(
|
||||||
|
cipherViewList = listOf(createMockCipherView(number = 1)),
|
||||||
|
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||||
|
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
createMockVaultState(
|
||||||
|
viewState = VaultState.ViewState.Content(
|
||||||
|
loginItemsCount = 1,
|
||||||
|
cardItemsCount = 0,
|
||||||
|
identityItemsCount = 0,
|
||||||
|
secureNoteItemsCount = 0,
|
||||||
|
favoriteItems = listOf(),
|
||||||
|
folderItems = listOf(
|
||||||
|
VaultState.ViewState.FolderItem(
|
||||||
|
id = "mockId-1",
|
||||||
|
name = "mockName-1".asText(),
|
||||||
|
itemCount = 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
collectionItems = listOf(
|
||||||
|
VaultState.ViewState.CollectionItem(
|
||||||
|
id = "mockId-1",
|
||||||
|
name = "mockName-1",
|
||||||
|
itemCount = 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
noFolderItems = listOf(),
|
||||||
|
trashItemsCount = 0,
|
||||||
|
),
|
||||||
|
dialog = VaultState.DialogState.Error(
|
||||||
|
title = R.string.internet_connection_required_title.asText(),
|
||||||
|
message = R.string.internet_connection_required_message.asText(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `vaultDataStateFlow NoNetwork with empty items should update state to NoItems and show an error dialog`() =
|
||||||
|
runTest {
|
||||||
|
mutableVaultDataStateFlow.tryEmit(
|
||||||
|
value = DataState.NoNetwork(
|
||||||
|
data = VaultData(
|
||||||
|
cipherViewList = emptyList(),
|
||||||
|
collectionViewList = emptyList(),
|
||||||
|
folderViewList = emptyList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
assertEquals(
|
||||||
|
createMockVaultState(
|
||||||
|
viewState = VaultState.ViewState.NoItems,
|
||||||
|
dialog = VaultState.DialogState.Error(
|
||||||
|
title = R.string.internet_connection_required_title.asText(),
|
||||||
|
message = R.string.internet_connection_required_message.asText(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `vaultDataStateFlow updates should do nothing when switching accounts`() {
|
fun `vaultDataStateFlow updates should do nothing when switching accounts`() {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
@ -532,6 +607,46 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `TryAgainClick should sync the vault data`() {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
viewModel.trySendAction(VaultAction.TryAgainClick)
|
||||||
|
|
||||||
|
verify { vaultRepository.sync() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `DialogDismiss should clear the active dialog`() {
|
||||||
|
// Show the No Network error dialog
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
mutableVaultDataStateFlow.value = DataState.NoNetwork(
|
||||||
|
data = VaultData(
|
||||||
|
cipherViewList = emptyList(),
|
||||||
|
collectionViewList = emptyList(),
|
||||||
|
folderViewList = emptyList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val initialState = DEFAULT_STATE.copy(
|
||||||
|
viewState = VaultState.ViewState.NoItems,
|
||||||
|
dialog = VaultState.DialogState.Error(
|
||||||
|
title = R.string.internet_connection_required_title.asText(),
|
||||||
|
message = R.string.internet_connection_required_message.asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
initialState,
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModel.trySendAction(VaultAction.DialogDismiss)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
initialState.copy(dialog = null),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun createViewModel(): VaultViewModel =
|
private fun createViewModel(): VaultViewModel =
|
||||||
VaultViewModel(
|
VaultViewModel(
|
||||||
authRepository = authRepository,
|
authRepository = authRepository,
|
||||||
|
@ -566,7 +681,10 @@ private val DEFAULT_USER_STATE = UserState(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun createMockVaultState(viewState: VaultState.ViewState): VaultState =
|
private fun createMockVaultState(
|
||||||
|
viewState: VaultState.ViewState,
|
||||||
|
dialog: VaultState.DialogState? = null,
|
||||||
|
): VaultState =
|
||||||
VaultState(
|
VaultState(
|
||||||
avatarColorString = "#aa00aa",
|
avatarColorString = "#aa00aa",
|
||||||
initials = "AU",
|
initials = "AU",
|
||||||
|
@ -591,5 +709,6 @@ private fun createMockVaultState(viewState: VaultState.ViewState): VaultState =
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
viewState = viewState,
|
viewState = viewState,
|
||||||
|
dialog = dialog,
|
||||||
isSwitchingAccounts = false,
|
isSwitchingAccounts = false,
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue