Add BitwardenStepper component (#226)

This commit is contained in:
Andrew Haisting 2023-11-08 12:12:17 -06:00 committed by Álison Fernandes
parent 0185456dca
commit bcffbe6fce
8 changed files with 323 additions and 144 deletions

View file

@ -0,0 +1,73 @@
package com.x8bit.bitwarden.ui.platform.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
/**
* Displays a stepper that allows the user to increment and decrement an int value.
*
* @param label Label for the stepper.
* @param value Value to display. Null will display nothing. Will be clamped to [range] before
* display.
* @param onValueChange callback invoked when the user increments or decrements the count. Note
* that this will not be called if the attempts to move value outside of [range].
* @param modifier Modifier.
* @param range Range of valid values.
* @param isIncrementEnabled whether or not the increment button should be enabled.
* @param isDecrementEnabled whether or not the decrement button should be enabled.
*/
@Composable
fun BitwardenStepper(
label: String,
value: Int?,
onValueChange: (Int) -> Unit,
modifier: Modifier = Modifier,
range: ClosedRange<Int> = 1..Int.MAX_VALUE,
isIncrementEnabled: Boolean = true,
isDecrementEnabled: Boolean = true,
) {
val clampedValue = value?.coerceIn(range)
if (clampedValue != value && clampedValue != null) {
onValueChange(clampedValue)
}
BitwardenReadOnlyTextFieldWithActions(
label = label,
// we use a space instead of empty string to make sure label is shown small and above
// the input
value = clampedValue
?.toString()
?: " ",
actions = {
BitwardenIconButtonWithResource(
iconRes = IconResource(
iconPainter = painterResource(id = R.drawable.ic_minus),
contentDescription = "\u2212",
),
onClick = {
val decrementedValue = ((value ?: 0) - 1).coerceIn(range)
if (decrementedValue != value) {
onValueChange(decrementedValue)
}
},
isEnabled = isDecrementEnabled,
)
BitwardenIconButtonWithResource(
iconRes = IconResource(
iconPainter = painterResource(id = R.drawable.ic_plus),
contentDescription = "+",
),
onClick = {
val incrementedValue = ((value ?: 0) + 1).coerceIn(range)
if (incrementedValue != value) {
onValueChange(incrementedValue)
}
},
isEnabled = isIncrementEnabled,
)
},
modifier = modifier,
)
}

View file

@ -50,10 +50,15 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenReadOnlyTextFieldWithActions import com.x8bit.bitwarden.ui.platform.components.BitwardenReadOnlyTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenStepper
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
import com.x8bit.bitwarden.ui.platform.components.model.IconResource import com.x8bit.bitwarden.ui.platform.components.model.IconResource
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme 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.Passcode.PasscodeType.Password.Companion.PASSWORD_COUNTER_MAX
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_COUNTER_MIN
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_LENGTH_SLIDER_MAX import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_LENGTH_SLIDER_MAX
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_LENGTH_SLIDER_MIN import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_LENGTH_SLIDER_MIN
@ -472,29 +477,11 @@ private fun PasswordMinNumbersCounterItem(
minNumbers: Int, minNumbers: Int,
onPasswordMinNumbersCounterChange: (Int) -> Unit, onPasswordMinNumbersCounterChange: (Int) -> Unit,
) { ) {
BitwardenReadOnlyTextFieldWithActions( BitwardenStepper(
label = stringResource(id = R.string.min_numbers), label = stringResource(id = R.string.min_numbers),
value = minNumbers.toString(), value = minNumbers,
actions = { range = PASSWORD_COUNTER_MIN..PASSWORD_COUNTER_MAX,
BitwardenIconButtonWithResource( onValueChange = onPasswordMinNumbersCounterChange,
iconRes = IconResource(
iconPainter = painterResource(id = R.drawable.ic_minus),
contentDescription = "\u2212",
),
onClick = {
onPasswordMinNumbersCounterChange(minNumbers - 1)
},
)
BitwardenIconButtonWithResource(
iconRes = IconResource(
iconPainter = painterResource(id = R.drawable.ic_plus),
contentDescription = "+",
),
onClick = {
onPasswordMinNumbersCounterChange(minNumbers + 1)
},
)
},
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
) )
} }
@ -504,29 +491,11 @@ private fun PasswordMinSpecialCharactersCounterItem(
minSpecial: Int, minSpecial: Int,
onPasswordMinSpecialCharactersChange: (Int) -> Unit, onPasswordMinSpecialCharactersChange: (Int) -> Unit,
) { ) {
BitwardenReadOnlyTextFieldWithActions( BitwardenStepper(
label = stringResource(id = R.string.min_special), label = stringResource(id = R.string.min_special),
value = minSpecial.toString(), value = minSpecial,
actions = { range = PASSWORD_COUNTER_MIN..PASSWORD_COUNTER_MAX,
BitwardenIconButtonWithResource( onValueChange = onPasswordMinSpecialCharactersChange,
iconRes = IconResource(
iconPainter = painterResource(id = R.drawable.ic_minus),
contentDescription = "\u2212",
),
onClick = {
onPasswordMinSpecialCharactersChange(minSpecial - 1)
},
)
BitwardenIconButtonWithResource(
iconRes = IconResource(
iconPainter = painterResource(id = R.drawable.ic_plus),
contentDescription = "+",
),
onClick = {
onPasswordMinSpecialCharactersChange(minSpecial + 1)
},
)
},
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
) )
} }
@ -586,29 +555,11 @@ private fun PassphraseNumWordsCounterItem(
numWords: Int, numWords: Int,
onPassphraseNumWordsCounterChange: (Int) -> Unit, onPassphraseNumWordsCounterChange: (Int) -> Unit,
) { ) {
BitwardenReadOnlyTextFieldWithActions( BitwardenStepper(
label = stringResource(id = R.string.number_of_words), label = stringResource(id = R.string.number_of_words),
value = numWords.toString(), value = numWords,
actions = { range = PASSPHRASE_MIN_NUMBER_OF_WORDS..PASSPHRASE_MAX_NUMBER_OF_WORDS,
BitwardenIconButtonWithResource( onValueChange = onPassphraseNumWordsCounterChange,
iconRes = IconResource(
iconPainter = painterResource(id = R.drawable.ic_minus),
contentDescription = "\u2212",
),
onClick = {
onPassphraseNumWordsCounterChange(numWords - 1)
},
)
BitwardenIconButtonWithResource(
iconRes = IconResource(
iconPainter = painterResource(id = R.drawable.ic_plus),
contentDescription = "+",
),
onClick = {
onPassphraseNumWordsCounterChange(numWords + 1)
},
)
},
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
) )
} }

View file

@ -9,11 +9,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode 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.Passphrase
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Passphrase.Companion.PASSPHRASE_MAX_NUMBER_OF_WORDS
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Passphrase.Companion.PASSPHRASE_MIN_NUMBER_OF_WORDS
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_COUNTER_MAX
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_COUNTER_MIN
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeTypeOption import 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
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType.AnonAddy import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType.AnonAddy
@ -24,8 +20,6 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.lang.Integer.max
import java.lang.Integer.min
import javax.inject.Inject import javax.inject.Inject
private const val KEY_STATE = "state" private const val KEY_STATE = "state"
@ -266,24 +260,16 @@ class GeneratorViewModel @Inject constructor(
private fun handleMinNumbersChange( private fun handleMinNumbersChange(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange, action: GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange,
) { ) {
val adjustedMinNumbers = action
.minNumbers
.coerceIn(PASSWORD_COUNTER_MIN, PASSWORD_COUNTER_MAX)
updatePasswordType { currentPasswordType -> updatePasswordType { currentPasswordType ->
currentPasswordType.copy(minNumbers = adjustedMinNumbers) currentPasswordType.copy(minNumbers = action.minNumbers)
} }
} }
private fun handleMinSpecialChange( private fun handleMinSpecialChange(
action: GeneratorAction.MainType.Passcode.PasscodeType.Password.MinSpecialCharactersChange, action: GeneratorAction.MainType.Passcode.PasscodeType.Password.MinSpecialCharactersChange,
) { ) {
val adjustedMinSpecial = action
.minSpecial
.coerceIn(PASSWORD_COUNTER_MIN, PASSWORD_COUNTER_MAX)
updatePasswordType { currentPasswordType -> updatePasswordType { currentPasswordType ->
currentPasswordType.copy(minSpecial = adjustedMinSpecial) currentPasswordType.copy(minSpecial = action.minSpecial)
} }
} }
@ -352,11 +338,7 @@ class GeneratorViewModel @Inject constructor(
action: GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.NumWordsCounterChange, action: GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.NumWordsCounterChange,
) { ) {
updatePassphraseType { passphraseType -> updatePassphraseType { passphraseType ->
val newNumWords = max( passphraseType.copy(numWords = action.numWords)
PASSPHRASE_MIN_NUMBER_OF_WORDS,
min(PASSPHRASE_MAX_NUMBER_OF_WORDS, action.numWords),
)
passphraseType.copy(numWords = newNumWords)
} }
} }

