mirror of
https://github.com/bitwarden/android.git
synced 2024-11-24 02:15:53 +03:00
BIT-698: Add landing email validation (#143)
This commit is contained in:
parent
5a53755f4c
commit
06384e17ab
4 changed files with 141 additions and 8 deletions
|
@ -41,6 +41,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenSwitch
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
|
||||
|
@ -67,6 +68,13 @@ fun LandingScreen(
|
|||
}
|
||||
}
|
||||
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = state.errorDialogState,
|
||||
onDismissRequest = remember(viewModel) {
|
||||
{ viewModel.trySendAction(LandingAction.ErrorDialogDismiss) }
|
||||
},
|
||||
)
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
|
|
|
@ -3,8 +3,12 @@ package com.x8bit.bitwarden.ui.auth.feature.landing
|
|||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.isValidEmail
|
||||
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
@ -28,6 +32,7 @@ class LandingViewModel @Inject constructor(
|
|||
isContinueButtonEnabled = authRepository.rememberedEmailAddress != null,
|
||||
isRememberMeEnabled = authRepository.rememberedEmailAddress != null,
|
||||
selectedRegion = LandingState.RegionOption.BITWARDEN_US,
|
||||
errorDialogState = BasicDialogState.Hidden,
|
||||
),
|
||||
) {
|
||||
|
||||
|
@ -42,6 +47,7 @@ class LandingViewModel @Inject constructor(
|
|||
when (action) {
|
||||
is LandingAction.ContinueButtonClick -> handleContinueButtonClicked()
|
||||
LandingAction.CreateAccountClick -> handleCreateAccountClicked()
|
||||
is LandingAction.ErrorDialogDismiss -> handleErrorDialogDismiss()
|
||||
is LandingAction.RememberMeToggle -> handleRememberMeToggled(action)
|
||||
is LandingAction.EmailInputChanged -> handleEmailInputUpdated(action)
|
||||
is LandingAction.RegionOptionSelect -> handleRegionSelect(action)
|
||||
|
@ -59,8 +65,15 @@ class LandingViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleContinueButtonClicked() {
|
||||
// TODO: add actual validation here: BIT-193
|
||||
if (mutableStateFlow.value.emailInput.isBlank()) {
|
||||
if (!mutableStateFlow.value.emailInput.isValidEmail()) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
errorDialogState = BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.invalid_email.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -79,6 +92,12 @@ class LandingViewModel @Inject constructor(
|
|||
sendEvent(LandingEvent.NavigateToCreateAccount)
|
||||
}
|
||||
|
||||
private fun handleErrorDialogDismiss() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(errorDialogState = BasicDialogState.Hidden)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRememberMeToggled(action: LandingAction.RememberMeToggle) {
|
||||
mutableStateFlow.update { it.copy(isRememberMeEnabled = action.isChecked) }
|
||||
}
|
||||
|
@ -101,6 +120,7 @@ data class LandingState(
|
|||
val isContinueButtonEnabled: Boolean,
|
||||
val isRememberMeEnabled: Boolean,
|
||||
val selectedRegion: RegionOption,
|
||||
val errorDialogState: BasicDialogState,
|
||||
) : Parcelable {
|
||||
/**
|
||||
* Enumerates the possible region options with their corresponding labels.
|
||||
|
@ -143,6 +163,11 @@ sealed class LandingAction {
|
|||
*/
|
||||
data object CreateAccountClick : LandingAction()
|
||||
|
||||
/**
|
||||
* Indicates that an error dialog is attempting to be dismissed.
|
||||
*/
|
||||
data object ErrorDialogDismiss : LandingAction()
|
||||
|
||||
/**
|
||||
* Indicates that the Remember Me switch has been toggled.
|
||||
*/
|
||||
|
|
|
@ -1,15 +1,23 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.landing
|
||||
|
||||
import androidx.compose.ui.test.assert
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
import androidx.compose.ui.test.assertIsNotEnabled
|
||||
import androidx.compose.ui.test.assertIsOff
|
||||
import androidx.compose.ui.test.assertIsOn
|
||||
import androidx.compose.ui.test.assertTextEquals
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
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 com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
|
@ -21,7 +29,6 @@ import org.junit.Test
|
|||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
|
||||
class LandingScreenTest : BaseComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `continue button should be enabled or disabled according to the state`() {
|
||||
val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
|
@ -237,12 +244,83 @@ class LandingScreenTest : BaseComposeTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `error dialog should be shown or hidden according to the state`() {
|
||||
val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
val viewModel = mockk<LandingViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = {},
|
||||
onNavigateToLogin = { _ -> },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNode(isDialog()).assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
errorDialogState = BasicDialogState.Shown(
|
||||
title = "Error dialog title".asText(),
|
||||
message = "Error dialog message".asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNode(isDialog()).assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Error dialog title")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onNodeWithText("Error dialog message")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onNodeWithText("Ok")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `error dialog OK click should send ErrorDialogDismiss action`() {
|
||||
val viewModel = mockk<LandingViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { stateFlow } returns MutableStateFlow(
|
||||
DEFAULT_STATE.copy(
|
||||
errorDialogState = BasicDialogState.Shown(
|
||||
title = "title".asText(),
|
||||
message = "message".asText(),
|
||||
),
|
||||
),
|
||||
)
|
||||
every { trySendAction(LandingAction.ErrorDialogDismiss) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = {},
|
||||
onNavigateToLogin = { _ -> },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Ok")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(LandingAction.ErrorDialogDismiss) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DEFAULT_STATE = LandingState(
|
||||
emailInput = "",
|
||||
isContinueButtonEnabled = true,
|
||||
isRememberMeEnabled = false,
|
||||
selectedRegion = LandingState.RegionOption.BITWARDEN_US,
|
||||
errorDialogState = BasicDialogState.Hidden,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,10 @@ package com.x8bit.bitwarden.ui.auth.feature.landing
|
|||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.R
|
||||
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
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
@ -49,23 +52,41 @@ class LandingViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueButtonClick should emit NavigateToLogin`() = runTest {
|
||||
fun `ContinueButtonClick with valid email should emit NavigateToLogin`() = runTest {
|
||||
val validEmail = "email@bitwarden.com"
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(LandingAction.EmailInputChanged("input"))
|
||||
viewModel.trySendAction(LandingAction.EmailInputChanged(validEmail))
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(LandingAction.ContinueButtonClick)
|
||||
assertEquals(
|
||||
LandingEvent.NavigateToLogin("input"),
|
||||
LandingEvent.NavigateToLogin(validEmail),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueButtonClick with empty input should do nothing`() = runTest {
|
||||
fun `ContinueButtonClick with invalid email should display an error dialog`() = runTest {
|
||||
val invalidEmail = "bitwarden.com"
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(LandingAction.EmailInputChanged(invalidEmail))
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
emailInput = invalidEmail,
|
||||
isContinueButtonEnabled = true,
|
||||
)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(initialState, awaitItem())
|
||||
|
||||
viewModel.actionChannel.trySend(LandingAction.ContinueButtonClick)
|
||||
assertEquals(
|
||||
initialState.copy(
|
||||
errorDialogState = BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.invalid_email.asText(),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,6 +178,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
|||
isContinueButtonEnabled = false,
|
||||
isRememberMeEnabled = false,
|
||||
selectedRegion = LandingState.RegionOption.BITWARDEN_US,
|
||||
errorDialogState = BasicDialogState.Hidden,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue