1
0
Fork 0
mirror of https://github.com/bitwarden/android.git synced 2024-12-18 23:31:52 +03:00

[PM-11270] hide new UI in complete registration screen behind flag pt. 2 ()

This commit is contained in:
Dave Severns 2024-08-23 12:52:21 -04:00 committed by GitHub
parent b7330392cc
commit 9db09c18cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 394 additions and 103 deletions
app/src
main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration
test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration

View file

@ -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",
)
}
}

View file

@ -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()
}
}

View file

@ -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,
)
}

View file

@ -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,
)
}