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:
commit
a9cc57098c
21 changed files with 517 additions and 69 deletions
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 1–65535).</string>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in a new issue