Add AccountIncomingConfigContract and ViewModel

This commit is contained in:
Wolf-Martell Montwé 2023-06-02 18:29:22 +02:00
parent 2b436e7bc2
commit e04bc10729
No known key found for this signature in database
GPG key ID: 6D45B21512ACBF72
13 changed files with 479 additions and 33 deletions

View file

@ -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(),

View file

@ -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,
)
}

View file

@ -29,12 +29,16 @@ 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,
) {
@ -61,8 +65,8 @@ internal fun AccountIncomingConfigContent(
item {
SelectInput(
options = IncomingProtocolType.all(),
selectedOption = IncomingProtocolType.DEFAULT,
onOptionChange = { },
selectedOption = state.protocolType,
onOptionChange = { onEvent(Event.ProtocolTypeChanged(it)) },
label = stringResource(id = R.string.account_setup_incoming_config_protocol_type_label),
contentPadding = defaultItemPadding(),
)
@ -70,8 +74,8 @@ internal fun AccountIncomingConfigContent(
item {
TextInput(
text = "",
onTextChange = { },
text = state.server.value,
onTextChange = { onEvent(Event.ServerChanged(it)) },
label = stringResource(id = R.string.account_setup_incoming_config_server_label),
contentPadding = defaultItemPadding(),
)
@ -81,8 +85,8 @@ internal fun AccountIncomingConfigContent(
SelectInput(
options = ConnectionSecurity.all(),
optionToStringTransformation = { it.toResourceString(resources) },
selectedOption = ConnectionSecurity.DEFAULT,
onOptionChange = { },
selectedOption = state.security,
onOptionChange = { onEvent(Event.SecurityChanged(it)) },
label = stringResource(id = R.string.account_setup_incoming_config_security_label),
contentPadding = defaultItemPadding(),
)
@ -90,8 +94,8 @@ internal fun AccountIncomingConfigContent(
item {
NumberInput(
value = null,
onValueChange = { },
value = state.port.value,
onValueChange = { onEvent(Event.PortChanged(it)) },
label = stringResource(id = R.string.account_setup_outgoing_config_port_label),
contentPadding = defaultItemPadding(),
)
@ -99,8 +103,8 @@ internal fun AccountIncomingConfigContent(
item {
TextInput(
text = "",
onTextChange = { },
text = state.username.value,
onTextChange = { onEvent(Event.UsernameChanged(it)) },
label = stringResource(id = R.string.account_setup_outgoing_config_username_label),
contentPadding = defaultItemPadding(),
)
@ -108,8 +112,8 @@ internal fun AccountIncomingConfigContent(
item {
PasswordInput(
password = "",
onPasswordChange = { },
password = state.password.value,
onPasswordChange = { onEvent(Event.PasswordChanged(it)) },
contentPadding = defaultItemPadding(),
)
}
@ -126,35 +130,37 @@ internal fun AccountIncomingConfigContent(
selectedOption = stringResource(
id = R.string.account_setup_client_certificate_none_available,
),
onOptionChange = { },
onOptionChange = { onEvent(Event.ClientCertificateChanged(it)) },
label = stringResource(id = R.string.account_setup_outgoing_config_client_certificate_label),
contentPadding = defaultItemPadding(),
)
}
item {
CheckboxInput(
text = stringResource(id = R.string.account_setup_incoming_config_imap_namespace_label),
checked = true,
onCheckedChange = { },
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 = "",
onTextChange = { },
label = stringResource(id = R.string.account_setup_incoming_config_imap_prefix_label),
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 = true,
onCheckedChange = { },
checked = state.useCompression,
onCheckedChange = { onEvent(Event.UseCompressionChanged(it)) },
contentPadding = defaultItemPadding(),
)
}
@ -167,6 +173,8 @@ internal fun AccountIncomingConfigContent(
internal fun AccountIncomingConfigContentK9Preview() {
K9Theme {
AccountIncomingConfigContent(
onEvent = { },
state = State(),
contentPadding = PaddingValues(),
)
}
@ -177,6 +185,8 @@ internal fun AccountIncomingConfigContentK9Preview() {
internal fun AccountIncomingConfigContentThunderbirdPreview() {
ThunderbirdTheme {
AccountIncomingConfigContent(
onEvent = { },
state = State(),
contentPadding = PaddingValues(),
)
}

View file

@ -0,0 +1,48 @@
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.toImapDefaultPort
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 = ConnectionSecurity.DEFAULT,
val port: NumberInputField = NumberInputField(ConnectionSecurity.DEFAULT.toImapDefaultPort()),
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()
}
}

View file

@ -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(),
)
}
}

View file

@ -0,0 +1,66 @@
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.toImapDefaultPort
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 PortChanged -> updateState { it.copy(port = it.port.updateValue(event.port)) }
is ProtocolTypeChanged -> updateState { it.copy(protocolType = event.protocolType) }
is SecurityChanged -> updateSecurity(event.security)
is ServerChanged -> updateState { it.copy(server = it.server.updateValue(event.server)) }
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 updateSecurity(security: ConnectionSecurity) {
updateState {
it.copy(
security = security,
port = it.port.updateValue(security.toImapDefaultPort()),
)
}
}
private fun submit() {
navigateNext()
}
private fun navigateBack() = emitEffect(Effect.NavigateBack)
private fun navigateNext() = emitEffect(Effect.NavigateNext)
}

View file

@ -42,8 +42,8 @@ class AccountOutgoingConfigViewModel(
is ImapAutoDetectNamespaceChanged -> updateState { it.copy(imapAutodetectNamespaceEnabled = event.enabled) }
is UseCompressionChanged -> updateState { it.copy(useCompression = event.useCompression) }
OnBackClicked -> navigateBack()
OnNextClicked -> submit()
OnBackClicked -> navigateBack()
}
}

View file

@ -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,
)

View file

@ -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)
}
}

View file

@ -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()
}
}
}

View file

@ -0,0 +1,189 @@
package app.k9mail.feature.account.setup.ui.incoming
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.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 state when ProtocolTypeChanged event is received`() = runTest {
eventStateTest(
viewModel = testSubject,
initialState = State(),
event = Event.ProtocolTypeChanged(IncomingProtocolType.POP3),
expectedState = State(protocolType = IncomingProtocolType.POP3),
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)
}
}
}

View file

@ -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)
}
}

View file

@ -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