mirror of
https://github.com/bitwarden/android.git
synced 2024-12-18 07:11:51 +03:00
[PM-11270] hide new UI in complete registration screen behind flag pt. 2 (#3812)
This commit is contained in:
parent
b7330392cc
commit
9db09c18cc
4 changed files with 394 additions and 103 deletions
|
@ -46,6 +46,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.asText
|
|||
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCard
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
|
@ -135,11 +136,24 @@ fun CompleteRegistrationScreen(
|
|||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.create_account),
|
||||
title = if (state.onboardingEnabled) {
|
||||
stringResource(id = R.string.create_account)
|
||||
} else {
|
||||
stringResource(id = R.string.set_password)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = rememberVectorPainter(id = R.drawable.ic_back),
|
||||
navigationIconContentDescription = stringResource(id = R.string.back),
|
||||
onNavigationIconClick = handler.onBackClick,
|
||||
actions = {
|
||||
if (!state.onboardingEnabled) {
|
||||
BitwardenTextButton(
|
||||
label = state.callToActionText(),
|
||||
onClick = handler.onCallToAction,
|
||||
modifier = Modifier.testTag("CreateAccountButton"),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
|
@ -157,10 +171,11 @@ fun CompleteRegistrationScreen(
|
|||
passwordHintInput = state.passwordHintInput,
|
||||
isCheckDataBreachesToggled = state.isCheckDataBreachesToggled,
|
||||
handler = handler,
|
||||
modifier = Modifier.standardHorizontalMargin(),
|
||||
nextButtonEnabled = state.hasValidMasterPassword,
|
||||
nextButtonEnabled = state.validSubmissionReady,
|
||||
callToActionText = state.callToActionText(),
|
||||
minimumPasswordLength = state.minimumPasswordLength,
|
||||
showNewOnboardingUi = state.onboardingEnabled,
|
||||
userEmail = state.userEmail,
|
||||
)
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
|
@ -170,6 +185,7 @@ fun CompleteRegistrationScreen(
|
|||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun CompleteRegistrationContent(
|
||||
userEmail: String,
|
||||
passwordInput: String,
|
||||
passwordStrengthState: PasswordStrengthState,
|
||||
confirmPasswordInput: String,
|
||||
|
@ -179,6 +195,7 @@ private fun CompleteRegistrationContent(
|
|||
minimumPasswordLength: Int,
|
||||
callToActionText: String,
|
||||
handler: CompleteRegistrationHandler,
|
||||
showNewOnboardingUi: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
|
@ -186,8 +203,11 @@ private fun CompleteRegistrationContent(
|
|||
.fillMaxWidth(),
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
if (showNewOnboardingUi) {
|
||||
CompleteRegistrationContentHeader(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
BitwardenActionCard(
|
||||
|
@ -195,8 +215,17 @@ private fun CompleteRegistrationContent(
|
|||
actionText = stringResource(id = R.string.what_makes_a_password_strong),
|
||||
callToActionText = stringResource(id = R.string.learn_more),
|
||||
onCardClicked = handler.onMakeStrongPassword,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
} else {
|
||||
LegacyHeaderContent(
|
||||
userEmail = userEmail,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
var showPassword by rememberSaveable { mutableStateOf(false) }
|
||||
|
@ -208,7 +237,8 @@ private fun CompleteRegistrationContent(
|
|||
onValueChange = handler.onPasswordInputChange,
|
||||
modifier = Modifier
|
||||
.testTag("MasterPasswordEntry")
|
||||
.fillMaxWidth(),
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
showPasswordTestTag = "PasswordVisibilityToggle",
|
||||
imeAction = ImeAction.Next,
|
||||
)
|
||||
|
@ -216,7 +246,8 @@ private fun CompleteRegistrationContent(
|
|||
PasswordStrengthIndicator(
|
||||
state = passwordStrengthState,
|
||||
currentCharacterCount = passwordInput.length,
|
||||
minimumCharacterCount = minimumPasswordLength,
|
||||
minimumCharacterCount = minimumPasswordLength.takeIf { showNewOnboardingUi },
|
||||
modifier = Modifier.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenPasswordField(
|
||||
|
@ -227,7 +258,8 @@ private fun CompleteRegistrationContent(
|
|||
onValueChange = handler.onConfirmPasswordInputChange,
|
||||
modifier = Modifier
|
||||
.testTag("ConfirmMasterPasswordEntry")
|
||||
.fillMaxWidth(),
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
showPasswordTestTag = "ConfirmPasswordVisibilityToggle",
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
@ -235,34 +267,50 @@ private fun CompleteRegistrationContent(
|
|||
label = stringResource(id = R.string.master_password_hint),
|
||||
value = passwordHintInput,
|
||||
onValueChange = handler.onPasswordHintChange,
|
||||
hint = stringResource(
|
||||
hint = if (showNewOnboardingUi) {
|
||||
stringResource(
|
||||
R.string.bitwarden_cannot_recover_a_lost_or_forgotten_master_password,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
stringResource(id = R.string.master_password_description)
|
||||
},
|
||||
modifier = Modifier
|
||||
.testTag("MasterPasswordHintLabel")
|
||||
.fillMaxWidth(),
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
if (showNewOnboardingUi) {
|
||||
BitwardenClickableText(
|
||||
label = stringResource(id = R.string.learn_about_other_ways_to_prevent_account_lockout),
|
||||
label = stringResource(
|
||||
id = R.string.learn_about_other_ways_to_prevent_account_lockout,
|
||||
),
|
||||
onClick = handler.onLearnToPreventLockout,
|
||||
style = nonMaterialTypography.labelMediumProminent,
|
||||
modifier = Modifier.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
BitwardenSwitch(
|
||||
label = stringResource(id = R.string.check_known_data_breaches_for_this_password),
|
||||
isChecked = isCheckDataBreachesToggled,
|
||||
onCheckedChange = handler.onCheckDataBreachesToggle,
|
||||
modifier = Modifier.testTag("CheckExposedMasterPasswordToggle"),
|
||||
modifier = Modifier
|
||||
.testTag("CheckExposedMasterPasswordToggle")
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
if (showNewOnboardingUi) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
BitwardenFilledButton(
|
||||
label = callToActionText,
|
||||
isEnabled = nextButtonEnabled,
|
||||
onClick = handler.onCallToAction,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CompleteRegistrationContentHeader(
|
||||
|
@ -286,6 +334,24 @@ private fun CompleteRegistrationContentHeader(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LegacyHeaderContent(
|
||||
userEmail: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@Suppress("MaxLineLength")
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.follow_the_instructions_in_the_email_sent_to_x_to_continue_creating_your_account,
|
||||
userEmail,
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = modifier
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Header content ordered with the image "first" and the text "second" which can be placed in a
|
||||
* [Column] or [Row].
|
||||
|
@ -321,7 +387,7 @@ private fun OrderedHeaderContent() {
|
|||
|
||||
@PreviewScreenSizes
|
||||
@Composable
|
||||
private fun CompleteRegistrationContent_preview() {
|
||||
private fun CompleteRegistrationContentOldUI_preview() {
|
||||
BitwardenTheme {
|
||||
CompleteRegistrationContent(
|
||||
passwordInput = "tortor",
|
||||
|
@ -345,6 +411,40 @@ private fun CompleteRegistrationContent_preview() {
|
|||
nextButtonEnabled = true,
|
||||
modifier = Modifier.standardHorizontalMargin(),
|
||||
minimumPasswordLength = 12,
|
||||
showNewOnboardingUi = false,
|
||||
userEmail = "fake@email.com",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewScreenSizes
|
||||
@Composable
|
||||
private fun CompleteRegistrationContentNewUI_preview() {
|
||||
BitwardenTheme {
|
||||
CompleteRegistrationContent(
|
||||
passwordInput = "tortor",
|
||||
passwordStrengthState = PasswordStrengthState.WEAK_3,
|
||||
confirmPasswordInput = "consequat",
|
||||
passwordHintInput = "dissentiunt",
|
||||
isCheckDataBreachesToggled = false,
|
||||
handler = CompleteRegistrationHandler(
|
||||
onDismissErrorDialog = {},
|
||||
onContinueWithBreachedPasswordClick = {},
|
||||
onBackClick = {},
|
||||
onPasswordInputChange = {},
|
||||
onConfirmPasswordInputChange = {},
|
||||
onPasswordHintChange = {},
|
||||
onCheckDataBreachesToggle = {},
|
||||
onLearnToPreventLockout = {},
|
||||
onMakeStrongPassword = {},
|
||||
onCallToAction = {},
|
||||
),
|
||||
callToActionText = "Next",
|
||||
nextButtonEnabled = true,
|
||||
modifier = Modifier.standardHorizontalMargin(),
|
||||
minimumPasswordLength = 12,
|
||||
showNewOnboardingUi = true,
|
||||
userEmail = "fake@email.com",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -64,7 +65,7 @@ class CompleteRegistrationViewModel @Inject constructor(
|
|||
isCheckDataBreachesToggled = true,
|
||||
dialog = null,
|
||||
passwordStrengthState = PasswordStrengthState.NONE,
|
||||
onBoardingEnabled = featureFlagManager.getFeatureFlag(FlagKey.OnboardingFlow),
|
||||
onboardingEnabled = featureFlagManager.getFeatureFlag(FlagKey.OnboardingFlow),
|
||||
minimumPasswordLength = MIN_PASSWORD_LENGTH,
|
||||
)
|
||||
},
|
||||
|
@ -82,6 +83,14 @@ class CompleteRegistrationViewModel @Inject constructor(
|
|||
stateFlow
|
||||
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
featureFlagManager
|
||||
.getFeatureFlagFlow(FlagKey.OnboardingFlow)
|
||||
.map {
|
||||
Internal.UpdateOnboardingFeatureState(newValue = it)
|
||||
}
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
@ -114,6 +123,7 @@ class CompleteRegistrationViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
CompleteRegistrationAction.CallToActionClick -> handleCallToActionClick()
|
||||
is Internal.UpdateOnboardingFeatureState -> handleUpdateOnboardingFeatureState(action)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -131,6 +141,12 @@ class CompleteRegistrationViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleUpdateOnboardingFeatureState(action: Internal.UpdateOnboardingFeatureState) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(onboardingEnabled = action.newValue)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePasswordStrengthResult(action: ReceivePasswordStrengthResult) {
|
||||
when (val result = action.result) {
|
||||
is PasswordStrengthResult.Success -> {
|
||||
|
@ -267,14 +283,41 @@ class CompleteRegistrationViewModel @Inject constructor(
|
|||
mutableStateFlow.update { it.copy(confirmPasswordInput = action.input) }
|
||||
}
|
||||
|
||||
private fun handleCallToActionClick() {
|
||||
if (!state.userEmail.isValidEmail()) {
|
||||
private fun handleCallToActionClick() = when {
|
||||
state.userEmail.isBlank() -> {
|
||||
val dialog = BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.validation_field_required
|
||||
.asText(R.string.email_address.asText()),
|
||||
)
|
||||
mutableStateFlow.update { it.copy(dialog = CompleteRegistrationDialog.Error(dialog)) }
|
||||
}
|
||||
|
||||
!state.userEmail.isValidEmail() -> {
|
||||
val dialog = BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.invalid_email.asText(),
|
||||
)
|
||||
mutableStateFlow.update { it.copy(dialog = CompleteRegistrationDialog.Error(dialog)) }
|
||||
} else {
|
||||
}
|
||||
|
||||
state.passwordInput.length < MIN_PASSWORD_LENGTH -> {
|
||||
val dialog = BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.master_password_length_val_message_x.asText(MIN_PASSWORD_LENGTH),
|
||||
)
|
||||
mutableStateFlow.update { it.copy(dialog = CompleteRegistrationDialog.Error(dialog)) }
|
||||
}
|
||||
|
||||
state.passwordInput != state.confirmPasswordInput -> {
|
||||
val dialog = BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.master_password_confirmation_val_message.asText(),
|
||||
)
|
||||
mutableStateFlow.update { it.copy(dialog = CompleteRegistrationDialog.Error(dialog)) }
|
||||
}
|
||||
|
||||
else -> {
|
||||
submitRegisterAccountRequest(
|
||||
shouldCheckForDataBreaches = state.isCheckDataBreachesToggled,
|
||||
shouldIgnorePasswordStrength = false,
|
||||
|
@ -340,7 +383,7 @@ data class CompleteRegistrationState(
|
|||
val isCheckDataBreachesToggled: Boolean,
|
||||
val dialog: CompleteRegistrationDialog?,
|
||||
val passwordStrengthState: PasswordStrengthState,
|
||||
val onBoardingEnabled: Boolean,
|
||||
val onboardingEnabled: Boolean,
|
||||
val minimumPasswordLength: Int,
|
||||
) : Parcelable {
|
||||
|
||||
|
@ -348,7 +391,7 @@ data class CompleteRegistrationState(
|
|||
* The text to display on the call to action button.
|
||||
*/
|
||||
val callToActionText: Text
|
||||
get() = if (onBoardingEnabled) {
|
||||
get() = if (onboardingEnabled) {
|
||||
R.string.next.asText()
|
||||
} else {
|
||||
R.string.create_account.asText()
|
||||
|
@ -373,10 +416,10 @@ data class CompleteRegistrationState(
|
|||
/**
|
||||
* Whether the form is valid.
|
||||
*/
|
||||
val hasValidMasterPassword: Boolean
|
||||
get() = passwordInput == confirmPasswordInput &&
|
||||
passwordInput.isNotBlank() &&
|
||||
passwordInput.length >= MIN_PASSWORD_LENGTH
|
||||
val validSubmissionReady: Boolean
|
||||
get() = passwordInput.isNotBlank() &&
|
||||
confirmPasswordInput.isNotBlank() &&
|
||||
passwordInput.length >= minimumPasswordLength
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -516,5 +559,10 @@ sealed class CompleteRegistrationAction {
|
|||
data class ReceivePasswordStrengthResult(
|
||||
val result: PasswordStrengthResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicate on boarding feature state has been updated.
|
||||
*/
|
||||
data class UpdateOnboardingFeatureState(val newValue: Boolean) : Internal()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,6 +73,17 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `determine if using the old ui by title text`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Set password")
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onNode(hasText("Create account") and !hasClickAction())
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `call to action with valid input click should send CreateAccountClick action`() {
|
||||
mutableStateFlow.update {
|
||||
|
@ -83,30 +94,11 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
|
|||
}
|
||||
composeTestRule
|
||||
.onNode(hasText("Create account") and hasClickAction())
|
||||
.performScrollTo()
|
||||
.assertIsEnabled()
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `call to action not enabled if passwords don't match`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
passwordInput = "4321drowssap",
|
||||
confirmPasswordInput = "password1234",
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNode(hasText("Create account") and hasClickAction())
|
||||
.performScrollTo()
|
||||
.assertIsNotEnabled()
|
||||
.performClick()
|
||||
verify(exactly = 0) {
|
||||
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `close click should send CloseClick action`() {
|
||||
composeTestRule.onNodeWithContentDescription("Back").performClick()
|
||||
|
@ -261,28 +253,6 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
|
|||
.assertCountEquals(2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Click on action card should send MakePasswordStrongClick action`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Learn more")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify { viewModel.trySendAction(CompleteRegistrationAction.MakePasswordStrongClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Click on prevent account lockout should send LearnToPreventLockoutClick action`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Learn about other ways to prevent account lockout")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(CompleteRegistrationAction.LearnToPreventLockoutClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `NavigateToPreventAccountLockout event should invoke navigate to prevent account lockout lambda`() {
|
||||
|
@ -297,7 +267,90 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `Header should be displayed in portrait mode`() {
|
||||
fun `NavigateToLogin event should invoke navigate to login lambda`() {
|
||||
mutableEventFlow.tryEmit(
|
||||
CompleteRegistrationEvent.NavigateToLogin(
|
||||
email = EMAIL,
|
||||
captchaToken = TOKEN,
|
||||
),
|
||||
)
|
||||
|
||||
assertTrue(onNavigateToLoginCalled)
|
||||
}
|
||||
|
||||
// New Onboarding UI tests
|
||||
@Test
|
||||
fun `determine if using the new ui by title text`() = testWithFeatureFlagOn {
|
||||
composeTestRule
|
||||
.onNode(hasText("Create account") and !hasClickAction())
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Set password")
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `call to action state should update with input based on if both fields are populated`() =
|
||||
testWithFeatureFlagOn {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
passwordInput = "",
|
||||
confirmPasswordInput = "password1234",
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText("Next")
|
||||
.assertIsNotEnabled()
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
verify(exactly = 0) {
|
||||
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
|
||||
}
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
passwordInput = "password1234",
|
||||
confirmPasswordInput = "password1234",
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Next")
|
||||
.assertIsEnabled()
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Click on action card should send MakePasswordStrongClick action`() =
|
||||
testWithFeatureFlagOn {
|
||||
composeTestRule
|
||||
.onNodeWithText("Learn more")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify { viewModel.trySendAction(CompleteRegistrationAction.MakePasswordStrongClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Click on prevent account lockout should send LearnToPreventLockoutClick action`() =
|
||||
testWithFeatureFlagOn {
|
||||
composeTestRule
|
||||
.onNodeWithText("Learn about other ways to prevent account lockout")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(CompleteRegistrationAction.LearnToPreventLockoutClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Header should be displayed in portrait mode`() = testWithFeatureFlagOn {
|
||||
composeTestRule
|
||||
.onNodeWithText("Choose your master password")
|
||||
.performScrollTo()
|
||||
|
@ -311,7 +364,7 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
|
|||
|
||||
@Config(qualifiers = "land")
|
||||
@Test
|
||||
fun `Header should be displayed in landscape mode`() {
|
||||
fun `Header should be displayed in landscape mode`() = testWithFeatureFlagOn {
|
||||
composeTestRule
|
||||
.onNodeWithText("Choose your master password")
|
||||
.performScrollTo()
|
||||
|
@ -323,16 +376,22 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
|
|||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToLogin event should invoke navigate to login lambda`() {
|
||||
mutableEventFlow.tryEmit(
|
||||
CompleteRegistrationEvent.NavigateToLogin(
|
||||
email = EMAIL,
|
||||
captchaToken = TOKEN,
|
||||
),
|
||||
)
|
||||
private fun testWithFeatureFlagOn(test: () -> Unit) {
|
||||
turnFeatureFlagOn()
|
||||
test()
|
||||
turnFeatureFlagOff()
|
||||
}
|
||||
|
||||
assertTrue(onNavigateToLoginCalled)
|
||||
private fun turnFeatureFlagOn() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(onboardingEnabled = true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun turnFeatureFlagOff() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(onboardingEnabled = false)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -349,7 +408,7 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
|
|||
isCheckDataBreachesToggled = true,
|
||||
dialog = null,
|
||||
passwordStrengthState = PasswordStrengthState.NONE,
|
||||
onBoardingEnabled = false,
|
||||
onboardingEnabled = false,
|
||||
minimumPasswordLength = 12,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ import io.mockk.every
|
|||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
|
@ -55,9 +56,10 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager =
|
||||
SpecialCircumstanceManagerImpl()
|
||||
|
||||
private val mutableFeatureFlagFlow = MutableStateFlow(false)
|
||||
private val featureFlagManager = mockk<FeatureFlagManager>(relaxed = true) {
|
||||
every { getFeatureFlag(FlagKey.OnboardingFlow) } returns false
|
||||
every { getFeatureFlagFlow(FlagKey.OnboardingFlow) } returns mutableFeatureFlagFlow
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
|
@ -108,7 +110,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
val viewModel = createCompleteRegistrationViewModel()
|
||||
viewModel.trySendAction(PasswordInputChange(input))
|
||||
|
||||
assertFalse(viewModel.stateFlow.value.hasValidMasterPassword)
|
||||
assertFalse(viewModel.stateFlow.value.validSubmissionReady)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -120,7 +122,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
val viewModel = createCompleteRegistrationViewModel()
|
||||
viewModel.trySendAction(PasswordInputChange(PASSWORD))
|
||||
|
||||
assertFalse(viewModel.stateFlow.value.hasValidMasterPassword)
|
||||
assertFalse(viewModel.stateFlow.value.validSubmissionReady)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -507,6 +509,88 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `feature flag state update is captured in ViewModel state`() {
|
||||
mutableFeatureFlagFlow.value = true
|
||||
val viewModel = createCompleteRegistrationViewModel()
|
||||
assertTrue(viewModel.stateFlow.value.onboardingEnabled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CreateAccountClick with password below 12 chars should show password length dialog`() =
|
||||
runTest {
|
||||
val input = "abcdefghikl"
|
||||
coEvery {
|
||||
mockAuthRepository.getPasswordStrength(EMAIL, input)
|
||||
} returns PasswordStrengthResult.Error
|
||||
val viewModel = createCompleteRegistrationViewModel()
|
||||
viewModel.trySendAction(PasswordInputChange(input))
|
||||
val expectedState = DEFAULT_STATE.copy(
|
||||
passwordInput = input,
|
||||
dialog = CompleteRegistrationDialog.Error(
|
||||
BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.master_password_length_val_message_x.asText(12),
|
||||
),
|
||||
),
|
||||
)
|
||||
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(expectedState, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CreateAccountClick with passwords not matching should show password match dialog`() =
|
||||
runTest {
|
||||
coEvery {
|
||||
mockAuthRepository.getPasswordStrength(EMAIL, PASSWORD)
|
||||
} returns PasswordStrengthResult.Error
|
||||
val viewModel = createCompleteRegistrationViewModel()
|
||||
viewModel.trySendAction(PasswordInputChange(PASSWORD))
|
||||
val expectedState = DEFAULT_STATE.copy(
|
||||
userEmail = EMAIL,
|
||||
passwordInput = PASSWORD,
|
||||
dialog = CompleteRegistrationDialog.Error(
|
||||
BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.master_password_confirmation_val_message.asText(),
|
||||
),
|
||||
),
|
||||
)
|
||||
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(expectedState, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CreateAccountClick with no email not should show dialog`() =
|
||||
runTest {
|
||||
coEvery {
|
||||
mockAuthRepository.getPasswordStrength("", PASSWORD)
|
||||
} returns PasswordStrengthResult.Error
|
||||
val viewModel = createCompleteRegistrationViewModel(
|
||||
DEFAULT_STATE.copy(userEmail = ""),
|
||||
)
|
||||
viewModel.trySendAction(PasswordInputChange(PASSWORD))
|
||||
val expectedState = DEFAULT_STATE.copy(
|
||||
userEmail = "",
|
||||
passwordInput = PASSWORD,
|
||||
dialog = CompleteRegistrationDialog.Error(
|
||||
BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.validation_field_required
|
||||
.asText(R.string.email_address.asText()),
|
||||
),
|
||||
),
|
||||
)
|
||||
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(expectedState, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCompleteRegistrationViewModel(
|
||||
completeRegistrationState: CompleteRegistrationState? = DEFAULT_STATE,
|
||||
authRepository: AuthRepository = mockAuthRepository,
|
||||
|
@ -538,7 +622,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
isCheckDataBreachesToggled = true,
|
||||
dialog = null,
|
||||
passwordStrengthState = PasswordStrengthState.NONE,
|
||||
onBoardingEnabled = false,
|
||||
onboardingEnabled = false,
|
||||
minimumPasswordLength = 12,
|
||||
)
|
||||
private val VALID_INPUT_STATE = CompleteRegistrationState(
|
||||
|
@ -551,7 +635,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
|||
isCheckDataBreachesToggled = false,
|
||||
dialog = null,
|
||||
passwordStrengthState = PasswordStrengthState.GOOD,
|
||||
onBoardingEnabled = false,
|
||||
onboardingEnabled = false,
|
||||
minimumPasswordLength = 12,
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue