BIT-151: Added Wireframe for Vault Screen (#48)

This commit is contained in:
Ramsey Smith 2023-09-14 13:02:56 -06:00 committed by Álison Fernandes
parent f709e55ae6
commit 9d706121ed
16 changed files with 654 additions and 51 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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.

View file

@ -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")
}
}

View file

@ -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()
}
}

View file

@ -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)
}

View file

@ -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))
}
}
}

View file

@ -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,
)
}
}
}

View file

@ -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,
),
)
}

View file

@ -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<VaultState, VaultEvent, VaultAction>(
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<String>) : 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()
}

View file

@ -68,6 +68,7 @@ class FakeNavHostController : NavHostController(context = mockk()) {
private val internalGraph =
mockk<NavGraph>().apply {
every { id } returns graphId
every { startDestinationId } returns graphId
}
override var graph: NavGraph

View file

@ -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<VaultUnlockedNavBarEvent>(
extraBufferCapacity = Int.MAX_VALUE,
)
val viewModel = mockk<VaultUnlockedNavBarViewModel>(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<VaultUnlockedNavBarViewModel>(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<VaultUnlockedNavBarEvent>(
extraBufferCapacity = Int.MAX_VALUE,
)
val viewModel = mockk<VaultUnlockedNavBarViewModel>(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<VaultUnlockedNavBarViewModel>(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<VaultUnlockedNavBarEvent>(
extraBufferCapacity = Int.MAX_VALUE,
)
val viewModel = mockk<VaultUnlockedNavBarViewModel>(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<VaultUnlockedNavBarViewModel>(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<VaultUnlockedNavBarEvent>(
extraBufferCapacity = Int.MAX_VALUE,
)
val viewModel = mockk<VaultUnlockedNavBarViewModel>(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,
)
}
}
}
}

View file

@ -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())
}
}

View file

@ -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<VaultViewModel>(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<VaultViewModel>(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<VaultViewModel>(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) }
}
}

View file

@ -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())
}
}
}