mirror of
https://github.com/bitwarden/android.git
synced 2026-06-01 10:16:47 -05:00
BIT-665: Create Add UI for Login-type item (#179)
This commit is contained in:
@@ -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") }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user