mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
[PM-10118] update selected generator type when returning to main tab. (#3942)
This commit is contained in:
parent
f68b4df9f9
commit
f26374aae7
4 changed files with 177 additions and 16 deletions
|
@ -54,9 +54,11 @@ import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
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.base.util.LivecycleEventEffect
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.toDp
|
import com.x8bit.bitwarden.ui.platform.base.util.toDp
|
||||||
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenMediumTopAppBar
|
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.BitwardenTopAppBar
|
||||||
|
@ -107,6 +109,16 @@ fun GeneratorScreen(
|
||||||
val resources = context.resources
|
val resources = context.resources
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
LivecycleEventEffect { _, event ->
|
||||||
|
when (event) {
|
||||||
|
Lifecycle.Event.ON_RESUME -> {
|
||||||
|
viewModel.trySendAction(GeneratorAction.LifecycleResume)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
EventsEffect(viewModel = viewModel) { event ->
|
EventsEffect(viewModel = viewModel) { event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
GeneratorEvent.NavigateToPasswordHistory -> onNavigateToPasswordHistory()
|
GeneratorEvent.NavigateToPasswordHistory -> onNavigateToPasswordHistory()
|
||||||
|
|
|
@ -134,9 +134,16 @@ class GeneratorViewModel @Inject constructor(
|
||||||
is GeneratorAction.MainTypeOptionSelect -> handleMainTypeOptionSelect(action)
|
is GeneratorAction.MainTypeOptionSelect -> handleMainTypeOptionSelect(action)
|
||||||
is GeneratorAction.MainType -> handleMainTypeAction(action)
|
is GeneratorAction.MainType -> handleMainTypeAction(action)
|
||||||
is GeneratorAction.Internal -> handleInternalAction(action)
|
is GeneratorAction.Internal -> handleInternalAction(action)
|
||||||
|
GeneratorAction.LifecycleResume -> handleOnResumed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleOnResumed() {
|
||||||
|
// when the screen resumes we need to refresh the options for the current option from
|
||||||
|
// disk in the event they were changed while the screen was in the foreground.
|
||||||
|
loadOptions(shouldUseStorageOptions = true)
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
private fun handleMainTypeAction(action: GeneratorAction.MainType) {
|
private fun handleMainTypeAction(action: GeneratorAction.MainType) {
|
||||||
when (action) {
|
when (action) {
|
||||||
|
@ -267,16 +274,36 @@ class GeneratorViewModel @Inject constructor(
|
||||||
|
|
||||||
//region Generation Handlers
|
//region Generation Handlers
|
||||||
|
|
||||||
private fun loadOptions() {
|
private fun loadOptions(shouldUseStorageOptions: Boolean = false) {
|
||||||
when (val selectedType = state.selectedType) {
|
when (val selectedType = state.selectedType) {
|
||||||
is Passcode -> loadPasscodeOptions(
|
is Passcode -> {
|
||||||
selectedType = selectedType,
|
val mainType = if (shouldUseStorageOptions) {
|
||||||
)
|
generatorRepository
|
||||||
|
.getPasscodeGenerationOptions()
|
||||||
|
?.passcodeType
|
||||||
|
?.let { Passcode(it) }
|
||||||
|
?: selectedType
|
||||||
|
} else {
|
||||||
|
selectedType
|
||||||
|
}
|
||||||
|
loadPasscodeOptions(selectedType = mainType)
|
||||||
|
}
|
||||||
|
|
||||||
is Username -> loadUsernameOptions(
|
is Username -> {
|
||||||
selectedType = selectedType,
|
val mainType = if (shouldUseStorageOptions) {
|
||||||
forceRegeneration = selectedType.selectedType !is ForwardedEmailAlias,
|
generatorRepository
|
||||||
)
|
.getUsernameGenerationOptions()
|
||||||
|
?.usernameType
|
||||||
|
?.let { Username(it) }
|
||||||
|
?: selectedType
|
||||||
|
} else {
|
||||||
|
selectedType
|
||||||
|
}
|
||||||
|
loadUsernameOptions(
|
||||||
|
selectedType = mainType,
|
||||||
|
forceRegeneration = mainType.selectedType !is ForwardedEmailAlias,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2103,6 +2130,11 @@ data class GeneratorState(
|
||||||
*/
|
*/
|
||||||
sealed class GeneratorAction {
|
sealed class GeneratorAction {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates the UI has been entered a resumed lifecycle state.
|
||||||
|
*/
|
||||||
|
data object LifecycleResume : GeneratorAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that the overflow option for password history has been clicked.
|
* Indicates that the overflow option for password history has been clicked.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -545,7 +545,8 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
.performClick()
|
.performClick()
|
||||||
|
|
||||||
verify(exactly = 0) { viewModel.trySendAction(any()) }
|
verify(exactly = 1) { viewModel.trySendAction(GeneratorAction.LifecycleResume) }
|
||||||
|
verify(exactly = 1) { viewModel.trySendAction(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
|
@ -573,7 +574,8 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
.performClick()
|
.performClick()
|
||||||
|
|
||||||
verify(exactly = 0) { viewModel.trySendAction(any()) }
|
verify(exactly = 1) { viewModel.trySendAction(GeneratorAction.LifecycleResume) }
|
||||||
|
verify(exactly = 1) { viewModel.trySendAction(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
|
@ -642,8 +644,8 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
.filterToOne(hasContentDescription("\u2212"))
|
.filterToOne(hasContentDescription("\u2212"))
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
.performClick()
|
.performClick()
|
||||||
|
verify(exactly = 1) { viewModel.trySendAction(GeneratorAction.LifecycleResume) }
|
||||||
verify(exactly = 0) { viewModel.trySendAction(any()) }
|
verify(exactly = 1) { viewModel.trySendAction(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
|
@ -670,8 +672,8 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
.filterToOne(hasContentDescription("+"))
|
.filterToOne(hasContentDescription("+"))
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
.performClick()
|
.performClick()
|
||||||
|
verify(exactly = 1) { viewModel.trySendAction(GeneratorAction.LifecycleResume) }
|
||||||
verify(exactly = 0) { viewModel.trySendAction(any()) }
|
verify(exactly = 1) { viewModel.trySendAction(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
|
@ -1002,7 +1004,8 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
.filterToOne(hasContentDescription("\u2212"))
|
.filterToOne(hasContentDescription("\u2212"))
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
.performClick()
|
.performClick()
|
||||||
verify(exactly = 0) { viewModel.trySendAction(any()) }
|
verify(exactly = 1) { viewModel.trySendAction(GeneratorAction.LifecycleResume) }
|
||||||
|
verify(exactly = 1) { viewModel.trySendAction(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -1030,7 +1033,8 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
.filterToOne(hasContentDescription("+"))
|
.filterToOne(hasContentDescription("+"))
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
.performClick()
|
.performClick()
|
||||||
verify(exactly = 0) { viewModel.trySendAction(any()) }
|
verify(exactly = 1) { viewModel.trySendAction(GeneratorAction.LifecycleResume) }
|
||||||
|
verify(exactly = 1) { viewModel.trySendAction(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
|
@ -1690,6 +1694,11 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `send LifecycleResumed action on screen resume`() {
|
||||||
|
verify { viewModel.trySendAction(GeneratorAction.LifecycleResume) }
|
||||||
|
}
|
||||||
|
|
||||||
//endregion Random Word Tests
|
//endregion Random Word Tests
|
||||||
|
|
||||||
private fun updateState(state: GeneratorState) {
|
private fun updateState(state: GeneratorState) {
|
||||||
|
|
|
@ -856,6 +856,114 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `LifecycleResumedAction should use storage options derived state over VM state`() {
|
||||||
|
val initialState = initialUsernameState.copy(
|
||||||
|
selectedType = GeneratorState.MainType.Username(
|
||||||
|
selectedType = GeneratorState.MainType.Username.UsernameType.PlusAddressedEmail(
|
||||||
|
email = "currentEmail",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val viewModel = createViewModel(initialState)
|
||||||
|
fakeGeneratorRepository.saveUsernameGenerationOptions(
|
||||||
|
UsernameGenerationOptions(
|
||||||
|
type = UsernameGenerationOptions.UsernameType.RANDOM_WORD,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val expectedState = initialState.copy(
|
||||||
|
selectedType = GeneratorState.MainType.Username(
|
||||||
|
selectedType = GeneratorState.MainType.Username.UsernameType.RandomWord(),
|
||||||
|
),
|
||||||
|
generatedText = "randomWord",
|
||||||
|
)
|
||||||
|
viewModel.trySendAction(GeneratorAction.LifecycleResume)
|
||||||
|
assertEquals(
|
||||||
|
expectedState,
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `LifecycleResumedAction should use passcode storage options derived state over VM state`() {
|
||||||
|
val initialState = initialPasscodeState
|
||||||
|
val viewModel = createViewModel(initialState)
|
||||||
|
fakeGeneratorRepository.savePasscodeGenerationOptions(
|
||||||
|
PasscodeGenerationOptions(
|
||||||
|
type = PasscodeGenerationOptions.PasscodeType.PASSPHRASE,
|
||||||
|
length = 14,
|
||||||
|
allowAmbiguousChar = false,
|
||||||
|
hasNumbers = false,
|
||||||
|
minNumber = 3,
|
||||||
|
hasUppercase = false,
|
||||||
|
minUppercase = null,
|
||||||
|
hasLowercase = false,
|
||||||
|
minLowercase = null,
|
||||||
|
allowSpecial = false,
|
||||||
|
minSpecial = 0,
|
||||||
|
numWords = 3,
|
||||||
|
wordSeparator = "-",
|
||||||
|
allowCapitalize = false,
|
||||||
|
allowIncludeNumber = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val expectedState = initialState.copy(
|
||||||
|
selectedType = GeneratorState.MainType.Passcode(
|
||||||
|
selectedType = GeneratorState.MainType.Passcode.PasscodeType.Passphrase(
|
||||||
|
numWords = 3,
|
||||||
|
minNumWords = 3,
|
||||||
|
maxNumWords = 20,
|
||||||
|
wordSeparator = '-',
|
||||||
|
capitalize = false,
|
||||||
|
capitalizeEnabled = true,
|
||||||
|
includeNumber = false,
|
||||||
|
includeNumberEnabled = true,
|
||||||
|
|
||||||
|
),
|
||||||
|
),
|
||||||
|
generatedText = "updatedPassphrase",
|
||||||
|
)
|
||||||
|
viewModel.trySendAction(GeneratorAction.LifecycleResume)
|
||||||
|
assertEquals(
|
||||||
|
expectedState,
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `No loadOptions with default arguments should use VM state options derived state over VM state`() =
|
||||||
|
runTest {
|
||||||
|
val initialState = initialUsernameState.copy(
|
||||||
|
selectedType = GeneratorState.MainType.Username(
|
||||||
|
selectedType = GeneratorState.MainType.Username.UsernameType.PlusAddressedEmail(
|
||||||
|
email = "currentEmail",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val viewModel = createViewModel(initialState)
|
||||||
|
// the state is updated via the call to `loadOptions()` in the init block
|
||||||
|
viewModel.stateFlow.test {
|
||||||
|
assertEquals(
|
||||||
|
initialState.copy(generatedText = "email+abcd1234@address.com"),
|
||||||
|
awaitItem(),
|
||||||
|
)
|
||||||
|
// Setting the repository options to RANDOM_WORD to show this does NOT get used.
|
||||||
|
fakeGeneratorRepository.saveUsernameGenerationOptions(
|
||||||
|
UsernameGenerationOptions(
|
||||||
|
type = UsernameGenerationOptions.UsernameType.RANDOM_WORD,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// When this action is handled there will be another call to `loadOptions()`
|
||||||
|
// since we are using the default arguments with `shouldUseStorageOptions` set to
|
||||||
|
// false we should not expect a state update.
|
||||||
|
viewModel.trySendAction(
|
||||||
|
GeneratorAction.Internal.PasswordGeneratorPolicyReceive(policies = emptyList()),
|
||||||
|
)
|
||||||
|
expectNoEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
inner class PasswordActions {
|
inner class PasswordActions {
|
||||||
private val defaultPasswordState = createPasswordState()
|
private val defaultPasswordState = createPasswordState()
|
||||||
|
|
Loading…
Reference in a new issue