Add checkSettings with loading, error and success states to incoming config
This commit is contained in:
parent
22d1784d60
commit
e49d9e5c5e
6 changed files with 465 additions and 120 deletions
|
@ -36,7 +36,7 @@ internal class AccountAutoDiscoveryViewModel(
|
|||
|
||||
Event.OnNextClicked -> onNext()
|
||||
Event.OnBackClicked -> onBack()
|
||||
Event.OnRetryClicked -> retry()
|
||||
Event.OnRetryClicked -> onRetry()
|
||||
Event.OnEditConfigurationClicked -> navigateNext()
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ internal class AccountAutoDiscoveryViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
private fun retry() {
|
||||
private fun onRetry() {
|
||||
updateState {
|
||||
it.copy(error = null)
|
||||
}
|
||||
|
|
|
@ -28,6 +28,9 @@ 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.item.ErrorItem
|
||||
import app.k9mail.feature.account.setup.ui.common.item.LoadingItem
|
||||
import app.k9mail.feature.account.setup.ui.common.item.SuccessItem
|
||||
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
|
||||
|
@ -56,118 +59,144 @@ internal fun AccountIncomingConfigContent(
|
|||
.fillMaxSize()
|
||||
.imePadding(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
|
||||
verticalArrangement = if (state.isLoading || state.error != null || state.isSuccess) {
|
||||
Arrangement.spacedBy(MainTheme.spacings.double, Alignment.CenterVertically)
|
||||
} else {
|
||||
Arrangement.spacedBy(MainTheme.spacings.default)
|
||||
},
|
||||
) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.requiredHeight(MainTheme.sizes.smaller))
|
||||
}
|
||||
|
||||
item {
|
||||
SelectInput(
|
||||
options = IncomingProtocolType.all(),
|
||||
selectedOption = state.protocolType,
|
||||
onOptionChange = { onEvent(Event.ProtocolTypeChanged(it)) },
|
||||
label = stringResource(id = R.string.account_setup_incoming_config_protocol_type_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
TextInput(
|
||||
text = state.server.value,
|
||||
errorMessage = state.server.error?.toResourceString(resources),
|
||||
onTextChange = { onEvent(Event.ServerChanged(it)) },
|
||||
label = stringResource(id = R.string.account_setup_incoming_config_server_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SelectInput(
|
||||
options = ConnectionSecurity.all(),
|
||||
optionToStringTransformation = { it.toResourceString(resources) },
|
||||
selectedOption = state.security,
|
||||
onOptionChange = { onEvent(Event.SecurityChanged(it)) },
|
||||
label = stringResource(id = R.string.account_setup_incoming_config_security_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
NumberInput(
|
||||
value = state.port.value,
|
||||
errorMessage = state.port.error?.toResourceString(resources),
|
||||
onValueChange = { onEvent(Event.PortChanged(it)) },
|
||||
label = stringResource(id = R.string.account_setup_outgoing_config_port_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
TextInput(
|
||||
text = state.username.value,
|
||||
errorMessage = state.username.error?.toResourceString(resources),
|
||||
onTextChange = { onEvent(Event.UsernameChanged(it)) },
|
||||
label = stringResource(id = R.string.account_setup_outgoing_config_username_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
PasswordInput(
|
||||
password = state.password.value,
|
||||
errorMessage = state.password.error?.toResourceString(resources),
|
||||
onPasswordChange = { onEvent(Event.PasswordChanged(it)) },
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
// TODO add client certificate support
|
||||
SelectInput(
|
||||
options = persistentListOf(
|
||||
stringResource(
|
||||
id = R.string.account_setup_client_certificate_none_available,
|
||||
),
|
||||
),
|
||||
optionToStringTransformation = { it },
|
||||
selectedOption = stringResource(
|
||||
id = R.string.account_setup_client_certificate_none_available,
|
||||
),
|
||||
onOptionChange = { onEvent(Event.ClientCertificateChanged(it)) },
|
||||
label = stringResource(id = R.string.account_setup_outgoing_config_client_certificate_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
if (state.protocolType == IncomingProtocolType.IMAP) {
|
||||
if (state.isLoading) {
|
||||
item(key = "loading") {
|
||||
LoadingItem(
|
||||
message = stringResource(id = R.string.account_setup_incoming_config_loading_message),
|
||||
)
|
||||
}
|
||||
} else if (state.error != null) {
|
||||
item(key = "error") {
|
||||
ErrorItem(
|
||||
title = stringResource(id = R.string.account_setup_incoming_config_loading_error),
|
||||
message = state.error.toString(), // TODO map to string
|
||||
onRetry = { onEvent(Event.OnRetryClicked) },
|
||||
)
|
||||
}
|
||||
} else if (state.isSuccess) {
|
||||
item(key = "success") {
|
||||
SuccessItem(
|
||||
message = stringResource(id = R.string.account_setup_incoming_config_success),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
CheckboxInput(
|
||||
text = stringResource(id = R.string.account_setup_incoming_config_imap_namespace_label),
|
||||
checked = state.imapAutodetectNamespaceEnabled,
|
||||
onCheckedChange = { onEvent(Event.ImapAutoDetectNamespaceChanged(it)) },
|
||||
Spacer(modifier = Modifier.requiredHeight(MainTheme.sizes.smaller))
|
||||
}
|
||||
|
||||
item {
|
||||
SelectInput(
|
||||
options = IncomingProtocolType.all(),
|
||||
selectedOption = state.protocolType,
|
||||
onOptionChange = { onEvent(Event.ProtocolTypeChanged(it)) },
|
||||
label = stringResource(id = R.string.account_setup_incoming_config_protocol_type_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
TextInput(
|
||||
text = state.imapPrefix.value,
|
||||
errorMessage = state.imapPrefix.error?.toResourceString(resources),
|
||||
onTextChange = { onEvent(Event.ImapPrefixChanged(it)) },
|
||||
label = stringResource(id = R.string.account_setup_incoming_config_imap_prefix_label),
|
||||
text = state.server.value,
|
||||
errorMessage = state.server.error?.toResourceString(resources),
|
||||
onTextChange = { onEvent(Event.ServerChanged(it)) },
|
||||
label = stringResource(id = R.string.account_setup_incoming_config_server_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
CheckboxInput(
|
||||
text = stringResource(id = R.string.account_setup_incoming_config_compression_label),
|
||||
checked = state.useCompression,
|
||||
onCheckedChange = { onEvent(Event.UseCompressionChanged(it)) },
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
item {
|
||||
SelectInput(
|
||||
options = ConnectionSecurity.all(),
|
||||
optionToStringTransformation = { it.toResourceString(resources) },
|
||||
selectedOption = state.security,
|
||||
onOptionChange = { onEvent(Event.SecurityChanged(it)) },
|
||||
label = stringResource(id = R.string.account_setup_incoming_config_security_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
NumberInput(
|
||||
value = state.port.value,
|
||||
errorMessage = state.port.error?.toResourceString(resources),
|
||||
onValueChange = { onEvent(Event.PortChanged(it)) },
|
||||
label = stringResource(id = R.string.account_setup_outgoing_config_port_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
TextInput(
|
||||
text = state.username.value,
|
||||
errorMessage = state.username.error?.toResourceString(resources),
|
||||
onTextChange = { onEvent(Event.UsernameChanged(it)) },
|
||||
label = stringResource(id = R.string.account_setup_outgoing_config_username_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
PasswordInput(
|
||||
password = state.password.value,
|
||||
errorMessage = state.password.error?.toResourceString(resources),
|
||||
onPasswordChange = { onEvent(Event.PasswordChanged(it)) },
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
// TODO add client certificate support
|
||||
SelectInput(
|
||||
options = persistentListOf(
|
||||
stringResource(
|
||||
id = R.string.account_setup_client_certificate_none_available,
|
||||
),
|
||||
),
|
||||
optionToStringTransformation = { it },
|
||||
selectedOption = stringResource(
|
||||
id = R.string.account_setup_client_certificate_none_available,
|
||||
),
|
||||
onOptionChange = { onEvent(Event.ClientCertificateChanged(it)) },
|
||||
label = stringResource(id = R.string.account_setup_outgoing_config_client_certificate_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
if (state.protocolType == IncomingProtocolType.IMAP) {
|
||||
item {
|
||||
CheckboxInput(
|
||||
text = stringResource(id = R.string.account_setup_incoming_config_imap_namespace_label),
|
||||
checked = state.imapAutodetectNamespaceEnabled,
|
||||
onCheckedChange = { onEvent(Event.ImapAutoDetectNamespaceChanged(it)) },
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
TextInput(
|
||||
text = state.imapPrefix.value,
|
||||
errorMessage = state.imapPrefix.error?.toResourceString(resources),
|
||||
onTextChange = { onEvent(Event.ImapPrefixChanged(it)) },
|
||||
label = stringResource(id = R.string.account_setup_incoming_config_imap_prefix_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
CheckboxInput(
|
||||
text = stringResource(id = R.string.account_setup_incoming_config_compression_label),
|
||||
checked = state.useCompression,
|
||||
onCheckedChange = { onEvent(Event.UseCompressionChanged(it)) },
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ import app.k9mail.feature.account.setup.domain.entity.IncomingProtocolType
|
|||
import app.k9mail.feature.account.setup.domain.entity.toDefaultPort
|
||||
import app.k9mail.feature.account.setup.domain.input.NumberInputField
|
||||
import app.k9mail.feature.account.setup.domain.input.StringInputField
|
||||
import java.io.IOException
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
interface AccountIncomingConfigContract {
|
||||
|
||||
|
@ -27,6 +29,10 @@ interface AccountIncomingConfigContract {
|
|||
val imapAutodetectNamespaceEnabled: Boolean = true,
|
||||
val imapPrefix: StringInputField = StringInputField(),
|
||||
val useCompression: Boolean = true,
|
||||
|
||||
val isSuccess: Boolean = false,
|
||||
val error: Error? = null,
|
||||
val isLoading: Boolean = false,
|
||||
)
|
||||
|
||||
sealed class Event {
|
||||
|
@ -40,8 +46,10 @@ interface AccountIncomingConfigContract {
|
|||
data class ImapAutoDetectNamespaceChanged(val enabled: Boolean) : Event()
|
||||
data class ImapPrefixChanged(val imapPrefix: String) : Event()
|
||||
data class UseCompressionChanged(val useCompression: Boolean) : Event()
|
||||
|
||||
object OnNextClicked : Event()
|
||||
object OnBackClicked : Event()
|
||||
object OnRetryClicked : Event()
|
||||
}
|
||||
|
||||
sealed class Effect {
|
||||
|
@ -56,4 +64,12 @@ interface AccountIncomingConfigContract {
|
|||
fun validatePassword(password: String): ValidationResult
|
||||
fun validateImapPrefix(imapPrefix: String): ValidationResult
|
||||
}
|
||||
|
||||
sealed interface Error {
|
||||
data class NetworkError(val exception: IOException) : Error
|
||||
data class CertificateError(val certificateChain: List<X509Certificate>) : Error
|
||||
data class AuthenticationError(val serverMessage: String?) : Error
|
||||
data class ServerError(val serverMessage: String?) : Error
|
||||
data class UnknownError(val exception: Exception) : Error
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package app.k9mail.feature.account.setup.ui.incoming
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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.DomainContract.UseCase
|
||||
|
@ -7,11 +8,18 @@ import app.k9mail.feature.account.setup.domain.entity.ConnectionSecurity
|
|||
import app.k9mail.feature.account.setup.domain.entity.IncomingProtocolType
|
||||
import app.k9mail.feature.account.setup.domain.entity.toDefaultPort
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Error
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.State
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Validator
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.ViewModel
|
||||
import com.fsck.k9.mail.server.ServerSettingsValidationResult
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val CONTINUE_NEXT_DELAY = 1000L
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
internal class AccountIncomingConfigViewModel(
|
||||
initialState: State = State(),
|
||||
private val validator: Validator,
|
||||
|
@ -33,12 +41,27 @@ internal class AccountIncomingConfigViewModel(
|
|||
is Event.UsernameChanged -> updateState { it.copy(username = it.username.updateValue(event.username)) }
|
||||
is Event.PasswordChanged -> updateState { it.copy(password = it.password.updateValue(event.password)) }
|
||||
is Event.ClientCertificateChanged -> updateState { it.copy(clientCertificate = event.clientCertificate) }
|
||||
is Event.ImapAutoDetectNamespaceChanged -> updateState { it.copy(imapAutodetectNamespaceEnabled = event.enabled) }
|
||||
is Event.ImapPrefixChanged -> updateState { it.copy(imapPrefix = it.imapPrefix.updateValue(event.imapPrefix)) }
|
||||
is Event.ImapAutoDetectNamespaceChanged -> updateState {
|
||||
it.copy(imapAutodetectNamespaceEnabled = event.enabled)
|
||||
}
|
||||
|
||||
is Event.ImapPrefixChanged -> updateState {
|
||||
it.copy(imapPrefix = it.imapPrefix.updateValue(event.imapPrefix))
|
||||
}
|
||||
|
||||
is Event.UseCompressionChanged -> updateState { it.copy(useCompression = event.useCompression) }
|
||||
|
||||
Event.OnBackClicked -> navigateBack()
|
||||
Event.OnNextClicked -> submit()
|
||||
Event.OnNextClicked -> onNext()
|
||||
Event.OnBackClicked -> onBack()
|
||||
Event.OnRetryClicked -> onRetry()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onNext() {
|
||||
if (state.value.isSuccess) {
|
||||
navigateNext()
|
||||
} else {
|
||||
submit()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,10 +107,94 @@ internal class AccountIncomingConfigViewModel(
|
|||
}
|
||||
|
||||
if (!hasError) {
|
||||
checkSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkSettings() {
|
||||
viewModelScope.launch {
|
||||
updateState {
|
||||
it.copy(isLoading = true)
|
||||
}
|
||||
|
||||
val result = checkIncomingServerConfig.execute(state.value.protocolType, state.value.toServerSettings())
|
||||
when (result) {
|
||||
ServerSettingsValidationResult.Success -> updateSuccess()
|
||||
|
||||
is ServerSettingsValidationResult.AuthenticationError -> updateError(
|
||||
Error.AuthenticationError(result.serverMessage),
|
||||
)
|
||||
|
||||
is ServerSettingsValidationResult.CertificateError -> updateError(
|
||||
Error.CertificateError(result.certificateChain),
|
||||
)
|
||||
|
||||
is ServerSettingsValidationResult.NetworkError -> updateError(
|
||||
Error.NetworkError(result.exception),
|
||||
)
|
||||
|
||||
is ServerSettingsValidationResult.ServerError -> updateError(
|
||||
Error.ServerError(result.serverMessage),
|
||||
)
|
||||
|
||||
is ServerSettingsValidationResult.UnknownError -> updateError(
|
||||
Error.UnknownError(result.exception),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSuccess() {
|
||||
updateState {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
isSuccess = true,
|
||||
)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
delay(CONTINUE_NEXT_DELAY)
|
||||
navigateNext()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateError(error: Error) {
|
||||
updateState {
|
||||
it.copy(
|
||||
error = error,
|
||||
isLoading = false,
|
||||
isSuccess = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onBack() {
|
||||
if (state.value.isSuccess) {
|
||||
updateState {
|
||||
it.copy(
|
||||
isSuccess = false,
|
||||
)
|
||||
}
|
||||
} else if (state.value.error != null) {
|
||||
updateState {
|
||||
it.copy(
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
navigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRetry() {
|
||||
updateState {
|
||||
it.copy(
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
checkSettings()
|
||||
}
|
||||
|
||||
private fun navigateBack() = emitEffect(Effect.NavigateBack)
|
||||
|
||||
private fun navigateNext() = emitEffect(Effect.NavigateNext)
|
||||
|
|
|
@ -40,6 +40,9 @@
|
|||
<string name="account_setup_incoming_config_imap_namespace_label">Auto-detect IMAP namespace</string>
|
||||
<string name="account_setup_incoming_config_imap_prefix_label">IMAP path prefix</string>
|
||||
<string name="account_setup_incoming_config_compression_label">Use compression</string>
|
||||
<string name="account_setup_incoming_config_loading_message">Checking incoming server settings…</string>
|
||||
<string name="account_setup_incoming_config_loading_error">Incoming server Settings are not valid</string>
|
||||
<string name="account_setup_incoming_config_success">Incoming server Settings are valid!</string>
|
||||
|
||||
<string name="account_setup_outgoing_config_top_bar_title">Outgoing server settings</string>
|
||||
<string name="account_setup_outgoing_config_server_label">Server</string>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package app.k9mail.feature.account.setup.ui.incoming
|
||||
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import app.cash.turbine.testIn
|
||||
import app.k9mail.core.common.domain.usecase.validation.ValidationError
|
||||
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
|
||||
|
@ -13,18 +12,20 @@ import app.k9mail.feature.account.setup.domain.input.NumberInputField
|
|||
import app.k9mail.feature.account.setup.domain.input.StringInputField
|
||||
import app.k9mail.feature.account.setup.testing.eventStateTest
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Error
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.State
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.assertThatAndTurbinesConsumed
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isFalse
|
||||
import assertk.assertions.isTrue
|
||||
import com.fsck.k9.mail.server.ServerSettingsValidationResult
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class AccountIncomingConfigViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
|
@ -32,7 +33,10 @@ class AccountIncomingConfigViewModelTest {
|
|||
|
||||
private val testSubject = AccountIncomingConfigViewModel(
|
||||
validator = FakeAccountIncomingConfigValidator(),
|
||||
checkIncomingServerConfig = { _, _ -> ServerSettingsValidationResult.Success },
|
||||
checkIncomingServerConfig = { _, _ ->
|
||||
delay(50)
|
||||
ServerSettingsValidationResult.Success
|
||||
},
|
||||
)
|
||||
|
||||
@Test
|
||||
|
@ -159,7 +163,32 @@ class AccountIncomingConfigViewModelTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `should change state and emit NavigateNext effect when OnNextClicked event is received and input is valid`() =
|
||||
fun `should emit effect NavigateNext when OnNextClicked is received in success state`() = runTest {
|
||||
val initialState = State(isSuccess = true)
|
||||
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)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = effectTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(Effect.NavigateNext)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should checkSettings when OnNextClicked event is received and input is valid`() =
|
||||
runTest {
|
||||
val viewModel = testSubject
|
||||
val stateTurbine = viewModel.state.testIn(backgroundScope)
|
||||
|
@ -175,15 +204,23 @@ class AccountIncomingConfigViewModelTest {
|
|||
|
||||
viewModel.event(Event.OnNextClicked)
|
||||
|
||||
assertThat(stateTurbine.awaitItem()).isEqualTo(
|
||||
State(
|
||||
server = StringInputField(value = "", isValid = true),
|
||||
port = NumberInputField(value = 993L, isValid = true),
|
||||
username = StringInputField(value = "", isValid = true),
|
||||
password = StringInputField(value = "", isValid = true),
|
||||
imapPrefix = StringInputField(value = "", isValid = true),
|
||||
),
|
||||
val validState = State(
|
||||
server = StringInputField(value = "", isValid = true),
|
||||
port = NumberInputField(value = 993L, isValid = true),
|
||||
username = StringInputField(value = "", isValid = true),
|
||||
password = StringInputField(value = "", isValid = true),
|
||||
imapPrefix = StringInputField(value = "", isValid = true),
|
||||
)
|
||||
assertThat(stateTurbine.awaitItem()).isEqualTo(validState)
|
||||
|
||||
val loadingState = validState.copy(isLoading = true)
|
||||
assertThat(stateTurbine.awaitItem()).isEqualTo(loadingState)
|
||||
|
||||
val successState = loadingState.copy(
|
||||
isLoading = false,
|
||||
isSuccess = true,
|
||||
)
|
||||
assertThat(stateTurbine.awaitItem()).isEqualTo(successState)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = effectTurbine.awaitItem(),
|
||||
|
@ -196,11 +233,15 @@ class AccountIncomingConfigViewModelTest {
|
|||
@Test
|
||||
fun `should change state and not emit NavigateNext effect when OnNextClicked event received and input invalid`() =
|
||||
runTest {
|
||||
var checkSettingsCalled = false
|
||||
val viewModel = AccountIncomingConfigViewModel(
|
||||
validator = FakeAccountIncomingConfigValidator(
|
||||
serverAnswer = ValidationResult.Failure(TestError),
|
||||
),
|
||||
checkIncomingServerConfig = { _, _ -> ServerSettingsValidationResult.Success },
|
||||
checkIncomingServerConfig = { _, _ ->
|
||||
checkSettingsCalled = true
|
||||
ServerSettingsValidationResult.Success
|
||||
},
|
||||
)
|
||||
val stateTurbine = viewModel.state.testIn(backgroundScope)
|
||||
val effectTurbine = viewModel.effect.testIn(backgroundScope)
|
||||
|
@ -229,8 +270,56 @@ class AccountIncomingConfigViewModelTest {
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
assertThat(checkSettingsCalled).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should set error state when OnNextClicked and input valid but check settings failed`() = runTest {
|
||||
val viewModel = AccountIncomingConfigViewModel(
|
||||
validator = FakeAccountIncomingConfigValidator(),
|
||||
checkIncomingServerConfig = { _, _ ->
|
||||
delay(50)
|
||||
ServerSettingsValidationResult.ServerError("server error")
|
||||
},
|
||||
)
|
||||
val stateTurbine = viewModel.state.testIn(backgroundScope)
|
||||
val effectTurbine = viewModel.effect.testIn(backgroundScope)
|
||||
val turbines = listOf(stateTurbine, effectTurbine)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(State())
|
||||
}
|
||||
|
||||
viewModel.event(Event.OnNextClicked)
|
||||
|
||||
val validState = State(
|
||||
server = StringInputField(value = "", isValid = true),
|
||||
port = NumberInputField(value = 993L, isValid = true),
|
||||
username = StringInputField(value = "", isValid = true),
|
||||
password = StringInputField(value = "", isValid = true),
|
||||
imapPrefix = StringInputField(value = "", isValid = true),
|
||||
)
|
||||
assertThat(stateTurbine.awaitItem()).isEqualTo(validState)
|
||||
|
||||
val loadingState = validState.copy(isLoading = true)
|
||||
assertThat(stateTurbine.awaitItem()).isEqualTo(loadingState)
|
||||
|
||||
val failureState = loadingState.copy(
|
||||
isLoading = false,
|
||||
error = Error.ServerError("server error"),
|
||||
)
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(failureState)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit NavigateBack effect when OnBackClicked event received`() = runTest {
|
||||
val viewModel = testSubject
|
||||
|
@ -255,5 +344,106 @@ class AccountIncomingConfigViewModelTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should clear isSuccess when OnBackClicked event received`() = runTest {
|
||||
val initialState = State(isSuccess = true)
|
||||
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.OnBackClicked)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(initialState.copy(isSuccess = false))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should clear error when OnBackClicked event received`() = runTest {
|
||||
val initialState = State(error = Error.ServerError("server error"))
|
||||
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.OnBackClicked)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(initialState.copy(error = null))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should clear error and trigger check settings when OnRetryClicked event received`() = runTest {
|
||||
val initialState = State(
|
||||
server = StringInputField(value = "", isValid = true),
|
||||
port = NumberInputField(value = 993L, isValid = true),
|
||||
username = StringInputField(value = "", isValid = true),
|
||||
password = StringInputField(value = "", isValid = true),
|
||||
imapPrefix = StringInputField(value = "", isValid = true),
|
||||
error = Error.ServerError("server error"),
|
||||
)
|
||||
var checkSettingsCalled = false
|
||||
val viewModel = AccountIncomingConfigViewModel(
|
||||
validator = FakeAccountIncomingConfigValidator(),
|
||||
checkIncomingServerConfig = { _, _ ->
|
||||
checkSettingsCalled = true
|
||||
delay(50)
|
||||
ServerSettingsValidationResult.Success
|
||||
},
|
||||
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.OnRetryClicked)
|
||||
|
||||
val stateWithoutError = initialState.copy(error = null)
|
||||
assertThat(stateTurbine.awaitItem()).isEqualTo(stateWithoutError)
|
||||
|
||||
val loadingState = stateWithoutError.copy(isLoading = true)
|
||||
assertThat(stateTurbine.awaitItem()).isEqualTo(loadingState)
|
||||
|
||||
val successState = loadingState.copy(isLoading = false, isSuccess = true)
|
||||
assertThat(stateTurbine.awaitItem()).isEqualTo(successState)
|
||||
assertThat(checkSettingsCalled).isTrue()
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = effectTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(Effect.NavigateNext)
|
||||
}
|
||||
}
|
||||
|
||||
private object TestError : ValidationError
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue