Remove AccountOptions

This commit is contained in:
Wolf-Martell Montwé 2024-01-10 13:39:26 +01:00
parent 38a1be4f07
commit 34d0da026b
No known key found for this signature in database
GPG key ID: 6D45B21512ACBF72
17 changed files with 22 additions and 901 deletions

View file

@ -15,9 +15,10 @@ import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryCon
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryValidator
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryViewModel
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountViewModel
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
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsContract
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsValidator
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsViewModel
import app.k9mail.feature.account.setup.ui.options.sync.SyncOptionsViewModel
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersFormUiModel
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersViewModel
@ -61,7 +62,7 @@ val featureAccountSetupModule: Module = module {
}
factory<AccountAutoDiscoveryContract.Validator> { AccountAutoDiscoveryValidator() }
factory<AccountOptionsContract.Validator> { AccountOptionsValidator() }
factory<DisplayOptionsContract.Validator> { DisplayOptionsValidator() }
viewModel {
AccountAutoDiscoveryViewModel(
@ -107,12 +108,18 @@ val featureAccountSetupModule: Module = module {
}
viewModel {
AccountOptionsViewModel(
DisplayOptionsViewModel(
validator = get(),
accountStateRepository = get(),
)
}
viewModel {
SyncOptionsViewModel(
accountStateRepository = get(),
)
}
viewModel {
CreateAccountViewModel(
createAccount = get(),

View file

@ -11,7 +11,7 @@ import app.k9mail.feature.account.setup.domain.entity.toAuthenticationType
import app.k9mail.feature.account.setup.domain.entity.toConnectionSecurity
import app.k9mail.feature.account.setup.domain.entity.toIncomingProtocolType
import app.k9mail.feature.account.setup.domain.toServerSettings
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsContract
internal fun AccountAutoDiscoveryContract.State.toAccountState(): AccountState {
return AccountState(
@ -67,8 +67,8 @@ internal fun AccountAutoDiscoveryContract.State.toOutgoingConfigState(): Outgoin
}
}
internal fun AccountAutoDiscoveryContract.State.toOptionsState(): AccountOptionsContract.State {
return AccountOptionsContract.State(
internal fun AccountAutoDiscoveryContract.State.toOptionsState(): DisplayOptionsContract.State {
return DisplayOptionsContract.State(
accountName = StringInputField(value = emailAddress.value),
)
}

View file

@ -1,172 +0,0 @@
package app.k9mail.feature.account.setup.ui.options
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
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.PreviewDevices
import app.k9mail.core.ui.compose.designsystem.atom.text.TextOverline
import app.k9mail.core.ui.compose.designsystem.molecule.input.SelectInput
import app.k9mail.core.ui.compose.designsystem.molecule.input.SwitchInput
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.common.ui.item.defaultHeadlineItemPadding
import app.k9mail.feature.account.common.ui.item.defaultItemPadding
import app.k9mail.feature.account.setup.R
import app.k9mail.feature.account.setup.domain.entity.EmailCheckFrequency
import app.k9mail.feature.account.setup.domain.entity.EmailDisplayCount
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.Event
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.State
@OptIn(ExperimentalLayoutApi::class)
@Suppress("LongMethod")
@Composable
internal fun AccountOptionsContent(
state: State,
onEvent: (Event) -> Unit,
contentPadding: PaddingValues,
modifier: Modifier = Modifier,
) {
val resources = LocalContext.current.resources
ResponsiveWidthContainer(
modifier = Modifier
.testTag("AccountOptionsContent")
.consumeWindowInsets(contentPadding)
.padding(contentPadding)
.then(modifier),
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.imePadding(),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
) {
item {
TextOverline(
text = stringResource(id = R.string.account_setup_options_section_display_options),
modifier = Modifier
.fillMaxWidth()
.padding(defaultHeadlineItemPadding()),
)
}
item {
TextInput(
text = state.accountName.value,
errorMessage = state.accountName.error?.toResourceString(resources),
onTextChange = { onEvent(Event.OnAccountNameChanged(it)) },
label = stringResource(id = R.string.account_setup_options_account_name_label),
contentPadding = defaultItemPadding(),
)
}
item {
TextInput(
text = state.displayName.value,
errorMessage = state.displayName.error?.toResourceString(resources),
onTextChange = { onEvent(Event.OnDisplayNameChanged(it)) },
label = stringResource(id = R.string.account_setup_options_display_name_label),
contentPadding = defaultItemPadding(),
isRequired = true,
)
}
item {
TextInput(
text = state.emailSignature.value,
errorMessage = state.emailSignature.error?.toResourceString(resources),
onTextChange = { onEvent(Event.OnEmailSignatureChanged(it)) },
label = stringResource(id = R.string.account_setup_options_email_signature_label),
contentPadding = defaultItemPadding(),
isSingleLine = false,
)
}
item {
TextOverline(
text = stringResource(id = R.string.account_setup_options_section_sync_options),
modifier = Modifier
.fillMaxWidth()
.padding(defaultHeadlineItemPadding()),
)
}
item {
SelectInput(
options = EmailCheckFrequency.all(),
optionToStringTransformation = { it.toResourceString(resources) },
selectedOption = state.checkFrequency,
onOptionChange = { onEvent(Event.OnCheckFrequencyChanged(it)) },
label = stringResource(id = R.string.account_setup_options_account_check_frequency_label),
contentPadding = defaultItemPadding(),
)
}
item {
SelectInput(
options = EmailDisplayCount.all(),
optionToStringTransformation = { it.toResourceString(resources) },
selectedOption = state.messageDisplayCount,
onOptionChange = { onEvent(Event.OnMessageDisplayCountChanged(it)) },
label = stringResource(id = R.string.account_setup_options_email_display_count_label),
contentPadding = defaultItemPadding(),
)
}
item {
SwitchInput(
text = stringResource(id = R.string.account_setup_options_show_notifications_label),
checked = state.showNotification,
onCheckedChange = { onEvent(Event.OnShowNotificationChanged(it)) },
contentPadding = defaultItemPadding(),
)
}
item {
Spacer(modifier = Modifier.requiredHeight(MainTheme.sizes.smaller))
}
}
}
}
@Composable
@PreviewDevices
internal fun AccountOptionsContentK9Preview() {
K9Theme {
AccountOptionsContent(
state = State(),
onEvent = {},
contentPadding = PaddingValues(),
)
}
}
@Composable
@PreviewDevices
internal fun AccountOptionsContentThunderbirdPreview() {
ThunderbirdTheme {
AccountOptionsContent(
state = State(),
onEvent = {},
contentPadding = PaddingValues(),
)
}
}

View file

@ -1,46 +0,0 @@
package app.k9mail.feature.account.setup.ui.options
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
import app.k9mail.feature.account.common.domain.input.StringInputField
import app.k9mail.feature.account.setup.domain.entity.EmailCheckFrequency
import app.k9mail.feature.account.setup.domain.entity.EmailDisplayCount
interface AccountOptionsContract {
interface ViewModel : UnidirectionalViewModel<State, Event, Effect>
data class State(
val accountName: StringInputField = StringInputField(),
val displayName: StringInputField = StringInputField(),
val emailSignature: StringInputField = StringInputField(),
val checkFrequency: EmailCheckFrequency = EmailCheckFrequency.DEFAULT,
val messageDisplayCount: EmailDisplayCount = EmailDisplayCount.DEFAULT,
val showNotification: Boolean = false,
)
sealed interface Event {
data class OnAccountNameChanged(val accountName: String) : Event
data class OnDisplayNameChanged(val displayName: String) : Event
data class OnEmailSignatureChanged(val emailSignature: String) : Event
data class OnCheckFrequencyChanged(val checkFrequency: EmailCheckFrequency) : Event
data class OnMessageDisplayCountChanged(val messageDisplayCount: EmailDisplayCount) : Event
data class OnShowNotificationChanged(val showNotification: Boolean) : Event
object LoadAccountState : Event
object OnNextClicked : Event
object OnBackClicked : Event
}
sealed interface Effect {
object NavigateNext : Effect
object NavigateBack : Effect
}
interface Validator {
fun validateAccountName(accountName: String): ValidationResult
fun validateDisplayName(displayName: String): ValidationResult
fun validateEmailSignature(emailSignature: String): ValidationResult
}
}

View file

@ -1,93 +0,0 @@
package app.k9mail.feature.account.setup.ui.options
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import app.k9mail.core.ui.compose.common.PreviewDevices
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.common.ui.AccountTopAppBar
import app.k9mail.feature.account.common.ui.WizardNavigationBar
import app.k9mail.feature.account.common.ui.preview.PreviewAccountStateRepository
import app.k9mail.feature.account.setup.R.string
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.Effect
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.Event
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.ViewModel
@Composable
internal fun AccountOptionsScreen(
onNext: () -> Unit,
onBack: () -> Unit,
viewModel: ViewModel,
modifier: Modifier = Modifier,
) {
val (state, dispatch) = viewModel.observe { effect ->
when (effect) {
Effect.NavigateBack -> onBack()
Effect.NavigateNext -> onNext()
}
}
LaunchedEffect(key1 = Unit) {
dispatch(Event.LoadAccountState)
}
BackHandler {
dispatch(Event.OnBackClicked)
}
Scaffold(
topBar = {
AccountTopAppBar(
title = stringResource(id = string.account_setup_options_top_bar_title),
)
},
bottomBar = {
WizardNavigationBar(
onNextClick = { dispatch(Event.OnNextClicked) },
onBackClick = { dispatch(Event.OnBackClicked) },
)
},
modifier = modifier,
) { innerPadding ->
AccountOptionsContent(
state = state.value,
onEvent = { dispatch(it) },
contentPadding = innerPadding,
)
}
}
@Composable
@PreviewDevices
internal fun AccountOptionsScreenK9Preview() {
K9Theme {
AccountOptionsScreen(
onNext = {},
onBack = {},
viewModel = AccountOptionsViewModel(
validator = AccountOptionsValidator(),
accountStateRepository = PreviewAccountStateRepository(),
),
)
}
}
@Composable
@PreviewDevices
internal fun AccountOptionsScreenThunderbirdPreview() {
ThunderbirdTheme {
AccountOptionsScreen(
onNext = {},
onBack = {},
viewModel = AccountOptionsViewModel(
validator = AccountOptionsValidator(),
accountStateRepository = PreviewAccountStateRepository(),
),
)
}
}

View file

@ -1,39 +0,0 @@
package app.k9mail.feature.account.setup.ui.options
import app.k9mail.feature.account.common.domain.entity.AccountOptions
import app.k9mail.feature.account.common.domain.entity.AccountState
import app.k9mail.feature.account.common.domain.input.StringInputField
import app.k9mail.feature.account.setup.domain.entity.EmailCheckFrequency
import app.k9mail.feature.account.setup.domain.entity.EmailDisplayCount
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.State
internal fun AccountState.toAccountOptionsState(): State {
val options = options
return if (options == null) {
State(
accountName = StringInputField(emailAddress ?: ""),
// displayName = StringInputField(""),
// TODO: get display name from: preferences.defaultAccount?.senderName ?: ""
)
} else {
State(
accountName = StringInputField(options.accountName),
displayName = StringInputField(options.displayName),
emailSignature = StringInputField(options.emailSignature ?: ""),
checkFrequency = EmailCheckFrequency.fromMinutes(options.checkFrequencyInMinutes),
messageDisplayCount = EmailDisplayCount.fromCount(options.messageDisplayCount),
showNotification = options.showNotification,
)
}
}
internal fun State.toAccountOptions(): AccountOptions {
return AccountOptions(
accountName = accountName.value,
displayName = displayName.value,
emailSignature = emailSignature.value.takeIf { it.isNotEmpty() },
checkFrequencyInMinutes = checkFrequency.minutes,
messageDisplayCount = messageDisplayCount.count,
showNotification = showNotification,
)
}

View file

@ -1,65 +0,0 @@
package app.k9mail.feature.account.setup.ui.options
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.EmailCheckFrequency
import app.k9mail.feature.account.setup.domain.entity.EmailDisplayCount
import app.k9mail.feature.account.setup.domain.usecase.ValidateAccountName.ValidateAccountNameError
import app.k9mail.feature.account.setup.domain.usecase.ValidateAccountName.ValidateAccountNameError.BlankAccountName
import app.k9mail.feature.account.setup.domain.usecase.ValidateDisplayName.ValidateDisplayNameError
import app.k9mail.feature.account.setup.domain.usecase.ValidateDisplayName.ValidateDisplayNameError.EmptyDisplayName
import app.k9mail.feature.account.setup.domain.usecase.ValidateEmailSignature.ValidateEmailSignatureError
import app.k9mail.feature.account.setup.domain.usecase.ValidateEmailSignature.ValidateEmailSignatureError.BlankEmailSignature
internal fun EmailDisplayCount.toResourceString(resources: Resources) = resources.getQuantityString(
R.plurals.account_setup_options_email_display_count_messages,
count,
count,
)
@Suppress("MagicNumber")
internal fun EmailCheckFrequency.toResourceString(resources: Resources): String {
return when (minutes) {
-1 -> resources.getString(R.string.account_setup_options_email_check_frequency_never)
in 1..59 -> resources.getQuantityString(
R.plurals.account_setup_options_email_check_frequency_minutes,
minutes,
minutes,
)
else -> resources.getQuantityString(
R.plurals.account_setup_options_email_check_frequency_hours,
(minutes / 60),
(minutes / 60),
)
}
}
internal fun ValidationError.toResourceString(resources: Resources): String {
return when (this) {
is ValidateAccountNameError -> toAccountNameErrorString(resources)
is ValidateDisplayNameError -> toDisplayNameErrorString(resources)
is ValidateEmailSignatureError -> toEmailSignatureErrorString(resources)
else -> throw IllegalArgumentException("Unknown error: $this")
}
}
private fun ValidateAccountNameError.toAccountNameErrorString(resources: Resources): String {
return when (this) {
is BlankAccountName -> resources.getString(R.string.account_setup_options_account_name_error_blank)
}
}
private fun ValidateDisplayNameError.toDisplayNameErrorString(resources: Resources): String {
return when (this) {
is EmptyDisplayName -> resources.getString(R.string.account_setup_options_display_name_error_required)
}
}
private fun ValidateEmailSignatureError.toEmailSignatureErrorString(resources: Resources): String {
return when (this) {
is BlankEmailSignature -> resources.getString(R.string.account_setup_options_email_signature_error_blank)
}
}

View file

@ -1,25 +0,0 @@
package app.k9mail.feature.account.setup.ui.options
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.feature.account.setup.domain.usecase.ValidateAccountName
import app.k9mail.feature.account.setup.domain.usecase.ValidateDisplayName
import app.k9mail.feature.account.setup.domain.usecase.ValidateEmailSignature
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.Validator
internal class AccountOptionsValidator(
private val accountNameValidator: ValidateAccountName = ValidateAccountName(),
private val displayNameValidator: ValidateDisplayName = ValidateDisplayName(),
private val emailSignatureValidator: ValidateEmailSignature = ValidateEmailSignature(),
) : Validator {
override fun validateAccountName(accountName: String): ValidationResult {
return accountNameValidator.execute(accountName)
}
override fun validateDisplayName(displayName: String): ValidationResult {
return displayNameValidator.execute(displayName)
}
override fun validateEmailSignature(emailSignature: String): ValidationResult {
return emailSignatureValidator.execute(emailSignature)
}
}

View file

@ -1,100 +0,0 @@
package app.k9mail.feature.account.setup.ui.options
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
import app.k9mail.feature.account.common.domain.AccountDomainContract
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.Effect
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.Event
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.State
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.Validator
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.ViewModel
internal class AccountOptionsViewModel(
private val validator: Validator,
private val accountStateRepository: AccountDomainContract.AccountStateRepository,
initialState: State? = null,
) : BaseViewModel<State, Event, Effect>(
initialState = initialState ?: accountStateRepository.getState().toAccountOptionsState(),
),
ViewModel {
override fun event(event: Event) {
when (event) {
Event.LoadAccountState -> handleOneTimeEvent(event, ::loadAccountState)
is Event.OnAccountNameChanged -> updateState { state ->
state.copy(
accountName = state.accountName.updateValue(event.accountName),
)
}
is Event.OnDisplayNameChanged -> updateState {
it.copy(
displayName = it.displayName.updateValue(event.displayName),
)
}
is Event.OnEmailSignatureChanged -> updateState {
it.copy(
emailSignature = it.emailSignature.updateValue(event.emailSignature),
)
}
is Event.OnCheckFrequencyChanged -> updateState {
it.copy(
checkFrequency = event.checkFrequency,
)
}
is Event.OnMessageDisplayCountChanged -> updateState { state ->
state.copy(
messageDisplayCount = event.messageDisplayCount,
)
}
is Event.OnShowNotificationChanged -> updateState { state ->
state.copy(
showNotification = event.showNotification,
)
}
Event.OnNextClicked -> submit()
Event.OnBackClicked -> navigateBack()
}
}
private fun loadAccountState() {
updateState {
accountStateRepository.getState().toAccountOptionsState()
}
}
private fun submit() = with(state.value) {
val accountNameResult = validator.validateAccountName(accountName.value)
val displayNameResult = validator.validateDisplayName(displayName.value)
val emailSignatureResult = validator.validateEmailSignature(emailSignature.value)
val hasError = listOf(
accountNameResult,
displayNameResult,
emailSignatureResult,
).any { it is ValidationResult.Failure }
updateState {
it.copy(
accountName = it.accountName.updateFromValidationResult(accountNameResult),
displayName = it.displayName.updateFromValidationResult(displayNameResult),
emailSignature = it.emailSignature.updateFromValidationResult(emailSignatureResult),
)
}
if (!hasError) {
accountStateRepository.setOptions(state.value.toAccountOptions())
navigateNext()
}
}
private fun navigateBack() = emitEffect(Effect.NavigateBack)
private fun navigateNext() = emitEffect(Effect.NavigateNext)
}

View file

@ -14,7 +14,8 @@ import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCrea
import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator.AccountCreatorResult
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsContract
import app.k9mail.feature.account.setup.ui.options.sync.SyncOptionsContract
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract
import com.fsck.k9.mail.oauth.AuthStateStorage
import com.fsck.k9.mail.oauth.OAuth2TokenProvider
@ -71,7 +72,8 @@ class AccountSetupModuleKtTest : KoinTest {
ServerValidationContract.State::class,
IncomingServerSettingsContract.State::class,
OutgoingServerSettingsContract.State::class,
AccountOptionsContract.State::class,
DisplayOptionsContract.State::class,
SyncOptionsContract.State::class,
AccountState::class,
ServerCertificateErrorContract.State::class,
AuthStateStorage::class,

View file

@ -15,7 +15,7 @@ import app.k9mail.feature.account.server.settings.ui.outgoing.OutgoingServerSett
import app.k9mail.feature.account.setup.domain.entity.AutoDiscoveryAuthenticationType
import app.k9mail.feature.account.setup.domain.entity.AutoDiscoveryConnectionSecurity
import app.k9mail.feature.account.setup.domain.entity.toConnectionSecurity
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsContract
import assertk.assertThat
import assertk.assertions.isEqualTo
import org.junit.Test
@ -146,7 +146,7 @@ class AccountAutoDiscoveryStateMapperKtTest {
fun `should map to OptionsState when empty`() {
val optionsState = EMPTY_STATE.toOptionsState()
assertThat(optionsState).isEqualTo(AccountOptionsContract.State())
assertThat(optionsState).isEqualTo(DisplayOptionsContract.State())
}
@Test
@ -154,7 +154,7 @@ class AccountAutoDiscoveryStateMapperKtTest {
val optionsState = EMAIL_PASSWORD_STATE.toOptionsState()
assertThat(optionsState).isEqualTo(
AccountOptionsContract.State(
DisplayOptionsContract.State(
accountName = StringInputField(value = EMAIL_ADDRESS),
),
)

View file

@ -1,45 +0,0 @@
package app.k9mail.feature.account.setup.ui.options
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.options.AccountOptionsContract.Effect
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.State
import assertk.assertThat
import assertk.assertions.isEqualTo
import kotlinx.coroutines.test.runTest
import org.junit.Test
class AccountOptionsScreenKtTest : ComposeTest() {
@Test
fun `should delegate navigation effects`() = runTest {
val initialState = State()
val viewModel = FakeAccountOptionsViewModel(initialState)
var onNextCounter = 0
var onBackCounter = 0
setContent {
ThunderbirdTheme {
AccountOptionsScreen(
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

@ -1,47 +0,0 @@
package app.k9mail.feature.account.setup.ui.options
import app.k9mail.feature.account.common.domain.entity.AccountOptions
import app.k9mail.feature.account.common.domain.input.StringInputField
import app.k9mail.feature.account.setup.domain.entity.EmailCheckFrequency
import app.k9mail.feature.account.setup.domain.entity.EmailDisplayCount
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isNull
import org.junit.Test
class AccountOptionsStateMapperKtTest {
@Test
fun `should map state to account options`() {
val state = AccountOptionsContract.State(
accountName = StringInputField("accountName"),
displayName = StringInputField("displayName"),
emailSignature = StringInputField("emailSignature"),
checkFrequency = EmailCheckFrequency.EVERY_2_HOURS,
messageDisplayCount = EmailDisplayCount.MESSAGES_100,
showNotification = true,
)
val result = state.toAccountOptions()
assertThat(result).isEqualTo(
AccountOptions(
accountName = "accountName",
displayName = "displayName",
emailSignature = "emailSignature",
checkFrequencyInMinutes = 120,
messageDisplayCount = 100,
showNotification = true,
),
)
}
@Test
fun `empty signature should map to null`() {
val state = AccountOptionsContract.State(emailSignature = StringInputField(""))
val result = state.toAccountOptions()
assertThat(result.emailSignature).isNull()
}
}

View file

@ -1,28 +0,0 @@
package app.k9mail.feature.account.setup.ui.options
import app.k9mail.feature.account.common.domain.input.StringInputField
import app.k9mail.feature.account.setup.domain.entity.EmailCheckFrequency
import app.k9mail.feature.account.setup.domain.entity.EmailDisplayCount
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.State
import assertk.assertThat
import assertk.assertions.isEqualTo
import org.junit.Test
class AccountOptionsStateTest {
@Test
fun `should set default values`() {
val state = State()
assertThat(state).isEqualTo(
State(
accountName = StringInputField(),
displayName = StringInputField(),
emailSignature = StringInputField(),
checkFrequency = EmailCheckFrequency.DEFAULT,
messageDisplayCount = EmailDisplayCount.DEFAULT,
showNotification = false,
),
)
}
}

View file

@ -1,192 +0,0 @@
package app.k9mail.feature.account.setup.ui.options
import app.cash.turbine.testIn
import app.k9mail.core.common.domain.usecase.validation.ValidationError
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.core.ui.compose.testing.MainDispatcherRule
import app.k9mail.core.ui.compose.testing.mvi.eventStateTest
import app.k9mail.feature.account.common.data.InMemoryAccountStateRepository
import app.k9mail.feature.account.common.domain.input.StringInputField
import app.k9mail.feature.account.setup.domain.entity.EmailCheckFrequency
import app.k9mail.feature.account.setup.domain.entity.EmailDisplayCount
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.Effect
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.Event
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.State
import assertk.assertThat
import assertk.assertions.assertThatAndTurbinesConsumed
import assertk.assertions.isEqualTo
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class AccountOptionsViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
private val testSubject = AccountOptionsViewModel(
validator = FakeAccountOptionsValidator(),
accountStateRepository = InMemoryAccountStateRepository(),
)
@Test
fun `should change state when OnAccountNameChanged event is received`() = runTest {
eventStateTest(
viewModel = testSubject,
initialState = State(),
event = Event.OnAccountNameChanged("accountName"),
expectedState = State(accountName = StringInputField(value = "accountName")),
coroutineScope = backgroundScope,
)
}
@Test
fun `should change state when OnDisplayNameChanged event is received`() = runTest {
eventStateTest(
viewModel = testSubject,
initialState = State(),
event = Event.OnDisplayNameChanged("displayName"),
expectedState = State(displayName = StringInputField(value = "displayName")),
coroutineScope = backgroundScope,
)
}
@Test
fun `should change state when OnEmailSignatureChanged event is received`() = runTest {
eventStateTest(
viewModel = testSubject,
initialState = State(),
event = Event.OnEmailSignatureChanged("emailSignature"),
expectedState = State(emailSignature = StringInputField(value = "emailSignature")),
coroutineScope = backgroundScope,
)
}
@Test
fun `should change state when OnCheckFrequencyChanged event is received`() = runTest {
eventStateTest(
viewModel = testSubject,
initialState = State(),
event = Event.OnCheckFrequencyChanged(EmailCheckFrequency.EVERY_12_HOURS),
expectedState = State(checkFrequency = EmailCheckFrequency.EVERY_12_HOURS),
coroutineScope = backgroundScope,
)
}
@Test
fun `should change state when OnMessageDisplayCountChanged event is received`() = runTest {
eventStateTest(
viewModel = testSubject,
initialState = State(),
event = Event.OnMessageDisplayCountChanged(EmailDisplayCount.MESSAGES_1000),
expectedState = State(messageDisplayCount = EmailDisplayCount.MESSAGES_1000),
coroutineScope = backgroundScope,
)
}
@Test
fun `should change state when OnShowNotificationChanged event is received`() = runTest {
eventStateTest(
viewModel = testSubject,
initialState = State(),
event = Event.OnShowNotificationChanged(true),
expectedState = State(showNotification = true),
coroutineScope = backgroundScope,
)
}
@Test
fun `should change state and emit NavigateNext effect when OnNextClicked event received and input valid`() =
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)
assertThat(stateTurbine.awaitItem()).isEqualTo(
State(
accountName = StringInputField(value = "", isValid = true),
displayName = StringInputField(value = "", isValid = true),
emailSignature = StringInputField(value = "", isValid = true),
),
)
assertThatAndTurbinesConsumed(
actual = effectTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(Effect.NavigateNext)
}
}
@Test
fun `should change state and not emit effect when OnNextClicked event received and input invalid`() =
runTest {
val viewModel = AccountOptionsViewModel(
validator = FakeAccountOptionsValidator(
accountNameAnswer = ValidationResult.Failure(TestError),
),
accountStateRepository = InMemoryAccountStateRepository(),
)
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 = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(
State(
accountName = StringInputField(value = "", error = TestError, isValid = false),
displayName = StringInputField(value = "", isValid = true),
emailSignature = StringInputField(value = "", isValid = true),
),
)
}
}
@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)
}
}
private object TestError : ValidationError
}

View file

@ -1,14 +0,0 @@
package app.k9mail.feature.account.setup.ui.options
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.Validator
internal class FakeAccountOptionsValidator(
private val accountNameAnswer: ValidationResult = ValidationResult.Success,
private val displayNameAnswer: ValidationResult = ValidationResult.Success,
private val emailSignatureAnswer: ValidationResult = ValidationResult.Success,
) : Validator {
override fun validateAccountName(accountName: String): ValidationResult = accountNameAnswer
override fun validateDisplayName(displayName: String): ValidationResult = displayNameAnswer
override fun validateEmailSignature(emailSignature: String): ValidationResult = emailSignatureAnswer
}

View file

@ -1,22 +0,0 @@
package app.k9mail.feature.account.setup.ui.options
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.Effect
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.Event
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.State
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract.ViewModel
class FakeAccountOptionsViewModel(
initialState: State = State(),
) : BaseViewModel<State, Event, Effect>(initialState), ViewModel {
val events = mutableListOf<Event>()
override fun event(event: Event) {
events.add(event)
}
fun effect(effect: Effect) {
emitEffect(effect)
}
}