From c94b303abcc50c83cd54069305eb3865a9a883eb Mon Sep 17 00:00:00 2001 From: Caleb Derosier <125901828+caleb-livefront@users.noreply.github.com> Date: Fri, 29 Dec 2023 14:11:41 -0600 Subject: [PATCH] BIT-457: Add Vault Settings and Folders screen UI (#457) --- .../feature/settings/SettingsNavigation.kt | 8 +- .../settings/folders/FoldersNavigation.kt | 30 ++++ .../feature/settings/folders/FoldersScreen.kt | 108 +++++++++++ .../settings/folders/FoldersViewModel.kt | 60 +++++++ .../settings/vault/VaultSettingsNavigation.kt | 6 +- .../settings/vault/VaultSettingsScreen.kt | 45 ++++- .../settings/vault/VaultSettingsViewModel.kt | 50 +++++- .../vaultunlocked/VaultUnlockedNavigation.kt | 4 + .../VaultUnlockedNavBarNavigation.kt | 2 + .../VaultUnlockedNavBarScreen.kt | 5 + .../settings/folders/FoldersScreenTest.kt | 56 ++++++ .../settings/folders/FoldersViewModelTest.kt | 33 ++++ .../settings/vault/VaultSettingsScreenTest.kt | 90 ++++++++-- .../vault/VaultSettingsViewModelTest.kt | 30 +++- .../VaultUnlockedNavBarScreenTest.kt | 167 ++++-------------- 15 files changed, 530 insertions(+), 164 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersNavigation.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersScreen.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersViewModel.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersScreenTest.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersViewModelTest.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt index bb22a564c..32f4f38de 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt @@ -27,6 +27,7 @@ private const val SETTINGS_ROUTE: String = "settings" fun NavGraphBuilder.settingsGraph( navController: NavController, onNavigateToDeleteAccount: () -> Unit, + onNavigateToFolders: () -> Unit, ) { navigation( startDestination = SETTINGS_ROUTE, @@ -52,12 +53,15 @@ fun NavGraphBuilder.settingsGraph( appearanceDestination(onNavigateBack = { navController.popBackStack() }) autoFillDestination(onNavigateBack = { navController.popBackStack() }) otherDestination(onNavigateBack = { navController.popBackStack() }) - vaultSettingsDestination(onNavigateBack = { navController.popBackStack() }) + vaultSettingsDestination( + onNavigateBack = { navController.popBackStack() }, + onNavigateToFolders = onNavigateToFolders, + ) } } /** - * Navigate to the settings screen screen. + * Navigate to the settings screen. */ fun NavController.navigateToSettingsGraph(navOptions: NavOptions? = null) { navigate(SETTINGS_GRAPH_ROUTE, navOptions) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersNavigation.kt new file mode 100644 index 000000000..d0362d257 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersNavigation.kt @@ -0,0 +1,30 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.folders + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions + +private const val FOLDERS_ROUTE = "settings_folders" + +/** + * Add folders destinations to the nav graph. + */ +fun NavGraphBuilder.foldersDestination( + onNavigateBack: () -> Unit, +) { + composableWithSlideTransitions( + route = FOLDERS_ROUTE, + ) { + FoldersScreen( + onNavigateBack = onNavigateBack, + ) + } +} + +/** + * Navigate to the folders screen. + */ +fun NavController.navigateToFolders(navOptions: NavOptions? = null) { + navigate(FOLDERS_ROUTE, navOptions) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersScreen.kt new file mode 100644 index 000000000..526e6c293 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersScreen.kt @@ -0,0 +1,108 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.folders + +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold +import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar + +/** + * Displays the folders screen. + */ +@Suppress("LongMethod") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FoldersScreen( + onNavigateBack: () -> Unit, + viewModel: FoldersViewModel = hiltViewModel(), +) { + val context = LocalContext.current + EventsEffect(viewModel = viewModel) { event -> + when (event) { + FoldersEvent.NavigateBack -> onNavigateBack() + is FoldersEvent.ShowToast -> { + Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show() + } + } + } + + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + BitwardenScaffold( + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + BitwardenTopAppBar( + title = stringResource(id = R.string.folders), + scrollBehavior = scrollBehavior, + navigationIcon = painterResource(id = R.drawable.ic_close), + navigationIconContentDescription = stringResource(id = R.string.close), + onNavigationIconClick = remember(viewModel) { + { viewModel.trySendAction(FoldersAction.CloseButtonClick) } + }, + ) + }, + floatingActionButton = { + FloatingActionButton( + containerColor = MaterialTheme.colorScheme.primaryContainer, + onClick = remember(viewModel) { + { viewModel.trySendAction(FoldersAction.AddFolderButtonClick) } + }, + modifier = Modifier.navigationBarsPadding(), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_plus), + contentDescription = stringResource(id = R.string.add_item), + tint = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } + }, + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.Center, + ) { + // TODO BIT-460 populate Folders screen + + Text( + text = stringResource(id = R.string.no_folders_to_list), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .fillMaxSize() + .padding( + vertical = 4.dp, + horizontal = 16.dp, + ), + ) + } + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersViewModel.kt new file mode 100644 index 000000000..9fd0f7bad --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersViewModel.kt @@ -0,0 +1,60 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.folders + +import com.x8bit.bitwarden.ui.platform.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +/** + * View model for the folders screen. + */ +@HiltViewModel +class FoldersViewModel @Inject constructor() : + BaseViewModel( + initialState = Unit, + ) { + override fun handleAction(action: FoldersAction): Unit = when (action) { + FoldersAction.AddFolderButtonClick -> handleAddFolderButtonClicked() + FoldersAction.CloseButtonClick -> handleCloseButtonClicked() + } + + private fun handleAddFolderButtonClicked() { + // TODO BIT-458 implement add folders + sendEvent(FoldersEvent.ShowToast("Not yet implemented.")) + } + + private fun handleCloseButtonClicked() { + sendEvent(FoldersEvent.NavigateBack) + } +} + +/** + * Models events for the folders screen. + */ +sealed class FoldersEvent { + /** + * Navigates back to the previous screen. + */ + data object NavigateBack : FoldersEvent() + + /** + * Shows a toast with the given [message]. + */ + data class ShowToast( + val message: String, + ) : FoldersEvent() +} + +/** + * Models actions for the folders screen. + */ +sealed class FoldersAction { + /** + * Indicates that the user clicked the add folder button. + */ + data object AddFolderButtonClick : FoldersAction() + + /** + * Indicates that the user clicked the close button. + */ + data object CloseButtonClick : FoldersAction() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsNavigation.kt index c8b37b128..2a85d63d5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsNavigation.kt @@ -12,11 +12,15 @@ private const val VAULT_SETTINGS_ROUTE = "vault_settings" */ fun NavGraphBuilder.vaultSettingsDestination( onNavigateBack: () -> Unit, + onNavigateToFolders: () -> Unit, ) { composableWithPushTransitions( route = VAULT_SETTINGS_ROUTE, ) { - VaultSettingsScreen(onNavigateBack = onNavigateBack) + VaultSettingsScreen( + onNavigateBack = onNavigateBack, + onNavigateToFolders = onNavigateToFolders, + ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt index 188b3e144..70b9cca45 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt @@ -1,7 +1,9 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.vault +import android.widget.Toast import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -12,26 +14,36 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.components.BitwardenExternalLinkRow import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold +import com.x8bit.bitwarden.ui.platform.components.BitwardenTextRow import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar /** - * Displays the vault screen. + * Displays the vault settings screen. */ +@Suppress("LongMethod") @OptIn(ExperimentalMaterial3Api::class) @Composable fun VaultSettingsScreen( onNavigateBack: () -> Unit, + onNavigateToFolders: () -> Unit, viewModel: VaultSettingsViewModel = hiltViewModel(), ) { + val context = LocalContext.current EventsEffect(viewModel = viewModel) { event -> when (event) { - VaultSettingsEvent.NavigateBack -> onNavigateBack.invoke() + VaultSettingsEvent.NavigateBack -> onNavigateBack() + VaultSettingsEvent.NavigateToFolders -> onNavigateToFolders() + is VaultSettingsEvent.ShowToast -> { + Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show() + } } } @@ -58,7 +70,34 @@ fun VaultSettingsScreen( .fillMaxSize() .verticalScroll(rememberScrollState()), ) { - // TODO: BIT-928 Display Vault UI + BitwardenTextRow( + text = stringResource(R.string.folders), + onClick = remember(viewModel) { + { viewModel.trySendAction(VaultSettingsAction.FoldersButtonClick) } + }, + withDivider = true, + modifier = Modifier.fillMaxWidth(), + ) + + BitwardenTextRow( + text = stringResource(R.string.export_vault), + onClick = remember(viewModel) { + { viewModel.trySendAction(VaultSettingsAction.ExportVaultClick) } + }, + withDivider = true, + modifier = Modifier.fillMaxWidth(), + ) + + BitwardenExternalLinkRow( + text = stringResource(R.string.import_items), + onConfirmClick = remember(viewModel) { + { viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) } + }, + withDivider = true, + dialogTitle = stringResource(id = R.string.import_items_confirmation), + dialogMessage = stringResource(id = R.string.import_items_description), + modifier = Modifier.fillMaxWidth(), + ) } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModel.kt index f1afc2adb..bf5ddefd6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModel.kt @@ -13,7 +13,28 @@ class VaultSettingsViewModel @Inject constructor() : initialState = Unit, ) { override fun handleAction(action: VaultSettingsAction): Unit = when (action) { - VaultSettingsAction.BackClick -> sendEvent(VaultSettingsEvent.NavigateBack) + VaultSettingsAction.BackClick -> handleBackClicked() + VaultSettingsAction.ExportVaultClick -> handleExportVaultClicked() + VaultSettingsAction.FoldersButtonClick -> handleFoldersButtonClicked() + VaultSettingsAction.ImportItemsClick -> handleImportItemsClicked() + } + + private fun handleBackClicked() { + sendEvent(VaultSettingsEvent.NavigateBack) + } + + private fun handleExportVaultClicked() { + // TODO BIT-1272 go to vault export screen + sendEvent(VaultSettingsEvent.ShowToast("Not yet implemented.")) + } + + private fun handleFoldersButtonClicked() { + sendEvent(VaultSettingsEvent.NavigateToFolders) + } + + private fun handleImportItemsClicked() { + // TODO BIT-972 implement import items functionality + sendEvent(VaultSettingsEvent.ShowToast("Not yet implemented.")) } } @@ -25,6 +46,18 @@ sealed class VaultSettingsEvent { * Navigate back. */ data object NavigateBack : VaultSettingsEvent() + + /** + * Navigate to the Folders screen. + */ + data object NavigateToFolders : VaultSettingsEvent() + + /** + * Shows a toast with the given [message]. + */ + data class ShowToast( + val message: String, + ) : VaultSettingsEvent() } /** @@ -35,4 +68,19 @@ sealed class VaultSettingsAction { * User clicked back button. */ data object BackClick : VaultSettingsAction() + + /** + * Indicates that the user clicked the Export Vault button. + */ + data object ExportVaultClick : VaultSettingsAction() + + /** + * Indicates that the user clicked the Folders button. + */ + data object FoldersButtonClick : VaultSettingsAction() + + /** + * Indicates that the user clicked the Import Items button. + */ + data object ImportItemsClick : VaultSettingsAction() } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt index 8b662c7be..1fe03647e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt @@ -6,6 +6,8 @@ import androidx.navigation.NavOptions import androidx.navigation.navigation import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount.deleteAccountDestination import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount.navigateToDeleteAccount +import com.x8bit.bitwarden.ui.platform.feature.settings.folders.foldersDestination +import com.x8bit.bitwarden.ui.platform.feature.settings.folders.navigateToFolders import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VAULT_UNLOCKED_NAV_BAR_ROUTE import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.vaultUnlockedNavBarDestination import com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory.navigateToPasswordHistory @@ -38,6 +40,7 @@ fun NavGraphBuilder.vaultUnlockedGraph( route = VAULT_UNLOCKED_GRAPH_ROUTE, ) { vaultUnlockedNavBarDestination( + onNavigateToFolders = { navController.navigateToFolders() }, onNavigateToVaultAddItem = { navController.navigateToVaultAddEditItem(VaultAddEditType.AddItem) }, @@ -59,5 +62,6 @@ fun NavGraphBuilder.vaultUnlockedGraph( ) newSendDestination(onNavigateBack = { navController.popBackStack() }) passwordHistoryDestination(onNavigateBack = { navController.popBackStack() }) + foldersDestination(onNavigateBack = { navController.popBackStack() }) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt index 72af0172e..a2220af81 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt @@ -27,6 +27,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination( onNavigateToVaultEditItem: (vaultItemId: String) -> Unit, onNavigateToNewSend: () -> Unit, onNavigateToDeleteAccount: () -> Unit, + onNavigateToFolders: () -> Unit, onNavigateToPasswordHistory: () -> Unit, ) { composableWithStayTransitions( @@ -38,6 +39,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination( onNavigateToVaultEditItem = onNavigateToVaultEditItem, onNavigateToNewSend = onNavigateToNewSend, onNavigateToDeleteAccount = onNavigateToDeleteAccount, + onNavigateToFolders = onNavigateToFolders, onNavigateToPasswordHistory = onNavigateToPasswordHistory, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt index e29f2fcf3..85fc357bd 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt @@ -62,6 +62,7 @@ import kotlinx.parcelize.Parcelize /** * Top level composable for the Vault Unlocked Screen. */ +@Suppress("LongParameterList") @Composable fun VaultUnlockedNavBarScreen( viewModel: VaultUnlockedNavBarViewModel = hiltViewModel(), @@ -71,6 +72,7 @@ fun VaultUnlockedNavBarScreen( onNavigateToVaultEditItem: (vaultItemId: String) -> Unit, onNavigateToNewSend: () -> Unit, onNavigateToDeleteAccount: () -> Unit, + onNavigateToFolders: () -> Unit, onNavigateToPasswordHistory: () -> Unit, ) { EventsEffect(viewModel = viewModel) { event -> @@ -102,6 +104,7 @@ fun VaultUnlockedNavBarScreen( navigateToVaultAddItem = onNavigateToVaultAddItem, navigateToNewSend = onNavigateToNewSend, navigateToDeleteAccount = onNavigateToDeleteAccount, + navigateToFolders = onNavigateToFolders, navigateToPasswordHistory = onNavigateToPasswordHistory, generatorTabClickedAction = { viewModel.trySendAction(VaultUnlockedNavBarAction.GeneratorTabClick) @@ -134,6 +137,7 @@ private fun VaultUnlockedNavBarScaffold( onNavigateToVaultEditItem: (vaultItemId: String) -> Unit, navigateToNewSend: () -> Unit, navigateToDeleteAccount: () -> Unit, + navigateToFolders: () -> Unit, navigateToPasswordHistory: () -> Unit, ) { var shouldDimNavBar by remember { mutableStateOf(false) } @@ -200,6 +204,7 @@ private fun VaultUnlockedNavBarScaffold( settingsGraph( navController = navController, onNavigateToDeleteAccount = navigateToDeleteAccount, + onNavigateToFolders = navigateToFolders, ) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersScreenTest.kt new file mode 100644 index 000000000..60f977ae7 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersScreenTest.kt @@ -0,0 +1,56 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.folders + +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.performClick +import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class FoldersScreenTest : BaseComposeTest() { + + private var onNavigateBackCalled = false + private val mutableEventFlow = MutableSharedFlow( + extraBufferCapacity = Int.MAX_VALUE, + ) + private val mutableStateFlow = MutableStateFlow(Unit) + val viewModel = mockk(relaxed = true) { + every { eventFlow } returns mutableEventFlow + every { stateFlow } returns mutableStateFlow + } + + @Before + fun setup() { + composeTestRule.setContent { + FoldersScreen( + viewModel = viewModel, + onNavigateBack = { onNavigateBackCalled = true }, + ) + } + } + + @Test + fun `close button click should send CloseButtonClick`() { + composeTestRule.onNodeWithContentDescription("Close").performClick() + verify { viewModel.trySendAction(FoldersAction.CloseButtonClick) } + } + + @Test + fun `add folder button click should send AddFolderButtonClick`() { + composeTestRule.onNodeWithContentDescription("Add item").performClick() + verify { + viewModel.trySendAction(FoldersAction.AddFolderButtonClick) + } + } + + @Test + fun `NavigateBack should call onNavigateBack`() { + mutableEventFlow.tryEmit(FoldersEvent.NavigateBack) + assertTrue(onNavigateBackCalled) + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersViewModelTest.kt new file mode 100644 index 000000000..3cdacc920 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersViewModelTest.kt @@ -0,0 +1,33 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.folders + +import app.cash.turbine.test +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 FoldersViewModelTest : BaseViewModelTest() { + + @Test + fun `BackClick should emit NavigateBack`() = runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(FoldersAction.CloseButtonClick) + assertEquals(FoldersEvent.NavigateBack, awaitItem()) + } + } + + @Test + fun `AddFolderButtonClick should emit ShowToast`() = runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(FoldersAction.AddFolderButtonClick) + assertEquals( + FoldersEvent.ShowToast("Not yet implemented."), + awaitItem(), + ) + } + } + + private fun createViewModel(): FoldersViewModel = FoldersViewModel() +} diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreenTest.kt index fe7e6bfa5..16810aa90 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreenTest.kt @@ -1,46 +1,98 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.vault +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasAnyAncestor +import androidx.compose.ui.test.isDialog import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import io.mockk.every import io.mockk.mockk import io.mockk.verify -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Test class VaultSettingsScreenTest : BaseComposeTest() { - @Test - fun `on back click should send BackClick`() { - val viewModel: VaultSettingsViewModel = mockk { - every { eventFlow } returns emptyFlow() - every { trySendAction(VaultSettingsAction.BackClick) } returns Unit - } + private var onNavigateBackCalled = false + private var onNavigateToFoldersCalled = false + private val mutableEventFlow = MutableSharedFlow( + extraBufferCapacity = Int.MAX_VALUE, + ) + private val mutableStateFlow = MutableStateFlow(Unit) + val viewModel = mockk(relaxed = true) { + every { eventFlow } returns mutableEventFlow + every { stateFlow } returns mutableStateFlow + } + + @Before + fun setup() { composeTestRule.setContent { VaultSettingsScreen( viewModel = viewModel, - onNavigateBack = { }, + onNavigateBack = { onNavigateBackCalled = true }, + onNavigateToFolders = { onNavigateToFoldersCalled = true }, ) } + } + + @Test + fun `on back click should send BackClick`() { + every { viewModel.trySendAction(VaultSettingsAction.BackClick) } returns Unit composeTestRule.onNodeWithContentDescription("Back").performClick() verify { viewModel.trySendAction(VaultSettingsAction.BackClick) } } @Test - fun `on NavigateAbout should call onNavigateToVault`() { - var haveCalledNavigateBack = false - val viewModel = mockk { - every { eventFlow } returns flowOf(VaultSettingsEvent.NavigateBack) + fun `export vault click should send ExportVaultClick`() { + composeTestRule.onNodeWithText("Export vault").performClick() + verify { + viewModel.trySendAction(VaultSettingsAction.ExportVaultClick) } - composeTestRule.setContent { - VaultSettingsScreen( - viewModel = viewModel, - onNavigateBack = { haveCalledNavigateBack = true }, - ) + } + + @Test + fun `import items click should display dialog and confirming should send ImportItemsClick`() { + composeTestRule.onNodeWithText("Import items").performClick() + composeTestRule + .onNodeWithText("Continue") + .assert(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + .performClick() + + verify { + viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) } - assertTrue(haveCalledNavigateBack) + } + + @Test + fun `import items click should display dialog & canceling should not send ImportItemsClick`() { + composeTestRule.onNodeWithText("Import items").performClick() + composeTestRule + .onNodeWithText("Cancel") + .assert(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + .performClick() + + verify(exactly = 0) { + viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) + } + } + + @Test + fun `NavigateBack should call onNavigateBack`() { + mutableEventFlow.tryEmit(VaultSettingsEvent.NavigateBack) + assertTrue(onNavigateBackCalled) + } + + @Test + fun `NavigateToFolders should call onNavigateToFolders`() { + mutableEventFlow.tryEmit(VaultSettingsEvent.NavigateToFolders) + assertTrue(onNavigateToFoldersCalled) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModelTest.kt index 0aae81b86..3511976de 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModelTest.kt @@ -9,11 +9,37 @@ import org.junit.jupiter.api.Test class VaultSettingsViewModelTest : BaseViewModelTest() { @Test - fun `on BackClick should emit NavigateBack`() = runTest { - val viewModel = VaultSettingsViewModel() + fun `BackClick should emit NavigateBack`() = runTest { + val viewModel = createViewModel() viewModel.eventFlow.test { viewModel.trySendAction(VaultSettingsAction.BackClick) assertEquals(VaultSettingsEvent.NavigateBack, awaitItem()) } } + + @Test + fun `ExportVaultClick should emit ShowToast`() = runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(VaultSettingsAction.ExportVaultClick) + assertEquals( + VaultSettingsEvent.ShowToast("Not yet implemented."), + awaitItem(), + ) + } + } + + @Test + fun `ImportItemsClick should emit ShowToast`() = runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) + assertEquals( + VaultSettingsEvent.ShowToast("Not yet implemented."), + awaitItem(), + ) + } + } + + private fun createViewModel(): VaultSettingsViewModel = VaultSettingsViewModel() } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt index 663aaf432..dadde6133 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt @@ -9,10 +9,20 @@ import io.mockk.every import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Before import org.junit.Test class VaultUnlockedNavBarScreenTest : BaseComposeTest() { private val fakeNavHostController = FakeNavHostController() + private val mutableEventFlow = MutableSharedFlow( + extraBufferCapacity = Int.MAX_VALUE, + ) + private val mutableStateFlow = MutableStateFlow(Unit) + val viewModel = mockk(relaxed = true) { + every { eventFlow } returns mutableEventFlow + every { stateFlow } returns mutableStateFlow + } private val expectedNavOptions = navOptions { // When changing root navigation state, pop everything else off the back stack: @@ -24,9 +34,8 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { restoreState = true } - @Test - fun `vault tab click should send VaultTabClick action`() { - val viewModel = mockk(relaxed = true) + @Before + fun setup() { composeTestRule.apply { setContent { VaultUnlockedNavBarScreen( @@ -37,90 +46,42 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { onNavigateToVaultEditItem = {}, onNavigateToNewSend = {}, onNavigateToDeleteAccount = {}, + onNavigateToFolders = {}, onNavigateToPasswordHistory = {}, ) } - onNodeWithText("My vault").performClick() } + } + + @Test + fun `vault tab click should send VaultTabClick action`() { + composeTestRule.onNodeWithText("My vault").performClick() verify { viewModel.trySendAction(VaultUnlockedNavBarAction.VaultTabClick) } } @Test fun `NavigateToVaultScreen should navigate to VaultScreen`() { - val vaultUnlockedNavBarEventFlow = MutableSharedFlow( - extraBufferCapacity = Int.MAX_VALUE, - ) - val viewModel = mockk(relaxed = true) { - every { eventFlow } returns vaultUnlockedNavBarEventFlow - } - composeTestRule.apply { - setContent { - VaultUnlockedNavBarScreen( - viewModel = viewModel, - navController = fakeNavHostController, - onNavigateToVaultAddItem = {}, - onNavigateToVaultItem = {}, - onNavigateToVaultEditItem = {}, - onNavigateToNewSend = {}, - onNavigateToDeleteAccount = {}, - onNavigateToPasswordHistory = {}, - ) - } - runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") } - vaultUnlockedNavBarEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToVaultScreen) - runOnIdle { - fakeNavHostController.assertLastNavigation( - route = "vault_graph", - navOptions = expectedNavOptions, - ) - } + composeTestRule.runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") } + mutableEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToVaultScreen) + composeTestRule.runOnIdle { + fakeNavHostController.assertLastNavigation( + route = "vault_graph", + navOptions = expectedNavOptions, + ) } } @Test fun `send tab click should send SendTabClick action`() { - val viewModel = mockk(relaxed = true) - composeTestRule.apply { - setContent { - VaultUnlockedNavBarScreen( - viewModel = viewModel, - navController = fakeNavHostController, - onNavigateToVaultAddItem = {}, - onNavigateToVaultItem = {}, - onNavigateToVaultEditItem = {}, - onNavigateToNewSend = {}, - onNavigateToDeleteAccount = {}, - onNavigateToPasswordHistory = {}, - ) - } - onNodeWithText("Send").performClick() - } + composeTestRule.onNodeWithText("Send").performClick() verify { viewModel.trySendAction(VaultUnlockedNavBarAction.SendTabClick) } } @Test fun `NavigateToSendScreen should navigate to SendScreen`() { - val vaultUnlockedNavBarEventFlow = MutableSharedFlow( - extraBufferCapacity = Int.MAX_VALUE, - ) - val viewModel = mockk(relaxed = true) { - every { eventFlow } returns vaultUnlockedNavBarEventFlow - } composeTestRule.apply { - setContent { - VaultUnlockedNavBarScreen( - viewModel = viewModel, - navController = fakeNavHostController, - onNavigateToVaultAddItem = {}, - onNavigateToVaultItem = {}, - onNavigateToVaultEditItem = {}, - onNavigateToNewSend = {}, - onNavigateToDeleteAccount = {}, - onNavigateToPasswordHistory = {}, - ) - } runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") } - vaultUnlockedNavBarEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToSendScreen) + mutableEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToSendScreen) runOnIdle { fakeNavHostController.assertLastNavigation( route = "send", @@ -132,48 +93,15 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { @Test fun `generator tab click should send GeneratorTabClick action`() { - val viewModel = mockk(relaxed = true) - composeTestRule.apply { - setContent { - VaultUnlockedNavBarScreen( - viewModel = viewModel, - navController = fakeNavHostController, - onNavigateToVaultAddItem = {}, - onNavigateToVaultItem = {}, - onNavigateToVaultEditItem = {}, - onNavigateToNewSend = {}, - onNavigateToDeleteAccount = {}, - onNavigateToPasswordHistory = {}, - ) - } - onNodeWithText("Generator").performClick() - } + composeTestRule.onNodeWithText("Generator").performClick() verify { viewModel.trySendAction(VaultUnlockedNavBarAction.GeneratorTabClick) } } @Test fun `NavigateToGeneratorScreen should navigate to GeneratorScreen`() { - val vaultUnlockedNavBarEventFlow = MutableSharedFlow( - extraBufferCapacity = Int.MAX_VALUE, - ) - val viewModel = mockk(relaxed = true) { - every { eventFlow } returns vaultUnlockedNavBarEventFlow - } composeTestRule.apply { - setContent { - VaultUnlockedNavBarScreen( - viewModel = viewModel, - navController = fakeNavHostController, - onNavigateToVaultAddItem = {}, - onNavigateToVaultItem = {}, - onNavigateToVaultEditItem = {}, - onNavigateToNewSend = {}, - onNavigateToDeleteAccount = {}, - onNavigateToPasswordHistory = {}, - ) - } runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") } - vaultUnlockedNavBarEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToGeneratorScreen) + mutableEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToGeneratorScreen) runOnIdle { fakeNavHostController.assertLastNavigation( route = "generator", @@ -185,48 +113,15 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { @Test fun `settings tab click should send SendTabClick action`() { - val viewModel = mockk(relaxed = true) - composeTestRule.apply { - setContent { - VaultUnlockedNavBarScreen( - viewModel = viewModel, - navController = fakeNavHostController, - onNavigateToVaultAddItem = {}, - onNavigateToVaultItem = {}, - onNavigateToVaultEditItem = {}, - onNavigateToNewSend = {}, - onNavigateToDeleteAccount = {}, - onNavigateToPasswordHistory = {}, - ) - } - onNodeWithText("Settings").performClick() - } + composeTestRule.onNodeWithText("Settings").performClick() verify { viewModel.trySendAction(VaultUnlockedNavBarAction.SettingsTabClick) } } @Test fun `NavigateToSettingsScreen should navigate to SettingsScreen`() { - val vaultUnlockedNavBarEventFlow = MutableSharedFlow( - extraBufferCapacity = Int.MAX_VALUE, - ) - val viewModel = mockk(relaxed = true) { - every { eventFlow } returns vaultUnlockedNavBarEventFlow - } composeTestRule.apply { - setContent { - VaultUnlockedNavBarScreen( - viewModel = viewModel, - navController = fakeNavHostController, - onNavigateToVaultAddItem = {}, - onNavigateToVaultItem = {}, - onNavigateToVaultEditItem = {}, - onNavigateToNewSend = {}, - onNavigateToDeleteAccount = {}, - onNavigateToPasswordHistory = {}, - ) - } runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") } - vaultUnlockedNavBarEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToSettingsScreen) + mutableEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToSettingsScreen) runOnIdle { fakeNavHostController.assertLastNavigation( route = "settings_graph",