From 65eb6ab5f8844c085c4c9b9e6f0267353b5b588d Mon Sep 17 00:00:00 2001 From: Brian Yencho Date: Thu, 14 Dec 2023 13:15:55 -0600 Subject: [PATCH] BIT-1157: Add error state for Vault Screen (#393) --- .../ui/vault/feature/vault/VaultViewModel.kt | 60 +++++++----- .../vault/feature/vault/VaultViewModelTest.kt | 97 ++++++++++++++++--- 2 files changed, 122 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt index 22a342c17..ecc63d57e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt @@ -24,6 +24,7 @@ import com.x8bit.bitwarden.ui.vault.feature.vault.util.toViewState import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update @@ -223,9 +224,11 @@ class VaultViewModel @Inject constructor( } private fun vaultErrorReceive(vaultData: DataState.Error) { - // TODO update state to error state BIT-1157 - mutableStateFlow.update { it.copy(viewState = VaultState.ViewState.NoItems) } - sendEvent(VaultEvent.ShowToast(message = "Vault error state not yet implemented")) + mutableStateFlow.updateToErrorStateOrDialog( + vaultData = vaultData.data, + errorTitle = R.string.an_error_has_occurred.asText(), + errorMessage = R.string.generic_error_message.asText(), + ) } private fun vaultLoadedReceive(vaultData: DataState.Loaded) { @@ -237,27 +240,11 @@ class VaultViewModel @Inject constructor( } private fun vaultNoNetworkReceive(vaultData: DataState.NoNetwork) { - val title = R.string.internet_connection_required_title.asText() - val message = R.string.internet_connection_required_message.asText() - 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, - ), - ) - } - } + mutableStateFlow.updateToErrorStateOrDialog( + vaultData = vaultData.data, + errorTitle = R.string.internet_connection_required_title.asText(), + errorMessage = R.string.internet_connection_required_message.asText(), + ) } private fun vaultPendingReceive(vaultData: DataState.Pending) { @@ -265,6 +252,7 @@ class VaultViewModel @Inject constructor( mutableStateFlow.update { it.copy(viewState = vaultData.data.toViewState()) } sendEvent(VaultEvent.ShowToast(message = "Refreshing")) } + //endregion VaultAction Handlers } @@ -658,3 +646,27 @@ sealed class VaultAction { ) : Internal() } } + +private fun MutableStateFlow.updateToErrorStateOrDialog( + vaultData: VaultData?, + errorTitle: Text, + errorMessage: Text, +) { + this.update { + if (vaultData != null) { + it.copy( + viewState = vaultData.toViewState(), + dialog = VaultState.DialogState.Error( + title = errorTitle, + message = errorMessage, + ), + ) + } else { + it.copy( + viewState = VaultState.ViewState.Error( + message = errorMessage, + ), + ) + } + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt index efb5464dc..a2f7508af 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +@Suppress("LargeClass") class VaultViewModelTest : BaseViewModelTest() { private val mutableUserStateFlow = @@ -331,27 +332,101 @@ class VaultViewModelTest : BaseViewModelTest() { } @Test - fun `vaultDataStateFlow Error should show toast and update state to NoItems`() = runTest { + fun `vaultDataStateFlow Error without data should update state to Error`() = runTest { mutableVaultDataStateFlow.tryEmit( - value = DataState.Error( - error = IllegalStateException(), - ), + value = DataState.Error(error = IllegalStateException()), ) val viewModel = createViewModel() - viewModel.eventFlow.test { - assertEquals( - VaultEvent.ShowToast("Vault error state not yet implemented"), - awaitItem(), - ) - } assertEquals( - createMockVaultState(viewState = VaultState.ViewState.NoItems), + createMockVaultState( + viewState = VaultState.ViewState.Error( + message = R.string.generic_error_message.asText(), + ), + ), viewModel.stateFlow.value, ) } + @Suppress("MaxLineLength") + @Test + fun `vaultDataStateFlow Error with items should update state to Content and show an error dialog`() = + runTest { + mutableVaultDataStateFlow.tryEmit( + value = DataState.Error( + error = IllegalStateException(), + 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.an_error_has_occurred.asText(), + message = R.string.generic_error_message.asText(), + ), + ), + viewModel.stateFlow.value, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `vaultDataStateFlow Error with empty items should update state to NoItems and show an error dialog`() = + runTest { + mutableVaultDataStateFlow.tryEmit( + value = DataState.Error( + error = IllegalStateException(), + data = VaultData( + cipherViewList = emptyList(), + collectionViewList = emptyList(), + folderViewList = emptyList(), + ), + ), + ) + val viewModel = createViewModel() + assertEquals( + createMockVaultState( + viewState = VaultState.ViewState.NoItems, + dialog = VaultState.DialogState.Error( + title = R.string.an_error_has_occurred.asText(), + message = R.string.generic_error_message.asText(), + ), + ), + viewModel.stateFlow.value, + ) + } + @Test fun `vaultDataStateFlow NoNetwork without data should update state to Error`() = runTest { mutableVaultDataStateFlow.tryEmit(