[PM-26803] Show empty state when no items are available for export (#6023)

This commit is contained in:
Patrick Honkonen 2025-10-14 16:01:17 -04:00 committed by GitHub
parent 5b5176db40
commit af737b3f07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 306 additions and 74 deletions

View File

@ -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() },
)
}
}

View File

@ -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(),
)
}
}

View File

@ -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

View File

@ -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)
},

View File

@ -40,3 +40,13 @@ fun NavController.navigateToSelectAccountScreen(
navOptions = navOptions,
)
}
/**
* Pop up to the [SelectAccountScreen].
*/
fun NavController.popUpToSelectAccountScreen() {
popBackStack(
route = SelectAccountRoute,
inclusive = false,
)
}

View File

@ -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,

View File

@ -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(

View File

@ -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 isnt 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>