Add save settings to edit account

This commit is contained in:
Wolf-Martell Montwé 2023-09-19 11:04:14 +02:00
parent c29bc90535
commit b90301ec75
No known key found for this signature in database
GPG key ID: 6D45B21512ACBF72
36 changed files with 1134 additions and 144 deletions

View file

@ -5,7 +5,7 @@ import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import app.k9mail.feature.account.edit.navigation.accountEditRoute
import app.k9mail.feature.account.edit.navigation.navigateToAccountEditConfigIncoming
import app.k9mail.feature.account.edit.navigation.navigateToAccountEditIncomingServerSettings
import app.k9mail.feature.account.setup.navigation.accountSetupRoute
import app.k9mail.feature.account.setup.navigation.navigateToAccountSetup
import app.k9mail.feature.onboarding.navigation.NAVIGATION_ROUTE_ONBOARDING
@ -30,7 +30,7 @@ fun FeatureNavHost(
accountSetupRoute(
onBack = navController::popBackStack,
onFinish = { accountUuid ->
navController.navigateToAccountEditConfigIncoming(accountUuid)
navController.navigateToAccountEditIncomingServerSettings(accountUuid)
},
)
accountEditRoute(

View file

@ -1,7 +1,6 @@
package app.k9mail.core.ui.compose.designsystem.molecule
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@ -14,7 +13,6 @@ import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.atom.text.TextSubtitle1
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun ContentLoadingErrorView(
state: ContentLoadingErrorState,

View file

@ -17,7 +17,7 @@ import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
@Composable
internal fun SuccessView(
fun SuccessView(
message: String,
modifier: Modifier = Modifier,
) {

View file

@ -5,7 +5,7 @@ import com.fsck.k9.mail.ServerSettings
interface AccountEditExternalContract {
sealed interface AccountUpdaterResult {
data class Success(val message: String) : AccountUpdaterResult
data class Success(val accountUuid: String) : AccountUpdaterResult
data class Failure(val error: AccountUpdaterFailure) : AccountUpdaterResult
}

View file

@ -2,9 +2,13 @@ package app.k9mail.feature.account.edit
import app.k9mail.feature.account.common.featureAccountCommonModule
import app.k9mail.feature.account.edit.domain.AccountEditDomainContract
import app.k9mail.feature.account.edit.domain.usecase.GetAccountState
import app.k9mail.feature.account.edit.domain.usecase.LoadAccountState
import app.k9mail.feature.account.edit.ui.EditIncomingServerSettingsViewModel
import app.k9mail.feature.account.edit.ui.EditOutgoingServerSettingsViewModel
import app.k9mail.feature.account.edit.domain.usecase.SaveServerSettings
import app.k9mail.feature.account.edit.ui.server.settings.modify.ModifyIncomingServerSettingsViewModel
import app.k9mail.feature.account.edit.ui.server.settings.modify.ModifyOutgoingServerSettingsViewModel
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveIncomingServerSettingsViewModel
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveOutgoingServerSettingsViewModel
import app.k9mail.feature.account.oauth.featureAccountOAuthModule
import app.k9mail.feature.account.server.certificate.featureAccountServerCertificateModule
import app.k9mail.feature.account.server.settings.featureAccountServerSettingsModule
@ -28,8 +32,21 @@ val featureAccountEditModule = module {
)
}
factory<AccountEditDomainContract.UseCase.GetAccountState> {
GetAccountState(
accountStateRepository = get(),
)
}
factory<AccountEditDomainContract.UseCase.SaveServerSettings> {
SaveServerSettings(
getAccountState = get(),
serverSettingsUpdater = get(),
)
}
viewModel { (accountUuid: String) ->
EditIncomingServerSettingsViewModel(
ModifyIncomingServerSettingsViewModel(
accountUuid = accountUuid,
accountStateLoader = get(),
validator = get(),
@ -38,11 +55,25 @@ val featureAccountEditModule = module {
}
viewModel { (accountUuid: String) ->
EditOutgoingServerSettingsViewModel(
ModifyOutgoingServerSettingsViewModel(
accountUuid = accountUuid,
accountStateLoader = get(),
validator = get(),
accountStateRepository = get(),
)
}
viewModel { (accountUuid: String) ->
SaveIncomingServerSettingsViewModel(
accountUuid = accountUuid,
saveServerSettings = get(),
)
}
viewModel { (accountUuid: String) ->
SaveOutgoingServerSettingsViewModel(
accountUuid = accountUuid,
saveServerSettings = get(),
)
}
}

View file

@ -9,5 +9,13 @@ interface AccountEditDomainContract {
fun interface LoadAccountState {
suspend fun execute(accountUuid: String): AccountState
}
fun interface GetAccountState {
suspend fun execute(accountUuid: String): AccountState
}
fun interface SaveServerSettings {
suspend fun execute(accountUuid: String, isIncoming: Boolean)
}
}
}

View file

@ -0,0 +1,18 @@
package app.k9mail.feature.account.edit.domain.usecase
import app.k9mail.feature.account.common.domain.AccountDomainContract
import app.k9mail.feature.account.common.domain.entity.AccountState
import app.k9mail.feature.account.edit.domain.AccountEditDomainContract.UseCase
class GetAccountState(
private val accountStateRepository: AccountDomainContract.AccountStateRepository,
) : UseCase.GetAccountState {
override suspend fun execute(accountUuid: String): AccountState {
val accountState = accountStateRepository.getState()
return if (accountState.uuid == accountUuid) {
accountState
} else {
error("Account state for $accountUuid not found")
}
}
}

View file

@ -10,20 +10,14 @@ class LoadAccountState(
private val accountStateRepository: AccountDomainContract.AccountStateRepository,
) : AccountEditDomainContract.UseCase.LoadAccountState {
override suspend fun execute(accountUuid: String): AccountState {
val accountState = accountStateRepository.getState()
return if (accountState.uuid == accountUuid) {
accountState
} else {
loadState(accountUuid)
}
}
private suspend fun loadState(accountUuid: String): AccountState {
val accountState = accountStateLoader.loadAccountState(accountUuid)
return if (accountState != null) {
accountState
if (accountState != null) {
accountStateRepository.setState(accountState)
} else {
AccountState(uuid = accountUuid)
}.also { accountStateRepository.setState(it) }
error("Account state for $accountUuid not found")
}
return accountState
}
}

View file

@ -0,0 +1,42 @@
package app.k9mail.feature.account.edit.domain.usecase
import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountServerSettingsUpdater
import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountUpdaterResult
import app.k9mail.feature.account.edit.domain.AccountEditDomainContract.UseCase
import com.fsck.k9.mail.ServerSettings
class SaveServerSettings(
private val getAccountState: UseCase.GetAccountState,
private val serverSettingsUpdater: AccountServerSettingsUpdater,
) : UseCase.SaveServerSettings {
override suspend fun execute(accountUuid: String, isIncoming: Boolean) {
val serverSettings = loadServerSettings(accountUuid, isIncoming)
if (serverSettings != null) {
updateServerSettings(accountUuid, isIncoming, serverSettings)
} else {
error("Server settings not found")
}
}
private suspend fun loadServerSettings(accountUuid: String, isIncoming: Boolean): ServerSettings? {
val accountState = getAccountState.execute(accountUuid)
return if (isIncoming) {
accountState.incomingServerSettings
} else {
accountState.outgoingServerSettings
}
}
private suspend fun updateServerSettings(accountUuid: String, isIncoming: Boolean, serverSettings: ServerSettings) {
val result = serverSettingsUpdater.updateServerSettings(
accountUuid = accountUuid,
isIncoming = isIncoming,
serverSettings = serverSettings,
)
if (result is AccountUpdaterResult.Failure) {
error("Server settings update failed")
}
}
}

View file

@ -6,23 +6,23 @@ import androidx.navigation.NavType
import androidx.navigation.navArgument
import app.k9mail.core.ui.compose.common.navigation.deepLinkComposable
import app.k9mail.core.ui.compose.common.navigation.getStringArgument
import app.k9mail.feature.account.edit.ui.EditIncomingServerSettingsNavHost
import app.k9mail.feature.account.edit.ui.EditOutgoingServerSettingsNavHost
import app.k9mail.feature.account.edit.ui.server.settings.EditIncomingServerSettingsNavHost
import app.k9mail.feature.account.edit.ui.server.settings.EditOutgoingServerSettingsNavHost
internal const val ARGUMENT_ACCOUNT_UUID = "accountUuid"
const val NAVIGATION_ROUTE_ACCOUNT_EDIT_CONFIG_INCOMING = "account/edit/config/incoming/{accountUuid}"
const val NAVIGATION_ROUTE_ACCOUNT_EDIT_CONFIG_OUTGOING = "account/edit/config/outgoing/{accountUuid}"
const val NAVIGATION_ROUTE_ACCOUNT_EDIT_SERVER_SETTINGS_INCOMING = "account/edit/server/settings/incoming/{accountUuid}"
const val NAVIGATION_ROUTE_ACCOUNT_EDIT_SERVER_SETTINGS_OUTGOING = "account/edit/server/settings/outgoing/{accountUuid}"
fun NavController.navigateToAccountEditConfigIncoming(accountUuid: String) {
fun NavController.navigateToAccountEditIncomingServerSettings(accountUuid: String) {
navigate(
route = NAVIGATION_ROUTE_ACCOUNT_EDIT_CONFIG_INCOMING.withAccountUuid(accountUuid),
route = NAVIGATION_ROUTE_ACCOUNT_EDIT_SERVER_SETTINGS_INCOMING.withAccountUuid(accountUuid),
)
}
fun NavController.navigateToAccountEditConfigOutgoing(accountUuid: String) {
fun NavController.navigateToAccountEditOutgoingServerSettings(accountUuid: String) {
navigate(
route = NAVIGATION_ROUTE_ACCOUNT_EDIT_CONFIG_OUTGOING.withAccountUuid(accountUuid),
route = NAVIGATION_ROUTE_ACCOUNT_EDIT_SERVER_SETTINGS_OUTGOING.withAccountUuid(accountUuid),
)
}
@ -31,7 +31,7 @@ fun NavGraphBuilder.accountEditRoute(
onFinish: () -> Unit,
) {
deepLinkComposable(
route = NAVIGATION_ROUTE_ACCOUNT_EDIT_CONFIG_INCOMING,
route = NAVIGATION_ROUTE_ACCOUNT_EDIT_SERVER_SETTINGS_INCOMING,
arguments = listOf(
navArgument(ARGUMENT_ACCOUNT_UUID) {
type = NavType.StringType
@ -46,7 +46,7 @@ fun NavGraphBuilder.accountEditRoute(
)
}
deepLinkComposable(
route = NAVIGATION_ROUTE_ACCOUNT_EDIT_CONFIG_OUTGOING,
route = NAVIGATION_ROUTE_ACCOUNT_EDIT_SERVER_SETTINGS_OUTGOING,
arguments = listOf(
navArgument(ARGUMENT_ACCOUNT_UUID) {
type = NavType.StringType

View file

@ -1,52 +0,0 @@
package app.k9mail.feature.account.edit.ui
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import app.k9mail.feature.account.server.settings.ui.incoming.IncomingServerSettingsScreen
import app.k9mail.feature.account.server.validation.ui.IncomingServerValidationViewModel
import app.k9mail.feature.account.server.validation.ui.ServerValidationScreen
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
private const val NESTED_NAVIGATION_ROUTE_CONFIG = "config"
private const val NESTED_NAVIGATION_ROUTE_VALIDATION = "validation"
private fun NavController.navigateToValidation() {
navigate(NESTED_NAVIGATION_ROUTE_VALIDATION)
}
@Composable
fun EditIncomingServerSettingsNavHost(
accountUuid: String,
onFinish: () -> Unit,
onBack: () -> Unit,
) {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = NESTED_NAVIGATION_ROUTE_CONFIG,
) {
composable(route = NESTED_NAVIGATION_ROUTE_CONFIG) {
IncomingServerSettingsScreen(
onBack = onBack,
onNext = { navController.navigateToValidation() },
viewModel = koinViewModel<EditIncomingServerSettingsViewModel> {
parametersOf(accountUuid)
},
)
}
composable(route = NESTED_NAVIGATION_ROUTE_VALIDATION) {
ServerValidationScreen(
onBack = { navController.popBackStack() },
onNext = onFinish,
viewModel = koinViewModel<IncomingServerValidationViewModel> {
parametersOf(accountUuid)
},
)
}
}
}

View file

@ -0,0 +1,69 @@
package app.k9mail.feature.account.edit.ui.server.settings
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import app.k9mail.feature.account.edit.ui.server.settings.modify.ModifyIncomingServerSettingsViewModel
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveIncomingServerSettingsViewModel
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsScreen
import app.k9mail.feature.account.server.settings.ui.incoming.IncomingServerSettingsScreen
import app.k9mail.feature.account.server.validation.ui.IncomingServerValidationViewModel
import app.k9mail.feature.account.server.validation.ui.ServerValidationScreen
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
private const val NESTED_NAVIGATION_ROUTE_MODIFY = "modify"
private const val NESTED_NAVIGATION_ROUTE_VALIDATE = "validate"
private const val NESTED_NAVIGATION_ROUTE_SAVE = "save"
private fun NavController.navigateToValidate() {
navigate(NESTED_NAVIGATION_ROUTE_VALIDATE)
}
private fun NavController.navigateToSave() {
navigate(NESTED_NAVIGATION_ROUTE_SAVE)
}
@Composable
fun EditIncomingServerSettingsNavHost(
accountUuid: String,
onFinish: () -> Unit,
onBack: () -> Unit,
) {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = NESTED_NAVIGATION_ROUTE_MODIFY,
) {
composable(route = NESTED_NAVIGATION_ROUTE_MODIFY) {
IncomingServerSettingsScreen(
onBack = onBack,
onNext = { navController.navigateToValidate() },
viewModel = koinViewModel<ModifyIncomingServerSettingsViewModel> {
parametersOf(accountUuid)
},
)
}
composable(route = NESTED_NAVIGATION_ROUTE_VALIDATE) {
ServerValidationScreen(
onBack = { navController.popBackStack() },
onNext = { navController.navigateToSave() },
viewModel = koinViewModel<IncomingServerValidationViewModel> {
parametersOf(accountUuid)
},
)
}
composable(route = NESTED_NAVIGATION_ROUTE_SAVE) {
SaveServerSettingsScreen(
onNext = onFinish,
onBack = { navController.popBackStack(route = NESTED_NAVIGATION_ROUTE_MODIFY, inclusive = false) },
viewModel = koinViewModel<SaveIncomingServerSettingsViewModel> {
parametersOf(accountUuid)
},
)
}
}
}

View file

@ -1,10 +1,12 @@
package app.k9mail.feature.account.edit.ui
package app.k9mail.feature.account.edit.ui.server.settings
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveOutgoingServerSettingsViewModel
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsScreen
import app.k9mail.feature.account.server.settings.ui.outgoing.OutgoingServerSettingsScreen
import app.k9mail.feature.account.server.settings.ui.outgoing.OutgoingServerSettingsViewModel
import app.k9mail.feature.account.server.validation.ui.OutgoingServerValidationViewModel
@ -12,11 +14,16 @@ import app.k9mail.feature.account.server.validation.ui.ServerValidationScreen
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
private const val NESTED_NAVIGATION_ROUTE_CONFIG = "config"
private const val NESTED_NAVIGATION_ROUTE_VALIDATION = "validation"
private const val NESTED_NAVIGATION_ROUTE_MODIFY = "modify"
private const val NESTED_NAVIGATION_ROUTE_VALIDATE = "validate"
private const val NESTED_NAVIGATION_ROUTE_SAVE = "save"
private fun NavController.navigateToValidation() {
navigate(NESTED_NAVIGATION_ROUTE_VALIDATION)
private fun NavController.navigateToValidate() {
navigate(NESTED_NAVIGATION_ROUTE_VALIDATE)
}
private fun NavController.navigateToSave() {
navigate(NESTED_NAVIGATION_ROUTE_SAVE)
}
@Composable
@ -29,25 +36,34 @@ fun EditOutgoingServerSettingsNavHost(
NavHost(
navController = navController,
startDestination = NESTED_NAVIGATION_ROUTE_CONFIG,
startDestination = NESTED_NAVIGATION_ROUTE_MODIFY,
) {
composable(route = NESTED_NAVIGATION_ROUTE_CONFIG) {
composable(route = NESTED_NAVIGATION_ROUTE_MODIFY) {
OutgoingServerSettingsScreen(
onBack = onBack,
onNext = { navController.navigateToValidation() },
onNext = { navController.navigateToValidate() },
viewModel = koinViewModel<OutgoingServerSettingsViewModel> {
parametersOf(accountUuid)
},
)
}
composable(route = NESTED_NAVIGATION_ROUTE_VALIDATION) {
composable(route = NESTED_NAVIGATION_ROUTE_VALIDATE) {
ServerValidationScreen(
onBack = { navController.popBackStack() },
onNext = onFinish,
onNext = { navController.navigateToSave() },
viewModel = koinViewModel<OutgoingServerValidationViewModel> {
parametersOf(accountUuid)
},
)
}
composable(route = NESTED_NAVIGATION_ROUTE_SAVE) {
SaveServerSettingsScreen(
onNext = onFinish,
onBack = { navController.popBackStack(route = NESTED_NAVIGATION_ROUTE_MODIFY, inclusive = false) },
viewModel = koinViewModel<SaveOutgoingServerSettingsViewModel> {
parametersOf(accountUuid)
},
)
}
}
}

View file

@ -1,4 +1,4 @@
package app.k9mail.feature.account.edit.ui
package app.k9mail.feature.account.edit.ui.server.settings.modify
import androidx.lifecycle.viewModelScope
import app.k9mail.feature.account.common.domain.AccountDomainContract
@ -9,7 +9,7 @@ import app.k9mail.feature.account.server.settings.ui.incoming.IncomingServerSett
import app.k9mail.feature.account.server.settings.ui.incoming.toIncomingConfigState
import kotlinx.coroutines.launch
class EditIncomingServerSettingsViewModel(
class ModifyIncomingServerSettingsViewModel(
val accountUuid: String,
private val accountStateLoader: AccountEditDomainContract.UseCase.LoadAccountState,
validator: IncomingServerSettingsContract.Validator,

View file

@ -1,4 +1,4 @@
package app.k9mail.feature.account.edit.ui
package app.k9mail.feature.account.edit.ui.server.settings.modify
import androidx.lifecycle.viewModelScope
import app.k9mail.feature.account.common.domain.AccountDomainContract
@ -9,7 +9,7 @@ import app.k9mail.feature.account.server.settings.ui.outgoing.OutgoingServerSett
import app.k9mail.feature.account.server.settings.ui.outgoing.toOutgoingConfigState
import kotlinx.coroutines.launch
class EditOutgoingServerSettingsViewModel(
class ModifyOutgoingServerSettingsViewModel(
val accountUuid: String,
private val accountStateLoader: AccountEditDomainContract.UseCase.LoadAccountState,
validator: OutgoingServerSettingsContract.Validator,

View file

@ -0,0 +1,80 @@
package app.k9mail.feature.account.edit.ui.server.settings.save
import androidx.lifecycle.viewModelScope
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
import app.k9mail.feature.account.edit.domain.AccountEditDomainContract
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Effect
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Event
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Failure
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.State
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.ViewModel
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
private const val CONTINUE_NEXT_DELAY = 1500L
abstract class BaseSaveServerSettingsViewModel(
val accountUuid: String,
override val isIncoming: Boolean,
private val saveServerSettings: AccountEditDomainContract.UseCase.SaveServerSettings,
initialState: State = State(),
) : BaseViewModel<State, Event, Effect>(initialState),
ViewModel {
override fun event(event: Event) {
when (event) {
Event.SaveServerSettings -> onSaveServerSettings()
Event.OnNextClicked -> navigateNext()
Event.OnBackClicked -> navigateBack()
}
}
@Suppress("TooGenericExceptionCaught")
private fun onSaveServerSettings() {
viewModelScope.launch {
try {
saveServerSettings.execute(accountUuid, isIncoming)
updateSuccess()
} catch (e: Exception) {
updateFailure(Failure.SaveServerSettingsFailed(e.message ?: "Unknown error"))
}
}
}
private fun updateSuccess() {
updateState {
it.copy(
isLoading = false,
)
}
viewModelScope.launch {
delay(CONTINUE_NEXT_DELAY)
navigateNext()
}
}
private fun updateFailure(failure: Failure) {
updateState {
it.copy(
error = failure,
isLoading = false,
)
}
}
private fun navigateNext() {
if (state.value.isLoading || state.value.error != null) return
viewModelScope.coroutineContext.cancelChildren()
emitEffect(Effect.NavigateNext)
}
private fun navigateBack() {
if (state.value.isLoading || state.value.error == null) return
viewModelScope.coroutineContext.cancelChildren()
emitEffect(Effect.NavigateBack)
}
}

View file

@ -0,0 +1,15 @@
package app.k9mail.feature.account.edit.ui.server.settings.save
import app.k9mail.feature.account.edit.domain.AccountEditDomainContract.UseCase
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.State
class SaveIncomingServerSettingsViewModel(
accountUuid: String,
saveServerSettings: UseCase.SaveServerSettings,
initialState: State = State(),
) : BaseSaveServerSettingsViewModel(
accountUuid = accountUuid,
isIncoming = true,
saveServerSettings = saveServerSettings,
initialState = initialState,
)

View file

@ -0,0 +1,15 @@
package app.k9mail.feature.account.edit.ui.server.settings.save
import app.k9mail.feature.account.edit.domain.AccountEditDomainContract.UseCase
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.State
class SaveOutgoingServerSettingsViewModel(
accountUuid: String,
saveServerSettings: UseCase.SaveServerSettings,
initialState: State = State(),
) : BaseSaveServerSettingsViewModel(
accountUuid = accountUuid,
isIncoming = false,
saveServerSettings = saveServerSettings,
initialState = initialState,
)

View file

@ -0,0 +1,97 @@
package app.k9mail.feature.account.edit.ui.server.settings.save
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.molecule.ContentLoadingErrorView
import app.k9mail.core.ui.compose.designsystem.molecule.ErrorView
import app.k9mail.core.ui.compose.designsystem.molecule.LoadingView
import app.k9mail.core.ui.compose.designsystem.template.ResponsiveWidthContainer
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import app.k9mail.feature.account.common.ui.loadingerror.rememberContentLoadingErrorViewState
import app.k9mail.feature.account.common.ui.view.SuccessView
import app.k9mail.feature.account.edit.R
@Composable
fun SaveServerSettingsContent(
state: SaveServerSettingsContract.State,
contentPadding: PaddingValues,
modifier: Modifier = Modifier,
) {
ResponsiveWidthContainer(
modifier = Modifier
.testTag("SaveServerSettingsContent")
.padding(contentPadding)
.then(modifier),
) {
ContentLoadingErrorView(
state = rememberContentLoadingErrorViewState(state),
loading = {
LoadingView(
message = stringResource(id = R.string.account_edit_save_server_settings_loading_message),
)
},
error = {
ErrorView(
title = stringResource(id = R.string.account_edit_save_server_settings_error_message),
)
},
content = {
SuccessView(
message = stringResource(id = R.string.account_edit_save_server_settings_success_message),
)
},
modifier = Modifier.fillMaxSize(),
)
}
}
@Preview(showBackground = true)
@Composable
internal fun UpdateServerSettingsContentPreview() {
PreviewWithThemes {
SaveServerSettingsContent(
state = SaveServerSettingsContract.State(
isLoading = false,
error = null,
),
contentPadding = PaddingValues(),
)
}
}
@Preview(showBackground = true)
@Composable
internal fun UpdateServerSettingsContentLoadingPreview() {
PreviewWithThemes {
SaveServerSettingsContent(
state = SaveServerSettingsContract.State(
isLoading = true,
error = null,
),
contentPadding = PaddingValues(),
)
}
}
@Preview(showBackground = true)
@Composable
internal fun UpdateServerSettingsContentErrorPreview() {
PreviewWithThemes {
SaveServerSettingsContent(
state = SaveServerSettingsContract.State(
isLoading = false,
error = SaveServerSettingsContract.Failure.SaveServerSettingsFailed(
message = "Error",
),
),
contentPadding = PaddingValues(),
)
}
}

View file

@ -0,0 +1,33 @@
package app.k9mail.feature.account.edit.ui.server.settings.save
import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
import app.k9mail.feature.account.common.ui.loadingerror.LoadingErrorState
interface SaveServerSettingsContract {
interface ViewModel : UnidirectionalViewModel<State, Event, Effect> {
val isIncoming: Boolean
}
data class State(
override val error: Failure? = null,
override val isLoading: Boolean = true,
) : LoadingErrorState<Failure>
sealed interface Event {
data object SaveServerSettings : Event
data object OnNextClicked : Event
data object OnBackClicked : Event
}
sealed interface Effect {
data object NavigateNext : Effect
data object NavigateBack : Effect
}
sealed interface Failure {
data class SaveServerSettingsFailed(
val message: String,
) : Failure
}
}

View file

@ -0,0 +1,100 @@
package app.k9mail.feature.account.edit.ui.server.settings.save
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
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.AccountTopAppBarWithBackButton
import app.k9mail.feature.account.common.ui.WizardNavigationBar
import app.k9mail.feature.account.common.ui.WizardNavigationBarState
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Effect
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Event
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.ViewModel
import app.k9mail.feature.account.edit.ui.server.settings.save.fake.FakeSaveServerSettingsViewModel
@Composable
fun SaveServerSettingsScreen(
onNext: () -> Unit,
onBack: () -> Unit,
viewModel: ViewModel,
modifier: Modifier = Modifier,
) {
val (state, dispatch) = viewModel.observe { effect ->
when (effect) {
is Effect.NavigateNext -> onNext()
Effect.NavigateBack -> onBack()
}
}
LaunchedEffect(key1 = Unit) {
dispatch(Event.SaveServerSettings)
}
BackHandler {
dispatch(Event.OnBackClicked)
}
Scaffold(
topBar = {
AccountTopAppBarWithBackButton(
title = "Edit Server Settings",
onBackClicked = {
dispatch(Event.OnBackClicked)
},
)
},
bottomBar = {
WizardNavigationBar(
onNextClick = {
dispatch(Event.OnNextClicked)
},
onBackClick = {
dispatch(Event.OnBackClicked)
},
state = WizardNavigationBarState(
isNextEnabled = state.value.error == null && !state.value.isLoading,
isBackEnabled = state.value.error != null,
),
)
},
modifier = modifier,
) { innerPadding ->
SaveServerSettingsContent(
state = state.value,
contentPadding = innerPadding,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun SaveServerSettingsScreenK9Preview() {
K9Theme {
SaveServerSettingsScreen(
onNext = {},
onBack = {},
viewModel = FakeSaveServerSettingsViewModel(
isIncoming = true,
),
)
}
}
@Composable
@Preview(showBackground = true)
internal fun SaveServerSettingsScreenThunderbirdPreview() {
ThunderbirdTheme {
SaveServerSettingsScreen(
onNext = {},
onBack = {},
viewModel = FakeSaveServerSettingsViewModel(
isIncoming = true,
),
)
}
}

View file

@ -0,0 +1,23 @@
package app.k9mail.feature.account.edit.ui.server.settings.save.fake
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Effect
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Event
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.State
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.ViewModel
class FakeSaveServerSettingsViewModel(
override val isIncoming: Boolean,
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)
}
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="account_edit_save_server_settings_loading_message">Saving server settings in progress</string>
<string name="account_edit_save_server_settings_error_message">Saving server settings failed</string>
<string name="account_edit_save_server_settings_success_message">Saving server settings was successful</string>
</resources>

View file

@ -5,6 +5,7 @@ import app.k9mail.core.common.oauth.OAuthConfigurationFactory
import app.k9mail.feature.account.common.AccountCommonExternalContract
import app.k9mail.feature.account.common.domain.entity.AccountState
import app.k9mail.feature.account.common.domain.entity.InteractionMode
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract
import app.k9mail.feature.account.server.certificate.ui.ServerCertificateErrorContract
import app.k9mail.feature.account.server.settings.ui.incoming.IncomingServerSettingsContract
import app.k9mail.feature.account.server.settings.ui.outgoing.OutgoingServerSettingsContract
@ -46,6 +47,7 @@ class AccountEditModuleKtTest : KoinTest {
}
}
}
single<AccountEditExternalContract.AccountServerSettingsUpdater> { Mockito.mock() }
}
@Test
@ -59,6 +61,8 @@ class AccountEditModuleKtTest : KoinTest {
ServerCertificateErrorContract.State::class,
IncomingServerSettingsContract.State::class,
OutgoingServerSettingsContract.State::class,
SaveServerSettingsContract.State::class,
AccountEditExternalContract.AccountServerSettingsUpdater::class,
InteractionMode::class,
),
)

View file

@ -0,0 +1,90 @@
package app.k9mail.feature.account.edit.domain.usecase
import app.k9mail.feature.account.common.data.InMemoryAccountStateRepository
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.entity.AuthorizationState
import app.k9mail.feature.account.common.domain.entity.MailConnectionSecurity
import assertk.assertFailure
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf
import assertk.assertions.prop
import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.ServerSettings
import kotlinx.coroutines.test.runTest
import org.junit.Test
class GetAccountStateTest {
@Test
fun `should get account state from repository`() = runTest {
val testSubject = GetAccountState(
accountStateRepository = InMemoryAccountStateRepository(state = ACCOUNT_STATE),
)
val result = testSubject.execute(ACCOUNT_UUID)
assertThat(result).isEqualTo(ACCOUNT_STATE)
}
@Test
fun `should throw exception WHEN account state repository contains state for different account uuid`() = runTest {
val testSubject = GetAccountState(
accountStateRepository = InMemoryAccountStateRepository(
state = ACCOUNT_STATE.copy(uuid = "differentAccountUuid"),
),
)
assertFailure {
testSubject.execute(ACCOUNT_UUID)
}.isInstanceOf<IllegalStateException>()
.prop(IllegalStateException::message)
.isEqualTo("Account state for $ACCOUNT_UUID not found")
}
private companion object {
const val ACCOUNT_UUID = "accountUuid"
const val EMAIL_ADDRESS = "test@example.com"
val INCOMING_SERVER_SETTINGS = ServerSettings(
type = "imap",
host = "imap.example.com",
port = 993,
connectionSecurity = MailConnectionSecurity.SSL_TLS_REQUIRED,
authenticationType = AuthType.PLAIN,
username = "user",
password = "password",
clientCertificateAlias = null,
)
val OUTGOING_SERVER_SETTINGS = ServerSettings(
type = "smtp",
host = "smtp.example.com",
port = 465,
connectionSecurity = MailConnectionSecurity.SSL_TLS_REQUIRED,
authenticationType = AuthType.PLAIN,
username = "user",
password = "password",
clientCertificateAlias = null,
)
val AUTHORIZATION_STATE = AuthorizationState("authorization state")
val OPTIONS = AccountOptions(
accountName = "accountName",
displayName = "displayName",
emailSignature = null,
checkFrequencyInMinutes = 15,
messageDisplayCount = 25,
showNotification = true,
)
val ACCOUNT_STATE = AccountState(
uuid = ACCOUNT_UUID,
emailAddress = EMAIL_ADDRESS,
incomingServerSettings = INCOMING_SERVER_SETTINGS,
outgoingServerSettings = OUTGOING_SERVER_SETTINGS,
authorizationState = AUTHORIZATION_STATE,
options = OPTIONS,
)
}
}

View file

@ -5,67 +5,46 @@ 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.entity.AuthorizationState
import app.k9mail.feature.account.common.domain.entity.MailConnectionSecurity
import assertk.assertFailure
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf
import assertk.assertions.prop
import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.ServerSettings
import kotlin.test.DefaultAsserter.fail
import kotlinx.coroutines.test.runTest
import org.junit.Test
class LoadAccountStateTest {
@Test
fun `should load account state WHEN account in state repository has different UUID`() = runTest {
fun `should load account state and update account state repository`() = runTest {
val accountStateRepository = InMemoryAccountStateRepository()
val testSubject = LoadAccountState(
accountStateLoader = { _ ->
ACCOUNT_STATE
},
accountStateRepository = InMemoryAccountStateRepository(
state = ACCOUNT_STATE.copy(uuid = "differentUuid"),
),
accountStateRepository = accountStateRepository,
)
val result = testSubject.execute(ACCOUNT_UUID)
assertThat(result).isEqualTo(ACCOUNT_STATE)
assertThat(accountStateRepository.getState()).isEqualTo(ACCOUNT_STATE)
}
@Test
fun `should return account state WHEN account in state repository has same UUID`() = runTest {
val testSubject = LoadAccountState(
accountStateLoader = { _ ->
fail("AccountStateLoader should not be called in this test")
},
accountStateRepository = InMemoryAccountStateRepository(
state = ACCOUNT_STATE,
),
)
val result = testSubject.execute(ACCOUNT_UUID)
assertThat(result).isEqualTo(ACCOUNT_STATE)
}
@Test
fun `should return empty account state WHEN account loader returns null`() = runTest {
fun `should throw exception WHEN account loader returns null`() = runTest {
val testSubject = LoadAccountState(
accountStateLoader = { null },
accountStateRepository = InMemoryAccountStateRepository(),
)
val result = testSubject.execute(ACCOUNT_UUID)
assertThat(result).isEqualTo(
AccountState(
uuid = ACCOUNT_UUID,
emailAddress = null,
incomingServerSettings = null,
outgoingServerSettings = null,
authorizationState = null,
options = null,
),
)
assertFailure {
testSubject.execute(ACCOUNT_UUID)
}.isInstanceOf<IllegalStateException>()
.prop(IllegalStateException::message)
.isEqualTo("Account state for $ACCOUNT_UUID not found")
}
private companion object {

View file

@ -0,0 +1,161 @@
package app.k9mail.feature.account.edit.domain.usecase
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.entity.AuthorizationState
import app.k9mail.feature.account.common.domain.entity.MailConnectionSecurity
import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountUpdaterFailure
import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountUpdaterResult
import assertk.assertFailure
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf
import assertk.assertions.prop
import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.ServerSettings
import kotlinx.coroutines.test.runTest
import org.junit.Test
class SaveServerSettingsTest {
@Test
fun `should get account state and update incoming server settings`() = runTest {
var recordedAccountUuid: String? = null
var recordedIsIncoming: Boolean? = null
var recordedServerSettings: ServerSettings? = null
val testSubject = SaveServerSettings(
getAccountState = { _ -> ACCOUNT_STATE },
serverSettingsUpdater = { accountUuid, isIncoming, serverSettings ->
recordedAccountUuid = accountUuid
recordedIsIncoming = isIncoming
recordedServerSettings = serverSettings
AccountUpdaterResult.Success(accountUuid)
},
)
testSubject.execute(ACCOUNT_UUID, isIncoming = true)
assertThat(recordedAccountUuid).isEqualTo(ACCOUNT_UUID)
assertThat(recordedIsIncoming).isEqualTo(true)
assertThat(recordedServerSettings).isEqualTo(INCOMING_SERVER_SETTINGS)
}
@Test
fun `should throw exception WHEN no incoming server settings present`() = runTest {
val testSubject = SaveServerSettings(
getAccountState = { _ -> ACCOUNT_STATE.copy(incomingServerSettings = null) },
serverSettingsUpdater = { accountUuid, _, _ ->
AccountUpdaterResult.Success(accountUuid)
},
)
assertFailure {
testSubject.execute(ACCOUNT_UUID, isIncoming = true)
}.isInstanceOf<IllegalStateException>()
.prop(IllegalStateException::message)
.isEqualTo("Server settings not found")
}
@Test
fun `should get account state and update outgoing server settings`() = runTest {
var recordedAccountUuid: String? = null
var recordedIsIncoming: Boolean? = null
var recordedServerSettings: ServerSettings? = null
val testSubject = SaveServerSettings(
getAccountState = { _ -> ACCOUNT_STATE },
serverSettingsUpdater = { accountUuid, isIncoming, serverSettings ->
recordedAccountUuid = accountUuid
recordedIsIncoming = isIncoming
recordedServerSettings = serverSettings
AccountUpdaterResult.Success(accountUuid)
},
)
testSubject.execute(ACCOUNT_UUID, isIncoming = false)
assertThat(recordedAccountUuid).isEqualTo(ACCOUNT_UUID)
assertThat(recordedIsIncoming).isEqualTo(false)
assertThat(recordedServerSettings).isEqualTo(OUTGOING_SERVER_SETTINGS)
}
@Test
fun `should throw exception WHEN no outgoing server settings present`() = runTest {
val testSubject = SaveServerSettings(
getAccountState = { _ -> ACCOUNT_STATE.copy(outgoingServerSettings = null) },
serverSettingsUpdater = { accountUuid, _, _ ->
AccountUpdaterResult.Success(accountUuid)
},
)
assertFailure {
testSubject.execute(ACCOUNT_UUID, isIncoming = false)
}.isInstanceOf<IllegalStateException>()
.prop(IllegalStateException::message)
.isEqualTo("Server settings not found")
}
@Test
fun `should throw exception WHEN update failed`() = runTest {
val testSubject = SaveServerSettings(
getAccountState = { _ -> ACCOUNT_STATE },
serverSettingsUpdater = { _, _, _ ->
AccountUpdaterResult.Failure(
AccountUpdaterFailure.AccountNotFound(ACCOUNT_UUID),
)
},
)
assertFailure {
testSubject.execute(ACCOUNT_UUID, isIncoming = true)
}.isInstanceOf<IllegalStateException>()
.prop(IllegalStateException::message)
.isEqualTo("Server settings update failed")
}
private companion object {
const val ACCOUNT_UUID = "accountUuid"
const val EMAIL_ADDRESS = "test@example.com"
val INCOMING_SERVER_SETTINGS = ServerSettings(
type = "imap",
host = "imap.example.com",
port = 993,
connectionSecurity = MailConnectionSecurity.SSL_TLS_REQUIRED,
authenticationType = AuthType.PLAIN,
username = "user",
password = "password",
clientCertificateAlias = null,
)
val OUTGOING_SERVER_SETTINGS = ServerSettings(
type = "smtp",
host = "smtp.example.com",
port = 465,
connectionSecurity = MailConnectionSecurity.SSL_TLS_REQUIRED,
authenticationType = AuthType.PLAIN,
username = "user",
password = "password",
clientCertificateAlias = null,
)
val AUTHORIZATION_STATE = AuthorizationState("authorization state")
val OPTIONS = AccountOptions(
accountName = "accountName",
displayName = "displayName",
emailSignature = null,
checkFrequencyInMinutes = 15,
messageDisplayCount = 25,
showNotification = true,
)
val ACCOUNT_STATE = AccountState(
uuid = ACCOUNT_UUID,
emailAddress = EMAIL_ADDRESS,
incomingServerSettings = INCOMING_SERVER_SETTINGS,
outgoingServerSettings = OUTGOING_SERVER_SETTINGS,
authorizationState = AUTHORIZATION_STATE,
options = OPTIONS,
)
}
}

View file

@ -1,4 +1,4 @@
package app.k9mail.feature.account.edit.ui
package app.k9mail.feature.account.edit.ui.server.settings.modify
import app.k9mail.core.ui.compose.testing.MainDispatcherRule
import app.k9mail.core.ui.compose.testing.mvi.assertThatAndMviTurbinesConsumed
@ -21,7 +21,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class EditIncomingServerSettingsViewModelTest {
class ModifyIncomingServerSettingsViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@ -45,7 +45,7 @@ class EditIncomingServerSettingsViewModelTest {
),
)
val testSubject = EditIncomingServerSettingsViewModel(
val testSubject = ModifyIncomingServerSettingsViewModel(
accountUuid = accountUuid,
accountStateLoader = { _ ->
delay(50)

View file

@ -1,4 +1,4 @@
package app.k9mail.feature.account.edit.ui
package app.k9mail.feature.account.edit.ui.server.settings.modify
import app.k9mail.core.ui.compose.testing.MainDispatcherRule
import app.k9mail.core.ui.compose.testing.mvi.assertThatAndMviTurbinesConsumed
@ -21,7 +21,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class EditOutgoingServerSettingsViewModelTest {
class ModifyOutgoingServerSettingsViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@ -44,7 +44,7 @@ class EditOutgoingServerSettingsViewModelTest {
extra = emptyMap(),
),
)
val testSubject = EditOutgoingServerSettingsViewModel(
val testSubject = ModifyOutgoingServerSettingsViewModel(
accountUuid = accountUuid,
accountStateLoader = { _ ->
delay(50)

View file

@ -0,0 +1,155 @@
package app.k9mail.feature.account.edit.ui.server.settings.save
import app.k9mail.core.ui.compose.testing.MainDispatcherRule
import app.k9mail.core.ui.compose.testing.mvi.assertThatAndEffectTurbineConsumed
import app.k9mail.core.ui.compose.testing.mvi.assertThatAndStateTurbineConsumed
import app.k9mail.core.ui.compose.testing.mvi.turbinesWithInitialStateCheck
import app.k9mail.feature.account.edit.domain.AccountEditDomainContract
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Effect
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Event
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Failure
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.State
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isNotNull
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class BaseSaveServerSettingsViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@Test
fun `should save server settings when SaveServerSettings event received and emit NavigateNext`() = runTest {
var recordedAccountUuid: String? = null
var recordedIsIncoming: Boolean? = null
val testSubject = TestSaveServerSettingsViewModel(
accountUuid = ACCOUNT_UUID,
saveServerSettings = { accountUuid, isIncoming ->
recordedAccountUuid = accountUuid
recordedIsIncoming = isIncoming
},
)
val turbines = turbinesWithInitialStateCheck(testSubject, State())
testSubject.event(Event.SaveServerSettings)
turbines.assertThatAndStateTurbineConsumed {
isEqualTo(State(isLoading = false))
}
assertThat(recordedAccountUuid).isNotNull().isEqualTo(ACCOUNT_UUID)
assertThat(recordedIsIncoming).isNotNull().isEqualTo(true)
turbines.assertThatAndEffectTurbineConsumed {
isEqualTo(Effect.NavigateNext)
}
}
@Test
fun `should set error state when save settings failed`() = runTest {
val testSubject = TestSaveServerSettingsViewModel(
accountUuid = ACCOUNT_UUID,
saveServerSettings = { _, _ ->
error("Test exception")
},
)
val turbines = turbinesWithInitialStateCheck(testSubject, State())
testSubject.event(Event.SaveServerSettings)
turbines.assertThatAndStateTurbineConsumed {
isEqualTo(
State(
error = Failure.SaveServerSettingsFailed("Test exception"),
isLoading = false,
),
)
}
}
@Test
fun `should prevent navigation effects when in loading state`() = runTest {
val testSubject = TestSaveServerSettingsViewModel(
accountUuid = ACCOUNT_UUID,
saveServerSettings = { _, _ ->
// Do nothing
},
initialState = State(isLoading = true),
)
val turbines = turbinesWithInitialStateCheck(testSubject, State(isLoading = true))
testSubject.event(Event.OnNextClicked)
turbines.effectTurbine.ensureAllEventsConsumed()
testSubject.event(Event.OnBackClicked)
turbines.effectTurbine.ensureAllEventsConsumed()
}
@Test
fun `should allow NavigateNext when no error and not loading`() = runTest {
val testSubject = TestSaveServerSettingsViewModel(
accountUuid = ACCOUNT_UUID,
saveServerSettings = { _, _ ->
// Do nothing
},
initialState = State(isLoading = false),
)
val turbines = turbinesWithInitialStateCheck(testSubject, State(isLoading = false))
testSubject.event(Event.OnNextClicked)
turbines.assertThatAndEffectTurbineConsumed {
isEqualTo(Effect.NavigateNext)
}
testSubject.event(Event.OnBackClicked)
turbines.effectTurbine.ensureAllEventsConsumed()
}
@Test
fun `should allow NavigateBack when error and not loading`() = runTest {
val failure = Failure.SaveServerSettingsFailed("Test exception")
val testSubject = TestSaveServerSettingsViewModel(
accountUuid = ACCOUNT_UUID,
saveServerSettings = { _, _ ->
// Do nothing
},
initialState = State(
isLoading = false,
error = failure,
),
)
val turbines = turbinesWithInitialStateCheck(testSubject, State(isLoading = false, error = failure))
testSubject.event(Event.OnNextClicked)
turbines.effectTurbine.ensureAllEventsConsumed()
testSubject.event(Event.OnBackClicked)
turbines.assertThatAndEffectTurbineConsumed {
isEqualTo(Effect.NavigateBack)
}
}
private class TestSaveServerSettingsViewModel(
accountUuid: String,
saveServerSettings: AccountEditDomainContract.UseCase.SaveServerSettings,
initialState: State = State(),
) : BaseSaveServerSettingsViewModel(
accountUuid = accountUuid,
isIncoming = true,
saveServerSettings = saveServerSettings,
initialState = initialState,
)
private companion object {
const val ACCOUNT_UUID = "accountUuid"
}
}

View file

@ -0,0 +1,18 @@
package app.k9mail.feature.account.edit.ui.server.settings.save
import assertk.assertThat
import assertk.assertions.isTrue
import org.junit.Test
class SaveIncomingServerSettingsViewModelTest {
@Test
fun `should set is incoming to true`() {
val testSubject = SaveIncomingServerSettingsViewModel(
accountUuid = "accountUuid",
saveServerSettings = { _, _ -> },
)
assertThat(testSubject.isIncoming).isTrue()
}
}

View file

@ -0,0 +1,18 @@
package app.k9mail.feature.account.edit.ui.server.settings.save
import assertk.assertThat
import assertk.assertions.isFalse
import org.junit.Test
class SaveOutgoingServerSettingsViewModelTest {
@Test
fun `should set is incoming to true`() {
val testSubject = SaveOutgoingServerSettingsViewModel(
accountUuid = "accountUuid",
saveServerSettings = { _, _ -> },
)
assertThat(testSubject.isIncoming).isFalse()
}
}

View file

@ -0,0 +1,49 @@
package app.k9mail.feature.account.edit.ui.server.settings.save
import app.k9mail.core.ui.compose.testing.ComposeTest
import app.k9mail.core.ui.compose.testing.setContent
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Effect
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.State
import app.k9mail.feature.account.edit.ui.server.settings.save.fake.FakeSaveServerSettingsViewModel
import assertk.assertThat
import assertk.assertions.isEqualTo
import kotlinx.coroutines.test.runTest
import org.junit.Test
class SaveServerSettingsScreenKtTest : ComposeTest() {
@Test
fun `should delegate navigation effects`() = runTest {
val initialState = State(
isLoading = false,
error = null,
)
val viewModel = FakeSaveServerSettingsViewModel(
isIncoming = true,
initialState = initialState,
)
var onNextCounter = 0
var onBackCounter = 0
setContent {
SaveServerSettingsScreen(
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,21 @@
package app.k9mail.feature.account.edit.ui.server.settings.save
import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.State
import assertk.assertThat
import assertk.assertions.isEqualTo
import org.junit.Test
class SaveServerSettingsStateTest {
@Test
fun `should set default values`() {
val state = State()
assertThat(state).isEqualTo(
State(
error = null,
isLoading = true,
),
)
}
}

View file

@ -11,7 +11,7 @@ import assertk.assertThat
import assertk.assertions.isEqualTo
import org.junit.Test
class IncomingServerSettingsConfigStateTest {
class IncomingServerSettingsStateTest {
@Test
fun `should set default values`() {

View file

@ -7,8 +7,8 @@ import androidx.activity.ComponentActivity
import androidx.core.view.WindowCompat
import app.k9mail.core.ui.compose.common.activity.setActivityContent
import app.k9mail.core.ui.compose.common.navigation.toDeepLinkUri
import app.k9mail.feature.account.edit.navigation.NAVIGATION_ROUTE_ACCOUNT_EDIT_CONFIG_INCOMING
import app.k9mail.feature.account.edit.navigation.NAVIGATION_ROUTE_ACCOUNT_EDIT_CONFIG_OUTGOING
import app.k9mail.feature.account.edit.navigation.NAVIGATION_ROUTE_ACCOUNT_EDIT_SERVER_SETTINGS_INCOMING
import app.k9mail.feature.account.edit.navigation.NAVIGATION_ROUTE_ACCOUNT_EDIT_SERVER_SETTINGS_OUTGOING
import app.k9mail.feature.account.edit.navigation.withAccountUuid
import app.k9mail.feature.account.setup.navigation.NAVIGATION_ROUTE_ACCOUNT_SETUP
import app.k9mail.feature.launcher.ui.FeatureLauncherApp
@ -47,7 +47,8 @@ class FeatureLauncherActivity : ComponentActivity() {
@JvmStatic
fun launchEditIncomingSettings(context: Context, accountUuid: String) {
val intent = Intent(context, FeatureLauncherActivity::class.java).apply {
data = NAVIGATION_ROUTE_ACCOUNT_EDIT_CONFIG_INCOMING.withAccountUuid(accountUuid).toDeepLinkUri()
data = NAVIGATION_ROUTE_ACCOUNT_EDIT_SERVER_SETTINGS_INCOMING
.withAccountUuid(accountUuid).toDeepLinkUri()
}
context.startActivity(intent)
}
@ -55,7 +56,8 @@ class FeatureLauncherActivity : ComponentActivity() {
@JvmStatic
fun launchEditOutgoingSettings(context: Context, accountUuid: String) {
val intent = Intent(context, FeatureLauncherActivity::class.java).apply {
data = NAVIGATION_ROUTE_ACCOUNT_EDIT_CONFIG_OUTGOING.withAccountUuid(accountUuid).toDeepLinkUri()
data = NAVIGATION_ROUTE_ACCOUNT_EDIT_SERVER_SETTINGS_OUTGOING
.withAccountUuid(accountUuid).toDeepLinkUri()
}
context.startActivity(intent)
}