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 GitHub
parent ef2bdeabca
commit 4deeb40b10
15 changed files with 530 additions and 164 deletions

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",