BIT-188 Add switches and links for check password and terms and privacy (#106)

This commit is contained in:
Andrew Haisting 2023-10-12 11:39:09 -05:00 committed by Álison Fernandes
parent bb9c260160
commit 2cda9db9a2
8 changed files with 435 additions and 47 deletions

View file

@ -2,33 +2,63 @@ package com.x8bit.bitwarden.ui.auth.feature.createaccount
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.AcceptPoliciesToggle
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CheckDataBreachesToggle
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CloseClick
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ConfirmPasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.EmailInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ErrorDialogDismiss
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordHintChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PrivacyPolicyClick
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.SubmitClick
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.TermsClick
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountEvent.NavigateToPrivacyPolicy
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountEvent.NavigateToTerms
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButtonTopAppBar
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.theme.clickableSpanStyle
/**
* Top level composable for the create account screen.
@ -37,12 +67,21 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
@Composable
fun CreateAccountScreen(
onNavigateBack: () -> Unit,
intentHandler: IntentHandler = IntentHandler(context = LocalContext.current),
viewModel: CreateAccountViewModel = hiltViewModel(),
) {
val state by viewModel.stateFlow.collectAsState()
val context = LocalContext.current
EventsEffect(viewModel) { event ->
when (event) {
is NavigateToPrivacyPolicy -> {
intentHandler.launchUri("https://bitwarden.com/privacy/".toUri())
}
is NavigateToTerms -> {
intentHandler.launchUri("https://bitwarden.com/terms/".toUri())
}
is CreateAccountEvent.NavigateBack -> onNavigateBack.invoke()
is CreateAccountEvent.ShowToast -> {
Toast.makeText(context, event.text, Toast.LENGTH_SHORT).show()
@ -57,54 +96,183 @@ fun CreateAccountScreen(
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface),
.background(MaterialTheme.colorScheme.surface)
.verticalScroll(rememberScrollState()),
) {
BitwardenTextButtonTopAppBar(
title = stringResource(id = R.string.create_account),
navigationIcon = painterResource(id = R.drawable.ic_close),
navigationIconContentDescription = stringResource(id = R.string.close),
onNavigationIconClick = remember(viewModel) {
{ viewModel.trySendAction(CreateAccountAction.CloseClick) }
{ viewModel.trySendAction(CloseClick) }
},
buttonText = stringResource(id = R.string.submit),
onButtonClick = remember(viewModel) {
{ viewModel.trySendAction(CreateAccountAction.SubmitClick) }
{ viewModel.trySendAction(SubmitClick) }
},
isButtonEnabled = true,
)
Column(
modifier = Modifier.padding(horizontal = 16.dp),
verticalArrangement = spacedBy(16.dp),
) {
BitwardenTextField(
label = stringResource(id = R.string.email_address),
value = state.emailInput,
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(EmailInputChange(it)) }
},
modifier = Modifier.fillMaxWidth(),
Spacer(modifier = Modifier.height(16.dp))
BitwardenTextField(
label = stringResource(id = R.string.email_address),
value = state.emailInput,
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(EmailInputChange(it)) }
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.master_password),
value = state.passwordInput,
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(PasswordInputChange(it)) }
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.retype_master_password),
value = state.confirmPasswordInput,
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(ConfirmPasswordInputChange(it)) }
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenTextField(
label = stringResource(id = R.string.master_password_hint),
value = state.passwordHintInput,
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(PasswordHintChange(it)) }
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(id = R.string.master_password_hint_description),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier
.padding(horizontal = 32.dp),
)
Spacer(modifier = Modifier.height(24.dp))
BitwardenSwitch(
label = stringResource(id = R.string.check_known_data_breaches_for_this_password),
isChecked = state.isCheckDataBreachesToggled,
onCheckedChange = remember(viewModel) {
{ newState ->
viewModel.trySendAction(CheckDataBreachesToggle(newState = newState))
}
},
modifier = Modifier
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
TermsAndPrivacySwitch(
isChecked = state.isAcceptPoliciesToggled,
onCheckedChange = remember(viewModel) {
{ viewModel.trySendAction(AcceptPoliciesToggle(it)) }
},
onTermsClick = remember(viewModel) {
{ viewModel.trySendAction(TermsClick) }
},
onPrivacyPolicyClick = remember(viewModel) {
{ viewModel.trySendAction(PrivacyPolicyClick) }
},
)
}
}
@Suppress("LongMethod")
@Composable
private fun TermsAndPrivacySwitch(
isChecked: Boolean,
onCheckedChange: (Boolean) -> Unit,
onTermsClick: () -> Unit,
onPrivacyPolicyClick: () -> Unit,
) {
val clickableStyle = clickableSpanStyle()
val termsString = stringResource(id = R.string.terms_of_service)
val privacyString = stringResource(id = R.string.privacy_policy)
val annotatedString = remember {
buildAnnotatedString {
withStyle(style = clickableStyle) {
pushStringAnnotation(tag = termsString, annotation = termsString)
append("$termsString,")
}
append(" ")
withStyle(style = clickableStyle) {
pushStringAnnotation(tag = privacyString, annotation = privacyString)
append(privacyString)
}
}
}
Row(
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.semantics(mergeDescendants = true) {
customActions = listOf(
CustomAccessibilityAction(
label = termsString,
action = {
onTermsClick.invoke()
true
},
),
CustomAccessibilityAction(
label = privacyString,
action = {
onPrivacyPolicyClick.invoke()
true
},
),
)
}
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colorScheme.primary),
onClick = { onCheckedChange.invoke(!isChecked) },
)
BitwardenPasswordField(
label = stringResource(id = R.string.master_password),
value = state.passwordInput,
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(PasswordInputChange(it)) }
},
modifier = Modifier.fillMaxWidth(),
.padding(start = 16.dp)
.fillMaxWidth(),
) {
Switch(
modifier = Modifier
.height(32.dp)
.width(52.dp),
checked = isChecked,
onCheckedChange = null,
)
Column(Modifier.padding(start = 16.dp, top = 4.dp, bottom = 4.dp)) {
Text(
text = stringResource(id = R.string.accept_policies),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
BitwardenPasswordField(
label = stringResource(id = R.string.retype_master_password),
value = state.confirmPasswordInput,
onValueChange = remember {
{ viewModel.trySendAction(ConfirmPasswordInputChange(it)) }
ClickableText(
text = annotatedString,
onClick = { offset ->
annotatedString
.getStringAnnotations(offset, offset)
.firstOrNull()
?.let { span ->
when (span.tag) {
termsString -> onTermsClick.invoke()
privacyString -> onPrivacyPolicyClick.invoke()
else -> onCheckedChange.invoke(!isChecked)
}
} ?: onCheckedChange.invoke(!isChecked)
},
modifier = Modifier.fillMaxWidth(),
)
BitwardenTextField(
label = stringResource(id = R.string.master_password_hint),
value = state.passwordHintInput,
onValueChange = remember { { viewModel.trySendAction(PasswordHintChange(it)) } },
modifier = Modifier.fillMaxWidth(),
)
}
}

View file

@ -4,11 +4,15 @@ import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.AcceptPoliciesToggle
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CheckDataBreachesToggle
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ConfirmPasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.EmailInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordHintChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PrivacyPolicyClick
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.SubmitClick
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.TermsClick
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
@ -25,6 +29,7 @@ private const val MIN_PASSWORD_LENGTH = 12
/**
* Models logic for the create account screen.
*/
@Suppress("TooManyFunctions")
@HiltViewModel
class CreateAccountViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
@ -35,6 +40,8 @@ class CreateAccountViewModel @Inject constructor(
passwordInput = "",
confirmPasswordInput = "",
passwordHintInput = "",
isAcceptPoliciesToggled = false,
isCheckDataBreachesToggled = false,
errorDialogState = BasicDialogState.Hidden,
),
) {
@ -55,6 +62,26 @@ class CreateAccountViewModel @Inject constructor(
is PasswordInputChange -> handlePasswordInputChanged(action)
is CreateAccountAction.CloseClick -> handleCloseClick()
is CreateAccountAction.ErrorDialogDismiss -> handleDialogDismiss()
is AcceptPoliciesToggle -> handleAcceptPoliciesToggle(action)
is CheckDataBreachesToggle -> handleCheckDataBreachesToggle(action)
is PrivacyPolicyClick -> handlePrivacyPolicyClick()
is TermsClick -> handleTermsClick()
}
}
private fun handlePrivacyPolicyClick() = sendEvent(CreateAccountEvent.NavigateToPrivacyPolicy)
private fun handleTermsClick() = sendEvent(CreateAccountEvent.NavigateToTerms)
private fun handleAcceptPoliciesToggle(action: AcceptPoliciesToggle) {
mutableStateFlow.update {
it.copy(isAcceptPoliciesToggled = action.newState)
}
}
private fun handleCheckDataBreachesToggle(action: CheckDataBreachesToggle) {
mutableStateFlow.update {
it.copy(isCheckDataBreachesToggled = action.newState)
}
}
@ -108,6 +135,8 @@ data class CreateAccountState(
val passwordInput: String,
val confirmPasswordInput: String,
val passwordHintInput: String,
val isCheckDataBreachesToggled: Boolean,
val isAcceptPoliciesToggled: Boolean,
val errorDialogState: BasicDialogState,
) : Parcelable
@ -125,6 +154,16 @@ sealed class CreateAccountEvent {
* Placeholder event for showing a toast. Can be removed once there are real events.
*/
data class ShowToast(val text: String) : CreateAccountEvent()
/**
* Navigate to terms and conditions.
*/
data object NavigateToTerms : CreateAccountEvent()
/**
* Navigate to privacy policy.
*/
data object NavigateToPrivacyPolicy : CreateAccountEvent()
}
/**
@ -165,4 +204,24 @@ sealed class CreateAccountAction {
* User dismissed the error dialog.
*/
data object ErrorDialogDismiss : CreateAccountAction()
/**
* User tapped check data breaches toggle.
*/
data class CheckDataBreachesToggle(val newState: Boolean) : CreateAccountAction()
/**
* User tapped accept policies toggle.
*/
data class AcceptPoliciesToggle(val newState: Boolean) : CreateAccountAction()
/**
* User tapped privacy policy link.
*/
data object PrivacyPolicyClick : CreateAccountAction()
/**
* User tapped terms link.
*/
data object TermsClick : CreateAccountAction()
}

