BIT-383: Finish UI for syncing the Vault with the Sync button (#463)

This commit is contained in:
Brian Yencho 2023-12-29 17:11:56 -06:00 committed by Álison Fernandes
parent 17b50d96f1
commit 23479d6750
3 changed files with 169 additions and 5 deletions

View file

@ -33,16 +33,19 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountActionItem
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.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenMediumTopAppBar
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenSearchActionItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
@ -87,7 +90,7 @@ fun VaultScreen(
VaultEvent.NavigateOutOfApp -> intentHandler.exitApplication()
is VaultEvent.ShowToast -> {
Toast
.makeText(context, event.message, Toast.LENGTH_SHORT)
.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT)
.show()
}
}
@ -206,6 +209,14 @@ private fun VaultScreenScaffold(
// Dynamic dialogs
when (val dialog = state.dialog) {
is VaultState.DialogState.Syncing -> {
BitwardenLoadingDialog(
visibilityState = LoadingDialogState.Shown(
text = R.string.syncing.asText(),
),
)
}
is VaultState.DialogState.Error -> {
BitwardenBasicDialog(
visibilityState = BasicDialogState.Shown(

View file

@ -186,6 +186,9 @@ class VaultViewModel @Inject constructor(
}
private fun handleSyncClick() {
mutableStateFlow.update {
it.copy(dialog = VaultState.DialogState.Syncing)
}
vaultRepository.sync()
}
@ -285,8 +288,18 @@ class VaultViewModel @Inject constructor(
}
private fun vaultLoadedReceive(vaultData: DataState.Loaded<VaultData>) {
if (state.dialog == VaultState.DialogState.Syncing) {
sendEvent(
VaultEvent.ShowToast(
message = R.string.syncing_complete.asText(),
),
)
}
mutableStateFlow.update {
it.copy(viewState = vaultData.data.toViewState(vaultFilterTypeOrDefault))
it.copy(
viewState = vaultData.data.toViewState(vaultFilterTypeOrDefault),
dialog = null,
)
}
}
@ -308,7 +321,6 @@ class VaultViewModel @Inject constructor(
mutableStateFlow.update {
it.copy(viewState = vaultData.data.toViewState(vaultFilterTypeOrDefault))
}
sendEvent(VaultEvent.ShowToast(message = "Refreshing"))
}
//endregion VaultAction Handlers
@ -550,6 +562,12 @@ data class VaultState(
*/
sealed class DialogState : Parcelable {
/**
* Represents a dialog indication and ongoing manual sync.
*/
@Parcelize
data object Syncing : DialogState()
/**
* Represents an error dialog with the given [title] and [message].
*/
@ -612,7 +630,7 @@ sealed class VaultEvent {
/**
* Show a toast with the given [message].
*/
data class ShowToast(val message: String) : VaultEvent()
data class ShowToast(val message: Text) : VaultEvent()
}
/**
@ -778,6 +796,7 @@ private fun MutableStateFlow<VaultState>.updateToErrorStateOrDialog(
viewState = VaultState.ViewState.Error(
message = errorMessage,
),
dialog = null,
)
}
}

View file

@ -281,9 +281,15 @@ class VaultViewModelTest : BaseViewModelTest() {
}
@Test
fun `on SyncClick should call sync on the VaultRepository`() {
fun `on SyncClick should call sync on the VaultRepository and show the syncing dialog`() {
val viewModel = createViewModel()
viewModel.trySendAction(VaultAction.SyncClick)
assertEquals(
DEFAULT_STATE.copy(
dialog = VaultState.DialogState.Syncing,
),
viewModel.stateFlow.value,
)
verify {
vaultRepository.sync()
}
@ -405,6 +411,45 @@ class VaultViewModelTest : BaseViewModelTest() {
)
}
@Suppress("MaxLineLength")
@Test
fun `vaultDataStateFlow Loaded with items when manually syncing with the sync button should update state to Content and show a success Toast`() =
runTest {
val expectedState = createMockVaultState(
viewState = VaultState.ViewState.Content(
loginItemsCount = 1,
cardItemsCount = 0,
identityItemsCount = 0,
secureNoteItemsCount = 0,
favoriteItems = listOf(),
folderItems = listOf(),
collectionItems = listOf(),
noFolderItems = listOf(),
trashItemsCount = 0,
),
)
val viewModel = createViewModel()
viewModel.trySendAction(VaultAction.SyncClick)
viewModel.eventFlow.test {
mutableVaultDataStateFlow.tryEmit(
value = DataState.Loaded(
data = VaultData(
cipherViewList = listOf(createMockCipherView(number = 1)),
collectionViewList = emptyList(),
folderViewList = emptyList(),
),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
assertEquals(
VaultEvent.ShowToast(R.string.syncing_complete.asText()),
awaitItem(),
)
}
}
@Test
fun `vaultDataStateFlow Loaded with empty items should update state to NoItems`() = runTest {
mutableVaultDataStateFlow.tryEmit(
@ -423,6 +468,95 @@ class VaultViewModelTest : BaseViewModelTest() {
)
}
@Suppress("MaxLineLength")
@Test
fun `vaultDataStateFlow Loaded with empty items when manually syncing with the sync button should update state to NoItems and show a success Toast`() =
runTest {
val expectedState = createMockVaultState(
viewState = VaultState.ViewState.NoItems,
)
val viewModel = createViewModel()
viewModel.trySendAction(VaultAction.SyncClick)
viewModel.eventFlow.test {
mutableVaultDataStateFlow.value = DataState.Loaded(
data = VaultData(
cipherViewList = emptyList(),
collectionViewList = emptyList(),
folderViewList = emptyList(),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
assertEquals(
VaultEvent.ShowToast(R.string.syncing_complete.asText()),
awaitItem(),
)
}
}
@Test
fun `vaultDataStateFlow Pending with items should update state to Content`() {
mutableVaultDataStateFlow.tryEmit(
value = DataState.Pending(
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,
),
),
viewModel.stateFlow.value,
)
}
@Test
fun `vaultDataStateFlow Pending with empty items should update state to NoItems`() = runTest {
mutableVaultDataStateFlow.tryEmit(
value = DataState.Pending(
data = VaultData(
cipherViewList = emptyList(),
collectionViewList = emptyList(),
folderViewList = emptyList(),
),
),
)
val viewModel = createViewModel()
assertEquals(
createMockVaultState(viewState = VaultState.ViewState.NoItems),
viewModel.stateFlow.value,
)
}
@Test
fun `vaultDataStateFlow Loading should update state to Loading`() = runTest {
mutableVaultDataStateFlow.tryEmit(value = DataState.Loading)