Change ViewModel orchestration in account setup

Using more direct dependencies makes it easier to follow what's going on.
This commit is contained in:
cketti 2023-07-30 15:38:07 +02:00
parent a233a63bf4
commit cb6520efd2
7 changed files with 214 additions and 168 deletions

View file

@ -82,20 +82,26 @@ val featureAccountSetupModule: Module = module {
viewModel {
AccountSetupViewModel(
createAccount = get(),
autoDiscoveryViewModel = get(),
incomingViewModel = get(),
incomingValidationViewModel = get(named(NAME_INCOMING_VALIDATION)),
outgoingViewModel = get(),
outgoingValidationViewModel = get(named(NAME_OUTGOING_VALIDATION)),
optionsViewModel = get(),
)
}
viewModel {
factory<AccountAutoDiscoveryContract.ViewModel> {
AccountAutoDiscoveryViewModel(
validator = get(),
getAutoDiscovery = get(),
)
}
viewModel {
factory<AccountIncomingConfigContract.ViewModel> {
AccountIncomingConfigViewModel(
validator = get(),
)
}
viewModel(named(NAME_INCOMING_VALIDATION)) {
factory<AccountValidationContract.ViewModel>(named(NAME_INCOMING_VALIDATION)) {
AccountValidationViewModel(
validateServerSettings = get(),
initialState = AccountValidationContract.State(
@ -103,12 +109,12 @@ val featureAccountSetupModule: Module = module {
),
)
}
viewModel {
factory<AccountOutgoingConfigContract.ViewModel> {
AccountOutgoingConfigViewModel(
validator = get(),
)
}
viewModel(named(NAME_OUTGOING_VALIDATION)) {
factory<AccountValidationContract.ViewModel>(named(NAME_OUTGOING_VALIDATION)) {
AccountValidationViewModel(
validateServerSettings = get(),
initialState = AccountValidationContract.State(
@ -116,7 +122,7 @@ val featureAccountSetupModule: Module = module {
),
)
}
viewModel {
factory<AccountOptionsContract.ViewModel> {
AccountOptionsViewModel(
validator = get(),
)

View file

@ -5,6 +5,7 @@ import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryCon
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract
interface AccountSetupContract {
@ -17,7 +18,14 @@ interface AccountSetupContract {
OPTIONS,
}
interface ViewModel : UnidirectionalViewModel<State, Event, Effect>
interface ViewModel : UnidirectionalViewModel<State, Event, Effect> {
val autoDiscoveryViewModel: AccountAutoDiscoveryContract.ViewModel
val incomingViewModel: AccountIncomingConfigContract.ViewModel
val incomingValidationViewModel: AccountValidationContract.ViewModel
val outgoingViewModel: AccountOutgoingConfigContract.ViewModel
val outgoingValidationViewModel: AccountValidationContract.ViewModel
val optionsViewModel: AccountOptionsContract.ViewModel
}
data class State(
val setupStep: SetupStep = SetupStep.AUTO_CONFIG,
@ -32,36 +40,11 @@ interface AccountSetupContract {
val isAutomaticConfig: Boolean,
) : Event
data class OnStateCollected(
val autoDiscoveryState: AccountAutoDiscoveryContract.State,
val incomingState: AccountIncomingConfigContract.State,
val outgoingState: AccountOutgoingConfigContract.State,
val optionsState: AccountOptionsContract.State,
) : Event
object OnBack : Event
}
sealed interface Effect {
data class UpdateIncomingConfig(
val state: AccountIncomingConfigContract.State,
) : Effect
object UpdateIncomingConfigValidation : Effect
data class UpdateOutgoingConfig(
val state: AccountOutgoingConfigContract.State,
) : Effect
object UpdateOutgoingConfigValidation : Effect
data class UpdateOptions(
val state: AccountOptionsContract.State,
) : Effect
object CollectExternalStates : Effect
data class NavigateNext(
val accountUuid: String,
) : Effect

View file

@ -3,75 +3,26 @@ package app.k9mail.feature.account.setup.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.common.mvi.observe
import app.k9mail.feature.account.setup.NAME_INCOMING_VALIDATION
import app.k9mail.feature.account.setup.NAME_OUTGOING_VALIDATION
import app.k9mail.feature.account.setup.ui.AccountSetupContract.Effect
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.autodiscovery.AccountAutoDiscoveryContract
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryScreen
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryViewModel
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.incoming.toValidationState
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
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigScreen
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigViewModel
import app.k9mail.feature.account.setup.ui.outgoing.toValidationState
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract
import app.k9mail.feature.account.setup.ui.validation.AccountValidationScreen
import app.k9mail.feature.account.setup.ui.validation.AccountValidationViewModel
import org.koin.androidx.compose.koinViewModel
import org.koin.core.qualifier.named
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Suppress("LongMethod")
@Composable
fun AccountSetupScreen(
onFinish: (String) -> Unit,
onBack: () -> Unit,
viewModel: ViewModel = koinViewModel<AccountSetupViewModel>(),
autoDiscoveryViewModel: AccountAutoDiscoveryContract.ViewModel = koinViewModel<AccountAutoDiscoveryViewModel>(),
incomingViewModel: AccountIncomingConfigContract.ViewModel = koinViewModel<AccountIncomingConfigViewModel>(),
incomingValidationViewModel: AccountValidationContract.ViewModel = koinViewModel<AccountValidationViewModel>(
named(
NAME_INCOMING_VALIDATION,
),
),
outgoingViewModel: AccountOutgoingConfigContract.ViewModel = koinViewModel<AccountOutgoingConfigViewModel>(),
outgoingValidationViewModel: AccountValidationContract.ViewModel = koinViewModel<AccountValidationViewModel>(
named(
NAME_OUTGOING_VALIDATION,
),
),
optionsViewModel: AccountOptionsContract.ViewModel = koinViewModel<AccountOptionsViewModel>(),
) {
val (state, dispatch) = viewModel.observe { effect ->
when (effect) {
is Effect.UpdateIncomingConfig -> incomingViewModel.initState(effect.state)
is Effect.UpdateIncomingConfigValidation -> {
incomingValidationViewModel.initState(incomingViewModel.state.value.toValidationState())
}
is Effect.UpdateOutgoingConfig -> outgoingViewModel.initState(effect.state)
is Effect.UpdateOutgoingConfigValidation -> {
outgoingValidationViewModel.initState(outgoingViewModel.state.value.toValidationState())
}
is Effect.UpdateOptions -> optionsViewModel.initState(effect.state)
is Effect.CollectExternalStates -> viewModel.event(
Event.OnStateCollected(
autoDiscoveryState = autoDiscoveryViewModel.state.value,
incomingState = incomingViewModel.state.value,
outgoingState = outgoingViewModel.state.value,
optionsState = optionsViewModel.state.value,
),
)
is Effect.NavigateNext -> onFinish(effect.accountUuid)
Effect.NavigateBack -> onBack()
}
@ -89,7 +40,7 @@ fun AccountSetupScreen(
)
},
onBack = { dispatch(Event.OnBack) },
viewModel = autoDiscoveryViewModel,
viewModel = viewModel.autoDiscoveryViewModel,
)
}
@ -97,7 +48,7 @@ fun AccountSetupScreen(
AccountIncomingConfigScreen(
onNext = { dispatch(Event.OnNext) },
onBack = { dispatch(Event.OnBack) },
viewModel = incomingViewModel,
viewModel = viewModel.incomingViewModel,
)
}
@ -105,7 +56,7 @@ fun AccountSetupScreen(
AccountValidationScreen(
onNext = { dispatch(Event.OnNext) },
onBack = { dispatch(Event.OnBack) },
viewModel = incomingValidationViewModel,
viewModel = viewModel.incomingValidationViewModel,
)
}
@ -113,7 +64,7 @@ fun AccountSetupScreen(
AccountOutgoingConfigScreen(
onNext = { dispatch(Event.OnNext) },
onBack = { dispatch(Event.OnBack) },
viewModel = outgoingViewModel,
viewModel = viewModel.outgoingViewModel,
)
}
@ -121,7 +72,7 @@ fun AccountSetupScreen(
AccountValidationScreen(
onNext = { dispatch(Event.OnNext) },
onBack = { dispatch(Event.OnBack) },
viewModel = outgoingValidationViewModel,
viewModel = viewModel.outgoingValidationViewModel,
)
}
@ -129,7 +80,7 @@ fun AccountSetupScreen(
AccountOptionsScreen(
onNext = { dispatch(Event.OnNext) },
onBack = { dispatch(Event.OnBack) },
viewModel = optionsViewModel,
viewModel = viewModel.optionsViewModel,
)
}
}

View file

@ -7,23 +7,31 @@ import app.k9mail.feature.account.setup.ui.AccountSetupContract.Effect
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.State
import app.k9mail.feature.account.setup.ui.AccountSetupContract.ViewModel
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract
import app.k9mail.feature.account.setup.ui.common.mapper.toIncomingConfigState
import app.k9mail.feature.account.setup.ui.common.mapper.toOptionsState
import app.k9mail.feature.account.setup.ui.common.mapper.toOutgoingConfigState
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract
import app.k9mail.feature.account.setup.ui.incoming.toServerSettings
import app.k9mail.feature.account.setup.ui.incoming.toValidationState
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract
import app.k9mail.feature.account.setup.ui.options.toAccountOptions
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract
import app.k9mail.feature.account.setup.ui.outgoing.toServerSettings
import app.k9mail.feature.account.setup.ui.outgoing.toValidationState
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract
import kotlinx.coroutines.launch
class AccountSetupViewModel(
private val createAccount: UseCase.CreateAccount,
override val autoDiscoveryViewModel: AccountAutoDiscoveryContract.ViewModel,
override val incomingViewModel: AccountIncomingConfigContract.ViewModel,
override val incomingValidationViewModel: AccountValidationContract.ViewModel,
override val outgoingViewModel: AccountOutgoingConfigContract.ViewModel,
override val outgoingValidationViewModel: AccountValidationContract.ViewModel,
override val optionsViewModel: AccountOptionsContract.ViewModel,
initialState: State = State(),
) : BaseViewModel<State, Event, Effect>(initialState), ViewModel {
) : BaseViewModel<State, Event, Effect>(initialState), AccountSetupContract.ViewModel {
override fun event(event: Event) {
when (event) {
@ -36,13 +44,6 @@ class AccountSetupViewModel(
onAutoDiscoveryFinished(event.state)
}
is Event.OnStateCollected -> onStateCollected(
autoDiscoveryState = event.autoDiscoveryState,
incomingState = event.incomingState,
outgoingState = event.outgoingState,
optionsState = event.optionsState,
)
Event.OnBack -> onBack()
Event.OnNext -> onNext()
}
@ -51,9 +52,9 @@ class AccountSetupViewModel(
private fun onAutoDiscoveryFinished(
autoDiscoveryState: AccountAutoDiscoveryContract.State,
) {
emitEffect(Effect.UpdateIncomingConfig(autoDiscoveryState.toIncomingConfigState()))
emitEffect(Effect.UpdateOutgoingConfig(autoDiscoveryState.toOutgoingConfigState()))
emitEffect(Effect.UpdateOptions(autoDiscoveryState.toOptionsState()))
incomingViewModel.initState(autoDiscoveryState.toIncomingConfigState())
outgoingViewModel.initState(autoDiscoveryState.toOutgoingConfigState())
optionsViewModel.initState(autoDiscoveryState.toOptionsState())
onNext()
}
@ -86,8 +87,8 @@ class AccountSetupViewModel(
when (state.value.setupStep) {
SetupStep.AUTO_CONFIG -> {
if (state.value.isAutomaticConfig) {
emitEffect(Effect.UpdateIncomingConfigValidation)
emitEffect(Effect.UpdateOutgoingConfigValidation)
incomingValidationViewModel.initState(incomingViewModel.state.value.toValidationState())
outgoingValidationViewModel.initState(outgoingViewModel.state.value.toValidationState())
changeToSetupStep(SetupStep.INCOMING_VALIDATION)
} else {
changeToSetupStep(SetupStep.INCOMING_CONFIG)
@ -95,7 +96,7 @@ class AccountSetupViewModel(
}
SetupStep.INCOMING_CONFIG -> {
emitEffect(Effect.UpdateIncomingConfigValidation)
incomingValidationViewModel.initState(incomingViewModel.state.value.toValidationState())
changeToSetupStep(SetupStep.INCOMING_VALIDATION)
}
@ -108,7 +109,7 @@ class AccountSetupViewModel(
}
SetupStep.OUTGOING_CONFIG -> {
emitEffect(Effect.UpdateOutgoingConfigValidation)
outgoingValidationViewModel.initState(outgoingViewModel.state.value.toValidationState())
changeToSetupStep(SetupStep.OUTGOING_VALIDATION)
}
@ -129,15 +130,11 @@ class AccountSetupViewModel(
}
private fun onFinish() {
emitEffect(Effect.CollectExternalStates)
}
val autoDiscoveryState = autoDiscoveryViewModel.state.value
val incomingState = incomingViewModel.state.value
val outgoingState = outgoingViewModel.state.value
val optionsState = optionsViewModel.state.value
private fun onStateCollected(
autoDiscoveryState: AccountAutoDiscoveryContract.State,
incomingState: AccountIncomingConfigContract.State,
outgoingState: AccountOutgoingConfigContract.State,
optionsState: AccountOptionsContract.State,
) {
viewModelScope.launch {
val result = createAccount.execute(
emailAddress = autoDiscoveryState.emailAddress.value,

View file

@ -7,11 +7,6 @@ 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.autodiscovery.FakeAccountAutoDiscoveryViewModel
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 app.k9mail.feature.account.setup.ui.validation.FakeAccountValidationViewModel
import assertk.assertThat
import assertk.assertions.isEqualTo
import kotlinx.coroutines.test.runTest
@ -22,12 +17,6 @@ class AccountSetupScreenKtTest : ComposeTest() {
@Test
fun `should display correct screen for every setup step`() = runTest {
val viewModel = FakeAccountSetupViewModel()
val autoDiscoveryViewModel = FakeAccountAutoDiscoveryViewModel()
val incomingViewModel = FakeAccountIncomingConfigViewModel()
val incomingValidationViewModel = FakeAccountValidationViewModel()
val outgoingViewModel = FakeAccountOutgoingConfigViewModel()
val outgoingValidationViewModel = FakeAccountValidationViewModel()
val optionsViewModel = FakeAccountOptionsViewModel()
setContent {
ThunderbirdTheme {
@ -35,12 +24,6 @@ class AccountSetupScreenKtTest : ComposeTest() {
onFinish = { },
onBack = { },
viewModel = viewModel,
autoDiscoveryViewModel = autoDiscoveryViewModel,
incomingViewModel = incomingViewModel,
incomingValidationViewModel = incomingValidationViewModel,
outgoingViewModel = outgoingViewModel,
outgoingValidationViewModel = outgoingValidationViewModel,
optionsViewModel = optionsViewModel,
)
}
}
@ -54,13 +37,7 @@ class AccountSetupScreenKtTest : ComposeTest() {
@Test
fun `should delegate navigation effects`() = runTest {
val initialState = State()
val viewModel = FakeAccountSetupViewModel(initialState)
val autoDiscoveryViewModel = FakeAccountAutoDiscoveryViewModel()
val incomingViewModel = FakeAccountIncomingConfigViewModel()
val incomingValidationViewModel = FakeAccountValidationViewModel()
val outgoingViewModel = FakeAccountOutgoingConfigViewModel()
val outgoingValidationViewModel = FakeAccountValidationViewModel()
val optionsViewModel = FakeAccountOptionsViewModel()
val viewModel = FakeAccountSetupViewModel(initialState = initialState)
var onFinishCounter = 0
var onBackCounter = 0
@ -70,12 +47,6 @@ class AccountSetupScreenKtTest : ComposeTest() {
onFinish = { onFinishCounter++ },
onBack = { onBackCounter++ },
viewModel = viewModel,
autoDiscoveryViewModel = autoDiscoveryViewModel,
incomingViewModel = incomingViewModel,
incomingValidationViewModel = incomingValidationViewModel,
outgoingViewModel = outgoingViewModel,
outgoingValidationViewModel = outgoingValidationViewModel,
optionsViewModel = optionsViewModel,
)
}
}

View file

@ -1,32 +1,81 @@
package app.k9mail.feature.account.setup.ui
import app.cash.turbine.testIn
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
import app.k9mail.autodiscovery.api.ImapServerSettings
import app.k9mail.autodiscovery.api.SmtpServerSettings
import app.k9mail.core.common.net.toHostname
import app.k9mail.core.common.net.toPort
import app.k9mail.core.ui.compose.testing.MainDispatcherRule
import app.k9mail.feature.account.setup.domain.entity.AccountOptions
import app.k9mail.feature.account.setup.domain.entity.AuthenticationType
import app.k9mail.feature.account.setup.domain.entity.ConnectionSecurity
import app.k9mail.feature.account.setup.domain.entity.EmailCheckFrequency
import app.k9mail.feature.account.setup.domain.entity.EmailDisplayCount
import app.k9mail.feature.account.setup.domain.entity.IncomingProtocolType
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.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.autodiscovery.AccountAutoDiscoveryContract
import app.k9mail.feature.account.setup.ui.autodiscovery.FakeAccountAutoDiscoveryViewModel
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract
import app.k9mail.feature.account.setup.ui.incoming.FakeAccountIncomingConfigViewModel
import app.k9mail.feature.account.setup.ui.incoming.toServerSettings
import app.k9mail.feature.account.setup.ui.incoming.toValidationState
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract
import app.k9mail.feature.account.setup.ui.options.FakeAccountOptionsViewModel
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract
import app.k9mail.feature.account.setup.ui.outgoing.FakeAccountOutgoingConfigViewModel
import app.k9mail.feature.account.setup.ui.outgoing.toServerSettings
import app.k9mail.feature.account.setup.ui.outgoing.toValidationState
import app.k9mail.feature.account.setup.ui.validation.FakeAccountValidationViewModel
import assertk.assertThat
import assertk.assertions.assertThatAndTurbinesConsumed
import assertk.assertions.isEqualTo
import assertk.assertions.prop
import com.fsck.k9.mail.ServerSettings
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import app.k9mail.autodiscovery.api.AuthenticationType as AutoDiscoveryAuthenticationType
import app.k9mail.autodiscovery.api.ConnectionSecurity as AutoDiscoveryConnectionSecurity
@Suppress("LongMethod")
class AccountSetupViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@Suppress("LongMethod")
private val autoDiscoveryViewModel = FakeAccountAutoDiscoveryViewModel()
private val incomingViewModel = FakeAccountIncomingConfigViewModel()
private val incomingValidationViewModel = FakeAccountValidationViewModel()
private val outgoingViewModel = FakeAccountOutgoingConfigViewModel()
private val outgoingValidationViewModel = FakeAccountValidationViewModel()
private val optionsViewModel = FakeAccountOptionsViewModel()
@Test
fun `should forward step state on next event`() = runTest {
var createAccountEmailAddress: String? = null
var createAccountIncomingServerSettings: ServerSettings? = null
var createAccountOutgoingServerSettings: ServerSettings? = null
var createAccountOptions: AccountOptions? = null
val viewModel = AccountSetupViewModel(
createAccount = { _, _, _, _ -> "accountUuid" },
createAccount = { emailAddress, incomingServerSettings, outgoingServerSettings, options ->
createAccountEmailAddress = emailAddress
createAccountIncomingServerSettings = incomingServerSettings
createAccountOutgoingServerSettings = outgoingServerSettings
createAccountOptions = options
"accountUuid"
},
autoDiscoveryViewModel = autoDiscoveryViewModel,
incomingViewModel = incomingViewModel,
incomingValidationViewModel = incomingValidationViewModel,
outgoingViewModel = outgoingViewModel,
outgoingValidationViewModel = outgoingValidationViewModel,
optionsViewModel = optionsViewModel,
)
val stateTurbine = viewModel.state.testIn(backgroundScope)
val effectTurbine = viewModel.effect.testIn(backgroundScope)
@ -40,21 +89,40 @@ class AccountSetupViewModelTest {
prop(State::setupStep).isEqualTo(SetupStep.AUTO_CONFIG)
}
autoDiscoveryViewModel.initState(AUTODISCOVERY_STATE)
viewModel.event(
AccountSetupContract.Event.OnAutoDiscoveryFinished(
state = AccountAutoDiscoveryContract.State(),
state = AUTODISCOVERY_STATE,
isAutomaticConfig = false,
),
)
assertThat(effectTurbine.awaitItem())
.isEqualTo(Effect.UpdateIncomingConfig(AccountIncomingConfigContract.State()))
val expectedIncomingConfigState = AccountIncomingConfigContract.State(
protocolType = IncomingProtocolType.IMAP,
server = StringInputField(INCOMING_SERVER_NAME),
security = ConnectionSecurity.TLS,
port = NumberInputField(INCOMING_SERVER_PORT.toLong()),
authenticationType = AuthenticationType.PasswordEncrypted,
username = StringInputField(USERNAME),
password = StringInputField(PASSWORD),
)
assertThat(incomingViewModel.state.value).isEqualTo(expectedIncomingConfigState)
assertThat(effectTurbine.awaitItem())
.isEqualTo(Effect.UpdateOutgoingConfig(AccountOutgoingConfigContract.State()))
val expectedOutgoingConfigState = AccountOutgoingConfigContract.State(
server = StringInputField(OUTGOING_SERVER_NAME),
security = ConnectionSecurity.TLS,
port = NumberInputField(OUTGOING_SERVER_PORT.toLong()),
authenticationType = AuthenticationType.PasswordEncrypted,
username = StringInputField(USERNAME),
password = StringInputField(PASSWORD),
)
assertThat(outgoingViewModel.state.value).isEqualTo(expectedOutgoingConfigState)
assertThat(effectTurbine.awaitItem())
.isEqualTo(Effect.UpdateOptions(AccountOptionsContract.State()))
assertThat(optionsViewModel.state.value).isEqualTo(
AccountOptionsContract.State(
accountName = StringInputField(EMAIL_ADDRESS),
),
)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
@ -65,7 +133,7 @@ class AccountSetupViewModelTest {
viewModel.event(AccountSetupContract.Event.OnNext)
assertThat(effectTurbine.awaitItem()).isEqualTo(Effect.UpdateIncomingConfigValidation)
assertThat(incomingValidationViewModel.state.value).isEqualTo(expectedIncomingConfigState.toValidationState())
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
@ -85,7 +153,7 @@ class AccountSetupViewModelTest {
viewModel.event(AccountSetupContract.Event.OnNext)
assertThat(effectTurbine.awaitItem()).isEqualTo(Effect.UpdateOutgoingConfigValidation)
assertThat(outgoingValidationViewModel.state.value).isEqualTo(expectedOutgoingConfigState.toValidationState())
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
@ -103,30 +171,39 @@ class AccountSetupViewModelTest {
prop(State::setupStep).isEqualTo(SetupStep.OPTIONS)
}
viewModel.event(AccountSetupContract.Event.OnNext)
assertThatAndTurbinesConsumed(
actual = effectTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(Effect.CollectExternalStates)
}
viewModel.event(
AccountSetupContract.Event.OnStateCollected(
autoDiscoveryState = AccountAutoDiscoveryContract.State(),
incomingState = AccountIncomingConfigContract.State(),
outgoingState = AccountOutgoingConfigContract.State(),
optionsState = AccountOptionsContract.State(),
optionsViewModel.initState(
optionsViewModel.state.value.copy(
accountName = StringInputField("account name"),
displayName = StringInputField("display name"),
emailSignature = StringInputField("signature"),
checkFrequency = EmailCheckFrequency.EVERY_15_MINUTES,
messageDisplayCount = EmailDisplayCount.MESSAGES_100,
showNotification = true,
),
)
viewModel.event(AccountSetupContract.Event.OnNext)
assertThatAndTurbinesConsumed(
actual = effectTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(Effect.NavigateNext("accountUuid"))
}
assertThat(createAccountEmailAddress).isEqualTo(EMAIL_ADDRESS)
assertThat(createAccountIncomingServerSettings).isEqualTo(expectedIncomingConfigState.toServerSettings())
assertThat(createAccountOutgoingServerSettings).isEqualTo(expectedOutgoingConfigState.toServerSettings())
assertThat(createAccountOptions).isEqualTo(
AccountOptions(
accountName = "account name",
displayName = "display name",
emailSignature = "signature",
checkFrequencyInMinutes = 15,
messageDisplayCount = 100,
showNotification = true,
),
)
}
@Test
@ -134,6 +211,12 @@ class AccountSetupViewModelTest {
val initialState = State(setupStep = SetupStep.OPTIONS)
val viewModel = AccountSetupViewModel(
createAccount = { _, _, _, _ -> "accountUuid" },
autoDiscoveryViewModel = autoDiscoveryViewModel,
incomingViewModel = FakeAccountIncomingConfigViewModel(),
incomingValidationViewModel = FakeAccountValidationViewModel(),
outgoingViewModel = FakeAccountOutgoingConfigViewModel(),
outgoingValidationViewModel = FakeAccountValidationViewModel(),
optionsViewModel = FakeAccountOptionsViewModel(),
initialState = initialState,
)
val stateTurbine = viewModel.state.testIn(backgroundScope)
@ -202,4 +285,44 @@ class AccountSetupViewModelTest {
isEqualTo(Effect.NavigateBack)
}
}
companion object {
private const val EMAIL_ADDRESS = "test@domain.example"
private const val USERNAME = EMAIL_ADDRESS
private const val PASSWORD = "password"
private const val INCOMING_SERVER_NAME = "imap.domain.example"
private const val INCOMING_SERVER_PORT = 993
private const val OUTGOING_SERVER_NAME = "smtp.domain.example"
private const val OUTGOING_SERVER_PORT = 465
private val INCOMING_SERVER_SETTINGS = ImapServerSettings(
hostname = INCOMING_SERVER_NAME.toHostname(),
port = INCOMING_SERVER_PORT.toPort(),
connectionSecurity = AutoDiscoveryConnectionSecurity.TLS,
authenticationTypes = listOf(AutoDiscoveryAuthenticationType.PasswordEncrypted),
username = USERNAME,
)
private val OUTGOING_SERVER_SETTINGS = SmtpServerSettings(
hostname = OUTGOING_SERVER_NAME.toHostname(),
port = OUTGOING_SERVER_PORT.toPort(),
connectionSecurity = AutoDiscoveryConnectionSecurity.TLS,
authenticationTypes = listOf(AutoDiscoveryAuthenticationType.PasswordEncrypted),
username = USERNAME,
)
private val AUTODISCOVERY_RESULT = AutoDiscoveryResult.Settings(
incomingServerSettings = INCOMING_SERVER_SETTINGS,
outgoingServerSettings = OUTGOING_SERVER_SETTINGS,
isTrusted = true,
source = "test",
)
private val AUTODISCOVERY_STATE = AccountAutoDiscoveryContract.State(
configStep = AccountAutoDiscoveryContract.ConfigStep.PASSWORD,
emailAddress = StringInputField(EMAIL_ADDRESS),
password = StringInputField(PASSWORD),
autoDiscoverySettings = AUTODISCOVERY_RESULT,
)
}
}

View file

@ -4,8 +4,23 @@ import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
import app.k9mail.feature.account.setup.ui.AccountSetupContract.Effect
import app.k9mail.feature.account.setup.ui.AccountSetupContract.Event
import app.k9mail.feature.account.setup.ui.AccountSetupContract.State
import app.k9mail.feature.account.setup.ui.autodiscovery.FakeAccountAutoDiscoveryViewModel
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract
import app.k9mail.feature.account.setup.ui.incoming.FakeAccountIncomingConfigViewModel
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract
import app.k9mail.feature.account.setup.ui.options.FakeAccountOptionsViewModel
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract
import app.k9mail.feature.account.setup.ui.outgoing.FakeAccountOutgoingConfigViewModel
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract
import app.k9mail.feature.account.setup.ui.validation.FakeAccountValidationViewModel
internal class FakeAccountSetupViewModel(
override val autoDiscoveryViewModel: FakeAccountAutoDiscoveryViewModel = FakeAccountAutoDiscoveryViewModel(),
override val incomingViewModel: AccountIncomingConfigContract.ViewModel = FakeAccountIncomingConfigViewModel(),
override val incomingValidationViewModel: AccountValidationContract.ViewModel = FakeAccountValidationViewModel(),
override val outgoingViewModel: AccountOutgoingConfigContract.ViewModel = FakeAccountOutgoingConfigViewModel(),
override val outgoingValidationViewModel: AccountValidationContract.ViewModel = FakeAccountValidationViewModel(),
override val optionsViewModel: AccountOptionsContract.ViewModel = FakeAccountOptionsViewModel(),
initialState: State = State(),
) : BaseViewModel<State, Event, Effect>(initialState), AccountSetupContract.ViewModel {