Merge pull request #6967 from thundernest/add_account_setup_autoconfig_ui_with_validation

Add account setup autoconfig UI with validation
This commit is contained in:
Wolf-Martell Montwé 2023-06-09 13:47:29 +02:00 committed by GitHub
commit a9cc57098c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 517 additions and 69 deletions

View file

@ -16,6 +16,7 @@ fun EmailAddressInput(
modifier: Modifier = Modifier,
emailAddress: String = "",
errorMessage: String? = null,
isEnabled: Boolean = true,
contentPadding: PaddingValues = inputContentPadding(),
) {
InputLayout(
@ -27,6 +28,7 @@ fun EmailAddressInput(
value = emailAddress,
onValueChange = onEmailAddressChange,
label = stringResource(id = R.string.designsystem_molecule_email_address_input_label),
isEnabled = isEnabled,
hasError = errorMessage != null,
modifier = Modifier.fillMaxWidth(),
)

View file

@ -1,6 +1,8 @@
package app.k9mail.feature.account.setup
import app.k9mail.feature.account.setup.ui.AccountSetupViewModel
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigValidator
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigViewModel
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigValidator
@ -16,12 +18,17 @@ import org.koin.core.module.Module
import org.koin.dsl.module
val featureAccountSetupModule: Module = module {
factory<AccountAutoConfigContract.Validator> { AccountAutoConfigValidator() }
factory<AccountIncomingConfigContract.Validator> { AccountIncomingConfigValidator() }
factory<AccountOutgoingConfigContract.Validator> { AccountOutgoingConfigValidator() }
factory<AccountOptionsContract.Validator> { AccountOptionsValidator() }
viewModel { AccountSetupViewModel() }
viewModel { AccountAutoConfigViewModel() }
viewModel {
AccountAutoConfigViewModel(
validator = get(),
)
}
viewModel {
AccountIncomingConfigViewModel(
validator = get(),

View file

@ -0,0 +1,29 @@
package app.k9mail.feature.account.setup.domain.usecase
import app.k9mail.core.common.domain.usecase.validation.ValidationError
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.core.common.domain.usecase.validation.ValidationUseCase
class ValidateEmailAddress : ValidationUseCase<String> {
// TODO replace by new email validation
override fun execute(input: String): ValidationResult {
return when {
input.isBlank() -> ValidationResult.Failure(ValidateEmailAddressError.EmptyEmailAddress)
!EMAIL_ADDRESS.matches(input) -> ValidationResult.Failure(ValidateEmailAddressError.InvalidEmailAddress)
else -> ValidationResult.Success
}
}
sealed interface ValidateEmailAddressError : ValidationError {
object EmptyEmailAddress : ValidateEmailAddressError
object InvalidEmailAddress : ValidateEmailAddressError
}
private companion object {
val EMAIL_ADDRESS =
"[a-zA-Z0-9+._%\\-]{1,256}@[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}(\\.[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25})+".toRegex()
}
}

View file

@ -1,7 +1,6 @@
package app.k9mail.feature.account.setup.ui.autoconfig
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@ -13,13 +12,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import app.k9mail.core.ui.compose.common.DevicePreviews
import app.k9mail.core.ui.compose.designsystem.atom.textfield.TextFieldOutlined
import app.k9mail.core.ui.compose.designsystem.template.ResponsiveWidthContainer
import app.k9mail.core.ui.compose.theme.K9Theme
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.ThunderbirdTheme
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.Event
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.State
import app.k9mail.feature.account.setup.ui.autoconfig.item.contentItems
@Composable
internal fun AccountAutoConfigContent(
@ -41,35 +40,14 @@ internal fun AccountAutoConfigContent(
.imePadding(),
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.double, Alignment.CenterVertically),
) {
item {
AccountSetupEmailForm(
state = state,
onEvent = onEvent,
modifier = Modifier.fillMaxWidth(),
)
}
contentItems(
state = state,
onEvent = onEvent,
)
}
}
}
@Composable
private fun AccountSetupEmailForm(
state: State,
onEvent: (Event) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
) {
TextFieldOutlined(
value = state.emailAddress.value,
onValueChange = { onEvent(Event.EmailAddressChanged(it)) },
label = "Email address",
)
}
}
@Composable
@DevicePreviews
internal fun AccountAutoConfigContentK9Preview() {

View file

@ -1,5 +1,6 @@
package app.k9mail.feature.account.setup.ui.autoconfig
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
import app.k9mail.feature.account.setup.domain.entity.AutoConfig
import app.k9mail.feature.account.setup.domain.input.StringInputField
@ -38,4 +39,9 @@ interface AccountAutoConfigContract {
object NavigateNext : Effect()
object NavigateBack : Effect()
}
interface Validator {
fun validateEmailAddress(emailAddress: String): ValidationResult
fun validatePassword(password: String): ValidationResult
}
}

View file

@ -58,7 +58,9 @@ internal fun AccountAutoConfigScreenK9Preview() {
AccountAutoConfigScreen(
onNext = {},
onBack = {},
viewModel = AccountAutoConfigViewModel(),
viewModel = AccountAutoConfigViewModel(
validator = AccountAutoConfigValidator(),
),
)
}
}
@ -70,7 +72,9 @@ internal fun AccountAutoConfigScreenThunderbirdPreview() {
AccountAutoConfigScreen(
onNext = {},
onBack = {},
viewModel = AccountAutoConfigViewModel(),
viewModel = AccountAutoConfigViewModel(
validator = AccountAutoConfigValidator(),
),
)
}
}