View file

@ -43,19 +43,16 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledTonalButton import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledTonalButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenIconButtonWithResource
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.BitwardenReadOnlyTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenSegmentedButton import com.x8bit.bitwarden.ui.platform.components.BitwardenSegmentedButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenStepper
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
import com.x8bit.bitwarden.ui.platform.components.SegmentedButtonState import com.x8bit.bitwarden.ui.platform.components.SegmentedButtonState
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
import com.x8bit.bitwarden.ui.tools.feature.send.NewSendAction.MaxAccessCountChange
/** /**
* Displays new send UX. * Displays new send UX.
@ -211,11 +208,8 @@ fun NewSendScreen(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
NewSendOptions( NewSendOptions(
state = state, state = state,
onIncrementMaxAccessCountClick = remember(viewModel) { onMaxAccessCountChange = remember(viewModel) {
{ viewModel.trySendAction(MaxAccessCountChange(it)) } { viewModel.trySendAction(NewSendAction.MaxAccessCountChange(it)) }
},
onDecrementMaxAccessCountClick = remember(viewModel) {
{ viewModel.trySendAction(MaxAccessCountChange(it)) }
}, },
onPasswordChange = remember(viewModel) { onPasswordChange = remember(viewModel) {
{ viewModel.trySendAction(NewSendAction.PasswordChange(it)) } { viewModel.trySendAction(NewSendAction.PasswordChange(it)) }
@ -238,8 +232,7 @@ fun NewSendScreen(
* Displays a collapsable set of new send options. * Displays a collapsable set of new send options.
* *
* @param state state. * @param state state.
* @param onIncrementMaxAccessCountClick called when increment max access count is clicked. * @param onMaxAccessCountChange called when max access count changes.
* @param onDecrementMaxAccessCountClick called when decrement max access count is clicked.
* @param onPasswordChange called when the password changes. * @param onPasswordChange called when the password changes.
* @param onNoteChange called when the notes changes. * @param onNoteChange called when the notes changes.
* @param onHideEmailChecked called when hide email is checked. * @param onHideEmailChecked called when hide email is checked.
@ -249,8 +242,7 @@ fun NewSendScreen(
@Composable @Composable
private fun NewSendOptions( private fun NewSendOptions(
state: NewSendState, state: NewSendState,
onIncrementMaxAccessCountClick: (Int) -> Unit, onMaxAccessCountChange: (Int) -> Unit,
onDecrementMaxAccessCountClick: (Int) -> Unit,
onPasswordChange: (String) -> Unit, onPasswordChange: (String) -> Unit,
onNoteChange: (String) -> Unit, onNoteChange: (String) -> Unit,
onHideEmailChecked: (Boolean) -> Unit, onHideEmailChecked: (Boolean) -> Unit,
@ -300,33 +292,13 @@ private fun NewSendOptions(
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
BitwardenReadOnlyTextFieldWithActions( BitwardenStepper(
label = stringResource(R.string.maximum_access_count), label = stringResource(id = R.string.maximum_access_count),
// we use a space instead of empty string to make sure label is shown small and value = state.maxAccessCount,
// above the input onValueChange = onMaxAccessCountChange,
value = state.maxAccessCount?.toString() ?: " ", isDecrementEnabled = state.maxAccessCount != null,
actions = { modifier = Modifier
BitwardenIconButtonWithResource( .padding(horizontal = 16.dp),
iconRes = IconResource(
iconPainter = painterResource(id = R.drawable.ic_minus),
contentDescription = "\u2212",
),
onClick = {
onIncrementMaxAccessCountClick.invoke((state.maxAccessCount ?: 0) - 1)
},
isEnabled = state.maxAccessCount != null,
)
BitwardenIconButtonWithResource(
iconRes = IconResource(
iconPainter = painterResource(id = R.drawable.ic_plus),
contentDescription = "+",
),
onClick = {
onDecrementMaxAccessCountClick.invoke((state.maxAccessCount ?: 0) + 1)
},
)
},
modifier = Modifier.padding(horizontal = 16.dp),
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Text( Text(

View file

@ -10,7 +10,6 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.max
private const val KEY_STATE = "state" private const val KEY_STATE = "state"
@ -127,7 +126,7 @@ class NewSendViewModel @Inject constructor(
private fun handleMaxAccessCountChange(action: NewSendAction.MaxAccessCountChange) { private fun handleMaxAccessCountChange(action: NewSendAction.MaxAccessCountChange) {
mutableStateFlow.update { mutableStateFlow.update {
it.copy(maxAccessCount = max(1, action.newValue)) it.copy(maxAccessCount = action.value)
} }
} }
} }
@ -239,9 +238,9 @@ sealed class NewSendAction {
data class HideByDefaultToggle(val isChecked: Boolean) : NewSendAction() data class HideByDefaultToggle(val isChecked: Boolean) : NewSendAction()
/** /**
* User incremented or decremented the max access count. * User incremented the max access count.
*/ */
data class MaxAccessCountChange(val newValue: Int) : NewSendAction() data class MaxAccessCountChange(val value: Int) : NewSendAction()
/** /**
* User toggled the "hide my email" toggle. * User toggled the "hide my email" toggle.

View file

@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import org.junit.Test import org.junit.Test
@Suppress("LargeClass")
class GeneratorScreenTest : BaseComposeTest() { class GeneratorScreenTest : BaseComposeTest() {
private val mutableStateFlow = MutableStateFlow( private val mutableStateFlow = MutableStateFlow(
GeneratorState( GeneratorState(
@ -409,6 +410,70 @@ class GeneratorScreenTest : BaseComposeTest() {
} }
} }
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Password state, decrementing the minimum numbers counter below 0 should do nothing`() {
val initialMinNumbers = 0
updateState(
GeneratorState(
generatedText = "Placeholder",
selectedType = GeneratorState
.MainType
.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Password(minNumbers = initialMinNumbers),
),
),
)
composeTestRule.setContent {
GeneratorScreen(viewModel = viewModel)
}
composeTestRule.onNodeWithContentDescription("Minimum numbers, $initialMinNumbers")
.onChildren()
.filterToOne(hasContentDescription("\u2212"))
.performScrollTo()
.performClick()
verify(exactly = 0) { viewModel.trySendAction(any()) }
}
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Password state, incrementing the minimum numbers counter above 5 should do nothing`() {
val initialMinNumbers = 5
updateState(
GeneratorState(
generatedText = "Placeholder",
selectedType = GeneratorState
.MainType
.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Password(minNumbers = initialMinNumbers),
),
),
)
composeTestRule.setContent {
GeneratorScreen(viewModel = viewModel)
}
composeTestRule.onNodeWithContentDescription("Minimum numbers, $initialMinNumbers")
.onChildren()
.filterToOne(hasContentDescription("+"))
.performScrollTo()
.performClick()
verify(exactly = 0) { viewModel.trySendAction(any()) }
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `in Passcode_Password state, decrementing the minimum special characters counter should send MinSpecialCharactersChange action`() { fun `in Passcode_Password state, decrementing the minimum special characters counter should send MinSpecialCharactersChange action`() {
@ -485,6 +550,70 @@ class GeneratorScreenTest : BaseComposeTest() {
} }
} }
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Password state, decrementing the minimum special characters below 0 should do nothing`() {
val initialSpecialChars = 0
updateState(
GeneratorState(
generatedText = "Placeholder",
selectedType = GeneratorState
.MainType
.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Password(minSpecial = initialSpecialChars),
),
),
)
composeTestRule.setContent {
GeneratorScreen(viewModel = viewModel)
}
composeTestRule.onNodeWithContentDescription("Minimum special, $initialSpecialChars")
.onChildren()
.filterToOne(hasContentDescription("\u2212"))
.performScrollTo()
.performClick()
verify(exactly = 0) { viewModel.trySendAction(any()) }
}
@Suppress("MaxLineLength")
@Test
fun `in Passcode_Password state, decrementing the minimum special characters above 5 should do nothing`() {
val initialSpecialChars = 5
updateState(
GeneratorState(
generatedText = "Placeholder",
selectedType = GeneratorState
.MainType
.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Password(minSpecial = initialSpecialChars),
),
),
)
composeTestRule.setContent {
GeneratorScreen(viewModel = viewModel)
}
composeTestRule.onNodeWithContentDescription("Minimum special, $initialSpecialChars")
.onChildren()
.filterToOne(hasContentDescription("+"))
.performScrollTo()
.performClick()
verify(exactly = 0) { viewModel.trySendAction(any()) }
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `in Passcode_Password state, toggling the use avoid ambiguous characters toggle should send ToggleSpecialCharactersChange action`() { fun `in Passcode_Password state, toggling the use avoid ambiguous characters toggle should send ToggleSpecialCharactersChange action`() {
@ -517,7 +646,7 @@ class GeneratorScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `in Passcode_Passphrase state, decrementing number of words should send NumWordsCounterChange action with decremented value`() { fun `in Passcode_Passphrase state, decrementing number of words should send NumWordsCounterChange action with decremented value`() {
val initialNumWords = 3 val initialNumWords = 4
updateState( updateState(
GeneratorState( GeneratorState(
generatedText = "Placeholder", generatedText = "Placeholder",
@ -528,7 +657,7 @@ class GeneratorScreenTest : BaseComposeTest() {
.MainType .MainType
.Passcode .Passcode
.PasscodeType .PasscodeType
.Passphrase(), .Passphrase(numWords = initialNumWords),
), ),
), ),
) )
@ -539,7 +668,7 @@ class GeneratorScreenTest : BaseComposeTest() {
// Unicode for "minus" used for content description // Unicode for "minus" used for content description
composeTestRule composeTestRule
.onNodeWithContentDescription("Number of words, 3") .onNodeWithContentDescription("Number of words, $initialNumWords")
.onChildren() .onChildren()
.filterToOne(hasContentDescription("\u2212")) .filterToOne(hasContentDescription("\u2212"))
.performScrollTo() .performScrollTo()
@ -554,6 +683,70 @@ class GeneratorScreenTest : BaseComposeTest() {
} }
} }
@Test
fun `in Passcode_Passphrase state, decrementing number of words under 3 should do nothing`() {
val initialNumWords = 3
updateState(
GeneratorState(
generatedText = "Placeholder",
selectedType = GeneratorState
.MainType
.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Passphrase(numWords = initialNumWords),
),
),
)
composeTestRule.setContent {
GeneratorScreen(viewModel = viewModel)
}
// Unicode for "minus" used for content description
composeTestRule
.onNodeWithContentDescription("Number of words, $initialNumWords")
.onChildren()
.filterToOne(hasContentDescription("\u2212"))
.performScrollTo()
.performClick()
verify(exactly = 0) { viewModel.trySendAction(any()) }
}
@Test
fun `in Passcode_Passphrase state, incrementing number of words over 20 should do nothing`() {
val initialNumWords = 20
updateState(
GeneratorState(
generatedText = "Placeholder",
selectedType = GeneratorState
.MainType
.Passcode(
GeneratorState
.MainType
.Passcode
.PasscodeType
.Passphrase(numWords = initialNumWords),
),
),
)
composeTestRule.setContent {
GeneratorScreen(viewModel = viewModel)
}
// Unicode for "minus" used for content description
composeTestRule
.onNodeWithContentDescription("Number of words, $initialNumWords")
.onChildren()
.filterToOne(hasContentDescription("+"))
.performScrollTo()
.performClick()
verify(exactly = 0) { viewModel.trySendAction(any()) }
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `in Passcode_Passphrase state, incrementing number of words should send NumWordsCounterChange action with incremented value`() { fun `in Passcode_Passphrase state, incrementing number of words should send NumWordsCounterChange action with incremented value`() {

View file

@ -248,6 +248,25 @@ class NewSendScreenTest : BaseComposeTest() {
verify { viewModel.trySendAction(NewSendAction.MaxAccessCountChange(2)) } verify { viewModel.trySendAction(NewSendAction.MaxAccessCountChange(2)) }
} }
@Test
fun `max access count decrement when set to 1 should do nothing`() =
runTest {
mutableStateFlow.update {
it.copy(maxAccessCount = 1)
}
// Expand options section:
composeTestRule
.onNodeWithText("Options")
.performScrollTo()
.performClick()
composeTestRule
.onNodeWithContentDescription("\u2212")
.performScrollTo()
.performClick()
verify(exactly = 0) { viewModel.trySendAction(any()) }
}
@Test @Test
fun `on max access count increment should send MaxAccessCountChange`() = runTest { fun `on max access count increment should send MaxAccessCountChange`() = runTest {
// Expand options section: // Expand options section:

View file

@ -87,16 +87,6 @@ class NewSendViewModelTest : BaseViewModelTest() {
} }
} }
@Test
fun `MaxAccessCountChang below 1 should keep maxAccessCount at 1`() = runTest {
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
viewModel.trySendAction(NewSendAction.MaxAccessCountChange(0))
assertEquals(DEFAULT_STATE.copy(maxAccessCount = 1), awaitItem())
}
}
@Test @Test
fun `TextChange should update text input`() = runTest { fun `TextChange should update text input`() = runTest {
val viewModel = createViewModel() val viewModel = createViewModel()