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:
commit
90b5334af8
9 changed files with 82 additions and 68 deletions
|
|
@ -83,6 +83,9 @@ class AuthComposeInstrumentedTest : KoinTest {
|
||||||
robot.setUsername("banan")
|
robot.setUsername("banan")
|
||||||
.assertUsername("banan")
|
.assertUsername("banan")
|
||||||
.clickOnLogin()
|
.clickOnLogin()
|
||||||
|
composeTestRule.mainClock.autoAdvance = false
|
||||||
|
composeTestRule.waitForIdle()
|
||||||
|
composeTestRule.mainClock.autoAdvance = true
|
||||||
|
|
||||||
robot.assertErrorIsShown(R.string.password_is_invalid)
|
robot.assertErrorIsShown(R.string.password_is_invalid)
|
||||||
.assertNotLoading()
|
.assertNotLoading()
|
||||||
|
|
@ -99,6 +102,9 @@ class AuthComposeInstrumentedTest : KoinTest {
|
||||||
.setPassword("banan")
|
.setPassword("banan")
|
||||||
.assertPassword("banan")
|
.assertPassword("banan")
|
||||||
.clickOnLogin()
|
.clickOnLogin()
|
||||||
|
composeTestRule.mainClock.autoAdvance = false
|
||||||
|
composeTestRule.waitForIdle()
|
||||||
|
composeTestRule.mainClock.autoAdvance = true
|
||||||
|
|
||||||
robot.assertErrorIsShown(R.string.username_is_invalid)
|
robot.assertErrorIsShown(R.string.username_is_invalid)
|
||||||
.assertNotLoading()
|
.assertNotLoading()
|
||||||
|
|
@ -109,7 +115,7 @@ class AuthComposeInstrumentedTest : KoinTest {
|
||||||
@Test
|
@Test
|
||||||
fun invalidCredentialsGivenShowsProperErrorMessage() {
|
fun invalidCredentialsGivenShowsProperErrorMessage() {
|
||||||
mockServerScenarioSetup.setScenario(
|
mockServerScenarioSetup.setScenario(
|
||||||
AuthScenario.InvalidCredentials(password = "alma", username = "banan")
|
AuthScenario.InvalidCredentials(username = "alma", password = "banan")
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.mainClock.advanceTimeBy(SPLASH_DELAY)
|
composeTestRule.mainClock.advanceTimeBy(SPLASH_DELAY)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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
|
##### Initializing the UI
|
||||||
We don't need an activity scenario. We will use instead `createComposeRule()` which will handle the host activity.
|
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>()`.
|
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.
|
> 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.
|
> 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.
|
||||||
> 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.
|
|
||||||
|
|
||||||
### 2. `emptyPasswordShowsProperErrorMessage`
|
### 2. `emptyPasswordShowsProperErrorMessage`
|
||||||
|
|
||||||
|
|
@ -245,7 +242,18 @@ robot.setUsername("banan")
|
||||||
.clickOnLogin()
|
.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
|
```kotlin
|
||||||
robot.assertErrorIsShown(R.string.password_is_invalid)
|
robot.assertErrorIsShown(R.string.password_is_invalid)
|
||||||
.assertNotLoading()
|
.assertNotLoading()
|
||||||
|
|
@ -265,6 +273,9 @@ robot
|
||||||
.setPassword("banan")
|
.setPassword("banan")
|
||||||
.assertPassword("banan")
|
.assertPassword("banan")
|
||||||
.clickOnLogin()
|
.clickOnLogin()
|
||||||
|
composeTestRule.mainClock.autoAdvance = false
|
||||||
|
composeTestRule.waitForIdle()
|
||||||
|
composeTestRule.mainClock.autoAdvance = true
|
||||||
|
|
||||||
robot.assertErrorIsShown(R.string.username_is_invalid)
|
robot.assertErrorIsShown(R.string.username_is_invalid)
|
||||||
.assertNotLoading()
|
.assertNotLoading()
|
||||||
|
|
@ -296,6 +307,9 @@ robot.assertLoading()
|
||||||
composeTestRule.mainClock.autoAdvance = true
|
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:
|
Now at the end verify the error is shown properly:
|
||||||
```kotlin
|
```kotlin
|
||||||
robot.assertErrorIsShown(R.string.credentials_invalid)
|
robot.assertErrorIsShown(R.string.credentials_invalid)
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,9 @@ class AuthComposeInstrumentedTest {
|
||||||
robot.setUsername("banan")
|
robot.setUsername("banan")
|
||||||
.assertUsername("banan")
|
.assertUsername("banan")
|
||||||
.clickOnLogin()
|
.clickOnLogin()
|
||||||
|
composeTestRule.mainClock.autoAdvance = false
|
||||||
|
composeTestRule.waitForIdle()
|
||||||
|
composeTestRule.mainClock.autoAdvance = true
|
||||||
|
|
||||||
robot.assertErrorIsShown(R.string.password_is_invalid)
|
robot.assertErrorIsShown(R.string.password_is_invalid)
|
||||||
.assertNotLoading()
|
.assertNotLoading()
|
||||||
|
|
@ -115,6 +118,9 @@ class AuthComposeInstrumentedTest {
|
||||||
.setPassword("banan")
|
.setPassword("banan")
|
||||||
.assertPassword("banan")
|
.assertPassword("banan")
|
||||||
.clickOnLogin()
|
.clickOnLogin()
|
||||||
|
composeTestRule.mainClock.autoAdvance = false
|
||||||
|
composeTestRule.waitForIdle()
|
||||||
|
composeTestRule.mainClock.autoAdvance = true
|
||||||
|
|
||||||
robot.assertErrorIsShown(R.string.username_is_invalid)
|
robot.assertErrorIsShown(R.string.username_is_invalid)
|
||||||
.assertNotLoading()
|
.assertNotLoading()
|
||||||
|
|
@ -125,7 +131,7 @@ class AuthComposeInstrumentedTest {
|
||||||
@Test
|
@Test
|
||||||
fun invalidCredentialsGivenShowsProperErrorMessage() {
|
fun invalidCredentialsGivenShowsProperErrorMessage() {
|
||||||
mockServerScenarioSetup.setScenario(
|
mockServerScenarioSetup.setScenario(
|
||||||
AuthScenario.InvalidCredentials(password = "alma", username = "banan")
|
AuthScenario.InvalidCredentials(username = "alma", password = "banan")
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.mainClock.advanceTimeBy(SPLASH_DELAY)
|
composeTestRule.mainClock.advanceTimeBy(SPLASH_DELAY)
|
||||||
|
|
|
||||||
|
|
@ -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.Composable
|
||||||
import androidx.compose.runtime.remember
|
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.FetchContentUseCase
|
||||||
import org.fnives.test.showcase.hilt.core.content.GetAllContentUseCase
|
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.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
|
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
|
@EntryPoint
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface MainDependencies {
|
interface AuthDependencies : EntryPointDependencies {
|
||||||
|
val loginUseCase: LoginUseCase
|
||||||
|
}
|
||||||
|
|
||||||
|
@EntryPoint
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
interface MainDependencies : EntryPointDependencies {
|
||||||
val getAllContentUseCase: GetAllContentUseCase
|
val getAllContentUseCase: GetAllContentUseCase
|
||||||
val logoutUseCase: LogoutUseCase
|
val logoutUseCase: LogoutUseCase
|
||||||
val fetchContentUseCase: FetchContentUseCase
|
val fetchContentUseCase: FetchContentUseCase
|
||||||
|
|
@ -25,9 +42,9 @@ object HomeEntryPoint {
|
||||||
val removeContentFromFavouritesUseCase: RemoveContentFromFavouritesUseCase
|
val removeContentFromFavouritesUseCase: RemoveContentFromFavouritesUseCase
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@EntryPoint
|
||||||
fun get(): MainDependencies {
|
@InstallIn(SingletonComponent::class)
|
||||||
val context = LocalContext.current.applicationContext
|
interface AppNavigationDependencies : EntryPointDependencies {
|
||||||
return remember { EntryPoints.get(context, MainDependencies::class.java) }
|
val isUserLoggedInUseCase: IsUserLoggedInUseCase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -11,6 +11,8 @@ import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import kotlinx.coroutines.delay
|
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.AuthScreen
|
||||||
import org.fnives.test.showcase.hilt.compose.screen.auth.rememberAuthScreenState
|
import org.fnives.test.showcase.hilt.compose.screen.auth.rememberAuthScreenState
|
||||||
import org.fnives.test.showcase.hilt.compose.screen.home.HomeScreen
|
import org.fnives.test.showcase.hilt.compose.screen.home.HomeScreen
|
||||||
|
|
@ -20,8 +22,11 @@ import org.fnives.test.showcase.hilt.core.login.IsUserLoggedInUseCase
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppNavigation(
|
fun AppNavigation(
|
||||||
isUserLogeInUseCase: IsUserLoggedInUseCase = AppNavigationEntryPoint.get().isUserLoggedInUseCase
|
navigationDependencies: AppNavigationDependencies = rememberEntryPoint()
|
||||||
) {
|
) = AppNavigation(navigationDependencies.isUserLoggedInUseCase)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppNavigation(isUserLogeInUseCase: IsUserLoggedInUseCase) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
LaunchedEffect(isUserLogeInUseCase) {
|
LaunchedEffect(isUserLogeInUseCase) {
|
||||||
|
|
|
||||||
|
|
@ -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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -11,6 +11,8 @@ import androidx.compose.runtime.setValue
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
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.hilt.core.login.LoginUseCase
|
||||||
import org.fnives.test.showcase.model.auth.LoginCredentials
|
import org.fnives.test.showcase.model.auth.LoginCredentials
|
||||||
import org.fnives.test.showcase.model.auth.LoginStatus
|
import org.fnives.test.showcase.model.auth.LoginStatus
|
||||||
|
|
@ -19,7 +21,19 @@ import org.fnives.test.showcase.model.shared.Answer
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberAuthScreenState(
|
fun rememberAuthScreenState(
|
||||||
stateScope: CoroutineScope = rememberCoroutineScope { Dispatchers.Main },
|
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 = {},
|
onLoginSuccess: () -> Unit = {},
|
||||||
): AuthScreenState {
|
): AuthScreenState {
|
||||||
return rememberSaveable(saver = AuthScreenState.getSaver(stateScope, loginUseCase, onLoginSuccess)) {
|
return rememberSaveable(saver = AuthScreenState.getSaver(stateScope, loginUseCase, onLoginSuccess)) {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import androidx.compose.runtime.setValue
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
import kotlinx.coroutines.launch
|
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.AddContentToFavouriteUseCase
|
||||||
import org.fnives.test.showcase.hilt.core.content.FetchContentUseCase
|
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.GetAllContentUseCase
|
||||||
|
|
@ -21,7 +23,7 @@ import org.fnives.test.showcase.model.shared.Resource
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberHomeScreenState(
|
fun rememberHomeScreenState(
|
||||||
stateScope: CoroutineScope = rememberCoroutineScope(),
|
stateScope: CoroutineScope = rememberCoroutineScope(),
|
||||||
mainDependencies: HomeEntryPoint.MainDependencies = HomeEntryPoint.get(),
|
mainDependencies: MainDependencies = rememberEntryPoint(),
|
||||||
onLogout: () -> Unit = {},
|
onLogout: () -> Unit = {},
|
||||||
) =
|
) =
|
||||||
rememberHomeScreenState(
|
rememberHomeScreenState(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue