Merge pull request #7353 from thunderbird/create_account_screen
Add "create account" screen
This commit is contained in:
commit
3dc3b526ed
20 changed files with 584 additions and 57 deletions
|
@ -13,6 +13,7 @@ import app.k9mail.feature.account.setup.ui.AccountSetupViewModel
|
|||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract
|
||||
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
|
||||
|
@ -57,7 +58,6 @@ val featureAccountSetupModule: Module = module {
|
|||
|
||||
viewModel {
|
||||
AccountSetupViewModel(
|
||||
createAccount = get(),
|
||||
accountStateRepository = get(),
|
||||
)
|
||||
}
|
||||
|
@ -76,4 +76,11 @@ val featureAccountSetupModule: Module = module {
|
|||
accountStateRepository = get(),
|
||||
)
|
||||
}
|
||||
|
||||
viewModel {
|
||||
CreateAccountViewModel(
|
||||
createAccount = get(),
|
||||
accountStateRepository = get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package app.k9mail.feature.account.setup.domain
|
|||
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
|
||||
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
|
||||
import app.k9mail.feature.account.common.domain.entity.AccountOptions
|
||||
import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator.AccountCreatorResult
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
|
||||
interface DomainContract {
|
||||
|
@ -19,7 +20,7 @@ interface DomainContract {
|
|||
outgoingServerSettings: ServerSettings,
|
||||
authorizationState: String?,
|
||||
options: AccountOptions,
|
||||
): String
|
||||
): AccountCreatorResult
|
||||
}
|
||||
|
||||
fun interface ValidateEmailAddress {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
package app.k9mail.feature.account.setup.domain.entity
|
||||
|
||||
@JvmInline
|
||||
value class AccountUuid(val value: String)
|
|
@ -18,7 +18,7 @@ class CreateAccount(
|
|||
outgoingServerSettings: ServerSettings,
|
||||
authorizationState: String?,
|
||||
options: AccountOptions,
|
||||
): String {
|
||||
): AccountCreatorResult {
|
||||
val account = Account(
|
||||
uuid = uuidGenerator(),
|
||||
emailAddress = emailAddress,
|
||||
|
@ -28,9 +28,6 @@ class CreateAccount(
|
|||
options = options,
|
||||
)
|
||||
|
||||
return when (val result = accountCreator.createAccount(account)) {
|
||||
is AccountCreatorResult.Success -> result.accountUuid
|
||||
is AccountCreatorResult.Error -> "" // TODO change to meaningful error
|
||||
}
|
||||
return accountCreator.createAccount(account)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package app.k9mail.feature.account.setup.ui
|
||||
|
||||
import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
|
||||
import app.k9mail.feature.account.setup.domain.entity.AccountUuid
|
||||
|
||||
interface AccountSetupContract {
|
||||
|
||||
|
@ -11,6 +12,7 @@ interface AccountSetupContract {
|
|||
OUTGOING_CONFIG,
|
||||
OUTGOING_VALIDATION,
|
||||
OPTIONS,
|
||||
CREATE_ACCOUNT,
|
||||
}
|
||||
|
||||
interface ViewModel : UnidirectionalViewModel<State, Event, Effect>
|
||||
|
@ -27,6 +29,8 @@ interface AccountSetupContract {
|
|||
|
||||
object OnNext : Event
|
||||
object OnBack : Event
|
||||
|
||||
data class OnAccountCreated(val accountUuid: AccountUuid) : Event
|
||||
}
|
||||
|
||||
sealed interface Effect {
|
||||
|
|
|
@ -20,6 +20,9 @@ import app.k9mail.feature.account.setup.ui.AccountSetupContract.ViewModel
|
|||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryScreen
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryViewModel
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountScreen
|
||||
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.AccountOptionsScreen
|
||||
import app.k9mail.feature.account.setup.ui.options.AccountOptionsViewModel
|
||||
|
@ -39,6 +42,7 @@ fun AccountSetupScreen(
|
|||
outgoingValidationViewModel: ServerValidationContract.ViewModel =
|
||||
koinViewModel<OutgoingServerValidationViewModel>(),
|
||||
optionsViewModel: AccountOptionsContract.ViewModel = koinViewModel<AccountOptionsViewModel>(),
|
||||
createAccountViewModel: CreateAccountContract.ViewModel = koinViewModel<CreateAccountViewModel>(),
|
||||
) {
|
||||
val (state, dispatch) = viewModel.observe { effect ->
|
||||
when (effect) {
|
||||
|
@ -101,6 +105,14 @@ fun AccountSetupScreen(
|
|||
viewModel = optionsViewModel,
|
||||
)
|
||||
}
|
||||
|
||||
SetupStep.CREATE_ACCOUNT -> {
|
||||
CreateAccountScreen(
|
||||
onNext = { accountUuid -> dispatch(Event.OnAccountCreated(accountUuid)) },
|
||||
onBack = { dispatch(Event.OnBack) },
|
||||
viewModel = createAccountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,16 @@
|
|||
package app.k9mail.feature.account.setup.ui
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
|
||||
import app.k9mail.feature.account.common.domain.AccountDomainContract
|
||||
import app.k9mail.feature.account.common.domain.entity.AuthorizationState
|
||||
import app.k9mail.feature.account.setup.domain.DomainContract.UseCase
|
||||
import app.k9mail.feature.account.setup.domain.entity.AccountUuid
|
||||
import app.k9mail.feature.account.setup.ui.AccountSetupContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.AccountSetupContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.AccountSetupContract.SetupStep
|
||||
import app.k9mail.feature.account.setup.ui.AccountSetupContract.State
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
class AccountSetupViewModel(
|
||||
private val createAccount: UseCase.CreateAccount,
|
||||
private val accountStateRepository: AccountDomainContract.AccountStateRepository,
|
||||
initialState: State = State(),
|
||||
) : BaseViewModel<State, Event, Effect>(initialState), AccountSetupContract.ViewModel {
|
||||
|
@ -24,6 +21,8 @@ class AccountSetupViewModel(
|
|||
|
||||
Event.OnBack -> onBack()
|
||||
Event.OnNext -> onNext()
|
||||
|
||||
is Event.OnAccountCreated -> navigateNext(event.accountUuid)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,7 +68,11 @@ class AccountSetupViewModel(
|
|||
changeToSetupStep(SetupStep.OPTIONS)
|
||||
}
|
||||
|
||||
SetupStep.OPTIONS -> onFinish()
|
||||
SetupStep.OPTIONS -> {
|
||||
changeToSetupStep(SetupStep.CREATE_ACCOUNT)
|
||||
}
|
||||
|
||||
SetupStep.CREATE_ACCOUNT -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,6 +102,8 @@ class AccountSetupViewModel(
|
|||
} else {
|
||||
changeToSetupStep(SetupStep.OUTGOING_CONFIG)
|
||||
}
|
||||
|
||||
SetupStep.CREATE_ACCOUNT -> changeToSetupStep(SetupStep.OPTIONS)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,23 +119,7 @@ class AccountSetupViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onFinish() {
|
||||
val accountState = accountStateRepository.getState()
|
||||
|
||||
viewModelScope.launch {
|
||||
val result = createAccount.execute(
|
||||
emailAddress = accountState.emailAddress ?: "",
|
||||
incomingServerSettings = accountState.incomingServerSettings!!,
|
||||
outgoingServerSettings = accountState.outgoingServerSettings!!,
|
||||
authorizationState = accountState.authorizationState?.state,
|
||||
options = accountState.options!!,
|
||||
)
|
||||
|
||||
navigateNext(result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateNext(accountUuid: String) = emitEffect(Effect.NavigateNext(accountUuid))
|
||||
private fun navigateNext(accountUuid: AccountUuid) = emitEffect(Effect.NavigateNext(accountUuid.value))
|
||||
|
||||
private fun navigateBack() = emitEffect(Effect.NavigateBack)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package app.k9mail.feature.account.setup.ui.createaccount
|
||||
|
||||
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 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.feature.account.common.ui.loadingerror.rememberContentLoadingErrorViewState
|
||||
import app.k9mail.feature.account.common.ui.view.SuccessView
|
||||
import app.k9mail.feature.account.setup.R
|
||||
|
||||
@Composable
|
||||
internal fun CreateAccountContent(
|
||||
state: CreateAccountContract.State,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ResponsiveWidthContainer(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.testTag("CreateAccountContent")
|
||||
.then(modifier),
|
||||
) {
|
||||
ContentLoadingErrorView(
|
||||
state = rememberContentLoadingErrorViewState(state),
|
||||
loading = {
|
||||
LoadingView(
|
||||
message = stringResource(R.string.account_setup_create_account_creating),
|
||||
)
|
||||
},
|
||||
error = {
|
||||
ErrorView(
|
||||
title = stringResource(R.string.account_setup_create_account_error),
|
||||
)
|
||||
},
|
||||
content = {
|
||||
SuccessView(
|
||||
message = stringResource(R.string.account_setup_create_account_created),
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package app.k9mail.feature.account.setup.ui.createaccount
|
||||
|
||||
import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
|
||||
import app.k9mail.feature.account.common.ui.loadingerror.LoadingErrorState
|
||||
import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator.AccountCreatorResult.Error
|
||||
import app.k9mail.feature.account.setup.domain.entity.AccountUuid
|
||||
|
||||
interface CreateAccountContract {
|
||||
|
||||
interface ViewModel : UnidirectionalViewModel<State, Event, Effect>
|
||||
|
||||
data class State(
|
||||
override val isLoading: Boolean = true,
|
||||
override val error: Error? = null,
|
||||
) : LoadingErrorState<Error>
|
||||
|
||||
sealed interface Event {
|
||||
data object CreateAccount : Event
|
||||
data object OnBackClicked : Event
|
||||
}
|
||||
|
||||
sealed interface Effect {
|
||||
data class NavigateNext(val accountUuid: AccountUuid) : Effect
|
||||
data object NavigateBack : Effect
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package app.k9mail.feature.account.setup.ui.createaccount
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
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.feature.account.common.data.InMemoryAccountStateRepository
|
||||
import app.k9mail.feature.account.common.ui.AppTitleTopHeader
|
||||
import app.k9mail.feature.account.common.ui.WizardNavigationBar
|
||||
import app.k9mail.feature.account.common.ui.WizardNavigationBarState
|
||||
import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator.AccountCreatorResult
|
||||
import app.k9mail.feature.account.setup.domain.entity.AccountUuid
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract.ViewModel
|
||||
|
||||
@Composable
|
||||
internal fun CreateAccountScreen(
|
||||
onNext: (AccountUuid) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
viewModel: ViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val (state, dispatch) = viewModel.observe { effect ->
|
||||
when (effect) {
|
||||
Effect.NavigateBack -> onBack()
|
||||
is Effect.NavigateNext -> onNext(effect.accountUuid)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
dispatch(Event.CreateAccount)
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
dispatch(Event.OnBackClicked)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTitleTopHeader()
|
||||
},
|
||||
bottomBar = {
|
||||
WizardNavigationBar(
|
||||
onNextClick = {},
|
||||
onBackClick = {
|
||||
dispatch(Event.OnBackClicked)
|
||||
},
|
||||
state = WizardNavigationBarState(
|
||||
showNext = false,
|
||||
isBackEnabled = state.value.error != null,
|
||||
),
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
) { innerPadding ->
|
||||
CreateAccountContent(
|
||||
state = state.value,
|
||||
contentPadding = innerPadding,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@PreviewDevices
|
||||
internal fun AccountOptionsScreenK9Preview() {
|
||||
K9Theme {
|
||||
CreateAccountScreen(
|
||||
onNext = {},
|
||||
onBack = {},
|
||||
viewModel = CreateAccountViewModel(
|
||||
createAccount = { _, _, _, _, _ -> AccountCreatorResult.Success("irrelevant") },
|
||||
accountStateRepository = InMemoryAccountStateRepository(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
package app.k9mail.feature.account.setup.ui.createaccount
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
|
||||
import app.k9mail.feature.account.common.domain.AccountDomainContract.AccountStateRepository
|
||||
import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator.AccountCreatorResult
|
||||
import app.k9mail.feature.account.setup.domain.DomainContract.UseCase.CreateAccount
|
||||
import app.k9mail.feature.account.setup.domain.entity.AccountUuid
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract.State
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val CONTINUE_NEXT_DELAY = 2000L
|
||||
|
||||
class CreateAccountViewModel(
|
||||
private val createAccount: CreateAccount,
|
||||
private val accountStateRepository: AccountStateRepository,
|
||||
initialState: State = State(),
|
||||
) : BaseViewModel<State, Event, Effect>(initialState),
|
||||
CreateAccountContract.ViewModel {
|
||||
|
||||
override fun event(event: Event) {
|
||||
when (event) {
|
||||
Event.CreateAccount -> handleOneTimeEvent(event, ::createAccount)
|
||||
Event.OnBackClicked -> maybeNavigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAccount() {
|
||||
val accountState = accountStateRepository.getState()
|
||||
|
||||
viewModelScope.launch {
|
||||
val result = createAccount.execute(
|
||||
emailAddress = accountState.emailAddress ?: "",
|
||||
incomingServerSettings = accountState.incomingServerSettings!!,
|
||||
outgoingServerSettings = accountState.outgoingServerSettings!!,
|
||||
authorizationState = accountState.authorizationState?.state,
|
||||
options = accountState.options!!,
|
||||
)
|
||||
|
||||
when (result) {
|
||||
is AccountCreatorResult.Success -> showSuccess(AccountUuid(result.accountUuid))
|
||||
is AccountCreatorResult.Error -> showError(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSuccess(accountUuid: AccountUuid) {
|
||||
updateState {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
delay(CONTINUE_NEXT_DELAY)
|
||||
navigateNext(accountUuid)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showError(error: AccountCreatorResult.Error) {
|
||||
updateState {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
error = error,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeNavigateBack() {
|
||||
if (!state.value.isLoading) {
|
||||
navigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateBack() {
|
||||
viewModelScope.coroutineContext.cancelChildren()
|
||||
emitEffect(Effect.NavigateBack)
|
||||
}
|
||||
|
||||
private fun navigateNext(accountUuid: AccountUuid) {
|
||||
viewModelScope.coroutineContext.cancelChildren()
|
||||
emitEffect(Effect.NavigateNext(accountUuid))
|
||||
}
|
||||
}
|
|
@ -38,4 +38,8 @@
|
|||
<string name="account_setup_options_email_check_frequency_never">Never</string>
|
||||
<string name="account_setup_options_email_display_count_label">Number of messages to display</string>
|
||||
<string name="account_setup_options_show_notifications_label">Show notifications</string>
|
||||
|
||||
<string name="account_setup_create_account_creating">Creating account…</string>
|
||||
<string name="account_setup_create_account_error">An error occurred while trying to create the account</string>
|
||||
<string name="account_setup_create_account_created">Account successfully created</string>
|
||||
</resources>
|
||||
|
|
|
@ -14,6 +14,7 @@ import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCrea
|
|||
import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator.AccountCreatorResult
|
||||
import app.k9mail.feature.account.setup.ui.AccountSetupContract
|
||||
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 com.fsck.k9.mail.oauth.AuthStateStorage
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider
|
||||
|
@ -79,6 +80,7 @@ class AccountSetupModuleKtTest : KoinTest {
|
|||
Boolean::class,
|
||||
Class.forName("net.openid.appauth.AppAuthConfiguration").kotlin,
|
||||
InteractionMode::class,
|
||||
CreateAccountContract.State::class,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ class CreateAccountTest {
|
|||
options,
|
||||
)
|
||||
|
||||
assertThat(result).isEqualTo("uuid")
|
||||
assertThat(result).isEqualTo(AccountCreatorResult.Success("uuid"))
|
||||
assertThat(recordedAccount).isEqualTo(
|
||||
Account(
|
||||
uuid = "uuid",
|
||||
|
|
|
@ -11,6 +11,7 @@ import app.k9mail.feature.account.setup.ui.AccountSetupContract.Effect
|
|||
import app.k9mail.feature.account.setup.ui.AccountSetupContract.SetupStep
|
||||
import app.k9mail.feature.account.setup.ui.AccountSetupContract.State
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.FakeAccountAutoDiscoveryViewModel
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.FakeCreateAccountViewModel
|
||||
import app.k9mail.feature.account.setup.ui.options.FakeAccountOptionsViewModel
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
|
@ -35,6 +36,7 @@ class AccountSetupScreenKtTest : ComposeTest() {
|
|||
outgoingViewModel = FakeOutgoingServerSettingsViewModel(),
|
||||
outgoingValidationViewModel = FakeServerValidationViewModel(),
|
||||
optionsViewModel = FakeAccountOptionsViewModel(),
|
||||
createAccountViewModel = FakeCreateAccountViewModel(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +66,7 @@ class AccountSetupScreenKtTest : ComposeTest() {
|
|||
outgoingViewModel = FakeOutgoingServerSettingsViewModel(),
|
||||
outgoingValidationViewModel = FakeServerValidationViewModel(),
|
||||
optionsViewModel = FakeAccountOptionsViewModel(),
|
||||
createAccountViewModel = FakeCreateAccountViewModel(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -89,5 +92,6 @@ class AccountSetupScreenKtTest : ComposeTest() {
|
|||
SetupStep.OUTGOING_CONFIG -> "OutgoingServerSettingsContent"
|
||||
SetupStep.OUTGOING_VALIDATION -> "AccountValidationContent"
|
||||
SetupStep.OPTIONS -> "AccountOptionsContent"
|
||||
SetupStep.CREATE_ACCOUNT -> "CreateAccountContent"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,12 +7,11 @@ 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.MailConnectionSecurity
|
||||
import app.k9mail.feature.account.setup.domain.entity.AccountUuid
|
||||
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 assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isNull
|
||||
import assertk.assertions.prop
|
||||
import com.fsck.k9.mail.AuthType
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
|
@ -28,22 +27,8 @@ class AccountSetupViewModelTest {
|
|||
|
||||
@Test
|
||||
fun `should forward step state on next event`() = runTest {
|
||||
var createAccountEmailAddress: String? = null
|
||||
var createAccountIncomingServerSettings: ServerSettings? = null
|
||||
var createAccountOutgoingServerSettings: ServerSettings? = null
|
||||
var createAccountAuthorizationState: String? = null
|
||||
var createAccountOptions: AccountOptions? = null
|
||||
val accountStateRepository = InMemoryAccountStateRepository()
|
||||
val viewModel = AccountSetupViewModel(
|
||||
createAccount = { emailAddress, incomingServerSettings, outgoingServerSettings, authState, options ->
|
||||
createAccountEmailAddress = emailAddress
|
||||
createAccountIncomingServerSettings = incomingServerSettings
|
||||
createAccountOutgoingServerSettings = outgoingServerSettings
|
||||
createAccountAuthorizationState = authState
|
||||
createAccountOptions = options
|
||||
|
||||
"accountUuid"
|
||||
},
|
||||
accountStateRepository = accountStateRepository,
|
||||
)
|
||||
val turbines = turbinesWithInitialStateCheck(viewModel, State(setupStep = SetupStep.AUTO_CONFIG))
|
||||
|
@ -132,9 +117,18 @@ class AccountSetupViewModelTest {
|
|||
prop(State::setupStep).isEqualTo(SetupStep.OPTIONS)
|
||||
}
|
||||
|
||||
viewModel.event(AccountSetupContract.Event.OnNext)
|
||||
|
||||
assertThatAndMviTurbinesConsumed(
|
||||
actual = turbines.stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
prop(State::setupStep).isEqualTo(SetupStep.CREATE_ACCOUNT)
|
||||
}
|
||||
|
||||
accountStateRepository.setState(expectedAccountState)
|
||||
|
||||
viewModel.event(AccountSetupContract.Event.OnNext)
|
||||
viewModel.event(AccountSetupContract.Event.OnAccountCreated(AccountUuid("accountUuid")))
|
||||
|
||||
assertThatAndMviTurbinesConsumed(
|
||||
actual = turbines.effectTurbine.awaitItem(),
|
||||
|
@ -142,19 +136,12 @@ class AccountSetupViewModelTest {
|
|||
) {
|
||||
isEqualTo(Effect.NavigateNext("accountUuid"))
|
||||
}
|
||||
|
||||
assertThat(createAccountEmailAddress).isEqualTo(EMAIL_ADDRESS)
|
||||
assertThat(createAccountIncomingServerSettings).isEqualTo(expectedAccountState.incomingServerSettings)
|
||||
assertThat(createAccountOutgoingServerSettings).isEqualTo(expectedAccountState.outgoingServerSettings)
|
||||
assertThat(createAccountAuthorizationState).isNull()
|
||||
assertThat(createAccountOptions).isEqualTo(expectedAccountState.options)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should rewind step state on back event`() = runTest {
|
||||
val initialState = State(setupStep = SetupStep.OPTIONS)
|
||||
val viewModel = AccountSetupViewModel(
|
||||
createAccount = { _, _, _, _, _ -> "accountUuid" },
|
||||
accountStateRepository = InMemoryAccountStateRepository(),
|
||||
initialState = initialState,
|
||||
)
|
||||
|
@ -204,7 +191,6 @@ class AccountSetupViewModelTest {
|
|||
isAutomaticConfig = true,
|
||||
)
|
||||
val viewModel = AccountSetupViewModel(
|
||||
createAccount = { _, _, _, _, _ -> "accountUuid" },
|
||||
accountStateRepository = InMemoryAccountStateRepository(),
|
||||
initialState = initialState,
|
||||
)
|
||||
|
@ -236,7 +222,6 @@ class AccountSetupViewModelTest {
|
|||
isAutomaticConfig = true,
|
||||
)
|
||||
val viewModel = AccountSetupViewModel(
|
||||
createAccount = { _, _, _, _, _ -> "accountUuid" },
|
||||
accountStateRepository = InMemoryAccountStateRepository(),
|
||||
initialState = initialState,
|
||||
)
|
||||
|
@ -268,7 +253,6 @@ class AccountSetupViewModelTest {
|
|||
isAutomaticConfig = true,
|
||||
)
|
||||
val viewModel = AccountSetupViewModel(
|
||||
createAccount = { _, _, _, _, _ -> "accountUuid" },
|
||||
accountStateRepository = InMemoryAccountStateRepository(),
|
||||
initialState = initialState,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
package app.k9mail.feature.account.setup.ui.createaccount
|
||||
|
||||
import app.k9mail.core.ui.compose.testing.ComposeTest
|
||||
import app.k9mail.core.ui.compose.testing.setContent
|
||||
import app.k9mail.core.ui.compose.theme.K9Theme
|
||||
import app.k9mail.feature.account.setup.domain.entity.AccountUuid
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract.State
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.containsExactly
|
||||
import assertk.assertions.isEmpty
|
||||
import assertk.assertions.isEqualTo
|
||||
import kotlin.test.Test
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
||||
class CreateAccountScreenTest : ComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `should delegate navigation effects`() = runTest {
|
||||
val accountUuid = AccountUuid("irrelevant")
|
||||
val initialState = State(
|
||||
isLoading = false,
|
||||
error = null,
|
||||
)
|
||||
val viewModel = FakeCreateAccountViewModel(initialState)
|
||||
val navigateNextArguments = mutableListOf<AccountUuid>()
|
||||
var navigateBackCounter = 0
|
||||
|
||||
setContent {
|
||||
K9Theme {
|
||||
CreateAccountScreen(
|
||||
onNext = { accountUuid -> navigateNextArguments.add(accountUuid) },
|
||||
onBack = { navigateBackCounter++ },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
assertThat(navigateNextArguments).isEmpty()
|
||||
assertThat(navigateBackCounter).isEqualTo(0)
|
||||
|
||||
viewModel.effect(Effect.NavigateNext(accountUuid))
|
||||
|
||||
assertThat(navigateNextArguments).containsExactly(accountUuid)
|
||||
assertThat(navigateBackCounter).isEqualTo(0)
|
||||
|
||||
viewModel.effect(Effect.NavigateBack)
|
||||
|
||||
assertThat(navigateNextArguments).containsExactly(accountUuid)
|
||||
assertThat(navigateBackCounter).isEqualTo(1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
package app.k9mail.feature.account.setup.ui.createaccount
|
||||
|
||||
import app.cash.turbine.testIn
|
||||
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.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.setup.AccountSetupExternalContract.AccountCreator.AccountCreatorResult
|
||||
import app.k9mail.feature.account.setup.domain.entity.AccountUuid
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract.State
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.containsExactly
|
||||
import assertk.assertions.isEqualTo
|
||||
import com.fsck.k9.mail.AuthType
|
||||
import com.fsck.k9.mail.ConnectionSecurity
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import kotlin.test.Test
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
|
||||
class CreateAccountViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
private val fakeCreateAccount = FakeCreateAccount()
|
||||
private val accountStateRepository = InMemoryAccountStateRepository().apply {
|
||||
setState(ACCOUNT_STATE)
|
||||
}
|
||||
private val createAccountViewModel = CreateAccountViewModel(
|
||||
createAccount = fakeCreateAccount,
|
||||
accountStateRepository = accountStateRepository,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `initial state should be loading state`() {
|
||||
assertThat(createAccountViewModel.state.value).isEqualTo(State(isLoading = true, error = null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should change state and emit navigate effect after successfully creating account`() = runTest {
|
||||
val accountUuid = "accountUuid"
|
||||
fakeCreateAccount.result = AccountCreatorResult.Success(accountUuid)
|
||||
|
||||
eventStateTest(
|
||||
viewModel = createAccountViewModel,
|
||||
initialState = State(isLoading = true, error = null),
|
||||
event = Event.CreateAccount,
|
||||
expectedState = State(isLoading = false, error = null),
|
||||
coroutineScope = backgroundScope,
|
||||
)
|
||||
|
||||
assertThat(fakeCreateAccount.recordedInvocations).containsExactly(
|
||||
CreateAccountArguments(
|
||||
emailAddress = EMAIL_ADDRESS,
|
||||
incomingServerSettings = INCOMING_SERVER_SETTINGS,
|
||||
outgoingServerSettings = OUTGOING_SERVER_SETTINGS,
|
||||
authorizationState = AUTHORIZATION_STATE.state,
|
||||
options = ACCOUNT_OPTIONS,
|
||||
),
|
||||
)
|
||||
|
||||
val effectTurbine = createAccountViewModel.effect.testIn(backgroundScope)
|
||||
assertThat(effectTurbine.awaitItem()).isEqualTo(Effect.NavigateNext(AccountUuid(accountUuid)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should change state when creating account has failed`() = runTest {
|
||||
val errorResult = AccountCreatorResult.Error("something went wrong")
|
||||
fakeCreateAccount.result = errorResult
|
||||
|
||||
eventStateTest(
|
||||
viewModel = createAccountViewModel,
|
||||
initialState = State(isLoading = true, error = null),
|
||||
event = Event.CreateAccount,
|
||||
expectedState = State(isLoading = false, error = errorResult),
|
||||
coroutineScope = backgroundScope,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should ignore OnBackClicked event when in loading state`() = runTest {
|
||||
val effectTurbine = createAccountViewModel.effect.testIn(scope = backgroundScope)
|
||||
|
||||
createAccountViewModel.event(Event.OnBackClicked)
|
||||
|
||||
effectTurbine.ensureAllEventsConsumed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit NavigateBack effect when OnBackClicked event was received while in success state`() = runTest {
|
||||
fakeCreateAccount.result = AccountCreatorResult.Success("accountUuid")
|
||||
createAccountViewModel.event(Event.CreateAccount)
|
||||
val effectTurbine = createAccountViewModel.effect.testIn(backgroundScope)
|
||||
|
||||
createAccountViewModel.event(Event.OnBackClicked)
|
||||
|
||||
assertThat(effectTurbine.awaitItem()).isEqualTo(Effect.NavigateBack)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit NavigateBack effect when OnBackClicked event was received while in error state`() = runTest {
|
||||
fakeCreateAccount.result = AccountCreatorResult.Error("something went wrong")
|
||||
createAccountViewModel.event(Event.CreateAccount)
|
||||
val effectTurbine = createAccountViewModel.effect.testIn(backgroundScope)
|
||||
|
||||
createAccountViewModel.event(Event.OnBackClicked)
|
||||
|
||||
assertThat(effectTurbine.awaitItem()).isEqualTo(Effect.NavigateBack)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EMAIL_ADDRESS = "test@domain.example"
|
||||
|
||||
private val INCOMING_SERVER_SETTINGS = ServerSettings(
|
||||
"imap",
|
||||
"imap.domain.example",
|
||||
993,
|
||||
ConnectionSecurity.SSL_TLS_REQUIRED,
|
||||
AuthType.PLAIN,
|
||||
"username",
|
||||
"password",
|
||||
null,
|
||||
)
|
||||
|
||||
private val OUTGOING_SERVER_SETTINGS = ServerSettings(
|
||||
"smtp",
|
||||
"smtp.domain.example",
|
||||
465,
|
||||
ConnectionSecurity.SSL_TLS_REQUIRED,
|
||||
AuthType.PLAIN,
|
||||
"username",
|
||||
"password",
|
||||
null,
|
||||
)
|
||||
|
||||
private val AUTHORIZATION_STATE = AuthorizationState("authorization state")
|
||||
|
||||
private val ACCOUNT_OPTIONS = AccountOptions(
|
||||
accountName = "account name",
|
||||
displayName = "display name",
|
||||
emailSignature = null,
|
||||
checkFrequencyInMinutes = 0,
|
||||
messageDisplayCount = 50,
|
||||
showNotification = false,
|
||||
)
|
||||
|
||||
private val ACCOUNT_STATE = AccountState(
|
||||
emailAddress = EMAIL_ADDRESS,
|
||||
incomingServerSettings = INCOMING_SERVER_SETTINGS,
|
||||
outgoingServerSettings = OUTGOING_SERVER_SETTINGS,
|
||||
authorizationState = AUTHORIZATION_STATE,
|
||||
options = ACCOUNT_OPTIONS,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package app.k9mail.feature.account.setup.ui.createaccount
|
||||
|
||||
import app.k9mail.feature.account.common.domain.entity.AccountOptions
|
||||
import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator.AccountCreatorResult
|
||||
import app.k9mail.feature.account.setup.domain.DomainContract.UseCase.CreateAccount
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
|
||||
class FakeCreateAccount : CreateAccount {
|
||||
val recordedInvocations = mutableListOf<CreateAccountArguments>()
|
||||
|
||||
var result: AccountCreatorResult = AccountCreatorResult.Success("default result")
|
||||
|
||||
override suspend fun execute(
|
||||
emailAddress: String,
|
||||
incomingServerSettings: ServerSettings,
|
||||
outgoingServerSettings: ServerSettings,
|
||||
authorizationState: String?,
|
||||
options: AccountOptions,
|
||||
): AccountCreatorResult {
|
||||
recordedInvocations.add(
|
||||
CreateAccountArguments(
|
||||
emailAddress,
|
||||
incomingServerSettings,
|
||||
outgoingServerSettings,
|
||||
authorizationState,
|
||||
options,
|
||||
),
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
data class CreateAccountArguments(
|
||||
val emailAddress: String,
|
||||
val incomingServerSettings: ServerSettings,
|
||||
val outgoingServerSettings: ServerSettings,
|
||||
val authorizationState: String?,
|
||||
val options: AccountOptions,
|
||||
)
|
|
@ -0,0 +1,21 @@
|
|||
package app.k9mail.feature.account.setup.ui.createaccount
|
||||
|
||||
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract.State
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract.ViewModel
|
||||
|
||||
class FakeCreateAccountViewModel(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)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue