BIT-634: Create Password Generation UI (#109)

This commit is contained in:
joshua-livefront
2023-10-12 14:41:58 -04:00
committed by GitHub
parent 2babd278c2
commit 86b44a802a
4 changed files with 1229 additions and 78 deletions

View File

@@ -2,19 +2,29 @@
package com.x8bit.bitwarden.ui.tools.feature.generator
import androidx.compose.ui.semantics.ProgressBarRangeInfo
import androidx.compose.ui.semantics.Role.Companion.Switch
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.SemanticsProperties.Role
import androidx.compose.ui.test.SemanticsMatcher.Companion.expectValue
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsOff
import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasProgressBarRangeInfo
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onChildren
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 androidx.compose.ui.test.swipeRight
import androidx.compose.ui.text.AnnotatedString
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import io.mockk.every
import io.mockk.mockk
@@ -79,6 +89,333 @@ class GeneratorScreenTest : BaseComposeTest() {
}
}
//region Passcode Password Tests
@Test
fun `in Passcode_Password state, the ViewModel state should update the UI correctly`() {
composeTestRule.setContent {
GeneratorScreen(viewModel = viewModel)
}
composeTestRule
.onNodeWithContentDescription(label = "What would you like to generate?, Password")
.assertIsDisplayed()
composeTestRule
.onNodeWithContentDescription(label = "Password, Password")
.assertIsDisplayed()
composeTestRule
.onNode(
expectValue(
SemanticsProperties.EditableText, AnnotatedString("14"),
),
)
.assertExists()
composeTestRule
.onNodeWithText("Uppercase (A to Z)")
.onChildren()
.filterToOne(expectValue(Role, Switch))
.assertIsOn()
composeTestRule
.onNodeWithText("Lowercase (A to Z)")
.onChildren()
.filterToOne(expectValue(Role, Switch))
.assertIsOn()
composeTestRule
.onNodeWithText("Numbers (0 to 9)")
.onChildren()
.filterToOne(expectValue(Role, Switch))
.assertIsOn()
composeTestRule
.onNodeWithText("Special characters (!@#$%^*)")
.onChildren()
.filterToOne(expectValue(Role, Switch))
.assertIsOff()
composeTestRule
.onNodeWithContentDescription("Minimum numbers, 1")
.onChildren()
.filterToOne(hasContentDescription("\u2212"))
.performScrollTo()
.assertIsDisplayed()
composeTestRule
.onNodeWithContentDescription("Minimum numbers, 1")
.onChildren()
.filterToOne(hasContentDescription("+"))
.performScrollTo()
.assertIsDisplayed()
composeTestRule
.onNodeWithText("Avoid ambiguous characters")
.onChildren()
.filterToOne(expectValue(Role, Switch))
.performScrollTo()
.assertIsOff()
}
@Test
fun `in Passcode_Password state, adjusting the slider should send SliderLengthChange action with length not equal to default`() {
composeTestRule.setContent {
GeneratorScreen(viewModel = viewModel)
}
composeTestRule
.onNodeWithText("Length")
.onSiblings()
.filterToOne(
hasProgressBarRangeInfo(
ProgressBarRangeInfo(
current = 13.6484375f,
range = 5.0f..128.0f,
steps = 127,
),
),
)
.performScrollTo()
.performTouchInput {
swipeRight(50f, 800f)
}
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.SliderLengthChange(
length = 128,
),
)
}
}
@Test
fun `in Passcode_Password state, toggling the capital letters toggle should send ToggleCapitalLettersChange action`() {
composeTestRule.setContent {
GeneratorScreen(viewModel = viewModel)
}
composeTestRule.onNodeWithText("Uppercase (A to Z)")
.onChildren()
.filterToOne(expectValue(Role, Switch))
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleCapitalLettersChange(
useCapitals = false,
),
)
}
}
@Test
fun `in Passcode_Password state, toggling the use lowercase toggle should send ToggleLowercaseLettersChange action`() {
composeTestRule.setContent {
GeneratorScreen(viewModel = viewModel)
}
composeTestRule.onNodeWithText("Lowercase (A to Z)")
.onChildren()
.filterToOne(expectValue(Role, Switch))
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleLowercaseLettersChange(
useLowercase = false,
),
)
}
}
@Test
fun `in Passcode_Password state, toggling the use numbers toggle should send ToggleNumbersChange action`() {
composeTestRule.setContent {
GeneratorScreen(viewModel = viewModel)
}
composeTestRule.onNodeWithText("Numbers (0 to 9)")
.onChildren()
.filterToOne(expectValue(Role, Switch))
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleNumbersChange(
useNumbers = false,
),
)
}
}
@Test
fun `in Passcode_Password state, toggling the use special characters toggle should send ToggleSpecialCharactersChange action`() {
composeTestRule.setContent {
GeneratorScreen(viewModel = viewModel)
}
composeTestRule.onNodeWithText("Special characters (!@#$%^*)")
.onChildren()
.filterToOne(expectValue(Role, Switch))
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleSpecialCharactersChange(
useSpecialChars = true,
),
)
}
}
@Test
fun `in Passcode_Password state, decrementing the minimum numbers counter should send MinNumbersCounterChange action`() {
val initialMinNumbers = 1
updateState(
GeneratorState(
generatedText = "Placeholder",
selectedType = GeneratorState.MainType.Passcode(GeneratorState.MainType.Passcode.PasscodeType.Password()),
),
)
composeTestRule.setContent {
GeneratorScreen(viewModel = viewModel)
}
composeTestRule.onNodeWithContentDescription("Minimum numbers, 1")
.onChildren()
.filterToOne(hasContentDescription("\u2212"))
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange(
minNumbers = initialMinNumbers - 1,
),
)
}
}
@Test
fun `in Passcode_Password state, incrementing the minimum numbers counter should send MinNumbersCounterChange action`() {
val initialMinNumbers = 1
updateState(
GeneratorState(
generatedText = "Placeholder",
selectedType = GeneratorState.MainType.Passcode(GeneratorState.MainType.Passcode.PasscodeType.Password()),
),
)
composeTestRule.setContent {
GeneratorScreen(viewModel = viewModel)
}
composeTestRule.onNodeWithContentDescription("Minimum numbers, 1")
.onChildren()
.filterToOne(hasContentDescription("+"))
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange(
minNumbers = initialMinNumbers + 1,
),
)
}
}
@Test
fun `in Passcode_Password state, decrementing the minimum special characters counter should send MinSpecialCharactersChange action`() {
val initialSpecialChars = 1
updateState(
GeneratorState(
generatedText = "Placeholder",
selectedType = GeneratorState.MainType.Passcode(GeneratorState.MainType.Passcode.PasscodeType.Password()),
),
)
composeTestRule.setContent {
GeneratorScreen(viewModel = viewModel)
}
composeTestRule.onNodeWithContentDescription("Minimum special, 1")
.onChildren()
.filterToOne(hasContentDescription("\u2212"))
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinSpecialCharactersChange(
minSpecial = initialSpecialChars - 1,
),
)
}
}
@Test
fun `in Passcode_Password state, incrementing the minimum special characters counter should send MinSpecialCharactersChange action`() {
val initialSpecialChars = 1
updateState(
GeneratorState(
generatedText = "Placeholder",
selectedType = GeneratorState.MainType.Passcode(GeneratorState.MainType.Passcode.PasscodeType.Password()),
),
)
composeTestRule.setContent {
GeneratorScreen(viewModel = viewModel)
}
composeTestRule.onNodeWithContentDescription("Minimum special, 1")
.onChildren()
.filterToOne(hasContentDescription("+"))
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinSpecialCharactersChange(
minSpecial = initialSpecialChars + 1,
),
)
}
}
@Test
fun `in Passcode_Password state, toggling the use avoid ambiguous characters toggle should send ToggleSpecialCharactersChange action`() {
composeTestRule.setContent {
GeneratorScreen(viewModel = viewModel)
}
composeTestRule.onNodeWithText("Avoid ambiguous characters")
.onChildren()
.filterToOne(expectValue(Role, Switch))
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleAvoidAmbigousCharactersChange(
avoidAmbiguousChars = true,
),
)
}
}
//endregion Passcode Password Tests
//region Passcode Passphrase Tests
@Test
fun `in Passcode_Passphrase state, decrementing number of words should send NumWordsCounterChange action with decremented value`() {
val initialNumWords = 3
@@ -224,6 +561,8 @@ class GeneratorScreenTest : BaseComposeTest() {
}
}
//endregion Passcode Passphrase Tests
private fun updateState(state: GeneratorState) {
mutableStateFlow.value = state
}

