Merge pull request #6953 from thundernest/add_account_setup_incoming_config-ui
Add account setup incoming config UI
This commit is contained in:
commit
7155b09d9a
25 changed files with 833 additions and 55 deletions
|
@ -20,11 +20,6 @@
|
|||
</option>
|
||||
</JavaCodeStyleSettings>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value>
|
||||
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="PACKAGES_IMPORT_LAYOUT">
|
||||
<value>
|
||||
<package name="" alias="false" withSubpackages="true" />
|
||||
|
@ -33,7 +28,6 @@
|
|||
</option>
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="99" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="99" />
|
||||
<option name="IMPORT_NESTED_CLASSES" value="true" />
|
||||
<option name="ALLOW_TRAILING_COMMA" value="true" />
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package app.k9mail.feature.account.setup
|
||||
|
||||
import app.k9mail.feature.account.setup.ui.AccountSetupViewModel
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigViewModel
|
||||
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract
|
||||
import app.k9mail.feature.account.setup.ui.options.AccountOptionsValidator
|
||||
import app.k9mail.feature.account.setup.ui.options.AccountOptionsViewModel
|
||||
|
@ -16,6 +17,7 @@ val featureAccountSetupModule: Module = module {
|
|||
factory<AccountOptionsContract.Validator> { AccountOptionsValidator() }
|
||||
|
||||
viewModel { AccountSetupViewModel() }
|
||||
viewModel { AccountIncomingConfigViewModel() }
|
||||
viewModel {
|
||||
AccountOutgoingConfigViewModel(
|
||||
validator = get(),
|
||||
|
|
|
@ -34,3 +34,12 @@ fun ConnectionSecurity.toImapDefaultPort(): Long {
|
|||
TLS -> 993
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun ConnectionSecurity.toPop3DefaultPort(): Long {
|
||||
return when (this) {
|
||||
None -> 110
|
||||
StartTLS -> 110
|
||||
TLS -> 995
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package app.k9mail.feature.account.setup.domain.entity
|
||||
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
enum class IncomingProtocolType {
|
||||
IMAP,
|
||||
POP3,
|
||||
;
|
||||
|
||||
companion object {
|
||||
val DEFAULT = IMAP
|
||||
|
||||
fun all() = values().toList().toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("SameReturnValue")
|
||||
fun IncomingProtocolType.toDefaultSecurity(): ConnectionSecurity {
|
||||
return when (this) {
|
||||
IncomingProtocolType.IMAP -> ConnectionSecurity.TLS
|
||||
IncomingProtocolType.POP3 -> ConnectionSecurity.TLS
|
||||
}
|
||||
}
|
||||
|
||||
fun IncomingProtocolType.toDefaultPort(connectionSecurity: ConnectionSecurity): Long {
|
||||
return when (this) {
|
||||
IncomingProtocolType.IMAP -> connectionSecurity.toImapDefaultPort()
|
||||
IncomingProtocolType.POP3 -> connectionSecurity.toPop3DefaultPort()
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ package app.k9mail.feature.account.setup.domain.input
|
|||
import app.k9mail.core.common.domain.usecase.validation.ValidationError
|
||||
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
|
||||
|
||||
data class NumberInputField(
|
||||
class NumberInputField(
|
||||
override val value: Long? = null,
|
||||
override val error: ValidationError? = null,
|
||||
override val isValid: Boolean = false,
|
||||
|
@ -37,15 +37,35 @@ data class NumberInputField(
|
|||
|
||||
override fun updateFromValidationResult(result: ValidationResult): NumberInputField {
|
||||
return when (result) {
|
||||
is ValidationResult.Success -> copy(
|
||||
is ValidationResult.Success -> NumberInputField(
|
||||
value = value,
|
||||
error = null,
|
||||
isValid = true,
|
||||
)
|
||||
|
||||
is ValidationResult.Failure -> copy(
|
||||
is ValidationResult.Failure -> NumberInputField(
|
||||
value = value,
|
||||
error = result.error,
|
||||
isValid = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as NumberInputField
|
||||
|
||||
if (value != other.value) return false
|
||||
if (error != other.error) return false
|
||||
return isValid == other.isValid
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = value?.hashCode() ?: 0
|
||||
result = 31 * result + (error?.hashCode() ?: 0)
|
||||
result = 31 * result + isValid.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package app.k9mail.feature.account.setup.domain.input
|
|||
import app.k9mail.core.common.domain.usecase.validation.ValidationError
|
||||
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
|
||||
|
||||
data class StringInputField(
|
||||
class StringInputField(
|
||||
override val value: String = "",
|
||||
override val error: ValidationError? = null,
|
||||
override val isValid: Boolean = false,
|
||||
|
@ -37,15 +37,35 @@ data class StringInputField(
|
|||
|
||||
override fun updateFromValidationResult(result: ValidationResult): StringInputField {
|
||||
return when (result) {
|
||||
is ValidationResult.Success -> copy(
|
||||
is ValidationResult.Success -> StringInputField(
|
||||
value = value,
|
||||
error = null,
|
||||
isValid = true,
|
||||
)
|
||||
|
||||
is ValidationResult.Failure -> copy(
|
||||
is ValidationResult.Failure -> StringInputField(
|
||||
value = value,
|
||||
error = result.error,
|
||||
isValid = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as StringInputField
|
||||
|
||||
if (value != other.value) return false
|
||||
if (error != other.error) return false
|
||||
return isValid == other.isValid
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = value.hashCode()
|
||||
result = 31 * result + (error?.hashCode() ?: 0)
|
||||
result = 31 * result + isValid.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,9 @@ import app.k9mail.feature.account.setup.ui.AccountSetupContract.Event
|
|||
import app.k9mail.feature.account.setup.ui.AccountSetupContract.SetupStep
|
||||
import app.k9mail.feature.account.setup.ui.AccountSetupContract.ViewModel
|
||||
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigScreen
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigScreen
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigViewModel
|
||||
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract
|
||||
import app.k9mail.feature.account.setup.ui.options.AccountOptionsScreen
|
||||
import app.k9mail.feature.account.setup.ui.options.AccountOptionsViewModel
|
||||
|
@ -22,6 +24,7 @@ fun AccountSetupScreen(
|
|||
onFinish: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
viewModel: ViewModel = koinViewModel<AccountSetupViewModel>(),
|
||||
incomingViewModel: AccountIncomingConfigContract.ViewModel = koinViewModel<AccountIncomingConfigViewModel>(),
|
||||
outgoingViewModel: AccountOutgoingConfigContract.ViewModel = koinViewModel<AccountOutgoingConfigViewModel>(),
|
||||
optionsViewModel: AccountOptionsContract.ViewModel = koinViewModel<AccountOptionsViewModel>(),
|
||||
) {
|
||||
|
@ -44,6 +47,7 @@ fun AccountSetupScreen(
|
|||
AccountIncomingConfigScreen(
|
||||
onNext = { dispatch(Event.OnNext) },
|
||||
onBack = { dispatch(Event.OnBack) },
|
||||
viewModel = incomingViewModel,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package app.k9mail.feature.account.setup.ui
|
||||
|
||||
import android.content.res.Resources
|
||||
import app.k9mail.feature.account.setup.R
|
||||
import app.k9mail.feature.account.setup.domain.entity.ConnectionSecurity
|
||||
|
||||
internal fun ConnectionSecurity.toResourceString(resources: Resources): String {
|
||||
return when (this) {
|
||||
ConnectionSecurity.None -> resources.getString(R.string.account_setup_connection_security_none)
|
||||
ConnectionSecurity.StartTLS -> resources.getString(R.string.account_setup_connection_security_start_tls)
|
||||
ConnectionSecurity.TLS -> resources.getString(R.string.account_setup_connection_security_ssl)
|
||||
}
|
||||
}
|
|
@ -2,27 +2,48 @@ package app.k9mail.feature.account.setup.ui.incoming
|
|||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.k9mail.core.ui.compose.common.DevicePreviews
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBody1
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.input.CheckboxInput
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.input.NumberInput
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.input.PasswordInput
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.input.SelectInput
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.input.TextInput
|
||||
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.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.incoming.AccountIncomingConfigContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.State
|
||||
import app.k9mail.feature.account.setup.ui.toResourceString
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun AccountIncomingConfigContent(
|
||||
state: State,
|
||||
onEvent: (Event) -> Unit,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val resources = LocalContext.current.resources
|
||||
|
||||
ResponsiveWidthContainer(
|
||||
modifier = Modifier
|
||||
.testTag("AccountIncomingConfigContent")
|
||||
|
@ -38,7 +59,110 @@ internal fun AccountIncomingConfigContent(
|
|||
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
|
||||
) {
|
||||
item {
|
||||
TextBody1(text = "Incoming Config")
|
||||
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,
|
||||
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,
|
||||
onValueChange = { onEvent(Event.PortChanged(it)) },
|
||||
label = stringResource(id = R.string.account_setup_outgoing_config_port_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
TextInput(
|
||||
text = state.username.value,
|
||||
onTextChange = { onEvent(Event.UsernameChanged(it)) },
|
||||
label = stringResource(id = R.string.account_setup_outgoing_config_username_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
PasswordInput(
|
||||
password = state.password.value,
|
||||
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,
|
||||
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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +173,8 @@ internal fun AccountIncomingConfigContent(
|
|||
internal fun AccountIncomingConfigContentK9Preview() {
|
||||
K9Theme {
|
||||
AccountIncomingConfigContent(
|
||||
onEvent = { },
|
||||
state = State(),
|
||||
contentPadding = PaddingValues(),
|
||||
)
|
||||
}
|
||||
|
@ -59,6 +185,8 @@ internal fun AccountIncomingConfigContentK9Preview() {
|
|||
internal fun AccountIncomingConfigContentThunderbirdPreview() {
|
||||
ThunderbirdTheme {
|
||||
AccountIncomingConfigContent(
|
||||
onEvent = { },
|
||||
state = State(),
|
||||
contentPadding = PaddingValues(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package app.k9mail.feature.account.setup.ui.incoming
|
||||
|
||||
import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
|
||||
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.domain.entity.toDefaultSecurity
|
||||
import app.k9mail.feature.account.setup.domain.input.NumberInputField
|
||||
import app.k9mail.feature.account.setup.domain.input.StringInputField
|
||||
|
||||
interface AccountIncomingConfigContract {
|
||||
|
||||
interface ViewModel : UnidirectionalViewModel<State, Event, Effect> {
|
||||
fun initState(state: State)
|
||||
}
|
||||
|
||||
data class State(
|
||||
val protocolType: IncomingProtocolType = IncomingProtocolType.DEFAULT,
|
||||
val server: StringInputField = StringInputField(),
|
||||
val security: ConnectionSecurity = IncomingProtocolType.DEFAULT.toDefaultSecurity(),
|
||||
val port: NumberInputField = NumberInputField(
|
||||
IncomingProtocolType.DEFAULT.toDefaultPort(IncomingProtocolType.DEFAULT.toDefaultSecurity()),
|
||||
),
|
||||
val username: StringInputField = StringInputField(),
|
||||
val password: StringInputField = StringInputField(),
|
||||
val clientCertificate: String = "",
|
||||
val imapAutodetectNamespaceEnabled: Boolean = true,
|
||||
val imapPrefix: StringInputField = StringInputField(),
|
||||
val useCompression: Boolean = true,
|
||||
)
|
||||
|
||||
sealed class Event {
|
||||
data class ProtocolTypeChanged(val protocolType: IncomingProtocolType) : Event()
|
||||
data class ServerChanged(val server: String) : Event()
|
||||
data class SecurityChanged(val security: ConnectionSecurity) : Event()
|
||||
data class PortChanged(val port: Long?) : Event()
|
||||
data class UsernameChanged(val username: String) : Event()
|
||||
data class PasswordChanged(val password: String) : Event()
|
||||
data class ClientCertificateChanged(val clientCertificate: String) : Event()
|
||||
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()
|
||||
}
|
||||
|
||||
sealed class Effect {
|
||||
object NavigateNext : Effect()
|
||||
object NavigateBack : Effect()
|
||||
}
|
||||
}
|
|
@ -4,19 +4,31 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.k9mail.core.ui.compose.common.DevicePreviews
|
||||
import app.k9mail.core.ui.compose.common.mvi.observe
|
||||
import app.k9mail.core.ui.compose.designsystem.template.Scaffold
|
||||
import app.k9mail.core.ui.compose.theme.K9Theme
|
||||
import app.k9mail.core.ui.compose.theme.ThunderbirdTheme
|
||||
import app.k9mail.feature.account.setup.R
|
||||
import app.k9mail.feature.account.setup.ui.common.AccountSetupBottomBar
|
||||
import app.k9mail.feature.account.setup.ui.common.AccountSetupTopAppBar
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.ViewModel
|
||||
|
||||
@Composable
|
||||
fun AccountIncomingConfigScreen(
|
||||
onNext: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
viewModel: ViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val (state, dispatch) = viewModel.observe { effect ->
|
||||
when (effect) {
|
||||
is Effect.NavigateNext -> onNext()
|
||||
is Effect.NavigateBack -> onBack()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AccountSetupTopAppBar(
|
||||
|
@ -27,13 +39,15 @@ fun AccountIncomingConfigScreen(
|
|||
AccountSetupBottomBar(
|
||||
nextButtonText = stringResource(id = R.string.account_setup_button_next),
|
||||
backButtonText = stringResource(id = R.string.account_setup_button_back),
|
||||
onNextClick = onNext,
|
||||
onBackClick = onBack,
|
||||
onNextClick = { dispatch(Event.OnNextClicked) },
|
||||
onBackClick = { dispatch(Event.OnBackClicked) },
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
) { innerPadding ->
|
||||
AccountIncomingConfigContent(
|
||||
onEvent = { dispatch(it) },
|
||||
state = state.value,
|
||||
contentPadding = innerPadding,
|
||||
)
|
||||
}
|
||||
|
@ -46,6 +60,7 @@ internal fun AccountIncomingConfigScreenK9Preview() {
|
|||
AccountIncomingConfigScreen(
|
||||
onNext = {},
|
||||
onBack = {},
|
||||
viewModel = AccountIncomingConfigViewModel(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +72,7 @@ internal fun AccountIncomingConfigScreenThunderbirdPreview() {
|
|||
AccountIncomingConfigScreen(
|
||||
onNext = {},
|
||||
onBack = {},
|
||||
viewModel = AccountIncomingConfigViewModel(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
package app.k9mail.feature.account.setup.ui.incoming
|
||||
|
||||
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
|
||||
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.domain.entity.toDefaultSecurity
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.ClientCertificateChanged
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.ImapAutoDetectNamespaceChanged
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.ImapPrefixChanged
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.OnBackClicked
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.OnNextClicked
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.PasswordChanged
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.PortChanged
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.ProtocolTypeChanged
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.SecurityChanged
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.ServerChanged
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.UseCompressionChanged
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Event.UsernameChanged
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.State
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.ViewModel
|
||||
|
||||
class AccountIncomingConfigViewModel(
|
||||
initialState: State = State(),
|
||||
) : BaseViewModel<State, Event, Effect>(initialState), ViewModel {
|
||||
|
||||
override fun initState(state: State) {
|
||||
updateState {
|
||||
state.copy()
|
||||
}
|
||||
}
|
||||
|
||||
override fun event(event: Event) {
|
||||
when (event) {
|
||||
is ProtocolTypeChanged -> updateProtocolType(event.protocolType)
|
||||
is ServerChanged -> updateState { it.copy(server = it.server.updateValue(event.server)) }
|
||||
is SecurityChanged -> updateSecurity(event.security)
|
||||
is PortChanged -> updateState { it.copy(port = it.port.updateValue(event.port)) }
|
||||
is UsernameChanged -> updateState { it.copy(username = it.username.updateValue(event.username)) }
|
||||
is PasswordChanged -> updateState { it.copy(password = it.password.updateValue(event.password)) }
|
||||
is ClientCertificateChanged -> updateState { it.copy(clientCertificate = event.clientCertificate) }
|
||||
is ImapAutoDetectNamespaceChanged -> updateState { it.copy(imapAutodetectNamespaceEnabled = event.enabled) }
|
||||
is ImapPrefixChanged -> updateState { it.copy(imapPrefix = it.imapPrefix.updateValue(event.imapPrefix)) }
|
||||
is UseCompressionChanged -> updateState { it.copy(useCompression = event.useCompression) }
|
||||
OnBackClicked -> navigateBack()
|
||||
OnNextClicked -> submit()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateProtocolType(protocolType: IncomingProtocolType) {
|
||||
updateState {
|
||||
it.copy(
|
||||
protocolType = protocolType,
|
||||
security = protocolType.toDefaultSecurity(),
|
||||
port = it.port.updateValue(
|
||||
protocolType.toDefaultPort(protocolType.toDefaultSecurity()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSecurity(security: ConnectionSecurity) {
|
||||
updateState {
|
||||
it.copy(
|
||||
security = security,
|
||||
port = it.port.updateValue(it.protocolType.toDefaultPort(security)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
navigateNext()
|
||||
}
|
||||
|
||||
private fun navigateBack() = emitEffect(Effect.NavigateBack)
|
||||
|
||||
private fun navigateNext() = emitEffect(Effect.NavigateNext)
|
||||
}
|
|
@ -30,6 +30,7 @@ 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.outgoing.AccountOutgoingConfigContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract.State
|
||||
import app.k9mail.feature.account.setup.ui.toResourceString
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Suppress("LongMethod")
|
||||
|
@ -119,12 +120,12 @@ internal fun AccountOutgoingConfigContent(
|
|||
SelectInput(
|
||||
options = persistentListOf(
|
||||
stringResource(
|
||||
id = R.string.account_setup_outgoing_config_client_certificate_none_available,
|
||||
id = R.string.account_setup_client_certificate_none_available,
|
||||
),
|
||||
),
|
||||
optionToStringTransformation = { it },
|
||||
selectedOption = stringResource(
|
||||
id = R.string.account_setup_outgoing_config_client_certificate_none_available,
|
||||
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),
|
||||
|
|
|
@ -33,17 +33,17 @@ class AccountOutgoingConfigViewModel(
|
|||
|
||||
override fun event(event: Event) {
|
||||
when (event) {
|
||||
is ServerChanged -> updateState { it.copy(server = it.server.copy(value = event.server)) }
|
||||
is ServerChanged -> updateState { it.copy(server = it.server.updateValue(event.server)) }
|
||||
is SecurityChanged -> updateSecurity(event.security)
|
||||
is PortChanged -> updateState { it.copy(port = it.port.copy(value = event.port)) }
|
||||
is UsernameChanged -> updateState { it.copy(username = it.username.copy(value = event.username)) }
|
||||
is PasswordChanged -> updateState { it.copy(password = it.password.copy(value = event.password)) }
|
||||
is PortChanged -> updateState { it.copy(port = it.port.updateValue(event.port)) }
|
||||
is UsernameChanged -> updateState { it.copy(username = it.username.updateValue(event.username)) }
|
||||
is PasswordChanged -> updateState { it.copy(password = it.password.updateValue(event.password)) }
|
||||
is ClientCertificateChanged -> updateState { it.copy(clientCertificate = event.clientCertificate) }
|
||||
is ImapAutoDetectNamespaceChanged -> updateState { it.copy(imapAutodetectNamespaceEnabled = event.enabled) }
|
||||
is UseCompressionChanged -> updateState { it.copy(useCompression = event.useCompression) }
|
||||
|
||||
OnBackClicked -> navigateBack()
|
||||
OnNextClicked -> submit()
|
||||
OnBackClicked -> navigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,10 +51,7 @@ class AccountOutgoingConfigViewModel(
|
|||
updateState {
|
||||
it.copy(
|
||||
security = security,
|
||||
port = it.port.copy(
|
||||
value = security.toSmtpDefaultPort(),
|
||||
error = null,
|
||||
),
|
||||
port = it.port.updateValue(security.toSmtpDefaultPort()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,20 +3,11 @@ package app.k9mail.feature.account.setup.ui.outgoing
|
|||
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.ValidatePassword.ValidatePasswordError
|
||||
import app.k9mail.feature.account.setup.domain.usecase.ValidatePort.ValidatePortError
|
||||
import app.k9mail.feature.account.setup.domain.usecase.ValidateServer.ValidateServerError
|
||||
import app.k9mail.feature.account.setup.domain.usecase.ValidateUsername.ValidateUsernameError
|
||||
|
||||
internal fun ConnectionSecurity.toResourceString(resources: Resources): String {
|
||||
return when (this) {
|
||||
ConnectionSecurity.None -> resources.getString(R.string.account_setup_outgoing_config_security_none)
|
||||
ConnectionSecurity.StartTLS -> resources.getString(R.string.account_setup_outgoing_config_security_start_tls)
|
||||
ConnectionSecurity.TLS -> resources.getString(R.string.account_setup_outgoing_config_security_ssl)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ValidationError.toResourceString(resources: Resources): String {
|
||||
return when (this) {
|
||||
is ValidateServerError -> toServerErrorString(resources)
|
||||
|
|
|
@ -4,18 +4,25 @@
|
|||
<string name="account_setup_button_next">Next</string>
|
||||
<string name="account_setup_button_back">Back</string>
|
||||
<string name="account_setup_button_finish">Finish</string>
|
||||
<string name="account_setup_connection_security_none">None</string>
|
||||
<string name="account_setup_connection_security_ssl">SSL/TLS</string>
|
||||
<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_auto_config_title">K-9 Mail</string>
|
||||
|
||||
<string name="account_setup_incoming_config_top_bar_title">Incoming server settings</string>
|
||||
<string name="account_setup_incoming_config_protocol_type_label">Protocol</string>
|
||||
<string name="account_setup_incoming_config_server_label">Server</string>
|
||||
<string name="account_setup_incoming_config_security_label">Security</string>
|
||||
<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_outgoing_config_top_bar_title">Outgoing server settings</string>
|
||||
<string name="account_setup_outgoing_config_server_label">Server</string>
|
||||
<string name="account_setup_outgoing_config_server_error_required">Server name is required.</string>
|
||||
<string name="account_setup_outgoing_config_security_label">Security</string>
|
||||
<string name="account_setup_outgoing_config_security_none">None</string>
|
||||
<string name="account_setup_outgoing_config_security_ssl">SSL/TLS</string>
|
||||
<string name="account_setup_outgoing_config_security_start_tls">StartTLS</string>
|
||||
<string name="account_setup_outgoing_config_port_label">Port</string>
|
||||
<string name="account_setup_outgoing_config_port_error_required">Port is required.</string>
|
||||
<string name="account_setup_outgoing_config_port_error_invalid">Port is invalid (must be 1–65535).</string>
|
||||
|
@ -23,7 +30,6 @@
|
|||
<string name="account_setup_outgoing_config_username_error_required">Username is required.</string>
|
||||
<string name="account_setup_outgoing_config_password_error_required">Password is required.</string>
|
||||
<string name="account_setup_outgoing_config_client_certificate_label">Client certificate</string>
|
||||
<string name="account_setup_outgoing_config_client_certificate_none_available">None available</string>
|
||||
<string name="account_setup_outgoing_config_imap_namespace_label">Auto-detect IMAP namespace</string>
|
||||
<string name="account_setup_outgoing_config_compression_label">Use compression</string>
|
||||
|
||||
|
|
|
@ -1,36 +1,59 @@
|
|||
package app.k9mail.feature.account.setup.domain.entity
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import org.junit.Test
|
||||
|
||||
class ConnectionSecurityTest {
|
||||
|
||||
@Test
|
||||
fun `should provide right default smtp port`() {
|
||||
val connectionSecurity = ConnectionSecurity.all()
|
||||
val connectionSecurities = ConnectionSecurity.all()
|
||||
|
||||
for (security in connectionSecurity) {
|
||||
for (security in connectionSecurities) {
|
||||
val port = security.toSmtpDefaultPort()
|
||||
|
||||
assertThat(port).isEqualTo(
|
||||
when (security) {
|
||||
ConnectionSecurity.None -> assert(port == 587L)
|
||||
ConnectionSecurity.StartTLS -> assert(port == 587L)
|
||||
ConnectionSecurity.TLS -> assert(port == 465L)
|
||||
}
|
||||
ConnectionSecurity.None -> 587L
|
||||
ConnectionSecurity.StartTLS -> 587L
|
||||
ConnectionSecurity.TLS -> 465L
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should provide right default imap port`() {
|
||||
val connectionSecurity = ConnectionSecurity.all()
|
||||
val connectionSecurities = ConnectionSecurity.all()
|
||||
|
||||
for (security in connectionSecurity) {
|
||||
for (security in connectionSecurities) {
|
||||
val port = security.toImapDefaultPort()
|
||||
|
||||
assertThat(port).isEqualTo(
|
||||
when (security) {
|
||||
ConnectionSecurity.None -> assert(port == 143L)
|
||||
ConnectionSecurity.StartTLS -> assert(port == 143L)
|
||||
ConnectionSecurity.TLS -> assert(port == 993L)
|
||||
}
|
||||
ConnectionSecurity.None -> 143L
|
||||
ConnectionSecurity.StartTLS -> 143L
|
||||
ConnectionSecurity.TLS -> 993L
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should provide right default pop3 port`() {
|
||||
val connectionSecurities = ConnectionSecurity.all()
|
||||
|
||||
for (security in connectionSecurities) {
|
||||
val port = security.toPop3DefaultPort()
|
||||
|
||||
assertThat(port).isEqualTo(
|
||||
when (security) {
|
||||
ConnectionSecurity.None -> 110L
|
||||
ConnectionSecurity.StartTLS -> 110L
|
||||
ConnectionSecurity.TLS -> 995L
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
package app.k9mail.feature.account.setup.domain.entity
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import org.junit.Test
|
||||
|
||||
class IncomingProtocolTypeTest {
|
||||
|
||||
@Test
|
||||
fun `should provide right default security`() {
|
||||
val incomingProtocolTypes = IncomingProtocolType.all()
|
||||
|
||||
for (incomingProtocolType in incomingProtocolTypes) {
|
||||
val security = incomingProtocolType.toDefaultSecurity()
|
||||
|
||||
assertThat(security).isEqualTo(
|
||||
when (incomingProtocolType) {
|
||||
IncomingProtocolType.IMAP -> ConnectionSecurity.TLS
|
||||
IncomingProtocolType.POP3 -> ConnectionSecurity.TLS
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should provide right default port`() {
|
||||
val incomingProtocolTypes = IncomingProtocolType.all()
|
||||
|
||||
for (incomingProtocolType in incomingProtocolTypes) {
|
||||
val port = incomingProtocolType.toDefaultPort(ConnectionSecurity.TLS)
|
||||
|
||||
assertThat(port).isEqualTo(
|
||||
when (incomingProtocolType) {
|
||||
IncomingProtocolType.IMAP -> 993L
|
||||
IncomingProtocolType.POP3 -> 995L
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,7 +7,9 @@ import assertk.all
|
|||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isFalse
|
||||
import assertk.assertions.isNotSameAs
|
||||
import assertk.assertions.isNull
|
||||
import assertk.assertions.isSameAs
|
||||
import assertk.assertions.isTrue
|
||||
import assertk.assertions.prop
|
||||
import org.junit.Test
|
||||
|
@ -27,7 +29,7 @@ data class InputFieldTestData<T>(
|
|||
|
||||
@RunWith(Parameterized::class)
|
||||
class InputFieldTest(
|
||||
val data: InputFieldTestData<Any>,
|
||||
private val data: InputFieldTestData<Any>,
|
||||
) {
|
||||
|
||||
@Test
|
||||
|
@ -50,6 +52,7 @@ class InputFieldTest(
|
|||
val result = initialInput.updateValue(data.updatedValue)
|
||||
|
||||
assertThat(result).all {
|
||||
isNotSameAs(initialInput)
|
||||
hasValue(data.updatedValue)
|
||||
hasNoError()
|
||||
isNotValid()
|
||||
|
@ -67,6 +70,7 @@ class InputFieldTest(
|
|||
val result = initialInput.updateError(TestValidationError)
|
||||
|
||||
assertThat(result).all {
|
||||
isNotSameAs(initialInput)
|
||||
hasValue(data.initialValue)
|
||||
hasError(TestValidationError)
|
||||
isNotValid()
|
||||
|
@ -84,6 +88,7 @@ class InputFieldTest(
|
|||
val result = initialInput.updateValidity(isValid = true)
|
||||
|
||||
assertThat(result).all {
|
||||
isNotSameAs(initialInput)
|
||||
hasValue(data.initialValue)
|
||||
hasNoError()
|
||||
isValid()
|
||||
|
@ -101,6 +106,7 @@ class InputFieldTest(
|
|||
val result = initialInput.updateValidity(isValid = false)
|
||||
|
||||
assertThat(result).all {
|
||||
isSameAs(initialInput)
|
||||
hasValue(data.initialValue)
|
||||
hasError(TestValidationError)
|
||||
isNotValid()
|
||||
|
@ -118,6 +124,7 @@ class InputFieldTest(
|
|||
val result = initialInput.updateError(TestValidationError2)
|
||||
|
||||
assertThat(result).all {
|
||||
isNotSameAs(initialInput)
|
||||
hasValue(data.initialValue)
|
||||
hasError(TestValidationError2)
|
||||
isNotValid()
|
||||
|
@ -135,6 +142,7 @@ class InputFieldTest(
|
|||
val result = initialInput.updateFromValidationResult(ValidationResult.Success)
|
||||
|
||||
assertThat(result).all {
|
||||
isNotSameAs(initialInput)
|
||||
hasValue(data.initialValue)
|
||||
hasNoError()
|
||||
isValid()
|
||||
|
@ -152,12 +160,45 @@ class InputFieldTest(
|
|||
val result = initialInput.updateFromValidationResult(ValidationResult.Failure(TestValidationError))
|
||||
|
||||
assertThat(result).all {
|
||||
isNotSameAs(initialInput)
|
||||
hasValue(data.initialValue)
|
||||
hasError(TestValidationError)
|
||||
isNotValid()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should decide equality on properties`() {
|
||||
val input1 = data.createInitialInput(
|
||||
data.initialValue,
|
||||
data.initialError,
|
||||
data.initialIsValid,
|
||||
)
|
||||
val input2 = data.createInitialInput(
|
||||
data.initialValue,
|
||||
data.initialError,
|
||||
data.initialIsValid,
|
||||
)
|
||||
|
||||
assertThat(input1.equals(input2)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should have same hashCode`() {
|
||||
val input1 = data.createInitialInput(
|
||||
data.initialValue,
|
||||
data.initialError,
|
||||
data.initialIsValid,
|
||||
)
|
||||
val input2 = data.createInitialInput(
|
||||
data.initialValue,
|
||||
data.initialError,
|
||||
data.initialIsValid,
|
||||
)
|
||||
|
||||
assertThat(input1.hashCode()).isEqualTo(input2.hashCode())
|
||||
}
|
||||
|
||||
private fun Assert<InputField<Any>>.hasValue(value: Any) {
|
||||
prop("value") { InputField<*>::value.call(it) }.isEqualTo(value)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import app.k9mail.core.ui.compose.theme.ThunderbirdTheme
|
|||
import app.k9mail.feature.account.setup.ui.AccountSetupContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.AccountSetupContract.SetupStep
|
||||
import app.k9mail.feature.account.setup.ui.AccountSetupContract.State
|
||||
import app.k9mail.feature.account.setup.ui.incoming.FakeAccountIncomingConfigViewModel
|
||||
import app.k9mail.feature.account.setup.ui.options.FakeAccountOptionsViewModel
|
||||
import app.k9mail.feature.account.setup.ui.outgoing.FakeAccountOutgoingConfigViewModel
|
||||
import assertk.assertThat
|
||||
|
@ -21,6 +22,7 @@ class AccountSetupScreenKtTest : ComposeTest() {
|
|||
@Test
|
||||
fun `should display correct screen for every setup step`() = runTest {
|
||||
val viewModel = FakeAccountSetupViewModel()
|
||||
val incomingViewModel = FakeAccountIncomingConfigViewModel()
|
||||
val outgoingViewModel = FakeAccountOutgoingConfigViewModel()
|
||||
val optionsViewModel = FakeAccountOptionsViewModel()
|
||||
|
||||
|
@ -30,6 +32,7 @@ class AccountSetupScreenKtTest : ComposeTest() {
|
|||
onFinish = { },
|
||||
onBack = { },
|
||||
viewModel = viewModel,
|
||||
incomingViewModel = incomingViewModel,
|
||||
outgoingViewModel = outgoingViewModel,
|
||||
optionsViewModel = optionsViewModel,
|
||||
)
|
||||
|
@ -46,6 +49,7 @@ class AccountSetupScreenKtTest : ComposeTest() {
|
|||
fun `should delegate navigation effects`() = runTest {
|
||||
val initialState = State()
|
||||
val viewModel = FakeAccountSetupViewModel(initialState)
|
||||
val incomingViewModel = FakeAccountIncomingConfigViewModel()
|
||||
val outgoingViewModel = FakeAccountOutgoingConfigViewModel()
|
||||
val optionsViewModel = FakeAccountOptionsViewModel()
|
||||
var onFinishCounter = 0
|
||||
|
@ -57,6 +61,7 @@ class AccountSetupScreenKtTest : ComposeTest() {
|
|||
onFinish = { onFinishCounter++ },
|
||||
onBack = { onBackCounter++ },
|
||||
viewModel = viewModel,
|
||||
incomingViewModel = incomingViewModel,
|
||||
outgoingViewModel = outgoingViewModel,
|
||||
optionsViewModel = optionsViewModel,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
package app.k9mail.feature.account.setup.ui.incoming
|
||||
|
||||
import app.k9mail.core.ui.compose.testing.ComposeTest
|
||||
import app.k9mail.core.ui.compose.testing.setContent
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.State
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class AccountIncomingConfigScreenKtTest : ComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `should delegate navigation effects`() = runTest {
|
||||
val initialState = State()
|
||||
val viewModel = FakeAccountIncomingConfigViewModel(initialState)
|
||||
var onNextCounter = 0
|
||||
var onBackCounter = 0
|
||||
|
||||
setContent {
|
||||
AccountIncomingConfigScreen(
|
||||
onNext = { onNextCounter++ },
|
||||
onBack = { onBackCounter++ },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
|
||||
assertThat(onNextCounter).isEqualTo(0)
|
||||
assertThat(onBackCounter).isEqualTo(0)
|
||||
|
||||
viewModel.effect(Effect.NavigateNext)
|
||||
|
||||
assertThat(onNextCounter).isEqualTo(1)
|
||||
assertThat(onBackCounter).isEqualTo(0)
|
||||
|
||||
viewModel.effect(Effect.NavigateBack)
|
||||
|
||||
assertThat(onNextCounter).isEqualTo(1)
|
||||
assertThat(onBackCounter).isEqualTo(1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package app.k9mail.feature.account.setup.ui.incoming
|
||||
|
||||
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.toImapDefaultPort
|
||||
import app.k9mail.feature.account.setup.domain.input.NumberInputField
|
||||
import app.k9mail.feature.account.setup.domain.input.StringInputField
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.State
|
||||
import assertk.all
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isTrue
|
||||
import assertk.assertions.prop
|
||||
import org.junit.Test
|
||||
|
||||
class AccountIncomingConfigStateTest {
|
||||
|
||||
@Test
|
||||
fun `should set default values`() {
|
||||
val state = State()
|
||||
|
||||
assertThat(state).all {
|
||||
prop(State::protocolType).isEqualTo(IncomingProtocolType.IMAP)
|
||||
prop(State::server).isEqualTo(StringInputField())
|
||||
prop(State::security).isEqualTo(ConnectionSecurity.DEFAULT)
|
||||
prop(State::port).isEqualTo(NumberInputField(value = ConnectionSecurity.DEFAULT.toImapDefaultPort()))
|
||||
prop(State::username).isEqualTo(StringInputField())
|
||||
prop(State::password).isEqualTo(StringInputField())
|
||||
prop(State::clientCertificate).isEqualTo("")
|
||||
prop(State::imapAutodetectNamespaceEnabled).isTrue()
|
||||
prop(State::useCompression).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
package app.k9mail.feature.account.setup.ui.incoming
|
||||
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import app.cash.turbine.testIn
|
||||
import app.k9mail.core.ui.compose.testing.MainDispatcherRule
|
||||
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.toImapDefaultPort
|
||||
import app.k9mail.feature.account.setup.domain.entity.toPop3DefaultPort
|
||||
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.Event
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.State
|
||||
import assertk.assertions.assertThatAndTurbinesConsumed
|
||||
import assertk.assertions.isEqualTo
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class AccountIncomingConfigViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
private val testSubject = AccountIncomingConfigViewModel()
|
||||
|
||||
@Test
|
||||
fun `should change protocol, security and port when ProtocolTypeChanged event is received`() = runTest {
|
||||
val initialState = State(
|
||||
security = ConnectionSecurity.StartTLS,
|
||||
port = NumberInputField(value = ConnectionSecurity.StartTLS.toImapDefaultPort()),
|
||||
)
|
||||
testSubject.initState(initialState)
|
||||
|
||||
eventStateTest(
|
||||
viewModel = testSubject,
|
||||
initialState = initialState,
|
||||
event = Event.ProtocolTypeChanged(IncomingProtocolType.POP3),
|
||||
expectedState = State(
|
||||
protocolType = IncomingProtocolType.POP3,
|
||||
security = ConnectionSecurity.TLS,
|
||||
port = NumberInputField(value = ConnectionSecurity.TLS.toPop3DefaultPort()),
|
||||
),
|
||||
coroutineScope = backgroundScope,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should change state when ServerChanged event is received`() = runTest {
|
||||
eventStateTest(
|
||||
viewModel = testSubject,
|
||||
initialState = State(),
|
||||
event = Event.ServerChanged("server"),
|
||||
expectedState = State(server = StringInputField(value = "server")),
|
||||
coroutineScope = backgroundScope,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should change security and port when SecurityChanged event is received`() = runTest {
|
||||
eventStateTest(
|
||||
viewModel = testSubject,
|
||||
initialState = State(),
|
||||
event = Event.SecurityChanged(ConnectionSecurity.StartTLS),
|
||||
expectedState = State(
|
||||
security = ConnectionSecurity.StartTLS,
|
||||
port = NumberInputField(value = ConnectionSecurity.StartTLS.toImapDefaultPort()),
|
||||
),
|
||||
coroutineScope = backgroundScope,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should change state when PortChanged event is received`() = runTest {
|
||||
eventStateTest(
|
||||
viewModel = testSubject,
|
||||
initialState = State(),
|
||||
event = Event.PortChanged(456L),
|
||||
expectedState = State(port = NumberInputField(value = 456L)),
|
||||
coroutineScope = backgroundScope,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should change state when UsernameChanged event is received`() = runTest {
|
||||
eventStateTest(
|
||||
viewModel = testSubject,
|
||||
initialState = State(),
|
||||
event = Event.UsernameChanged("username"),
|
||||
expectedState = State(username = StringInputField(value = "username")),
|
||||
coroutineScope = backgroundScope,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should change state when PasswordChanged event is received`() = runTest {
|
||||
eventStateTest(
|
||||
viewModel = testSubject,
|
||||
initialState = State(),
|
||||
event = Event.PasswordChanged("password"),
|
||||
expectedState = State(password = StringInputField(value = "password")),
|
||||
coroutineScope = backgroundScope,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should change state when ClientCertificateChanged event is received`() = runTest {
|
||||
eventStateTest(
|
||||
viewModel = testSubject,
|
||||
initialState = State(),
|
||||
event = Event.ClientCertificateChanged("clientCertificate"),
|
||||
expectedState = State(clientCertificate = "clientCertificate"),
|
||||
coroutineScope = backgroundScope,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should change state when ImapAutoDetectNamespaceChanged event is received`() = runTest {
|
||||
eventStateTest(
|
||||
viewModel = testSubject,
|
||||
initialState = State(imapAutodetectNamespaceEnabled = true),
|
||||
event = Event.ImapAutoDetectNamespaceChanged(false),
|
||||
expectedState = State(imapAutodetectNamespaceEnabled = false),
|
||||
coroutineScope = backgroundScope,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should change state when ImapPrefixChanged event is received`() = runTest {
|
||||
eventStateTest(
|
||||
viewModel = testSubject,
|
||||
initialState = State(),
|
||||
event = Event.ImapPrefixChanged("imapPrefix"),
|
||||
expectedState = State(imapPrefix = StringInputField(value = "imapPrefix")),
|
||||
coroutineScope = backgroundScope,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should change state when UseCompressionChanged event is received`() = runTest {
|
||||
eventStateTest(
|
||||
viewModel = testSubject,
|
||||
initialState = State(useCompression = true),
|
||||
event = Event.UseCompressionChanged(false),
|
||||
expectedState = State(useCompression = false),
|
||||
coroutineScope = backgroundScope,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit NavigateNext effect when OnNextClicked event received`() = runTest {
|
||||
val viewModel = testSubject
|
||||
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)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = effectTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(Effect.NavigateNext)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit NavigateBack effect when OnBackClicked event received`() = runTest {
|
||||
val viewModel = testSubject
|
||||
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.OnBackClicked)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = effectTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(Effect.NavigateBack)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package app.k9mail.feature.account.setup.ui.incoming
|
||||
|
||||
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
|
||||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract.Effect
|
||||
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.ViewModel
|
||||
|
||||
class FakeAccountIncomingConfigViewModel(
|
||||
initialState: State = State(),
|
||||
) : BaseViewModel<State, Event, Effect>(initialState), ViewModel {
|
||||
|
||||
val events = mutableListOf<Event>()
|
||||
|
||||
override fun initState(state: State) {
|
||||
updateState { state }
|
||||
}
|
||||
|
||||
override fun event(event: Event) {
|
||||
events.add(event)
|
||||
}
|
||||
|
||||
fun effect(effect: Effect) {
|
||||
emitEffect(effect)
|
||||
}
|
||||
}
|
|
@ -6,9 +6,11 @@ import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContrac
|
|||
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract.State
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class AccountOutgoingConfigScreenKtTest : ComposeTest() {
|
||||
|
||||
@Test
|
||||
|
|
Loading…
Reference in a new issue