From 75fbadb67b2acd927a3e2a416c78175fba900617 Mon Sep 17 00:00:00 2001 From: David Perez Date: Mon, 22 Jan 2024 09:23:32 -0600 Subject: [PATCH] Add basic navigation for the search screen (#707) --- .../feature/search/SearchNavigation.kt | 131 ++++++++++++++++++ .../platform/feature/search/SearchScreen.kt | 84 +++++++++++ .../feature/search/SearchViewModel.kt | 63 +++++++++ .../feature/search/model/SearchType.kt | 85 ++++++++++++ .../vaultunlocked/VaultUnlockedNavigation.kt | 5 + .../VaultUnlockedNavBarNavigation.kt | 5 + .../VaultUnlockedNavBarScreen.kt | 9 ++ .../tools/feature/send/SendGraphNavigation.kt | 4 + .../ui/tools/feature/send/SendNavigation.kt | 3 + .../ui/tools/feature/send/SendScreen.kt | 6 +- .../ui/tools/feature/send/SendViewModel.kt | 8 +- .../itemlisting/VaultItemListingNavigation.kt | 7 + .../itemlisting/VaultItemListingScreen.kt | 9 +- .../itemlisting/VaultItemListingViewModel.kt | 12 +- .../util/VaultItemListingStateExtensions.kt | 28 ++++ .../feature/vault/VaultGraphNavigation.kt | 6 +- .../ui/vault/feature/vault/VaultNavigation.kt | 3 + .../ui/vault/feature/vault/VaultScreen.kt | 9 +- .../feature/search/SearchScreenTest.kt | 42 ++++++ .../feature/search/SearchViewModelTest.kt | 83 +++++++++++ .../VaultUnlockedNavBarScreenTest.kt | 2 + .../ui/tools/feature/send/SendScreenTest.kt | 8 ++ .../tools/feature/send/SendViewModelTest.kt | 4 +- .../itemlisting/VaultItemListingScreenTest.kt | 10 ++ .../VaultItemListingViewModelTest.kt | 4 +- .../VaultItemListingStateExtensionsTest.kt | 111 +++++++++++++++ .../ui/vault/feature/vault/VaultScreenTest.kt | 8 ++ 27 files changed, 728 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchNavigation.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreen.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/model/SearchType.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensions.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreenTest.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensionsTest.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchNavigation.kt new file mode 100644 index 000000000..f68411268 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchNavigation.kt @@ -0,0 +1,131 @@ +package com.x8bit.bitwarden.ui.platform.feature.search + +import androidx.lifecycle.SavedStateHandle +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.NavType +import androidx.navigation.navArgument +import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage +import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType + +private const val SEARCH_TYPE: String = "search_type" +private const val SEARCH_TYPE_SEND_ALL: String = "search_type_sends_all" +private const val SEARCH_TYPE_SEND_TEXT: String = "search_type_sends_text" +private const val SEARCH_TYPE_SEND_FILE: String = "search_type_sends_file" +private const val SEARCH_TYPE_VAULT_ALL: String = "search_type_vault_all" +private const val SEARCH_TYPE_VAULT_LOGINS: String = "search_type_vault_logins" +private const val SEARCH_TYPE_VAULT_CARDS: String = "search_type_vault_cards" +private const val SEARCH_TYPE_VAULT_IDENTITIES: String = "search_type_vault_identities" +private const val SEARCH_TYPE_VAULT_SECURE_NOTES: String = "search_type_vault_secure_notes" +private const val SEARCH_TYPE_VAULT_COLLECTION: String = "search_type_vault_collection" +private const val SEARCH_TYPE_VAULT_NO_FOLDER: String = "search_type_vault_no_folder" +private const val SEARCH_TYPE_VAULT_FOLDER: String = "search_type_vault_folder" +private const val SEARCH_TYPE_VAULT_TRASH: String = "search_type_vault_trash" +private const val SEARCH_TYPE_ID: String = "search_type_id" + +private const val SEARCH_ROUTE_PREFIX: String = "search" +private const val SEARCH_ROUTE: String = "$SEARCH_ROUTE_PREFIX/{$SEARCH_TYPE}/{$SEARCH_TYPE_ID}" + +/** + * Class to retrieve search arguments from the [SavedStateHandle]. + */ +@OmitFromCoverage +data class SearchArgs( + val type: SearchType, +) { + constructor(savedStateHandle: SavedStateHandle) : this( + type = determineSearchType( + searchTypeString = requireNotNull(savedStateHandle.get(SEARCH_TYPE)), + id = savedStateHandle.get(SEARCH_TYPE_ID), + ), + ) +} + +/** + * Add search destinations to the nav graph. + */ +fun NavGraphBuilder.searchDestination( + onNavigateBack: () -> Unit, +) { + composableWithSlideTransitions( + route = SEARCH_ROUTE, + arguments = listOf( + navArgument(SEARCH_TYPE) { type = NavType.StringType }, + navArgument(SEARCH_TYPE_ID) { + type = NavType.StringType + nullable = true + }, + ), + ) { + SearchScreen( + onNavigateBack = onNavigateBack, + ) + } +} + +/** + * Navigate to the search screen. + */ +fun NavController.navigateToSearch( + searchType: SearchType, + navOptions: NavOptions? = null, +) { + navigate( + route = "$SEARCH_ROUTE_PREFIX/${searchType.toTypeString()}/${searchType.toIdOrNull()}", + navOptions = navOptions, + ) +} + +private fun determineSearchType( + searchTypeString: String, + id: String?, +): SearchType = + when (searchTypeString) { + SEARCH_TYPE_SEND_ALL -> SearchType.Sends.All + SEARCH_TYPE_SEND_TEXT -> SearchType.Sends.Texts + SEARCH_TYPE_SEND_FILE -> SearchType.Sends.Files + SEARCH_TYPE_VAULT_ALL -> SearchType.Vault.All + SEARCH_TYPE_VAULT_LOGINS -> SearchType.Vault.Logins + SEARCH_TYPE_VAULT_CARDS -> SearchType.Vault.Cards + SEARCH_TYPE_VAULT_IDENTITIES -> SearchType.Vault.Identities + SEARCH_TYPE_VAULT_SECURE_NOTES -> SearchType.Vault.SecureNotes + SEARCH_TYPE_VAULT_COLLECTION -> SearchType.Vault.Collection(requireNotNull(id)) + SEARCH_TYPE_VAULT_NO_FOLDER -> SearchType.Vault.NoFolder + SEARCH_TYPE_VAULT_FOLDER -> SearchType.Vault.Folder(requireNotNull(id)) + SEARCH_TYPE_VAULT_TRASH -> SearchType.Vault.Trash + else -> throw IllegalArgumentException("Invalid Search Type") + } + +private fun SearchType.toTypeString(): String = + when (this) { + SearchType.Sends.All -> SEARCH_TYPE_SEND_ALL + SearchType.Sends.Files -> SEARCH_TYPE_SEND_FILE + SearchType.Sends.Texts -> SEARCH_TYPE_SEND_TEXT + SearchType.Vault.All -> SEARCH_TYPE_VAULT_ALL + SearchType.Vault.Cards -> SEARCH_TYPE_VAULT_CARDS + is SearchType.Vault.Collection -> SEARCH_TYPE_VAULT_COLLECTION + is SearchType.Vault.Folder -> SEARCH_TYPE_VAULT_FOLDER + SearchType.Vault.Identities -> SEARCH_TYPE_VAULT_IDENTITIES + SearchType.Vault.Logins -> SEARCH_TYPE_VAULT_LOGINS + SearchType.Vault.NoFolder -> SEARCH_TYPE_VAULT_NO_FOLDER + SearchType.Vault.SecureNotes -> SEARCH_TYPE_VAULT_SECURE_NOTES + SearchType.Vault.Trash -> SEARCH_TYPE_VAULT_TRASH + } + +private fun SearchType.toIdOrNull(): String? = + when (this) { + SearchType.Sends.All -> null + SearchType.Sends.Files -> null + SearchType.Sends.Texts -> null + SearchType.Vault.All -> null + SearchType.Vault.Cards -> null + is SearchType.Vault.Collection -> collectionId + is SearchType.Vault.Folder -> folderId + SearchType.Vault.Identities -> null + SearchType.Vault.Logins -> null + SearchType.Vault.NoFolder -> null + SearchType.Vault.SecureNotes -> null + SearchType.Vault.Trash -> null + } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreen.kt new file mode 100644 index 000000000..ec817235c --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreen.kt @@ -0,0 +1,84 @@ +package com.x8bit.bitwarden.ui.platform.feature.search + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +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.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +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.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold +import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar +import com.x8bit.bitwarden.ui.platform.components.NavigationIcon + +/** + * The search UI for vault items or send items. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchScreen( + onNavigateBack: () -> Unit, + viewModel: SearchViewModel = hiltViewModel(), +) { + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + EventsEffect(viewModel = viewModel) { event -> + when (event) { + SearchEvent.NavigateBack -> onNavigateBack() + } + } + + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + BitwardenScaffold( + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + // TODO: Implement search appbar (BIT-640) + BitwardenTopAppBar( + title = "Search", + scrollBehavior = scrollBehavior, + navigationIcon = NavigationIcon( + navigationIcon = painterResource(id = R.drawable.ic_close), + navigationIconContentDescription = stringResource(id = R.string.close), + onNavigationIconClick = remember(viewModel) { + { viewModel.trySendAction(SearchAction.BackClick) } + }, + ), + ) + }, + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + // TODO: Search Bar (BIT-640) + Text(text = state.searchType.toString()) + Spacer(modifier = Modifier.height(8.dp)) + Text(text = "Not yet implemented") + Spacer(modifier = Modifier.navigationBarsPadding()) + } + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt new file mode 100644 index 000000000..6e245bd17 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt @@ -0,0 +1,63 @@ +package com.x8bit.bitwarden.ui.platform.feature.search + +import android.os.Parcelable +import androidx.lifecycle.SavedStateHandle +import com.x8bit.bitwarden.ui.platform.base.BaseViewModel +import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.parcelize.Parcelize +import javax.inject.Inject + +private const val KEY_STATE = "state" + +/** + * View model for the search screen. + */ +@HiltViewModel +class SearchViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, +) : BaseViewModel( + // We load the state from the savedStateHandle for testing purposes. + initialState = savedStateHandle[KEY_STATE] + ?: SearchState( + searchType = SearchArgs(savedStateHandle).type, + ), +) { + override fun handleAction(action: SearchAction) { + when (action) { + SearchAction.BackClick -> handleBackClick() + } + } + + private fun handleBackClick() { + sendEvent(SearchEvent.NavigateBack) + } +} + +/** + * Represents the overall state for the [SearchScreen]. + */ +@Parcelize +data class SearchState( + val searchType: SearchType, +) : Parcelable + +/** + * Models actions for the [SearchScreen]. + */ +sealed class SearchAction { + /** + * User clicked the back button. + */ + data object BackClick : SearchAction() +} + +/** + * Models events for the [SearchScreen]. + */ +sealed class SearchEvent { + /** + * Navigates back to the previous screen. + */ + data object NavigateBack : SearchEvent() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/model/SearchType.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/model/SearchType.kt new file mode 100644 index 000000000..7e296a981 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/model/SearchType.kt @@ -0,0 +1,85 @@ +package com.x8bit.bitwarden.ui.platform.feature.search.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * Represents the difference between searching sends and searching vault items. + */ +sealed class SearchType : Parcelable { + /** + * Indicates that we should be searching sends. + */ + @Parcelize + sealed class Sends : SearchType() { + /** + * Indicates that we should be searching all sends. + */ + data object All : Sends() + + /** + * Indicates that we should be searching only text sends. + */ + data object Texts : Sends() + + /** + * Indicates that we should be searching only file sends. + */ + data object Files : Sends() + } + + /** + * Indicates that we should be searching vault items. + */ + @Parcelize + sealed class Vault : SearchType() { + /** + * Indicates that we should be searching all vault items. + */ + data object All : Vault() + + /** + * Indicates that we should be searching only login ciphers. + */ + data object Logins : Vault() + + /** + * Indicates that we should be searching only card ciphers. + */ + data object Cards : Vault() + + /** + * Indicates that we should be searching only identity ciphers. + */ + data object Identities : Vault() + + /** + * Indicates that we should be searching only secure note ciphers. + */ + data object SecureNotes : Vault() + + /** + * Indicates that we should be searching only ciphers in the given collection. + */ + data class Collection( + val collectionId: String, + ) : Vault() + + /** + * Indicates that we should be searching only ciphers not in a folder. + */ + data object NoFolder : Vault() + + /** + * Indicates that we should be searching only ciphers in the given folder. + */ + data class Folder( + val folderId: String, + ) : Vault() + + /** + * Indicates that we should be searching only ciphers in the trash. + */ + data object Trash : Vault() + } +} 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 e1b52f4e2..f0cefd639 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 @@ -4,6 +4,8 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.navigation +import com.x8bit.bitwarden.ui.platform.feature.search.navigateToSearch +import com.x8bit.bitwarden.ui.platform.feature.search.searchDestination 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 @@ -67,6 +69,8 @@ fun NavGraphBuilder.vaultUnlockedGraph( onNavigateToVaultEditItem = { navController.navigateToVaultAddEdit(VaultAddEditType.EditItem(it)) }, + onNavigateToSearchVault = { navController.navigateToSearch(searchType = it) }, + onNavigateToSearchSend = { navController.navigateToSearch(searchType = it) }, onNavigateToAddSend = { navController.navigateToAddSend(AddSendType.AddItem) }, onNavigateToEditSend = { navController.navigateToAddSend(AddSendType.EditItem(it)) }, onNavigateToDeleteAccount = { navController.navigateToDeleteAccount() }, @@ -114,6 +118,7 @@ fun NavGraphBuilder.vaultUnlockedGraph( passwordHistoryDestination(onNavigateBack = { navController.popBackStack() }) foldersDestination(onNavigateBack = { navController.popBackStack() }) generatorModalDestination(onNavigateBack = { navController.popBackStack() }) + searchDestination(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 8e5935821..1e8c981d4 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 @@ -4,6 +4,7 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.x8bit.bitwarden.ui.platform.base.util.composableWithStayTransitions +import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType /** * The functions below pertain to entry into the [VaultUnlockedNavBarScreen]. @@ -25,6 +26,8 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination( onNavigateToVaultAddItem: () -> Unit, onNavigateToVaultItem: (vaultItemId: String) -> Unit, onNavigateToVaultEditItem: (vaultItemId: String) -> Unit, + onNavigateToSearchSend: (searchType: SearchType.Sends) -> Unit, + onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit, onNavigateToAddSend: () -> Unit, onNavigateToEditSend: (sendItemId: String) -> Unit, onNavigateToDeleteAccount: () -> Unit, @@ -38,6 +41,8 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination( onNavigateToVaultAddItem = onNavigateToVaultAddItem, onNavigateToVaultItem = onNavigateToVaultItem, onNavigateToVaultEditItem = onNavigateToVaultEditItem, + onNavigateToSearchSend = onNavigateToSearchSend, + onNavigateToSearchVault = onNavigateToSearchVault, onNavigateToAddSend = onNavigateToAddSend, onNavigateToEditSend = onNavigateToEditSend, onNavigateToDeleteAccount = onNavigateToDeleteAccount, 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 0746fe221..162f359f1 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 @@ -46,6 +46,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.max import com.x8bit.bitwarden.ui.platform.base.util.toDp import com.x8bit.bitwarden.ui.platform.components.BitwardenAnimatedScrim import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold +import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.platform.feature.settings.SETTINGS_GRAPH_ROUTE import com.x8bit.bitwarden.ui.platform.feature.settings.navigateToSettingsGraph import com.x8bit.bitwarden.ui.platform.feature.settings.settingsGraph @@ -74,6 +75,8 @@ fun VaultUnlockedNavBarScreen( onNavigateToVaultAddItem: () -> Unit, onNavigateToVaultItem: (vaultItemId: String) -> Unit, onNavigateToVaultEditItem: (vaultItemId: String) -> Unit, + onNavigateToSearchSend: (searchType: SearchType.Sends) -> Unit, + onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit, onNavigateToAddSend: () -> Unit, onNavigateToEditSend: (sendItemId: String) -> Unit, onNavigateToDeleteAccount: () -> Unit, @@ -116,6 +119,8 @@ fun VaultUnlockedNavBarScreen( onNavigateToVaultItem = onNavigateToVaultItem, onNavigateToVaultEditItem = onNavigateToVaultEditItem, navigateToVaultAddItem = onNavigateToVaultAddItem, + onNavigateToSearchSend = onNavigateToSearchSend, + onNavigateToSearchVault = onNavigateToSearchVault, navigateToAddSend = onNavigateToAddSend, onNavigateToEditSend = onNavigateToEditSend, navigateToDeleteAccount = onNavigateToDeleteAccount, @@ -151,6 +156,8 @@ private fun VaultUnlockedNavBarScaffold( navigateToVaultAddItem: () -> Unit, onNavigateToVaultItem: (vaultItemId: String) -> Unit, onNavigateToVaultEditItem: (vaultItemId: String) -> Unit, + onNavigateToSearchSend: (searchType: SearchType.Sends) -> Unit, + onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit, navigateToAddSend: () -> Unit, onNavigateToEditSend: (sendItemId: String) -> Unit, navigateToDeleteAccount: () -> Unit, @@ -210,6 +217,7 @@ private fun VaultUnlockedNavBarScaffold( onNavigateToVaultAddItemScreen = navigateToVaultAddItem, onNavigateToVaultItemScreen = onNavigateToVaultItem, onNavigateToVaultEditItemScreen = onNavigateToVaultEditItem, + onNavigateToSearchVault = onNavigateToSearchVault, onDimBottomNavBarRequest = { shouldDim -> shouldDimNavBar = shouldDim }, @@ -218,6 +226,7 @@ private fun VaultUnlockedNavBarScaffold( navController = navController, onNavigateToAddSend = navigateToAddSend, onNavigateToEditSend = onNavigateToEditSend, + onNavigateToSearchSend = onNavigateToSearchSend, ) generatorGraph( onNavigateToPasswordHistory = { navigateToPasswordHistory() }, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendGraphNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendGraphNavigation.kt index 3147b7b75..27d45ca39 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendGraphNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendGraphNavigation.kt @@ -4,6 +4,7 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.navigation +import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.vault.feature.itemlisting.navigateToSendItemListing import com.x8bit.bitwarden.ui.vault.feature.itemlisting.sendItemListingDestination import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType @@ -17,6 +18,7 @@ fun NavGraphBuilder.sendGraph( navController: NavController, onNavigateToAddSend: () -> Unit, onNavigateToEditSend: (sendItemId: String) -> Unit, + onNavigateToSearchSend: (searchType: SearchType.Sends) -> Unit, ) { navigation( startDestination = SEND_ROUTE, @@ -31,11 +33,13 @@ fun NavGraphBuilder.sendGraph( onNavigateToSendTextList = { navController.navigateToSendItemListing(VaultItemListingType.SendText) }, + onNavigateToSearchSend = onNavigateToSearchSend, ) sendItemListingDestination( onNavigateBack = { navController.popBackStack() }, onNavigateToAddSendItem = onNavigateToAddSend, onNavigateToEditSendItem = onNavigateToEditSend, + onNavigateToSearchSend = onNavigateToSearchSend, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendNavigation.kt index 4513960ca..326e32159 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendNavigation.kt @@ -4,6 +4,7 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.x8bit.bitwarden.ui.platform.base.util.composableWithRootPushTransitions +import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType const val SEND_ROUTE: String = "send" @@ -15,6 +16,7 @@ fun NavGraphBuilder.sendDestination( onNavigateToEditSend: (sendItemId: String) -> Unit, onNavigateToSendFilesList: () -> Unit, onNavigateToSendTextList: () -> Unit, + onNavigateToSearchSend: (searchType: SearchType.Sends) -> Unit, ) { composableWithRootPushTransitions( route = SEND_ROUTE, @@ -24,6 +26,7 @@ fun NavGraphBuilder.sendDestination( onNavigateToEditSend = onNavigateToEditSend, onNavigateToSendFilesList = onNavigateToSendFilesList, onNavigateToSendTextList = onNavigateToSendTextList, + onNavigateToSearchSend = onNavigateToSearchSend, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreen.kt index 48e7114e2..a8594bb42 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreen.kt @@ -39,6 +39,7 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.BitwardenSearchActionItem import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData +import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager import com.x8bit.bitwarden.ui.tools.feature.send.handlers.SendHandlers @@ -47,7 +48,7 @@ import kotlinx.collections.immutable.persistentListOf /** * UI for the send screen. */ -@Suppress("LongMethod") +@Suppress("LongMethod", "CyclomaticComplexMethod") @OptIn(ExperimentalMaterial3Api::class) @Composable fun SendScreen( @@ -55,6 +56,7 @@ fun SendScreen( onNavigateToEditSend: (sendItemId: String) -> Unit, onNavigateToSendFilesList: () -> Unit, onNavigateToSendTextList: () -> Unit, + onNavigateToSearchSend: (searchType: SearchType.Sends) -> Unit, viewModel: SendViewModel = hiltViewModel(), intentManager: IntentManager = LocalIntentManager.current, ) { @@ -72,6 +74,8 @@ fun SendScreen( when (event) { is SendEvent.DismissPullToRefresh -> pullToRefreshState?.endRefresh() + is SendEvent.NavigateToSearch -> onNavigateToSearchSend(SearchType.Sends.All) + is SendEvent.NavigateNewSend -> onNavigateToAddSend() is SendEvent.NavigateToEditSend -> onNavigateToEditSend(event.sendId) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModel.kt index c8b9173f4..1da20eced 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModel.kt @@ -233,8 +233,7 @@ class SendViewModel @Inject constructor( } private fun handleSearchClick() { - // TODO: navigate to send search BIT-594 - sendEvent(SendEvent.ShowToast("Search Not Implemented".asText())) + sendEvent(SendEvent.NavigateToSearch) } private fun handleSyncClick() { @@ -566,6 +565,11 @@ sealed class SendEvent { */ data object NavigateToFileSends : SendEvent() + /** + * Navigate to the send search screen. + */ + data object NavigateToSearch : SendEvent() + /** * Navigate to the send text screen. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt index 584749552..6f18d750b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt @@ -8,6 +8,7 @@ import androidx.navigation.NavType import androidx.navigation.navArgument import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions +import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType private const val CARD: String = "card" @@ -54,6 +55,7 @@ fun NavGraphBuilder.vaultItemListingDestination( onNavigateBack: () -> Unit, onNavigateToVaultItemScreen: (id: String) -> Unit, onNavigateToVaultAddItemScreen: () -> Unit, + onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit, ) { internalVaultItemListingDestination( route = VAULT_ITEM_LISTING_ROUTE, @@ -62,6 +64,7 @@ fun NavGraphBuilder.vaultItemListingDestination( onNavigateToEditSendItem = { }, onNavigateToVaultAddItemScreen = onNavigateToVaultAddItemScreen, onNavigateToVaultItemScreen = onNavigateToVaultItemScreen, + onNavigateToSearch = { onNavigateToSearchVault(it as SearchType.Vault) }, ) } @@ -72,6 +75,7 @@ fun NavGraphBuilder.sendItemListingDestination( onNavigateBack: () -> Unit, onNavigateToAddSendItem: () -> Unit, onNavigateToEditSendItem: (sendId: String) -> Unit, + onNavigateToSearchSend: (searchType: SearchType.Sends) -> Unit, ) { internalVaultItemListingDestination( route = SEND_ITEM_LISTING_ROUTE, @@ -80,6 +84,7 @@ fun NavGraphBuilder.sendItemListingDestination( onNavigateToEditSendItem = onNavigateToEditSendItem, onNavigateToVaultAddItemScreen = { }, onNavigateToVaultItemScreen = { }, + onNavigateToSearch = { onNavigateToSearchSend(it as SearchType.Sends) }, ) } @@ -94,6 +99,7 @@ private fun NavGraphBuilder.internalVaultItemListingDestination( onNavigateToVaultAddItemScreen: () -> Unit, onNavigateToAddSendItem: () -> Unit, onNavigateToEditSendItem: (sendId: String) -> Unit, + onNavigateToSearch: (searchType: SearchType) -> Unit, ) { composableWithPushTransitions( route = route, @@ -117,6 +123,7 @@ private fun NavGraphBuilder.internalVaultItemListingDestination( onNavigateToVaultAddItemScreen = onNavigateToVaultAddItemScreen, onNavigateToAddSendItem = onNavigateToAddSendItem, onNavigateToEditSendItem = onNavigateToEditSendItem, + onNavigateToSearch = onNavigateToSearch, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt index 7eae8de00..0bf2e809f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt @@ -35,6 +35,7 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenSearchActionItem import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData +import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager import com.x8bit.bitwarden.ui.vault.feature.itemlisting.handlers.VaultItemListingHandlers @@ -51,6 +52,7 @@ fun VaultItemListingScreen( onNavigateToVaultAddItemScreen: () -> Unit, onNavigateToAddSendItem: () -> Unit, onNavigateToEditSendItem: (sendId: String) -> Unit, + onNavigateToSearch: (searchType: SearchType) -> Unit, intentManager: IntentManager = LocalIntentManager.current, viewModel: VaultItemListingViewModel = hiltViewModel(), ) { @@ -94,11 +96,8 @@ fun VaultItemListingScreen( onNavigateToEditSendItem(event.id) } - is VaultItemListingEvent.NavigateToVaultSearchScreen -> { - // TODO Create vault search screen and navigation implementation BIT-213 - Toast - .makeText(context, "Navigate to the vault search screen.", Toast.LENGTH_SHORT) - .show() + is VaultItemListingEvent.NavigateToSearchScreen -> { + onNavigateToSearch(event.searchType) } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt index 2e20ff3fe..ba8b5d90b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt @@ -20,9 +20,11 @@ import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.concat import com.x8bit.bitwarden.ui.platform.components.model.IconData import com.x8bit.bitwarden.ui.platform.components.model.IconRes +import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.determineListingPredicate import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toItemListingType +import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toSearchType import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toViewState import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.updateWithAdditionalDataIfNecessary import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType @@ -207,7 +209,9 @@ class VaultItemListingViewModel @Inject constructor( private fun handleSearchIconClick() { sendEvent( - event = VaultItemListingEvent.NavigateToVaultSearchScreen, + event = VaultItemListingEvent.NavigateToSearchScreen( + searchType = state.itemListingType.toSearchType(), + ), ) } @@ -693,9 +697,11 @@ sealed class VaultItemListingEvent { data class NavigateToVaultItem(val id: String) : VaultItemListingEvent() /** - * Navigates to the VaultSearchScreen. + * Navigates to the SearchScreen with the given type filter. */ - data object NavigateToVaultSearchScreen : VaultItemListingEvent() + data class NavigateToSearchScreen( + val searchType: SearchType, + ) : VaultItemListingEvent() /** * Show a share sheet with the given content. diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensions.kt new file mode 100644 index 000000000..65f2b3e7c --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensions.kt @@ -0,0 +1,28 @@ +package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util + +import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType +import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState + +/** + * Transforms a [VaultItemListingState.ItemListingType] into a [SearchType]. + */ +fun VaultItemListingState.ItemListingType.toSearchType(): SearchType = + when (this) { + is VaultItemListingState.ItemListingType.Vault.Card -> SearchType.Vault.Cards + is VaultItemListingState.ItemListingType.Vault.Folder -> { + folderId + ?.let { SearchType.Vault.Folder(folderId = it) } + ?: SearchType.Vault.NoFolder + } + + is VaultItemListingState.ItemListingType.Vault.Identity -> SearchType.Vault.Identities + is VaultItemListingState.ItemListingType.Vault.Login -> SearchType.Vault.Logins + is VaultItemListingState.ItemListingType.Vault.SecureNote -> SearchType.Vault.SecureNotes + is VaultItemListingState.ItemListingType.Vault.Trash -> SearchType.Vault.Trash + is VaultItemListingState.ItemListingType.Vault.Collection -> { + SearchType.Vault.Collection(collectionId = collectionId) + } + + is VaultItemListingState.ItemListingType.Send.SendFile -> SearchType.Sends.Files + is VaultItemListingState.ItemListingType.Send.SendText -> SearchType.Sends.Texts + } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt index 7db970106..ce5d05d3e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt @@ -4,6 +4,7 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.navigation +import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.vault.feature.itemlisting.navigateToVaultItemListing import com.x8bit.bitwarden.ui.vault.feature.itemlisting.vaultItemListingDestination import com.x8bit.bitwarden.ui.vault.feature.verificationcode.navigateToVerificationCodeScreen @@ -14,11 +15,13 @@ const val VAULT_GRAPH_ROUTE: String = "vault_graph" /** * Add vault destinations to the nav graph. */ +@Suppress("LongParameterList") fun NavGraphBuilder.vaultGraph( navController: NavController, onNavigateToVaultAddItemScreen: () -> Unit, onNavigateToVaultItemScreen: (vaultItemId: String) -> Unit, onNavigateToVaultEditItemScreen: (vaultItemId: String) -> Unit, + onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit, onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit, ) { navigation( @@ -33,13 +36,14 @@ fun NavGraphBuilder.vaultGraph( onNavigateToVerificationCodeScreen = { navController.navigateToVerificationCodeScreen() }, - + onNavigateToSearchVault = onNavigateToSearchVault, onDimBottomNavBarRequest = onDimBottomNavBarRequest, ) vaultItemListingDestination( onNavigateBack = { navController.popBackStack() }, onNavigateToVaultItemScreen = onNavigateToVaultItemScreen, onNavigateToVaultAddItemScreen = onNavigateToVaultAddItemScreen, + onNavigateToSearchVault = onNavigateToSearchVault, ) vaultVerificationCodeDestination( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNavigation.kt index 16cdd66e3..94ddeb27d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNavigation.kt @@ -4,6 +4,7 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.x8bit.bitwarden.ui.platform.base.util.composableWithRootPushTransitions +import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType const val VAULT_ROUTE: String = "vault" @@ -18,6 +19,7 @@ fun NavGraphBuilder.vaultDestination( onNavigateToVaultItemScreen: (vaultItemId: String) -> Unit, onNavigateToVaultEditItemScreen: (vaultItemId: String) -> Unit, onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit, + onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit, onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit, ) { composableWithRootPushTransitions( @@ -29,6 +31,7 @@ fun NavGraphBuilder.vaultDestination( onNavigateToVaultEditItemScreen = onNavigateToVaultEditItemScreen, onNavigateToVaultItemListingScreen = onNavigateToVaultItemListingScreen, onNavigateToVerificationCodeScreen = onNavigateToVerificationCodeScreen, + onNavigateToSearchVault = onNavigateToSearchVault, onDimBottomNavBarRequest = onDimBottomNavBarRequest, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt index 981afd578..bda131829 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt @@ -51,6 +51,7 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary +import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager import com.x8bit.bitwarden.ui.platform.theme.LocalExitManager import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType @@ -71,6 +72,7 @@ fun VaultScreen( onNavigateToVaultEditItemScreen: (vaultItemId: String) -> Unit, onNavigateToVerificationCodeScreen: () -> Unit, onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit, + onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit, onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit, exitManager: ExitManager = LocalExitManager.current, ) { @@ -88,12 +90,7 @@ fun VaultScreen( VaultEvent.NavigateToAddItemScreen -> onNavigateToVaultAddItemScreen() - VaultEvent.NavigateToVaultSearchScreen -> { - // TODO Create vault search screen and navigation implementation BIT-213 - Toast - .makeText(context, "Navigate to the vault search screen.", Toast.LENGTH_SHORT) - .show() - } + VaultEvent.NavigateToVaultSearchScreen -> onNavigateToSearchVault(SearchType.Vault.All) is VaultEvent.NavigateToVerificationCodeScreen -> { onNavigateToVerificationCodeScreen() diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreenTest.kt new file mode 100644 index 000000000..f08ce8b35 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreenTest.kt @@ -0,0 +1,42 @@ +package com.x8bit.bitwarden.ui.platform.feature.search + +import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow +import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest +import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class SearchScreenTest : BaseComposeTest() { + private val mutableEventFlow = bufferedMutableSharedFlow() + private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) + private val viewModel = mockk { + every { eventFlow } returns mutableEventFlow + every { stateFlow } returns mutableStateFlow + } + + private var onNavigateBackCalled = false + + @Before + fun setup() { + composeTestRule.setContent { + SearchScreen( + viewModel = viewModel, + onNavigateBack = { onNavigateBackCalled = true }, + ) + } + } + + @Test + fun `NavigateBack should call onNavigateBack`() { + mutableEventFlow.tryEmit(SearchEvent.NavigateBack) + assertTrue(onNavigateBackCalled) + } +} + +private val DEFAULT_STATE: SearchState = SearchState( + searchType = SearchType.Vault.All, +) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt new file mode 100644 index 000000000..206558c47 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt @@ -0,0 +1,83 @@ +package com.x8bit.bitwarden.ui.platform.feature.search + +import androidx.lifecycle.SavedStateHandle +import app.cash.turbine.test +import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest +import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class SearchViewModelTest : BaseViewModelTest() { + + @Test + fun `initial state should be correct when not set`() { + val viewModel = createViewModel(initialState = null) + assertEquals(DEFAULT_STATE, viewModel.stateFlow.value) + } + + @Test + fun `initial state should be correct when set`() { + val state = DEFAULT_STATE.copy(searchType = SearchType.Sends.All) + val viewModel = createViewModel(initialState = state) + assertEquals(state, viewModel.stateFlow.value) + } + + @Test + fun `BackClick should emit NavigateBack`() = runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(SearchAction.BackClick) + assertEquals(SearchEvent.NavigateBack, awaitItem()) + } + } + + @Suppress("CyclomaticComplexMethod") + private fun createViewModel( + initialState: SearchState? = null, + ): SearchViewModel = SearchViewModel( + SavedStateHandle().apply { + set("state", initialState) + set( + "search_type", + when (initialState?.searchType) { + SearchType.Sends.All -> "search_type_sends_all" + SearchType.Sends.Files -> "search_type_sends_file" + SearchType.Sends.Texts -> "search_type_sends_text" + SearchType.Vault.All -> "search_type_vault_all" + SearchType.Vault.Cards -> "search_type_vault_cards" + is SearchType.Vault.Collection -> "search_type_vault_collection" + is SearchType.Vault.Folder -> "search_type_vault_folder" + SearchType.Vault.Identities -> "search_type_vault_identities" + SearchType.Vault.Logins -> "search_type_vault_logins" + SearchType.Vault.NoFolder -> "search_type_vault_no_folder" + SearchType.Vault.SecureNotes -> "search_type_vault_secure_notes" + SearchType.Vault.Trash -> "search_type_vault_trash" + null -> "search_type_vault_all" + }, + ) + set( + "search_type_id", + when (val searchType = initialState?.searchType) { + SearchType.Sends.All -> null + SearchType.Sends.Files -> null + SearchType.Sends.Texts -> null + SearchType.Vault.All -> null + SearchType.Vault.Cards -> null + is SearchType.Vault.Collection -> searchType.collectionId + is SearchType.Vault.Folder -> searchType.folderId + SearchType.Vault.Identities -> null + SearchType.Vault.Logins -> null + SearchType.Vault.NoFolder -> null + SearchType.Vault.SecureNotes -> null + SearchType.Vault.Trash -> null + null -> null + }, + ) + }, + ) +} + +private val DEFAULT_STATE: SearchState = SearchState( + searchType = SearchType.Vault.All, +) 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 591523443..297d5c349 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 @@ -47,6 +47,8 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { onNavigateToDeleteAccount = {}, onNavigateToFolders = {}, onNavigateToPasswordHistory = {}, + onNavigateToSearchVault = {}, + onNavigateToSearchSend = {}, ) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreenTest.kt index 501267392..b83da5441 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreenTest.kt @@ -41,6 +41,7 @@ class SendScreenTest : BaseComposeTest() { private var onNavigateToNewSendCalled = false private var onNavigateToSendFilesListCalled = false private var onNavigateToSendTextListCalled = false + private var onNavigateToSendSearchCalled = false private var onNavigateToEditSendId: String? = null private val intentManager = mockk { @@ -63,6 +64,7 @@ class SendScreenTest : BaseComposeTest() { onNavigateToEditSend = { onNavigateToEditSendId = it }, onNavigateToSendFilesList = { onNavigateToSendFilesListCalled = true }, onNavigateToSendTextList = { onNavigateToSendTextListCalled = true }, + onNavigateToSearchSend = { onNavigateToSendSearchCalled = true }, intentManager = intentManager, ) } @@ -93,6 +95,12 @@ class SendScreenTest : BaseComposeTest() { assertTrue(onNavigateToSendTextListCalled) } + @Test + fun `on NavigateToSearch should call onNavigateToSendSearch`() { + mutableEventFlow.tryEmit(SendEvent.NavigateToSearch) + assertTrue(onNavigateToSendSearchCalled) + } + @Test fun `on NavigateToAboutSend should call launchUri on intentManager`() { mutableEventFlow.tryEmit(SendEvent.NavigateToAboutSend) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModelTest.kt index af46f576c..0e593f0d5 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModelTest.kt @@ -108,11 +108,11 @@ class SendViewModelTest : BaseViewModelTest() { } @Test - fun `SearchClick should emit ShowToast`() = runTest { + fun `SearchClick should emit NavigateToSearch`() = runTest { val viewModel = createViewModel() viewModel.eventFlow.test { viewModel.trySendAction(SendAction.SearchClick) - assertEquals(SendEvent.ShowToast("Search Not Implemented".asText()), awaitItem()) + assertEquals(SendEvent.NavigateToSearch, awaitItem()) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt index 466093419..ecfb7dad9 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt @@ -26,6 +26,7 @@ import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.components.model.IconData import com.x8bit.bitwarden.ui.platform.components.model.IconRes +import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.util.assertNoDialogExists import com.x8bit.bitwarden.ui.util.isProgressBar @@ -50,6 +51,7 @@ class VaultItemListingScreenTest : BaseComposeTest() { private var onNavigateToAddSendScreenCalled = false private var onNavigateToEditSendItemId: String? = null private var onNavigateToVaultItemId: String? = null + private var onNavigateToSearchType: SearchType? = null private val intentManager: IntentManager = mockk { every { shareText(any()) } just runs @@ -72,6 +74,7 @@ class VaultItemListingScreenTest : BaseComposeTest() { onNavigateToVaultAddItemScreen = { onNavigateToVaultAddItemScreenCalled = true }, onNavigateToAddSendItem = { onNavigateToAddSendScreenCalled = true }, onNavigateToEditSendItem = { onNavigateToEditSendItemId = it }, + onNavigateToSearch = { onNavigateToSearchType = it }, ) } } @@ -147,6 +150,13 @@ class VaultItemListingScreenTest : BaseComposeTest() { assertTrue(onNavigateToAddSendScreenCalled) } + @Test + fun `NavigateToVaultSearchScreen should call onNavigateToSearch`() { + val searchType = SearchType.Vault.SecureNotes + mutableEventFlow.tryEmit(VaultItemListingEvent.NavigateToSearchScreen(searchType)) + assertEquals(searchType, onNavigateToSearchType) + } + @Test fun `NavigateToSendItem event should call onNavigateToEditSendItemId`() { val sendId = "sendId" diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt index c4718dbe6..8368c38c9 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt @@ -22,6 +22,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.concat +import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.createMockDisplayItemForCipher import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType @@ -106,10 +107,11 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { @Test fun `SearchIconClick should emit NavigateToVaultSearchScreen`() = runTest { + val searchType = SearchType.Vault.Logins val viewModel = createVaultItemListingViewModel() viewModel.eventFlow.test { viewModel.actionChannel.trySend(VaultItemListingsAction.SearchIconClick) - assertEquals(VaultItemListingEvent.NavigateToVaultSearchScreen, awaitItem()) + assertEquals(VaultItemListingEvent.NavigateToSearchScreen(searchType), awaitItem()) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensionsTest.kt new file mode 100644 index 000000000..b5499806c --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensionsTest.kt @@ -0,0 +1,111 @@ +package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util + +import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType +import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class VaultItemListingStateExtensionsTest { + + @Test + fun `toSearchType should return Texts when item type is SendText`() { + val expected = SearchType.Sends.Texts + val itemType = VaultItemListingState.ItemListingType.Send.SendText + + val result = itemType.toSearchType() + + assertEquals(expected, result) + } + + @Test + fun `toSearchType should return Files when item type is SendFile`() { + val expected = SearchType.Sends.Files + val itemType = VaultItemListingState.ItemListingType.Send.SendFile + + val result = itemType.toSearchType() + + assertEquals(expected, result) + } + + @Test + fun `toSearchType should return Logins when item type is Login`() { + val expected = SearchType.Vault.Logins + val itemType = VaultItemListingState.ItemListingType.Vault.Login + + val result = itemType.toSearchType() + + assertEquals(expected, result) + } + + @Test + fun `toSearchType should return Card when item type is Card`() { + val expected = SearchType.Vault.Cards + val itemType = VaultItemListingState.ItemListingType.Vault.Card + + val result = itemType.toSearchType() + + assertEquals(expected, result) + } + + @Test + fun `toSearchType should return Identities when item type is Identity`() { + val expected = SearchType.Vault.Identities + val itemType = VaultItemListingState.ItemListingType.Vault.Identity + + val result = itemType.toSearchType() + + assertEquals(expected, result) + } + + @Test + fun `toSearchType should return SecureNotes when item type is SecureNote`() { + val expected = SearchType.Vault.SecureNotes + val itemType = VaultItemListingState.ItemListingType.Vault.SecureNote + + val result = itemType.toSearchType() + + assertEquals(expected, result) + } + + @Test + fun `toSearchType should return Trash when item type is Trash`() { + val expected = SearchType.Vault.Trash + val itemType = VaultItemListingState.ItemListingType.Vault.Trash + + val result = itemType.toSearchType() + + assertEquals(expected, result) + } + + @Test + fun `toSearchType should return Folder when item type is Folder with an ID`() { + val folderId = "folderId" + val expected = SearchType.Vault.Folder(folderId) + val itemType = VaultItemListingState.ItemListingType.Vault.Folder(folderId) + + val result = itemType.toSearchType() + + assertEquals(expected, result) + } + + @Test + fun `toSearchType should return NoFolder when item type is Folder without an ID`() { + val expected = SearchType.Vault.NoFolder + val itemType = VaultItemListingState.ItemListingType.Vault.Folder(null) + + val result = itemType.toSearchType() + + assertEquals(expected, result) + } + + @Test + fun `toSearchType should return Collection when item type is Collection`() { + val collectionId = "collectionId" + val expected = SearchType.Vault.Collection(collectionId) + val itemType = VaultItemListingState.ItemListingType.Vault.Collection(collectionId) + + val result = itemType.toSearchType() + + assertEquals(expected, result) + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt index 878d05bb1..cdd42f9a6 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt @@ -61,6 +61,7 @@ class VaultScreenTest : BaseComposeTest() { private var onNavigateToVaultItemListingType: VaultItemListingType? = null private var onDimBottomNavBarRequestCalled = false private var onNavigateToVerificationCodeScreen = false + private var onNavigateToSearchScreen = false private val exitManager = mockk(relaxed = true) private val mutableEventFlow = bufferedMutableSharedFlow() @@ -81,6 +82,7 @@ class VaultScreenTest : BaseComposeTest() { onNavigateToVaultItemListingScreen = { onNavigateToVaultItemListingType = it }, onDimBottomNavBarRequest = { onDimBottomNavBarRequestCalled = true }, onNavigateToVerificationCodeScreen = { onNavigateToVerificationCodeScreen = true }, + onNavigateToSearchVault = { onNavigateToSearchScreen = true }, exitManager = exitManager, ) } @@ -586,6 +588,12 @@ class VaultScreenTest : BaseComposeTest() { assertTrue(onNavigateToVaultAddItemScreenCalled) } + @Test + fun `NavigateToVaultSearchScreen event should call onNavigateToSearchScreen`() { + mutableEventFlow.tryEmit(VaultEvent.NavigateToVaultSearchScreen) + assertTrue(onNavigateToSearchScreen) + } + @Test fun `NavigateToVaultItem event should call onNavigateToVaultItemScreen`() { val id = "id4321"