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

This commit is contained in:
joshua-livefront 2023-10-12 14:41:58 -04:00 committed by Álison Fernandes
parent 2cda9db9a2
commit 9879e6fd23
4 changed files with 1229 additions and 78 deletions

View file

@ -12,11 +12,16 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
@ -29,6 +34,8 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
@ -42,6 +49,8 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenTextFieldWithTwoIcons
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_LENGTH_SLIDER_MAX
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_LENGTH_SLIDER_MIN
/**
* Top level composable for the generator screen.
@ -84,7 +93,90 @@ fun GeneratorScreen(viewModel: GeneratorViewModel = hiltViewModel()) {
}
}
val onNumWordsCounterChange: (Int) -> Unit = remember(viewModel) {
val onPasswordSliderLengthChange: (Int) -> Unit = remember(viewModel) {
{ newLength ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.SliderLengthChange(
length = newLength,
),
)
}
}
val onPasswordToggleCapitalLettersChange: (Boolean) -> Unit = remember(viewModel) {
{ shouldUseCapitals ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleCapitalLettersChange(
useCapitals = shouldUseCapitals,
),
)
}
}
val onPasswordToggleLowercaseLettersChange: (Boolean) -> Unit = remember(viewModel) {
{ shouldUseLowercase ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleLowercaseLettersChange(
useLowercase = shouldUseLowercase,
),
)
}
}
val onPasswordToggleNumbersChange: (Boolean) -> Unit = remember(viewModel) {
{ shouldUseNumbers ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleNumbersChange(
useNumbers = shouldUseNumbers,
),
)
}
}
val onPasswordToggleSpecialCharactersChange: (Boolean) -> Unit = remember(viewModel) {
{ shouldUseSpecialChars ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleSpecialCharactersChange(
useSpecialChars = shouldUseSpecialChars,
),
)
}
}
val onPasswordMinNumbersCounterChange: (Int) -> Unit = remember(viewModel) {
{ newMinNumbers ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange(
minNumbers = newMinNumbers,
),
)
}
}
val onPasswordMinSpecialCharactersChange: (Int) -> Unit = remember(viewModel) {
{ newMinSpecial ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinSpecialCharactersChange(
minSpecial = newMinSpecial,
),
)
}
}
val onPasswordToggleAvoidAmbiguousCharsChange: (Boolean) -> Unit = remember(viewModel) {
{ shouldAvoidAmbiguousChars ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleAvoidAmbigousCharactersChange(
avoidAmbiguousChars = shouldAvoidAmbiguousChars,
),
)
}
}
val onPassphraseNumWordsCounterChange: (Int) -> Unit = remember(viewModel) {
{ changeInCounter ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.NumWordsCounterChange(
@ -104,7 +196,7 @@ fun GeneratorScreen(viewModel: GeneratorViewModel = hiltViewModel()) {
}
}
val onIncludeNumberToggleChange: (Boolean) -> Unit = remember(viewModel) {
val onPassphraseIncludeNumberToggleChange: (Boolean) -> Unit = remember(viewModel) {
{ shouldIncludeNumber ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.ToggleIncludeNumberChange(
@ -114,7 +206,7 @@ fun GeneratorScreen(viewModel: GeneratorViewModel = hiltViewModel()) {
}
}
val onWordSeparatorChange: (Char?) -> Unit = remember(viewModel) {
val onPassphraseWordSeparatorChange: (Char?) -> Unit = remember(viewModel) {
{ newSeparator ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.WordSeparatorTextChange(
@ -137,16 +229,29 @@ fun GeneratorScreen(viewModel: GeneratorViewModel = hiltViewModel()) {
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { innerPadding ->
ScrollContent(
state,
onRegenerateClick,
onCopyClick,
onMainStateOptionClicked,
onPasscodeOptionClicked,
onNumWordsCounterChange,
onWordSeparatorChange,
onPassphraseCapitalizeToggleChange,
onIncludeNumberToggleChange,
Modifier.padding(innerPadding),
state = state,
onRegenerateClick = onRegenerateClick,
onCopyClick = onCopyClick,
onMainStateOptionClicked = onMainStateOptionClicked,
onSubStateOptionClicked = onPasscodeOptionClicked,
// Password handlers
onPasswordSliderLengthChange = onPasswordSliderLengthChange,
onPasswordToggleCapitalLettersChange = onPasswordToggleCapitalLettersChange,
onPasswordToggleLowercaseLettersChange = onPasswordToggleLowercaseLettersChange,
onPasswordToggleNumbersChange = onPasswordToggleNumbersChange,
onPasswordToggleSpecialCharactersChange = onPasswordToggleSpecialCharactersChange,
onPasswordMinNumbersCounterChange = onPasswordMinNumbersCounterChange,
onPasswordMinSpecialCharactersChange = onPasswordMinSpecialCharactersChange,
onPasswordToggleAvoidAmbiguousCharsChange = onPasswordToggleAvoidAmbiguousCharsChange,
// Passphrase handlers
onPassphraseNumWordsCounterChange = onPassphraseNumWordsCounterChange,
onPassphraseWordSeparatorChange = onPassphraseWordSeparatorChange,
onPassphraseCapitalizeToggleChange = onPassphraseCapitalizeToggleChange,
onPassphraseIncludeNumberToggleChange = onPassphraseIncludeNumberToggleChange,
modifier = Modifier.padding(innerPadding),
)
}
}
@ -161,10 +266,18 @@ private fun ScrollContent(
onCopyClick: () -> Unit,
onMainStateOptionClicked: (GeneratorState.MainTypeOption) -> Unit,
onSubStateOptionClicked: (GeneratorState.MainType.Passcode.PasscodeTypeOption) -> Unit,
onNumWordsCounterChange: (Int) -> Unit,
onWordSeparatorChange: (Char?) -> Unit,
onCapitalizeToggleChange: (Boolean) -> Unit,
onIncludeNumberToggleChange: (Boolean) -> Unit,
onPasswordSliderLengthChange: (Int) -> Unit,
onPasswordToggleCapitalLettersChange: (Boolean) -> Unit,
onPasswordToggleLowercaseLettersChange: (Boolean) -> Unit,
onPasswordToggleNumbersChange: (Boolean) -> Unit,
onPasswordToggleSpecialCharactersChange: (Boolean) -> Unit,
onPasswordMinNumbersCounterChange: (Int) -> Unit,
onPasswordMinSpecialCharactersChange: (Int) -> Unit,
onPasswordToggleAvoidAmbiguousCharsChange: (Boolean) -> Unit,
onPassphraseNumWordsCounterChange: (Int) -> Unit,
onPassphraseWordSeparatorChange: (Char?) -> Unit,
onPassphraseCapitalizeToggleChange: (Boolean) -> Unit,
onPassphraseIncludeNumberToggleChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
@ -202,12 +315,26 @@ private fun ScrollContent(
when (val selectedType = state.selectedType) {
is GeneratorState.MainType.Passcode -> {
PasscodeTypeItems(
selectedType,
onSubStateOptionClicked,
onNumWordsCounterChange,
onWordSeparatorChange,
onCapitalizeToggleChange,
onIncludeNumberToggleChange,
passcodeState = selectedType,
onSubStateOptionClicked = onSubStateOptionClicked,
// Password handlers
onPasswordSliderLengthChange = onPasswordSliderLengthChange,
onPasswordToggleCapitalLettersChange = onPasswordToggleCapitalLettersChange,
onPasswordToggleLowercaseLettersChange = onPasswordToggleLowercaseLettersChange,
onPasswordToggleNumbersChange = onPasswordToggleNumbersChange,
onPasswordToggleSpecialCharactersChange =
onPasswordToggleSpecialCharactersChange,
onPasswordMinNumbersCounterChange = onPasswordMinNumbersCounterChange,
onPasswordMinSpecialCharactersChange = onPasswordMinSpecialCharactersChange,
onPasswordToggleAvoidAmbiguousCharsChange =
onPasswordToggleAvoidAmbiguousCharsChange,
// Passphrase handlers
onPassphraseNumWordsCounterChange = onPassphraseNumWordsCounterChange,
onPassphraseWordSeparatorChange = onPassphraseWordSeparatorChange,
onPassphraseCapitalizeToggleChange = onPassphraseCapitalizeToggleChange,
onPassphraseIncludeNumberToggleChange = onPassphraseIncludeNumberToggleChange,
)
}
@ -265,49 +392,49 @@ private fun MainStateOptionsItem(
//region PasscodeType Composables
/**
* A composable function to represent a collection of passcode type items based on the selected
* [GeneratorState.MainType.Passcode.PasscodeType]. It dynamically displays content depending on
* the currently selected passcode type.
*
* @param passcodeState The current state of the passcode generator,
* holding the selected passcode type and other settings.
* @param onSubStateOptionClicked A lambda function invoked when a substate option is clicked.
* It takes the selected [GeneratorState.MainType.Passcode.PasscodeTypeOption] as a parameter.
* @param onNumWordsCounterChange A lambda function invoked when there is a change
* in the number of words for passphrase. It takes the updated number of words as a parameter.
* @param onWordSeparatorChange A lambda function invoked when there is a change
* in the word separator character for passphrase. It takes the updated character as a parameter,
* `null` if there is no separator.
* @param onCapitalizeToggleChange A lambda function invoked when the capitalize
* toggle state changes for passphrase. It takes the updated toggle state as a parameter.
* @param onIncludeNumberToggleChange A lambda function invoked when the include number toggle
* state changes for passphrase. It takes the updated toggle state as a parameter.
*/
@Composable
fun PasscodeTypeItems(
private fun PasscodeTypeItems(
passcodeState: GeneratorState.MainType.Passcode,
onSubStateOptionClicked: (GeneratorState.MainType.Passcode.PasscodeTypeOption) -> Unit,
onNumWordsCounterChange: (Int) -> Unit,
onWordSeparatorChange: (Char?) -> Unit,
onCapitalizeToggleChange: (Boolean) -> Unit,
onIncludeNumberToggleChange: (Boolean) -> Unit,
onPasswordSliderLengthChange: (Int) -> Unit,
onPasswordToggleCapitalLettersChange: (Boolean) -> Unit,
onPasswordToggleLowercaseLettersChange: (Boolean) -> Unit,
onPasswordToggleNumbersChange: (Boolean) -> Unit,
onPasswordToggleSpecialCharactersChange: (Boolean) -> Unit,
onPasswordMinNumbersCounterChange: (Int) -> Unit,
onPasswordMinSpecialCharactersChange: (Int) -> Unit,
onPasswordToggleAvoidAmbiguousCharsChange: (Boolean) -> Unit,
onPassphraseNumWordsCounterChange: (Int) -> Unit,
onPassphraseWordSeparatorChange: (Char?) -> Unit,
onPassphraseCapitalizeToggleChange: (Boolean) -> Unit,
onPassphraseIncludeNumberToggleChange: (Boolean) -> Unit,
) {
PasscodeOptionsItem(passcodeState, onSubStateOptionClicked)
when (val selectedType = passcodeState.selectedType) {
is GeneratorState.MainType.Passcode.PasscodeType.Passphrase -> {
PassphraseTypeContent(
selectedType,
onNumWordsCounterChange,
onWordSeparatorChange,
onCapitalizeToggleChange,
onIncludeNumberToggleChange,
is GeneratorState.MainType.Passcode.PasscodeType.Password -> {
PasswordTypeContent(
passwordTypeState = selectedType,
onPasswordSliderLengthChange = onPasswordSliderLengthChange,
onPasswordToggleCapitalLettersChange = onPasswordToggleCapitalLettersChange,
onPasswordToggleLowercaseLettersChange = onPasswordToggleLowercaseLettersChange,
onPasswordToggleNumbersChange = onPasswordToggleNumbersChange,
onPasswordToggleSpecialCharactersChange = onPasswordToggleSpecialCharactersChange,
onPasswordMinNumbersCounterChange = onPasswordMinNumbersCounterChange,
onPasswordMinSpecialCharactersChange = onPasswordMinSpecialCharactersChange,
onPasswordToggleAvoidAmbiguousCharsChange =
onPasswordToggleAvoidAmbiguousCharsChange,
)
}
is GeneratorState.MainType.Passcode.PasscodeType.Password -> {
// TODO(BIT-334): Render UI for Password type
is GeneratorState.MainType.Passcode.PasscodeType.Passphrase -> {
PassphraseTypeContent(
passphraseTypeState = selectedType,
onPassphraseNumWordsCounterChange = onPassphraseNumWordsCounterChange,
onPassphraseWordSeparatorChange = onPassphraseWordSeparatorChange,
onPassphraseCapitalizeToggleChange = onPassphraseCapitalizeToggleChange,
onPassphraseIncludeNumberToggleChange = onPassphraseIncludeNumberToggleChange,
)
}
}
}
@ -335,34 +462,238 @@ private fun PasscodeOptionsItem(
//endregion PasscodeType Composables
//region PasswordType Composables
@Composable
private fun PasswordTypeContent(
passwordTypeState: GeneratorState.MainType.Passcode.PasscodeType.Password,
onPasswordSliderLengthChange: (Int) -> Unit,
onPasswordToggleCapitalLettersChange: (Boolean) -> Unit,
onPasswordToggleLowercaseLettersChange: (Boolean) -> Unit,
onPasswordToggleNumbersChange: (Boolean) -> Unit,
onPasswordToggleSpecialCharactersChange: (Boolean) -> Unit,
onPasswordMinNumbersCounterChange: (Int) -> Unit,
onPasswordMinSpecialCharactersChange: (Int) -> Unit,
onPasswordToggleAvoidAmbiguousCharsChange: (Boolean) -> Unit,
) {
PasswordLengthSliderItem(
length = passwordTypeState.length,
onPasswordSliderLengthChange = onPasswordSliderLengthChange,
)
Column(
modifier = Modifier.fillMaxWidth(),
) {
PasswordCapitalLettersToggleItem(
useCapitals = passwordTypeState.useCapitals,
onPasswordToggleCapitalLettersChange = onPasswordToggleCapitalLettersChange,
)
PasswordLowercaseLettersToggleItem(
useLowercase = passwordTypeState.useLowercase,
onPasswordToggleLowercaseLettersChange = onPasswordToggleLowercaseLettersChange,
)
PasswordNumbersToggleItem(
useNumbers = passwordTypeState.useNumbers,
onPasswordToggleNumbersChange = onPasswordToggleNumbersChange,
)
PasswordSpecialCharactersToggleItem(
useSpecialChars = passwordTypeState.useSpecialChars,
onPasswordToggleSpecialCharactersChange = onPasswordToggleSpecialCharactersChange,
)
}
PasswordMinNumbersCounterItem(
minNumbers = passwordTypeState.minNumbers,
onPasswordMinNumbersCounterChange = onPasswordMinNumbersCounterChange,
)
PasswordMinSpecialCharactersCounterItem(
minSpecial = passwordTypeState.minSpecial,
onPasswordMinSpecialCharactersChange = onPasswordMinSpecialCharactersChange,
)
PasswordAvoidAmbiguousCharsToggleItem(
avoidAmbiguousChars = passwordTypeState.avoidAmbiguousChars,
onPasswordToggleAvoidAmbiguousCharsChange = onPasswordToggleAvoidAmbiguousCharsChange,
)
}
@Composable
private fun PasswordLengthSliderItem(
length: Int,
onPasswordSliderLengthChange: (Int) -> Unit,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.semantics(mergeDescendants = true) {},
) {
OutlinedTextField(
value = length.toString(),
readOnly = true,
onValueChange = { newText ->
newText.toIntOrNull()?.let { newValue ->
onPasswordSliderLengthChange(newValue)
}
},
label = { Text(stringResource(id = R.string.length)) },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier
.wrapContentWidth()
.widthIn(max = 71.dp),
)
Slider(
value = length.toFloat(),
onValueChange = { newValue ->
onPasswordSliderLengthChange(newValue.toInt())
},
valueRange =
PASSWORD_LENGTH_SLIDER_MIN.toFloat()..PASSWORD_LENGTH_SLIDER_MAX.toFloat(),
steps = PASSWORD_LENGTH_SLIDER_MAX - 1,
)
}
}
@Composable
private fun PasswordCapitalLettersToggleItem(
useCapitals: Boolean,
onPasswordToggleCapitalLettersChange: (Boolean) -> Unit,
) {
BitwardenWideSwitch(
label = stringResource(id = R.string.uppercase_ato_z),
isChecked = useCapitals,
onCheckedChange = onPasswordToggleCapitalLettersChange,
)
}
@Composable
private fun PasswordLowercaseLettersToggleItem(
useLowercase: Boolean,
onPasswordToggleLowercaseLettersChange: (Boolean) -> Unit,
) {
BitwardenWideSwitch(
label = stringResource(id = R.string.lowercase_ato_z),
isChecked = useLowercase,
onCheckedChange = onPasswordToggleLowercaseLettersChange,
)
}
@Composable
private fun PasswordNumbersToggleItem(
useNumbers: Boolean,
onPasswordToggleNumbersChange: (Boolean) -> Unit,
) {
BitwardenWideSwitch(
label = stringResource(id = R.string.numbers_zero_to_nine),
isChecked = useNumbers,
onCheckedChange = onPasswordToggleNumbersChange,
)
}
@Composable
private fun PasswordSpecialCharactersToggleItem(
useSpecialChars: Boolean,
onPasswordToggleSpecialCharactersChange: (Boolean) -> Unit,
) {
BitwardenWideSwitch(
label = stringResource(id = R.string.special_characters),
isChecked = useSpecialChars,
onCheckedChange = onPasswordToggleSpecialCharactersChange,
)
}
@Composable
private fun PasswordMinNumbersCounterItem(
minNumbers: Int,
onPasswordMinNumbersCounterChange: (Int) -> Unit,
) {
BitwardenTextFieldWithTwoIcons(
label = stringResource(id = R.string.min_numbers),
value = minNumbers.toString(),
firstIconResource = IconResource(
iconPainter = painterResource(id = R.drawable.ic_minus),
contentDescription = "\u2212",
),
onFirstIconClick = {
onPasswordMinNumbersCounterChange(minNumbers - 1)
},
secondIconResource = IconResource(
iconPainter = painterResource(id = R.drawable.ic_plus),
contentDescription = "+",
),
onSecondIconClick = {
onPasswordMinNumbersCounterChange(minNumbers + 1)
},
)
}
@Composable
private fun PasswordMinSpecialCharactersCounterItem(
minSpecial: Int,
onPasswordMinSpecialCharactersChange: (Int) -> Unit,
) {
BitwardenTextFieldWithTwoIcons(
label = stringResource(id = R.string.min_special),
value = minSpecial.toString(),
firstIconResource = IconResource(
iconPainter = painterResource(id = R.drawable.ic_minus),
contentDescription = "\u2212",
),
onFirstIconClick = {
onPasswordMinSpecialCharactersChange(minSpecial - 1)
},
secondIconResource = IconResource(
iconPainter = painterResource(id = R.drawable.ic_plus),
contentDescription = "+",
),
onSecondIconClick = {
onPasswordMinSpecialCharactersChange(minSpecial + 1)
},
)
}
@Composable
private fun PasswordAvoidAmbiguousCharsToggleItem(
avoidAmbiguousChars: Boolean,
onPasswordToggleAvoidAmbiguousCharsChange: (Boolean) -> Unit,
) {
BitwardenWideSwitch(
label = stringResource(id = R.string.avoid_ambiguous_characters),
isChecked = avoidAmbiguousChars,
onCheckedChange = onPasswordToggleAvoidAmbiguousCharsChange,
)
}
//endregion PasswordType Composables
//region PassphraseType Composables
@Composable
private fun PassphraseTypeContent(
passphraseTypeState: GeneratorState.MainType.Passcode.PasscodeType.Passphrase,
onNumWordsCounterChange: (Int) -> Unit,
onWordSeparatorChange: (Char?) -> Unit,
onCapitalizeToggleChange: (Boolean) -> Unit,
onIncludeNumberToggleChange: (Boolean) -> Unit,
onPassphraseNumWordsCounterChange: (Int) -> Unit,
onPassphraseWordSeparatorChange: (Char?) -> Unit,
onPassphraseCapitalizeToggleChange: (Boolean) -> Unit,
onPassphraseIncludeNumberToggleChange: (Boolean) -> Unit,
) {
PassphraseNumWordsCounterItem(
numWords = passphraseTypeState.numWords,
onNumWordsCounterChange = onNumWordsCounterChange,
onPassphraseNumWordsCounterChange = onPassphraseNumWordsCounterChange,
)
PassphraseWordSeparatorInputItem(
wordSeparator = passphraseTypeState.wordSeparator,
onWordSeparatorChange = onWordSeparatorChange,
onPassphraseWordSeparatorChange = onPassphraseWordSeparatorChange,
)
Column(
modifier = Modifier.fillMaxWidth(),
) {
PassphraseCapitalizeToggleItem(
capitalize = passphraseTypeState.capitalize,
onPassphraseCapitalizeToggleChange = onCapitalizeToggleChange,
onPassphraseCapitalizeToggleChange = onPassphraseCapitalizeToggleChange,
)
PassphraseIncludeNumberToggleItem(
includeNumber = passphraseTypeState.includeNumber,
onIncludeNumberToggleChange = onIncludeNumberToggleChange,
onPassphraseIncludeNumberToggleChange = onPassphraseIncludeNumberToggleChange,
)
}
}
@ -370,7 +701,7 @@ private fun PassphraseTypeContent(
@Composable
private fun PassphraseNumWordsCounterItem(
numWords: Int,
onNumWordsCounterChange: (Int) -> Unit,
onPassphraseNumWordsCounterChange: (Int) -> Unit,
) {
BitwardenTextFieldWithTwoIcons(
label = stringResource(id = R.string.number_of_words),
@ -380,14 +711,14 @@ private fun PassphraseNumWordsCounterItem(
contentDescription = "\u2212",
),
onFirstIconClick = {
onNumWordsCounterChange(numWords - 1)
onPassphraseNumWordsCounterChange(numWords - 1)
},
secondIconResource = IconResource(
iconPainter = painterResource(id = R.drawable.ic_plus),
contentDescription = "+",
),
onSecondIconClick = {
onNumWordsCounterChange(numWords + 1)
onPassphraseNumWordsCounterChange(numWords + 1)
},
)
}
@ -395,13 +726,13 @@ private fun PassphraseNumWordsCounterItem(
@Composable
private fun PassphraseWordSeparatorInputItem(
wordSeparator: Char?,
onWordSeparatorChange: (wordSeparator: Char?) -> Unit,
onPassphraseWordSeparatorChange: (wordSeparator: Char?) -> Unit,
) {
BitwardenTextField(
label = stringResource(id = R.string.word_separator),
value = wordSeparator?.toString() ?: "",
onValueChange = {
onWordSeparatorChange(it.toCharArray().firstOrNull())
onPassphraseWordSeparatorChange(it.toCharArray().firstOrNull())
},
modifier = Modifier.width(267.dp),
)
@ -422,12 +753,12 @@ private fun PassphraseCapitalizeToggleItem(
@Composable
private fun PassphraseIncludeNumberToggleItem(
includeNumber: Boolean,
onIncludeNumberToggleChange: (Boolean) -> Unit,
onPassphraseIncludeNumberToggleChange: (Boolean) -> Unit,
) {
BitwardenWideSwitch(
label = stringResource(id = R.string.include_number),
isChecked = includeNumber,
onCheckedChange = onIncludeNumberToggleChange,
onCheckedChange = onPassphraseIncludeNumberToggleChange,
)
}

View file

@ -12,6 +12,8 @@ import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Pa
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Passphrase.Companion.PASSPHRASE_MAX_NUMBER_OF_WORDS
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Passphrase.Companion.PASSPHRASE_MIN_NUMBER_OF_WORDS
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_COUNTER_MAX
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_COUNTER_MIN
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeTypeOption
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
@ -67,6 +69,10 @@ class GeneratorViewModel @Inject constructor(
handlePasscodeTypeOptionSelect(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password -> {
handlePasswordSpecificAction(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Passphrase -> {
handlePassphraseSpecificAction(action)
}
@ -158,6 +164,139 @@ class GeneratorViewModel @Inject constructor(
//endregion Passcode Type Handlers
//region Password Specific Handlers
private fun handlePasswordSpecificAction(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password,
) {
when (action) {
is GeneratorAction.MainType.Passcode.PasscodeType.Password.SliderLengthChange,
-> {
handlePasswordLengthSliderChange(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleCapitalLettersChange,
-> {
handleToggleCapitalLetters(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleLowercaseLettersChange,
-> {
handleToggleLowercaseLetters(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleNumbersChange,
-> {
handleToggleNumbers(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleSpecialCharactersChange,
-> {
handleToggleSpecialChars(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange,
-> {
handleMinNumbersChange(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password.MinSpecialCharactersChange,
-> {
handleMinSpecialChange(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleAvoidAmbigousCharactersChange,
-> {
handleToggleAmbiguousChars(action)
}
}
}
private fun handlePasswordLengthSliderChange(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password.SliderLengthChange,
) {
val adjustedLength = action.length
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(length = adjustedLength)
}
}
private fun handleToggleCapitalLetters(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleCapitalLettersChange,
) {
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(
useCapitals = action.useCapitals,
)
}
}
private fun handleToggleLowercaseLetters(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleLowercaseLettersChange,
) {
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(useLowercase = action.useLowercase)
}
}
private fun handleToggleNumbers(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleNumbersChange,
) {
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(useNumbers = action.useNumbers)
}
}
private fun handleToggleSpecialChars(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleSpecialCharactersChange,
) {
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(useSpecialChars = action.useSpecialChars)
}
}
private fun handleMinNumbersChange(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange,
) {
val adjustedMinNumbers = action
.minNumbers
.coerceIn(PASSWORD_COUNTER_MIN, PASSWORD_COUNTER_MAX)
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(minNumbers = adjustedMinNumbers)
}
}
private fun handleMinSpecialChange(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password.MinSpecialCharactersChange,
) {
val adjustedMinSpecial = action
.minSpecial
.coerceIn(PASSWORD_COUNTER_MIN, PASSWORD_COUNTER_MAX)
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(minSpecial = adjustedMinSpecial)
}
}
private fun handleToggleAmbiguousChars(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleAvoidAmbigousCharactersChange,
) {
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(
avoidAmbiguousChars = action.avoidAmbiguousChars,
)
}
}
//endregion Password Specific Handlers
//region Passphrase Specific Handlers
private fun handlePassphraseSpecificAction(
@ -248,6 +387,18 @@ class GeneratorViewModel @Inject constructor(
}
}
private inline fun updatePasswordType(
crossinline block: (Password) -> Password,
) {
updateGeneratorMainTypePassword { currentSelectedType ->
val currentPasswordType = currentSelectedType.selectedType
if (currentPasswordType !is Password) {
return@updateGeneratorMainTypePassword currentSelectedType
}
currentSelectedType.copy(selectedType = block(currentPasswordType))
}
}
private inline fun updatePassphraseType(
crossinline block: (Passphrase) -> Passphrase,
) {
@ -268,8 +419,7 @@ class GeneratorViewModel @Inject constructor(
val INITIAL_STATE: GeneratorState = GeneratorState(
generatedText = PLACEHOLDER_GENERATED_TEXT,
selectedType = Passcode(
// TODO (BIT-634): Update the initial state to Password
selectedType = Passphrase(),
selectedType = Password(),
),
)
}
@ -358,19 +508,41 @@ data class GeneratorState(
abstract val displayStringResId: Int
/**
* Represents a standard PASSWORD type, with a specified length.
* Represents a standard PASSWORD type, with configurable options for
* length, character types, and requirements.
*
* @property length The length of the generated password.
* @property useCapitals Whether to include capital letters.
* @property useLowercase Whether to include lowercase letters.
* @property useNumbers Whether to include numbers.
* @property useSpecialChars Whether to include special characters.
* @property minNumbers The minimum number of numeric characters.
* @property minSpecial The minimum number of special characters.
* @property avoidAmbiguousChars Whether to avoid characters that look similar.
*/
@Parcelize
data class Password(
val length: Int = DEFAULT_PASSWORD_LENGTH,
val useCapitals: Boolean = true,
val useLowercase: Boolean = true,
val useNumbers: Boolean = true,
val useSpecialChars: Boolean = false,
val minNumbers: Int = MIN_NUMBERS,
val minSpecial: Int = MIN_SPECIAL,
val avoidAmbiguousChars: Boolean = false,
) : PasscodeType(), Parcelable {
override val displayStringResId: Int
get() = PasscodeTypeOption.PASSWORD.labelRes
companion object {
const val DEFAULT_PASSWORD_LENGTH: Int = 10
private const val DEFAULT_PASSWORD_LENGTH: Int = 14
private const val MIN_NUMBERS: Int = 1
private const val MIN_SPECIAL: Int = 1
const val PASSWORD_LENGTH_SLIDER_MIN: Int = 5
const val PASSWORD_LENGTH_SLIDER_MAX: Int = 128
const val PASSWORD_COUNTER_MIN: Int = 0
const val PASSWORD_COUNTER_MAX: Int = 5
}
}
@ -470,7 +642,94 @@ sealed class GeneratorAction {
/**
* Represents actions specifically related to passwords, a subtype of passcode.
*/
sealed class Password : PasscodeType()
sealed class Password : PasscodeType() {
/**
* Represents a change action for the length of the password,
* adjusted using a slider.
*
* @property length The new desired length for the password.
*/
data class SliderLengthChange(
val length: Int,
) : Password()
/**
* Represents a change action to toggle the usage of
* capital letters in the password.
*
* @property useCapitals Flag indicating whether capital letters
* should be used.
*/
data class ToggleCapitalLettersChange(
val useCapitals: Boolean,
) : Password()
/**
* Represents a change action to toggle the usage of lowercase letters
* in the password.
*
* @property useLowercase Flag indicating whether lowercase letters
* should be used.
*/
data class ToggleLowercaseLettersChange(
val useLowercase: Boolean,
) : Password()
/**
* Represents a change action to toggle the inclusion of numbers
* in the password.
*
* @property useNumbers Flag indicating whether numbers
* should be used.
*/
data class ToggleNumbersChange(
val useNumbers: Boolean,
) : Password()
/**
* Represents a change action to toggle the usage of special characters
* in the password.
*
* @property useSpecialChars Flag indicating whether special characters
* should be used.
*/
data class ToggleSpecialCharactersChange(
val useSpecialChars: Boolean,
) : Password()
/**
* Represents a change action for the minimum required number of numbers
* in the password.
*
* @property minNumbers The minimum required number of numbers
* for the password.
*/
data class MinNumbersCounterChange(
val minNumbers: Int,
) : Password()
/**
* Represents a change action for the minimum required number of special
* characters in the password.
*
* @property minSpecial The minimum required number of special characters
* for the password.
*/
data class MinSpecialCharactersChange(
val minSpecial: Int,
) : Password()
/**
* Represents a change action to toggle the avoidance of ambiguous
* characters in the password.
*
* @property avoidAmbiguousChars Flag indicating whether ambiguous characters
* should be avoided.
*/
data class ToggleAvoidAmbigousCharactersChange(
val avoidAmbiguousChars: Boolean,
) : Password()
}
/**
* Represents actions specifically related to passphrases, a subtype of passcode.

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,
),
),
)