mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
BIT-662: Adding UI for random word generator (#313)
This commit is contained in:
parent
b0e930b098
commit
ff8b9cd0b4
4 changed files with 338 additions and 7 deletions
|
@ -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,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue