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 1d0d4c63f..cee74237e 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 @@ -10,8 +10,7 @@ import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.vaultUnlocked const val VAULT_UNLOCKED_ROUTE: String = "VaultUnlocked" /** - * Navigate to the vault unlocked screen. Note this will only work if vault unlocked destinations were added - * via [vaultUnlockedDestinations]. + * Navigate to the vault unlocked screen. */ fun NavController.navigateToVaultUnlocked(navOptions: NavOptions? = null) { navigate(VAULT_UNLOCKED_ROUTE, navOptions) 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 d3ef21bf1..826dc8d6c 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,7 +4,6 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable -import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.vaultUnlockedDestinations /** * The functions below pertain to entry into the [VaultUnlockedNavBarScreen]. @@ -12,9 +11,7 @@ import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.vaultUnlockedDestin const val VAULT_UNLOCKED_NAV_BAR_ROUTE: String = "VaultUnlockedNavBar" /** - * Navigate to the vault unlocked nav bar screen. - * Note this will only work if vault unlocked nav bar destination was added - * via [vaultUnlockedDestinations]. + * Navigate to the [VaultUnlockedNavBarScreen]. */ fun NavController.navigateToVaultUnlockedNavBar(navOptions: NavOptions? = null) { navigate(VAULT_UNLOCKED_NAV_BAR_ROUTE, navOptions) 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 b3c4661a1..b0a26bd6c 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 @@ -1,4 +1,3 @@ -@file:Suppress("TooManyFunctions") package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar import android.os.Parcelable @@ -14,12 +13,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController -import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.NavOptions @@ -30,6 +27,9 @@ import androidx.navigation.navOptions import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.components.PlaceholderComposable +import com.x8bit.bitwarden.ui.vault.feature.vault.VAULT_ROUTE +import com.x8bit.bitwarden.ui.vault.feature.vault.navigateToVault +import com.x8bit.bitwarden.ui.vault.feature.vault.vaultDestination import kotlinx.parcelize.Parcelize /** @@ -44,7 +44,7 @@ fun VaultUnlockedNavBarScreen( navController.apply { val navOptions = vaultUnlockedNavBarScreenNavOptions() when (event) { - VaultUnlockedNavBarEvent.NavigateToVaultScreenNavBar -> navigateToVault(navOptions) + VaultUnlockedNavBarEvent.NavigateToVaultScreen -> navigateToVault(navOptions) VaultUnlockedNavBarEvent.NavigateToSendScreen -> navigateToSend(navOptions) VaultUnlockedNavBarEvent.NavigateToGeneratorScreen -> navigateToGenerator(navOptions) VaultUnlockedNavBarEvent.NavigateToSettingsScreen -> navigateToSettings(navOptions) @@ -85,11 +85,12 @@ private fun VaultUnlockedNavBarScaffold( ) destinations.forEach { destination -> NavigationBarItem( - modifier = Modifier.testTag(destination.route), icon = { Icon( painter = painterResource(id = destination.iconRes), - contentDescription = stringResource(id = destination.contentDescriptionRes), + contentDescription = stringResource( + id = destination.contentDescriptionRes, + ), ) }, label = { @@ -198,7 +199,7 @@ private sealed class VaultUnlockedNavBarTab : Parcelable { */ private fun NavController.vaultUnlockedNavBarScreenNavOptions(): NavOptions = navOptions { - popUpTo(graph.findStartDestination().id) { + popUpTo(graph.startDestinationId) { saveState = true } launchSingleTop = true @@ -297,32 +298,3 @@ private fun NavController.navigateToSettings(navOptions: NavOptions? = null) { navigate(SETTINGS_ROUTE, navOptions) } // #endregion Settings - -// #region Vault -/** - * TODO: move to vault package (BIT-178) - */ -private const val VAULT_ROUTE = "vault" - -/** - * Add vault destination to the nav graph. - * - * TODO: move to vault package (BIT-178) - */ -private fun NavGraphBuilder.vaultDestination() { - composable(VAULT_ROUTE) { - PlaceholderComposable(text = "Vault") - } -} - -/** - * Navigate to the vault screen. Note this will only work if vault screen was added - * via [vaultDestination]. - * - * TODO: move to vault package (BIT-178) - * - */ -private fun NavController.navigateToVault(navOptions: NavOptions? = null) { - navigate(VAULT_ROUTE, navOptions) -} -// #endregion Vault diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModel.kt index 389108ac1..8276520d2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModel.kt @@ -37,10 +37,10 @@ class VaultUnlockedNavBarViewModel @Inject constructor() : } /** - * Attempts to send [VaultUnlockedNavBarEvent.NavigateToVaultScreenNavBar] event + * Attempts to send [VaultUnlockedNavBarEvent.NavigateToVaultScreen] event */ private fun handleVaultTabClicked() { - sendEvent(VaultUnlockedNavBarEvent.NavigateToVaultScreenNavBar) + sendEvent(VaultUnlockedNavBarEvent.NavigateToVaultScreen) } /** @@ -94,7 +94,7 @@ sealed class VaultUnlockedNavBarEvent { /** * Navigate to the Vault screen. */ - data object NavigateToVaultScreenNavBar : VaultUnlockedNavBarEvent() + data object NavigateToVaultScreen : VaultUnlockedNavBarEvent() /** * Navigate to the Settings screen. diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContentView.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContentView.kt new file mode 100644 index 000000000..6b178a225 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContentView.kt @@ -0,0 +1,31 @@ +package com.x8bit.bitwarden.ui.vault.feature.vault + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +/** + * Content view for the [VaultScreen]. + */ +@Composable +fun VaultContentView(paddingValues: PaddingValues) { + // TODO create proper VaultContentView in BIT-205 + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .padding(paddingValues), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text(text = "Vault Content View") + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultLoadingView.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultLoadingView.kt new file mode 100644 index 000000000..62d98ebec --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultLoadingView.kt @@ -0,0 +1,30 @@ +package com.x8bit.bitwarden.ui.vault.feature.vault + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +/** + * Loading view for the [VaultScreen]. + */ +@Composable +fun VaultLoadingView(paddingValues: PaddingValues) { + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .padding(paddingValues), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + CircularProgressIndicator() + } +} 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 new file mode 100644 index 000000000..519cfc47e --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNavigation.kt @@ -0,0 +1,24 @@ +package com.x8bit.bitwarden.ui.vault.feature.vault + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable + +const val VAULT_ROUTE: String = "vault" + +/** + * Add vault destination to the nav graph. + */ +fun NavGraphBuilder.vaultDestination() { + composable(VAULT_ROUTE) { + VaultScreen() + } +} + +/** + * Navigate to the [VaultScreen]. + */ +fun NavController.navigateToVault(navOptions: NavOptions? = null) { + navigate(VAULT_ROUTE, navOptions) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNoItemsView.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNoItemsView.kt new file mode 100644 index 000000000..466c2026f --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNoItemsView.kt @@ -0,0 +1,43 @@ +package com.x8bit.bitwarden.ui.vault.feature.vault + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.x8bit.bitwarden.R + +/** + * No items view for the [VaultScreen]. + */ +@Composable +fun VaultNoItemsView( + paddingValues: PaddingValues, + addItemClickAction: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier.padding(16.dp), + text = stringResource(id = R.string.no_items), + ) + Button( + modifier = Modifier.padding(16.dp), + onClick = addItemClickAction, + ) { + Text(text = stringResource(id = R.string.add_an_item)) + } + } +} 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 new file mode 100644 index 000000000..9ce4ce825 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt @@ -0,0 +1,105 @@ +package com.x8bit.bitwarden.ui.vault.feature.vault + +import android.widget.Toast +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandIn +import androidx.compose.animation.fadeIn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.IntSize +import androidx.hilt.navigation.compose.hiltViewModel +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect + +/** + * The vault screen for the application. + */ +@Composable +fun VaultScreen( + viewModel: VaultViewModel = hiltViewModel(), +) { + val context = LocalContext.current + EventsEffect(viewModel = viewModel) { event -> + when (event) { + VaultEvent.NavigateToAddItemScreen -> { + // TODO Create add item screen and navigation implementation BIT-207 + Toast.makeText(context, "Navigate to the add item screen.", Toast.LENGTH_SHORT) + .show() + } + + 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() + } + } + } + VaultScreenScaffold( + state = viewModel.stateFlow.collectAsState().value, + addItemClickAction = { viewModel.trySendAction(VaultAction.AddItemClick) }, + searchIconClickAction = { viewModel.trySendAction(VaultAction.SearchIconClick) }, + ) +} + +/** + * Scaffold for the [VaultScreen] + */ +@Composable +private fun VaultScreenScaffold( + state: VaultState, + addItemClickAction: () -> Unit, + searchIconClickAction: () -> Unit, +) { + // TODO Create account menu and logging in ability BIT-205 + var accountMenuVisible by rememberSaveable { + mutableStateOf(false) + } + Scaffold( + topBar = { + VaultTopBar( + accountIconClickAction = { accountMenuVisible = !accountMenuVisible }, + searchIconClickAction = searchIconClickAction, + ) + }, + floatingActionButton = { + AnimatedVisibility( + visible = !accountMenuVisible, + // The enter transition is required for AnimatedVisibility to work correctly on + // FloatingActionButton. See - https://issuetracker.google.com/issues/224005027?pli=1 + enter = fadeIn() + expandIn { IntSize(width = 1, height = 1) }, + ) { + FloatingActionButton( + containerColor = MaterialTheme.colorScheme.primary, + onClick = addItemClickAction, + ) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = stringResource(id = R.string.add_item), + tint = MaterialTheme.colorScheme.onPrimary, + ) + } + } + }, + ) { paddingValues -> + when (state) { + is VaultState.Content -> VaultContentView(paddingValues = paddingValues) + is VaultState.Loading -> VaultLoadingView(paddingValues = paddingValues) + is VaultState.NoItems -> VaultNoItemsView( + paddingValues = paddingValues, + addItemClickAction = addItemClickAction, + ) + } + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultTopBar.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultTopBar.kt new file mode 100644 index 000000000..56d77460b --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultTopBar.kt @@ -0,0 +1,89 @@ +package com.x8bit.bitwarden.ui.vault.feature.vault + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.x8bit.bitwarden.R + +/** + * The top bar for the [VaultScreen]. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun VaultTopBar( + accountIconClickAction: () -> Unit, + searchIconClickAction: () -> Unit, +) { + // TODO Create overflow menu and syncing BIT-217 + var overFlowMenuVisible by rememberSaveable { + mutableStateOf(false) + } + TopAppBar( + title = { + Text( + text = stringResource(id = R.string.my_vault), + modifier = Modifier.fillMaxWidth(), + ) + }, + actions = { + IconButton( + onClick = accountIconClickAction, + ) { + Icon( + imageVector = Icons.Default.AccountCircle, + contentDescription = stringResource(id = R.string.account), + tint = MaterialTheme.colorScheme.onPrimary, + ) + } + IconButton( + onClick = searchIconClickAction, + ) { + Icon( + imageVector = Icons.Default.Search, + contentDescription = stringResource(id = R.string.search_vault), + tint = MaterialTheme.colorScheme.onPrimary, + ) + } + Box { + IconButton( + onClick = { overFlowMenuVisible = !overFlowMenuVisible }, + ) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = stringResource(id = R.string.more), + tint = MaterialTheme.colorScheme.onPrimary, + ) + } + // TODO Create overflow menu and syncing BIT-217 + DropdownMenu( + expanded = overFlowMenuVisible, + onDismissRequest = { overFlowMenuVisible = false }, + ) { + Text(text = "PLACEHOLDER") + } + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primary, + titleContentColor = MaterialTheme.colorScheme.onPrimary, + ), + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt new file mode 100644 index 000000000..29b31a9c5 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt @@ -0,0 +1,95 @@ +package com.x8bit.bitwarden.ui.vault.feature.vault + +import androidx.lifecycle.viewModelScope +import com.x8bit.bitwarden.ui.platform.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * Manages [VaultState], handles [VaultAction], and launches [VaultEvent] for the [VaultScreen]. + */ +@HiltViewModel +class VaultViewModel @Inject constructor() : BaseViewModel( + initialState = VaultState.Loading, +) { + + init { + viewModelScope.launch { + // TODO will need to load actual vault items BIT-205 + @Suppress("MagicNumber") + delay(2000) + mutableStateFlow.update { VaultState.NoItems } + } + } + + override fun handleAction(action: VaultAction) { + when (action) { + VaultAction.AddItemClick -> handleAddItemClick() + VaultAction.SearchIconClick -> handleSearchIconClick() + } + } + + //region VaultAction Handlers + private fun handleAddItemClick() { + sendEvent(VaultEvent.NavigateToAddItemScreen) + } + + private fun handleSearchIconClick() { + sendEvent(VaultEvent.NavigateToVaultSearchScreen) + } + //endregion VaultAction Handlers +} + +/** + * Models state for the [VaultScreen]. + */ +sealed class VaultState { + /** + * Loading state for the [VaultScreen]. + */ + data object Loading : VaultState() + + /** + * No items state for the [VaultScreen]. + */ + data object NoItems : VaultState() + + /** + * Content state for the [VaultScreen]. + */ + data class Content(val itemList: List) : VaultState() +} + +/** + * Models effects for the [VaultScreen]. + */ +sealed class VaultEvent { + /** + * Navigate to the Vault Search screen. + */ + data object NavigateToVaultSearchScreen : VaultEvent() + + /** + * Navigate to the Add Item screen. + */ + data object NavigateToAddItemScreen : VaultEvent() +} + +/** + * Models actions for the [VaultScreen]. + */ +sealed class VaultAction { + /** + * Click the add an item button. + * This can either be the floating action button or actual add an item button. + */ + data object AddItemClick : VaultAction() + + /** + * Click the search icon. + */ + data object SearchIconClick : VaultAction() +} diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/FakeNavHostController.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/FakeNavHostController.kt index d3bfc3c37..5826e3f38 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/FakeNavHostController.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/FakeNavHostController.kt @@ -68,6 +68,7 @@ class FakeNavHostController : NavHostController(context = mockk()) { private val internalGraph = mockk().apply { every { id } returns graphId + every { startDestinationId } returns graphId } override var graph: NavGraph 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 e2873d328..cf75045d8 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 @@ -1,13 +1,28 @@ package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar -import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.navigation.navOptions import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest +import com.x8bit.bitwarden.ui.platform.base.FakeNavHostController +import io.mockk.every import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.flow.MutableSharedFlow import org.junit.Test class VaultUnlockedNavBarScreenTest : BaseComposeTest() { + private val fakeNavHostController = FakeNavHostController() + + private val expectedNavOptions = navOptions { + // When changing root navigation state, pop everything else off the back stack: + popUpTo(fakeNavHostController.graphId) { + inclusive = false + saveState = true + } + launchSingleTop = true + restoreState = true + } @Test fun `vault tab click should send VaultTabClick action`() { @@ -16,13 +31,40 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { setContent { VaultUnlockedNavBarScreen( viewModel = viewModel, + navController = fakeNavHostController, ) } - onNodeWithTag("vault").performClick() + 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, + ) + } + runOnIdle { fakeNavHostController.assertCurrentRoute("vault") } + vaultUnlockedNavBarEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToVaultScreen) + runOnIdle { + fakeNavHostController.assertLastNavigation( + route = "vault", + navOptions = expectedNavOptions, + ) + } + } + } + @Test fun `send tab click should send SendTabClick action`() { val viewModel = mockk(relaxed = true) @@ -30,13 +72,40 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { setContent { VaultUnlockedNavBarScreen( viewModel = viewModel, + navController = fakeNavHostController, ) } - onNodeWithTag("send").performClick() + 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, + ) + } + runOnIdle { fakeNavHostController.assertCurrentRoute("vault") } + vaultUnlockedNavBarEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToSendScreen) + runOnIdle { + fakeNavHostController.assertLastNavigation( + route = "send", + navOptions = expectedNavOptions, + ) + } + } + } + @Test fun `generator tab click should send GeneratorTabClick action`() { val viewModel = mockk(relaxed = true) @@ -44,13 +113,40 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { setContent { VaultUnlockedNavBarScreen( viewModel = viewModel, + navController = fakeNavHostController, ) } - onNodeWithTag("generator").performClick() + 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, + ) + } + runOnIdle { fakeNavHostController.assertCurrentRoute("vault") } + vaultUnlockedNavBarEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToGeneratorScreen) + runOnIdle { + fakeNavHostController.assertLastNavigation( + route = "generator", + navOptions = expectedNavOptions, + ) + } + } + } + @Test fun `settings tab click should send SendTabClick action`() { val viewModel = mockk(relaxed = true) @@ -58,10 +154,37 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { setContent { VaultUnlockedNavBarScreen( viewModel = viewModel, + navController = fakeNavHostController, ) } - onNodeWithTag("settings").performClick() + 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, + ) + } + runOnIdle { fakeNavHostController.assertCurrentRoute("vault") } + vaultUnlockedNavBarEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToSettingsScreen) + runOnIdle { + fakeNavHostController.assertLastNavigation( + route = "settings", + navOptions = expectedNavOptions, + ) + } + } + } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt index fb1e48461..02a53a060 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt @@ -12,7 +12,7 @@ class VaultUnlockedNavBarViewModelTest : BaseViewModelTest() { val viewModel = VaultUnlockedNavBarViewModel() viewModel.eventFlow.test { viewModel.trySendAction(VaultUnlockedNavBarAction.VaultTabClick) - assertEquals(VaultUnlockedNavBarEvent.NavigateToVaultScreenNavBar, awaitItem()) + assertEquals(VaultUnlockedNavBarEvent.NavigateToVaultScreen, awaitItem()) } } 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 new file mode 100644 index 000000000..3990b01d8 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt @@ -0,0 +1,66 @@ +package com.x8bit.bitwarden.ui.vault.feature.vault + +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.MutableStateFlow +import kotlinx.coroutines.flow.emptyFlow +import org.junit.Test + +class VaultScreenTest : BaseComposeTest() { + + @Test + fun `search icon click should send SearchIconClick action`() { + val viewModel = mockk(relaxed = true) { + every { eventFlow } returns emptyFlow() + every { stateFlow } returns MutableStateFlow(VaultState.NoItems) + } + composeTestRule.apply { + setContent { + VaultScreen( + viewModel = viewModel, + ) + } + onNodeWithContentDescription("Search vault").performClick() + } + verify { viewModel.trySendAction(VaultAction.SearchIconClick) } + } + + @Test + fun `floating action button click should send AddItemClick action`() { + val viewModel = mockk(relaxed = true) { + every { eventFlow } returns emptyFlow() + every { stateFlow } returns MutableStateFlow(VaultState.NoItems) + } + composeTestRule.apply { + setContent { + VaultScreen( + viewModel = viewModel, + ) + } + onNodeWithContentDescription("Add Item").performClick() + } + verify { viewModel.trySendAction(VaultAction.AddItemClick) } + } + + @Test + fun `add an item button click should send AddItemClick action`() { + val viewModel = mockk(relaxed = true) { + every { eventFlow } returns emptyFlow() + every { stateFlow } returns MutableStateFlow(VaultState.NoItems) + } + composeTestRule.apply { + setContent { + VaultScreen( + viewModel = viewModel, + ) + } + onNodeWithText("Add an Item").performClick() + } + verify { viewModel.trySendAction(VaultAction.AddItemClick) } + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt new file mode 100644 index 000000000..00c272026 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt @@ -0,0 +1,28 @@ +package com.x8bit.bitwarden.ui.vault.feature.vault + +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 VaultViewModelTest : BaseViewModelTest() { + + @Test + fun `AddItemClick should navigate to the add item screen`() = runTest { + val viewModel = VaultViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(VaultAction.AddItemClick) + assertEquals(VaultEvent.NavigateToAddItemScreen, awaitItem()) + } + } + + @Test + fun `SearchIconClick should navigate to the vault search screen`() = runTest { + val viewModel = VaultViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(VaultAction.SearchIconClick) + assertEquals(VaultEvent.NavigateToVaultSearchScreen, awaitItem()) + } + } +}