View File

@@ -106,6 +106,210 @@ class GeneratorViewModelTest : BaseViewModelTest() {
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Nested
inner class PasswordActions {
private val defaultPasswordState = createPasswordState()
private lateinit var viewModel: GeneratorViewModel
@BeforeEach
fun setup() {
viewModel = GeneratorViewModel(initialSavedStateHandle)
}
@Test
fun `SliderLengthChange should update password length correctly to new value`() = runTest {
viewModel.eventFlow.test {
val newLength = 16
viewModel.actionChannel.trySend(
GeneratorAction.MainType.Passcode.PasscodeType.Password.SliderLengthChange(
length = newLength,
),
)
val expectedState = defaultPasswordState.copy(
generatedText = "redlohecalP",
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
length = newLength,
),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
}
}
@Test
fun `ToggleCapitalLettersChange should update useCapitals correctly`() = runTest {
viewModel.eventFlow.test {
val useCapitals = true
viewModel.actionChannel.trySend(
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleCapitalLettersChange(
useCapitals = useCapitals,
),
)
val expectedState = defaultPasswordState.copy(
generatedText = "redlohecalP",
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
useCapitals = useCapitals,
),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
}
}
@Test
fun `ToggleLowercaseLettersChange should update useLowercase correctly`() = runTest {
viewModel.eventFlow.test {
val useLowercase = true
viewModel.actionChannel.trySend(
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleLowercaseLettersChange(
useLowercase = useLowercase,
),
)
val expectedState = defaultPasswordState.copy(
generatedText = "redlohecalP",
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
useLowercase = useLowercase,
),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
}
}
@Test
fun `ToggleNumbersChange should update useNumbers correctly`() = runTest {
viewModel.eventFlow.test {
val useNumbers = true
viewModel.actionChannel.trySend(
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleNumbersChange(
useNumbers = useNumbers,
),
)
val expectedState = defaultPasswordState.copy(
generatedText = "redlohecalP",
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
useNumbers = useNumbers,
),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
}
}
@Test
fun `ToggleSpecialCharactersChange should update useSpecialChars correctly`() = runTest {
viewModel.eventFlow.test {
val useSpecialChars = true
viewModel.actionChannel.trySend(
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleSpecialCharactersChange(
useSpecialChars = useSpecialChars,
),
)
val expectedState = defaultPasswordState.copy(
generatedText = "redlohecalP",
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
useSpecialChars = useSpecialChars,
),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
}
}
@Test
fun `MinNumbersCounterChange should update minNumbers correctly`() = runTest {
viewModel.eventFlow.test {
val minNumbers = 4
viewModel.actionChannel.trySend(
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange(
minNumbers = minNumbers,
),
)
val expectedState = defaultPasswordState.copy(
generatedText = "redlohecalP",
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
minNumbers = minNumbers,
),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
}
}
@Test
fun `MinSpecialCharactersChange should update minSpecial correctly`() = runTest {
viewModel.eventFlow.test {
val minSpecial = 2
viewModel.actionChannel.trySend(
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinSpecialCharactersChange(
minSpecial = minSpecial,
),
)
val expectedState = defaultPasswordState.copy(
generatedText = "redlohecalP",
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
minSpecial = minSpecial,
),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
}
}
@Test
fun `ToggleAvoidAmbigousCharactersChange should update avoidAmbiguousChars correctly`() =
runTest {
viewModel.eventFlow.test {
val avoidAmbiguousChars = true
viewModel.actionChannel.trySend(
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleAvoidAmbigousCharactersChange(
avoidAmbiguousChars = avoidAmbiguousChars,
),
)
val expectedState = defaultPasswordState.copy(
generatedText = "redlohecalP",
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
avoidAmbiguousChars = avoidAmbiguousChars,
),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
}
}
}
@Nested
inner class PassphraseActions {
@@ -221,14 +425,32 @@ class GeneratorViewModelTest : BaseViewModelTest() {
}
//region Helper Functions
@Suppress("LongParameterList")
private fun createPasswordState(
generatedText: String = "Placeholder",
length: Int = 10,
length: Int = 14,
useCapitals: Boolean = true,
useLowercase: Boolean = true,
useNumbers: Boolean = true,
useSpecialChars: Boolean = false,
minNumbers: Int = 1,
minSpecial: Int = 1,
avoidAmbiguousChars: Boolean = false,
): GeneratorState =
GeneratorState(
generatedText = generatedText,
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(length = length),
GeneratorState.MainType.Passcode.PasscodeType.Password(
length = length,
useCapitals = useCapitals,
useLowercase = useLowercase,
useNumbers = useNumbers,
useSpecialChars = useSpecialChars,
minNumbers = minNumbers,
minSpecial = minSpecial,
avoidAmbiguousChars = avoidAmbiguousChars,
),
),
)