Merge pull request #128 from fknives/#41-hilt-entry-point-update

Issue#41 Update Hilt Entry Point access in compose
This commit is contained in:
Gergely Hegedis 2022-09-29 16:12:27 +03:00 committed by GitHub
commit 90b5334af8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 82 additions and 68 deletions

View file

@ -83,6 +83,9 @@ class AuthComposeInstrumentedTest : KoinTest {
robot.setUsername("banan")
.assertUsername("banan")
.clickOnLogin()
composeTestRule.mainClock.autoAdvance = false
composeTestRule.waitForIdle()
composeTestRule.mainClock.autoAdvance = true
robot.assertErrorIsShown(R.string.password_is_invalid)
.assertNotLoading()
@ -99,6 +102,9 @@ class AuthComposeInstrumentedTest : KoinTest {
.setPassword("banan")
.assertPassword("banan")
.clickOnLogin()
composeTestRule.mainClock.autoAdvance = false
composeTestRule.waitForIdle()
composeTestRule.mainClock.autoAdvance = true
robot.assertErrorIsShown(R.string.username_is_invalid)
.assertNotLoading()
@ -109,7 +115,7 @@ class AuthComposeInstrumentedTest : KoinTest {
@Test
fun invalidCredentialsGivenShowsProperErrorMessage() {
mockServerScenarioSetup.setScenario(
AuthScenario.InvalidCredentials(password = "alma", username = "banan")
AuthScenario.InvalidCredentials(username = "alma", password = "banan")
)
composeTestRule.mainClock.advanceTimeBy(SPLASH_DELAY)

View file

@ -111,7 +111,6 @@ Put a tag on it and use the same finders and assertions.
The setup is the mostly the same as for View so for the sake of simplicity let's focus on the differences.
##### Initializing the UI
We don't need an activity scenario. We will use instead `createComposeRule()` which will handle the host activity.
If you need a specific activity, use `createAndroidComposeRule<YourActivity>()`.
@ -224,9 +223,7 @@ navigationRobot.assertHomeScreen()
```
> Note: Any node interactions call waitForIdle which waits for the Coroutine then the Network Call to finish. The Network call is running on OkHttps's own thread, so we use IdlingResources to synchronize with it. This is done in the ComposeNetworkSynchronizationTestRule.
> waitForIdle blocks the current thread while the Resources are busy. There is an alternative awaitIdle() which can be useful in runTest suspendable tests, feel free to look inside the Interface of ComposeTestRule.
> If you don't interact with a node but want to synchronize, then you will need waitForIdle. For example to verify something was called or written into like FakeLocalStorage in this example
> Basically since we have OkHttpIdlingResource as an EspressoIdlingResource we adapt that to Compose's IdlingResource class and register it with the ComposeTestRule and unregister it at the end.
> In ComposeNetworkSynchronizationTestRuleBasically since we have OkHttpIdlingResource as an EspressoIdlingResource we adapt that to Compose's IdlingResource class and register it with the ComposeTestRule and unregister it at the end.
### 2. `emptyPasswordShowsProperErrorMessage`
@ -245,7 +242,18 @@ robot.setUsername("banan")
.clickOnLogin()
```
Finally we let coroutines go and verify the error is shown and we have not navigated:
Now, we will let the coroutine go and await the network call:
```kotlin
composeTestRule.mainClock.autoAdvance = false
composeTestRule.waitForIdle()
composeTestRule.mainClock.autoAdvance = true
```
This may seem a bit odd, but what we want is to be sure that while waiting for the request or any idling resource we do not advance the Compose Clock. That's because Snackbar has a LaunchedEffect to dismiss, if we let the clock run, it could sometime dismiss the Snackbar which could result in a Flaky test.
> autoAdvance=off, waitForIdle(), autoAdvance=on pattern can be used to await external resources without affecting Compose Side Effects.
Finally we verify the error is shown and we have not navigated:
```kotlin
robot.assertErrorIsShown(R.string.password_is_invalid)
.assertNotLoading()
@ -265,6 +273,9 @@ robot
.setPassword("banan")
.assertPassword("banan")
.clickOnLogin()
composeTestRule.mainClock.autoAdvance = false
composeTestRule.waitForIdle()
composeTestRule.mainClock.autoAdvance = true
robot.assertErrorIsShown(R.string.username_is_invalid)
.assertNotLoading()
@ -296,6 +307,9 @@ robot.assertLoading()
composeTestRule.mainClock.autoAdvance = true
```
> Note: `robot.assertLoading` since it is a node interaction already calls waitForIdle.
> We only advance the time by one frame to be able to verify the Loading, otherwise it would already disappear.
Now at the end verify the error is shown properly:
```kotlin
robot.assertErrorIsShown(R.string.credentials_invalid)

View file

@ -99,6 +99,9 @@ class AuthComposeInstrumentedTest {
robot.setUsername("banan")
.assertUsername("banan")
.clickOnLogin()
composeTestRule.mainClock.autoAdvance = false
composeTestRule.waitForIdle()
composeTestRule.mainClock.autoAdvance = true
robot.assertErrorIsShown(R.string.password_is_invalid)
.assertNotLoading()
@ -115,6 +118,9 @@ class AuthComposeInstrumentedTest {
.setPassword("banan")
.assertPassword("banan")
.clickOnLogin()
composeTestRule.mainClock.autoAdvance = false
composeTestRule.waitForIdle()
composeTestRule.mainClock.autoAdvance = true
robot.assertErrorIsShown(R.string.username_is_invalid)
.assertNotLoading()
@ -125,7 +131,7 @@ class AuthComposeInstrumentedTest {
@Test
fun invalidCredentialsGivenShowsProperErrorMessage() {
mockServerScenarioSetup.setScenario(
AuthScenario.InvalidCredentials(password = "alma", username = "banan")
AuthScenario.InvalidCredentials(username = "alma", password = "banan")
)
composeTestRule.mainClock.advanceTimeBy(SPLASH_DELAY)

View file

@ -1,4 +1,4 @@
package org.fnives.test.showcase.hilt.compose.screen.home
package org.fnives.test.showcase.hilt.compose.di
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@ -11,13 +11,30 @@ import org.fnives.test.showcase.hilt.core.content.AddContentToFavouriteUseCase
import org.fnives.test.showcase.hilt.core.content.FetchContentUseCase
import org.fnives.test.showcase.hilt.core.content.GetAllContentUseCase
import org.fnives.test.showcase.hilt.core.content.RemoveContentFromFavouritesUseCase
import org.fnives.test.showcase.hilt.core.login.IsUserLoggedInUseCase
import org.fnives.test.showcase.hilt.core.login.LoginUseCase
import org.fnives.test.showcase.hilt.core.login.LogoutUseCase
object HomeEntryPoint {
object ComposeEntryPoint {
/**
* Helper method to easily remember and access Hilt Dependencies in Compose.
*/
@Composable
inline fun <reified T : Any> rememberEntryPoint(component: Any = LocalContext.current.applicationContext): T =
remember(component) { EntryPoints.get(component, T::class.java) }
sealed interface EntryPointDependencies
@EntryPoint
@InstallIn(SingletonComponent::class)
interface MainDependencies {
interface AuthDependencies : EntryPointDependencies {
val loginUseCase: LoginUseCase
}
@EntryPoint
@InstallIn(SingletonComponent::class)
interface MainDependencies : EntryPointDependencies {
val getAllContentUseCase: GetAllContentUseCase
val logoutUseCase: LogoutUseCase
val fetchContentUseCase: FetchContentUseCase
@ -25,9 +42,9 @@ object HomeEntryPoint {
val removeContentFromFavouritesUseCase: RemoveContentFromFavouritesUseCase
}
@Composable
fun get(): MainDependencies {
val context = LocalContext.current.applicationContext
return remember { EntryPoints.get(context, MainDependencies::class.java) }
@EntryPoint
@InstallIn(SingletonComponent::class)
interface AppNavigationDependencies : EntryPointDependencies {
val isUserLoggedInUseCase: IsUserLoggedInUseCase
}
}

View file

@ -11,6 +11,8 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.delay
import org.fnives.test.showcase.hilt.compose.di.ComposeEntryPoint.AppNavigationDependencies
import org.fnives.test.showcase.hilt.compose.di.ComposeEntryPoint.rememberEntryPoint
import org.fnives.test.showcase.hilt.compose.screen.auth.AuthScreen
import org.fnives.test.showcase.hilt.compose.screen.auth.rememberAuthScreenState
import org.fnives.test.showcase.hilt.compose.screen.home.HomeScreen
@ -20,8 +22,11 @@ import org.fnives.test.showcase.hilt.core.login.IsUserLoggedInUseCase
@Composable
fun AppNavigation(
isUserLogeInUseCase: IsUserLoggedInUseCase = AppNavigationEntryPoint.get().isUserLoggedInUseCase
) {
navigationDependencies: AppNavigationDependencies = rememberEntryPoint()
) = AppNavigation(navigationDependencies.isUserLoggedInUseCase)
@Composable
fun AppNavigation(isUserLogeInUseCase: IsUserLoggedInUseCase) {
val navController = rememberNavController()
LaunchedEffect(isUserLogeInUseCase) {

View file

@ -1,25 +0,0 @@
package org.fnives.test.showcase.hilt.compose.screen
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import dagger.hilt.EntryPoint
import dagger.hilt.EntryPoints
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.fnives.test.showcase.hilt.core.login.IsUserLoggedInUseCase
object AppNavigationEntryPoint {
@EntryPoint
@InstallIn(SingletonComponent::class)
interface AppNavigationDependencies {
val isUserLoggedInUseCase: IsUserLoggedInUseCase
}
@Composable
fun get(): AppNavigationDependencies {
val context = LocalContext.current.applicationContext
return remember { EntryPoints.get(context, AppNavigationDependencies::class.java) }
}
}

View file

@ -1,25 +0,0 @@
package org.fnives.test.showcase.hilt.compose.screen.auth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import dagger.hilt.EntryPoint
import dagger.hilt.EntryPoints
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.fnives.test.showcase.hilt.core.login.LoginUseCase
object AuthEntryPoint {
@EntryPoint
@InstallIn(SingletonComponent::class)
interface AuthDependencies {
val loginUseCase: LoginUseCase
}
@Composable
fun get(): AuthDependencies {
val context = LocalContext.current.applicationContext
return remember { EntryPoints.get(context, AuthDependencies::class.java) }
}
}

View file

@ -11,6 +11,8 @@ import androidx.compose.runtime.setValue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.fnives.test.showcase.hilt.compose.di.ComposeEntryPoint.AuthDependencies
import org.fnives.test.showcase.hilt.compose.di.ComposeEntryPoint.rememberEntryPoint
import org.fnives.test.showcase.hilt.core.login.LoginUseCase
import org.fnives.test.showcase.model.auth.LoginCredentials
import org.fnives.test.showcase.model.auth.LoginStatus
@ -19,7 +21,19 @@ import org.fnives.test.showcase.model.shared.Answer
@Composable
fun rememberAuthScreenState(
stateScope: CoroutineScope = rememberCoroutineScope { Dispatchers.Main },
loginUseCase: LoginUseCase = AuthEntryPoint.get().loginUseCase,
authDependencies: AuthDependencies = rememberEntryPoint(),
onLoginSuccess: () -> Unit = {},
): AuthScreenState =
rememberAuthScreenState(
stateScope = stateScope,
loginUseCase = authDependencies.loginUseCase,
onLoginSuccess = onLoginSuccess
)
@Composable
fun rememberAuthScreenState(
stateScope: CoroutineScope = rememberCoroutineScope { Dispatchers.Main },
loginUseCase: LoginUseCase,
onLoginSuccess: () -> Unit = {},
): AuthScreenState {
return rememberSaveable(saver = AuthScreenState.getSaver(stateScope, loginUseCase, onLoginSuccess)) {

View file

@ -9,6 +9,8 @@ import androidx.compose.runtime.setValue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
import org.fnives.test.showcase.hilt.compose.di.ComposeEntryPoint.MainDependencies
import org.fnives.test.showcase.hilt.compose.di.ComposeEntryPoint.rememberEntryPoint
import org.fnives.test.showcase.hilt.core.content.AddContentToFavouriteUseCase
import org.fnives.test.showcase.hilt.core.content.FetchContentUseCase
import org.fnives.test.showcase.hilt.core.content.GetAllContentUseCase
@ -21,7 +23,7 @@ import org.fnives.test.showcase.model.shared.Resource
@Composable
fun rememberHomeScreenState(
stateScope: CoroutineScope = rememberCoroutineScope(),
mainDependencies: HomeEntryPoint.MainDependencies = HomeEntryPoint.get(),
mainDependencies: MainDependencies = rememberEntryPoint(),
onLogout: () -> Unit = {},
) =
rememberHomeScreenState(