mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 00:06:22 -06:00
[PM-26803] Show empty state when no items are available for export (#6023)
This commit is contained in:
parent
5b5176db40
commit
af737b3f07
@ -7,6 +7,7 @@ import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
|
||||
import com.x8bit.bitwarden.ui.vault.feature.exportitems.selectaccount.popUpToSelectAccountScreen
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
@ -34,6 +35,7 @@ fun NavGraphBuilder.reviewExportDestination(
|
||||
composableWithPushTransitions<ReviewExportRoute> {
|
||||
ReviewExportScreen(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToAccountSelection = { navController.popUpToSelectAccountScreen() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.exportitems.reviewexport
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@ -32,7 +31,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.cxf.manager.CredentialExchangeCompletionManager
|
||||
import com.bitwarden.cxf.model.ImportCredentialsRequestData
|
||||
import com.bitwarden.cxf.ui.composition.LocalCredentialExchangeCompletionManager
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.cardStyle
|
||||
@ -40,6 +38,8 @@ import com.bitwarden.ui.platform.base.util.nullableTestTag
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
||||
import com.bitwarden.ui.platform.components.button.model.BitwardenButtonData
|
||||
import com.bitwarden.ui.platform.components.content.BitwardenEmptyContent
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||
@ -72,10 +72,12 @@ import com.x8bit.bitwarden.ui.vault.feature.exportitems.reviewexport.handlers.re
|
||||
* export process.
|
||||
* Defaults to the manager provided by [LocalCredentialExchangeCompletionManager].
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ReviewExportScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToAccountSelection: () -> Unit,
|
||||
viewModel: ReviewExportViewModel = hiltViewModel(),
|
||||
credentialExchangeCompletionManager: CredentialExchangeCompletionManager =
|
||||
LocalCredentialExchangeCompletionManager.current,
|
||||
@ -86,6 +88,7 @@ fun ReviewExportScreen(
|
||||
EventsEffect(viewModel) {
|
||||
when (it) {
|
||||
is ReviewExportEvent.NavigateBack -> onNavigateBack()
|
||||
is ReviewExportEvent.NavigateToAccountSelection -> onNavigateToAccountSelection()
|
||||
is ReviewExportEvent.CompleteExport -> {
|
||||
credentialExchangeCompletionManager.completeCredentialExport(it.result)
|
||||
}
|
||||
@ -107,14 +110,39 @@ fun ReviewExportScreen(
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
ReviewExportContent(
|
||||
state = state,
|
||||
onImportItemsClick = handler.onImportItemsClick,
|
||||
onCancelClick = handler.onCancelClick,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
when (val viewState = state.viewState) {
|
||||
ReviewExportState.ViewState.NoItems -> {
|
||||
BitwardenEmptyContent(
|
||||
title = stringResource(BitwardenString.no_items_available_to_import),
|
||||
text = stringResource(
|
||||
BitwardenString
|
||||
.your_vault_may_be_empty_or_import_some_item_types_isnt_supported,
|
||||
),
|
||||
primaryButton = BitwardenButtonData(
|
||||
label = BitwardenString.select_a_different_account.asText(),
|
||||
testTag = "SelectADifferentAccountButton",
|
||||
onClick = handler.onSelectAnotherAccountClick,
|
||||
),
|
||||
secondaryButton = BitwardenButtonData(
|
||||
label = BitwardenString.cancel.asText(),
|
||||
testTag = "NoItemsCancelButton",
|
||||
onClick = handler.onCancelClick,
|
||||
),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
is ReviewExportState.ViewState.Content -> {
|
||||
ReviewExportContent(
|
||||
content = viewState,
|
||||
onImportItemsClick = handler.onImportItemsClick,
|
||||
onCancelClick = handler.onCancelClick,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -153,7 +181,7 @@ private fun ReviewExportDialogs(
|
||||
* This composable lays out the illustrative image, titles, list of items to export,
|
||||
* and action buttons.
|
||||
*
|
||||
* @param state The current [ReviewExportState] to render.
|
||||
* @param content The current [ReviewExportState] to render.
|
||||
* @param onImportItemsClick Callback invoked when the "Import Items" button is clicked.
|
||||
* @param onCancelClick Callback invoked when the "Cancel" button is clicked.
|
||||
* @param modifier The modifier to be applied to the content root.
|
||||
@ -161,7 +189,7 @@ private fun ReviewExportDialogs(
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun ReviewExportContent(
|
||||
state: ReviewExportState,
|
||||
content: ReviewExportState.ViewState.Content,
|
||||
onImportItemsClick: () -> Unit,
|
||||
onCancelClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@ -211,27 +239,27 @@ private fun ReviewExportContent(
|
||||
|
||||
ItemCountRow(
|
||||
label = stringResource(BitwardenString.passwords).asText(),
|
||||
itemCount = state.viewState.itemTypeCounts.passwordCount,
|
||||
itemCount = content.itemTypeCounts.passwordCount,
|
||||
cardStyle = CardStyle.Top(),
|
||||
)
|
||||
ItemCountRow(
|
||||
label = stringResource(BitwardenString.passkeys).asText(),
|
||||
itemCount = state.viewState.itemTypeCounts.passkeyCount,
|
||||
itemCount = content.itemTypeCounts.passkeyCount,
|
||||
cardStyle = CardStyle.Middle(),
|
||||
)
|
||||
ItemCountRow(
|
||||
label = stringResource(BitwardenString.identities).asText(),
|
||||
itemCount = state.viewState.itemTypeCounts.identityCount,
|
||||
itemCount = content.itemTypeCounts.identityCount,
|
||||
cardStyle = CardStyle.Middle(),
|
||||
)
|
||||
ItemCountRow(
|
||||
label = stringResource(BitwardenString.cards).asText(),
|
||||
itemCount = state.viewState.itemTypeCounts.cardCount,
|
||||
itemCount = content.itemTypeCounts.cardCount,
|
||||
cardStyle = CardStyle.Middle(),
|
||||
)
|
||||
ItemCountRow(
|
||||
label = stringResource(BitwardenString.secure_notes).asText(),
|
||||
itemCount = state.viewState.itemTypeCounts.secureNoteCount,
|
||||
itemCount = content.itemTypeCounts.secureNoteCount,
|
||||
cardStyle = CardStyle.Bottom,
|
||||
)
|
||||
|
||||
@ -298,24 +326,23 @@ private fun ItemCountRow(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Preview(showBackground = true, name = "Review Export Content")
|
||||
@Composable
|
||||
private fun ReviewExportContent_preview() {
|
||||
BitwardenTheme {
|
||||
ExportItemsScaffold(
|
||||
navIcon = rememberVectorPainter(BitwardenDrawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(BitwardenString.close),
|
||||
onNavigationIconClick = { },
|
||||
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
|
||||
) {
|
||||
ReviewExportContent(
|
||||
state = ReviewExportState(
|
||||
importCredentialsRequestData = ImportCredentialsRequestData(
|
||||
uri = Uri.EMPTY,
|
||||
requestJson = "",
|
||||
),
|
||||
viewState = ReviewExportState.ViewState(
|
||||
itemTypeCounts = ReviewExportState.ItemTypeCounts(
|
||||
passwordCount = 14,
|
||||
passkeyCount = 14,
|
||||
identityCount = 3,
|
||||
cardCount = 4,
|
||||
secureNoteCount = 5,
|
||||
),
|
||||
content = ReviewExportState.ViewState.Content(
|
||||
itemTypeCounts = ReviewExportState.ItemTypeCounts(
|
||||
passwordCount = 14,
|
||||
passkeyCount = 14,
|
||||
identityCount = 3,
|
||||
secureNoteCount = 5,
|
||||
),
|
||||
),
|
||||
onImportItemsClick = {},
|
||||
@ -326,3 +353,35 @@ private fun ReviewExportContent_preview() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Preview(showBackground = true, name = "Review Export Empty Content")
|
||||
@Composable
|
||||
private fun ReviewExportContent_NoItems_preview() {
|
||||
ExportItemsScaffold(
|
||||
navIcon = rememberVectorPainter(BitwardenDrawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(BitwardenString.close),
|
||||
onNavigationIconClick = { },
|
||||
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
|
||||
) {
|
||||
BitwardenEmptyContent(
|
||||
title = stringResource(BitwardenString.no_items_available_to_import),
|
||||
text = stringResource(
|
||||
BitwardenString
|
||||
.your_vault_may_be_empty_or_import_some_item_types_isnt_supported,
|
||||
),
|
||||
primaryButton = BitwardenButtonData(
|
||||
label = BitwardenString.select_a_different_account.asText(),
|
||||
testTag = "SelectADifferentAccountButton",
|
||||
onClick = { },
|
||||
),
|
||||
secondaryButton = BitwardenButtonData(
|
||||
label = BitwardenString.cancel.asText(),
|
||||
testTag = "NoItemsCancelButton",
|
||||
onClick = { },
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.vault.feature.exportitems.reviewexport
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.credentials.providerevents.exception.ImportCredentialsCancellationException
|
||||
import androidx.credentials.providerevents.exception.ImportCredentialsException
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
@ -29,6 +30,7 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -53,9 +55,10 @@ class ReviewExportViewModel @Inject constructor(
|
||||
.specialCircumstance
|
||||
?.toImportCredentialsRequestDataOrNull(),
|
||||
),
|
||||
viewState = ReviewExportState.ViewState(
|
||||
viewState = ReviewExportState.ViewState.Content(
|
||||
itemTypeCounts = ReviewExportState.ItemTypeCounts(),
|
||||
),
|
||||
dialog = null,
|
||||
),
|
||||
) {
|
||||
|
||||
@ -73,6 +76,7 @@ class ReviewExportViewModel @Inject constructor(
|
||||
is ReviewExportAction.CancelClick -> handleCancelClicked()
|
||||
is ReviewExportAction.DismissDialog -> handleDismissDialog()
|
||||
is ReviewExportAction.NavigateBackClick -> handleBackClick()
|
||||
is ReviewExportAction.SelectAnotherAccountClick -> handleSelectAnotherAccountClick()
|
||||
is ReviewExportAction.Internal -> handleInternalAction(action)
|
||||
}
|
||||
}
|
||||
@ -111,7 +115,13 @@ class ReviewExportViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleCancelClicked() {
|
||||
sendEvent(ReviewExportEvent.NavigateBack)
|
||||
sendEvent(
|
||||
ReviewExportEvent.CompleteExport(
|
||||
ExportCredentialsResult.Failure(
|
||||
ImportCredentialsCancellationException(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleDismissDialog() {
|
||||
@ -122,6 +132,10 @@ class ReviewExportViewModel @Inject constructor(
|
||||
sendEvent(ReviewExportEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleSelectAnotherAccountClick() {
|
||||
sendEvent(ReviewExportEvent.NavigateToAccountSelection)
|
||||
}
|
||||
|
||||
private fun handleInternalAction(action: ReviewExportAction.Internal) {
|
||||
when (action) {
|
||||
is ReviewExportAction.Internal.VaultDataReceive -> {
|
||||
@ -168,7 +182,7 @@ class ReviewExportViewModel @Inject constructor(
|
||||
private fun handleVaultDataError(data: DataState.Error<DecryptCipherListResult>) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = it.viewState.copy(
|
||||
viewState = ReviewExportState.ViewState.Content(
|
||||
itemTypeCounts = data.data.toItemTypeCounts(),
|
||||
),
|
||||
dialog = ReviewExportState.DialogState.General(
|
||||
@ -253,10 +267,17 @@ class ReviewExportViewModel @Inject constructor(
|
||||
clearDialog: Boolean,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
val itemTypeCounts = data.data.toItemTypeCounts()
|
||||
val viewState = if (itemTypeCounts.hasItemsToExport) {
|
||||
ReviewExportState.ViewState.Content(
|
||||
itemTypeCounts = itemTypeCounts,
|
||||
)
|
||||
} else {
|
||||
ReviewExportState.ViewState.NoItems
|
||||
}
|
||||
|
||||
it.copy(
|
||||
viewState = it.viewState.copy(
|
||||
itemTypeCounts = data.data.toItemTypeCounts(),
|
||||
),
|
||||
viewState = viewState,
|
||||
dialog = it.dialog.takeUnless { clearDialog },
|
||||
)
|
||||
}
|
||||
@ -281,9 +302,20 @@ data class ReviewExportState(
|
||||
* Represents the view state with item type counts.
|
||||
*/
|
||||
@Parcelize
|
||||
data class ViewState(
|
||||
val itemTypeCounts: ItemTypeCounts,
|
||||
) : Parcelable
|
||||
sealed class ViewState : Parcelable {
|
||||
|
||||
/**
|
||||
* Represents the content state with item type counts.
|
||||
*/
|
||||
data class Content(
|
||||
val itemTypeCounts: ItemTypeCounts,
|
||||
) : ViewState()
|
||||
|
||||
/**
|
||||
* Represents the state when there are no items to be exported.
|
||||
*/
|
||||
data object NoItems : ViewState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the counts of different item types to be exported.
|
||||
@ -295,7 +327,17 @@ data class ReviewExportState(
|
||||
val identityCount: Int = 0,
|
||||
val cardCount: Int = 0,
|
||||
val secureNoteCount: Int = 0,
|
||||
) : Parcelable
|
||||
) : Parcelable {
|
||||
/**
|
||||
* Whether there are any items to be exported.
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
val hasItemsToExport: Boolean = passwordCount > 0 ||
|
||||
passkeyCount > 0 ||
|
||||
identityCount > 0 ||
|
||||
cardCount > 0 ||
|
||||
secureNoteCount > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the possible dialog states for the Review Import screen.
|
||||
@ -350,6 +392,11 @@ sealed class ReviewExportAction {
|
||||
*/
|
||||
data object NavigateBackClick : ReviewExportAction()
|
||||
|
||||
/**
|
||||
* Action triggered when the Select another account button is clicked by the user.
|
||||
*/
|
||||
data object SelectAnotherAccountClick : ReviewExportAction()
|
||||
|
||||
/**
|
||||
* Internal actions that the [ReviewExportViewModel] itself may send.
|
||||
*/
|
||||
@ -382,6 +429,11 @@ sealed class ReviewExportEvent {
|
||||
*/
|
||||
data object NavigateBack : ReviewExportEvent()
|
||||
|
||||
/**
|
||||
* Event to navigate to account selection.
|
||||
*/
|
||||
data object NavigateToAccountSelection : ReviewExportEvent()
|
||||
|
||||
/**
|
||||
* Event indicating that the import attempt has completed.
|
||||
* The consuming screen or navigation controller should handle this event to proceed
|
||||
|
||||
@ -15,6 +15,7 @@ import com.x8bit.bitwarden.ui.vault.feature.exportitems.reviewexport.ReviewExpor
|
||||
*/
|
||||
data class ReviewExportHandlers(
|
||||
val onImportItemsClick: () -> Unit,
|
||||
val onSelectAnotherAccountClick: () -> Unit,
|
||||
val onCancelClick: () -> Unit,
|
||||
val onDismissDialog: () -> Unit,
|
||||
val onNavigateBackClick: () -> Unit,
|
||||
@ -36,6 +37,9 @@ data class ReviewExportHandlers(
|
||||
onImportItemsClick = {
|
||||
viewModel.trySendAction(ReviewExportAction.ImportItemsClick)
|
||||
},
|
||||
onSelectAnotherAccountClick = {
|
||||
viewModel.trySendAction(ReviewExportAction.SelectAnotherAccountClick)
|
||||
},
|
||||
onCancelClick = {
|
||||
viewModel.trySendAction(ReviewExportAction.CancelClick)
|
||||
},
|
||||
|
||||
@ -40,3 +40,13 @@ fun NavController.navigateToSelectAccountScreen(
|
||||
navOptions = navOptions,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop up to the [SelectAccountScreen].
|
||||
*/
|
||||
fun NavController.popUpToSelectAccountScreen() {
|
||||
popBackStack(
|
||||
route = SelectAccountRoute,
|
||||
inclusive = false,
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.exportitems.reviewexport
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsNotDisplayed
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.isDisplayed
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.cxf.manager.CredentialExchangeCompletionManager
|
||||
@ -28,6 +31,7 @@ import org.junit.Test
|
||||
class ReviewExportScreenTest : BitwardenComposeTest() {
|
||||
|
||||
private var onNavigateBackCalled = false
|
||||
private var onSelectAnotherAccountCalled = false
|
||||
private val credentialExchangeCompletionManager = mockk<CredentialExchangeCompletionManager> {
|
||||
every { completeCredentialExport(any()) } just runs
|
||||
}
|
||||
@ -46,6 +50,7 @@ class ReviewExportScreenTest : BitwardenComposeTest() {
|
||||
) {
|
||||
ReviewExportScreen(
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
onNavigateToAccountSelection = { onSelectAnotherAccountCalled = true },
|
||||
viewModel = mockViewModel,
|
||||
)
|
||||
}
|
||||
@ -134,11 +139,52 @@ class ReviewExportScreenTest : BitwardenComposeTest() {
|
||||
mockViewModel.trySendAction(ReviewExportAction.NavigateBackClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `EmptyContent should be displayed when no items to import`() {
|
||||
// Verify initial state is ReviewExportContent
|
||||
composeTestRule
|
||||
.onNodeWithText("No items available to import")
|
||||
.assertIsNotDisplayed()
|
||||
|
||||
mockStateFlow.tryEmit(
|
||||
DEFAULT_STATE.copy(
|
||||
viewState = ReviewExportState.ViewState.NoItems,
|
||||
),
|
||||
)
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("No items available to import")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SelectAnotherAccount click should send SelectAnotherAccountClick action`() {
|
||||
mockStateFlow.tryEmit(
|
||||
DEFAULT_STATE.copy(
|
||||
viewState = ReviewExportState.ViewState.NoItems,
|
||||
),
|
||||
)
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Select a different account")
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
mockViewModel.trySendAction(ReviewExportAction.SelectAnotherAccountClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_STATE = ReviewExportState(
|
||||
viewState = ReviewExportState.ViewState(
|
||||
itemTypeCounts = ReviewExportState.ItemTypeCounts(),
|
||||
viewState = ReviewExportState.ViewState.Content(
|
||||
itemTypeCounts = ReviewExportState.ItemTypeCounts(
|
||||
passwordCount = 1,
|
||||
passkeyCount = 1,
|
||||
identityCount = 1,
|
||||
cardCount = 1,
|
||||
secureNoteCount = 1,
|
||||
),
|
||||
),
|
||||
importCredentialsRequestData = ImportCredentialsRequestData(
|
||||
uri = Uri.EMPTY,
|
||||
|
||||
@ -46,12 +46,7 @@ class ReviewExportViewModelTest : BaseViewModelTest() {
|
||||
every { userStateFlow } returns mutableUserStateFlow
|
||||
}
|
||||
private val decryptCipherListResultFlow = MutableStateFlow<DataState<DecryptCipherListResult>>(
|
||||
DataState.Loaded(
|
||||
data = DecryptCipherListResult(
|
||||
successes = emptyList(),
|
||||
failures = emptyList(),
|
||||
),
|
||||
),
|
||||
DataState.Loaded(data = createMockDecryptCipherListResult(number = 1)),
|
||||
)
|
||||
private val vaultRepository = mockk<VaultRepository> {
|
||||
every { decryptCipherListResultStateFlow } returns decryptCipherListResultFlow
|
||||
@ -68,13 +63,42 @@ class ReviewExportViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class Initialization {
|
||||
inner class State {
|
||||
@Test
|
||||
fun `initial state is correct when SavedStateHandle is empty`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
fun `State should be NoItems when no items to export`() = runTest {
|
||||
val initialState = ReviewExportState(
|
||||
viewState = ReviewExportState.ViewState.NoItems,
|
||||
dialog = null,
|
||||
importCredentialsRequestData = DEFAULT_REQUEST_DATA,
|
||||
)
|
||||
decryptCipherListResultFlow.value = DataState.Loaded(
|
||||
data = DecryptCipherListResult(
|
||||
successes = emptyList(),
|
||||
failures = emptyList(),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(initialState, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `State should be Content when items to export`() = runTest {
|
||||
val expectedState = ReviewExportState(
|
||||
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
|
||||
itemTypeCounts = DEFAULT_CONTENT_VIEW_STATE.itemTypeCounts.copy(
|
||||
passwordCount = 1,
|
||||
),
|
||||
),
|
||||
dialog = null,
|
||||
importCredentialsRequestData = DEFAULT_REQUEST_DATA,
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(expectedState, awaitItem())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@ -83,12 +107,13 @@ class ReviewExportViewModelTest : BaseViewModelTest() {
|
||||
@Test
|
||||
fun `ImportItemsClick shows loading, and calls exportVaultDataToCxf with all active items if there are no item restrictions`() =
|
||||
runTest {
|
||||
val mockActiveCipherListView = createMockCipherListView(
|
||||
val mockActiveCardCipherListView = createMockCipherListView(
|
||||
number = 1,
|
||||
type = CipherListViewType.Card(
|
||||
createMockCardListView(number = 1),
|
||||
),
|
||||
)
|
||||
val mockActiveLoginCipherListView = createMockCipherListView(number = 1)
|
||||
val mockDeletedCipherListView = createMockCipherListView(
|
||||
number = 1,
|
||||
isDeleted = true,
|
||||
@ -98,14 +123,20 @@ class ReviewExportViewModelTest : BaseViewModelTest() {
|
||||
createMockDecryptCipherListResult(
|
||||
number = 1,
|
||||
successes = listOf(
|
||||
mockActiveCipherListView,
|
||||
mockActiveLoginCipherListView,
|
||||
mockActiveCardCipherListView,
|
||||
mockDeletedCipherListView,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
coEvery {
|
||||
vaultRepository.exportVaultDataToCxf(listOf(mockActiveCipherListView))
|
||||
vaultRepository.exportVaultDataToCxf(
|
||||
listOf(
|
||||
mockActiveLoginCipherListView,
|
||||
mockActiveCardCipherListView,
|
||||
),
|
||||
)
|
||||
} just awaits
|
||||
|
||||
val viewModel = createViewModel()
|
||||
@ -114,8 +145,8 @@ class ReviewExportViewModelTest : BaseViewModelTest() {
|
||||
// Check for loading dialog
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
viewState = DEFAULT_STATE.viewState.copy(
|
||||
itemTypeCounts = DEFAULT_STATE.viewState.itemTypeCounts.copy(
|
||||
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
|
||||
itemTypeCounts = DEFAULT_CONTENT_VIEW_STATE.itemTypeCounts.copy(
|
||||
cardCount = 1,
|
||||
),
|
||||
),
|
||||
@ -127,7 +158,12 @@ class ReviewExportViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
|
||||
coVerify {
|
||||
vaultRepository.exportVaultDataToCxf(listOf(mockActiveCipherListView))
|
||||
vaultRepository.exportVaultDataToCxf(
|
||||
listOf(
|
||||
mockActiveLoginCipherListView,
|
||||
mockActiveCardCipherListView,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -207,24 +243,41 @@ class ReviewExportViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CancelClicked sends NavigateBack event`() = runTest {
|
||||
fun `NavigateToAccountSelection sends SelectAnotherAccount event`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(ReviewExportAction.SelectAnotherAccountClick)
|
||||
assertEquals(
|
||||
ReviewExportEvent.NavigateToAccountSelection,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CancelClicked sends CompleteExport event`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(ReviewExportAction.CancelClick)
|
||||
assertEquals(ReviewExportEvent.NavigateBack, awaitItem())
|
||||
assertTrue(awaitItem() is ReviewExportEvent.CompleteExport)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DismissDialog clears dialog from state`() = runTest {
|
||||
decryptCipherListResultFlow.value = DataState.Loading
|
||||
val viewModel = createViewModel()
|
||||
val exception = IllegalStateException()
|
||||
decryptCipherListResultFlow.value = DataState.Error(
|
||||
error = exception,
|
||||
data = createMockDecryptCipherListResult(number = 1),
|
||||
)
|
||||
// Check for loading dialog
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
dialog = ReviewExportState.DialogState.Loading(
|
||||
BitwardenString.loading.asText(),
|
||||
dialog = ReviewExportState.DialogState.General(
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.generic_error_message.asText(),
|
||||
error = exception,
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
@ -254,8 +307,8 @@ class ReviewExportViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
|
||||
val expectedState = DEFAULT_STATE.copy(
|
||||
viewState = DEFAULT_STATE.viewState.copy(
|
||||
itemTypeCounts = DEFAULT_STATE.viewState.itemTypeCounts.copy(
|
||||
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
|
||||
itemTypeCounts = DEFAULT_CONTENT_VIEW_STATE.itemTypeCounts.copy(
|
||||
passwordCount = 1,
|
||||
),
|
||||
),
|
||||
@ -283,8 +336,8 @@ class ReviewExportViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
|
||||
val expectedState = DEFAULT_STATE.copy(
|
||||
viewState = DEFAULT_STATE.viewState.copy(
|
||||
itemTypeCounts = DEFAULT_STATE.viewState.itemTypeCounts.copy(
|
||||
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
|
||||
itemTypeCounts = DEFAULT_CONTENT_VIEW_STATE.itemTypeCounts.copy(
|
||||
passwordCount = 1,
|
||||
),
|
||||
),
|
||||
@ -312,8 +365,8 @@ class ReviewExportViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
|
||||
val expectedState = DEFAULT_STATE.copy(
|
||||
viewState = DEFAULT_STATE.viewState.copy(
|
||||
itemTypeCounts = DEFAULT_STATE.viewState.itemTypeCounts.copy(
|
||||
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
|
||||
itemTypeCounts = DEFAULT_CONTENT_VIEW_STATE.itemTypeCounts.copy(
|
||||
passwordCount = 1,
|
||||
),
|
||||
),
|
||||
@ -365,11 +418,14 @@ private val DEFAULT_REQUEST_DATA = ImportCredentialsRequestData(
|
||||
uri = MOCK_URI,
|
||||
requestJson = "mockRequestJson",
|
||||
)
|
||||
private val DEFAULT_CONTENT_VIEW_STATE = ReviewExportState.ViewState.Content(
|
||||
itemTypeCounts = ReviewExportState.ItemTypeCounts(
|
||||
passwordCount = 1,
|
||||
),
|
||||
)
|
||||
private val DEFAULT_STATE: ReviewExportState = ReviewExportState(
|
||||
importCredentialsRequestData = DEFAULT_REQUEST_DATA,
|
||||
viewState = ReviewExportState.ViewState(
|
||||
itemTypeCounts = ReviewExportState.ItemTypeCounts(),
|
||||
),
|
||||
viewState = DEFAULT_CONTENT_VIEW_STATE,
|
||||
)
|
||||
private const val DEFAULT_USER_ID: String = "activeUserId"
|
||||
private val DEFAULT_USER_STATE = UserState(
|
||||
|
||||
@ -1132,4 +1132,7 @@ Do you want to switch to this account?</string>
|
||||
<string name="kdf_update_failed_active_account_not_found">Kdf update failed, active account not found. Please try again or contact us.</string>
|
||||
<string name="an_error_occurred_while_trying_to_update_your_kdf_settings">An error occurred while trying to update your kdf settings. Please try again or contact us.</string>
|
||||
<string name="the_import_request_could_not_be_processed">The import request could not be processed.</string>
|
||||
<string name="your_vault_may_be_empty_or_import_some_item_types_isnt_supported">Your vault may be empty, or importing some item types isn’t allowed for your account.</string>
|
||||
<string name="no_items_available_to_import">No items available to import</string>
|
||||
<string name="select_a_different_account">Select a different account</string>
|
||||
</resources>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user