BIT-665: Create Add UI for Login-type item (#179)

This commit is contained in:
joshua-livefront
2023-10-31 11:47:11 -04:00
committed by GitHub
parent 72c454768d
commit e0838cf4a4
29 changed files with 3050 additions and 225 deletions

View File

@@ -32,6 +32,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
VaultUnlockedNavBarScreen(
viewModel = viewModel,
navController = fakeNavHostController,
onNavigateToVaultAddItem = {},
)
}
onNodeWithText("My vault").performClick()
@@ -52,6 +53,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
VaultUnlockedNavBarScreen(
viewModel = viewModel,
navController = fakeNavHostController,
onNavigateToVaultAddItem = {},
)
}
runOnIdle { fakeNavHostController.assertCurrentRoute("vault") }
@@ -73,6 +75,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
VaultUnlockedNavBarScreen(
viewModel = viewModel,
navController = fakeNavHostController,
onNavigateToVaultAddItem = {},
)
}
onNodeWithText("Send").performClick()
@@ -93,6 +96,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
VaultUnlockedNavBarScreen(
viewModel = viewModel,
navController = fakeNavHostController,
onNavigateToVaultAddItem = {},
)
}
runOnIdle { fakeNavHostController.assertCurrentRoute("vault") }
@@ -114,6 +118,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
VaultUnlockedNavBarScreen(
viewModel = viewModel,
navController = fakeNavHostController,
onNavigateToVaultAddItem = {},
)
}
onNodeWithText("Generator").performClick()
@@ -134,6 +139,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
VaultUnlockedNavBarScreen(
viewModel = viewModel,
navController = fakeNavHostController,
onNavigateToVaultAddItem = {},
)
}
runOnIdle { fakeNavHostController.assertCurrentRoute("vault") }
@@ -155,6 +161,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
VaultUnlockedNavBarScreen(
viewModel = viewModel,
navController = fakeNavHostController,
onNavigateToVaultAddItem = {},
)
}
onNodeWithText("Settings").performClick()
@@ -175,6 +182,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
VaultUnlockedNavBarScreen(
viewModel = viewModel,
navController = fakeNavHostController,
onNavigateToVaultAddItem = {},
)
}
runOnIdle { fakeNavHostController.assertCurrentRoute("vault") }

View File

@@ -0,0 +1,642 @@
package com.x8bit.bitwarden.ui.vault.feature.vault
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsOff
import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.assertTextContains
import androidx.compose.ui.test.click
import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasSetTextAction
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onLast
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onSiblings
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.performTouchInput
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 kotlinx.coroutines.flow.update
import org.junit.Test
class VaultAddItemScreenTest : BaseComposeTest() {
private val mutableStateFlow = MutableStateFlow(
VaultAddItemState(
selectedType = VaultAddItemState.ItemType.Login(),
),
)
private val viewModel = mockk<VaultAddItemViewModel>(relaxed = true) {
every { eventFlow } returns emptyFlow()
every { stateFlow } returns mutableStateFlow
}
@Test
fun `clicking close button should send CloseClick action`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithContentDescription(label = "Close")
.performClick()
verify {
viewModel.trySendAction(
VaultAddItemAction.CloseClick,
)
}
}
@Test
fun `clicking save button should send SaveClick action`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithText(text = "Save")
.performClick()
verify {
viewModel.trySendAction(
VaultAddItemAction.SaveClick,
)
}
}
@Test
fun `clicking a Type Option should send TypeOptionSelect action`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
// Opens the menu
composeTestRule
.onNodeWithContentDescription(label = "Type, Login")
.performClick()
// Choose the option from the menu
composeTestRule
.onAllNodesWithText(text = "Login")
.onLast()
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
VaultAddItemAction.TypeOptionSelect(VaultAddItemState.ItemTypeOption.LOGIN),
)
}
}
@Test
fun `the Type Option field should display the text of the selected item type`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithContentDescription(label = "Type, Login")
.assertIsDisplayed()
mutableStateFlow.update { it.copy(selectedType = VaultAddItemState.ItemType.Card) }
composeTestRule
.onNodeWithContentDescription(label = "Type, Card")
.assertIsDisplayed()
}
@Test
fun `in ItemType_Login state changing Name text field should trigger NameTextChange`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithText(text = "Name")
.performTextInput(text = "TestName")
verify {
viewModel.trySendAction(
VaultAddItemAction.ItemType.LoginType.NameTextChange(name = "TestName"),
)
}
}
@Test
fun `in ItemType_Login the name control should display the text provided by the state`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithText(text = "Name")
.assertTextContains("")
mutableStateFlow.update { currentState ->
updateLoginType(currentState) { copy(name = "NewName") }
}
composeTestRule
.onNodeWithText(text = "Name")
.assertTextContains("NewName")
}
@Test
fun `in ItemType_Login state changing Username text field should trigger UsernameTextChange`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithText(text = "Username")
.performTextInput(text = "TestUsername")
verify {
viewModel.trySendAction(
VaultAddItemAction.ItemType.LoginType.UsernameTextChange(username = "TestUsername"),
)
}
}
@Test
fun `in ItemType_Login the Username control should display the text provided by the state`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithText(text = "Username")
.assertTextContains("")
mutableStateFlow.update { currentState ->
updateLoginType(currentState) { copy(username = "NewUsername") }
}
composeTestRule
.onNodeWithText(text = "Username")
.assertTextContains("NewUsername")
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Login state clicking Username generator action should trigger OpenUsernameGeneratorClick`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithContentDescription(label = "Generate username")
.performClick()
verify {
viewModel.trySendAction(
VaultAddItemAction.ItemType.LoginType.OpenUsernameGeneratorClick,
)
}
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Login state clicking Password checker action should trigger PasswordCheckerClick`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithText(text = "Password")
.onSiblings()
.onFirst()
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(VaultAddItemAction.ItemType.LoginType.PasswordCheckerClick)
}
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Login state click Password text field generator action should trigger OpenPasswordGeneratorClick`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithText(text = "Password")
.onSiblings()
.onLast()
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
VaultAddItemAction.ItemType.LoginType.OpenPasswordGeneratorClick,
)
}
}
@Test
fun `in ItemType_Login state changing Password text field should trigger PasswordTextChange`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithText(text = "Password")
.performTextInput(text = "TestPassword")
verify {
viewModel.trySendAction(
VaultAddItemAction.ItemType.LoginType.PasswordTextChange("TestPassword"),
)
}
}
@Test
fun `in ItemType_Login the Password control should display the text provided by the state`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithText(text = "Password")
.assertTextContains("")
mutableStateFlow.update { currentState ->
updateLoginType(currentState) { copy(password = "NewPassword") }
}
composeTestRule
.onNodeWithText(text = "Password")
.assertTextContains("•••••••••••")
}
@Test
fun `in ItemType_Login state clicking Set up TOTP button should trigger SetupTotpClick`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithText(text = "Set up TOTP")
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
VaultAddItemAction.ItemType.LoginType.SetupTotpClick,
)
}
}
@Test
fun `in ItemType_Login state changing URI text field should trigger UriTextChange`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithText("URI")
.performScrollTo()
.performTextInput("TestURI")
verify {
viewModel.trySendAction(
VaultAddItemAction.ItemType.LoginType.UriTextChange("TestURI"),
)
}
}
@Test
fun `in ItemType_Login the URI control should display the text provided by the state`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithText(text = "URI")
.assertTextContains("")
mutableStateFlow.update { currentState ->
updateLoginType(currentState) { copy(uri = "NewURI") }
}
composeTestRule
.onNodeWithText(text = "URI")
.assertTextContains("NewURI")
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Login state clicking the URI settings action should trigger UriSettingsClick`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithText(text = "URI")
.onSiblings()
.filterToOne(hasContentDescription(value = "Options"))
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
VaultAddItemAction.ItemType.LoginType.UriSettingsClick,
)
}
}
@Test
fun `in ItemType_Login state clicking the New URI button should trigger AddNewUriClick`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithText(text = "New URI")
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
VaultAddItemAction.ItemType.LoginType.AddNewUriClick,
)
}
}
@Test
fun `in ItemType_Login state clicking a Folder Option should send FolderChange action`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
// Opens the menu
composeTestRule
.onNodeWithContentDescription(label = "Folder, No Folder")
.performScrollTo()
.performClick()
// Choose the option from the menu
composeTestRule
.onAllNodesWithText(text = "Folder 1")
.onLast()
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
VaultAddItemAction.ItemType.LoginType.FolderChange("Folder 1"),
)
}
}
@Test
fun `in ItemType_Login the folder control should display the text provided by the state`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithContentDescription(label = "Folder, No Folder")
.performScrollTo()
.assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateLoginType(currentState) { copy(folder = "Folder 2") }
}
composeTestRule
.onNodeWithContentDescription(label = "Folder, Folder 2")
.performScrollTo()
.assertIsDisplayed()
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Login state, toggling the favorite toggle should send ToggleFavorite action`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule.onNodeWithText("Favorite")
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
VaultAddItemAction.ItemType.LoginType.ToggleFavorite(
isFavorite = true,
),
)
}
}
@Test
fun `in ItemType_Login the favorite toggle should be enabled or disabled according to state`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithText(text = "Favorite")
.assertIsOff()
mutableStateFlow.update { currentState ->
updateLoginType(currentState) { copy(favorite = true) }
}
composeTestRule
.onNodeWithText(text = "Favorite")
.assertIsOn()
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Login state, toggling the Master password re-prompt toggle should send ToggleMasterPasswordReprompt action`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithText("Master password re-prompt")
.performScrollTo()
.performTouchInput {
click(position = Offset(x = 1f, y = center.y))
}
verify {
viewModel.trySendAction(
VaultAddItemAction.ItemType.LoginType.ToggleMasterPasswordReprompt(
isMasterPasswordReprompt = true,
),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Login the master password re-prompt toggle should be enabled or disabled according to state`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithText("Master password re-prompt")
.assertIsOff()
mutableStateFlow.update { currentState ->
updateLoginType(currentState) { copy(masterPasswordReprompt = true) }
}
composeTestRule
.onNodeWithText("Master password re-prompt")
.assertIsOn()
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Login state, toggling the Master password re-prompt tooltip button should send TooltipClick action`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule.onNodeWithContentDescription(label = "Master password re-prompt help")
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
VaultAddItemAction.ItemType.LoginType.TooltipClick,
)
}
}
@Test
fun `in ItemType_Login state changing Notes text field should trigger NotesTextChange`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNode(hasSetTextAction() and hasText("Notes"))
.performScrollTo()
.performTextInput("TestNotes")
verify {
viewModel.trySendAction(
VaultAddItemAction.ItemType.LoginType.NotesTextChange("TestNotes"),
)
}
}
@Test
fun `in ItemType_Login the Notes control should display the text provided by the state`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNode(hasSetTextAction() and hasText("Notes"))
.assertTextContains("")
mutableStateFlow.update { currentState ->
updateLoginType(currentState) { copy(notes = "NewNote") }
}
composeTestRule
.onNode(hasSetTextAction() and hasText("Notes"))
.assertTextContains("NewNote")
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Login state clicking New Custom Field button should trigger AddNewCustomFieldClick`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithText(text = "New custom field")
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(VaultAddItemAction.ItemType.LoginType.AddNewCustomFieldClick)
}
}
@Test
fun `in ItemType_Login state clicking a Ownership option should send OwnershipChange action`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
// Opens the menu
composeTestRule
.onNodeWithContentDescription(label = "Who owns this item?, placeholder@email.com")
.performScrollTo()
.performClick()
// Choose the option from the menu
composeTestRule
.onAllNodesWithText(text = "a@b.com")
.onLast()
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
VaultAddItemAction.ItemType.LoginType.OwnershipChange("a@b.com"),
)
}
}
@Test
fun `in ItemType_Login the Ownership control should display the text provided by the state`() {
composeTestRule.setContent {
VaultAddItemScreen(viewModel = viewModel, onNavigateBack = {})
}
composeTestRule
.onNodeWithContentDescription(label = "Who owns this item?, placeholder@email.com")
.performScrollTo()
.assertIsDisplayed()
mutableStateFlow.update { currentState ->
updateLoginType(currentState) { copy(ownership = "Owner 2") }
}
composeTestRule
.onNodeWithContentDescription(label = "Who owns this item?, Owner 2")
.performScrollTo()
.assertIsDisplayed()
}
//region Helper functions
private fun updateLoginType(
currentState: VaultAddItemState,
transform: VaultAddItemState.ItemType.Login.() -> VaultAddItemState.ItemType.Login,
): VaultAddItemState {
val updatedType = when (val currentType = currentState.selectedType) {
is VaultAddItemState.ItemType.Login -> currentType.transform()
else -> currentType
}
return currentState.copy(selectedType = updatedType)
}
//endregion Helper functions
}

View File

@@ -0,0 +1,362 @@
package com.x8bit.bitwarden.ui.vault.feature.vault
import androidx.lifecycle.SavedStateHandle
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.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
class VaultAddItemViewModelTest : BaseViewModelTest() {
private val initialState = createVaultAddLoginItemState()
private val initialSavedStateHandle = createSavedStateHandleWithState(initialState)
@Test
fun `initial state should be correct`() = runTest {
val viewModel = VaultAddItemViewModel(initialSavedStateHandle)
viewModel.stateFlow.test {
assertEquals(initialState, awaitItem())
}
}
@Test
fun `CloseClick should emit NavigateBack`() = runTest {
val viewModel = VaultAddItemViewModel(initialSavedStateHandle)
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(VaultAddItemAction.CloseClick)
assertEquals(VaultAddItemEvent.NavigateBack, awaitItem())
}
}
@Test
fun `SaveClick should emit ShowToast`() = runTest {
val viewModel = VaultAddItemViewModel(initialSavedStateHandle)
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(VaultAddItemAction.SaveClick)
assertEquals(VaultAddItemEvent.ShowToast("Save Item"), awaitItem())
}
}
@Test
fun `TypeOptionSelect LOGIN should switch to LoginItem`() = runTest {
val viewModel = VaultAddItemViewModel(initialSavedStateHandle)
val action = VaultAddItemAction.TypeOptionSelect(VaultAddItemState.ItemTypeOption.LOGIN)
viewModel.actionChannel.trySend(action)
val expectedState = initialState.copy(selectedType = VaultAddItemState.ItemType.Login())
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Nested
inner class VaultAddLoginTypeItemActions {
private lateinit var viewModel: VaultAddItemViewModel
@BeforeEach
fun setup() {
viewModel = VaultAddItemViewModel(initialSavedStateHandle)
}
@Test
fun `NameTextChange should update name in LoginItem`() = runTest {
val viewModel = VaultAddItemViewModel(initialSavedStateHandle)
val action = VaultAddItemAction.ItemType.LoginType.NameTextChange("newName")
viewModel.actionChannel.trySend(action)
val expectedLoginItem =
(initialState.selectedType as VaultAddItemState.ItemType.Login)
.copy(name = "newName")
val expectedState = initialState.copy(selectedType = expectedLoginItem)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Suppress("MaxLineLength")
@Test
fun `UsernameTextChange should update username in LoginItem`() = runTest {
val viewModel = VaultAddItemViewModel(initialSavedStateHandle)
val action = VaultAddItemAction.ItemType.LoginType.UsernameTextChange("newUsername")
viewModel.actionChannel.trySend(action)
val expectedLoginItem =
(initialState.selectedType as VaultAddItemState.ItemType.Login)
.copy(username = "newUsername")
val expectedState = initialState.copy(selectedType = expectedLoginItem)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Suppress("MaxLineLength")
@Test
fun `PasswordTextChange should update password in LoginItem`() = runTest {
val viewModel = VaultAddItemViewModel(initialSavedStateHandle)
val action = VaultAddItemAction.ItemType.LoginType.PasswordTextChange("newPassword")
viewModel.actionChannel.trySend(action)
val expectedLoginItem =
(initialState.selectedType as VaultAddItemState.ItemType.Login)
.copy(password = "newPassword")
val expectedState = initialState.copy(selectedType = expectedLoginItem)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Test
fun `UriTextChange should update uri in LoginItem`() = runTest {
val viewModel = VaultAddItemViewModel(initialSavedStateHandle)
val action = VaultAddItemAction.ItemType.LoginType.UriTextChange("newUri")
viewModel.actionChannel.trySend(action)
val expectedLoginItem =
(initialState.selectedType as VaultAddItemState.ItemType.Login)
.copy(uri = "newUri")
val expectedState = initialState.copy(selectedType = expectedLoginItem)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Test
fun `FolderChange should update folder in LoginItem`() = runTest {
val viewModel = VaultAddItemViewModel(initialSavedStateHandle)
val action = VaultAddItemAction.ItemType.LoginType.FolderChange("newFolder")
viewModel.actionChannel.trySend(action)
val expectedLoginItem =
(initialState.selectedType as VaultAddItemState.ItemType.Login)
.copy(folder = "newFolder")
val expectedState = initialState.copy(selectedType = expectedLoginItem)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Test
fun `ToggleFavorite should update favorite in LoginItem`() = runTest {
val viewModel = VaultAddItemViewModel(initialSavedStateHandle)
val action = VaultAddItemAction.ItemType.LoginType.ToggleFavorite(true)
viewModel.actionChannel.trySend(action)
val expectedLoginItem =
(initialState.selectedType as VaultAddItemState.ItemType.Login)
.copy(favorite = true)
val expectedState = initialState.copy(selectedType = expectedLoginItem)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Suppress("MaxLineLength")
@Test
fun `ToggleMasterPasswordReprompt should update masterPasswordReprompt in LoginItem`() =
runTest {
val viewModel = VaultAddItemViewModel(initialSavedStateHandle)
val action = VaultAddItemAction.ItemType.LoginType.ToggleMasterPasswordReprompt(
isMasterPasswordReprompt = true,
)
viewModel.actionChannel.trySend(action)
val expectedLoginItem =
(initialState.selectedType as VaultAddItemState.ItemType.Login)
.copy(masterPasswordReprompt = true)
val expectedState = initialState.copy(selectedType = expectedLoginItem)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Test
fun `NotesTextChange should update notes in LoginItem`() = runTest {
val viewModel = VaultAddItemViewModel(initialSavedStateHandle)
val action = VaultAddItemAction.ItemType.LoginType.NotesTextChange(notes = "newNotes")
viewModel.actionChannel.trySend(action)
val expectedLoginItem =
(initialState.selectedType as VaultAddItemState.ItemType.Login)
.copy(notes = "newNotes")
val expectedState = initialState.copy(selectedType = expectedLoginItem)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Suppress("MaxLineLength")
@Test
fun `OwnershipChange should update ownership in LoginItem`() = runTest {
val viewModel = VaultAddItemViewModel(initialSavedStateHandle)
val action =
VaultAddItemAction.ItemType.LoginType.OwnershipChange(ownership = "newOwner")
viewModel.actionChannel.trySend(action)
val expectedLoginItem =
(initialState.selectedType as VaultAddItemState.ItemType.Login)
.copy(ownership = "newOwner")
val expectedState = initialState.copy(selectedType = expectedLoginItem)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Suppress("MaxLineLength")
@Test
fun `OpenUsernameGeneratorClick should emit ShowToast with 'Open Username Generator' message`() =
runTest {
val viewModel = VaultAddItemViewModel(initialSavedStateHandle)
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(
VaultAddItemAction.ItemType.LoginType.OpenUsernameGeneratorClick,
)
assertEquals(
VaultAddItemEvent.ShowToast("Open Username Generator"),
awaitItem(),
)
}
}
@Test
fun `PasswordCheckerClick should emit ShowToast with 'Password Checker' message`() =
runTest {
val viewModel = VaultAddItemViewModel(initialSavedStateHandle)
viewModel.eventFlow.test {
viewModel
.actionChannel
.trySend(VaultAddItemAction.ItemType.LoginType.PasswordCheckerClick)
assertEquals(VaultAddItemEvent.ShowToast("Password Checker"), awaitItem())
}
}
@Suppress("MaxLineLength")
@Test
fun `OpenPasswordGeneratorClick should emit ShowToast with 'Open Password Generator' message`() =
runTest {
val viewModel = VaultAddItemViewModel(initialSavedStateHandle)
viewModel.eventFlow.test {
viewModel
.actionChannel
.trySend(VaultAddItemAction.ItemType.LoginType.OpenPasswordGeneratorClick)
assertEquals(
VaultAddItemEvent.ShowToast("Open Password Generator"),
awaitItem(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `SetupTotpClick should emit ShowToast with 'Setup TOTP' message`() = runTest {
val viewModel = VaultAddItemViewModel(initialSavedStateHandle)
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(VaultAddItemAction.ItemType.LoginType.SetupTotpClick)
assertEquals(VaultAddItemEvent.ShowToast("Setup TOTP"), awaitItem())
}
}
@Suppress("MaxLineLength")
@Test
fun `UriSettingsClick should emit ShowToast with 'URI Settings' message`() = runTest {
val viewModel = VaultAddItemViewModel(initialSavedStateHandle)
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(VaultAddItemAction.ItemType.LoginType.UriSettingsClick)
assertEquals(VaultAddItemEvent.ShowToast("URI Settings"), awaitItem())
}
}
@Test
fun `AddNewUriClick should emit ShowToast with 'Add New URI' message`() = runTest {
val viewModel = VaultAddItemViewModel(initialSavedStateHandle)
viewModel.eventFlow.test {
viewModel
.actionChannel
.trySend(
VaultAddItemAction.ItemType.LoginType.AddNewUriClick,
)
assertEquals(VaultAddItemEvent.ShowToast("Add New URI"), awaitItem())
}
}
@Test
fun `TooltipClick should emit ShowToast with 'Tooltip' message`() = runTest {
val viewModel = VaultAddItemViewModel(initialSavedStateHandle)
viewModel.eventFlow.test {
viewModel
.actionChannel
.trySend(
VaultAddItemAction.ItemType.LoginType.TooltipClick,
)
assertEquals(VaultAddItemEvent.ShowToast("Tooltip"), awaitItem())
}
}
@Test
fun `AddNewCustomFieldClick should emit ShowToast with 'Add New Custom Field' message`() =
runTest {
val viewModel = VaultAddItemViewModel(initialSavedStateHandle)
viewModel.eventFlow.test {
viewModel
.actionChannel
.trySend(
VaultAddItemAction.ItemType.LoginType.AddNewCustomFieldClick,
)
assertEquals(VaultAddItemEvent.ShowToast("Add New Custom Field"), awaitItem())
}
}
}
@Suppress("LongParameterList")
private fun createVaultAddLoginItemState(
name: String = "",
username: String = "",
password: String = "",
uri: String = "",
folder: String = "No Folder",
favorite: Boolean = false,
masterPasswordReprompt: Boolean = false,
notes: String = "",
ownership: String = "placeholder@email.com",
): VaultAddItemState =
VaultAddItemState(
selectedType = VaultAddItemState.ItemType.Login(
name = name,
username = username,
password = password,
uri = uri,
folder = folder,
favorite = favorite,
masterPasswordReprompt = masterPasswordReprompt,
notes = notes,
ownership = ownership,
),
)
private fun createSavedStateHandleWithState(state: VaultAddItemState) =
SavedStateHandle().apply {
set("state", state)
}
}

View File

@@ -10,6 +10,8 @@ import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import org.junit.Assert.assertTrue
import org.junit.Test
class VaultScreenTest : BaseComposeTest() {
@@ -30,6 +32,7 @@ class VaultScreenTest : BaseComposeTest() {
setContent {
VaultScreen(
viewModel = viewModel,
onNavigateToVaultAddItemScreen = {},
)
}
onNodeWithContentDescription("Search vault").performClick()
@@ -53,6 +56,7 @@ class VaultScreenTest : BaseComposeTest() {
setContent {
VaultScreen(
viewModel = viewModel,
onNavigateToVaultAddItemScreen = {},
)
}
onNodeWithContentDescription("Add Item").performClick()
@@ -77,10 +81,35 @@ class VaultScreenTest : BaseComposeTest() {
setContent {
VaultScreen(
viewModel = viewModel,
onNavigateToVaultAddItemScreen = {},
)
}
onNodeWithText("Add an Item").performClick()
}
verify { viewModel.trySendAction(VaultAction.AddItemClick) }
}
@Test
fun `NavigateToAddItemScreen event should call onNavigateToVaultAddItemScreen`() {
var onNavigateToVaultAddItemScreenCalled = false
val viewModel = mockk<VaultViewModel>(relaxed = true) {
every { eventFlow } returns flowOf(VaultEvent.NavigateToAddItemScreen)
every { stateFlow } returns MutableStateFlow(
VaultState(
avatarColor = Color.Blue,
initials = "BW",
viewState = VaultState.ViewState.NoItems,
),
)
}
composeTestRule.setContent {
VaultScreen(
onNavigateToVaultAddItemScreen = { onNavigateToVaultAddItemScreenCalled = true },
viewModel = viewModel,
)
}
assertTrue(onNavigateToVaultAddItemScreenCalled)
}
}