Add AccountOAuthScreen
This commit is contained in:
parent
93cf883fca
commit
e658b8256b
16 changed files with 706 additions and 0 deletions
|
@ -1,3 +1,5 @@
|
|||
@file:Suppress("TooManyFunctions")
|
||||
|
||||
package app.k9mail.core.ui.compose.testing
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
@ -55,6 +57,25 @@ fun ComposeTest.onNodeWithText(
|
|||
useUnmergedTree: Boolean = false,
|
||||
) = composeTestRule.onNodeWithText(text, substring, ignoreCase, useUnmergedTree)
|
||||
|
||||
fun ComposeTest.onNodeWithText(
|
||||
@StringRes resourceId: Int,
|
||||
substring: Boolean = false,
|
||||
ignoreCase: Boolean = false,
|
||||
useUnmergedTree: Boolean = false,
|
||||
) = composeTestRule.onNodeWithText(getString(resourceId), substring, ignoreCase, useUnmergedTree)
|
||||
|
||||
fun ComposeTest.onNodeWithTextIgnoreCase(
|
||||
text: String,
|
||||
substring: Boolean = false,
|
||||
useUnmergedTree: Boolean = false,
|
||||
) = composeTestRule.onNodeWithText(text, substring, true, useUnmergedTree)
|
||||
|
||||
fun ComposeTest.onNodeWithTextIgnoreCase(
|
||||
@StringRes resourceId: Int,
|
||||
substring: Boolean = false,
|
||||
useUnmergedTree: Boolean = false,
|
||||
) = composeTestRule.onNodeWithText(getString(resourceId), substring, true, useUnmergedTree)
|
||||
|
||||
fun ComposeTest.onAllNodesWithText(
|
||||
text: String,
|
||||
substring: Boolean = false,
|
||||
|
|
|
@ -19,6 +19,7 @@ android {
|
|||
dependencies {
|
||||
implementation(projects.core.ui.compose.designsystem)
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.feature.account.common)
|
||||
|
||||
implementation(libs.appauth)
|
||||
implementation(libs.androidx.compose.material)
|
||||
|
|
|
@ -6,8 +6,10 @@ import app.k9mail.feature.account.oauth.domain.DomainContract
|
|||
import app.k9mail.feature.account.oauth.domain.DomainContract.UseCase
|
||||
import app.k9mail.feature.account.oauth.domain.usecase.GetOAuthRequestIntent
|
||||
import app.k9mail.feature.account.oauth.domain.usecase.SuggestServerName
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthViewModel
|
||||
import net.openid.appauth.AuthorizationService
|
||||
import org.koin.android.ext.koin.androidApplication
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
@ -34,4 +36,10 @@ val featureAccountOAuthModule: Module = module {
|
|||
configurationProvider = get(),
|
||||
)
|
||||
}
|
||||
|
||||
viewModel {
|
||||
AccountOAuthViewModel(
|
||||
getOAuthRequestIntent = get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package app.k9mail.feature.account.oauth.domain.entity
|
||||
|
||||
data class AuthorizationState(
|
||||
val state: String? = null,
|
||||
)
|
|
@ -0,0 +1,9 @@
|
|||
package app.k9mail.feature.account.oauth.domain.entity
|
||||
|
||||
sealed interface OAuthResult {
|
||||
data class Success(
|
||||
val authorizationState: AuthorizationState,
|
||||
) : OAuthResult
|
||||
|
||||
object Failure : OAuthResult
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package app.k9mail.feature.account.oauth.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
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.DevicePreviews
|
||||
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.ContentListView
|
||||
import app.k9mail.feature.account.common.ui.item.ErrorItem
|
||||
import app.k9mail.feature.account.common.ui.item.LoadingItem
|
||||
import app.k9mail.feature.account.oauth.R
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Event
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.State
|
||||
import app.k9mail.feature.account.oauth.ui.item.SignInItem
|
||||
|
||||
@Composable
|
||||
internal fun AccountOAuthContent(
|
||||
state: State,
|
||||
onEvent: (Event) -> Unit,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val resources = LocalContext.current.resources
|
||||
|
||||
ContentListView(
|
||||
modifier = Modifier
|
||||
.testTag("AccountOAuthContent")
|
||||
.then(modifier),
|
||||
contentPadding = contentPadding,
|
||||
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.double, Alignment.CenterVertically),
|
||||
) {
|
||||
if (state.isLoading) {
|
||||
item(key = "loading") {
|
||||
LoadingItem(
|
||||
message = stringResource(id = R.string.account_oauth_loading_message),
|
||||
)
|
||||
}
|
||||
} else if (state.error != null) {
|
||||
item(key = "error") {
|
||||
ErrorItem(
|
||||
title = stringResource(id = R.string.account_oauth_loading_error),
|
||||
// message = state.error.toResourceString(resources),
|
||||
onRetry = { onEvent(Event.OnRetryClicked) },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
item(key = "sign_in") {
|
||||
SignInItem(
|
||||
emailAddress = state.emailAddress,
|
||||
onSignInClick = { onEvent(Event.SignInClicked) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@DevicePreviews
|
||||
internal fun AccountOAuthContentK9Preview() {
|
||||
K9Theme {
|
||||
AccountOAuthContent(
|
||||
state = State(),
|
||||
onEvent = {},
|
||||
contentPadding = PaddingValues(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@DevicePreviews
|
||||
internal fun AccountOAuthContentThunderbirdPreview() {
|
||||
ThunderbirdTheme {
|
||||
AccountOAuthContent(
|
||||
state = State(),
|
||||
onEvent = {},
|
||||
contentPadding = PaddingValues(),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package app.k9mail.feature.account.oauth.ui
|
||||
|
||||
import android.content.Intent
|
||||
import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
|
||||
import app.k9mail.feature.account.common.ui.WizardNavigationBarState
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationState
|
||||
|
||||
interface AccountOAuthContract {
|
||||
|
||||
interface ViewModel : UnidirectionalViewModel<State, Event, Effect> {
|
||||
fun initState(state: State)
|
||||
}
|
||||
|
||||
data class State(
|
||||
val hostname: String = "",
|
||||
val emailAddress: String = "",
|
||||
val authorizationState: AuthorizationState = AuthorizationState(),
|
||||
val wizardNavigationBarState: WizardNavigationBarState = WizardNavigationBarState(
|
||||
isNextEnabled = false,
|
||||
),
|
||||
val isGoogleSignIn: Boolean = false,
|
||||
val error: Error? = null,
|
||||
val isLoading: Boolean = false,
|
||||
)
|
||||
|
||||
sealed interface Event {
|
||||
object SignInClicked : Event
|
||||
object OnNextClicked : Event
|
||||
object OnBackClicked : Event
|
||||
object OnRetryClicked : Event
|
||||
}
|
||||
|
||||
sealed interface Effect {
|
||||
data class LaunchOAuth(
|
||||
val intent: Intent,
|
||||
) : Effect
|
||||
|
||||
data class NavigateNext(
|
||||
val state: AuthorizationState,
|
||||
) : Effect
|
||||
object NavigateBack : Effect
|
||||
}
|
||||
|
||||
sealed interface Error {
|
||||
object NotSupported : Error
|
||||
object NetworkError : Error
|
||||
object UnknownError : Error
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package app.k9mail.feature.account.oauth.ui
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.k9mail.core.ui.compose.common.mvi.observe
|
||||
import app.k9mail.core.ui.compose.designsystem.template.Scaffold
|
||||
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.oauth.R
|
||||
import app.k9mail.feature.account.oauth.domain.entity.OAuthResult
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Effect
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Event
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.ViewModel
|
||||
|
||||
@Composable
|
||||
fun AccountOAuthScreen(
|
||||
onOAuthResult: (OAuthResult) -> Unit,
|
||||
viewModel: ViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val oAuthLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult(),
|
||||
) {
|
||||
if (it.resultCode == Activity.RESULT_OK && it.data != null) {
|
||||
// TODO handle success
|
||||
}
|
||||
// TODO handle error
|
||||
}
|
||||
|
||||
val (state, dispatch) = viewModel.observe { effect ->
|
||||
when (effect) {
|
||||
is Effect.NavigateNext -> onOAuthResult(OAuthResult.Success(effect.state))
|
||||
is Effect.NavigateBack -> onOAuthResult(OAuthResult.Failure)
|
||||
is Effect.LaunchOAuth -> oAuthLauncher.launch(effect.intent)
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
dispatch(Event.OnBackClicked)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTitleTopHeader(stringResource(id = R.string.account_oauth_title))
|
||||
},
|
||||
bottomBar = {
|
||||
WizardNavigationBar(
|
||||
state = state.value.wizardNavigationBarState,
|
||||
nextButtonText = stringResource(id = R.string.account_oauth_button_next),
|
||||
backButtonText = stringResource(id = R.string.account_oauth_button_back),
|
||||
onNextClick = { dispatch(Event.OnNextClicked) },
|
||||
onBackClick = { dispatch(Event.OnBackClicked) },
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
) { innerPadding ->
|
||||
AccountOAuthContent(
|
||||
state = state.value,
|
||||
onEvent = { dispatch(it) },
|
||||
contentPadding = innerPadding,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package app.k9mail.feature.account.oauth.ui
|
||||
|
||||
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
|
||||
import app.k9mail.feature.account.oauth.domain.DomainContract.UseCase.GetOAuthRequestIntent
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationIntentResult
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Effect
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Error
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Event
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.State
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.ViewModel
|
||||
|
||||
class AccountOAuthViewModel(
|
||||
initialState: State = State(),
|
||||
private val getOAuthRequestIntent: GetOAuthRequestIntent,
|
||||
) : BaseViewModel<State, Event, Effect>(initialState), ViewModel {
|
||||
|
||||
override fun initState(state: State) {
|
||||
updateState {
|
||||
state.copy()
|
||||
}
|
||||
}
|
||||
|
||||
override fun event(event: Event) {
|
||||
when (event) {
|
||||
Event.SignInClicked -> launchOAuth()
|
||||
|
||||
Event.OnNextClicked -> TODO()
|
||||
|
||||
Event.OnBackClicked -> navigateBack()
|
||||
|
||||
Event.OnRetryClicked -> {
|
||||
updateState { state ->
|
||||
state.copy(
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
launchOAuth()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchOAuth() {
|
||||
val result = getOAuthRequestIntent.execute(
|
||||
hostname = state.value.hostname,
|
||||
emailAddress = state.value.emailAddress,
|
||||
)
|
||||
|
||||
when (result) {
|
||||
AuthorizationIntentResult.NotSupported -> {
|
||||
updateState { state ->
|
||||
state.copy(
|
||||
error = Error.NotSupported,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is AuthorizationIntentResult.Success -> {
|
||||
emitEffect(Effect.LaunchOAuth(result.intent))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateBack() = emitEffect(Effect.NavigateBack)
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package app.k9mail.feature.account.oauth.ui.item
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import app.k9mail.feature.account.common.ui.item.ListItem
|
||||
import app.k9mail.feature.account.oauth.ui.view.SignInView
|
||||
|
||||
@Composable
|
||||
internal fun LazyItemScope.SignInItem(
|
||||
emailAddress: String,
|
||||
onSignInClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ListItem(
|
||||
modifier = modifier,
|
||||
) {
|
||||
SignInView(
|
||||
emailAddress = emailAddress,
|
||||
onSignInClick = onSignInClick,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package app.k9mail.feature.account.oauth.ui.view
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.button.Button
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextCaption
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextSubtitle1
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.input.EmailAddressInput
|
||||
import app.k9mail.core.ui.compose.theme.MainTheme
|
||||
import app.k9mail.feature.account.oauth.R
|
||||
|
||||
@Composable
|
||||
internal fun SignInView(
|
||||
emailAddress: String,
|
||||
onSignInClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.double),
|
||||
modifier = modifier,
|
||||
) {
|
||||
TextSubtitle1(text = stringResource(id = R.string.account_oauth_sign_in_title))
|
||||
|
||||
EmailAddressInput(
|
||||
emailAddress = emailAddress,
|
||||
onEmailAddressChange = {},
|
||||
isEnabled = false,
|
||||
)
|
||||
|
||||
TextCaption(
|
||||
text = stringResource(id = R.string.account_oauth_sign_in_description),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
Button(
|
||||
text = stringResource(id = R.string.account_oauth_sign_in_button),
|
||||
onClick = onSignInClick,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_oauth_title">K-9 Mail</string>
|
||||
<string name="account_oauth_button_next">Next</string>
|
||||
<string name="account_oauth_button_back">Back</string>
|
||||
<string name="account_oauth_sign_in_title">Sign in with OAuth2</string>
|
||||
<string name="account_oauth_sign_in_description">We\'ll redirect you to your email provider to sign in. You need to grant the app access to your email account.</string>
|
||||
<string name="account_oauth_sign_in_button">Sign in</string>
|
||||
<string name="account_oauth_sign_in_with_google_button">Sign in with Google</string>
|
||||
<string name="account_oauth_sign_in_with_google_button_loading">Signing in with Google …</string>
|
||||
<string name="account_oauth_loading_message">Signing in using OAuth</string>
|
||||
<string name="account_oauth_loading_error">OAuth sign in failed</string>
|
||||
</resources>
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
package app.k9mail.feature.account.oauth.ui
|
||||
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
import androidx.compose.ui.test.assertIsNotEnabled
|
||||
import app.k9mail.core.ui.compose.testing.ComposeTest
|
||||
import app.k9mail.core.ui.compose.testing.onNodeWithTextIgnoreCase
|
||||
import app.k9mail.core.ui.compose.testing.setContent
|
||||
import app.k9mail.core.ui.compose.theme.ThunderbirdTheme
|
||||
import app.k9mail.feature.account.common.ui.WizardNavigationBarState
|
||||
import app.k9mail.feature.account.oauth.R
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationState
|
||||
import app.k9mail.feature.account.oauth.domain.entity.OAuthResult
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Effect
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.State
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isNull
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class AccountOAuthScreenKtTest : ComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `should delegate navigation effects`() = runTest {
|
||||
val initialState = State()
|
||||
val viewModel = FakeAccountOAuthViewModel(initialState)
|
||||
var oAuthResult: OAuthResult? = null
|
||||
val authorizationState = AuthorizationState()
|
||||
|
||||
setContent {
|
||||
ThunderbirdTheme {
|
||||
AccountOAuthScreen(
|
||||
onOAuthResult = { oAuthResult = it },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
assertThat(oAuthResult).isNull()
|
||||
|
||||
viewModel.effect(Effect.NavigateNext(authorizationState))
|
||||
|
||||
assertThat(oAuthResult).isEqualTo(OAuthResult.Success(authorizationState))
|
||||
|
||||
viewModel.effect(Effect.NavigateBack)
|
||||
|
||||
assertThat(oAuthResult).isEqualTo(OAuthResult.Failure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should set navigation bar enabled state`() {
|
||||
val initialState = State(
|
||||
wizardNavigationBarState = WizardNavigationBarState(
|
||||
isNextEnabled = true,
|
||||
isBackEnabled = true,
|
||||
),
|
||||
)
|
||||
val viewModel = FakeAccountOAuthViewModel(initialState)
|
||||
|
||||
setContent {
|
||||
ThunderbirdTheme {
|
||||
AccountOAuthScreen(
|
||||
onOAuthResult = {},
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onNodeWithTextIgnoreCase(R.string.account_oauth_button_next).assertIsEnabled()
|
||||
onNodeWithTextIgnoreCase(R.string.account_oauth_button_back).assertIsEnabled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should set navigation bar disabled state`() {
|
||||
val initialState = State(
|
||||
wizardNavigationBarState = WizardNavigationBarState(
|
||||
isNextEnabled = false,
|
||||
isBackEnabled = false,
|
||||
),
|
||||
)
|
||||
val viewModel = FakeAccountOAuthViewModel(initialState)
|
||||
|
||||
setContent {
|
||||
ThunderbirdTheme {
|
||||
AccountOAuthScreen(
|
||||
onOAuthResult = {},
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onNodeWithTextIgnoreCase(R.string.account_oauth_button_next).assertIsNotEnabled()
|
||||
onNodeWithTextIgnoreCase(R.string.account_oauth_button_back).assertIsNotEnabled()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package app.k9mail.feature.account.oauth.ui
|
||||
|
||||
import app.k9mail.feature.account.common.ui.WizardNavigationBarState
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationState
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.State
|
||||
import assertk.all
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.prop
|
||||
import org.junit.Test
|
||||
|
||||
class AccountOAuthStateTest {
|
||||
|
||||
@Test
|
||||
fun `should set default values`() {
|
||||
val state = State()
|
||||
|
||||
assertThat(state).all {
|
||||
prop(State::hostname).isEqualTo("")
|
||||
prop(State::emailAddress).isEqualTo("")
|
||||
prop(State::authorizationState).isEqualTo(AuthorizationState())
|
||||
prop(State::wizardNavigationBarState).isEqualTo(
|
||||
WizardNavigationBarState(
|
||||
isNextEnabled = false,
|
||||
),
|
||||
)
|
||||
prop(State::isGoogleSignIn).isEqualTo(false)
|
||||
prop(State::error).isEqualTo(null)
|
||||
prop(State::isLoading).isEqualTo(false)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
package app.k9mail.feature.account.oauth.ui
|
||||
|
||||
import android.content.Intent
|
||||
import app.cash.turbine.testIn
|
||||
import app.k9mail.core.ui.compose.testing.MainDispatcherRule
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationIntentResult
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Effect
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Error
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Event
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.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
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class AccountOAuthViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
private val testSubject = AccountOAuthViewModel(
|
||||
getOAuthRequestIntent = { _, _ ->
|
||||
AuthorizationIntentResult.Success(intent = Intent())
|
||||
},
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `should launch OAuth when SignInClicked event received`() = runTest {
|
||||
val initialState = State(
|
||||
hostname = "example.com",
|
||||
emailAddress = "test@example.com",
|
||||
)
|
||||
val intent = Intent()
|
||||
val testSubject = AccountOAuthViewModel(
|
||||
getOAuthRequestIntent = { _, _ ->
|
||||
AuthorizationIntentResult.Success(intent = intent)
|
||||
},
|
||||
initialState = initialState,
|
||||
)
|
||||
|
||||
val stateTurbine = testSubject.state.testIn(backgroundScope)
|
||||
val effectTurbine = testSubject.effect.testIn(backgroundScope)
|
||||
val turbines = listOf(stateTurbine, effectTurbine)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(initialState)
|
||||
}
|
||||
|
||||
testSubject.event(Event.SignInClicked)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = effectTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(Effect.LaunchOAuth(intent))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should show error when SignInClicked event received and OAuth is not supported`() = runTest {
|
||||
val initialState = State(
|
||||
hostname = "example.com",
|
||||
emailAddress = "test@example.com",
|
||||
)
|
||||
val testSubject = AccountOAuthViewModel(
|
||||
getOAuthRequestIntent = { _, _ ->
|
||||
AuthorizationIntentResult.NotSupported
|
||||
},
|
||||
initialState = initialState,
|
||||
)
|
||||
|
||||
val stateTurbine = testSubject.state.testIn(backgroundScope)
|
||||
val effectTurbine = testSubject.effect.testIn(backgroundScope)
|
||||
val turbines = listOf(stateTurbine, effectTurbine)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(initialState)
|
||||
}
|
||||
|
||||
testSubject.event(Event.SignInClicked)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(initialState.copy(error = Error.NotSupported))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should remove error and launch OAuth when OnRetryClicked event received`() = runTest {
|
||||
val initialState = State(
|
||||
hostname = "example.com",
|
||||
emailAddress = "test@example.com",
|
||||
error = Error.NotSupported,
|
||||
)
|
||||
val intent = Intent()
|
||||
val testSubject = AccountOAuthViewModel(
|
||||
getOAuthRequestIntent = { _, _ ->
|
||||
AuthorizationIntentResult.Success(intent = intent)
|
||||
},
|
||||
initialState = initialState,
|
||||
)
|
||||
|
||||
val stateTurbine = testSubject.state.testIn(backgroundScope)
|
||||
val effectTurbine = testSubject.effect.testIn(backgroundScope)
|
||||
val turbines = listOf(stateTurbine, effectTurbine)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(initialState)
|
||||
}
|
||||
|
||||
testSubject.event(Event.OnRetryClicked)
|
||||
|
||||
assertThat(stateTurbine.awaitItem()).isEqualTo(
|
||||
initialState.copy(error = null)
|
||||
)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = effectTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(Effect.LaunchOAuth(intent))
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package app.k9mail.feature.account.oauth.ui
|
||||
|
||||
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Effect
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Event
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.State
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.ViewModel
|
||||
|
||||
class FakeAccountOAuthViewModel(
|
||||
initialState: State = State(),
|
||||
) : BaseViewModel<State, Event, Effect>(initialState), ViewModel {
|
||||
|
||||
val events = mutableListOf<Event>()
|
||||
|
||||
override fun initState(state: State) {
|
||||
updateState { state }
|
||||
}
|
||||
|
||||
override fun event(event: Event) {
|
||||
events.add(event)
|
||||
}
|
||||
|
||||
fun effect(effect: Effect) {
|
||||
emitEffect(effect)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue