PM-11179 PM-11180 PM-11181 Add import logins screen and dialogs. (#4067)

This commit is contained in:
Dave Severns 2024-10-11 15:09:57 -04:00 committed by GitHub
parent 86db9bd3fa
commit bde47d7919
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 991 additions and 8 deletions

View file

@ -39,6 +39,8 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.navigateToVaultAddEdit
import com.x8bit.bitwarden.ui.vault.feature.addedit.vaultAddEditDestination
import com.x8bit.bitwarden.ui.vault.feature.attachments.attachmentDestination
import com.x8bit.bitwarden.ui.vault.feature.attachments.navigateToAttachment
import com.x8bit.bitwarden.ui.vault.feature.importlogins.importLoginsScreenDestination
import com.x8bit.bitwarden.ui.vault.feature.importlogins.navigateToImportLoginsScreen
import com.x8bit.bitwarden.ui.vault.feature.item.navigateToVaultItem
import com.x8bit.bitwarden.ui.vault.feature.item.vaultItemDestination
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.vaultItemListingDestinationAsRoot
@ -104,6 +106,7 @@ fun NavGraphBuilder.vaultUnlockedGraph(
},
onNavigateToSetupUnlockScreen = { navController.navigateToSetupUnlockScreen() },
onNavigateToSetupAutoFillScreen = { navController.navigateToSetupAutoFillScreen() },
onNavigateToImportLogins = { navController.navigateToImportLoginsScreen() },
)
deleteAccountDestination(
onNavigateBack = { navController.popBackStack() },
@ -216,5 +219,8 @@ fun NavGraphBuilder.vaultUnlockedGraph(
navController.popBackStack()
},
)
importLoginsScreenDestination(
onNavigateBack = { navController.popBackStack() },
)
}
}

View file

@ -38,6 +38,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination(
onNavigateToPasswordHistory: () -> Unit,
onNavigateToSetupUnlockScreen: () -> Unit,
onNavigateToSetupAutoFillScreen: () -> Unit,
onNavigateToImportLogins: () -> Unit,
) {
composableWithStayTransitions(
route = VAULT_UNLOCKED_NAV_BAR_ROUTE,
@ -57,6 +58,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination(
onNavigateToPasswordHistory = onNavigateToPasswordHistory,
onNavigateToSetupUnlockScreen = onNavigateToSetupUnlockScreen,
onNavigateToSetupAutoFillScreen = onNavigateToSetupAutoFillScreen,
onNavigateToImportLogins = onNavigateToImportLogins,
)
}
}

View file

@ -77,6 +77,7 @@ fun VaultUnlockedNavBarScreen(
onNavigateToPasswordHistory: () -> Unit,
onNavigateToSetupUnlockScreen: () -> Unit,
onNavigateToSetupAutoFillScreen: () -> Unit,
onNavigateToImportLogins: () -> Unit,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
@ -136,6 +137,7 @@ fun VaultUnlockedNavBarScreen(
},
onNavigateToSetupUnlockScreen = onNavigateToSetupUnlockScreen,
onNavigateToSetupAutoFillScreen = onNavigateToSetupAutoFillScreen,
onNavigateToImportLogins = onNavigateToImportLogins,
)
}
@ -165,6 +167,7 @@ private fun VaultUnlockedNavBarScaffold(
navigateToPasswordHistory: () -> Unit,
onNavigateToSetupUnlockScreen: () -> Unit,
onNavigateToSetupAutoFillScreen: () -> Unit,
onNavigateToImportLogins: () -> Unit,
) {
var shouldDimNavBar by remember { mutableStateOf(false) }
@ -224,6 +227,7 @@ private fun VaultUnlockedNavBarScaffold(
onDimBottomNavBarRequest = { shouldDim ->
shouldDimNavBar = shouldDim
},
onNavigateToImportLogins = onNavigateToImportLogins,
)
sendGraph(
navController = navController,

View file

@ -0,0 +1,30 @@
package com.x8bit.bitwarden.ui.vault.feature.importlogins
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
private const val IMPORT_LOGINS_ROUTE = "import-logins"
/**
* Helper function to navigate to the import logins screen.
*/
fun NavController.navigateToImportLoginsScreen(navOptions: NavOptions? = null) {
navigate(route = IMPORT_LOGINS_ROUTE, navOptions = navOptions)
}
/**
* Adds the import logins screen to the navigation graph.
*/
fun NavGraphBuilder.importLoginsScreenDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(
route = IMPORT_LOGINS_ROUTE,
) {
ImportLoginsScreen(
onNavigateBack = onNavigateBack,
)
}
}

View file

@ -0,0 +1,253 @@
package com.x8bit.bitwarden.ui.vault.feature.importlogins
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.appbar.NavigationIcon
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.vault.feature.importlogins.handlers.ImportLoginHandler
import com.x8bit.bitwarden.ui.vault.feature.importlogins.handlers.rememberImportLoginHandler
/**
* Top level component for the import logins screen.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ImportLoginsScreen(
onNavigateBack: () -> Unit,
viewModel: ImportLoginsViewModel = hiltViewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val handler = rememberImportLoginHandler(viewModel = viewModel)
EventsEffect(viewModel = viewModel) { event ->
when (event) {
ImportLoginsEvent.NavigateBack -> onNavigateBack()
}
}
ImportLoginsDialogContent(state = state, handler = handler)
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
BitwardenScaffold(
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
BitwardenTopAppBar(
title = stringResource(R.string.import_logins),
navigationIcon = NavigationIcon(
navigationIcon = rememberVectorPainter(R.drawable.ic_close),
onNavigationIconClick = handler.onCloseClick,
navigationIconContentDescription = stringResource(R.string.close),
),
scrollBehavior = scrollBehavior,
)
},
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues = innerPadding),
) {
ImportLoginsContent(
onGetStartedClick = handler.onGetStartedClick,
onImportLaterClick = handler.onImportLaterClick,
)
}
}
}
@Composable
private fun ImportLoginsDialogContent(
state: ImportLoginsState,
handler: ImportLoginHandler,
) {
val confirmButtonText = stringResource(R.string.confirm)
val dismissButtonText = stringResource(R.string.cancel)
when (val dialogState = state.dialogState) {
ImportLoginsState.DialogState.GetStarted -> {
BitwardenTwoButtonDialog(
title = dialogState.title(),
message = dialogState.message(),
onDismissRequest = handler.onDismissDialog,
confirmButtonText = confirmButtonText,
dismissButtonText = dismissButtonText,
onConfirmClick = handler.onConfirmGetStarted,
onDismissClick = handler.onDismissDialog,
)
}
ImportLoginsState.DialogState.ImportLater -> {
BitwardenTwoButtonDialog(
title = dialogState.title(),
message = dialogState.message(),
onDismissRequest = handler.onDismissDialog,
confirmButtonText = confirmButtonText,
dismissButtonText = dismissButtonText,
onConfirmClick = handler.onConfirmImportLater,
onDismissClick = handler.onDismissDialog,
)
}
null -> Unit
}
}
@Composable
private fun ImportLoginsContent(
onGetStartedClick: () -> Unit,
onImportLaterClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.verticalScroll(rememberScrollState())
.fillMaxSize()
.padding(top = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = rememberVectorPainter(R.drawable.img_import_logins),
contentDescription = null,
modifier = Modifier
.standardHorizontalMargin()
.size(124.dp),
)
Spacer(Modifier.height(24.dp))
Text(
text = stringResource(R.string.give_your_vault_a_head_start),
style = BitwardenTheme.typography.titleMedium,
color = BitwardenTheme.colorScheme.text.primary,
textAlign = TextAlign.Center,
modifier = Modifier.standardHorizontalMargin(),
)
Spacer(Modifier.height(12.dp))
Text(
text = stringResource(
R.string.from_your_computer_follow_these_instructions_to_export_saved_passwords,
),
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
textAlign = TextAlign.Center,
modifier = Modifier.standardHorizontalMargin(),
)
Spacer(Modifier.height(24.dp))
BitwardenFilledButton(
label = stringResource(R.string.get_started),
onClick = onGetStartedClick,
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(Modifier.height(12.dp))
BitwardenOutlinedButton(
label = stringResource(R.string.import_logins_later),
onClick = onImportLaterClick,
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(Modifier.navigationBarsPadding())
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ImportLoginsInitialContent_preview() {
BitwardenTheme {
Column(
modifier = Modifier.background(
BitwardenTheme.colorScheme.background.primary,
),
) {
ImportLoginsContent(
onGetStartedClick = {},
onImportLaterClick = {},
)
}
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ImportLoginsScreenDialog_preview(
@PreviewParameter(ImportLoginsDialogContentPreviewProvider::class) state: ImportLoginsState,
) {
BitwardenTheme {
Column(
modifier = Modifier.background(
BitwardenTheme.colorScheme.background.primary,
),
) {
ImportLoginsDialogContent(
state = state,
handler = ImportLoginHandler(
onDismissDialog = {},
onConfirmGetStarted = {},
onConfirmImportLater = {},
onCloseClick = {},
onGetStartedClick = {},
onImportLaterClick = {},
),
)
ImportLoginsContent(
onGetStartedClick = {},
onImportLaterClick = {},
)
}
}
}
@OmitFromCoverage
private class ImportLoginsDialogContentPreviewProvider :
PreviewParameterProvider<ImportLoginsState> {
override val values: Sequence<ImportLoginsState>
get() = sequenceOf(
ImportLoginsState(
dialogState = ImportLoginsState.DialogState.GetStarted,
),
ImportLoginsState(
dialogState = ImportLoginsState.DialogState.ImportLater,
),
)
}

View file

@ -0,0 +1,146 @@
package com.x8bit.bitwarden.ui.vault.feature.importlogins
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.update
import javax.inject.Inject
/**
* View model for the [ImportLoginsScreen].
*/
@HiltViewModel
class ImportLoginsViewModel @Inject constructor() :
BaseViewModel<ImportLoginsState, ImportLoginsEvent, ImportLoginsAction>(
initialState = ImportLoginsState(
null,
),
) {
override fun handleAction(action: ImportLoginsAction) {
when (action) {
ImportLoginsAction.ConfirmGetStarted -> handleConfirmGetStarted()
ImportLoginsAction.ConfirmImportLater -> handleConfirmImportLater()
ImportLoginsAction.DismissDialog -> handleDismissDialog()
ImportLoginsAction.GetStartedClick -> handleGetStartedClick()
ImportLoginsAction.ImportLaterClick -> handleImportLaterClick()
ImportLoginsAction.CloseClick -> handleCloseClick()
}
}
private fun handleCloseClick() {
sendEvent(ImportLoginsEvent.NavigateBack)
}
private fun handleImportLaterClick() {
updateDialogState(ImportLoginsState.DialogState.ImportLater)
}
private fun handleGetStartedClick() {
updateDialogState(ImportLoginsState.DialogState.GetStarted)
}
private fun handleDismissDialog() {
dismissDialog()
}
private fun handleConfirmImportLater() {
dismissDialog()
sendEvent(ImportLoginsEvent.NavigateBack)
}
private fun handleConfirmGetStarted() {
dismissDialog()
// TODO - PM-11182: Move to first step in instructions.
}
private fun dismissDialog() {
updateDialogState(null)
}
private fun updateDialogState(dialogState: ImportLoginsState.DialogState?) {
mutableStateFlow.update {
it.copy(dialogState = dialogState)
}
}
}
/**
* Model state for the [ImportLoginsViewModel].
*/
data class ImportLoginsState(
val dialogState: DialogState?,
) {
/**
* Dialog states for the [ImportLoginsViewModel].
*/
sealed class DialogState {
abstract val message: Text
abstract val title: Text
/**
* Import logins later dialog state.
*/
data object ImportLater : DialogState() {
override val message: Text =
R.string.you_can_return_to_complete_this_step_anytime_from_settings.asText()
override val title: Text = R.string.import_logins_later_dialog_title.asText()
}
/**
* Get started dialog state.
*/
data object GetStarted : DialogState() {
override val message: Text =
R.string.the_following_instructions_will_guide_you_through_importing_logins.asText()
override val title: Text = R.string.do_you_have_a_computer_available.asText()
}
}
}
/**
* Model events that can be sent from the [ImportLoginsViewModel]
*/
sealed class ImportLoginsEvent {
/**
* Navigate back to the previous screen.
*/
data object NavigateBack : ImportLoginsEvent()
}
/**
* Model actions that can be handled by the [ImportLoginsViewModel].
*/
sealed class ImportLoginsAction {
/**
* User has clicked the "Get Started" button.
*/
data object GetStartedClick : ImportLoginsAction()
/**
* User has clicked the "Import Later" button.
*/
data object ImportLaterClick : ImportLoginsAction()
/**
* User has clicked the "Close" button on the dialog or outside the dialog.
*/
data object DismissDialog : ImportLoginsAction()
/**
* User has confirmed the "Import Later" dialog.
*/
data object ConfirmImportLater : ImportLoginsAction()
/**
* User has confirmed the "Get Started" dialog.
*/
data object ConfirmGetStarted : ImportLoginsAction()
/**
* User has clicked the "Close" icon button.
*/
data object CloseClick : ImportLoginsAction()
}

View file

@ -0,0 +1,44 @@
package com.x8bit.bitwarden.ui.vault.feature.importlogins.handlers
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.x8bit.bitwarden.ui.vault.feature.importlogins.ImportLoginsAction
import com.x8bit.bitwarden.ui.vault.feature.importlogins.ImportLoginsViewModel
/**
* Action handlers for the [ImportLoginsScreen].
*/
data class ImportLoginHandler(
val onGetStartedClick: () -> Unit,
val onImportLaterClick: () -> Unit,
val onDismissDialog: () -> Unit,
val onConfirmGetStarted: () -> Unit,
val onConfirmImportLater: () -> Unit,
val onCloseClick: () -> Unit,
) {
@Suppress("UndocumentedPublicClass")
companion object {
/**
* Creates an instance of [ImportLoginHandler] using the provided [ImportLoginsViewModel].
*/
fun create(viewModel: ImportLoginsViewModel) = ImportLoginHandler(
onGetStartedClick = { viewModel.trySendAction(ImportLoginsAction.GetStartedClick) },
onImportLaterClick = { viewModel.trySendAction(ImportLoginsAction.ImportLaterClick) },
onDismissDialog = { viewModel.trySendAction(ImportLoginsAction.DismissDialog) },
onConfirmGetStarted = { viewModel.trySendAction(ImportLoginsAction.ConfirmGetStarted) },
onConfirmImportLater = {
viewModel.trySendAction(ImportLoginsAction.ConfirmImportLater)
},
onCloseClick = { viewModel.trySendAction(ImportLoginsAction.CloseClick) },
)
}
}
/**
* Helper function to remember a [ImportLoginHandler] instance in a [Composable] scope.
*/
@Composable
fun rememberImportLoginHandler(viewModel: ImportLoginsViewModel): ImportLoginHandler =
remember(viewModel) {
ImportLoginHandler.create(viewModel = viewModel)
}

View file

@ -24,6 +24,7 @@ fun NavGraphBuilder.vaultGraph(
onNavigateToVaultEditItemScreen: (vaultItemId: String) -> Unit,
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
onNavigateToImportLogins: () -> Unit,
) {
navigation(
route = VAULT_GRAPH_ROUTE,
@ -41,6 +42,7 @@ fun NavGraphBuilder.vaultGraph(
},
onNavigateToSearchVault = onNavigateToSearchVault,
onDimBottomNavBarRequest = onDimBottomNavBarRequest,
onNavigateToImportLogins = onNavigateToImportLogins,
)
vaultItemListingDestination(
onNavigateBack = { navController.popBackStack() },

View file

@ -21,6 +21,7 @@ fun NavGraphBuilder.vaultDestination(
onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit,
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
onNavigateToImportLogins: () -> Unit,
) {
composableWithRootPushTransitions(
route = VAULT_ROUTE,
@ -33,6 +34,7 @@ fun NavGraphBuilder.vaultDestination(
onNavigateToVerificationCodeScreen = onNavigateToVerificationCodeScreen,
onNavigateToSearchVault = onNavigateToSearchVault,
onDimBottomNavBarRequest = onDimBottomNavBarRequest,
onNavigateToImportLogins = onNavigateToImportLogins,
)
}
}

View file

@ -83,6 +83,7 @@ fun VaultScreen(
onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit,
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
onNavigateToImportLogins: () -> Unit,
exitManager: ExitManager = LocalExitManager.current,
intentManager: IntentManager = LocalIntentManager.current,
permissionsManager: PermissionsManager = LocalPermissionsManager.current,
@ -122,6 +123,8 @@ fun VaultScreen(
.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT)
.show()
}
VaultEvent.NavigateToImportLogins -> onNavigateToImportLogins()
}
}
val vaultHandlers = remember(viewModel) { VaultHandlers.create(viewModel) }

View file

@ -181,7 +181,7 @@ class VaultViewModel @Inject constructor(
private fun handleImportActionCardClick() {
dismissImportLoginCard()
// TODO: PM-11179 - navigate to import logins screen
sendEvent(VaultEvent.NavigateToImportLogins)
}
private fun handleDismissImportActionCard() {
@ -1004,6 +1004,11 @@ sealed class VaultEvent {
*/
data object NavigateOutOfApp : VaultEvent()
/**
* Navigate to the import logins screen.
*/
data object NavigateToImportLogins : VaultEvent()
/**
* Show a toast with the given [message].
*/

View file

@ -0,0 +1,73 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="201dp"
android:viewportWidth="200"
android:viewportHeight="201">
<group>
<clip-path
android:pathData="M0,0.67h200v200h-200z"/>
<path
android:pathData="M0,38.17C0,31.26 5.6,25.67 12.5,25.67H154.17C161.07,25.67 166.67,31.26 166.67,38.17V138.17C166.67,145.07 161.07,150.67 154.17,150.67H12.5C5.6,150.67 0,145.07 0,138.17V38.17Z"
android:fillColor="#79A1E9"/>
<path
android:pathData="M154.17,29.83H12.5C7.9,29.83 4.17,33.56 4.17,38.17V138.17C4.17,142.77 7.9,146.5 12.5,146.5H154.17C158.77,146.5 162.5,142.77 162.5,138.17V38.17C162.5,33.56 158.77,29.83 154.17,29.83ZM12.5,25.67C5.6,25.67 0,31.26 0,38.17V138.17C0,145.07 5.6,150.67 12.5,150.67H154.17C161.07,150.67 166.67,145.07 166.67,138.17V38.17C166.67,31.26 161.07,25.67 154.17,25.67H12.5Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M164.58,50.67L2.08,50.67L2.08,46.5L164.58,46.5L164.58,50.67Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M158.33,38.17C158.33,40.47 156.47,42.33 154.17,42.33C151.87,42.33 150,40.47 150,38.17C150,35.87 151.87,34 154.17,34C156.47,34 158.33,35.87 158.33,38.17Z"
android:fillColor="#175DDC"/>
<path
android:pathData="M145.83,38.17C145.83,40.47 143.97,42.33 141.67,42.33C139.37,42.33 137.5,40.47 137.5,38.17C137.5,35.87 139.37,34 141.67,34C143.97,34 145.83,35.87 145.83,38.17Z"
android:fillColor="#175DDC"/>
<path
android:pathData="M133.33,38.17C133.33,40.47 131.47,42.33 129.17,42.33C126.86,42.33 125,40.47 125,38.17C125,35.87 126.86,34 129.17,34C131.47,34 133.33,35.87 133.33,38.17Z"
android:fillColor="#175DDC"/>
<path
android:pathData="M125,67.33C125,60.43 130.6,54.83 137.5,54.83H187.5C194.4,54.83 200,60.43 200,67.33V175.67C200,182.57 194.4,188.17 187.5,188.17H137.5C130.6,188.17 125,182.57 125,175.67V67.33Z"
android:fillColor="#AAC3EF"/>
<path
android:pathData="M187.5,59H137.5C132.9,59 129.17,62.73 129.17,67.33V175.67C129.17,180.27 132.9,184 137.5,184H187.5C192.1,184 195.83,180.27 195.83,175.67V67.33C195.83,62.73 192.1,59 187.5,59ZM137.5,54.83C130.6,54.83 125,60.43 125,67.33V175.67C125,182.57 130.6,188.17 137.5,188.17H187.5C194.4,188.17 200,182.57 200,175.67V67.33C200,60.43 194.4,54.83 187.5,54.83H137.5Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M152.08,67.33C152.08,66.18 153.02,65.25 154.17,65.25H170.83C171.99,65.25 172.92,66.18 172.92,67.33C172.92,68.48 171.99,69.42 170.83,69.42H154.17C153.02,69.42 152.08,68.48 152.08,67.33Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M141.67,173.58C141.67,170.13 144.46,167.33 147.92,167.33H177.08C180.53,167.33 183.33,170.13 183.33,173.58V194.42C183.33,197.87 180.53,200.67 177.08,200.67H147.92C144.46,200.67 141.67,197.87 141.67,194.42V173.58Z"
android:fillColor="#FFBF00"/>
<path
android:pathData="M177.08,171.5H147.92C146.76,171.5 145.83,172.43 145.83,173.58V194.42C145.83,195.57 146.76,196.5 147.92,196.5H177.08C178.23,196.5 179.17,195.57 179.17,194.42V173.58C179.17,172.43 178.23,171.5 177.08,171.5ZM147.92,167.33C144.46,167.33 141.67,170.13 141.67,173.58V194.42C141.67,197.87 144.46,200.67 147.92,200.67H177.08C180.53,200.67 183.33,197.87 183.33,194.42V173.58C183.33,170.13 180.53,167.33 177.08,167.33H147.92Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M160.42,179.83C160.42,178.68 161.35,177.75 162.5,177.75C163.65,177.75 164.58,178.68 164.58,179.83V188.17C164.58,189.32 163.65,190.25 162.5,190.25C161.35,190.25 160.42,189.32 160.42,188.17V179.83Z"
android:fillColor="#175DDC"/>
<path
android:pathData="M152.08,165.25C152.08,159.5 156.75,154.83 162.5,154.83C168.25,154.83 172.92,159.5 172.92,165.25V167.33H168.75V165.25C168.75,161.8 165.95,159 162.5,159C159.05,159 156.25,161.8 156.25,165.25V167.33H152.08V165.25Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M83.33,111.08C84.48,111.08 85.42,112.02 85.42,113.17V125.67C85.42,129.12 88.21,131.92 91.67,131.92H159.55L150.61,122.97C149.8,122.16 149.8,120.84 150.61,120.03C151.42,119.21 152.74,119.21 153.56,120.03L166.06,132.53C166.87,133.34 166.87,134.66 166.06,135.47L153.56,147.97C152.74,148.79 151.42,148.79 150.61,147.97C149.8,147.16 149.8,145.84 150.61,145.03L159.55,136.08H91.67C85.91,136.08 81.25,131.42 81.25,125.67V113.17C81.25,112.02 82.18,111.08 83.33,111.08Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M54.17,81.92C54.17,78.46 56.96,75.67 60.42,75.67H106.25C109.7,75.67 112.5,78.46 112.5,81.92V115.25C112.5,118.7 109.7,121.5 106.25,121.5H60.42C56.96,121.5 54.17,118.7 54.17,115.25V81.92Z"
android:fillColor="#F3F6F9"/>
<path
android:pathData="M106.25,79.83H60.42C59.27,79.83 58.33,80.77 58.33,81.92V115.25C58.33,116.4 59.27,117.33 60.42,117.33H106.25C107.4,117.33 108.33,116.4 108.33,115.25V81.92C108.33,80.77 107.4,79.83 106.25,79.83ZM60.42,75.67C56.96,75.67 54.17,78.46 54.17,81.92V115.25C54.17,118.7 56.96,121.5 60.42,121.5H106.25C109.7,121.5 112.5,118.7 112.5,115.25V81.92C112.5,78.46 109.7,75.67 106.25,75.67H60.42Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
<path
android:pathData="M81.25,92.33C81.25,91.18 82.18,90.25 83.33,90.25C84.48,90.25 85.42,91.18 85.42,92.33V104.83C85.42,105.98 84.48,106.92 83.33,106.92C82.18,106.92 81.25,105.98 81.25,104.83V92.33Z"
android:fillColor="#175DDC"/>
<path
android:pathData="M66.67,71.5C66.67,62.29 74.13,54.83 83.33,54.83C92.54,54.83 100,62.29 100,71.5V75.67H95.83V71.5C95.83,64.6 90.24,59 83.33,59C76.43,59 70.83,64.6 70.83,71.5V75.67H66.67V71.5Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
</group>
</vector>

View file

@ -0,0 +1,73 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="201dp"
android:viewportWidth="200"
android:viewportHeight="201">
<group>
<clip-path
android:pathData="M0,0.67h200v200h-200z"/>
<path
android:pathData="M0,38.17C0,31.26 5.6,25.67 12.5,25.67H154.17C161.07,25.67 166.67,31.26 166.67,38.17V138.17C166.67,145.07 161.07,150.67 154.17,150.67H12.5C5.6,150.67 0,145.07 0,138.17V38.17Z"
android:fillColor="#AAC3EF"/>
<path
android:pathData="M154.17,29.83H12.5C7.9,29.83 4.17,33.56 4.17,38.17V138.17C4.17,142.77 7.9,146.5 12.5,146.5H154.17C158.77,146.5 162.5,142.77 162.5,138.17V38.17C162.5,33.56 158.77,29.83 154.17,29.83ZM12.5,25.67C5.6,25.67 0,31.26 0,38.17V138.17C0,145.07 5.6,150.67 12.5,150.67H154.17C161.07,150.67 166.67,145.07 166.67,138.17V38.17C166.67,31.26 161.07,25.67 154.17,25.67H12.5Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M164.58,50.67L2.08,50.67L2.08,46.5L164.58,46.5L164.58,50.67Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M158.33,38.17C158.33,40.47 156.47,42.33 154.17,42.33C151.87,42.33 150,40.47 150,38.17C150,35.87 151.87,34 154.17,34C156.47,34 158.33,35.87 158.33,38.17Z"
android:fillColor="#020F66"/>
<path
android:pathData="M145.83,38.17C145.83,40.47 143.97,42.33 141.67,42.33C139.37,42.33 137.5,40.47 137.5,38.17C137.5,35.87 139.37,34 141.67,34C143.97,34 145.83,35.87 145.83,38.17Z"
android:fillColor="#020F66"/>
<path
android:pathData="M133.33,38.17C133.33,40.47 131.47,42.33 129.17,42.33C126.86,42.33 125,40.47 125,38.17C125,35.87 126.86,34 129.17,34C131.47,34 133.33,35.87 133.33,38.17Z"
android:fillColor="#020F66"/>
<path
android:pathData="M125,67.33C125,60.43 130.6,54.83 137.5,54.83H187.5C194.4,54.83 200,60.43 200,67.33V175.67C200,182.57 194.4,188.17 187.5,188.17H137.5C130.6,188.17 125,182.57 125,175.67V67.33Z"
android:fillColor="#DBE5F6"/>
<path
android:pathData="M187.5,59H137.5C132.9,59 129.17,62.73 129.17,67.33V175.67C129.17,180.27 132.9,184 137.5,184H187.5C192.1,184 195.83,180.27 195.83,175.67V67.33C195.83,62.73 192.1,59 187.5,59ZM137.5,54.83C130.6,54.83 125,60.43 125,67.33V175.67C125,182.57 130.6,188.17 137.5,188.17H187.5C194.4,188.17 200,182.57 200,175.67V67.33C200,60.43 194.4,54.83 187.5,54.83H137.5Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M152.08,67.33C152.08,66.18 153.02,65.25 154.17,65.25H170.83C171.99,65.25 172.92,66.18 172.92,67.33C172.92,68.48 171.99,69.42 170.83,69.42H154.17C153.02,69.42 152.08,68.48 152.08,67.33Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M141.67,173.58C141.67,170.13 144.46,167.33 147.92,167.33H177.08C180.53,167.33 183.33,170.13 183.33,173.58V194.42C183.33,197.87 180.53,200.67 177.08,200.67H147.92C144.46,200.67 141.67,197.87 141.67,194.42V173.58Z"
android:fillColor="#FFBF00"/>
<path
android:pathData="M177.08,171.5H147.92C146.76,171.5 145.83,172.43 145.83,173.58V194.42C145.83,195.57 146.76,196.5 147.92,196.5H177.08C178.23,196.5 179.17,195.57 179.17,194.42V173.58C179.17,172.43 178.23,171.5 177.08,171.5ZM147.92,167.33C144.46,167.33 141.67,170.13 141.67,173.58V194.42C141.67,197.87 144.46,200.67 147.92,200.67H177.08C180.53,200.67 183.33,197.87 183.33,194.42V173.58C183.33,170.13 180.53,167.33 177.08,167.33H147.92Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M160.42,179.83C160.42,178.68 161.35,177.75 162.5,177.75C163.65,177.75 164.58,178.68 164.58,179.83V188.17C164.58,189.32 163.65,190.25 162.5,190.25C161.35,190.25 160.42,189.32 160.42,188.17V179.83Z"
android:fillColor="#020F66"/>
<path
android:pathData="M152.08,165.25C152.08,159.5 156.75,154.83 162.5,154.83C168.25,154.83 172.92,159.5 172.92,165.25V167.33H168.75V165.25C168.75,161.8 165.95,159 162.5,159C159.05,159 156.25,161.8 156.25,165.25V167.33H152.08V165.25Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M83.33,111.08C84.48,111.08 85.42,112.02 85.42,113.17V125.67C85.42,129.12 88.21,131.92 91.67,131.92H159.55L150.61,122.97C149.8,122.16 149.8,120.84 150.61,120.03C151.42,119.21 152.74,119.21 153.56,120.03L166.06,132.53C166.87,133.34 166.87,134.66 166.06,135.47L153.56,147.97C152.74,148.79 151.42,148.79 150.61,147.97C149.8,147.16 149.8,145.84 150.61,145.03L159.55,136.08H91.67C85.91,136.08 81.25,131.42 81.25,125.67V113.17C81.25,112.02 82.18,111.08 83.33,111.08Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M54.17,81.92C54.17,78.46 56.96,75.67 60.42,75.67H106.25C109.7,75.67 112.5,78.46 112.5,81.92V115.25C112.5,118.7 109.7,121.5 106.25,121.5H60.42C56.96,121.5 54.17,118.7 54.17,115.25V81.92Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M106.25,79.83H60.42C59.27,79.83 58.33,80.77 58.33,81.92V115.25C58.33,116.4 59.27,117.33 60.42,117.33H106.25C107.4,117.33 108.33,116.4 108.33,115.25V81.92C108.33,80.77 107.4,79.83 106.25,79.83ZM60.42,75.67C56.96,75.67 54.17,78.46 54.17,81.92V115.25C54.17,118.7 56.96,121.5 60.42,121.5H106.25C109.7,121.5 112.5,118.7 112.5,115.25V81.92C112.5,78.46 109.7,75.67 106.25,75.67H60.42Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
<path
android:pathData="M81.25,92.33C81.25,91.18 82.18,90.25 83.33,90.25C84.48,90.25 85.42,91.18 85.42,92.33V104.83C85.42,105.98 84.48,106.92 83.33,106.92C82.18,106.92 81.25,105.98 81.25,104.83V92.33Z"
android:fillColor="#020F66"/>
<path
android:pathData="M66.67,71.5C66.67,62.29 74.13,54.83 83.33,54.83C92.54,54.83 100,62.29 100,71.5V75.67H95.83V71.5C95.83,64.6 90.24,59 83.33,59C76.43,59 70.83,64.6 70.83,71.5V75.67H66.67V71.5Z"
android:fillColor="#020F66"
android:fillType="evenOdd"/>
</group>
</vector>

View file

@ -1020,4 +1020,12 @@ Do you want to switch to this account?</string>
<string name="send_sensitive_information_safely">Send sensitive information, safely</string>
<string name="import_saved_logins">Import saved logins</string>
<string name="use_a_computer_to_import_logins">Use a computer to import logins from an existing password manager</string>
<string name="you_can_return_to_complete_this_step_anytime_from_settings">You can return to complete this step anytime in Vault Settings.</string>
<string name="import_logins_later">Import logins later</string>
<string name="import_logins_later_dialog_title">Import logins later?</string>
<string name="do_you_have_a_computer_available">Do you have a computer available?</string>
<string name="the_following_instructions_will_guide_you_through_importing_logins">The following instructions will guide you through importing logins from your desktop or laptop computer</string>
<string name="import_logins">Import Logins</string>
<string name="from_your_computer_follow_these_instructions_to_export_saved_passwords">From your computer, follow these instructions to export saved passwords from your browser or other password manager. Then, safely import them to Bitwarden.</string>
<string name="give_your_vault_a_head_start">Give your vault a head start</string>
</resources>

View file

@ -55,6 +55,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
onNavigateToSearchSend = {},
onNavigateToSetupAutoFillScreen = {},
onNavigateToSetupUnlockScreen = {},
onNavigateToImportLogins = {},
)
}
}

View file

@ -0,0 +1,178 @@
package com.x8bit.bitwarden.ui.vault.feature.importlogins
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.isDialog
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.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
class ImportLoginsScreenTest : BaseComposeTest() {
private var navigateBackCalled = false
private val mutableImportLoginsStateFlow = MutableStateFlow(DEFAULT_STATE)
private val mutableImportLoginsEventFlow = bufferedMutableSharedFlow<ImportLoginsEvent>()
private val viewModel = mockk<ImportLoginsViewModel> {
every { eventFlow } returns mutableImportLoginsEventFlow
every { stateFlow } returns mutableImportLoginsStateFlow
every { trySendAction(any()) } just runs
}
@Before
fun setup() {
composeTestRule.setContent {
ImportLoginsScreen(
onNavigateBack = { navigateBackCalled = true },
viewModel = viewModel,
)
}
}
@Test
fun `navigate back when event is NavigateBack`() {
mutableImportLoginsEventFlow.tryEmit(ImportLoginsEvent.NavigateBack)
assertTrue(navigateBackCalled)
}
@Test
fun `when close icon clicked, CloseClick action is sent`() {
composeTestRule
.onNodeWithContentDescription("Close")
.performClick()
verifyActionSent(ImportLoginsAction.CloseClick)
}
@Test
fun `when get started clicked, GetStartedClick action is sent`() {
composeTestRule
.onNodeWithText("Get started")
.performClick()
verifyActionSent(ImportLoginsAction.GetStartedClick)
}
@Test
fun `when import later clicked, ImportLaterClick action is sent`() {
composeTestRule
.onNodeWithText("Import logins later")
.performClick()
verifyActionSent(ImportLoginsAction.ImportLaterClick)
}
@Test
fun `dialog content is shown when state updates and is hidden when null`() {
mutableImportLoginsStateFlow.tryEmit(
DEFAULT_STATE.copy(
dialogState = ImportLoginsState.DialogState.GetStarted,
),
)
composeTestRule
.onNode(isDialog())
.assertIsDisplayed()
mutableImportLoginsStateFlow.tryEmit(
DEFAULT_STATE.copy(
dialogState = null,
),
)
composeTestRule
.assertNoDialogExists()
mutableImportLoginsStateFlow.tryEmit(
DEFAULT_STATE.copy(
dialogState = ImportLoginsState.DialogState.ImportLater,
),
)
composeTestRule
.onNode(isDialog())
.assertIsDisplayed()
}
@Suppress("MaxLineLength")
@Test
fun `when dialog state is GetStarted, GetStarted dialog is shown and sends correct actions when clicked`() {
mutableImportLoginsStateFlow.tryEmit(
DEFAULT_STATE.copy(
dialogState = ImportLoginsState.DialogState.GetStarted,
),
)
composeTestRule
.onNode(isDialog())
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("Do you have a computer available?", useUnmergedTree = true)
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("Confirm")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
.performClick()
verifyActionSent(ImportLoginsAction.ConfirmGetStarted)
composeTestRule
.onAllNodesWithText("Cancel")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
.performClick()
verifyActionSent(ImportLoginsAction.DismissDialog)
}
@Suppress("MaxLineLength")
@Test
fun `when dialog state is ImportLater, ImportLater dialog is shown and sends correct actions when clicked`() {
mutableImportLoginsStateFlow.tryEmit(
DEFAULT_STATE.copy(
dialogState = ImportLoginsState.DialogState.ImportLater,
),
)
composeTestRule
.onNode(isDialog())
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("Import logins later?", useUnmergedTree = true)
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("Confirm")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
.performClick()
verifyActionSent(ImportLoginsAction.ConfirmImportLater)
composeTestRule
.onAllNodesWithText("Cancel")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
.performClick()
verifyActionSent(ImportLoginsAction.DismissDialog)
}
private fun verifyActionSent(action: ImportLoginsAction) {
verify { viewModel.trySendAction(action) }
}
}
private val DEFAULT_STATE = ImportLoginsState(dialogState = null)

View file

@ -0,0 +1,141 @@
package com.x8bit.bitwarden.ui.vault.feature.importlogins
import app.cash.turbine.test
import app.cash.turbine.turbineScope
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class ImportLoginsViewModelTest : BaseViewModelTest() {
@Test
fun `initial state is correct`() {
val viewModel = createViewModel()
assertEquals(
DEFAULT_STATE,
viewModel.stateFlow.value,
)
}
@Test
fun `GetStartedClick sets dialog state to GetStarted`() {
val viewModel = createViewModel()
viewModel.trySendAction(ImportLoginsAction.GetStartedClick)
assertEquals(
ImportLoginsState(
dialogState = ImportLoginsState.DialogState.GetStarted,
),
viewModel.stateFlow.value,
)
}
@Test
fun `ImportLaterClick sets dialog state to ImportLater`() {
val viewModel = createViewModel()
viewModel.trySendAction(ImportLoginsAction.ImportLaterClick)
assertEquals(
ImportLoginsState(
dialogState = ImportLoginsState.DialogState.ImportLater,
),
viewModel.stateFlow.value,
)
}
@Test
fun `DismissDialog sets dialog state to null`() = runTest {
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(
DEFAULT_STATE,
awaitItem(),
)
viewModel.trySendAction(ImportLoginsAction.GetStartedClick)
assertEquals(
ImportLoginsState(
dialogState = ImportLoginsState.DialogState.GetStarted,
),
awaitItem(),
)
viewModel.trySendAction(ImportLoginsAction.DismissDialog)
assertEquals(
ImportLoginsState(
dialogState = null,
),
awaitItem(),
)
}
}
@Test
fun `ConfirmImportLater sets dialog state to null and sends NavigateBack event`() = runTest {
val viewModel = createViewModel()
turbineScope {
val stateFlow = viewModel.stateFlow.testIn(backgroundScope)
val eventFlow = viewModel.eventFlow.testIn(backgroundScope)
// Initial state
assertEquals(DEFAULT_STATE, stateFlow.awaitItem())
// Set the dialog state to ImportLater
viewModel.trySendAction(ImportLoginsAction.ImportLaterClick)
assertEquals(
ImportLoginsState(
dialogState = ImportLoginsState.DialogState.ImportLater,
),
stateFlow.awaitItem(),
)
viewModel.trySendAction(ImportLoginsAction.ConfirmImportLater)
assertEquals(
ImportLoginsState(
dialogState = null,
),
stateFlow.awaitItem(),
)
assertEquals(
ImportLoginsEvent.NavigateBack,
eventFlow.awaitItem(),
)
}
}
@Test
fun `ConfirmGetStarted sets dialog state to null`() = runTest {
val viewModel = createViewModel()
viewModel.stateFlow.test {
// Initial state
assertEquals(DEFAULT_STATE, awaitItem())
// Set the dialog state to GetStarted
viewModel.trySendAction(ImportLoginsAction.GetStartedClick)
assertEquals(
ImportLoginsState(
dialogState = ImportLoginsState.DialogState.GetStarted,
),
awaitItem(),
)
viewModel.trySendAction(ImportLoginsAction.ConfirmGetStarted)
assertEquals(
ImportLoginsState(
dialogState = null,
),
awaitItem(),
)
}
}
@Test
fun `CloseClick sends NavigateBack event`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(ImportLoginsAction.CloseClick)
assertEquals(
ImportLoginsEvent.NavigateBack,
awaitItem(),
)
}
}
private fun createViewModel(): ImportLoginsViewModel = ImportLoginsViewModel()
}
private val DEFAULT_STATE = ImportLoginsState(dialogState = null)

View file

@ -60,7 +60,7 @@ import org.junit.Test
@Suppress("LargeClass")
class VaultScreenTest : BaseComposeTest() {
private var onNavigateToImportLoginsCalled = false
private var onNavigateToVaultAddItemScreenCalled = false
private var onNavigateToVaultItemId: String? = null
private var onNavigateToVaultEditItemId: String? = null
@ -91,6 +91,7 @@ class VaultScreenTest : BaseComposeTest() {
onDimBottomNavBarRequest = { onDimBottomNavBarRequestCalled = true },
onNavigateToVerificationCodeScreen = { onNavigateToVerificationCodeScreen = true },
onNavigateToSearchVault = { onNavigateToSearchScreen = true },
onNavigateToImportLogins = { onNavigateToImportLoginsCalled = true },
exitManager = exitManager,
intentManager = intentManager,
permissionsManager = permissionsManager,
@ -1194,6 +1195,12 @@ class VaultScreenTest : BaseComposeTest() {
.performClick()
verify { viewModel.trySendAction(VaultAction.DismissImportActionCard) }
}
@Test
fun `when NavigateToImportLogins is sent, it should call onNavigateToImportLogins`() {
mutableEventFlow.tryEmit(VaultEvent.NavigateToImportLogins)
assertTrue(onNavigateToImportLoginsCalled)
}
}
private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(

View file

@ -1576,14 +1576,19 @@ class VaultViewModelTest : BaseViewModelTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `when ImportActionCardClick is sent, repository called to set value to false`() {
val viewModel = createViewModel()
viewModel.trySendAction(VaultAction.ImportActionCardClick)
verify(exactly = 1) {
authRepository.setShowImportLogins(false)
fun `when ImportActionCardClick is sent, repository called to set value to false and NavigateToImportLogins event is sent`() =
runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(VaultAction.ImportActionCardClick)
assertEquals(VaultEvent.NavigateToImportLogins, awaitItem())
}
verify(exactly = 1) {
authRepository.setShowImportLogins(false)
}
}
}
@Suppress("MaxLineLength")
@Test