[PM-25825] Add ImportItems navigation (#5915)

Co-authored-by: David Perez <david@livefront.com>
This commit is contained in:
Patrick Honkonen 2025-09-22 17:33:08 -04:00 committed by GitHub
parent bc1dd730ec
commit 8a2bcfade8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 338 additions and 116 deletions

View File

@ -32,6 +32,7 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.other.navigateToOther
import com.x8bit.bitwarden.ui.platform.feature.settings.other.otherDestination
import com.x8bit.bitwarden.ui.platform.feature.settings.vault.navigateToVaultSettings
import com.x8bit.bitwarden.ui.platform.feature.settings.vault.vaultSettingsDestination
import com.x8bit.bitwarden.ui.vault.feature.importitems.importItemsDestination
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@ -114,6 +115,7 @@ fun NavGraphBuilder.settingsGraph(
onNavigateToFlightRecorder: () -> Unit,
onNavigateToRecordedLogs: () -> Unit,
onNavigateToImportLogins: () -> Unit,
onNavigateToImportItems: () -> Unit,
onNavigateToAboutPrivilegedApps: () -> Unit,
) {
navigation<SettingsGraphRoute>(
@ -162,6 +164,11 @@ fun NavGraphBuilder.settingsGraph(
onNavigateToExportVault = onNavigateToExportVault,
onNavigateToFolders = onNavigateToFolders,
onNavigateToImportLogins = onNavigateToImportLogins,
onNavigateToImportItems = onNavigateToImportItems,
)
importItemsDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateToImportLogins = onNavigateToImportLogins,
)
blockAutoFillDestination(onNavigateBack = { navController.popBackStack() })
privilegedAppsListDestination(onNavigateBack = { navController.popBackStack() })

View File

@ -20,6 +20,7 @@ fun NavGraphBuilder.vaultSettingsDestination(
onNavigateToExportVault: () -> Unit,
onNavigateToFolders: () -> Unit,
onNavigateToImportLogins: () -> Unit,
onNavigateToImportItems: () -> Unit,
) {
composableWithPushTransitions<VaultSettingsRoute> {
VaultSettingsScreen(
@ -27,6 +28,7 @@ fun NavGraphBuilder.vaultSettingsDestination(
onNavigateToExportVault = onNavigateToExportVault,
onNavigateToFolders = onNavigateToFolders,
onNavigateToImportLogins = onNavigateToImportLogins,
onNavigateToImportItems = onNavigateToImportItems,
)
}
}

View File

@ -49,6 +49,7 @@ fun VaultSettingsScreen(
onNavigateToExportVault: () -> Unit,
onNavigateToFolders: () -> Unit,
onNavigateToImportLogins: () -> Unit,
onNavigateToImportItems: () -> Unit,
viewModel: VaultSettingsViewModel = hiltViewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
@ -60,6 +61,7 @@ fun VaultSettingsScreen(
VaultSettingsEvent.NavigateToExportVault -> onNavigateToExportVault()
VaultSettingsEvent.NavigateToFolders -> onNavigateToFolders()
is VaultSettingsEvent.NavigateToImportVault -> onNavigateToImportLogins()
is VaultSettingsEvent.NavigateToImportItems -> onNavigateToImportItems()
is VaultSettingsEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data)
}
}

View File

@ -1,9 +1,11 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.vault
import androidx.lifecycle.viewModelScope
import com.bitwarden.core.data.manager.model.FlagKey
import com.bitwarden.ui.platform.base.BackgroundEvent
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
@ -22,6 +24,7 @@ import javax.inject.Inject
class VaultSettingsViewModel @Inject constructor(
snackbarRelayManager: SnackbarRelayManager,
private val firstTimeActionManager: FirstTimeActionManager,
private val featureFlagManager: FeatureFlagManager,
) : BaseViewModel<VaultSettingsState, VaultSettingsEvent, VaultSettingsAction>(
initialState = run {
val firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState
@ -106,7 +109,11 @@ class VaultSettingsViewModel @Inject constructor(
}
private fun handleImportItemsClicked() {
sendEvent(VaultSettingsEvent.NavigateToImportVault)
if (featureFlagManager.getFeatureFlag(FlagKey.CredentialExchangeProtocolImport)) {
sendEvent(VaultSettingsEvent.NavigateToImportItems)
} else {
sendEvent(VaultSettingsEvent.NavigateToImportVault)
}
}
}
@ -131,6 +138,11 @@ sealed class VaultSettingsEvent {
*/
data object NavigateToImportVault : VaultSettingsEvent()
/**
* Navigate to the import vault URL.
*/
data object NavigateToImportItems : VaultSettingsEvent()
/**
* Navigate to the Export Vault screen.
*/

View File

@ -1,4 +1,5 @@
@file:OmitFromCoverage
package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar
import androidx.navigation.NavController

View File

@ -40,6 +40,7 @@ import com.x8bit.bitwarden.ui.tools.feature.send.navigateToSendGraph
import com.x8bit.bitwarden.ui.tools.feature.send.sendGraph
import com.x8bit.bitwarden.ui.tools.feature.send.viewsend.ViewSendRoute
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
import com.x8bit.bitwarden.ui.vault.feature.importitems.navigateToImportItemsScreen
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemArgs
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultGraphRoute
import com.x8bit.bitwarden.ui.vault.feature.vault.navigateToVaultGraph
@ -265,6 +266,7 @@ private fun VaultUnlockedNavBarScaffold(
onNavigateToSetupUnlockScreen = onNavigateToSetupUnlockScreen,
onNavigateToSetupAutoFillScreen = onNavigateToSetupAutoFillScreen,
onNavigateToImportLogins = onNavigateToImportLogins,
onNavigateToImportItems = { navController.navigateToImportItemsScreen() },
onNavigateToFlightRecorder = onNavigateToFlightRecorder,
onNavigateToRecordedLogs = onNavigateToRecordedLogs,
onNavigateToAboutPrivilegedApps = onNavigateToAboutPrivilegedApps,

View File

@ -21,5 +21,4 @@ enum class SnackbarRelay {
LOGINS_IMPORTED,
SEND_DELETED,
SEND_UPDATED,
VAULT_SYNC_FAILED,
}

View File

@ -0,0 +1,40 @@
@file:OmitFromCoverage
package com.x8bit.bitwarden.ui.vault.feature.importitems
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
import kotlinx.serialization.Serializable
/**
* The type-safe route for the import items screen.
*/
@Serializable
data object ImportItemsRoute
/**
* Helper function to navigate to the import items screen.
*/
fun NavController.navigateToImportItemsScreen(
navOptions: NavOptions? = null,
) {
navigate(route = ImportItemsRoute, navOptions = navOptions)
}
/**
* Add the import items screen to the nav graph.
*/
fun NavGraphBuilder.importItemsDestination(
onNavigateBack: () -> Unit,
onNavigateToImportLogins: () -> Unit,
) {
composableWithPushTransitions<ImportItemsRoute> {
ImportItemsScreen(
onNavigateBack = onNavigateBack,
onNavigateToImportFromComputer = onNavigateToImportLogins,
)
}
}

View File

@ -30,6 +30,9 @@ import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.components.row.BitwardenTextRow
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarHostState
import com.bitwarden.ui.platform.components.snackbar.model.rememberBitwardenSnackbarHostState
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
@ -46,7 +49,6 @@ import kotlinx.coroutines.launch
@Composable
fun ImportItemsScreen(
onNavigateBack: () -> Unit,
onNavigateToVault: () -> Unit,
onNavigateToImportFromComputer: () -> Unit,
viewModel: ImportItemsViewModel = hiltViewModel(),
credentialExchangeImporter: CredentialExchangeImporter =
@ -55,11 +57,11 @@ fun ImportItemsScreen(
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val coroutineScope = rememberCoroutineScope()
val handler = rememberImportItemsHandler(viewModel = viewModel)
val snackbarHostState = rememberBitwardenSnackbarHostState()
EventsEffect(viewModel) { event ->
when (event) {
ImportItemsEvent.NavigateBack -> onNavigateBack()
ImportItemsEvent.NavigateToVault -> onNavigateToVault()
ImportItemsEvent.NavigateToImportFromComputer -> onNavigateToImportFromComputer()
is ImportItemsEvent.ShowRegisteredImportSources -> {
coroutineScope.launch {
@ -73,6 +75,17 @@ fun ImportItemsScreen(
)
}
}
is ImportItemsEvent.ShowBasicSnackbar -> {
snackbarHostState.showSnackbar(event.data)
}
is ImportItemsEvent.ShowSyncFailedSnackbar -> {
snackbarHostState.showSnackbar(
snackbarData = event.data,
onActionPerformed = handler.onSyncFailedTryAgainClick,
)
}
}
}
@ -85,6 +98,7 @@ fun ImportItemsScreen(
onNavigateBack = handler.onNavigateBack,
onImportFromComputerClick = handler.onImportFromComputerClick,
onImportFromAnotherAppClick = handler.onImportFromAnotherAppClick,
snackbarHostState = snackbarHostState,
)
}
@ -95,6 +109,7 @@ private fun ImportItemsScaffold(
onImportFromComputerClick: () -> Unit,
onImportFromAnotherAppClick: () -> Unit,
modifier: Modifier = Modifier,
snackbarHostState: BitwardenSnackbarHostState = rememberBitwardenSnackbarHostState(),
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
BitwardenScaffold(
@ -112,6 +127,11 @@ private fun ImportItemsScaffold(
scrollBehavior = scrollBehavior,
)
},
snackbarHost = {
BitwardenSnackbarHost(
bitwardenHostState = snackbarHostState,
)
},
) {
ImportItemsContent(
onImportFromComputerClick = onImportFromComputerClick,

View File

@ -13,10 +13,9 @@ import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asPluralsText
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.ImportCredentialsResult
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@ -33,7 +32,6 @@ private const val KEY_STATE = "state"
class ImportItemsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val vaultRepository: VaultRepository,
private val snackbarRelayManager: SnackbarRelayManager,
) : BaseViewModel<ImportItemsState, ImportItemsEvent, ImportItemsAction>(
initialState = savedStateHandle[KEY_STATE] ?: ImportItemsState(),
) {
@ -52,14 +50,6 @@ class ImportItemsViewModel @Inject constructor(
handleImportCredentialSelectionReceive(action)
}
ImportItemsAction.ReturnToVaultClick -> {
handleReturnToVaultClick()
}
is ImportItemsAction.Internal.ImportCredentialsResultReceive -> {
handleImportCredentialsResultReceive(action)
}
ImportItemsAction.ImportFromComputerClick -> {
handleImportFromComputerClick()
}
@ -67,11 +57,59 @@ class ImportItemsViewModel @Inject constructor(
ImportItemsAction.DismissDialog -> {
handleDismissDialog()
}
ImportItemsAction.SyncFailedTryAgainClick -> {
handleSyncFailedTryAgainClick()
}
is ImportItemsAction.Internal -> {
handleInternalAction(action)
}
}
}
private fun handleReturnToVaultClick() {
sendEvent(ImportItemsEvent.NavigateToVault)
private fun handleInternalAction(action: ImportItemsAction.Internal) {
when (action) {
is ImportItemsAction.Internal.ImportCredentialsResultReceive -> {
handleImportCredentialsResultReceive(action)
}
is ImportItemsAction.Internal.RetrySyncResultReceive -> {
handleRetrySyncResultReceive(action)
}
}
}
private fun handleRetrySyncResultReceive(
action: ImportItemsAction.Internal.RetrySyncResultReceive,
) {
clearDialogs()
when (action.result) {
is SyncVaultDataResult.Success -> {
sendEvent(
ImportItemsEvent.ShowBasicSnackbar(
data = BitwardenSnackbarData(
message = BitwardenString.syncing_complete.asText(),
),
),
)
}
is SyncVaultDataResult.Error -> {
showSyncFailedSnackbar()
}
}
}
private fun handleSyncFailedTryAgainClick() {
showLoadingDialog(message = BitwardenString.syncing.asText())
viewModelScope.launch {
sendAction(
ImportItemsAction.Internal.RetrySyncResultReceive(
result = vaultRepository.syncForResult(),
),
)
}
}
private fun handleBackClick() {
@ -118,7 +156,7 @@ class ImportItemsViewModel @Inject constructor(
}
is ImportCredentialsSelectionResult.Success -> {
updateImportProgress(BitwardenString.saving_items.asText())
showLoadingDialog(BitwardenString.saving_items.asText())
viewModelScope.launch {
sendAction(
ImportItemsAction.Internal.ImportCredentialsResultReceive(
@ -150,19 +188,19 @@ class ImportItemsViewModel @Inject constructor(
is ImportCredentialsResult.Success -> {
clearDialogs()
snackbarRelayManager.sendSnackbarData(
data = BitwardenSnackbarData(
messageHeader = BitwardenString.import_successful.asText(),
message = BitwardenPlurals
.x_items_have_been_imported_to_your_vault
.asPluralsText(
quantity = action.result.itemCount,
args = arrayOf(action.result.itemCount),
),
sendEvent(
ImportItemsEvent.ShowBasicSnackbar(
data = BitwardenSnackbarData(
messageHeader = BitwardenString.import_successful.asText(),
message = BitwardenPlurals
.x_items_have_been_imported_to_your_vault
.asPluralsText(
quantity = action.result.itemCount,
args = arrayOf(action.result.itemCount),
),
),
),
relay = SnackbarRelay.LOGINS_IMPORTED,
)
sendEvent(ImportItemsEvent.NavigateToVault)
}
ImportCredentialsResult.NoItems -> {
@ -175,20 +213,25 @@ class ImportItemsViewModel @Inject constructor(
is ImportCredentialsResult.SyncFailed -> {
clearDialogs()
snackbarRelayManager.sendSnackbarData(
data = BitwardenSnackbarData(
messageHeader = BitwardenString.vault_sync_failed.asText(),
message = BitwardenString
.your_items_have_been_successfully_imported_but_could_not_sync_vault
.asText(),
actionLabel = BitwardenString.try_again.asText(),
),
relay = SnackbarRelay.VAULT_SYNC_FAILED,
)
showSyncFailedSnackbar()
}
}
}
private fun showSyncFailedSnackbar() {
sendEvent(
ImportItemsEvent.ShowSyncFailedSnackbar(
data = BitwardenSnackbarData(
messageHeader = BitwardenString.vault_sync_failed.asText(),
message = BitwardenString
.your_items_have_been_successfully_imported_but_could_not_sync_vault
.asText(),
actionLabel = BitwardenString.try_again.asText(),
),
),
)
}
private fun clearDialogs() {
mutableStateFlow.update { it.copy(dialog = null) }
}
@ -209,7 +252,7 @@ class ImportItemsViewModel @Inject constructor(
}
}
private fun updateImportProgress(message: Text) {
private fun showLoadingDialog(message: Text) {
mutableStateFlow.update {
it.copy(
dialog = ImportItemsState.DialogState.Loading(message = message),
@ -272,11 +315,6 @@ sealed class ImportItemsAction {
val selectionResult: ImportCredentialsSelectionResult,
) : ImportItemsAction()
/**
* User clicked the Return to vault button.
*/
data object ReturnToVaultClick : ImportItemsAction()
/**
* User clicked the back button.
*/
@ -287,6 +325,11 @@ sealed class ImportItemsAction {
*/
data object DismissDialog : ImportItemsAction()
/**
* User clicked the try again button.
*/
data object SyncFailedTryAgainClick : ImportItemsAction()
/**
* Internal actions that the [ImportItemsViewModel] may itself send.
*/
@ -294,7 +337,16 @@ sealed class ImportItemsAction {
/**
* Import CXF result received.
*/
data class ImportCredentialsResultReceive(val result: ImportCredentialsResult) : Internal()
data class ImportCredentialsResultReceive(
val result: ImportCredentialsResult,
) : Internal()
/**
* Retry sync result received.
*/
data class RetrySyncResultReceive(
val result: SyncVaultDataResult,
) : Internal()
}
}
@ -313,11 +365,6 @@ sealed class ImportItemsEvent {
*/
data object NavigateToImportFromComputer : ImportItemsEvent()
/**
* Navigate to the vault.
*/
data object NavigateToVault : ImportItemsEvent()
/**
* Show registered import sources.
*
@ -326,4 +373,18 @@ sealed class ImportItemsEvent {
data class ShowRegisteredImportSources(
val credentialTypes: List<String>,
) : ImportItemsEvent(), BackgroundEvent
/**
* Show a basic snackbar.
*/
data class ShowBasicSnackbar(
val data: BitwardenSnackbarData,
) : ImportItemsEvent(), BackgroundEvent
/**
* Show a snackbar indicating that the sync failed, with an option to retry.
*/
data class ShowSyncFailedSnackbar(
val data: BitwardenSnackbarData,
) : ImportItemsEvent(), BackgroundEvent
}

View File

@ -18,6 +18,7 @@ data class ImportItemsHandler(
val onDismissDialog: () -> Unit,
val onImportFromAnotherAppClick: () -> Unit,
val onImportFromComputerClick: () -> Unit,
val onSyncFailedTryAgainClick: () -> Unit,
) {
@Suppress("UndocumentedPublicClass")
@ -30,14 +31,17 @@ data class ImportItemsHandler(
onNavigateBack = {
viewModel.trySendAction(ImportItemsAction.BackClick)
},
onDismissDialog = {
viewModel.trySendAction(ImportItemsAction.DismissDialog)
},
onImportFromAnotherAppClick = {
viewModel.trySendAction(ImportItemsAction.ImportFromAnotherAppClick)
},
onImportFromComputerClick = {
viewModel.trySendAction(ImportItemsAction.ImportFromComputerClick)
},
onDismissDialog = {
viewModel.trySendAction(ImportItemsAction.DismissDialog)
onSyncFailedTryAgainClick = {
viewModel.trySendAction(ImportItemsAction.SyncFailedTryAgainClick)
},
)
}

View File

@ -190,7 +190,6 @@ class VaultViewModel @Inject constructor(
SnackbarRelay.CIPHER_RESTORED,
SnackbarRelay.CIPHER_UPDATED,
SnackbarRelay.LOGINS_IMPORTED,
SnackbarRelay.VAULT_SYNC_FAILED,
),
)
.map { VaultAction.Internal.SnackbarDataReceive(it) }

View File

@ -24,6 +24,7 @@ import org.junit.Test
class VaultSettingsScreenTest : BitwardenComposeTest() {
private var onNavigateToImportLoginsCalled = false
private var onNavigateToImportItemsCalled = false
private var onNavigateBackCalled = false
private var onNavigateToExportVaultCalled = false
private var onNavigateToFoldersCalled = false
@ -48,6 +49,7 @@ class VaultSettingsScreenTest : BitwardenComposeTest() {
onNavigateToExportVault = { onNavigateToExportVaultCalled = true },
onNavigateToFolders = { onNavigateToFoldersCalled = true },
onNavigateToImportLogins = { onNavigateToImportLoginsCalled = true },
onNavigateToImportItems = { onNavigateToImportItemsCalled = true },
)
}
}

View File

@ -1,10 +1,12 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.vault
import app.cash.turbine.test
import com.bitwarden.core.data.manager.model.FlagKey
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.ui.platform.base.BaseViewModelTest
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
@ -29,6 +31,7 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
every { firstTimeStateFlow } returns mutableFirstTimeStateFlow
every { storeShowImportLoginsSettingsBadge(any()) } just runs
}
private val featureFlagManager = mockk<FeatureFlagManager>()
private val mutableSnackbarSharedFlow = bufferedMutableSharedFlow<BitwardenSnackbarData>()
private val snackbarRelayManager = mockk<SnackbarRelayManager> {
@ -58,17 +61,39 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `ImportItemsClick should emit send NavigateToImportVault`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(VaultSettingsAction.ImportItemsClick)
assertEquals(
VaultSettingsEvent.NavigateToImportVault,
awaitItem(),
)
fun `ImportItemsClick should emit NavigateToImportVault when CredentialExchangeProtocolImport is disabled`() =
runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
every {
featureFlagManager.getFeatureFlag(FlagKey.CredentialExchangeProtocolImport)
} returns false
viewModel.trySendAction(VaultSettingsAction.ImportItemsClick)
assertEquals(
VaultSettingsEvent.NavigateToImportVault,
awaitItem(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `ImportItemsClick should emit NavigateToImportItems when CredentialExchangeProtocolImport is enabled`() =
runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
every {
featureFlagManager.getFeatureFlag(FlagKey.CredentialExchangeProtocolImport)
} returns true
viewModel.trySendAction(VaultSettingsAction.ImportItemsClick)
assertEquals(
VaultSettingsEvent.NavigateToImportItems,
awaitItem(),
)
}
}
}
@Test
fun `shouldShowImportCard should update when first time state changes`() = runTest {
@ -131,6 +156,7 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
private fun createViewModel(): VaultSettingsViewModel = VaultSettingsViewModel(
firstTimeActionManager = firstTimeActionManager,
snackbarRelayManager = snackbarRelayManager,
featureFlagManager = featureFlagManager,
)
}

View File

@ -31,7 +31,6 @@ import org.junit.Test
class ImportItemsScreenTest : BitwardenComposeTest() {
private var onNavigateBackCalled = false
private var onNavigateToVaultCalled = false
private var onNavigateToImportFromComputerCalled = false
private val credentialExchangeImporter = mockk<CredentialExchangeImporter>()
@ -51,7 +50,6 @@ class ImportItemsScreenTest : BitwardenComposeTest() {
) {
ImportItemsScreen(
onNavigateBack = { onNavigateBackCalled = true },
onNavigateToVault = { onNavigateToVaultCalled = true },
onNavigateToImportFromComputer = { onNavigateToImportFromComputerCalled = true },
viewModel = viewModel,
)
@ -117,12 +115,6 @@ class ImportItemsScreenTest : BitwardenComposeTest() {
}
}
@Test
fun `NavigateToVault should call onNavigateToVault`() {
mockEventFlow.tryEmit(ImportItemsEvent.NavigateToVault)
assertTrue(onNavigateToVaultCalled)
}
@Test
fun `ShowRegisteredImportSources should call CredentialExchangeImporter`() = runTest {
val importCredentialsSelectionResult = ImportCredentialsSelectionResult.Success(
@ -205,4 +197,4 @@ class ImportItemsScreenTest : BitwardenComposeTest() {
}
}
private val DEFAULT_STATE: ImportItemsState = ImportItemsState(dialog = null)
private val DEFAULT_STATE: ImportItemsState = ImportItemsState()

View File

@ -11,17 +11,14 @@ import com.bitwarden.ui.platform.resource.BitwardenPlurals
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.asPluralsText
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.ImportCredentialsResult
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import io.mockk.awaits
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
@ -29,9 +26,6 @@ import org.junit.jupiter.api.Test
class ImportItemsViewModelTest : BaseViewModelTest() {
private val vaultRepository = mockk<VaultRepository>()
private val snackbarRelayManager = mockk<SnackbarRelayManager> {
every { sendSnackbarData(any(), any()) } just runs
}
@Test
fun `BackClick sends NavigateBack event`() = runTest {
@ -55,7 +49,7 @@ class ImportItemsViewModelTest : BaseViewModelTest() {
fun `DismissDialog updates state`() = runTest {
val viewModel = createViewModel()
viewModel.trySendAction(ImportItemsAction.DismissDialog)
assertEquals(ImportItemsState(dialog = null), viewModel.stateFlow.value)
assertEquals(ImportItemsState(), viewModel.stateFlow.value)
}
@Test
@ -158,14 +152,16 @@ class ImportItemsViewModelTest : BaseViewModelTest() {
}
@Test
fun `ReturnToVaultClick sends NavigateToVault event`() = runTest {
fun `SyncFailedTryAgainClick should update state and trigger sync`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(ImportItemsAction.ReturnToVaultClick)
assertEquals(
ImportItemsEvent.NavigateToVault,
awaitItem(),
)
coEvery {
vaultRepository.syncForResult()
} returns SyncVaultDataResult.Success(itemsAvailable = true)
viewModel.trySendAction(ImportItemsAction.SyncFailedTryAgainClick)
coVerify(exactly = 1) {
vaultRepository.syncForResult()
}
}
@ -193,7 +189,7 @@ class ImportItemsViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `Internal ImportCredentialsResultReceive with Success result should clear dialogs, show snackbar, and navigate to vault`() =
fun `Internal ImportCredentialsResultReceive with Success result should clear dialogs, and show snackbar`() =
runTest {
val viewModel = createViewModel()
@ -203,25 +199,21 @@ class ImportItemsViewModelTest : BaseViewModelTest() {
),
)
val expectedState = ImportItemsState(dialog = null)
val expectedState = ImportItemsState()
assertEquals(expectedState, viewModel.stateFlow.value)
coVerify(exactly = 1) {
snackbarRelayManager.sendSnackbarData(
data = BitwardenSnackbarData(
messageHeader = BitwardenString.import_successful.asText(),
message = BitwardenPlurals
.x_items_have_been_imported_to_your_vault
.asPluralsText(
quantity = 2,
args = arrayOf(2),
),
),
relay = SnackbarRelay.LOGINS_IMPORTED,
)
}
viewModel.eventFlow.test {
assertEquals(
ImportItemsEvent.NavigateToVault,
ImportItemsEvent.ShowBasicSnackbar(
BitwardenSnackbarData(
messageHeader = BitwardenString.import_successful.asText(),
message = BitwardenPlurals
.x_items_have_been_imported_to_your_vault
.asPluralsText(
quantity = 2,
args = arrayOf(2),
),
),
),
awaitItem(),
)
}
@ -249,7 +241,7 @@ class ImportItemsViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `Internal ImportCxfResultReceive and SyncFailed should clear dialogs, show snackbar, and navigate to vault`() =
fun `Internal ImportCxfResultReceive and SyncFailed should clear dialogs and show snackbar`() =
runTest {
val viewModel = createViewModel()
@ -261,18 +253,80 @@ class ImportItemsViewModelTest : BaseViewModelTest() {
),
)
val expectedState = ImportItemsState(dialog = null)
val expectedState = ImportItemsState()
assertEquals(expectedState, viewModel.stateFlow.value)
coVerify(exactly = 1) {
snackbarRelayManager.sendSnackbarData(
data = BitwardenSnackbarData(
messageHeader = BitwardenString.vault_sync_failed.asText(),
message = BitwardenString
.your_items_have_been_successfully_imported_but_could_not_sync_vault
.asText(),
actionLabel = BitwardenString.try_again.asText(),
viewModel.eventFlow.test {
assertEquals(
ImportItemsEvent.ShowSyncFailedSnackbar(
data = BitwardenSnackbarData(
messageHeader = BitwardenString.vault_sync_failed.asText(),
message = BitwardenString
.your_items_have_been_successfully_imported_but_could_not_sync_vault
.asText(),
actionLabel = BitwardenString.try_again.asText(),
),
),
relay = SnackbarRelay.VAULT_SYNC_FAILED,
awaitItem(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `Internal RetrySyncResultReceive with Success should clear dialogs and display success snackbar`() =
runTest {
val viewModel = createViewModel()
viewModel.trySendAction(
ImportItemsAction.Internal.RetrySyncResultReceive(
result = SyncVaultDataResult.Success(itemsAvailable = true),
),
)
assertEquals(
ImportItemsState(),
viewModel.stateFlow.value,
)
viewModel.eventFlow.test {
assertEquals(
ImportItemsEvent.ShowBasicSnackbar(
data = BitwardenSnackbarData(
message = BitwardenString.syncing_complete.asText(),
),
),
awaitItem(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `Internal RetrySyncResultReceive with Error should clear dialogs and show SyncFailed snackbar`() =
runTest {
val viewModel = createViewModel()
viewModel.trySendAction(
ImportItemsAction.Internal.RetrySyncResultReceive(
result = SyncVaultDataResult.Error(throwable = RuntimeException("Error")),
),
)
assertEquals(
ImportItemsState(),
viewModel.stateFlow.value,
)
viewModel.eventFlow.test {
assertEquals(
ImportItemsEvent.ShowSyncFailedSnackbar(
data = BitwardenSnackbarData(
messageHeader = BitwardenString.vault_sync_failed.asText(),
message = BitwardenString
.your_items_have_been_successfully_imported_but_could_not_sync_vault
.asText(),
actionLabel = BitwardenString.try_again.asText(),
),
),
awaitItem(),
)
}
}
@ -280,6 +334,5 @@ class ImportItemsViewModelTest : BaseViewModelTest() {
private fun createViewModel(): ImportItemsViewModel = ImportItemsViewModel(
vaultRepository = vaultRepository,
savedStateHandle = SavedStateHandle(),
snackbarRelayManager = snackbarRelayManager,
)
}