diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ea5dabc93..b200d54a1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -3,6 +3,7 @@ plugins { alias(libs.plugins.detekt) alias(libs.plugins.hilt) alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.parcelize) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.ksp) kotlin("kapt") diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt index e9c176d39..931683497 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt @@ -14,6 +14,8 @@ import androidx.navigation.navOptions import com.x8bit.bitwarden.ui.auth.feature.auth.authDestinations import com.x8bit.bitwarden.ui.auth.feature.auth.navigateToAuth import com.x8bit.bitwarden.ui.platform.components.PlaceholderComposable +import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.navigateToVaultUnlocked +import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.vaultUnlockedDestinations /** * Controls root level [NavHost] for the app. @@ -31,6 +33,7 @@ fun RootNavScreen( ) { splashDestinations() authDestinations(navController) + vaultUnlockedDestinations() } // When state changes, navigate to different root navigation state @@ -43,6 +46,7 @@ fun RootNavScreen( when (state) { RootNavState.Auth -> navController.navigateToAuth(rootNavOptions) RootNavState.Splash -> navController.navigateToSplash(rootNavOptions) + RootNavState.VaultUnlocked -> navController.navigateToVaultUnlocked(rootNavOptions) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt index 70a4ecc4c..d8fc84095 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt @@ -31,6 +31,11 @@ class RootNavViewModel @Inject constructor() : * Models state of the root level navigation of the app. */ sealed class RootNavState { + /** + * Show the vault unlocked screen. + */ + data object VaultUnlocked : RootNavState() + /** * Show the auth screens. */ 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 new file mode 100644 index 000000000..7de7ed78b --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt @@ -0,0 +1,30 @@ +package com.x8bit.bitwarden.ui.platform.feature.vaultunlocked + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.navigation +import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VAULT_UNLOCKED_NAV_BAR_ROUTE +import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.vaultUnlockedNavBarDestination + +private const val VAULT_UNLOCKED_ROUTE = "VaultUnlocked" + +/** + * Navigate to the vault unlocked screen. Note this will only work if vault unlocked destinations were added + * via [vaultUnlockedDestinations]. + */ +fun NavController.navigateToVaultUnlocked(navOptions: NavOptions? = null) { + navigate(VAULT_UNLOCKED_ROUTE, navOptions) +} + +/** + * Add vault unlocked destinations to the root nav graph. + */ +fun NavGraphBuilder.vaultUnlockedDestinations() { + navigation( + startDestination = VAULT_UNLOCKED_NAV_BAR_ROUTE, + route = VAULT_UNLOCKED_ROUTE, + ) { + vaultUnlockedNavBarDestination() + } +} 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 new file mode 100644 index 000000000..d3ef21bf1 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt @@ -0,0 +1,30 @@ +package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar + +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]. + */ +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]. + */ +fun NavController.navigateToVaultUnlockedNavBar(navOptions: NavOptions? = null) { + navigate(VAULT_UNLOCKED_NAV_BAR_ROUTE, navOptions) +} + +/** + * Add vault unlocked destination to the root nav graph. + */ +fun NavGraphBuilder.vaultUnlockedNavBarDestination() { + composable(VAULT_UNLOCKED_NAV_BAR_ROUTE) { + VaultUnlockedNavBarScreen() + } +} 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 new file mode 100644 index 000000000..f5a949f9f --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt @@ -0,0 +1,327 @@ +package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar + +import android.os.Parcelable +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +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.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.NavOptions +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navOptions +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.base.util.EventsEffect +import com.x8bit.bitwarden.ui.components.PlaceholderComposable +import kotlinx.parcelize.Parcelize + +/** + * Top level composable for the Vault Unlocked Screen. + */ +@Composable +fun VaultUnlockedNavBarScreen( + viewModel: VaultUnlockedNavBarViewModel = viewModel(), + navController: NavHostController = rememberNavController(), +) { + EventsEffect(viewModel = viewModel) { event -> + navController.apply { + val navOptions = vaultUnlockedNavBarScreenNavOptions() + when (event) { + VaultUnlockedNavBarEvent.NavigateToVaultScreenNavBar -> navigateToVault(navOptions) + VaultUnlockedNavBarEvent.NavigateToSendScreen -> navigateToSend(navOptions) + VaultUnlockedNavBarEvent.NavigateToGeneratorScreen -> navigateToGenerator(navOptions) + VaultUnlockedNavBarEvent.NavigateToSettingsScreen -> navigateToSettings(navOptions) + } + } + } + VaultUnlockedNavBarScaffold( + navController = navController, + generatorTabClickedAction = { viewModel.trySendAction(VaultUnlockedNavBarAction.GeneratorTabClick) }, + sendTabClickedAction = { viewModel.trySendAction(VaultUnlockedNavBarAction.SendTabClick) }, + vaultTabClickedAction = { viewModel.trySendAction(VaultUnlockedNavBarAction.VaultTabClick) }, + settingsTabClickedAction = { viewModel.trySendAction(VaultUnlockedNavBarAction.SettingsTabClick) }, + ) +} + +/** + * Scaffold that contains the bottom nav bar for the [VaultUnlockedNavBarScreen] + */ +@Composable +private fun VaultUnlockedNavBarScaffold( + navController: NavHostController, + vaultTabClickedAction: () -> Unit, + sendTabClickedAction: () -> Unit, + generatorTabClickedAction: () -> Unit, + settingsTabClickedAction: () -> Unit, +) { + var state by rememberSaveable { + mutableStateOf(VaultUnlockedNavBarTab.Vault) + } + Scaffold( + bottomBar = { + BottomAppBar { + val destinations = listOf( + VaultUnlockedNavBarTab.Vault, + VaultUnlockedNavBarTab.Send, + VaultUnlockedNavBarTab.Generator, + VaultUnlockedNavBarTab.Settings, + ) + destinations.forEach { destination -> + NavigationBarItem( + modifier = Modifier.testTag(destination.route), + icon = { + Icon( + painter = painterResource(id = destination.iconRes), + contentDescription = stringResource(id = destination.contentDescriptionRes), + ) + }, + label = { + Text(text = stringResource(id = destination.labelRes)) + }, + selected = destination == state, + onClick = { + state = destination + when (destination) { + VaultUnlockedNavBarTab.Vault -> vaultTabClickedAction() + VaultUnlockedNavBarTab.Send -> sendTabClickedAction() + VaultUnlockedNavBarTab.Generator -> generatorTabClickedAction() + VaultUnlockedNavBarTab.Settings -> settingsTabClickedAction() + } + }, + ) + } + } + }, + ) { innerPadding -> + NavHost( + navController = navController, + startDestination = state.route, + modifier = Modifier.padding(innerPadding), + ) { + vaultDestination() + sendDestination() + generatorDestination() + settingsDestination() + } + } +} + +/** + * Models tabs for the nav bar of the vault unlocked portion of the app. + */ +@Parcelize +private sealed class VaultUnlockedNavBarTab : Parcelable { + /** + * Resource id for the icon representing the tab. + */ + abstract val iconRes: Int + + /** + * Resource id for the label describing the tab. + */ + abstract val labelRes: Int + + /** + * Resource id for the content description describing the tab. + */ + abstract val contentDescriptionRes: Int + + /** + * Route of the tab. + */ + abstract val route: String + + /** + * Show the Generator screen. + */ + @Parcelize + data object Generator : VaultUnlockedNavBarTab() { + override val iconRes get() = R.drawable.generator_icon + override val labelRes get() = R.string.generator_label + override val contentDescriptionRes get() = R.string.generator_tab_content_description + override val route get() = GENERATOR_ROUTE + } + + /** + * Show the Send screen. + */ + @Parcelize + data object Send : VaultUnlockedNavBarTab() { + override val iconRes get() = R.drawable.send_icon + override val labelRes get() = R.string.send_label + override val contentDescriptionRes get() = R.string.send_tab_content_description + override val route get() = SEND_ROUTE + } + + /** + * Show the Vault screen. + */ + @Parcelize + data object Vault : VaultUnlockedNavBarTab() { + override val iconRes get() = R.drawable.sheild_icon + override val labelRes get() = R.string.vault_label + override val contentDescriptionRes get() = R.string.vault_tab_content_description + override val route get() = VAULT_ROUTE + } + + /** + * Show the Settings screen. + */ + @Parcelize + data object Settings : VaultUnlockedNavBarTab() { + override val iconRes get() = R.drawable.settings_icon + override val labelRes get() = R.string.settings_label + override val contentDescriptionRes get() = R.string.settings_tab_content_description + override val route get() = SETTINGS_ROUTE + } +} + +/** + * Helper function to generate [NavOptions] for [VaultUnlockedNavBarScreen]. + */ +private fun NavController.vaultUnlockedNavBarScreenNavOptions(): NavOptions = + navOptions { + popUpTo(graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + +/** + * The functions below should be moved to their respective feature packages once they exist. + * + * For an example of how to setup these nav extensions, see NIA project. + */ + +// #region Generator +/** + * TODO: move to generator package (BIT-148) + */ +private const val GENERATOR_ROUTE = "generator" + +/** + * Add generator destination to the nav graph. + * + * TODO: move to generator package (BIT-148) + */ +private fun NavGraphBuilder.generatorDestination() { + composable(GENERATOR_ROUTE) { + PlaceholderComposable(text = "Generator") + } +} + +/** + * Navigate to the generator screen. Note this will only work if generator screen was added + * via [generatorDestination]. + * + * TODO: move to generator package (BIT-148) + * + */ +private fun NavController.navigateToGenerator(navOptions: NavOptions? = null) { + navigate(GENERATOR_ROUTE, navOptions) +} +// #endregion Generator + +// #region Send +/** + * TODO: move to send package (BIT-149) + */ +private const val SEND_ROUTE = "send" + +/** + * Add send destination to the nav graph. + * + * TODO: move to send package (BIT-149) + */ +private fun NavGraphBuilder.sendDestination() { + composable(SEND_ROUTE) { + PlaceholderComposable(text = "Send") + } +} + +/** + * Navigate to the send screen. Note this will only work if send screen was added + * via [sendDestination]. + * + * TODO: move to send package (BIT-149) + * + */ +private fun NavController.navigateToSend(navOptions: NavOptions? = null) { + navigate(SEND_ROUTE, navOptions) +} +// #endregion Send + +// #region Settings +/** + * TODO: move to settings package (BIT-147) + */ +private const val SETTINGS_ROUTE = "settings" + +/** + * Add settings destination to the nav graph. + * + * TODO: move to settings package (BIT-147) + */ +private fun NavGraphBuilder.settingsDestination() { + composable(SETTINGS_ROUTE) { + PlaceholderComposable(text = "Settings") + } +} + +/** + * Navigate to the generator screen. Note this will only work if generator screen was added + * via [settingsDestination]. + * + * TODO: move to settings package (BIT-147) + * + */ +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 new file mode 100644 index 000000000..b3518f2c3 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModel.kt @@ -0,0 +1,103 @@ +package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar + +import com.x8bit.bitwarden.ui.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +/** + * Manages bottom tab navigation of the application. + */ +@HiltViewModel +class VaultUnlockedNavBarViewModel @Inject constructor() : + BaseViewModel( + initialState = Unit, + ) { + + override fun handleAction(action: VaultUnlockedNavBarAction) { + when (action) { + VaultUnlockedNavBarAction.GeneratorTabClick -> handleGeneratorTabClicked() + VaultUnlockedNavBarAction.SendTabClick -> handleSendTabClicked() + VaultUnlockedNavBarAction.SettingsTabClick -> handleSettingsTabClicked() + VaultUnlockedNavBarAction.VaultTabClick -> handleVaultTabClicked() + } + } + // #region BottomTabViewModel Action Handlers + /** + * Attempts to send [VaultUnlockedNavBarEvent.NavigateToGeneratorScreen] event + */ + private fun handleGeneratorTabClicked() { + sendEvent(VaultUnlockedNavBarEvent.NavigateToGeneratorScreen) + } + + /** + * Attempts to send [VaultUnlockedNavBarEvent.NavigateToSendScreen] event + */ + private fun handleSendTabClicked() { + sendEvent(VaultUnlockedNavBarEvent.NavigateToSendScreen) + } + + /** + * Attempts to send [VaultUnlockedNavBarEvent.NavigateToVaultScreenNavBar] event + */ + private fun handleVaultTabClicked() { + sendEvent(VaultUnlockedNavBarEvent.NavigateToVaultScreenNavBar) + } + + /** + * Attempts to send [VaultUnlockedNavBarEvent.NavigateToSettingsScreen] event + */ + private fun handleSettingsTabClicked() { + sendEvent(VaultUnlockedNavBarEvent.NavigateToSettingsScreen) + } + // #endregion BottomTabViewModel Action Handlers +} + +/** + * Models actions for the bottom tab of the vault unlocked portion of the app. + */ +sealed class VaultUnlockedNavBarAction { + /** + * click Generator tab. + */ + data object GeneratorTabClick : VaultUnlockedNavBarAction() + + /** + * click Send tab. + */ + data object SendTabClick : VaultUnlockedNavBarAction() + + /** + * click Vault tab. + */ + data object VaultTabClick : VaultUnlockedNavBarAction() + + /** + * click Settings tab. + */ + data object SettingsTabClick : VaultUnlockedNavBarAction() +} + +/** + * Models events for the bottom tab of the vault unlocked portion of the app. + */ +sealed class VaultUnlockedNavBarEvent { + /** + * Navigate to the Generator screen. + */ + data object NavigateToGeneratorScreen : VaultUnlockedNavBarEvent() + + /** + * Navigate to the Send screen. + */ + data object NavigateToSendScreen : VaultUnlockedNavBarEvent() + + /** + * Navigate to the Vault screen. + */ + data object NavigateToVaultScreenNavBar : VaultUnlockedNavBarEvent() + + /** + * Navigate to the Settings screen. + */ + data object NavigateToSettingsScreen : VaultUnlockedNavBarEvent() +} diff --git a/app/src/main/res/drawable/generator_icon.xml b/app/src/main/res/drawable/generator_icon.xml new file mode 100644 index 000000000..8a6aa8028 --- /dev/null +++ b/app/src/main/res/drawable/generator_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/send_icon.xml b/app/src/main/res/drawable/send_icon.xml new file mode 100644 index 000000000..0329c660a --- /dev/null +++ b/app/src/main/res/drawable/send_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/settings_icon.xml b/app/src/main/res/drawable/settings_icon.xml new file mode 100644 index 000000000..f99bf396e --- /dev/null +++ b/app/src/main/res/drawable/settings_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/sheild_icon.xml b/app/src/main/res/drawable/sheild_icon.xml new file mode 100644 index 000000000..b6691cf56 --- /dev/null +++ b/app/src/main/res/drawable/sheild_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3c7189875..808d5b471 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,4 +17,14 @@ Log in or create a new account to access your secure vault. New around here? Remember me + + + Press to navigate to the generator screen. + Press to navigate to the send screen. + Press to navigate to the vault screen. + Press to navigate to the settings screen. + Vaults + Send + Generator + Settings diff --git a/app/src/test/java/com/x8bit/bitwarden/example/ui/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/example/ui/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt new file mode 100644 index 000000000..ac70aaa95 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/example/ui/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt @@ -0,0 +1,70 @@ +package com.x8bit.bitwarden.example.ui.feature.vaultunlockednavbar + +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import com.x8bit.bitwarden.example.ui.BaseComposeTest +import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VaultUnlockedNavBarAction +import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VaultUnlockedNavBarScreen +import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VaultUnlockedNavBarViewModel +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test + +class VaultUnlockedNavBarScreenTest : BaseComposeTest() { + + @Test + fun `vault tab click should send VaultTabClick action`() { + val viewModel = mockk(relaxed = true) + composeTestRule.apply { + setContent { + VaultUnlockedNavBarScreen( + viewModel = viewModel, + ) + } + onNodeWithTag("vault").performClick() + } + verify { viewModel.trySendAction(VaultUnlockedNavBarAction.VaultTabClick) } + } + + @Test + fun `send tab click should send SendTabClick action`() { + val viewModel = mockk(relaxed = true) + composeTestRule.apply { + setContent { + VaultUnlockedNavBarScreen( + viewModel = viewModel, + ) + } + onNodeWithTag("send").performClick() + } + verify { viewModel.trySendAction(VaultUnlockedNavBarAction.SendTabClick) } + } + + @Test + fun `generator tab click should send GeneratorTabClick action`() { + val viewModel = mockk(relaxed = true) + composeTestRule.apply { + setContent { + VaultUnlockedNavBarScreen( + viewModel = viewModel, + ) + } + onNodeWithTag("generator").performClick() + } + verify { viewModel.trySendAction(VaultUnlockedNavBarAction.GeneratorTabClick) } + } + + @Test + fun `settings tab click should send SendTabClick action`() { + val viewModel = mockk(relaxed = true) + composeTestRule.apply { + setContent { + VaultUnlockedNavBarScreen( + viewModel = viewModel, + ) + } + onNodeWithTag("settings").performClick() + } + verify { viewModel.trySendAction(VaultUnlockedNavBarAction.SettingsTabClick) } + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/example/ui/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/example/ui/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt new file mode 100644 index 000000000..39b47eeae --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/example/ui/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt @@ -0,0 +1,48 @@ +package com.x8bit.bitwarden.example.ui.feature.vaultunlockednavbar + +import app.cash.turbine.test +import com.x8bit.bitwarden.example.ui.BaseViewModelTest +import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VaultUnlockedNavBarAction +import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VaultUnlockedNavBarEvent +import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VaultUnlockedNavBarViewModel +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class VaultUnlockedNavBarViewModelTest : BaseViewModelTest() { + @Test + fun `VaultTabClick should navigate to the vault screen`() = runTest { + val viewModel = VaultUnlockedNavBarViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(VaultUnlockedNavBarAction.VaultTabClick) + assertEquals(VaultUnlockedNavBarEvent.NavigateToVaultScreenNavBar, awaitItem()) + } + } + + @Test + fun `SendTabClick should navigate to the send screen`() = runTest { + val viewModel = VaultUnlockedNavBarViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(VaultUnlockedNavBarAction.SendTabClick) + assertEquals(VaultUnlockedNavBarEvent.NavigateToSendScreen, awaitItem()) + } + } + + @Test + fun `GeneratorTabClick should navigate to the generator screen`() = runTest { + val viewModel = VaultUnlockedNavBarViewModel() + viewModel.eventFlow.test { + viewModel.actionChannel.trySend(VaultUnlockedNavBarAction.GeneratorTabClick) + assertEquals(VaultUnlockedNavBarEvent.NavigateToGeneratorScreen, awaitItem()) + } + } + + @Test + fun `SettingsTabClick should navigate to the settings screen`() = runTest { + val viewModel = VaultUnlockedNavBarViewModel() + viewModel.eventFlow.test { + viewModel.actionChannel.trySend(VaultUnlockedNavBarAction.SettingsTabClick) + assertEquals(VaultUnlockedNavBarEvent.NavigateToSettingsScreen, awaitItem()) + } + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 3f2fd1c38..a4ba7c013 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,5 +2,6 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.hilt) apply false alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.parcelize) apply false alias(libs.plugins.ksp) apply false } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 38a364d71..0d0489379 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -91,5 +91,6 @@ android-application = { id = "com.android.application", version.ref = "androidGr detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }