BIT-457: Add Vault Settings and Folders screen UI (#457)

This commit is contained in:
Caleb Derosier 2023-12-29 14:11:41 -06:00 committed by Álison Fernandes
parent 78461394f3
commit c94b303abc
15 changed files with 530 additions and 164 deletions

View file

@ -27,6 +27,7 @@ private const val SETTINGS_ROUTE: String = "settings"
fun NavGraphBuilder.settingsGraph(
navController: NavController,
onNavigateToDeleteAccount: () -> Unit,
onNavigateToFolders: () -> Unit,
) {
navigation(
startDestination = SETTINGS_ROUTE,
@ -52,12 +53,15 @@ fun NavGraphBuilder.settingsGraph(
appearanceDestination(onNavigateBack = { navController.popBackStack() })
autoFillDestination(onNavigateBack = { navController.popBackStack() })
otherDestination(onNavigateBack = { navController.popBackStack() })
vaultSettingsDestination(onNavigateBack = { navController.popBackStack() })
vaultSettingsDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateToFolders = onNavigateToFolders,
)
}
}
/**
* Navigate to the settings screen screen.
* Navigate to the settings screen.
*/
fun NavController.navigateToSettingsGraph(navOptions: NavOptions? = null) {
navigate(SETTINGS_GRAPH_ROUTE, navOptions)

View file

@ -0,0 +1,30 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.folders
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
private const val FOLDERS_ROUTE = "settings_folders"
/**
* Add folders destinations to the nav graph.
*/
fun NavGraphBuilder.foldersDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(
route = FOLDERS_ROUTE,
) {
FoldersScreen(
onNavigateBack = onNavigateBack,
)
}
}
/**
* Navigate to the folders screen.
*/
fun NavController.navigateToFolders(navOptions: NavOptions? = null) {
navigate(FOLDERS_ROUTE, navOptions)
}

View file

@ -0,0 +1,108 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.folders
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
/**
* Displays the folders screen.
*/
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FoldersScreen(
onNavigateBack: () -> Unit,
viewModel: FoldersViewModel = hiltViewModel(),
) {
val context = LocalContext.current
EventsEffect(viewModel = viewModel) { event ->
when (event) {
FoldersEvent.NavigateBack -> onNavigateBack()
is FoldersEvent.ShowToast -> {
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
}
}
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
BitwardenScaffold(
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
BitwardenTopAppBar(
title = stringResource(id = R.string.folders),
scrollBehavior = scrollBehavior,
navigationIcon = painterResource(id = R.drawable.ic_close),
navigationIconContentDescription = stringResource(id = R.string.close),
onNavigationIconClick = remember(viewModel) {
{ viewModel.trySendAction(FoldersAction.CloseButtonClick) }
},
)
},
floatingActionButton = {
FloatingActionButton(
containerColor = MaterialTheme.colorScheme.primaryContainer,
onClick = remember(viewModel) {
{ viewModel.trySendAction(FoldersAction.AddFolderButtonClick) }
},
modifier = Modifier.navigationBarsPadding(),
) {
Icon(
painter = painterResource(id = R.drawable.ic_plus),
contentDescription = stringResource(id = R.string.add_item),
tint = MaterialTheme.colorScheme.onPrimaryContainer,
)
}
},
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.Center,
) {
// TODO BIT-460 populate Folders screen
Text(
text = stringResource(id = R.string.no_folders_to_list),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier
.fillMaxSize()
.padding(
vertical = 4.dp,
horizontal = 16.dp,
),
)
}
}
}

View file

@ -0,0 +1,60 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.folders
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
/**
* View model for the folders screen.
*/
@HiltViewModel
class FoldersViewModel @Inject constructor() :
BaseViewModel<Unit, FoldersEvent, FoldersAction>(
initialState = Unit,
) {
override fun handleAction(action: FoldersAction): Unit = when (action) {
FoldersAction.AddFolderButtonClick -> handleAddFolderButtonClicked()
FoldersAction.CloseButtonClick -> handleCloseButtonClicked()
}
private fun handleAddFolderButtonClicked() {
// TODO BIT-458 implement add folders
sendEvent(FoldersEvent.ShowToast("Not yet implemented."))
}
private fun handleCloseButtonClicked() {
sendEvent(FoldersEvent.NavigateBack)
}
}
/**
* Models events for the folders screen.
*/
sealed class FoldersEvent {
/**
* Navigates back to the previous screen.
*/
data object NavigateBack : FoldersEvent()
/**
* Shows a toast with the given [message].
*/
data class ShowToast(
val message: String,
) : FoldersEvent()
}
/**
* Models actions for the folders screen.
*/
sealed class FoldersAction {
/**
* Indicates that the user clicked the add folder button.
*/
data object AddFolderButtonClick : FoldersAction()
/**
* Indicates that the user clicked the close button.
*/
data object CloseButtonClick : FoldersAction()
}

View file

@ -12,11 +12,15 @@ private const val VAULT_SETTINGS_ROUTE = "vault_settings"
*/
fun NavGraphBuilder.vaultSettingsDestination(
onNavigateBack: () -> Unit,
onNavigateToFolders: () -> Unit,
) {
composableWithPushTransitions(
route = VAULT_SETTINGS_ROUTE,
) {
VaultSettingsScreen(onNavigateBack = onNavigateBack)
VaultSettingsScreen(
onNavigateBack = onNavigateBack,
onNavigateToFolders = onNavigateToFolders,
)
}
}

View file

@ -1,7 +1,9 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.vault
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@ -12,26 +14,36 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.components.BitwardenExternalLinkRow
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextRow
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
/**
* Displays the vault screen.
* Displays the vault settings screen.
*/
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VaultSettingsScreen(
onNavigateBack: () -> Unit,
onNavigateToFolders: () -> Unit,
viewModel: VaultSettingsViewModel = hiltViewModel(),
) {
val context = LocalContext.current
EventsEffect(viewModel = viewModel) { event ->
when (event) {
VaultSettingsEvent.NavigateBack -> onNavigateBack.invoke()
VaultSettingsEvent.NavigateBack -> onNavigateBack()
VaultSettingsEvent.NavigateToFolders -> onNavigateToFolders()
is VaultSettingsEvent.ShowToast -> {
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
}
}
}
@ -58,7 +70,34 @@ fun VaultSettingsScreen(
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
// TODO: BIT-928 Display Vault UI
BitwardenTextRow(
text = stringResource(R.string.folders),
onClick = remember(viewModel) {
{ viewModel.trySendAction(VaultSettingsAction.FoldersButtonClick) }
},
withDivider = true,
modifier = Modifier.fillMaxWidth(),
)
BitwardenTextRow(
text = stringResource(R.string.export_vault),
onClick = remember(viewModel) {
{ viewModel.trySendAction(VaultSettingsAction.ExportVaultClick) }
},
withDivider = true,
modifier = Modifier.fillMaxWidth(),
)
BitwardenExternalLinkRow(
text = stringResource(R.string.import_items),
onConfirmClick = remember(viewModel) {
{ viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) }
},
withDivider = true,
dialogTitle = stringResource(id = R.string.import_items_confirmation),
dialogMessage = stringResource(id = R.string.import_items_description),
modifier = Modifier.fillMaxWidth(),
)
}
}
}

View file

@ -13,7 +13,28 @@ class VaultSettingsViewModel @Inject constructor() :
initialState = Unit,
) {
override fun handleAction(action: VaultSettingsAction): Unit = when (action) {
VaultSettingsAction.BackClick -> sendEvent(VaultSettingsEvent.NavigateBack)
VaultSettingsAction.BackClick -> handleBackClicked()
VaultSettingsAction.ExportVaultClick -> handleExportVaultClicked()
VaultSettingsAction.FoldersButtonClick -> handleFoldersButtonClicked()
VaultSettingsAction.ImportItemsClick -> handleImportItemsClicked()
}
private fun handleBackClicked() {
sendEvent(VaultSettingsEvent.NavigateBack)
}
private fun handleExportVaultClicked() {
// TODO BIT-1272 go to vault export screen
sendEvent(VaultSettingsEvent.ShowToast("Not yet implemented."))
}
private fun handleFoldersButtonClicked() {
sendEvent(VaultSettingsEvent.NavigateToFolders)
}
private fun handleImportItemsClicked() {
// TODO BIT-972 implement import items functionality
sendEvent(VaultSettingsEvent.ShowToast("Not yet implemented."))
}
}
@ -25,6 +46,18 @@ sealed class VaultSettingsEvent {
* Navigate back.
*/
data object NavigateBack : VaultSettingsEvent()
/**
* Navigate to the Folders screen.
*/
data object NavigateToFolders : VaultSettingsEvent()
/**
* Shows a toast with the given [message].
*/
data class ShowToast(
val message: String,
) : VaultSettingsEvent()
}
/**
@ -35,4 +68,19 @@ sealed class VaultSettingsAction {
* User clicked back button.
*/
data object BackClick : VaultSettingsAction()
/**
* Indicates that the user clicked the Export Vault button.
*/
data object ExportVaultClick : VaultSettingsAction()
/**
* Indicates that the user clicked the Folders button.
*/
data object FoldersButtonClick : VaultSettingsAction()
/**
* Indicates that the user clicked the Import Items button.
*/
data object ImportItemsClick : VaultSettingsAction()
}

View file

@ -6,6 +6,8 @@ import androidx.navigation.NavOptions
import androidx.navigation.navigation
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount.deleteAccountDestination
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount.navigateToDeleteAccount
import com.x8bit.bitwarden.ui.platform.feature.settings.folders.foldersDestination
import com.x8bit.bitwarden.ui.platform.feature.settings.folders.navigateToFolders
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VAULT_UNLOCKED_NAV_BAR_ROUTE
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.vaultUnlockedNavBarDestination
import com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory.navigateToPasswordHistory
@ -38,6 +40,7 @@ fun NavGraphBuilder.vaultUnlockedGraph(
route = VAULT_UNLOCKED_GRAPH_ROUTE,
) {
vaultUnlockedNavBarDestination(
onNavigateToFolders = { navController.navigateToFolders() },
onNavigateToVaultAddItem = {
navController.navigateToVaultAddEditItem(VaultAddEditType.AddItem)
},
@ -59,5 +62,6 @@ fun NavGraphBuilder.vaultUnlockedGraph(
)
newSendDestination(onNavigateBack = { navController.popBackStack() })
passwordHistoryDestination(onNavigateBack = { navController.popBackStack() })
foldersDestination(onNavigateBack = { navController.popBackStack() })
}
}

View file

@ -27,6 +27,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination(
onNavigateToVaultEditItem: (vaultItemId: String) -> Unit,
onNavigateToNewSend: () -> Unit,
onNavigateToDeleteAccount: () -> Unit,
onNavigateToFolders: () -> Unit,
onNavigateToPasswordHistory: () -> Unit,
) {
composableWithStayTransitions(
@ -38,6 +39,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination(
onNavigateToVaultEditItem = onNavigateToVaultEditItem,
onNavigateToNewSend = onNavigateToNewSend,
onNavigateToDeleteAccount = onNavigateToDeleteAccount,
onNavigateToFolders = onNavigateToFolders,
onNavigateToPasswordHistory = onNavigateToPasswordHistory,
)
}

View file

@ -62,6 +62,7 @@ import kotlinx.parcelize.Parcelize
/**
* Top level composable for the Vault Unlocked Screen.
*/
@Suppress("LongParameterList")
@Composable
fun VaultUnlockedNavBarScreen(
viewModel: VaultUnlockedNavBarViewModel = hiltViewModel(),
@ -71,6 +72,7 @@ fun VaultUnlockedNavBarScreen(
onNavigateToVaultEditItem: (vaultItemId: String) -> Unit,
onNavigateToNewSend: () -> Unit,
onNavigateToDeleteAccount: () -> Unit,
onNavigateToFolders: () -> Unit,
onNavigateToPasswordHistory: () -> Unit,
) {
EventsEffect(viewModel = viewModel) { event ->
@ -102,6 +104,7 @@ fun VaultUnlockedNavBarScreen(
navigateToVaultAddItem = onNavigateToVaultAddItem,
navigateToNewSend = onNavigateToNewSend,
navigateToDeleteAccount = onNavigateToDeleteAccount,
navigateToFolders = onNavigateToFolders,
navigateToPasswordHistory = onNavigateToPasswordHistory,
generatorTabClickedAction = {
viewModel.trySendAction(VaultUnlockedNavBarAction.GeneratorTabClick)
@ -134,6 +137,7 @@ private fun VaultUnlockedNavBarScaffold(
onNavigateToVaultEditItem: (vaultItemId: String) -> Unit,
navigateToNewSend: () -> Unit,
navigateToDeleteAccount: () -> Unit,
navigateToFolders: () -> Unit,
navigateToPasswordHistory: () -> Unit,
) {
var shouldDimNavBar by remember { mutableStateOf(false) }
@ -200,6 +204,7 @@ private fun VaultUnlockedNavBarScaffold(
settingsGraph(
navController = navController,
onNavigateToDeleteAccount = navigateToDeleteAccount,
onNavigateToFolders = navigateToFolders,
)
}
}

View file

@ -0,0 +1,56 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.folders
import androidx.compose.ui.test.onNodeWithContentDescription
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.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
class FoldersScreenTest : BaseComposeTest() {
private var onNavigateBackCalled = false
private val mutableEventFlow = MutableSharedFlow<FoldersEvent>(
extraBufferCapacity = Int.MAX_VALUE,
)
private val mutableStateFlow = MutableStateFlow(Unit)
val viewModel = mockk<FoldersViewModel>(relaxed = true) {
every { eventFlow } returns mutableEventFlow
every { stateFlow } returns mutableStateFlow
}
@Before
fun setup() {
composeTestRule.setContent {
FoldersScreen(
viewModel = viewModel,
onNavigateBack = { onNavigateBackCalled = true },
)
}
}
@Test
fun `close button click should send CloseButtonClick`() {
composeTestRule.onNodeWithContentDescription("Close").performClick()
verify { viewModel.trySendAction(FoldersAction.CloseButtonClick) }
}
@Test
fun `add folder button click should send AddFolderButtonClick`() {
composeTestRule.onNodeWithContentDescription("Add item").performClick()
verify {
viewModel.trySendAction(FoldersAction.AddFolderButtonClick)
}
}
@Test
fun `NavigateBack should call onNavigateBack`() {
mutableEventFlow.tryEmit(FoldersEvent.NavigateBack)
assertTrue(onNavigateBackCalled)
}
}

View file

@ -0,0 +1,33 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.folders
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 FoldersViewModelTest : BaseViewModelTest() {
@Test
fun `BackClick should emit NavigateBack`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(FoldersAction.CloseButtonClick)
assertEquals(FoldersEvent.NavigateBack, awaitItem())
}
}
@Test
fun `AddFolderButtonClick should emit ShowToast`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(FoldersAction.AddFolderButtonClick)
assertEquals(
FoldersEvent.ShowToast("Not yet implemented."),
awaitItem(),
)
}
}
private fun createViewModel(): FoldersViewModel = FoldersViewModel()
}

View file

@ -1,46 +1,98 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.vault
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.isDialog
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.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
class VaultSettingsScreenTest : BaseComposeTest() {
@Test
fun `on back click should send BackClick`() {
val viewModel: VaultSettingsViewModel = mockk {
every { eventFlow } returns emptyFlow()
every { trySendAction(VaultSettingsAction.BackClick) } returns Unit
}
private var onNavigateBackCalled = false
private var onNavigateToFoldersCalled = false
private val mutableEventFlow = MutableSharedFlow<VaultSettingsEvent>(
extraBufferCapacity = Int.MAX_VALUE,
)
private val mutableStateFlow = MutableStateFlow(Unit)
val viewModel = mockk<VaultSettingsViewModel>(relaxed = true) {
every { eventFlow } returns mutableEventFlow
every { stateFlow } returns mutableStateFlow
}
@Before
fun setup() {
composeTestRule.setContent {
VaultSettingsScreen(
viewModel = viewModel,
onNavigateBack = { },
onNavigateBack = { onNavigateBackCalled = true },
onNavigateToFolders = { onNavigateToFoldersCalled = true },
)
}
}
@Test
fun `on back click should send BackClick`() {
every { viewModel.trySendAction(VaultSettingsAction.BackClick) } returns Unit
composeTestRule.onNodeWithContentDescription("Back").performClick()
verify { viewModel.trySendAction(VaultSettingsAction.BackClick) }
}
@Test
fun `on NavigateAbout should call onNavigateToVault`() {
var haveCalledNavigateBack = false
val viewModel = mockk<VaultSettingsViewModel> {
every { eventFlow } returns flowOf(VaultSettingsEvent.NavigateBack)
fun `export vault click should send ExportVaultClick`() {
composeTestRule.onNodeWithText("Export vault").performClick()
verify {
viewModel.trySendAction(VaultSettingsAction.ExportVaultClick)
}
composeTestRule.setContent {
VaultSettingsScreen(
viewModel = viewModel,
onNavigateBack = { haveCalledNavigateBack = true },
)
}
@Test
fun `import items click should display dialog and confirming should send ImportItemsClick`() {
composeTestRule.onNodeWithText("Import items").performClick()
composeTestRule
.onNodeWithText("Continue")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
.performClick()
verify {
viewModel.trySendAction(VaultSettingsAction.ImportItemsClick)
}
assertTrue(haveCalledNavigateBack)
}
@Test
fun `import items click should display dialog & canceling should not send ImportItemsClick`() {
composeTestRule.onNodeWithText("Import items").performClick()
composeTestRule
.onNodeWithText("Cancel")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
.performClick()
verify(exactly = 0) {
viewModel.trySendAction(VaultSettingsAction.ImportItemsClick)
}
}
@Test
fun `NavigateBack should call onNavigateBack`() {
mutableEventFlow.tryEmit(VaultSettingsEvent.NavigateBack)
assertTrue(onNavigateBackCalled)
}
@Test
fun `NavigateToFolders should call onNavigateToFolders`() {
mutableEventFlow.tryEmit(VaultSettingsEvent.NavigateToFolders)
assertTrue(onNavigateToFoldersCalled)
}
}

View file

@ -9,11 +9,37 @@ import org.junit.jupiter.api.Test
class VaultSettingsViewModelTest : BaseViewModelTest() {
@Test
fun `on BackClick should emit NavigateBack`() = runTest {
val viewModel = VaultSettingsViewModel()
fun `BackClick should emit NavigateBack`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(VaultSettingsAction.BackClick)
assertEquals(VaultSettingsEvent.NavigateBack, awaitItem())
}
}
@Test
fun `ExportVaultClick should emit ShowToast`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(VaultSettingsAction.ExportVaultClick)
assertEquals(
VaultSettingsEvent.ShowToast("Not yet implemented."),
awaitItem(),
)
}
}
@Test
fun `ImportItemsClick should emit ShowToast`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(VaultSettingsAction.ImportItemsClick)
assertEquals(
VaultSettingsEvent.ShowToast("Not yet implemented."),
awaitItem(),
)
}
}
private fun createViewModel(): VaultSettingsViewModel = VaultSettingsViewModel()
}

View file

@ -9,10 +9,20 @@ import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Before
import org.junit.Test
class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
private val fakeNavHostController = FakeNavHostController()
private val mutableEventFlow = MutableSharedFlow<VaultUnlockedNavBarEvent>(
extraBufferCapacity = Int.MAX_VALUE,
)
private val mutableStateFlow = MutableStateFlow(Unit)
val viewModel = mockk<VaultUnlockedNavBarViewModel>(relaxed = true) {
every { eventFlow } returns mutableEventFlow
every { stateFlow } returns mutableStateFlow
}
private val expectedNavOptions = navOptions {
// When changing root navigation state, pop everything else off the back stack:
@ -24,9 +34,8 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
restoreState = true
}
@Test
fun `vault tab click should send VaultTabClick action`() {
val viewModel = mockk<VaultUnlockedNavBarViewModel>(relaxed = true)
@Before
fun setup() {
composeTestRule.apply {
setContent {
VaultUnlockedNavBarScreen(
@ -37,90 +46,42 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
onNavigateToVaultEditItem = {},
onNavigateToNewSend = {},
onNavigateToDeleteAccount = {},
onNavigateToFolders = {},
onNavigateToPasswordHistory = {},
)
}
onNodeWithText("My vault").performClick()
}
}
@Test
fun `vault tab click should send VaultTabClick action`() {
composeTestRule.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,
onNavigateToVaultAddItem = {},
onNavigateToVaultItem = {},
onNavigateToVaultEditItem = {},
onNavigateToNewSend = {},
onNavigateToDeleteAccount = {},
onNavigateToPasswordHistory = {},
)
}
runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") }
vaultUnlockedNavBarEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToVaultScreen)
runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "vault_graph",
navOptions = expectedNavOptions,
)
}
composeTestRule.runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") }
mutableEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToVaultScreen)
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "vault_graph",
navOptions = expectedNavOptions,
)
}
}
@Test
fun `send tab click should send SendTabClick action`() {
val viewModel = mockk<VaultUnlockedNavBarViewModel>(relaxed = true)
composeTestRule.apply {
setContent {
VaultUnlockedNavBarScreen(
viewModel = viewModel,
navController = fakeNavHostController,
onNavigateToVaultAddItem = {},
onNavigateToVaultItem = {},
onNavigateToVaultEditItem = {},
onNavigateToNewSend = {},
onNavigateToDeleteAccount = {},
onNavigateToPasswordHistory = {},
)
}
onNodeWithText("Send").performClick()
}
composeTestRule.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,
onNavigateToVaultAddItem = {},
onNavigateToVaultItem = {},
onNavigateToVaultEditItem = {},
onNavigateToNewSend = {},
onNavigateToDeleteAccount = {},
onNavigateToPasswordHistory = {},
)
}
runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") }
vaultUnlockedNavBarEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToSendScreen)
mutableEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToSendScreen)
runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "send",
@ -132,48 +93,15 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
@Test
fun `generator tab click should send GeneratorTabClick action`() {
val viewModel = mockk<VaultUnlockedNavBarViewModel>(relaxed = true)
composeTestRule.apply {
setContent {
VaultUnlockedNavBarScreen(
viewModel = viewModel,
navController = fakeNavHostController,
onNavigateToVaultAddItem = {},
onNavigateToVaultItem = {},
onNavigateToVaultEditItem = {},
onNavigateToNewSend = {},
onNavigateToDeleteAccount = {},
onNavigateToPasswordHistory = {},
)
}
onNodeWithText("Generator").performClick()
}
composeTestRule.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,
onNavigateToVaultAddItem = {},
onNavigateToVaultItem = {},
onNavigateToVaultEditItem = {},
onNavigateToNewSend = {},
onNavigateToDeleteAccount = {},
onNavigateToPasswordHistory = {},
)
}
runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") }
vaultUnlockedNavBarEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToGeneratorScreen)
mutableEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToGeneratorScreen)
runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "generator",
@ -185,48 +113,15 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
@Test
fun `settings tab click should send SendTabClick action`() {
val viewModel = mockk<VaultUnlockedNavBarViewModel>(relaxed = true)
composeTestRule.apply {
setContent {
VaultUnlockedNavBarScreen(
viewModel = viewModel,
navController = fakeNavHostController,
onNavigateToVaultAddItem = {},
onNavigateToVaultItem = {},
onNavigateToVaultEditItem = {},
onNavigateToNewSend = {},
onNavigateToDeleteAccount = {},
onNavigateToPasswordHistory = {},
)
}
onNodeWithText("Settings").performClick()
}
composeTestRule.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,
onNavigateToVaultAddItem = {},
onNavigateToVaultItem = {},
onNavigateToVaultEditItem = {},
onNavigateToNewSend = {},
onNavigateToDeleteAccount = {},
onNavigateToPasswordHistory = {},
)
}
runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") }
vaultUnlockedNavBarEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToSettingsScreen)
mutableEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToSettingsScreen)
runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "settings_graph",