diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt index 09660d728..b384347ab 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt @@ -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,17 +203,29 @@ private fun CompleteRegistrationContent( .fillMaxWidth(), ) { Spacer(modifier = Modifier.height(8.dp)) - CompleteRegistrationContentHeader( - modifier = Modifier.align(Alignment.CenterHorizontally), - ) - Spacer(modifier = Modifier.height(24.dp)) - BitwardenActionCard( - actionIcon = rememberVectorPainter(id = R.drawable.ic_tooltip), - actionText = stringResource(id = R.string.what_makes_a_password_strong), - callToActionText = stringResource(id = R.string.learn_more), - onCardClicked = handler.onMakeStrongPassword, - modifier = Modifier.fillMaxWidth(), - ) + if (showNewOnboardingUi) { + CompleteRegistrationContentHeader( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .standardHorizontalMargin(), + ) + Spacer(modifier = Modifier.height(24.dp)) + BitwardenActionCard( + actionIcon = rememberVectorPainter(id = R.drawable.ic_tooltip), + actionText = stringResource(id = R.string.what_makes_a_password_strong), + callToActionText = stringResource(id = R.string.learn_more), + onCardClicked = handler.onMakeStrongPassword, + 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,32 +267,48 @@ private fun CompleteRegistrationContent( label = stringResource(id = R.string.master_password_hint), value = passwordHintInput, onValueChange = handler.onPasswordHintChange, - hint = stringResource( - R.string.bitwarden_cannot_recover_a_lost_or_forgotten_master_password, - ), + 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(), - ) - BitwardenClickableText( - label = stringResource(id = R.string.learn_about_other_ways_to_prevent_account_lockout), - onClick = handler.onLearnToPreventLockout, - style = nonMaterialTypography.labelMediumProminent, + .fillMaxWidth() + .standardHorizontalMargin(), ) + if (showNewOnboardingUi) { + BitwardenClickableText( + 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"), - ) - Spacer(modifier = Modifier.height(24.dp)) - BitwardenFilledButton( - label = callToActionText, - isEnabled = nextButtonEnabled, - onClick = handler.onCallToAction, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .testTag("CheckExposedMasterPasswordToggle") + .standardHorizontalMargin(), ) + if (showNewOnboardingUi) { + Spacer(modifier = Modifier.height(24.dp)) + BitwardenFilledButton( + label = callToActionText, + isEnabled = nextButtonEnabled, + onClick = handler.onCallToAction, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } } } @@ -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", ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt index f1969a23e..03bd7acd5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt @@ -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() } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreenTest.kt index dc0cb3d94..301f533a2 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreenTest.kt @@ -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, ) } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt index a40a7d6c5..e994c9553 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt @@ -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(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, ) }