View file

@ -0,0 +1,19 @@
package app.k9mail.feature.account.setup.ui.autoconfig
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.feature.account.setup.domain.usecase.ValidateEmailAddress
import app.k9mail.feature.account.setup.domain.usecase.ValidatePassword
class AccountAutoConfigValidator(
private val emailAddressValidator: ValidateEmailAddress = ValidateEmailAddress(),
private val passwordValidator: ValidatePassword = ValidatePassword(),
) : AccountAutoConfigContract.Validator {
override fun validateEmailAddress(emailAddress: String): ValidationResult {
return emailAddressValidator.execute(emailAddress)
}
override fun validatePassword(password: String): ValidationResult {
return passwordValidator.execute(password)
}
}

View file

@ -1,18 +1,19 @@
package app.k9mail.feature.account.setup.ui.autoconfig
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
import app.k9mail.feature.account.setup.domain.input.StringInputField
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.ConfigStep.EMAIL_ADDRESS
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.ConfigStep.OAUTH
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.ConfigStep.PASSWORD
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.ConfigStep
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.Effect
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.Event
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.State
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.Validator
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.ViewModel
@Suppress("TooManyFunctions")
class AccountAutoConfigViewModel(
initialState: State = State(),
private val validator: Validator,
) : BaseViewModel<State, Event, Effect>(initialState), ViewModel {
override fun initState(state: State) {
@ -25,8 +26,8 @@ class AccountAutoConfigViewModel(
when (event) {
is Event.EmailAddressChanged -> changeEmailAddress(event.emailAddress)
is Event.PasswordChanged -> changePassword(event.password)
Event.OnNextClicked -> submit()
Event.OnBackClicked -> navigateBack()
Event.OnNextClicked -> onNext()
Event.OnBackClicked -> onBack()
Event.OnRetryClicked -> retry()
}
}
@ -47,24 +48,66 @@ class AccountAutoConfigViewModel(
}
}
private fun submit() {
private fun onNext() {
when (state.value.configStep) {
EMAIL_ADDRESS -> submitEmail()
PASSWORD -> submitPassword()
OAUTH -> TODO()
ConfigStep.EMAIL_ADDRESS -> submitEmail()
ConfigStep.PASSWORD -> submitPassword()
ConfigStep.OAUTH -> TODO()
}
}
private fun retry() {
TODO()
updateState {
it.copy(configStep = ConfigStep.EMAIL_ADDRESS)
}
}
private fun submitEmail() {
navigateNext()
with(state.value) {
val emailValidationResult = validator.validateEmailAddress(emailAddress.value)
val hasError = emailValidationResult is ValidationResult.Failure
updateState {
it.copy(
configStep = if (hasError) ConfigStep.EMAIL_ADDRESS else ConfigStep.PASSWORD,
emailAddress = it.emailAddress.updateFromValidationResult(emailValidationResult),
)
}
}
}
private fun submitPassword() {
navigateNext()
with(state.value) {
val emailValidationResult = validator.validateEmailAddress(emailAddress.value)
val passwordValidationResult = validator.validatePassword(password.value)
val hasError = listOf(emailValidationResult, passwordValidationResult)
.any { it is ValidationResult.Failure }
updateState {
it.copy(
emailAddress = it.emailAddress.updateFromValidationResult(emailValidationResult),
password = it.password.updateFromValidationResult(passwordValidationResult),
)
}
if (!hasError) {
navigateNext()
}
}
}
private fun onBack() {
when (state.value.configStep) {
ConfigStep.EMAIL_ADDRESS -> navigateBack()
ConfigStep.PASSWORD -> updateState {
it.copy(
configStep = ConfigStep.EMAIL_ADDRESS,
password = StringInputField(),
)
}
ConfigStep.OAUTH -> TODO()
}
}
private fun navigateBack() = emitEffect(Effect.NavigateBack)

View file

@ -0,0 +1,30 @@
package app.k9mail.feature.account.setup.ui.autoconfig.item
import androidx.compose.foundation.lazy.LazyListScope
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.ConfigStep
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.Event
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.State
internal fun LazyListScope.contentItems(
state: State,
onEvent: (Event) -> Unit,
) {
item(key = "email") {
EmailAddressItem(
emailAddress = state.emailAddress.value,
error = state.emailAddress.error,
onEmailAddressChange = { onEvent(Event.EmailAddressChanged(it)) },
isEnabled = state.configStep == ConfigStep.EMAIL_ADDRESS,
)
}
if (state.configStep == ConfigStep.PASSWORD) {
item(key = "password") {
PasswordItem(
password = state.password.value,
error = state.password.error,
onPasswordChange = { onEvent(Event.PasswordChanged(it)) },
)
}
}
}

View file

@ -0,0 +1,34 @@
package app.k9mail.feature.account.setup.ui.autoconfig.item
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import app.k9mail.core.common.domain.usecase.validation.ValidationError
import app.k9mail.core.ui.compose.designsystem.molecule.input.EmailAddressInput
import app.k9mail.feature.account.setup.ui.common.item.ListItem
import app.k9mail.feature.account.setup.ui.common.toResourceString
@Composable
fun LazyItemScope.EmailAddressItem(
emailAddress: String,
error: ValidationError?,
onEmailAddressChange: (String) -> Unit,
modifier: Modifier = Modifier,
isEnabled: Boolean = true,
) {
val resources = LocalContext.current.resources
ListItem(
modifier = modifier,
) {
EmailAddressInput(
emailAddress = emailAddress,
errorMessage = error?.toResourceString(resources),
onEmailAddressChange = onEmailAddressChange,
isEnabled = isEnabled,
contentPadding = PaddingValues(),
)
}
}

View file

@ -0,0 +1,32 @@
package app.k9mail.feature.account.setup.ui.autoconfig.item
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import app.k9mail.core.common.domain.usecase.validation.ValidationError
import app.k9mail.core.ui.compose.designsystem.molecule.input.PasswordInput
import app.k9mail.feature.account.setup.ui.common.item.ListItem
import app.k9mail.feature.account.setup.ui.common.toResourceString
@Composable
fun LazyItemScope.PasswordItem(
password: String,
error: ValidationError?,
onPasswordChange: (String) -> Unit,
modifier: Modifier = Modifier,
) {
val resources = LocalContext.current.resources
ListItem(
modifier = modifier,
) {
PasswordInput(
password = password,
errorMessage = error?.toResourceString(resources),
onPasswordChange = onPasswordChange,
contentPadding = PaddingValues(),
)
}
}

View file

@ -4,6 +4,7 @@ import android.content.res.Resources
import app.k9mail.core.common.domain.usecase.validation.ValidationError
import app.k9mail.feature.account.setup.R
import app.k9mail.feature.account.setup.domain.entity.ConnectionSecurity
import app.k9mail.feature.account.setup.domain.usecase.ValidateEmailAddress.ValidateEmailAddressError
import app.k9mail.feature.account.setup.domain.usecase.ValidateImapPrefix.ValidateImapPrefixError
import app.k9mail.feature.account.setup.domain.usecase.ValidatePassword.ValidatePasswordError
import app.k9mail.feature.account.setup.domain.usecase.ValidatePort.ValidatePortError
@ -20,6 +21,7 @@ internal fun ConnectionSecurity.toResourceString(resources: Resources): String {
internal fun ValidationError.toResourceString(resources: Resources): String {
return when (this) {
is ValidateEmailAddressError -> toEmailAddressErrorString(resources)
is ValidateServerError -> toServerErrorString(resources)
is ValidatePortError -> toPortErrorString(resources)
is ValidateUsernameError -> toUsernameErrorString(resources)
@ -29,6 +31,18 @@ internal fun ValidationError.toResourceString(resources: Resources): String {
}
}
private fun ValidateEmailAddressError.toEmailAddressErrorString(resources: Resources): String {
return when (this) {
is ValidateEmailAddressError.EmptyEmailAddress -> resources.getString(
R.string.account_setup_validation_error_email_address_required,
)
is ValidateEmailAddressError.InvalidEmailAddress -> resources.getString(
R.string.account_setup_validation_error_email_address_invalid,
)
}
}
private fun ValidateServerError.toServerErrorString(resources: Resources): String {
return when (this) {
is ValidateServerError.EmptyServer -> resources.getString(

View file

@ -1,4 +1,4 @@
package app.k9mail.feature.account.setup.ui.common
package app.k9mail.feature.account.setup.ui.common.item
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable

View file

@ -0,0 +1,28 @@
package app.k9mail.feature.account.setup.ui.common.item
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LazyItemScope.ListItem(
modifier: Modifier = Modifier,
contentPaddingValues: PaddingValues = defaultItemPadding(),
content: @Composable () -> Unit,
) {
Box(
modifier = Modifier
.padding(contentPaddingValues)
.animateItemPlacement()
.fillMaxWidth()
.then(modifier),
) {
content()
}
}

View file

@ -28,7 +28,7 @@ import app.k9mail.core.ui.compose.theme.ThunderbirdTheme
import app.k9mail.feature.account.setup.R
import app.k9mail.feature.account.setup.domain.entity.ConnectionSecurity
import app.k9mail.feature.account.setup.domain.entity.IncomingProtocolType
import app.k9mail.feature.account.setup.ui.common.defaultItemPadding
import app.k9mail.feature.account.setup.ui.common.item.defaultItemPadding
import app.k9mail.feature.account.setup.ui.common.toResourceString
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.State

View file

@ -29,8 +29,8 @@ import app.k9mail.core.ui.compose.theme.ThunderbirdTheme
import app.k9mail.feature.account.setup.R
import app.k9mail.feature.account.setup.domain.entity.EmailCheckFrequency
import app.k9mail.feature.account.setup.domain.entity.EmailDisplayCount
import app.k9mail.feature.account.setup.ui.common.defaultHeadlineItemPadding
import app.k9mail.feature.account.setup.ui.common.defaultItemPadding
import app.k9mail.feature.account.setup.ui.common.item.defaultHeadlineItemPadding
import app.k9mail.feature.account.setup.ui.common.item.defaultItemPadding
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.Event
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.State

View file

@ -27,7 +27,7 @@ import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.ThunderbirdTheme
import app.k9mail.feature.account.setup.R
import app.k9mail.feature.account.setup.domain.entity.ConnectionSecurity
import app.k9mail.feature.account.setup.ui.common.defaultItemPadding
import app.k9mail.feature.account.setup.ui.common.item.defaultItemPadding
import app.k9mail.feature.account.setup.ui.common.toResourceString
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract.Event
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract.State

View file

@ -9,6 +9,8 @@
<string name="account_setup_connection_security_start_tls">StartTLS</string>
<string name="account_setup_client_certificate_none_available">None available</string>
<string name="account_setup_validation_error_email_address_required">Email address is required.</string>
<string name="account_setup_validation_error_email_address_invalid">Email address is invalid.</string>
<string name="account_setup_validation_error_server_required">Server name is required.</string>
<string name="account_setup_validation_error_port_required">Port is required.</string>
<string name="account_setup_validation_error_port_invalid">Port is invalid (must be 165535).</string>

View file

@ -0,0 +1,42 @@
package app.k9mail.feature.account.setup.domain.usecase
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.feature.account.setup.domain.usecase.ValidateEmailAddress.ValidateEmailAddressError
import assertk.assertThat
import assertk.assertions.isInstanceOf
import assertk.assertions.prop
import org.junit.Test
class ValidateEmailAddressTest {
@Test
fun `should succeed when email address is valid`() {
val useCase = ValidateEmailAddress()
val result = useCase.execute("test@example.com")
assertThat(result).isInstanceOf(ValidationResult.Success::class)
}
@Test
fun `should fail when email address is blank`() {
val useCase = ValidateEmailAddress()
val result = useCase.execute(" ")
assertThat(result).isInstanceOf(ValidationResult.Failure::class)
.prop(ValidationResult.Failure::error)
.isInstanceOf(ValidateEmailAddressError.EmptyEmailAddress::class)
}
@Test
fun `should fail when email address is invalid`() {
val useCase = ValidateEmailAddress()
val result = useCase.execute("test")
assertThat(result).isInstanceOf(ValidationResult.Failure::class)
.prop(ValidationResult.Failure::error)
.isInstanceOf(ValidateEmailAddressError.InvalidEmailAddress::class)
}
}

View file

@ -1,6 +1,8 @@
package app.k9mail.feature.account.setup.ui.autoconfig
import app.cash.turbine.testIn
import app.k9mail.core.common.domain.usecase.validation.ValidationError
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.core.ui.compose.testing.MainDispatcherRule
import app.k9mail.feature.account.setup.domain.input.StringInputField
import app.k9mail.feature.account.setup.testing.eventStateTest
@ -8,6 +10,7 @@ import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.Effect
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.Event
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.State
import assertk.assertThat
import assertk.assertions.assertThatAndTurbinesConsumed
import assertk.assertions.isEqualTo
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -21,7 +24,9 @@ class AccountAutoConfigViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
private val testSubject = AccountAutoConfigViewModel()
private val testSubject = AccountAutoConfigViewModel(
validator = FakeAccountAutoConfigValidator(),
)
@Test
fun `should reset state when EmailAddressChanged event is received`() = runTest {
@ -59,31 +64,155 @@ class AccountAutoConfigViewModelTest {
}
@Test
fun `should emit NavigateNext effect when NextClicked event is received`() = runTest {
val viewModel = testSubject
val stateTurbine = viewModel.state.testIn(backgroundScope)
val effectTurbine = viewModel.effect.testIn(backgroundScope)
val turbines = listOf(stateTurbine, effectTurbine)
fun `should change config step to password when OnNextClicked event is received`() = runTest {
val initialState = State(
configStep = ConfigStep.EMAIL_ADDRESS,
emailAddress = StringInputField(value = "email"),
)
testSubject.initState(initialState)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(State())
}
viewModel.event(Event.OnNextClicked)
assertThatAndTurbinesConsumed(
actual = effectTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(Effect.NavigateNext)
}
eventStateTest(
viewModel = testSubject,
initialState = initialState,
event = Event.OnNextClicked,
expectedState = State(
configStep = ConfigStep.PASSWORD,
emailAddress = StringInputField(
value = "email",
error = null,
isValid = true,
),
),
coroutineScope = backgroundScope,
)
}
@Test
fun `should emit NavigateBack effect when BackClicked event is received`() = runTest {
fun `should not change config step to password when OnNextClicked event is received and input invalid`() = runTest {
val initialState = State(
configStep = ConfigStep.EMAIL_ADDRESS,
emailAddress = StringInputField(value = "invalid email"),
)
val viewModel = AccountAutoConfigViewModel(
validator = FakeAccountAutoConfigValidator(
emailAddressAnswer = ValidationResult.Failure(TestError),
),
initialState = initialState,
)
eventStateTest(
viewModel = viewModel,
initialState = initialState,
event = Event.OnNextClicked,
expectedState = State(
configStep = ConfigStep.EMAIL_ADDRESS,
emailAddress = StringInputField(
value = "invalid email",
error = TestError,
isValid = false,
),
),
coroutineScope = backgroundScope,
)
}
@Test
fun `should emit NavigateNext when OnNextClicked received in password step with valid input`() =
runTest {
val initialState = State(
configStep = ConfigStep.PASSWORD,
emailAddress = StringInputField(value = "email"),
password = StringInputField(value = "password"),
)
testSubject.initState(initialState)
val stateTurbine = testSubject.state.testIn(backgroundScope)
val effectTurbine = testSubject.effect.testIn(backgroundScope)
val turbines = listOf(stateTurbine, effectTurbine)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(initialState)
}
testSubject.event(Event.OnNextClicked)
assertThat(stateTurbine.awaitItem()).isEqualTo(
State(
configStep = ConfigStep.PASSWORD,
emailAddress = StringInputField(
value = "email",
error = null,
isValid = true,
),
password = StringInputField(
value = "password",
error = null,
isValid = true,
),
),
)
assertThatAndTurbinesConsumed(
actual = effectTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(Effect.NavigateNext)
}
}
@Test
fun `should not emit NavigateNext when OnNextClicked received in password step with invalid input`() =
runTest {
val initialState = State(
configStep = ConfigStep.PASSWORD,
emailAddress = StringInputField(value = "email"),
password = StringInputField(value = "password"),
)
val viewModel = AccountAutoConfigViewModel(
validator = FakeAccountAutoConfigValidator(
passwordAnswer = ValidationResult.Failure(TestError),
),
initialState = initialState,
)
val stateTurbine = viewModel.state.testIn(backgroundScope)
val effectTurbine = viewModel.effect.testIn(backgroundScope)
val turbines = listOf(stateTurbine, effectTurbine)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(initialState)
}
viewModel.event(Event.OnNextClicked)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(
State(
configStep = ConfigStep.PASSWORD,
emailAddress = StringInputField(
value = "email",
error = null,
isValid = true,
),
password = StringInputField(
value = "password",
error = TestError,
isValid = false,
),
),
)
}
}
@Test
fun `should emit NavigateBack effect when OnBackClicked event is received`() = runTest {
val viewModel = testSubject
val stateTurbine = viewModel.state.testIn(backgroundScope)
val effectTurbine = viewModel.effect.testIn(backgroundScope)
@ -105,4 +234,42 @@ class AccountAutoConfigViewModelTest {
isEqualTo(Effect.NavigateBack)
}
}
@Test
fun `should change config step to email address when OnBackClicked event is received in password config step`() =
runTest {
val initialState = State(
configStep = ConfigStep.PASSWORD,
emailAddress = StringInputField(value = "email"),
password = StringInputField(value = "password"),
)
val viewModel = testSubject
viewModel.initState(initialState)
val stateTurbine = viewModel.state.testIn(backgroundScope)
val effectTurbine = viewModel.effect.testIn(backgroundScope)
val turbines = listOf(stateTurbine, effectTurbine)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(initialState)
}
viewModel.event(Event.OnBackClicked)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(
State(
configStep = ConfigStep.EMAIL_ADDRESS,
emailAddress = StringInputField(value = "email"),
),
)
}
}
private object TestError : ValidationError
}

View file

@ -0,0 +1,11 @@
package app.k9mail.feature.account.setup.ui.autoconfig
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
class FakeAccountAutoConfigValidator(
private val emailAddressAnswer: ValidationResult = ValidationResult.Success,
private val passwordAnswer: ValidationResult = ValidationResult.Success,
) : AccountAutoConfigContract.Validator {
override fun validateEmailAddress(emailAddress: String): ValidationResult = emailAddressAnswer
override fun validatePassword(password: String): ValidationResult = passwordAnswer
}