PM-11176: Update generator to use segmented control (#4075)

This commit is contained in:
David Perez 2024-10-14 15:43:17 -05:00 committed by GitHub
parent 2b87cdac9e
commit efbf84238d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 958 additions and 1505 deletions

View file

@ -46,6 +46,7 @@ fun BitwardenSegmentedButton(
) {
options.forEachIndexed { index, option ->
SegmentedButton(
enabled = option.isEnabled,
selected = option.isChecked,
onClick = option.onClick,
colors = bitwardenSegmentedButtonColors(),
@ -74,5 +75,6 @@ data class SegmentedButtonState(
val text: String,
val onClick: () -> Unit,
val isChecked: Boolean,
val isEnabled: Boolean = true,
val testTag: String? = null,
)

View file

@ -20,6 +20,6 @@ fun bitwardenSegmentedButtonColors(): SegmentedButtonColors = SegmentedButtonCol
disabledActiveContentColor = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
disabledActiveBorderColor = Color.Transparent,
disabledInactiveContainerColor = BitwardenTheme.colorScheme.background.primary,
disabledInactiveContentColor = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
disabledInactiveContentColor = BitwardenTheme.colorScheme.stroke.divider,
disabledInactiveBorderColor = Color.Transparent,
)

View file

@ -36,6 +36,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.LivecycleEventEffect
import com.x8bit.bitwarden.ui.platform.base.util.scrolledContainerBottomDivider
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenMediumTopAppBar
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.appbar.action.BitwardenOverflowActionItem
@ -49,7 +50,10 @@ import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.TooltipData
import com.x8bit.bitwarden.ui.platform.components.model.TopAppBarDividerStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.segment.BitwardenSegmentedButton
import com.x8bit.bitwarden.ui.platform.components.segment.SegmentedButtonState
import com.x8bit.bitwarden.ui.platform.components.slider.BitwardenSlider
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost
import com.x8bit.bitwarden.ui.platform.components.stepper.BitwardenStepper
@ -59,8 +63,8 @@ import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
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.Passphrase.Companion.PASSPHRASE_MAX_NUMBER_OF_WORDS
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passphrase.Companion.PASSPHRASE_MIN_NUMBER_OF_WORDS
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceTypeOption
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
@ -129,17 +133,6 @@ fun GeneratorScreen(
{ viewModel.trySendAction(GeneratorAction.MainTypeOptionSelect(it)) }
}
val onPasscodeOptionClicked: (GeneratorState.MainType.Passcode.PasscodeTypeOption) -> Unit =
remember(viewModel) {
{
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeTypeOptionSelect(
it,
),
)
}
}
val onUsernameOptionClicked: (GeneratorState.MainType.Username.UsernameTypeOption) -> Unit =
remember(viewModel) {
{
@ -183,9 +176,7 @@ fun GeneratorScreen(
BitwardenScaffold(
topBar = {
when (state.generatorMode) {
is GeneratorMode.Modal.Username,
GeneratorMode.Modal.Password,
-> {
is GeneratorMode.Modal -> {
ModalAppBar(
scrollBehavior = scrollBehavior,
onCloseClick = remember(viewModel) {
@ -212,12 +203,21 @@ fun GeneratorScreen(
},
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
if (state.generatorMode == GeneratorMode.Default) {
MainStateOptionsItem(
selectedType = state.selectedType,
passcodePolicyOverride = state.passcodePolicyOverride,
possibleMainStates = state.typeOptions.toImmutableList(),
onMainStateOptionClicked = onMainStateOptionClicked,
modifier = Modifier
.scrolledContainerBottomDivider(topAppBarScrollBehavior = scrollBehavior),
)
}
ScrollContent(
state = state,
onRegenerateClick = onRegenerateClick,
onCopyClick = onCopyClick,
onMainStateOptionClicked = onMainStateOptionClicked,
onPasscodeSubStateOptionClicked = onPasscodeOptionClicked,
onUsernameSubStateOptionClicked = onUsernameOptionClicked,
passwordHandlers = passwordHandlers,
passphraseHandlers = passphraseHandlers,
@ -226,9 +226,9 @@ fun GeneratorScreen(
plusAddressedEmailHandlers = plusAddressedEmailHandlers,
catchAllEmailHandlers = catchAllEmailHandlers,
randomWordHandlers = randomWordHandlers,
modifier = Modifier.padding(innerPadding),
)
}
}
}
//region Top App Bar Composables
@ -242,6 +242,7 @@ private fun DefaultAppBar(
BitwardenMediumTopAppBar(
title = stringResource(id = R.string.generator),
scrollBehavior = scrollBehavior,
dividerStyle = TopAppBarDividerStyle.NONE,
actions = {
BitwardenOverflowActionItem(
menuItemDataList = persistentListOf(
@ -288,8 +289,6 @@ private fun ScrollContent(
state: GeneratorState,
onRegenerateClick: () -> Unit,
onCopyClick: () -> Unit,
onMainStateOptionClicked: (GeneratorState.MainTypeOption) -> Unit,
onPasscodeSubStateOptionClicked: (GeneratorState.MainType.Passcode.PasscodeTypeOption) -> Unit,
onUsernameSubStateOptionClicked: (GeneratorState.MainType.Username.UsernameTypeOption) -> Unit,
passwordHandlers: PasswordHandlers,
passphraseHandlers: PassphraseHandlers,
@ -305,8 +304,8 @@ private fun ScrollContent(
.fillMaxHeight()
.verticalScroll(rememberScrollState()),
) {
if (state.isUnderPolicy) {
Spacer(modifier = Modifier.height(8.dp))
BitwardenInfoCalloutCard(
text = stringResource(id = R.string.password_generator_policy_in_effect),
modifier = Modifier
@ -324,15 +323,6 @@ private fun ScrollContent(
onRegenerateClick = onRegenerateClick,
)
if (state.generatorMode == GeneratorMode.Default) {
Spacer(modifier = Modifier.height(8.dp))
MainStateOptionsItem(
selectedType = state.selectedType,
possibleMainStates = state.typeOptions.toImmutableList(),
onMainStateOptionClicked = onMainStateOptionClicked,
)
}
Spacer(modifier = Modifier.height(16.dp))
BitwardenListHeaderText(
@ -345,13 +335,17 @@ private fun ScrollContent(
Spacer(modifier = Modifier.height(8.dp))
when (val selectedType = state.selectedType) {
is GeneratorState.MainType.Passcode -> {
PasscodeTypeItems(
passcodeState = selectedType,
onSubStateOptionClicked = onPasscodeSubStateOptionClicked,
passwordHandlers = passwordHandlers,
is GeneratorState.MainType.Passphrase -> {
PassphraseTypeContent(
passphraseTypeState = selectedType,
passphraseHandlers = passphraseHandlers,
overridePasswordPolicyRestriction = state.overridePassword,
)
}
is GeneratorState.MainType.Password -> {
PasswordTypeContent(
passwordTypeState = selectedType,
passwordHandlers = passwordHandlers,
)
}
@ -407,92 +401,55 @@ private fun GeneratedStringItem(
@Composable
private fun MainStateOptionsItem(
selectedType: GeneratorState.MainType,
passcodePolicyOverride: GeneratorState.PasscodePolicyOverride?,
possibleMainStates: ImmutableList<GeneratorState.MainTypeOption>,
onMainStateOptionClicked: (GeneratorState.MainTypeOption) -> Unit,
modifier: Modifier = Modifier,
) {
val optionsWithStrings = possibleMainStates.associateWith { stringResource(id = it.labelRes) }
BitwardenSegmentedButton(
options = possibleMainStates
.map { mainOptionType ->
SegmentedButtonState(
text = stringResource(id = mainOptionType.labelRes),
onClick = { onMainStateOptionClicked(mainOptionType) },
isChecked = selectedType.mainTypeOption == mainOptionType,
isEnabled = when (mainOptionType) {
GeneratorState.MainTypeOption.PASSWORD -> {
when (passcodePolicyOverride) {
GeneratorState.PasscodePolicyOverride.PASSWORD -> true
GeneratorState.PasscodePolicyOverride.PASSPHRASE -> false
null -> true
}
}
BitwardenMultiSelectButton(
label = stringResource(id = R.string.what_would_you_like_to_generate),
options = optionsWithStrings.values.toImmutableList(),
selectedOption = stringResource(id = selectedType.displayStringResId),
onOptionSelected = { selectedOption ->
val selectedOptionId =
optionsWithStrings.entries.first { it.value == selectedOption }.key
onMainStateOptionClicked(selectedOptionId)
GeneratorState.MainTypeOption.PASSPHRASE -> {
when (passcodePolicyOverride) {
GeneratorState.PasscodePolicyOverride.PASSWORD -> false
GeneratorState.PasscodePolicyOverride.PASSPHRASE -> true
null -> true
}
}
GeneratorState.MainTypeOption.USERNAME -> true
},
modifier = Modifier
.padding(horizontal = 16.dp)
testTag = mainOptionType.testTag,
)
}
.toImmutableList(),
modifier = modifier
.fillMaxWidth()
.testTag("GeneratorTypePicker"),
.testTag(tag = "GeneratorTypePicker"),
)
}
//endregion ScrollContent and Static Items
//region PasscodeType Composables
@Composable
private fun ColumnScope.PasscodeTypeItems(
passcodeState: GeneratorState.MainType.Passcode,
onSubStateOptionClicked: (GeneratorState.MainType.Passcode.PasscodeTypeOption) -> Unit,
passwordHandlers: PasswordHandlers,
passphraseHandlers: PassphraseHandlers,
overridePasswordPolicyRestriction: Boolean,
) {
PasscodeOptionsItem(passcodeState, onSubStateOptionClicked, overridePasswordPolicyRestriction)
when (val selectedType = passcodeState.selectedType) {
is GeneratorState.MainType.Passcode.PasscodeType.Password -> {
PasswordTypeContent(
passwordTypeState = selectedType,
passwordHandlers = passwordHandlers,
)
}
is GeneratorState.MainType.Passcode.PasscodeType.Passphrase -> {
PassphraseTypeContent(
passphraseTypeState = selectedType,
passphraseHandlers = passphraseHandlers,
)
}
}
}
@Composable
private fun PasscodeOptionsItem(
currentSubState: GeneratorState.MainType.Passcode,
onSubStateOptionClicked: (GeneratorState.MainType.Passcode.PasscodeTypeOption) -> Unit,
overridePasswordPolicyRestriction: Boolean,
) {
val possibleSubStates = GeneratorState.MainType.Passcode.PasscodeTypeOption.entries
val optionsWithStrings = possibleSubStates.associateWith { stringResource(id = it.labelRes) }
BitwardenMultiSelectButton(
label = stringResource(id = R.string.password_type),
options = optionsWithStrings.values.toImmutableList(),
selectedOption = stringResource(id = currentSubState.selectedType.displayStringResId),
onOptionSelected = { selectedOption ->
val selectedOptionId =
optionsWithStrings.entries.first { it.value == selectedOption }.key
onSubStateOptionClicked(selectedOptionId)
},
isEnabled = !overridePasswordPolicyRestriction,
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth()
.testTag("PasswordTypePicker"),
)
}
//endregion PasscodeType Composables
//region PasswordType Composables
@Suppress("LongMethod")
@Composable
private fun ColumnScope.PasswordTypeContent(
passwordTypeState: GeneratorState.MainType.Passcode.PasscodeType.Password,
passwordTypeState: GeneratorState.MainType.Password,
passwordHandlers: PasswordHandlers,
) {
Spacer(modifier = Modifier.height(8.dp))
@ -707,7 +664,7 @@ private fun PasswordAvoidAmbiguousCharsToggleItem(
@Composable
private fun ColumnScope.PassphraseTypeContent(
passphraseTypeState: GeneratorState.MainType.Passcode.PasscodeType.Passphrase,
passphraseTypeState: GeneratorState.MainType.Passphrase,
passphraseHandlers: PassphraseHandlers,
) {
Spacer(modifier = Modifier.height(8.dp))
@ -1232,8 +1189,7 @@ private data class PasswordHandlers(
return PasswordHandlers(
onPasswordSliderLengthChange = { newLength, isUserInteracting ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password
.SliderLengthChange(
GeneratorAction.MainType.Password.SliderLengthChange(
length = newLength,
isUserInteracting = isUserInteracting,
),
@ -1241,56 +1197,49 @@ private data class PasswordHandlers(
},
onPasswordToggleCapitalLettersChange = { shouldUseCapitals ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleCapitalLettersChange(
GeneratorAction.MainType.Password.ToggleCapitalLettersChange(
useCapitals = shouldUseCapitals,
),
)
},
onPasswordToggleLowercaseLettersChange = { shouldUseLowercase ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleLowercaseLettersChange(
GeneratorAction.MainType.Password.ToggleLowercaseLettersChange(
useLowercase = shouldUseLowercase,
),
)
},
onPasswordToggleNumbersChange = { shouldUseNumbers ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleNumbersChange(
GeneratorAction.MainType.Password.ToggleNumbersChange(
useNumbers = shouldUseNumbers,
),
)
},
onPasswordToggleSpecialCharactersChange = { shouldUseSpecialChars ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleSpecialCharactersChange(
GeneratorAction.MainType.Password.ToggleSpecialCharactersChange(
useSpecialChars = shouldUseSpecialChars,
),
)
},
onPasswordMinNumbersCounterChange = { newMinNumbers ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password
.MinNumbersCounterChange(
GeneratorAction.MainType.Password.MinNumbersCounterChange(
minNumbers = newMinNumbers,
),
)
},
onPasswordMinSpecialCharactersChange = { newMinSpecial ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password
.MinSpecialCharactersChange(
GeneratorAction.MainType.Password.MinSpecialCharactersChange(
minSpecial = newMinSpecial,
),
)
},
onPasswordToggleAvoidAmbiguousCharsChange = { shouldAvoidAmbiguousChars ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleAvoidAmbigousCharactersChange(
GeneratorAction.MainType.Password.ToggleAvoidAmbigousCharactersChange(
avoidAmbiguousChars = shouldAvoidAmbiguousChars,
),
)
@ -1317,32 +1266,28 @@ private data class PassphraseHandlers(
return PassphraseHandlers(
onPassphraseNumWordsCounterChange = { changeInCounter ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Passphrase
.NumWordsCounterChange(
GeneratorAction.MainType.Passphrase.NumWordsCounterChange(
numWords = changeInCounter,
),
)
},
onPassphraseWordSeparatorChange = { newSeparator ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Passphrase
.WordSeparatorTextChange(
GeneratorAction.MainType.Passphrase.WordSeparatorTextChange(
wordSeparator = newSeparator,
),
)
},
onPassphraseCapitalizeToggleChange = { shouldCapitalize ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Passphrase
.ToggleCapitalizeChange(
GeneratorAction.MainType.Passphrase.ToggleCapitalizeChange(
capitalize = shouldCapitalize,
),
)
},
onPassphraseIncludeNumberToggleChange = { shouldIncludeNumber ->
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Passphrase
.ToggleIncludeNumberChange(
GeneratorAction.MainType.Passphrase.ToggleIncludeNumberChange(
includeNumber = shouldIncludeNumber,
),
)

View file

@ -30,11 +30,6 @@ import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Passphrase
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
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType.AddyIo
@ -87,16 +82,18 @@ class GeneratorViewModel @Inject constructor(
generatedText = NO_GENERATED_TEXT,
selectedType = when (generatorMode) {
is GeneratorMode.Modal.Username -> {
val type = generatorRepository.getUsernameGenerationOptions().usernameType
Username(selectedType = type)
GeneratorState.MainType.Username(
selectedType = generatorRepository
.getUsernameGenerationOptions()
.usernameType,
)
}
GeneratorMode.Modal.Password -> {
val type = generatorRepository.getPasscodeGenerationOptions().passcodeType
Passcode(selectedType = type)
generatorRepository.getPasscodeGenerationOptions().passcodeType
}
GeneratorMode.Default -> Passcode(selectedType = Password())
GeneratorMode.Default -> GeneratorState.MainType.Password()
},
generatorMode = generatorMode,
currentEmailAddress = requireNotNull(
@ -147,15 +144,11 @@ class GeneratorViewModel @Inject constructor(
@Suppress("MaxLineLength")
private fun handleMainTypeAction(action: GeneratorAction.MainType) {
when (action) {
is GeneratorAction.MainType.Passcode.PasscodeTypeOptionSelect -> {
handlePasscodeTypeOptionSelect(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password -> {
is GeneratorAction.MainType.Password -> {
handlePasswordSpecificAction(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Passphrase -> {
is GeneratorAction.MainType.Passphrase -> {
handlePassphraseSpecificAction(action)
}
@ -255,13 +248,15 @@ class GeneratorViewModel @Inject constructor(
private fun handleSelectClick() {
when (state.selectedType) {
is Passcode -> {
is GeneratorState.MainType.Passphrase,
is GeneratorState.MainType.Password,
-> {
generatorRepository.emitGeneratorResult(
GeneratorResult.Password(state.generatedText),
)
}
is Username -> {
is GeneratorState.MainType.Username -> {
generatorRepository.emitGeneratorResult(
GeneratorResult.Username(state.generatedText),
)
@ -276,12 +271,13 @@ class GeneratorViewModel @Inject constructor(
private fun loadOptions(shouldUseStorageOptions: Boolean = false) {
when (val selectedType = state.selectedType) {
is Passcode -> {
is GeneratorState.MainType.Passphrase,
is GeneratorState.MainType.Password,
-> {
val mainType = if (shouldUseStorageOptions) {
generatorRepository
.getPasscodeGenerationOptions()
?.passcodeType
?.let { Passcode(it) }
?: selectedType
} else {
selectedType
@ -289,12 +285,12 @@ class GeneratorViewModel @Inject constructor(
loadPasscodeOptions(selectedType = mainType)
}
is Username -> {
is GeneratorState.MainType.Username -> {
val mainType = if (shouldUseStorageOptions) {
generatorRepository
.getUsernameGenerationOptions()
?.usernameType
?.let { Username(it) }
?.let { GeneratorState.MainType.Username(it) }
?: selectedType
} else {
selectedType
@ -308,32 +304,51 @@ class GeneratorViewModel @Inject constructor(
}
@Suppress("CyclomaticComplexMethod", "LongMethod")
private fun loadPasscodeOptions(selectedType: Passcode) {
val options = generatorRepository.getPasscodeGenerationOptions()
private fun loadPasscodeOptions(selectedType: GeneratorState.MainType) {
if (selectedType is GeneratorState.MainType.Username) {
// This can only handle passwords and passphrases
return
}
val options = generatorRepository
.getPasscodeGenerationOptions()
?: generatePasscodeDefaultOptions()
val policy = policyManager
.getActivePolicies<PolicyInformation.PasswordGenerator>()
.toStrictestPolicy()
val passwordType = if (!policy.overridePasswordType.isNullOrBlank()) {
val passwordType = when (policy.overridePasswordType.toPasscodePolicyOverride()) {
GeneratorState.PasscodePolicyOverride.PASSWORD -> {
mutableStateFlow.update {
it.copy(overridePassword = true)
}
Passcode(
selectedType = policy.overridePasswordType.toSelectedType(),
it.copy(
passcodePolicyOverride = GeneratorState.PasscodePolicyOverride.PASSWORD,
)
} else {
mutableStateFlow.update {
it.copy(overridePassword = false)
}
selectedType
GeneratorState.MainType.Password()
}
when (passwordType.selectedType) {
is Passphrase -> {
val minNumWords = policy.minNumberWords ?: Passphrase.PASSPHRASE_MIN_NUMBER_OF_WORDS
val passphrase = Passphrase(
GeneratorState.PasscodePolicyOverride.PASSPHRASE -> {
mutableStateFlow.update {
it.copy(
passcodePolicyOverride = GeneratorState.PasscodePolicyOverride.PASSPHRASE,
)
}
GeneratorState.MainType.Passphrase()
}
null -> {
mutableStateFlow.update { it.copy(passcodePolicyOverride = null) }
selectedType
}
}
when (passwordType) {
is GeneratorState.MainType.Passphrase -> {
val minNumWords = policy
.minNumberWords
?: GeneratorState.MainType.Passphrase.PASSPHRASE_MIN_NUMBER_OF_WORDS
updateGeneratorMainType {
GeneratorState.MainType.Passphrase(
numWords = max(options.numWords, minNumWords),
minNumWords = minNumWords,
wordSeparator = options.wordSeparator.toCharArray().first(),
@ -342,14 +357,15 @@ class GeneratorViewModel @Inject constructor(
includeNumber = options.allowIncludeNumber || policy.includeNumber == true,
includeNumberEnabled = policy.includeNumber != true,
)
updateGeneratorMainType {
Passcode(selectedType = passphrase)
}
}
is Password -> {
val minLength = policy.minLength ?: Password.PASSWORD_LENGTH_SLIDER_MIN
val password = Password(
is GeneratorState.MainType.Password -> {
val minLength = policy
.minLength
?: GeneratorState.MainType.Password.PASSWORD_LENGTH_SLIDER_MIN
updateGeneratorMainType {
GeneratorState.MainType.Password(
length = max(options.length, minLength),
minLength = minLength,
useCapitals = options.hasUppercase || policy.useUpper == true,
@ -366,14 +382,17 @@ class GeneratorViewModel @Inject constructor(
minSpecialAllowed = policy.minSpecial ?: 0,
avoidAmbiguousChars = options.allowAmbiguousChar,
)
updateGeneratorMainType {
Passcode(selectedType = password)
}
}
}
}
private fun loadUsernameOptions(selectedType: Username, forceRegeneration: Boolean = false) {
is GeneratorState.MainType.Username -> Unit
}
}
private fun loadUsernameOptions(
selectedType: GeneratorState.MainType.Username,
forceRegeneration: Boolean = false,
) {
val options = generatorRepository.getUsernameGenerationOptions()
val updatedSelectedType = when (val type = selectedType.selectedType) {
is PlusAddressedEmail -> {
@ -382,14 +401,16 @@ class GeneratorViewModel @Inject constructor(
?.orNullIfBlank()
?: state.currentEmailAddress
Username(selectedType = PlusAddressedEmail(email = emailToUse))
GeneratorState.MainType.Username(
selectedType = PlusAddressedEmail(email = emailToUse),
)
}
is CatchAllEmail -> {
val catchAllEmail = CatchAllEmail(
domainName = options?.catchAllEmailDomain ?: type.domainName,
)
Username(selectedType = catchAllEmail)
GeneratorState.MainType.Username(selectedType = catchAllEmail)
}
is RandomWord -> {
@ -397,7 +418,7 @@ class GeneratorViewModel @Inject constructor(
capitalize = options?.capitalizeRandomWordUsername ?: type.capitalize,
includeNumber = options?.includeNumberRandomWordUsername ?: type.includeNumber,
)
Username(selectedType = randomWord)
GeneratorState.MainType.Username(selectedType = randomWord)
}
is ForwardedEmailAlias -> {
@ -406,7 +427,7 @@ class GeneratorViewModel @Inject constructor(
?.toServiceType(options)
?: type.selectedServiceType
Username(
GeneratorState.MainType.Username(
selectedType = ForwardedEmailAlias(
selectedServiceType = mappedServiceType,
obfuscatedText = "",
@ -418,7 +439,7 @@ class GeneratorViewModel @Inject constructor(
updateGeneratorMainType(forceRegeneration = forceRegeneration) { updatedSelectedType }
}
private fun savePasswordOptionsToDisk(password: Password) {
private fun savePasswordOptionsToDisk(password: GeneratorState.MainType.Password) {
val options = generatorRepository
.getPasscodeGenerationOptions() ?: generatePasscodeDefaultOptions()
val newOptions = options.copy(
@ -435,7 +456,7 @@ class GeneratorViewModel @Inject constructor(
generatorRepository.savePasscodeGenerationOptions(newOptions)
}
private fun savePassphraseOptionsToDisk(passphrase: Passphrase) {
private fun savePassphraseOptionsToDisk(passphrase: GeneratorState.MainType.Passphrase) {
val options = generatorRepository
.getPasscodeGenerationOptions() ?: generatePasscodeDefaultOptions()
val newOptions = options.copy(
@ -532,8 +553,8 @@ class GeneratorViewModel @Inject constructor(
}
private fun generatePasscodeDefaultOptions(): PasscodeGenerationOptions {
val defaultPassword = Password()
val defaultPassphrase = Passphrase()
val defaultPassword = GeneratorState.MainType.Password()
val defaultPassphrase = GeneratorState.MainType.Passphrase()
return PasscodeGenerationOptions(
type = PasscodeGenerationOptions.PasscodeType.PASSWORD,
@ -572,7 +593,7 @@ class GeneratorViewModel @Inject constructor(
)
}
private suspend fun generatePassword(password: Password) {
private suspend fun generatePassword(password: GeneratorState.MainType.Password) {
val request = PasswordGeneratorRequest(
lowercase = password.useLowercase,
uppercase = password.useCapitals,
@ -591,7 +612,7 @@ class GeneratorViewModel @Inject constructor(
sendAction(GeneratorAction.Internal.UpdateGeneratedPasswordResult(result))
}
private suspend fun generatePassphrase(passphrase: Passphrase) {
private suspend fun generatePassphrase(passphrase: GeneratorState.MainType.Passphrase) {
val request = PassphraseGeneratorRequest(
numWords = passphrase.numWords.toUByte(),
wordSeparator = passphrase.wordSeparator?.toString() ?: " ",
@ -736,16 +757,17 @@ class GeneratorViewModel @Inject constructor(
private fun handleMainTypeOptionSelect(action: GeneratorAction.MainTypeOptionSelect) {
when (action.mainTypeOption) {
GeneratorState.MainTypeOption.PASSWORD -> {
val type = generatorRepository.getPasscodeGenerationOptions().passcodeType
loadPasscodeOptions(
selectedType = Passcode(selectedType = type),
)
loadPasscodeOptions(selectedType = GeneratorState.MainType.Password())
}
GeneratorState.MainTypeOption.PASSPHRASE -> {
loadPasscodeOptions(selectedType = GeneratorState.MainType.Passphrase())
}
GeneratorState.MainTypeOption.USERNAME -> {
val type = generatorRepository.getUsernameGenerationOptions().usernameType
loadUsernameOptions(
selectedType = Username(selectedType = type),
selectedType = GeneratorState.MainType.Username(selectedType = type),
forceRegeneration = true,
)
}
@ -754,76 +776,46 @@ class GeneratorViewModel @Inject constructor(
//endregion Main Type Option Handlers
//region Passcode Type Handlers
private fun handlePasscodeTypeOptionSelect(
action: GeneratorAction.MainType.Passcode.PasscodeTypeOptionSelect,
) {
when (action.passcodeTypeOption) {
PasscodeTypeOption.PASSWORD -> loadPasscodeOptions(
selectedType = Passcode(selectedType = Password()),
)
PasscodeTypeOption.PASSPHRASE -> loadPasscodeOptions(
selectedType = Passcode(selectedType = Passphrase()),
)
}
}
//endregion Passcode Type Handlers
//region Password Specific Handlers
private fun handlePasswordSpecificAction(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password,
) {
private fun handlePasswordSpecificAction(action: GeneratorAction.MainType.Password) {
when (action) {
is GeneratorAction.MainType.Passcode.PasscodeType.Password.SliderLengthChange,
-> {
is GeneratorAction.MainType.Password.SliderLengthChange -> {
handlePasswordLengthSliderChange(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleCapitalLettersChange,
-> {
is GeneratorAction.MainType.Password.ToggleCapitalLettersChange -> {
handleToggleCapitalLetters(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleLowercaseLettersChange,
-> {
is GeneratorAction.MainType.Password.ToggleLowercaseLettersChange -> {
handleToggleLowercaseLetters(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleNumbersChange,
-> {
is GeneratorAction.MainType.Password.ToggleNumbersChange -> {
handleToggleNumbers(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleSpecialCharactersChange,
-> {
is GeneratorAction.MainType.Password.ToggleSpecialCharactersChange -> {
handleToggleSpecialChars(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange,
-> {
is GeneratorAction.MainType.Password.MinNumbersCounterChange -> {
handleMinNumbersChange(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password.MinSpecialCharactersChange,
-> {
is GeneratorAction.MainType.Password.MinSpecialCharactersChange -> {
handleMinSpecialChange(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleAvoidAmbigousCharactersChange,
-> {
is GeneratorAction.MainType.Password.ToggleAvoidAmbigousCharactersChange -> {
handleToggleAmbiguousChars(action)
}
}
}
private fun handlePasswordLengthSliderChange(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password.SliderLengthChange,
action: GeneratorAction.MainType.Password.SliderLengthChange,
) {
val adjustedLength = action.length
@ -836,7 +828,7 @@ class GeneratorViewModel @Inject constructor(
}
private fun handleToggleCapitalLetters(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleCapitalLettersChange,
action: GeneratorAction.MainType.Password.ToggleCapitalLettersChange,
) {
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(
@ -847,8 +839,7 @@ class GeneratorViewModel @Inject constructor(
}
private fun handleToggleLowercaseLetters(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleLowercaseLettersChange,
action: GeneratorAction.MainType.Password.ToggleLowercaseLettersChange,
) {
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(
@ -859,7 +850,7 @@ class GeneratorViewModel @Inject constructor(
}
private fun handleToggleNumbers(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleNumbersChange,
action: GeneratorAction.MainType.Password.ToggleNumbersChange,
) {
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(
@ -870,8 +861,7 @@ class GeneratorViewModel @Inject constructor(
}
private fun handleToggleSpecialChars(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleSpecialCharactersChange,
action: GeneratorAction.MainType.Password.ToggleSpecialCharactersChange,
) {
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(
@ -882,7 +872,7 @@ class GeneratorViewModel @Inject constructor(
}
private fun handleMinNumbersChange(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange,
action: GeneratorAction.MainType.Password.MinNumbersCounterChange,
) {
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(
@ -893,7 +883,7 @@ class GeneratorViewModel @Inject constructor(
}
private fun handleMinSpecialChange(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password.MinSpecialCharactersChange,
action: GeneratorAction.MainType.Password.MinSpecialCharactersChange,
) {
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(
@ -904,8 +894,7 @@ class GeneratorViewModel @Inject constructor(
}
private fun handleToggleAmbiguousChars(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password
.ToggleAvoidAmbigousCharactersChange,
action: GeneratorAction.MainType.Password.ToggleAvoidAmbigousCharactersChange,
) {
updatePasswordType { currentPasswordType ->
currentPasswordType.copy(
@ -919,33 +908,29 @@ class GeneratorViewModel @Inject constructor(
//region Passphrase Specific Handlers
private fun handlePassphraseSpecificAction(
action: GeneratorAction.MainType.Passcode.PasscodeType.Passphrase,
action: GeneratorAction.MainType.Passphrase,
) {
when (action) {
is GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.NumWordsCounterChange,
-> {
is GeneratorAction.MainType.Passphrase.NumWordsCounterChange -> {
handleNumWordsCounterChange(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.ToggleCapitalizeChange,
-> {
is GeneratorAction.MainType.Passphrase.ToggleCapitalizeChange -> {
handlePassphraseToggleCapitalizeChange(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.ToggleIncludeNumberChange,
-> {
is GeneratorAction.MainType.Passphrase.ToggleIncludeNumberChange -> {
handlePassphraseToggleIncludeNumberChange(action)
}
is GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.WordSeparatorTextChange,
-> {
is GeneratorAction.MainType.Passphrase.WordSeparatorTextChange -> {
handleWordSeparatorTextInputChange(action)
}
}
}
private fun handlePassphraseToggleCapitalizeChange(
action: GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.ToggleCapitalizeChange,
action: GeneratorAction.MainType.Passphrase.ToggleCapitalizeChange,
) {
updatePassphraseType { currentPassphraseType ->
currentPassphraseType.copy(
@ -955,7 +940,7 @@ class GeneratorViewModel @Inject constructor(
}
private fun handlePassphraseToggleIncludeNumberChange(
action: GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.ToggleIncludeNumberChange,
action: GeneratorAction.MainType.Passphrase.ToggleIncludeNumberChange,
) {
updatePassphraseType { currentPassphraseType ->
currentPassphraseType.copy(
@ -965,7 +950,7 @@ class GeneratorViewModel @Inject constructor(
}
private fun handleNumWordsCounterChange(
action: GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.NumWordsCounterChange,
action: GeneratorAction.MainType.Passphrase.NumWordsCounterChange,
) {
updatePassphraseType { passphraseType ->
passphraseType.copy(numWords = action.numWords)
@ -973,7 +958,7 @@ class GeneratorViewModel @Inject constructor(
}
private fun handleWordSeparatorTextInputChange(
action: GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.WordSeparatorTextChange,
action: GeneratorAction.MainType.Passphrase.WordSeparatorTextChange,
) {
updatePassphraseType { passphraseType ->
val newWordSeparator =
@ -990,25 +975,37 @@ class GeneratorViewModel @Inject constructor(
action: GeneratorAction.MainType.Username.UsernameTypeOptionSelect,
) {
when (action.usernameTypeOption) {
Username.UsernameTypeOption.PLUS_ADDRESSED_EMAIL -> loadUsernameOptions(
selectedType = Username(selectedType = PlusAddressedEmail()),
GeneratorState.MainType.Username.UsernameTypeOption.PLUS_ADDRESSED_EMAIL -> {
loadUsernameOptions(
selectedType = GeneratorState.MainType.Username(
selectedType = PlusAddressedEmail(),
),
forceRegeneration = true,
)
}
Username.UsernameTypeOption.CATCH_ALL_EMAIL -> loadUsernameOptions(
selectedType = Username(selectedType = CatchAllEmail()),
GeneratorState.MainType.Username.UsernameTypeOption.CATCH_ALL_EMAIL -> {
loadUsernameOptions(
selectedType = GeneratorState.MainType.Username(
selectedType = CatchAllEmail(),
),
forceRegeneration = true,
)
}
// We do not force regeneration here since the API can fail if the data is entered
// incorrectly. This will only be generated when the user clicks the regenerate button.
Username.UsernameTypeOption.FORWARDED_EMAIL_ALIAS -> loadUsernameOptions(
selectedType = Username(selectedType = ForwardedEmailAlias()),
GeneratorState.MainType.Username.UsernameTypeOption.FORWARDED_EMAIL_ALIAS -> {
loadUsernameOptions(
selectedType = GeneratorState.MainType.Username(
selectedType = ForwardedEmailAlias(),
),
forceRegeneration = false,
)
}
Username.UsernameTypeOption.RANDOM_WORD -> loadUsernameOptions(
selectedType = Username(selectedType = RandomWord()),
GeneratorState.MainType.Username.UsernameTypeOption.RANDOM_WORD -> loadUsernameOptions(
selectedType = GeneratorState.MainType.Username(selectedType = RandomWord()),
forceRegeneration = true,
)
}
@ -1371,25 +1368,26 @@ class GeneratorViewModel @Inject constructor(
generateTextJob.cancel()
generateTextJob = viewModelScope.launch {
when (updatedMainType) {
is Passcode -> when (val selectedType = updatedMainType.selectedType) {
is Passphrase -> {
savePassphraseOptionsToDisk(selectedType)
generatePassphrase(selectedType)
is GeneratorState.MainType.Passphrase -> {
savePassphraseOptionsToDisk(updatedMainType)
generatePassphrase(updatedMainType)
}
is Password -> {
savePasswordOptionsToDisk(selectedType)
generatePassword(selectedType)
}
is GeneratorState.MainType.Password -> {
savePasswordOptionsToDisk(updatedMainType)
generatePassword(updatedMainType)
}
is Username -> when (val selectedType = updatedMainType.selectedType) {
is GeneratorState.MainType.Username -> {
when (val selectedType = updatedMainType.selectedType) {
is ForwardedEmailAlias -> {
saveForwardedEmailAliasServiceTypeToDisk(selectedType)
if (forceRegeneration) {
generateForwardedEmailAlias(selectedType)
} else {
mutableStateFlow.update { it.copy(generatedText = NO_GENERATED_TEXT) }
mutableStateFlow.update {
it.copy(generatedText = NO_GENERATED_TEXT)
}
}
}
@ -1398,7 +1396,9 @@ class GeneratorViewModel @Inject constructor(
if (forceRegeneration) {
generateCatchAllEmail(selectedType)
} else {
mutableStateFlow.update { it.copy(generatedText = NO_GENERATED_TEXT) }
mutableStateFlow.update {
it.copy(generatedText = NO_GENERATED_TEXT)
}
}
}
@ -1407,7 +1407,9 @@ class GeneratorViewModel @Inject constructor(
if (forceRegeneration) {
generatePlusAddressedEmail(selectedType)
} else {
mutableStateFlow.update { it.copy(generatedText = NO_GENERATED_TEXT) }
mutableStateFlow.update {
it.copy(generatedText = NO_GENERATED_TEXT)
}
}
}
@ -1419,6 +1421,7 @@ class GeneratorViewModel @Inject constructor(
}
}
}
}
private suspend fun generateForwardedEmailAlias(alias: ForwardedEmailAlias) {
val request = alias.selectedServiceType?.toUsernameGeneratorRequest(state.website) ?: run {
@ -1463,16 +1466,9 @@ class GeneratorViewModel @Inject constructor(
sendAction(GeneratorAction.Internal.UpdateGeneratedRandomWordUsernameResult(result))
}
private inline fun updateGeneratorMainTypePasscode(
crossinline block: (Passcode) -> Passcode,
) {
updateGeneratorMainType {
if (it !is Passcode) null else block(it)
}
}
/**
* Updates the length property of the [Password] to reflect the new minimum.
* Updates the length property of the [GeneratorState.MainType.Password] to reflect the new
* minimum.
*/
private fun updatePasswordLength() {
updatePasswordType { currentPasswordType ->
@ -1486,39 +1482,34 @@ class GeneratorViewModel @Inject constructor(
}
private inline fun updatePasswordType(
crossinline block: (Password) -> Password,
crossinline block: (GeneratorState.MainType.Password) -> GeneratorState.MainType.Password,
) {
updateGeneratorMainTypePasscode { currentSelectedType ->
val currentPasswordType = currentSelectedType.selectedType
if (currentPasswordType !is Password) {
return@updateGeneratorMainTypePasscode currentSelectedType
updateGeneratorMainType { currentSelectedType ->
if (currentSelectedType !is GeneratorState.MainType.Password) {
return@updateGeneratorMainType currentSelectedType
}
val updatedPasswordType = currentPasswordType
.let(block)
.enforceAtLeastOneToggleOn()
currentSelectedType.copy(selectedType = updatedPasswordType)
currentSelectedType.let(block).enforceAtLeastOneToggleOn()
}
}
private inline fun updatePassphraseType(
crossinline block: (Passphrase) -> Passphrase,
crossinline block: (
GeneratorState.MainType.Passphrase,
) -> GeneratorState.MainType.Passphrase,
) {
updateGeneratorMainTypePasscode { currentSelectedType ->
val currentPasswordType = currentSelectedType.selectedType
if (currentPasswordType !is Passphrase) {
return@updateGeneratorMainTypePasscode currentSelectedType
updateGeneratorMainType { currentSelectedType ->
if (currentSelectedType !is GeneratorState.MainType.Passphrase) {
return@updateGeneratorMainType currentSelectedType
}
currentSelectedType.copy(selectedType = block(currentPasswordType))
block(currentSelectedType)
}
}
private inline fun updateGeneratorMainTypeUsername(
crossinline block: (Username) -> Username,
crossinline block: (GeneratorState.MainType.Username) -> GeneratorState.MainType.Username,
) {
updateGeneratorMainType {
if (it !is Username) null else block(it)
if (it !is GeneratorState.MainType.Username) null else block(it)
}
}
@ -1731,7 +1722,7 @@ data class GeneratorState(
val currentEmailAddress: String,
val isUnderPolicy: Boolean = false,
val website: String? = null,
var overridePassword: Boolean = false,
var passcodePolicyOverride: PasscodePolicyOverride? = null,
) : Parcelable {
/**
@ -1741,13 +1732,27 @@ data class GeneratorState(
get() = MainTypeOption.entries.toList()
/**
* Enum representing the main type options for the generator, such as PASSWORD and USERNAME.
* Enum representing the main type options for the generator, such as PASSWORD PASSPHRASE, and
* USERNAME.
*
* @property labelRes The resource ID of the string that represents the label of each type.
* @property testTag The string used as the test tag for the option.
*/
enum class MainTypeOption(val labelRes: Int) {
PASSWORD(R.string.password),
USERNAME(R.string.username),
enum class MainTypeOption(
val labelRes: Int,
val testTag: String,
) {
PASSWORD(labelRes = R.string.password, testTag = "password_option"),
PASSPHRASE(labelRes = R.string.passphrase, testTag = "passphrase_option"),
USERNAME(labelRes = R.string.username, testTag = "username_option"),
}
/**
* Enum representing the passcode types for the generator, such as PASSWORD and PASSPHRASE.
*/
enum class PasscodePolicyOverride {
PASSWORD,
PASSPHRASE,
}
/**
@ -1758,50 +1763,9 @@ data class GeneratorState(
sealed class MainType : Parcelable {
/**
* Represents the resource ID for the display string. This is an abstract property
* that must be overridden by each subclass to provide the appropriate string resource ID
* for display purposes.
* Indicates the type this is.
*/
abstract val displayStringResId: Int
/**
* Represents the Passcode main type, allowing the user to specify the kind of passcode,
* such as a Password or a Passphrase, and configure their respective properties.
*
* @property selectedType The currently selected PasscodeType
*/
@Parcelize
data class Passcode(
val selectedType: PasscodeType = Password(),
) : MainType(), Parcelable {
override val displayStringResId: Int
get() = MainTypeOption.PASSWORD.labelRes
/**
* Enum representing the types of passcodes,
* allowing for different passcode configurations.
*
* @property labelRes The ID of the string that represents the label for each type.
*/
enum class PasscodeTypeOption(val labelRes: Int) {
PASSWORD(R.string.password),
PASSPHRASE(R.string.passphrase),
}
/**
* A sealed class representing the different types of PASSWORD,
* such as standard Password and Passphrase, each with its own properties.
*/
@Parcelize
sealed class PasscodeType : Parcelable {
/**
* Represents the resource ID for the display string specific to each
* PasscodeType subclass. Every subclass of PasscodeType must override
* this property to provide the appropriate string resource ID for
* its display string.
*/
abstract val displayStringResId: Int
abstract val mainTypeOption: MainTypeOption
/**
* Represents a standard PASSWORD type, with configurable options for
@ -1852,9 +1816,8 @@ data class GeneratorState(
val avoidAmbiguousChars: Boolean = false,
val ambiguousCharsEnabled: Boolean = true,
val isUserInteracting: Boolean = false,
) : PasscodeType(), Parcelable {
override val displayStringResId: Int
get() = PasscodeTypeOption.PASSWORD.labelRes
) : MainType() {
override val mainTypeOption: MainTypeOption get() = MainTypeOption.PASSWORD
/**
* The computed minimum length for the generated Password
@ -1904,9 +1867,8 @@ data class GeneratorState(
val capitalizeEnabled: Boolean = true,
val includeNumber: Boolean = false,
val includeNumberEnabled: Boolean = true,
) : PasscodeType(), Parcelable {
override val displayStringResId: Int
get() = PasscodeTypeOption.PASSPHRASE.labelRes
) : MainType() {
override val mainTypeOption: MainTypeOption get() = MainTypeOption.PASSPHRASE
@Suppress("UndocumentedPublicClass")
companion object {
@ -1917,8 +1879,6 @@ data class GeneratorState(
const val PASSPHRASE_MAX_NUMBER_OF_WORDS: Int = 20
}
}
}
}
/**
* Represents the USERNAME main type, holding the configuration
@ -1927,9 +1887,8 @@ data class GeneratorState(
@Parcelize
data class Username(
val selectedType: UsernameType = PlusAddressedEmail(),
) : MainType(), Parcelable {
override val displayStringResId: Int
get() = MainTypeOption.USERNAME.labelRes
) : MainType() {
override val mainTypeOption: MainTypeOption get() = MainTypeOption.USERNAME
/**
* Enum representing the types of usernames,
@ -2209,29 +2168,10 @@ sealed class GeneratorAction {
*/
sealed class MainType : GeneratorAction() {
/**
* Represents actions specifically related to [GeneratorState.MainType.Passcode].
*/
sealed class Passcode : MainType() {
/**
* Represents the action of selecting a passcode type option.
*
* @property passcodeTypeOption The selected passcode type option.
*/
data class PasscodeTypeOptionSelect(
val passcodeTypeOption: PasscodeTypeOption,
) : Passcode()
/**
* Represents actions related to the different types of passcodes.
*/
sealed class PasscodeType : Passcode() {
/**
* Represents actions specifically related to passwords, a subtype of passcode.
*/
sealed class Password : PasscodeType() {
sealed class Password : MainType() {
/**
* Represents a change action for the length of the password,
* adjusted using a slider.
@ -2324,7 +2264,7 @@ sealed class GeneratorAction {
/**
* Represents actions specifically related to passphrases, a subtype of passcode.
*/
sealed class Passphrase : PasscodeType() {
sealed class Passphrase : MainType() {
/**
* Fired when the number of words counter is changed.
@ -2354,8 +2294,6 @@ sealed class GeneratorAction {
*/
data class ToggleIncludeNumberChange(val includeNumber: Boolean) : Passphrase()
}
}
}
/**
* Represents actions related to the [GeneratorState.MainType.Username] in the generator.
@ -2631,8 +2569,8 @@ sealed class GeneratorEvent {
) : GeneratorEvent()
}
@Suppress("ComplexCondition")
private fun Password.enforceAtLeastOneToggleOn(): Password =
@Suppress("ComplexCondition", "MaxLineLength")
private fun GeneratorState.MainType.Password.enforceAtLeastOneToggleOn(): GeneratorState.MainType.Password =
// If all toggles are off, turn on useLowercase
if (!this.useCapitals &&
!this.useLowercase &&
@ -2644,14 +2582,14 @@ private fun Password.enforceAtLeastOneToggleOn(): Password =
this
}
private val PasscodeGenerationOptions?.passcodeType: Passcode.PasscodeType
private val PasscodeGenerationOptions?.passcodeType: GeneratorState.MainType
get() = when (this?.type) {
PasscodeGenerationOptions.PasscodeType.PASSWORD -> Password()
PasscodeGenerationOptions.PasscodeType.PASSPHRASE -> Passphrase()
else -> Password()
PasscodeGenerationOptions.PasscodeType.PASSWORD -> GeneratorState.MainType.Password()
PasscodeGenerationOptions.PasscodeType.PASSPHRASE -> GeneratorState.MainType.Passphrase()
else -> GeneratorState.MainType.Password()
}
private val UsernameGenerationOptions?.usernameType: Username.UsernameType
private val UsernameGenerationOptions?.usernameType: GeneratorState.MainType.Username.UsernameType
get() = when (this?.type) {
UsernameGenerationOptions.UsernameType.PLUS_ADDRESSED_EMAIL -> PlusAddressedEmail()
UsernameGenerationOptions.UsernameType.CATCH_ALL_EMAIL -> CatchAllEmail()
@ -2660,8 +2598,15 @@ private val UsernameGenerationOptions?.usernameType: Username.UsernameType
else -> PlusAddressedEmail()
}
private fun String?.toSelectedType(): Passcode.PasscodeType =
private fun String?.toPasscodePolicyOverride(): GeneratorState.PasscodePolicyOverride? =
if (this.isNullOrBlank()) {
null
} else {
when (this) {
PolicyInformation.PasswordGenerator.TYPE_PASSPHRASE -> Passphrase()
else -> Password()
PolicyInformation.PasswordGenerator.TYPE_PASSPHRASE -> {
GeneratorState.PasscodePolicyOverride.PASSPHRASE
}
else -> GeneratorState.PasscodePolicyOverride.PASSWORD
}
}

View file

@ -205,15 +205,7 @@ class GeneratorScreenTest : BaseComposeTest() {
fun `clicking a MainStateOption should send MainTypeOptionSelect action`() {
// Opens the menu
composeTestRule
.onNodeWithContentDescription(label = "Password. What would you like to generate?")
.performClick()
// Choose the option from the menu
composeTestRule
.onAllNodesWithText(text = "Password")
.onLast()
.performScrollTo()
.assert(hasAnyAncestor(isDialog()))
.onNodeWithText(text = "Password")
.performClick()
verify {
@ -228,45 +220,13 @@ class GeneratorScreenTest : BaseComposeTest() {
.assertDoesNotExist()
}
@Test
fun `clicking a PasscodeOption should send PasscodeTypeOption action`() {
// Opens the menu
composeTestRule
.onNodeWithContentDescription(label = "Password. Password type")
.performClick()
// Choose the option from the menu
composeTestRule
.onAllNodesWithText(text = "Passphrase")
.onLast()
.assert(hasAnyAncestor(isDialog()))
.performClick()
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeTypeOptionSelect(
GeneratorState.MainType.Passcode.PasscodeTypeOption.PASSPHRASE,
),
)
}
// Make sure dialog is hidden:
composeTestRule
.onNode(isDialog())
.assertDoesNotExist()
}
@Suppress("MaxLineLength")
@Test
fun `clicking a UsernameOption should send UsernameTypeOption action`() {
updateState(
DEFAULT_STATE.copy(
selectedType = GeneratorState.MainType.Username(
GeneratorState
.MainType
.Username
.UsernameType
.PlusAddressedEmail(),
GeneratorState.MainType.Username.UsernameType.PlusAddressedEmail(),
),
),
)
@ -302,15 +262,7 @@ class GeneratorScreenTest : BaseComposeTest() {
//region Passcode Password Tests
@Test
fun `in Passcode_Password state, the ViewModel state should update the UI correctly`() {
composeTestRule
.onNodeWithContentDescription(label = "Password. What would you like to generate?")
.assertIsDisplayed()
composeTestRule
.onNodeWithContentDescription(label = "Password. Password type")
.assertIsDisplayed()
fun `in Password state, the ViewModel state should update the UI correctly`() {
composeTestRule
.onNode(
expectValue(
@ -363,7 +315,7 @@ class GeneratorScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Password state, adjusting the slider should send SliderLengthChange action with length not equal to default`() {
fun `in Password state, adjusting the slider should send SliderLengthChange action with length not equal to default`() {
composeTestRule
.onNodeWithText("Length")
.onSiblings()
@ -383,7 +335,7 @@ class GeneratorScreenTest : BaseComposeTest() {
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.SliderLengthChange(
GeneratorAction.MainType.Password.SliderLengthChange(
length = 128,
isUserInteracting = true,
),
@ -396,7 +348,7 @@ class GeneratorScreenTest : BaseComposeTest() {
// actually get updated from its original value, and thus will be its original value of 14.
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.SliderLengthChange(
GeneratorAction.MainType.Password.SliderLengthChange(
length = 14,
isUserInteracting = false,
),
@ -406,14 +358,14 @@ class GeneratorScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Password state, toggling the capital letters toggle should send ToggleCapitalLettersChange action`() {
fun `in Password state, toggling the capital letters toggle should send ToggleCapitalLettersChange action`() {
composeTestRule.onNodeWithText("A—Z")
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleCapitalLettersChange(
GeneratorAction.MainType.Password.ToggleCapitalLettersChange(
useCapitals = false,
),
)
@ -422,19 +374,14 @@ class GeneratorScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Password state, toggling the use lowercase toggle should send ToggleLowercaseLettersChange action`() {
fun `in Password state, toggling the use lowercase toggle should send ToggleLowercaseLettersChange action`() {
composeTestRule.onNodeWithText("a—z")
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Password
.ToggleLowercaseLettersChange(
GeneratorAction.MainType.Password.ToggleLowercaseLettersChange(
useLowercase = false,
),
)
@ -443,35 +390,28 @@ class GeneratorScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Password state, toggling the use numbers toggle should send ToggleNumbersChange action`() {
fun `in Password state, toggling the use numbers toggle should send ToggleNumbersChange action`() {
composeTestRule.onNodeWithText("0-9")
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleNumbersChange(
useNumbers = false,
),
GeneratorAction.MainType.Password.ToggleNumbersChange(useNumbers = false),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Password state, toggling the use special characters toggle should send ToggleSpecialCharactersChange action`() {
fun `in Password state, toggling the use special characters toggle should send ToggleSpecialCharactersChange action`() {
composeTestRule.onNodeWithText("!@#$%^&*")
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Password
.ToggleSpecialCharactersChange(
GeneratorAction.MainType.Password.ToggleSpecialCharactersChange(
useSpecialChars = true,
),
)
@ -480,7 +420,7 @@ class GeneratorScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Password state, decrementing the minimum numbers counter should send MinNumbersCounterChange action`() {
fun `in Password state, decrementing the minimum numbers counter should send MinNumbersCounterChange action`() {
val initialMinNumbers = 1
composeTestRule.onNodeWithText("Minimum numbers")
@ -492,7 +432,7 @@ class GeneratorScreenTest : BaseComposeTest() {
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange(
GeneratorAction.MainType.Password.MinNumbersCounterChange(
minNumbers = initialMinNumbers - 1,
),
)
@ -501,7 +441,7 @@ class GeneratorScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Password state, incrementing the minimum numbers counter should send MinNumbersCounterChange action`() {
fun `in Password state, incrementing the minimum numbers counter should send MinNumbersCounterChange action`() {
val initialMinNumbers = 1
composeTestRule.onNodeWithText("Minimum numbers")
@ -513,28 +453,19 @@ class GeneratorScreenTest : BaseComposeTest() {
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange(
GeneratorAction.MainType.Password.MinNumbersCounterChange(
minNumbers = initialMinNumbers + 1,
),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Password state, decrementing the minimum numbers counter below 0 should do nothing`() {
fun `in Password state, decrementing the minimum numbers counter below 0 should do nothing`() {
val initialMinNumbers = 0
updateState(
DEFAULT_STATE.copy(
selectedType = GeneratorState.MainType.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Password(
minNumbers = initialMinNumbers,
),
),
selectedType = GeneratorState.MainType.Password(minNumbers = initialMinNumbers),
),
)
@ -549,21 +480,12 @@ class GeneratorScreenTest : BaseComposeTest() {
verify(exactly = 1) { viewModel.trySendAction(any()) }
}
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Password state, incrementing the minimum numbers counter above 9 should do nothing`() {
fun `in Password state, incrementing the minimum numbers counter above 9 should do nothing`() {
val initialMinNumbers = 9
updateState(
DEFAULT_STATE.copy(
selectedType = GeneratorState.MainType.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Password(
minNumbers = initialMinNumbers,
),
),
selectedType = GeneratorState.MainType.Password(minNumbers = initialMinNumbers),
),
)
@ -580,7 +502,7 @@ class GeneratorScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Password state, decrementing the minimum special characters counter should send MinSpecialCharactersChange action`() {
fun `in Password state, decrementing the minimum special characters counter should send MinSpecialCharactersChange action`() {
val initialSpecialChars = 1
composeTestRule.onNodeWithText("Minimum special")
@ -592,7 +514,7 @@ class GeneratorScreenTest : BaseComposeTest() {
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinSpecialCharactersChange(
GeneratorAction.MainType.Password.MinSpecialCharactersChange(
minSpecial = initialSpecialChars - 1,
),
)
@ -601,7 +523,7 @@ class GeneratorScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Password state, incrementing the minimum special characters counter should send MinSpecialCharactersChange action`() {
fun `in Password state, incrementing the minimum special characters counter should send MinSpecialCharactersChange action`() {
val initialSpecialChars = 1
composeTestRule.onNodeWithText("Minimum special")
@ -613,7 +535,7 @@ class GeneratorScreenTest : BaseComposeTest() {
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinSpecialCharactersChange(
GeneratorAction.MainType.Password.MinSpecialCharactersChange(
minSpecial = initialSpecialChars + 1,
),
)
@ -622,19 +544,11 @@ class GeneratorScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Password state, decrementing the minimum special characters below 0 should do nothing`() {
fun `in Password state, decrementing the minimum special characters below 0 should do nothing`() {
val initialSpecialChars = 0
updateState(
DEFAULT_STATE.copy(
selectedType = GeneratorState.MainType.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Password(
minSpecial = initialSpecialChars,
),
),
selectedType = GeneratorState.MainType.Password(minSpecial = initialSpecialChars),
),
)
@ -650,19 +564,11 @@ class GeneratorScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Password state, decrementing the minimum special characters above 9 should do nothing`() {
fun `in Password state, decrementing the minimum special characters above 9 should do nothing`() {
val initialSpecialChars = 9
updateState(
DEFAULT_STATE.copy(
selectedType = GeneratorState.MainType.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Password(
minSpecial = initialSpecialChars,
),
),
selectedType = GeneratorState.MainType.Password(minSpecial = initialSpecialChars),
),
)
@ -678,19 +584,14 @@ class GeneratorScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Password state, toggling the use avoid ambiguous characters toggle should send ToggleSpecialCharactersChange action`() {
fun `in Password state, toggling the use avoid ambiguous characters toggle should send ToggleSpecialCharactersChange action`() {
composeTestRule.onNodeWithText("Avoid ambiguous characters")
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Password
.ToggleAvoidAmbigousCharactersChange(
GeneratorAction.MainType.Password.ToggleAvoidAmbigousCharactersChange(
avoidAmbiguousChars = true,
),
)
@ -698,15 +599,10 @@ class GeneratorScreenTest : BaseComposeTest() {
}
@Test
fun `in Passcode_Password state, disabled elements should not send events`() {
fun `in Password state, disabled elements should not send events`() {
updateState(
DEFAULT_STATE.copy(
selectedType = GeneratorState.MainType.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Password(
selectedType = GeneratorState.MainType.Password(
capitalsEnabled = false,
lowercaseEnabled = false,
numbersEnabled = false,
@ -714,7 +610,6 @@ class GeneratorScreenTest : BaseComposeTest() {
ambiguousCharsEnabled = false,
),
),
),
)
composeTestRule.onNodeWithText("A—Z")
@ -735,52 +630,23 @@ class GeneratorScreenTest : BaseComposeTest() {
verify(exactly = 0) {
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Password
.ToggleCapitalLettersChange(
useCapitals = false,
),
GeneratorAction.MainType.Password.ToggleCapitalLettersChange(useCapitals = false),
)
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Password
.ToggleLowercaseLettersChange(
GeneratorAction.MainType.Password.ToggleLowercaseLettersChange(
useLowercase = false,
),
)
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Password
.ToggleNumbersChange(
useNumbers = false,
),
GeneratorAction.MainType.Password.ToggleNumbersChange(useNumbers = false),
)
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Password
.ToggleSpecialCharactersChange(
GeneratorAction.MainType.Password.ToggleSpecialCharactersChange(
useSpecialChars = true,
),
)
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Password
.ToggleAvoidAmbigousCharactersChange(
GeneratorAction.MainType.Password.ToggleAvoidAmbigousCharactersChange(
avoidAmbiguousChars = true,
),
)
@ -788,21 +654,15 @@ class GeneratorScreenTest : BaseComposeTest() {
}
@Test
fun `in Passcode_Password state, minimum numbers cannot go below minimum threshold`() {
fun `in Password state, minimum numbers cannot go below minimum threshold`() {
val initialMinNumbers = 5
updateState(
DEFAULT_STATE.copy(
selectedType = GeneratorState.MainType.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Password(
selectedType = GeneratorState.MainType.Password(
minNumbersAllowed = initialMinNumbers,
),
),
),
)
composeTestRule.onNodeWithText("Minimum numbers")
@ -814,7 +674,7 @@ class GeneratorScreenTest : BaseComposeTest() {
verify(exactly = 0) {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange(
GeneratorAction.MainType.Password.MinNumbersCounterChange(
minNumbers = 4,
),
)
@ -822,23 +682,17 @@ class GeneratorScreenTest : BaseComposeTest() {
}
@Test
fun `in Passcode_Password state, maximum numbers should match minimum if lower`() {
fun `in Password state, maximum numbers should match minimum if lower`() {
val initialMinNumbers = 7
val initialMaxNumbers = 5
updateState(
DEFAULT_STATE.copy(
selectedType = GeneratorState.MainType.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Password(
selectedType = GeneratorState.MainType.Password(
minNumbersAllowed = initialMinNumbers,
maxNumbersAllowed = initialMaxNumbers,
),
),
),
)
composeTestRule
@ -850,23 +704,16 @@ class GeneratorScreenTest : BaseComposeTest() {
.assertIsDisplayed()
}
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Password state, minimum special characters cannot go below minimum threshold`() {
fun `in Password state, minimum special characters cannot go below minimum threshold`() {
val initialMinSpecials = 5
updateState(
DEFAULT_STATE.copy(
selectedType = GeneratorState.MainType.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Password(
selectedType = GeneratorState.MainType.Password(
minSpecialAllowed = initialMinSpecials,
),
),
),
)
composeTestRule.onNodeWithText("Minimum special")
@ -878,7 +725,7 @@ class GeneratorScreenTest : BaseComposeTest() {
verify(exactly = 0) {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinSpecialCharactersChange(
GeneratorAction.MainType.Password.MinSpecialCharactersChange(
minSpecial = 4,
),
)
@ -886,23 +733,17 @@ class GeneratorScreenTest : BaseComposeTest() {
}
@Test
fun `in Passcode_Password state, maximum special should match minimum if lower `() {
fun `in Password state, maximum special should match minimum if lower `() {
val initialMinSpecials = 7
val initialMaxSpecials = 5
updateState(
DEFAULT_STATE.copy(
selectedType = GeneratorState.MainType.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Password(
selectedType = GeneratorState.MainType.Password(
minSpecialAllowed = initialMinSpecials,
maxSpecialAllowed = initialMaxSpecials,
),
),
),
)
composeTestRule.onNodeWithText("Minimum special")
@ -914,20 +755,14 @@ class GeneratorScreenTest : BaseComposeTest() {
}
@Test
fun `in Passcode_Passphrase state, disabled elements should not send events`() {
fun `in Passphrase state, disabled elements should not send events`() {
updateState(
DEFAULT_STATE.copy(
selectedType = GeneratorState.MainType.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Passphrase(
selectedType = GeneratorState.MainType.Passphrase(
capitalizeEnabled = false,
includeNumberEnabled = false,
),
),
),
)
composeTestRule.onNodeWithText("Capitalize")
@ -939,44 +774,25 @@ class GeneratorScreenTest : BaseComposeTest() {
verify(exactly = 0) {
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Passphrase
.ToggleCapitalizeChange(
GeneratorAction.MainType.Passphrase.ToggleCapitalizeChange(
capitalize = true,
),
)
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Passphrase
.ToggleIncludeNumberChange(
GeneratorAction.MainType.Passphrase.ToggleIncludeNumberChange(
includeNumber = true,
),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `in Passcode_passphrase state, minimum number of words cannot go below minimum threshold`() {
fun `in Passphrase state, minimum number of words cannot go below minimum threshold`() {
val initialMinWords = 5
updateState(
DEFAULT_STATE.copy(
selectedType = GeneratorState.MainType.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Passphrase(
minNumWords = initialMinWords,
),
),
selectedType = GeneratorState.MainType.Passphrase(minNumWords = initialMinWords),
),
)
@ -989,9 +805,7 @@ class GeneratorScreenTest : BaseComposeTest() {
verify(exactly = 0) {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.NumWordsCounterChange(
numWords = 4,
),
GeneratorAction.MainType.Passphrase.NumWordsCounterChange(numWords = 4),
)
}
}
@ -1002,19 +816,11 @@ class GeneratorScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Passphrase state, decrementing number of words should send NumWordsCounterChange action with decremented value`() {
fun `in Passphrase state, decrementing number of words should send NumWordsCounterChange action with decremented value`() {
val initialNumWords = 4
updateState(
DEFAULT_STATE.copy(
selectedType = GeneratorState.MainType.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Passphrase(
numWords = initialNumWords,
),
),
selectedType = GeneratorState.MainType.Passphrase(numWords = initialNumWords),
),
)
@ -1029,7 +835,7 @@ class GeneratorScreenTest : BaseComposeTest() {
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.NumWordsCounterChange(
GeneratorAction.MainType.Passphrase.NumWordsCounterChange(
numWords = initialNumWords - 1,
),
)
@ -1037,19 +843,11 @@ class GeneratorScreenTest : BaseComposeTest() {
}
@Test
fun `in Passcode_Passphrase state, decrementing number of words under 3 should do nothing`() {
fun `in Passphrase state, decrementing number of words under 3 should do nothing`() {
val initialNumWords = 3
updateState(
DEFAULT_STATE.copy(
selectedType = GeneratorState.MainType.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Passphrase(
numWords = initialNumWords,
),
),
selectedType = GeneratorState.MainType.Passphrase(numWords = initialNumWords),
),
)
@ -1066,19 +864,11 @@ class GeneratorScreenTest : BaseComposeTest() {
}
@Test
fun `in Passcode_Passphrase state, incrementing number of words over 20 should do nothing`() {
fun `in Passphrase state, incrementing number of words over 20 should do nothing`() {
val initialNumWords = 20
updateState(
DEFAULT_STATE.copy(
selectedType = GeneratorState.MainType.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Passphrase(
numWords = initialNumWords,
),
),
selectedType = GeneratorState.MainType.Passphrase(numWords = initialNumWords),
),
)
@ -1096,17 +886,11 @@ class GeneratorScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Passphrase state, incrementing number of words should send NumWordsCounterChange action with incremented value`() {
fun `in Passphrase state, incrementing number of words should send NumWordsCounterChange action with incremented value`() {
val initialNumWords = 3
updateState(
DEFAULT_STATE.copy(
selectedType = GeneratorState.MainType.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Passphrase(),
),
selectedType = GeneratorState.MainType.Passphrase(),
),
)
@ -1120,25 +904,18 @@ class GeneratorScreenTest : BaseComposeTest() {
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.NumWordsCounterChange(
GeneratorAction.MainType.Passphrase.NumWordsCounterChange(
numWords = initialNumWords + 1,
),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Passphrase state, toggling capitalize should send ToggleCapitalizeChange action`() {
fun `in Passphrase state, toggling capitalize should send ToggleCapitalizeChange action`() {
updateState(
DEFAULT_STATE.copy(
selectedType = GeneratorState.MainType.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Passphrase(),
),
selectedType = GeneratorState.MainType.Passphrase(),
),
)
@ -1149,7 +926,7 @@ class GeneratorScreenTest : BaseComposeTest() {
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.ToggleCapitalizeChange(
GeneratorAction.MainType.Passphrase.ToggleCapitalizeChange(
capitalize = true,
),
)
@ -1158,16 +935,10 @@ class GeneratorScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Passphrase state, toggling the include number toggle should send ToggleIncludeNumberChange action`() {
fun `in Passphrase state, toggling the include number toggle should send ToggleIncludeNumberChange action`() {
updateState(
DEFAULT_STATE.copy(
selectedType = GeneratorState.MainType.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Passphrase(),
),
selectedType = GeneratorState.MainType.Passphrase(),
),
)
@ -1177,7 +948,7 @@ class GeneratorScreenTest : BaseComposeTest() {
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.ToggleIncludeNumberChange(
GeneratorAction.MainType.Passphrase.ToggleIncludeNumberChange(
includeNumber = true,
),
)
@ -1186,16 +957,10 @@ class GeneratorScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Passphrase state, updating text in word separator should send WordSeparatorTextChange action`() {
fun `in Passphrase state, updating text in word separator should send WordSeparatorTextChange action`() {
updateState(
DEFAULT_STATE.copy(
selectedType = GeneratorState.MainType.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Passphrase(),
),
selectedType = GeneratorState.MainType.Passphrase(),
),
)
@ -1206,7 +971,7 @@ class GeneratorScreenTest : BaseComposeTest() {
verify {
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.WordSeparatorTextChange(
GeneratorAction.MainType.Passphrase.WordSeparatorTextChange(
wordSeparator = 'a',
),
)
@ -1765,8 +1530,6 @@ class GeneratorScreenTest : BaseComposeTest() {
private val DEFAULT_STATE = GeneratorState(
generatedText = "",
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(),
),
selectedType = GeneratorState.MainType.Password(),
currentEmailAddress = "currentEmail",
)

View file

@ -163,9 +163,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
fakeGeneratorRepository.savePasscodeGenerationOptions(passcodeGenerationOptions)
val expected = GeneratorState(
generatedText = "updatedPassphrase",
selectedType = GeneratorState.MainType.Passcode(
selectedType = GeneratorState.MainType.Passcode.PasscodeType.Passphrase(),
),
selectedType = GeneratorState.MainType.Passphrase(),
generatorMode = GeneratorMode.Modal.Password,
currentEmailAddress = "currentEmail",
isUnderPolicy = false,
@ -213,8 +211,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
mutablePolicyFlow.tryEmit(value = policies)
assertEquals(
initialPasscodeState.copy(
selectedType = GeneratorState.MainType.Passcode(
selectedType = GeneratorState.MainType.Passcode.PasscodeType.Password(
selectedType = GeneratorState.MainType.Password(
length = 14,
minLength = 10,
maxLength = 128,
@ -236,7 +233,6 @@ class GeneratorViewModelTest : BaseViewModelTest() {
ambiguousCharsEnabled = true,
isUserInteracting = false,
),
),
isUnderPolicy = true,
),
awaitItem(),
@ -247,8 +243,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
mutablePolicyFlow.tryEmit(value = emptyList())
assertEquals(
initialPasscodeState.copy(
selectedType = GeneratorState.MainType.Passcode(
selectedType = GeneratorState.MainType.Passcode.PasscodeType.Password(
selectedType = GeneratorState.MainType.Password(
length = 14,
minLength = 5,
maxLength = 128,
@ -270,7 +265,6 @@ class GeneratorViewModelTest : BaseViewModelTest() {
ambiguousCharsEnabled = true,
isUserInteracting = false,
),
),
isUnderPolicy = false,
),
awaitItem(),
@ -562,8 +556,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel()
assertEquals(
initialPasscodeState.copy(
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
selectedType = GeneratorState.MainType.Password(
length = 14,
minLength = 10,
useCapitals = true,
@ -581,7 +574,6 @@ class GeneratorViewModelTest : BaseViewModelTest() {
avoidAmbiguousChars = false,
),
),
),
viewModel.stateFlow.value,
)
}
@ -613,8 +605,8 @@ class GeneratorViewModelTest : BaseViewModelTest() {
} returns listOf(policy)
val viewModel = createViewModel()
val action = GeneratorAction.MainType.Passcode.PasscodeTypeOptionSelect(
passcodeTypeOption = GeneratorState.MainType.Passcode.PasscodeTypeOption.PASSPHRASE,
val action = GeneratorAction.MainTypeOptionSelect(
mainTypeOption = GeneratorState.MainTypeOption.PASSPHRASE,
)
viewModel.trySendAction(action)
@ -622,8 +614,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
assertEquals(
initialPasscodeState.copy(
generatedText = "updatedPassphrase",
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Passphrase(
selectedType = GeneratorState.MainType.Passphrase(
numWords = 5,
minNumWords = 5,
maxNumWords = 20,
@ -633,7 +624,6 @@ class GeneratorViewModelTest : BaseViewModelTest() {
includeNumberEnabled = false,
),
),
),
viewModel.stateFlow.value,
)
}
@ -659,10 +649,8 @@ class GeneratorViewModelTest : BaseViewModelTest() {
assertEquals(
initialPasscodeState.copy(
generatedText = "updatedPassphrase",
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Passphrase(),
),
overridePassword = true,
selectedType = GeneratorState.MainType.Passphrase(),
passcodePolicyOverride = GeneratorState.PasscodePolicyOverride.PASSPHRASE,
),
viewModel.stateFlow.value,
)
@ -701,10 +689,8 @@ class GeneratorViewModelTest : BaseViewModelTest() {
assertEquals(
initialPasscodeState.copy(
generatedText = "defaultPassword",
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(),
),
overridePassword = true,
selectedType = GeneratorState.MainType.Password(),
passcodePolicyOverride = GeneratorState.PasscodePolicyOverride.PASSWORD,
),
viewModel.stateFlow.value,
)
@ -721,9 +707,8 @@ class GeneratorViewModelTest : BaseViewModelTest() {
viewModel.trySendAction(action)
val expectedState =
initialPasscodeState.copy(
selectedType = GeneratorState.MainType.Passcode(),
val expectedState = initialPasscodeState.copy(
selectedType = GeneratorState.MainType.Password(),
generatedText = "updatedText",
)
@ -754,54 +739,6 @@ class GeneratorViewModelTest : BaseViewModelTest() {
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Test
fun `PasscodeTypeOptionSelect PASSWORD should switch to PasswordType`() = runTest {
val viewModel = createViewModel()
fakeGeneratorRepository.setMockGeneratePasswordResult(
GeneratedPasswordResult.Success("updatedText"),
)
val action = GeneratorAction.MainType.Passcode.PasscodeTypeOptionSelect(
passcodeTypeOption = GeneratorState.MainType.Passcode.PasscodeTypeOption.PASSWORD,
)
viewModel.trySendAction(action)
val expectedState = initialPasscodeState.copy(
selectedType = GeneratorState.MainType.Passcode(
selectedType = GeneratorState.MainType.Passcode.PasscodeType.Password(),
),
generatedText = "updatedText",
)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Test
fun `PasscodeTypeOptionSelect PASSPHRASE should switch to PassphraseType`() = runTest {
val viewModel = createViewModel()
val updatedText = "updatedPassphrase"
fakeGeneratorRepository.setMockGeneratePasswordResult(
GeneratedPasswordResult.Success(updatedText),
)
val action = GeneratorAction.MainType.Passcode.PasscodeTypeOptionSelect(
passcodeTypeOption = GeneratorState.MainType.Passcode.PasscodeTypeOption.PASSPHRASE,
)
viewModel.trySendAction(action)
val expectedState = initialPasscodeState.copy(
generatedText = updatedText,
selectedType = GeneratorState.MainType.Passcode(
selectedType = GeneratorState.MainType.Passcode.PasscodeType.Passphrase(),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Test
fun `UsernameTypeOptionSelect PLUS_ADDRESSED_EMAIL should switch to PlusAddressedEmail type`() =
runTest {
@ -975,8 +912,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
),
)
val expectedState = initialState.copy(
selectedType = GeneratorState.MainType.Passcode(
selectedType = GeneratorState.MainType.Passcode.PasscodeType.Passphrase(
selectedType = GeneratorState.MainType.Passphrase(
numWords = 3,
minNumWords = 3,
maxNumWords = 20,
@ -986,7 +922,6 @@ class GeneratorViewModelTest : BaseViewModelTest() {
includeNumber = false,
includeNumberEnabled = true,
),
),
generatedText = "updatedPassphrase",
)
viewModel.trySendAction(GeneratorAction.LifecycleResume)
@ -1055,7 +990,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
val newLength = 16
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.SliderLengthChange(
GeneratorAction.MainType.Password.SliderLengthChange(
length = newLength,
isUserInteracting = false,
),
@ -1063,11 +998,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
val expectedState = defaultPasswordState.copy(
generatedText = updatedGeneratedPassword,
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
length = newLength,
),
),
selectedType = GeneratorState.MainType.Password(length = newLength),
)
assertEquals(expectedState, viewModel.stateFlow.value)
@ -1084,7 +1015,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
// Update the length to something small enough that it updates on capitals toggle.
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.SliderLengthChange(
GeneratorAction.MainType.Password.SliderLengthChange(
length = 5,
isUserInteracting = false,
),
@ -1092,12 +1023,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
// Set useCapitals to false initially to ensure length change is reflected.
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Password
.ToggleCapitalLettersChange(
GeneratorAction.MainType.Password.ToggleCapitalLettersChange(
useCapitals = false,
),
)
@ -1105,18 +1031,16 @@ class GeneratorViewModelTest : BaseViewModelTest() {
// Verify useCapitals is reflected and initial length is correct.
val expectedState1 = defaultPasswordState.copy(
generatedText = updatedGeneratedPassword,
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
selectedType = GeneratorState.MainType.Password(
length = 5,
useCapitals = false,
),
),
)
assertEquals(expectedState1, viewModel.stateFlow.value)
// Update minNumbers so that length will exceed 5 when useCapitals is enabled.
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange(
GeneratorAction.MainType.Password.MinNumbersCounterChange(
minNumbers = 4,
),
)
@ -1124,25 +1048,18 @@ class GeneratorViewModelTest : BaseViewModelTest() {
// Verify length is still 5, minNumbers is updated, and useCapital is still false.
val expectedState2 = defaultPasswordState.copy(
generatedText = updatedGeneratedPassword,
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
selectedType = GeneratorState.MainType.Password(
length = 5, // 0 uppercase + 1 lowercase + 4 numbers + 0 special chars
minNumbers = 4,
useCapitals = false,
useNumbers = true,
),
),
)
assertEquals(expectedState2, viewModel.stateFlow.value)
// Enable useCapitals.
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Password
.ToggleCapitalLettersChange(
GeneratorAction.MainType.Password.ToggleCapitalLettersChange(
useCapitals = true,
),
)
@ -1150,13 +1067,11 @@ class GeneratorViewModelTest : BaseViewModelTest() {
// Verify this has caused length to increase.
val expectedState3 = defaultPasswordState.copy(
generatedText = updatedGeneratedPassword,
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
selectedType = GeneratorState.MainType.Password(
length = 6, // 1 uppercase + 1 lowercase + 4 numbers + 0 special chars
minNumbers = 4,
useCapitals = true,
),
),
)
assertEquals(expectedState3, viewModel.stateFlow.value)
}
@ -1172,7 +1087,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
// Update the length to something small enough that it updates on lowercase toggle.
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.SliderLengthChange(
GeneratorAction.MainType.Password.SliderLengthChange(
length = 5,
isUserInteracting = false,
),
@ -1180,12 +1095,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
// Set useLowercase to false initially to ensure length change is reflected.
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Password
.ToggleLowercaseLettersChange(
GeneratorAction.MainType.Password.ToggleLowercaseLettersChange(
useLowercase = false,
),
)
@ -1193,18 +1103,16 @@ class GeneratorViewModelTest : BaseViewModelTest() {
// Verify useLowercase is reflected and initial length is correct.
val expectedState1 = defaultPasswordState.copy(
generatedText = updatedGeneratedPassword,
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
selectedType = GeneratorState.MainType.Password(
length = 5,
useLowercase = false,
),
),
)
assertEquals(expectedState1, viewModel.stateFlow.value)
// Update minNumbers so that length will exceed 5 when useLowercase is enabled.
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange(
GeneratorAction.MainType.Password.MinNumbersCounterChange(
minNumbers = 4,
),
)
@ -1212,25 +1120,18 @@ class GeneratorViewModelTest : BaseViewModelTest() {
// Verify length is still 5, minNumbers is updated, and useLowercase is still false.
val expectedState2 = defaultPasswordState.copy(
generatedText = updatedGeneratedPassword,
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
selectedType = GeneratorState.MainType.Password(
length = 5, // 1 uppercase + 0 lowercase + 4 numbers + 0 special chars
minNumbers = 4,
useLowercase = false,
useNumbers = true,
),
),
)
assertEquals(expectedState2, viewModel.stateFlow.value)
// Enable useLowercase.
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Password
.ToggleLowercaseLettersChange(
GeneratorAction.MainType.Password.ToggleLowercaseLettersChange(
useLowercase = true,
),
)
@ -1238,13 +1139,11 @@ class GeneratorViewModelTest : BaseViewModelTest() {
// Verify this has caused length to increase.
val expectedState3 = defaultPasswordState.copy(
generatedText = updatedGeneratedPassword,
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
selectedType = GeneratorState.MainType.Password(
length = 6, // 1 uppercase + 1 lowercase + 4 numbers + 0 special chars
minNumbers = 4,
useLowercase = true,
),
),
)
assertEquals(expectedState3, viewModel.stateFlow.value)
}
@ -1259,18 +1158,16 @@ class GeneratorViewModelTest : BaseViewModelTest() {
val useNumbers = true
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleNumbersChange(
GeneratorAction.MainType.Password.ToggleNumbersChange(
useNumbers = useNumbers,
),
)
val expectedState = defaultPasswordState.copy(
generatedText = updatedGeneratedPassword,
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
selectedType = GeneratorState.MainType.Password(
useNumbers = useNumbers,
),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
@ -1288,23 +1185,16 @@ class GeneratorViewModelTest : BaseViewModelTest() {
val useSpecialChars = true
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Password
.ToggleSpecialCharactersChange(
GeneratorAction.MainType.Password.ToggleSpecialCharactersChange(
useSpecialChars = useSpecialChars,
),
)
val expectedState = defaultPasswordState.copy(
generatedText = updatedGeneratedPassword,
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
selectedType = GeneratorState.MainType.Password(
useSpecialChars = useSpecialChars,
),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
@ -1320,19 +1210,14 @@ class GeneratorViewModelTest : BaseViewModelTest() {
// Toggle numbers to false initially so we can verify length changes appropriately.
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Password
.ToggleNumbersChange(
GeneratorAction.MainType.Password.ToggleNumbersChange(
useNumbers = false,
),
)
// Update the length to something small enough that it's updated on numbers toggle.
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.SliderLengthChange(
GeneratorAction.MainType.Password.SliderLengthChange(
length = 5,
isUserInteracting = false,
),
@ -1340,44 +1225,35 @@ class GeneratorViewModelTest : BaseViewModelTest() {
val minNumbers = 5
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange(
GeneratorAction.MainType.Password.MinNumbersCounterChange(
minNumbers = minNumbers,
),
)
val expectedState1 = defaultPasswordState.copy(
generatedText = updatedGeneratedPassword,
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
selectedType = GeneratorState.MainType.Password(
length = 5, // the current slider value, which the min doesn't exceed
minNumbers = minNumbers,
useNumbers = false,
),
),
)
assertEquals(expectedState1, viewModel.stateFlow.value)
// Toggle numbers to true so we can verify length changes appropriately.
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Password
.ToggleNumbersChange(
GeneratorAction.MainType.Password.ToggleNumbersChange(
useNumbers = true,
),
)
val expectedState2 = defaultPasswordState.copy(
generatedText = updatedGeneratedPassword,
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
selectedType = GeneratorState.MainType.Password(
length = 7, // 1 uppercase + 1 lowercase + 5 numbers + 0 special
minNumbers = minNumbers,
useNumbers = true,
),
),
)
assertEquals(expectedState2, viewModel.stateFlow.value)
}
@ -1393,7 +1269,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
// Update the length to something small enough that it's updated on toggle.
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.SliderLengthChange(
GeneratorAction.MainType.Password.SliderLengthChange(
length = 5,
isUserInteracting = false,
),
@ -1402,12 +1278,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
val minSpecial = 5
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Password
.MinSpecialCharactersChange(
GeneratorAction.MainType.Password.MinSpecialCharactersChange(
minSpecial = minSpecial,
),
)
@ -1415,23 +1286,16 @@ class GeneratorViewModelTest : BaseViewModelTest() {
// Length should still be 5 because special characters are not enabled.
val expectedState1 = defaultPasswordState.copy(
generatedText = updatedGeneratedPassword,
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
selectedType = GeneratorState.MainType.Password(
length = 5,
minSpecial = minSpecial,
useSpecialChars = false,
),
),
)
assertEquals(expectedState1, viewModel.stateFlow.value)
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Password
.ToggleSpecialCharactersChange(
GeneratorAction.MainType.Password.ToggleSpecialCharactersChange(
useSpecialChars = true,
),
)
@ -1439,13 +1303,11 @@ class GeneratorViewModelTest : BaseViewModelTest() {
// Length should update to 7 because special characters are now enabled.
val expectedState2 = defaultPasswordState.copy(
generatedText = updatedGeneratedPassword,
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
selectedType = GeneratorState.MainType.Password(
length = 8, // 1 uppercase + 1 lowercase + 1 number + 5 special chars
minSpecial = minSpecial,
useSpecialChars = true,
),
),
)
assertEquals(expectedState2, viewModel.stateFlow.value)
}
@ -1462,23 +1324,16 @@ class GeneratorViewModelTest : BaseViewModelTest() {
val avoidAmbiguousChars = true
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Password
.ToggleAvoidAmbigousCharactersChange(
GeneratorAction.MainType.Password.ToggleAvoidAmbigousCharactersChange(
avoidAmbiguousChars = avoidAmbiguousChars,
),
)
val expectedState = defaultPasswordState.copy(
generatedText = updatedGeneratedPassword,
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
selectedType = GeneratorState.MainType.Password(
avoidAmbiguousChars = avoidAmbiguousChars,
),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
@ -1493,60 +1348,45 @@ class GeneratorViewModelTest : BaseViewModelTest() {
// Initially turn on all toggles
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleCapitalLettersChange(
GeneratorAction.MainType.Password.ToggleCapitalLettersChange(
useCapitals = true,
),
)
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Password
.ToggleLowercaseLettersChange(
GeneratorAction.MainType.Password.ToggleLowercaseLettersChange(
useLowercase = true,
),
)
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleNumbersChange(
GeneratorAction.MainType.Password.ToggleNumbersChange(
useNumbers = true,
),
)
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Password
.ToggleSpecialCharactersChange(
GeneratorAction.MainType.Password.ToggleSpecialCharactersChange(
useSpecialChars = true,
),
)
// Attempt to turn off all toggles
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleCapitalLettersChange(
GeneratorAction.MainType.Password.ToggleCapitalLettersChange(
useCapitals = false,
),
)
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Password
.ToggleLowercaseLettersChange(
GeneratorAction.MainType.Password.ToggleLowercaseLettersChange(
useLowercase = false,
),
)
viewModel.trySendAction(
GeneratorAction.MainType.Passcode.PasscodeType.Password.ToggleNumbersChange(
GeneratorAction.MainType.Password.ToggleNumbersChange(
useNumbers = false,
),
)
// Check the state with only one toggle (useSpecialChars) left on
val intermediatePasswordState = GeneratorState.MainType.Passcode.PasscodeType.Password(
val intermediatePasswordState = GeneratorState.MainType.Password(
useCapitals = false,
useLowercase = false,
useNumbers = false,
@ -1555,32 +1395,23 @@ class GeneratorViewModelTest : BaseViewModelTest() {
val intermediateState = defaultPasswordState.copy(
generatedText = updatedGeneratedPassword,
selectedType = GeneratorState.MainType.Passcode(
selectedType = intermediatePasswordState,
),
)
assertEquals(intermediateState, viewModel.stateFlow.value)
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Password
.ToggleSpecialCharactersChange(
GeneratorAction.MainType.Password.ToggleSpecialCharactersChange(
useSpecialChars = false,
),
)
// Check if useLowercase is turned on automatically
val expectedState = intermediateState.copy(
selectedType = GeneratorState.MainType.Passcode(
selectedType = intermediatePasswordState.copy(
useLowercase = true,
useSpecialChars = false,
),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
@ -1611,23 +1442,16 @@ class GeneratorViewModelTest : BaseViewModelTest() {
val newNumWords = 4
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Passphrase
.NumWordsCounterChange(
GeneratorAction.MainType.Passphrase.NumWordsCounterChange(
numWords = newNumWords,
),
)
val expectedState = defaultPassphraseState.copy(
generatedText = updatedGeneratedPassphrase,
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Passphrase(
selectedType = GeneratorState.MainType.Passphrase(
numWords = newNumWords,
),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
@ -1644,22 +1468,16 @@ class GeneratorViewModelTest : BaseViewModelTest() {
val newWordSeparatorChar = '_'
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType.Passphrase
.WordSeparatorTextChange(
GeneratorAction.MainType.Passphrase.WordSeparatorTextChange(
wordSeparator = newWordSeparatorChar,
),
)
val expectedState = defaultPassphraseState.copy(
generatedText = updatedGeneratedPassphrase,
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Passphrase(
selectedType = GeneratorState.MainType.Passphrase(
wordSeparator = newWordSeparatorChar,
),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
@ -1674,23 +1492,16 @@ class GeneratorViewModelTest : BaseViewModelTest() {
)
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Passphrase
.ToggleIncludeNumberChange(
GeneratorAction.MainType.Passphrase.ToggleIncludeNumberChange(
includeNumber = true,
),
)
val expectedState = defaultPassphraseState.copy(
generatedText = updatedGeneratedPassphrase,
selectedType = GeneratorState.MainType.Passcode(
selectedType = GeneratorState.MainType.Passcode.PasscodeType.Passphrase(
selectedType = GeneratorState.MainType.Passphrase(
includeNumber = true,
),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
@ -1705,23 +1516,16 @@ class GeneratorViewModelTest : BaseViewModelTest() {
)
viewModel.trySendAction(
GeneratorAction
.MainType
.Passcode
.PasscodeType
.Passphrase
.ToggleCapitalizeChange(
GeneratorAction.MainType.Passphrase.ToggleCapitalizeChange(
capitalize = true,
),
)
val expectedState = defaultPassphraseState.copy(
generatedText = updatedGeneratedPassphrase,
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Passphrase(
selectedType = GeneratorState.MainType.Passphrase(
capitalize = true,
),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
@ -2307,8 +2111,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
@Test
fun `Password minimumLength should be at least as long as the sum of the minimums`() {
val password =
GeneratorState.MainType.Passcode.PasscodeType.Password(
val password = GeneratorState.MainType.Password(
length = 14,
minLength = 10,
useCapitals = true,
@ -2331,8 +2134,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
@Test
fun `Password minimumLength should use minLength if higher than sum of the minimums`() {
val password =
GeneratorState.MainType.Passcode.PasscodeType.Password(
val password = GeneratorState.MainType.Password(
length = 14,
minLength = 10,
useCapitals = true,
@ -2367,8 +2169,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
): GeneratorState =
GeneratorState(
generatedText = generatedText,
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Password(
selectedType = GeneratorState.MainType.Password(
length = length,
useCapitals = useCapitals,
useLowercase = useLowercase,
@ -2378,7 +2179,6 @@ class GeneratorViewModelTest : BaseViewModelTest() {
minSpecial = minSpecial,
avoidAmbiguousChars = avoidAmbiguousChars,
),
),
currentEmailAddress = "currentEmail",
)
@ -2391,14 +2191,12 @@ class GeneratorViewModelTest : BaseViewModelTest() {
): GeneratorState =
GeneratorState(
generatedText = generatedText,
selectedType = GeneratorState.MainType.Passcode(
GeneratorState.MainType.Passcode.PasscodeType.Passphrase(
selectedType = GeneratorState.MainType.Passphrase(
numWords = numWords,
wordSeparator = wordSeparator,
capitalize = capitalize,
includeNumber = includeNumber,
),
),
currentEmailAddress = "currentEmail",
)