mirror of
https://github.com/bitwarden/android.git
synced 2024-11-21 17:05:44 +03:00
BIT-151: Added Wireframe for Vault Screen (#48)
This commit is contained in:
parent
f709e55ae6
commit
9d706121ed
16 changed files with 654 additions and 51 deletions
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue