Add PIN entry dialog and confirmation UI (#625)

This commit is contained in:
Brian Yencho
2024-01-15 16:50:01 -06:00
committed by GitHub
parent c30f3bb8e7
commit e6cb1fc357
5 changed files with 544 additions and 22 deletions

View File

@@ -15,6 +15,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTextInput
import androidx.core.net.toUri
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
@@ -114,12 +115,211 @@ class AccountSecurityScreenTest : BaseComposeTest() {
}
@Test
fun `on unlock with pin toggle should send UnlockWithPinToggle`() {
fun `on unlock with pin toggle when enabled should send UnlockWithPinToggle Disabled`() {
mutableStateFlow.update {
it.copy(isUnlockWithPinEnabled = true)
}
composeTestRule
.onNodeWithText("Unlock with PIN code")
.performScrollTo()
.performClick()
verify { viewModel.trySendAction(AccountSecurityAction.UnlockWithPinToggle(true)) }
verify { viewModel.trySendAction(AccountSecurityAction.UnlockWithPinToggle.Disabled) }
}
@Suppress("MaxLineLength")
@Test
fun `on unlock with pin toggle when disabled should show the PIN input dialog and send UnlockWithPinToggle PendingEnabled`() {
mutableStateFlow.update {
it.copy(isUnlockWithPinEnabled = false)
}
composeTestRule.assertNoDialogExists()
composeTestRule
.onNodeWithText("Unlock with PIN code")
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("Enter your PIN code.")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText(
"Set your PIN code for unlocking Bitwarden. Your PIN settings will be reset if " +
"you ever fully log out of the application.",
)
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("PIN")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("Cancel")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("Submit")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
verify { viewModel.trySendAction(AccountSecurityAction.UnlockWithPinToggle.PendingEnabled) }
}
@Suppress("MaxLineLength")
@Test
fun `PIN input dialog Cancel click should clear the dialog and send UnlockWithPinToggle Disabled`() {
mutableStateFlow.update {
it.copy(isUnlockWithPinEnabled = false)
}
composeTestRule
.onNodeWithText("Unlock with PIN code")
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("Cancel")
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
verify { viewModel.trySendAction(AccountSecurityAction.UnlockWithPinToggle.Disabled) }
composeTestRule.assertNoDialogExists()
}
@Suppress("MaxLineLength")
@Test
fun `PIN input dialog Submit click with empty pin should clear the dialog and send UnlockWithPinToggle Disabled`() {
mutableStateFlow.update {
it.copy(isUnlockWithPinEnabled = false)
}
composeTestRule
.onNodeWithText("Unlock with PIN code")
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("Submit")
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
verify { viewModel.trySendAction(AccountSecurityAction.UnlockWithPinToggle.Disabled) }
composeTestRule.assertNoDialogExists()
}
@Suppress("MaxLineLength")
@Test
fun `PIN input dialog Submit click with non-empty pin should show a confirmation dialog and send UnlockWithPinToggle PendingEnabled`() {
mutableStateFlow.update {
it.copy(isUnlockWithPinEnabled = false)
}
composeTestRule
.onNodeWithText("Unlock with PIN code")
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("PIN")
.filterToOne(hasAnyAncestor(isDialog()))
.performTextInput("1234")
composeTestRule
.onAllNodesWithText("Submit")
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
composeTestRule
.onAllNodesWithText("Unlock with PIN code")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText(
"Do you want to require unlocking with your master password when the application " +
"is restarted?",
)
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("Yes")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("No")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
verify { viewModel.trySendAction(AccountSecurityAction.UnlockWithPinToggle.PendingEnabled) }
}
@Suppress("MaxLineLength")
@Test
fun `PIN confirmation dialog No click should send UnlockWithPinToggle Enabled and close the dialog`() {
mutableStateFlow.update {
it.copy(isUnlockWithPinEnabled = false)
}
composeTestRule
.onNodeWithText("Unlock with PIN code")
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("PIN")
.filterToOne(hasAnyAncestor(isDialog()))
.performTextInput("1234")
composeTestRule
.onAllNodesWithText("Submit")
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
composeTestRule
.onAllNodesWithText("No")
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
verify {
viewModel.trySendAction(
AccountSecurityAction.UnlockWithPinToggle.Enabled(
pin = "1234",
shouldRequireMasterPasswordOnRestart = false,
),
)
}
composeTestRule.assertNoDialogExists()
}
@Suppress("MaxLineLength")
@Test
fun `PIN confirmation dialog Yes click should send UnlockWithPinToggle Enabled and close the dialog`() {
mutableStateFlow.update {
it.copy(isUnlockWithPinEnabled = false)
}
composeTestRule
.onNodeWithText("Unlock with PIN code")
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("PIN")
.filterToOne(hasAnyAncestor(isDialog()))
.performTextInput("1234")
composeTestRule
.onAllNodesWithText("Submit")
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
composeTestRule
.onAllNodesWithText("Yes")
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
verify {
viewModel.trySendAction(
AccountSecurityAction.UnlockWithPinToggle.Enabled(
pin = "1234",
shouldRequireMasterPasswordOnRestart = true,
),
)
}
composeTestRule.assertNoDialogExists()
}
@Test

View File

@@ -208,22 +208,71 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
)
}
@Suppress("MaxLineLength")
@Test
fun `on UnlockWithPinToggle should emit ShowToast`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(AccountSecurityAction.UnlockWithPinToggle(true))
fun `on UnlockWithPinToggle Disabled should set pin unlock to false and emit ShowToast`() =
runTest {
val initialState = DEFAULT_STATE.copy(
isUnlockWithPinEnabled = true,
)
val viewModel = createViewModel(initialState = initialState)
viewModel.eventFlow.test {
viewModel.trySendAction(
AccountSecurityAction.UnlockWithPinToggle.Disabled,
)
assertEquals(
AccountSecurityEvent.ShowToast("Handle unlock with pin.".asText()),
awaitItem(),
)
}
assertEquals(
AccountSecurityEvent.ShowToast("Handle unlock with pin.".asText()),
awaitItem(),
initialState.copy(isUnlockWithPinEnabled = false),
viewModel.stateFlow.value,
)
}
@Suppress("MaxLineLength")
@Test
fun `on UnlockWithPinToggle Enabled should set pin unlock to true`() {
val initialState = DEFAULT_STATE.copy(
isUnlockWithPinEnabled = false,
)
val viewModel = createViewModel(initialState = initialState)
viewModel.trySendAction(
AccountSecurityAction.UnlockWithPinToggle.PendingEnabled,
)
assertEquals(
DEFAULT_STATE.copy(isUnlockWithPinEnabled = true),
initialState.copy(isUnlockWithPinEnabled = true),
viewModel.stateFlow.value,
)
}
@Suppress("MaxLineLength")
@Test
fun `on UnlockWithPinToggle Enabled should set pin unlock to true and emit ShowToast`() =
runTest {
val initialState = DEFAULT_STATE.copy(
isUnlockWithPinEnabled = false,
)
val viewModel = createViewModel(initialState = initialState)
viewModel.eventFlow.test {
viewModel.trySendAction(
AccountSecurityAction.UnlockWithPinToggle.Enabled(
pin = "1234",
shouldRequireMasterPasswordOnRestart = true,
),
)
assertEquals(
AccountSecurityEvent.ShowToast("Handle unlock with pin.".asText()),
awaitItem(),
)
}
assertEquals(
initialState.copy(isUnlockWithPinEnabled = true),
viewModel.stateFlow.value,
)
}
@Test
fun `on LogoutClick should show confirm log out dialog`() = runTest {
val viewModel = createViewModel()
@@ -253,11 +302,12 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
}
private fun createViewModel(
initialState: AccountSecurityState = DEFAULT_STATE,
authRepository: AuthRepository = mockk(relaxed = true),
vaultRepository: VaultRepository = mockk(relaxed = true),
settingsRepository: SettingsRepository = mockk(relaxed = true),
savedStateHandle: SavedStateHandle = SavedStateHandle().apply {
set("state", DEFAULT_STATE)
set("state", initialState)
},
): AccountSecurityViewModel = AccountSecurityViewModel(
authRepository = authRepository,