BIT-662: Adding UI for random word generator (#313)

This commit is contained in:
joshua-livefront 2023-12-04 13:21:16 -05:00 committed by Álison Fernandes
parent b0e930b098
commit ff8b9cd0b4
4 changed files with 338 additions and 7 deletions

View file

@ -150,6 +150,10 @@ fun GeneratorScreen(
CatchAllEmailHandlers.create(viewModel = viewModel)
}
val randomWordHandlers = remember(viewModel) {
RandomWordHandlers.create(viewModel = viewModel)
}
val scrollBehavior =
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
@ -179,6 +183,7 @@ fun GeneratorScreen(
passphraseHandlers = passphraseHandlers,
plusAddressedEmailHandlers = plusAddressedEmailHandlers,
catchAllEmailHandlers = catchAllEmailHandlers,
randomWordHandlers = randomWordHandlers,
modifier = Modifier.padding(innerPadding),
)
}
@ -199,6 +204,7 @@ private fun ScrollContent(
passphraseHandlers: PassphraseHandlers,
plusAddressedEmailHandlers: PlusAddressedEmailHandlers,
catchAllEmailHandlers: CatchAllEmailHandlers,
randomWordHandlers: RandomWordHandlers,
modifier: Modifier = Modifier,
) {
Column(
@ -248,6 +254,7 @@ private fun ScrollContent(
onSubStateOptionClicked = onUsernameSubStateOptionClicked,
plusAddressedEmailHandlers = plusAddressedEmailHandlers,
catchAllEmailHandlers = catchAllEmailHandlers,
randomWordHandlers = randomWordHandlers,
)
}
}
@ -703,6 +710,7 @@ private fun ColumnScope.UsernameTypeItems(
onSubStateOptionClicked: (GeneratorState.MainType.Username.UsernameTypeOption) -> Unit,
plusAddressedEmailHandlers: PlusAddressedEmailHandlers,
catchAllEmailHandlers: CatchAllEmailHandlers,
randomWordHandlers: RandomWordHandlers,
) {
UsernameOptionsItem(usernameState, onSubStateOptionClicked)
@ -726,7 +734,10 @@ private fun ColumnScope.UsernameTypeItems(
}
is GeneratorState.MainType.Username.UsernameType.RandomWord -> {
// TODO: Implement RandomWord BIT-658
RandomWordTypeContent(
randomWordTypeState = selectedType,
randomWordHandlers = randomWordHandlers,
)
}
}
}
@ -833,6 +844,58 @@ private fun CatchAllEmailTextInputItem(
//endregion CatchAllEmailType Composables
//region Random Word Composables
@Composable
private fun ColumnScope.RandomWordTypeContent(
randomWordTypeState: GeneratorState.MainType.Username.UsernameType.RandomWord,
randomWordHandlers: RandomWordHandlers,
) {
Spacer(modifier = Modifier.height(16.dp))
RandomWordCapitalizeToggleItem(
capitalize = randomWordTypeState.capitalize,
onRandomWordCapitalizeToggleChange = randomWordHandlers.onCapitalizeChange,
)
RandomWordIncludeNumberToggleItem(
includeNumber = randomWordTypeState.includeNumber,
onRandomWordIncludeNumberToggleChange = randomWordHandlers.onIncludeNumberChange,
)
}
@Composable
private fun RandomWordCapitalizeToggleItem(
capitalize: Boolean,
onRandomWordCapitalizeToggleChange: (Boolean) -> Unit,
) {
BitwardenWideSwitch(
label = stringResource(id = R.string.capitalize),
isChecked = capitalize,
onCheckedChange = onRandomWordCapitalizeToggleChange,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
@Composable
private fun RandomWordIncludeNumberToggleItem(
includeNumber: Boolean,
onRandomWordIncludeNumberToggleChange: (Boolean) -> Unit,
) {
BitwardenWideSwitch(
label = stringResource(id = R.string.include_number),
isChecked = includeNumber,
onCheckedChange = onRandomWordIncludeNumberToggleChange,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
//endregion Random Word Composables
@Preview(showBackground = true)
@Composable
private fun GeneratorPreview() {
@ -1038,3 +1101,45 @@ private class CatchAllEmailHandlers(
}
}
}
/**
* A class dedicated to handling user interactions related to Random Word
* configuration.
* Each lambda corresponds to a specific user action, allowing for easy delegation of
* logic when user input is detected.
*/
private class RandomWordHandlers(
val onCapitalizeChange: (Boolean) -> Unit,
val onIncludeNumberChange: (Boolean) -> Unit,
) {
companion object {
fun create(viewModel: GeneratorViewModel): RandomWordHandlers {
return RandomWordHandlers(
onCapitalizeChange = { shouldCapitalize ->
viewModel.trySendAction(
GeneratorAction
.MainType
.Username
.UsernameType
.RandomWord
.ToggleCapitalizeChange(
capitalize = shouldCapitalize,
),
)
},
onIncludeNumberChange = { shouldIncludeNumber ->
viewModel.trySendAction(
GeneratorAction
.MainType
.Username
.UsernameType
.RandomWord
.ToggleIncludeNumberChange(
includeNumber = shouldIncludeNumber,
),
)
},
)
}
}
}

View file

@ -20,9 +20,10 @@ import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Pa
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeTypeOption
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.CatchAllEmail
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType.AnonAddy
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.PlusAddressedEmail
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.CatchAllEmail
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.RandomWord
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
@ -109,6 +110,10 @@ class GeneratorViewModel @Inject constructor(
is GeneratorAction.MainType.Username.UsernameType.CatchAllEmail.DomainTextChange -> {
handleCatchAllEmailTextInputChange(action)
}
is GeneratorAction.MainType.Username.UsernameType.RandomWord -> {
handleRandomWordSpecificAction(action)
}
}
}
@ -451,12 +456,12 @@ class GeneratorViewModel @Inject constructor(
is GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.ToggleCapitalizeChange,
-> {
handleToggleCapitalizeChange(action)
handlePassphraseToggleCapitalizeChange(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.ToggleIncludeNumberChange,
-> {
handleToggleIncludeNumberChange(action)
handlePassphraseToggleIncludeNumberChange(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.WordSeparatorTextChange,
@ -466,7 +471,7 @@ class GeneratorViewModel @Inject constructor(
}
}
private fun handleToggleCapitalizeChange(
private fun handlePassphraseToggleCapitalizeChange(
action: GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.ToggleCapitalizeChange,
) {
updatePassphraseType { currentPassphraseType ->
@ -476,7 +481,7 @@ class GeneratorViewModel @Inject constructor(
}
}
private fun handleToggleIncludeNumberChange(
private fun handlePassphraseToggleIncludeNumberChange(
action: GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.ToggleIncludeNumberChange,
) {
updatePassphraseType { currentPassphraseType ->
@ -558,6 +563,50 @@ class GeneratorViewModel @Inject constructor(
//endregion Catch-All Email Specific Handlers
//region Random Word Specific Handlers
private fun handleRandomWordSpecificAction(
action: GeneratorAction.MainType.Username.UsernameType.RandomWord,
) {
when (action) {
is GeneratorAction.MainType.Username.UsernameType.RandomWord.ToggleCapitalizeChange -> {
handleRandomWordToggleCapitalizeChange(action)
}
is GeneratorAction
.MainType
.Username
.UsernameType
.RandomWord
.ToggleIncludeNumberChange,
-> {
handleRandomWordToggleIncludeNumberChange(action)
}
}
}
private fun handleRandomWordToggleCapitalizeChange(
action: GeneratorAction.MainType.Username.UsernameType.RandomWord.ToggleCapitalizeChange,
) {
updateRandomWordType { currentRandomWordType ->
currentRandomWordType.copy(
capitalize = action.capitalize,
)
}
}
private fun handleRandomWordToggleIncludeNumberChange(
action: GeneratorAction.MainType.Username.UsernameType.RandomWord.ToggleIncludeNumberChange,
) {
updateRandomWordType { currentRandomWordType ->
currentRandomWordType.copy(
includeNumber = action.includeNumber,
)
}
}
//endregion Random Word Specific Handlers
//region Utility Functions
private inline fun updateGeneratorMainType(
@ -653,6 +702,18 @@ class GeneratorViewModel @Inject constructor(
}
}
private inline fun updateRandomWordType(
crossinline block: (RandomWord) -> RandomWord,
) {
updateGeneratorMainTypeUsername { currentSelectedType ->
val currentUsernameType = currentSelectedType.selectedType
if (currentUsernameType !is RandomWord) {
return@updateGeneratorMainTypeUsername currentSelectedType
}
currentSelectedType.copy(selectedType = block(currentUsernameType))
}
}
//endregion Utility Functions
companion object {
@ -1262,6 +1323,26 @@ sealed class GeneratorAction {
*/
data class DomainTextChange(val domain: String) : CatchAllEmail()
}
/**
* Represents actions specifically related to Random Word.
*/
sealed class RandomWord : UsernameType() {
/**
* Fired when the "capitalize" toggle is changed.
*
* @property capitalize The new value of the "capitalize" toggle.
*/
data class ToggleCapitalizeChange(val capitalize: Boolean) : RandomWord()
/**
* Fired when the "include number" toggle is changed.
*
* @property includeNumber The new value of the "include number" toggle.
*/
data class ToggleIncludeNumberChange(val includeNumber: Boolean) : RandomWord()
}
}
}
}

View file

@ -993,6 +993,10 @@ class GeneratorScreenTest : BaseComposeTest() {
}
}
//endregion Username Plus Addressed Email Tests
//region Catch-All Email Tests
@Suppress("MaxLineLength")
@Test
fun `in Username_CatchAllEmail state, updating text in email field should send EmailTextChange action`() {
@ -1028,7 +1032,69 @@ class GeneratorScreenTest : BaseComposeTest() {
}
}
//endregion Username Plus Addressed Email Tests
//endregion Catch-All Email Tests
//region Random Word Tests
@Suppress("MaxLineLength")
@Test
fun `in Username_RandomWord state, toggling capitalize should send ToggleCapitalizeChange action`() {
updateState(
GeneratorState(
generatedText = "Placeholder",
selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.RandomWord(),
),
),
)
composeTestRule.setContent {
GeneratorScreen(viewModel = viewModel)
}
composeTestRule.onNodeWithText("Capitalize")
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Username.UsernameType.RandomWord.ToggleCapitalizeChange(
capitalize = true,
),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `in Username_RandomWord state, toggling the include number toggle should send ToggleIncludeNumberChange action`() {
updateState(
GeneratorState(
generatedText = "Placeholder",
selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.RandomWord(),
),
),
)
composeTestRule.setContent {
GeneratorScreen(viewModel = viewModel)
}
composeTestRule.onNodeWithText("Include number")
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Username.UsernameType.RandomWord.ToggleIncludeNumberChange(
includeNumber = true,
),
)
}
}
//endregion Random Word Tests
private fun updateState(state: GeneratorState) {
mutableStateFlow.value = state

View file

@ -31,6 +31,9 @@ class GeneratorViewModelTest : BaseViewModelTest() {
private val catchAllEmailSavedStateHandle =
createSavedStateHandleWithState(initialCatchAllEmailState)
private val initialRandomWordState = createRandomWordState()
private val randomWordSavedStateHandle = createSavedStateHandleWithState(initialRandomWordState)
private val fakeGeneratorRepository = FakeGeneratorRepository().apply {
setMockGeneratePasswordResult(
GeneratedPasswordResult.Success("defaultPassword"),
@ -872,6 +875,67 @@ class GeneratorViewModelTest : BaseViewModelTest() {
assertEquals(expectedState, viewModel.stateFlow.value)
}
}
@Nested
inner class RandomWordActions {
private val defaultRandomWordState = createRandomWordState()
private lateinit var viewModel: GeneratorViewModel
@BeforeEach
fun setup() {
viewModel = GeneratorViewModel(randomWordSavedStateHandle, fakeGeneratorRepository)
}
@Suppress("MaxLineLength")
@Test
fun `ToggleCapitalizeChange should update the capitalize property correctly`() = runTest {
viewModel.actionChannel.trySend(
GeneratorAction
.MainType
.Username
.UsernameType
.RandomWord
.ToggleCapitalizeChange(
capitalize = true,
),
)
val expectedState = defaultRandomWordState.copy(
selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.RandomWord(
capitalize = true,
),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Suppress("MaxLineLength")
@Test
fun `ToggleIncludeNumberChange should update the includeNumber property correctly`() = runTest {
viewModel.actionChannel.trySend(
GeneratorAction
.MainType
.Username
.UsernameType
.RandomWord
.ToggleIncludeNumberChange(
includeNumber = true,
),
)
val expectedState = defaultRandomWordState.copy(
selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.RandomWord(
includeNumber = true,
),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
}
}
//region Helper Functions
@Suppress("LongParameterList")
@ -947,6 +1011,21 @@ class GeneratorViewModelTest : BaseViewModelTest() {
),
)
private fun createRandomWordState(
generatedText: String = "defaultRandomWord",
capitalize: Boolean = false,
includeNumber: Boolean = false,
): GeneratorState =
GeneratorState(
generatedText = generatedText,
selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.RandomWord(
capitalize = capitalize,
includeNumber = includeNumber,
),
),
)
private fun createSavedStateHandleWithState(state: GeneratorState) =
SavedStateHandle().apply {
set("state", state)