Add AccountAutoConfig contract and view model

This commit is contained in:
Wolf-Martell Montwé 2023-06-06 17:43:30 +02:00
parent a644d37742
commit fd3dfb3235
No known key found for this signature in database
GPG key ID: 6D45B21512ACBF72
11 changed files with 365 additions and 4 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.autoconfig.AccountAutoConfigViewModel
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigValidator
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigViewModel
@ -20,6 +21,7 @@ val featureAccountSetupModule: Module = module {
factory<AccountOptionsContract.Validator> { AccountOptionsValidator() }
viewModel { AccountSetupViewModel() }
viewModel { AccountAutoConfigViewModel() }
viewModel {
AccountIncomingConfigViewModel(
validator = get(),

View file

@ -7,7 +7,9 @@ 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.autoconfig.AccountAutoConfigContract
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigScreen
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigViewModel
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigScreen
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigViewModel
@ -24,6 +26,7 @@ fun AccountSetupScreen(
onFinish: () -> Unit,
onBack: () -> Unit,
viewModel: ViewModel = koinViewModel<AccountSetupViewModel>(),
autoConfigViewModel: AccountAutoConfigContract.ViewModel = koinViewModel<AccountAutoConfigViewModel>(),
incomingViewModel: AccountIncomingConfigContract.ViewModel = koinViewModel<AccountIncomingConfigViewModel>(),
outgoingViewModel: AccountOutgoingConfigContract.ViewModel = koinViewModel<AccountOutgoingConfigViewModel>(),
optionsViewModel: AccountOptionsContract.ViewModel = koinViewModel<AccountOptionsViewModel>(),
@ -40,6 +43,7 @@ fun AccountSetupScreen(
AccountAutoConfigScreen(
onNext = { dispatch(Event.OnNext) },
onBack = { dispatch(Event.OnBack) },
viewModel = autoConfigViewModel,
)
}

View file

@ -18,9 +18,13 @@ import app.k9mail.core.ui.compose.designsystem.template.ResponsiveWidthContainer
import app.k9mail.core.ui.compose.theme.K9Theme
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.ThunderbirdTheme
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.Event
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.State
@Composable
internal fun AccountAutoConfigContent(
state: State,
onEvent: (Event) -> Unit,
contentPadding: PaddingValues,
modifier: Modifier = Modifier,
) {
@ -39,6 +43,8 @@ internal fun AccountAutoConfigContent(
) {
item {
AccountSetupEmailForm(
state = state,
onEvent = onEvent,
modifier = Modifier.fillMaxWidth(),
)
}
@ -48,6 +54,8 @@ internal fun AccountAutoConfigContent(
@Composable
private fun AccountSetupEmailForm(
state: State,
onEvent: (Event) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
@ -55,8 +63,8 @@ private fun AccountSetupEmailForm(
horizontalAlignment = Alignment.CenterHorizontally,
) {
TextFieldOutlined(
value = "",
onValueChange = { /*TODO*/ },
value = state.emailAddress.value,
onValueChange = { onEvent(Event.EmailAddressChanged(it)) },
label = "Email address",
)
}
@ -67,6 +75,8 @@ private fun AccountSetupEmailForm(
internal fun AccountAutoConfigContentK9Preview() {
K9Theme {
AccountAutoConfigContent(
state = State(),
onEvent = {},
contentPadding = PaddingValues(),
)
}
@ -77,6 +87,8 @@ internal fun AccountAutoConfigContentK9Preview() {
internal fun AccountAutoConfigContentThunderbirdPreview() {
ThunderbirdTheme {
AccountAutoConfigContent(
state = State(),
onEvent = {},
contentPadding = PaddingValues(),
)
}

View file

@ -0,0 +1,41 @@
package app.k9mail.feature.account.setup.ui.autoconfig
import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
import app.k9mail.feature.account.setup.domain.entity.AutoConfig
import app.k9mail.feature.account.setup.domain.input.StringInputField
interface AccountAutoConfigContract {
enum class ConfigStep {
EMAIL_ADDRESS,
OAUTH,
PASSWORD,
}
interface ViewModel : UnidirectionalViewModel<State, Event, Effect> {
fun initState(state: State)
}
data class State(
val configStep: ConfigStep = ConfigStep.EMAIL_ADDRESS,
val emailAddress: StringInputField = StringInputField(),
val password: StringInputField = StringInputField(),
val autoConfig: AutoConfig? = null,
val errorMessage: String? = null,
val isLoading: Boolean = false,
)
sealed class Event {
data class EmailAddressChanged(val emailAddress: String) : Event()
data class PasswordChanged(val password: String) : Event()
object OnRetryClicked : Event()
object OnNextClicked : Event()
object OnBackClicked : Event()
}
sealed class Effect {
object NavigateNext : Effect()
object NavigateBack : Effect()
}
}

View file

@ -4,10 +4,14 @@ 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.autoconfig.AccountAutoConfigContract.Effect
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.Event
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.ViewModel
import app.k9mail.feature.account.setup.ui.common.AccountSetupBottomBar
import app.k9mail.feature.account.setup.ui.common.AccountSetupTopHeader
@ -15,8 +19,16 @@ import app.k9mail.feature.account.setup.ui.common.AccountSetupTopHeader
fun AccountAutoConfigScreen(
onNext: () -> Unit,
onBack: () -> Unit,
viewModel: ViewModel,
modifier: Modifier = Modifier,
) {
val (state, dispatch) = viewModel.observe { effect ->
when (effect) {
Effect.NavigateBack -> onBack()
Effect.NavigateNext -> onNext()
}
}
Scaffold(
topBar = {
AccountSetupTopHeader()
@ -25,13 +37,15 @@ fun AccountAutoConfigScreen(
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 ->
AccountAutoConfigContent(
state = state.value,
onEvent = { dispatch(it) },
contentPadding = innerPadding,
)
}
@ -44,6 +58,7 @@ internal fun AccountAutoConfigScreenK9Preview() {
AccountAutoConfigScreen(
onNext = {},
onBack = {},
viewModel = AccountAutoConfigViewModel(),
)
}
}
@ -55,6 +70,7 @@ internal fun AccountAutoConfigScreenThunderbirdPreview() {
AccountAutoConfigScreen(
onNext = {},
onBack = {},
viewModel = AccountAutoConfigViewModel(),
)
}
}

View file

@ -0,0 +1,73 @@
package app.k9mail.feature.account.setup.ui.autoconfig
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
import app.k9mail.feature.account.setup.domain.input.StringInputField
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.ConfigStep.EMAIL_ADDRESS
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.ConfigStep.OAUTH
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.ConfigStep.PASSWORD
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.Effect
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.Event
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.State
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.ViewModel
@Suppress("TooManyFunctions")
class AccountAutoConfigViewModel(
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 Event.EmailAddressChanged -> changeEmailAddress(event.emailAddress)
is Event.PasswordChanged -> changePassword(event.password)
Event.OnNextClicked -> submit()
Event.OnBackClicked -> navigateBack()
Event.OnRetryClicked -> retry()
}
}
private fun changeEmailAddress(emailAddress: String) {
updateState {
State(
emailAddress = StringInputField(value = emailAddress),
)
}
}
private fun changePassword(password: String) {
updateState {
it.copy(
password = it.password.updateValue(password),
)
}
}
private fun submit() {
when (state.value.configStep) {
EMAIL_ADDRESS -> submitEmail()
PASSWORD -> submitPassword()
OAUTH -> TODO()
}
}
private fun retry() {
TODO()
}
private fun submitEmail() {
navigateNext()
}
private fun submitPassword() {
navigateNext()
}
private fun navigateBack() = emitEffect(Effect.NavigateBack)
private fun navigateNext() = emitEffect(Effect.NavigateNext)
}

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.autoconfig.FakeAccountAutoConfigViewModel
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
@ -22,6 +23,7 @@ class AccountSetupScreenKtTest : ComposeTest() {
@Test
fun `should display correct screen for every setup step`() = runTest {
val viewModel = FakeAccountSetupViewModel()
val autoConfigViewModel = FakeAccountAutoConfigViewModel()
val incomingViewModel = FakeAccountIncomingConfigViewModel()
val outgoingViewModel = FakeAccountOutgoingConfigViewModel()
val optionsViewModel = FakeAccountOptionsViewModel()
@ -32,6 +34,7 @@ class AccountSetupScreenKtTest : ComposeTest() {
onFinish = { },
onBack = { },
viewModel = viewModel,
autoConfigViewModel = autoConfigViewModel,
incomingViewModel = incomingViewModel,
outgoingViewModel = outgoingViewModel,
optionsViewModel = optionsViewModel,
@ -49,6 +52,7 @@ class AccountSetupScreenKtTest : ComposeTest() {
fun `should delegate navigation effects`() = runTest {
val initialState = State()
val viewModel = FakeAccountSetupViewModel(initialState)
val autoConfigViewModel = FakeAccountAutoConfigViewModel()
val incomingViewModel = FakeAccountIncomingConfigViewModel()
val outgoingViewModel = FakeAccountOutgoingConfigViewModel()
val optionsViewModel = FakeAccountOptionsViewModel()
@ -61,6 +65,7 @@ class AccountSetupScreenKtTest : ComposeTest() {
onFinish = { onFinishCounter++ },
onBack = { onBackCounter++ },
viewModel = viewModel,
autoConfigViewModel = autoConfigViewModel,
incomingViewModel = incomingViewModel,
outgoingViewModel = outgoingViewModel,
optionsViewModel = optionsViewModel,

View file

@ -0,0 +1,47 @@
package app.k9mail.feature.account.setup.ui.autoconfig
import app.k9mail.core.ui.compose.testing.ComposeTest
import app.k9mail.core.ui.compose.testing.setContent
import app.k9mail.core.ui.compose.theme.ThunderbirdTheme
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.Effect
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.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 AccountAutoConfigScreenKtTest : ComposeTest() {
@Test
fun `should delegate navigation effects`() = runTest {
val initialState = State()
val viewModel = FakeAccountAutoConfigViewModel(initialState)
var onNextCounter = 0
var onBackCounter = 0
setContent {
ThunderbirdTheme {
AccountAutoConfigScreen(
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,27 @@
package app.k9mail.feature.account.setup.ui.autoconfig
import app.k9mail.feature.account.setup.domain.input.StringInputField
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.ConfigStep
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.State
import assertk.all
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.prop
import org.junit.Test
class AccountAutoConfigStateTest {
@Test
fun `should set default values`() {
val state = State()
assertThat(state).all {
prop(State::configStep).isEqualTo(ConfigStep.EMAIL_ADDRESS)
prop(State::emailAddress).isEqualTo(StringInputField())
prop(State::password).isEqualTo(StringInputField())
prop(State::autoConfig).isEqualTo(null)
prop(State::errorMessage).isEqualTo(null)
prop(State::isLoading).isEqualTo(false)
}
}
}

View file

@ -0,0 +1,108 @@
package app.k9mail.feature.account.setup.ui.autoconfig
import app.cash.turbine.testIn
import app.k9mail.core.ui.compose.testing.MainDispatcherRule
import app.k9mail.feature.account.setup.domain.input.StringInputField
import app.k9mail.feature.account.setup.testing.eventStateTest
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.ConfigStep
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.Effect
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.Event
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.State
import 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 AccountAutoConfigViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
private val testSubject = AccountAutoConfigViewModel()
@Test
fun `should reset state when EmailAddressChanged event is received`() = runTest {
val initialState = State(
configStep = ConfigStep.PASSWORD,
emailAddress = StringInputField(value = "email"),
password = StringInputField(value = "password"),
)
testSubject.initState(initialState)
eventStateTest(
viewModel = testSubject,
initialState = initialState,
event = Event.EmailAddressChanged("email"),
expectedState = State(
configStep = ConfigStep.EMAIL_ADDRESS,
emailAddress = StringInputField(value = "email"),
password = StringInputField(),
),
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 emit NavigateNext effect when NextClicked event is received`() = runTest {
val viewModel = testSubject
val stateTurbine = viewModel.state.testIn(backgroundScope)
val effectTurbine = viewModel.effect.testIn(backgroundScope)
val turbines = listOf(stateTurbine, effectTurbine)
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 BackClicked event is 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.autoconfig
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.Effect
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.Event
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.State
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.ViewModel
class FakeAccountAutoConfigViewModel(
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)
}
}