View file

@ -26,4 +26,11 @@ class IntentHandler(private val context: Context) {
.build()
.launchUrl(context, uri)
}
/**
* Start an activity to view the given [uri] in an external browser.
*/
fun launchUri(uri: Uri) {
startActivity(Intent(Intent.ACTION_VIEW, uri))
}
}

View file

@ -1,15 +1,18 @@
package com.x8bit.bitwarden.ui.platform.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.semantics
@ -34,23 +37,29 @@ fun BitwardenSwitch(
Row(
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colorScheme.primary),
onClick = { onCheckedChange?.invoke(!isChecked) },
)
.semantics(mergeDescendants = true) { }
.wrapContentHeight(),
.then(modifier),
) {
Switch(
modifier = Modifier
.padding(vertical = 8.dp)
.height(32.dp)
.width(52.dp),
checked = isChecked,
onCheckedChange = onCheckedChange,
onCheckedChange = null,
)
Text(
text = label,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(start = 16.dp),
modifier = Modifier.padding(start = 16.dp, top = 4.dp, bottom = 4.dp),
)
}
}

View file

@ -0,0 +1,20 @@
package com.x8bit.bitwarden.ui.platform.theme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.text.SpanStyle
/**
* Defines a span style for clickable span texts. Useful because spans require a
* [SpanStyle] instead of the typical [TextStyle].
*/
@Composable
@ReadOnlyComposable
fun clickableSpanStyle(): SpanStyle = SpanStyle(
color = MaterialTheme.colorScheme.primary,
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
fontWeight = MaterialTheme.typography.bodyMedium.fontWeight,
fontStyle = MaterialTheme.typography.bodyMedium.fontStyle,
fontFamily = MaterialTheme.typography.bodyMedium.fontFamily,
)

View file

@ -1,5 +1,7 @@
package com.x8bit.bitwarden.ui.auth.feature.createaccount
import android.content.Intent
import android.net.Uri
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasAnyAncestor
@ -8,7 +10,11 @@ import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTextInput
import androidx.core.net.toUri
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.AcceptPoliciesToggle
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CheckDataBreachesToggle
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CloseClick
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ConfirmPasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.EmailInputChange
@ -16,6 +22,7 @@ import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.Pas
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.SubmitClick
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
import io.mockk.every
@ -56,6 +63,40 @@ class CreateAccountScreenTest : BaseComposeTest() {
verify { viewModel.trySendAction(CloseClick) }
}
@Test
fun `check data breaches click should send CheckDataBreachesToggle action`() {
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns emptyFlow()
every { trySendAction(CheckDataBreachesToggle(true)) } returns Unit
}
composeTestRule.setContent {
CreateAccountScreen(onNavigateBack = {}, viewModel = viewModel)
}
composeTestRule
.onNodeWithText("Check known data breaches for this password")
.performScrollTo()
.performClick()
verify { viewModel.trySendAction(CheckDataBreachesToggle(true)) }
}
@Test
fun `accept policies click should send AcceptPoliciesToggle action`() {
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns emptyFlow()
every { trySendAction(AcceptPoliciesToggle(true)) } returns Unit
}
composeTestRule.setContent {
CreateAccountScreen(onNavigateBack = {}, viewModel = viewModel)
}
composeTestRule
.onNodeWithText("By activating this switch you agree", substring = true)
.performScrollTo()
.performClick()
verify { viewModel.trySendAction(AcceptPoliciesToggle(true)) }
}
@Test
fun `NavigateBack event should invoke navigate back lambda`() {
var onNavigateBackCalled = false
@ -70,6 +111,52 @@ class CreateAccountScreenTest : BaseComposeTest() {
assert(onNavigateBackCalled)
}
@Test
fun `NavigateToPrivacyPolicy event should invoke intent handler`() {
val expectedIntent =
Intent(Intent.ACTION_VIEW, Uri.parse("https://bitwarden.com/privacy/"))
val intentHandler = mockk<IntentHandler>(relaxed = true) {
every { startActivity(expectedIntent) } returns Unit
}
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns flowOf(CreateAccountEvent.NavigateToPrivacyPolicy)
}
composeTestRule.setContent {
CreateAccountScreen(
onNavigateBack = {},
viewModel = viewModel,
intentHandler = intentHandler,
)
}
verify {
intentHandler.launchUri("https://bitwarden.com/privacy/".toUri())
}
}
@Test
fun `NavigateToTerms event should invoke intent handler`() {
val expectedIntent =
Intent(Intent.ACTION_VIEW, Uri.parse("https://bitwarden.com/terms/"))
val intentHandler = mockk<IntentHandler>(relaxed = true) {
every { startActivity(expectedIntent) } returns Unit
}
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns flowOf(CreateAccountEvent.NavigateToTerms)
}
composeTestRule.setContent {
CreateAccountScreen(
onNavigateBack = {},
viewModel = viewModel,
intentHandler = intentHandler,
)
}
verify {
intentHandler.launchUri("https://bitwarden.com/terms/".toUri())
}
}
@Test
fun `email input change should send EmailInputChange action`() {
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
@ -179,6 +266,8 @@ class CreateAccountScreenTest : BaseComposeTest() {
passwordInput = "",
confirmPasswordInput = "",
passwordHintInput = "",
isCheckDataBreachesToggled = false,
isAcceptPoliciesToggled = false,
errorDialogState = BasicDialogState.Hidden,
)
}

View file

@ -8,6 +8,7 @@ import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.Con
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.EmailInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordHintChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountEvent.ShowToast
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
@ -30,6 +31,8 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
passwordInput = "password",
confirmPasswordInput = "confirmPassword",
passwordHintInput = "hint",
isCheckDataBreachesToggled = false,
isAcceptPoliciesToggled = false,
errorDialogState = BasicDialogState.Hidden,
)
val handle = SavedStateHandle(mapOf("state" to savedState))
@ -61,7 +64,7 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
viewModel.trySendAction(PasswordInputChange("longenoughpassword"))
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(CreateAccountAction.SubmitClick)
assert(awaitItem() is CreateAccountEvent.ShowToast)
assertEquals(ShowToast("TODO: Handle Submit Click"), awaitItem())
}
}
@ -70,7 +73,25 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
val viewModel = CreateAccountViewModel(SavedStateHandle())
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(CloseClick)
assert(awaitItem() is CreateAccountEvent.NavigateBack)
assertEquals(CreateAccountEvent.NavigateBack, awaitItem())
}
}
@Test
fun `PrivacyPolicyClick should emit NavigatePrivacyPolicy`() = runTest {
val viewModel = CreateAccountViewModel(SavedStateHandle())
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(CreateAccountAction.PrivacyPolicyClick)
assertEquals(CreateAccountEvent.NavigateToPrivacyPolicy, awaitItem())
}
}
@Test
fun `TermsClick should emit NavigateToTerms`() = runTest {
val viewModel = CreateAccountViewModel(SavedStateHandle())
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(CreateAccountAction.TermsClick)
assertEquals(CreateAccountEvent.NavigateToTerms, awaitItem())
}
}
@ -110,12 +131,32 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `CheckDataBreachesToggle should change isCheckDataBreachesToggled`() = runTest {
val viewModel = CreateAccountViewModel(SavedStateHandle())
viewModel.trySendAction(CreateAccountAction.CheckDataBreachesToggle(true))
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE.copy(isCheckDataBreachesToggled = true), awaitItem())
}
}
@Test
fun `AcceptPoliciesToggle should change isAcceptPoliciesToggled`() = runTest {
val viewModel = CreateAccountViewModel(SavedStateHandle())
viewModel.trySendAction(CreateAccountAction.AcceptPoliciesToggle(true))
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE.copy(isAcceptPoliciesToggled = true), awaitItem())
}
}
companion object {
private val DEFAULT_STATE = CreateAccountState(
passwordInput = "",
emailInput = "",
confirmPasswordInput = "",
passwordHintInput = "",
isCheckDataBreachesToggled = false,
isAcceptPoliciesToggled = false,
errorDialogState = BasicDialogState.Hidden,
)
}

View file

@ -2,9 +2,6 @@ package com.x8bit.bitwarden.ui.auth.feature.landing
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasClickAction
import androidx.compose.ui.test.onChildren
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
@ -69,7 +66,7 @@ class LandingScreenTest : BaseComposeTest() {
viewModel = viewModel,
)
}
composeTestRule.onNodeWithText("Continue").performClick()
composeTestRule.onNodeWithText("Continue").performScrollTo().performClick()
verify {
viewModel.trySendAction(LandingAction.ContinueButtonClick)
}
@ -97,8 +94,6 @@ class LandingScreenTest : BaseComposeTest() {
}
composeTestRule
.onNodeWithText("Remember me")
.onChildren()
.filterToOne(hasClickAction())
.performClick()
verify {
viewModel.trySendAction(LandingAction.RememberMeToggle(true))