mirror of
https://github.com/bitwarden/android.git
synced 2024-11-21 17:05:44 +03:00
BIT-168: Add FakeNavHostController and tests for RootNavScreen (#46)
Co-authored-by: Andrew Haisting <ahaisting@livefront.com>
This commit is contained in:
parent
4229918d74
commit
a358408ea7
3 changed files with 252 additions and 1 deletions
|
@ -5,6 +5,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navOptions
|
||||
|
@ -24,8 +25,8 @@ import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.vaultUnlockedDestin
|
|||
@Composable
|
||||
fun RootNavScreen(
|
||||
viewModel: RootNavViewModel = hiltViewModel(),
|
||||
navController: NavHostController = rememberNavController(),
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
NavHost(
|
||||
|
|
|
@ -0,0 +1,172 @@
|
|||
package com.x8bit.bitwarden.ui.platform.base
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.navigation.NavDeepLinkRequest
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavGraph
|
||||
import androidx.navigation.NavGraphNavigator
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.Navigator
|
||||
import androidx.navigation.NavigatorProvider
|
||||
import androidx.navigation.compose.ComposeNavigator
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.Assert.assertEquals
|
||||
|
||||
/**
|
||||
* A "fake" implementation of a [NavHostController] that serves as an alternative to the direct
|
||||
* use of a [TestNavHostController](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigation/navigation-testing/src/main/java/androidx/navigation/testing/TestNavHostController.kt?q=TestNavHostController).
|
||||
*
|
||||
* The primary features of this implementations are:
|
||||
*
|
||||
* - Access to a [Context] is not required, so the class can be instantiated immediately in a test.
|
||||
* - Requested navigation is never actually performed. Instead, a record of the [lastNavigation]
|
||||
* params is provided, as well as what would be the [currentRoute] if that navigation was
|
||||
* performed.
|
||||
* - Helper functions like [assertCurrentRoute] are provided to make clear what kind of
|
||||
* functionality is probably mocked/faked and suitable for testing. These are the recommended
|
||||
* way to interact with this class.
|
||||
*
|
||||
* Note that this should only be used in cases where testing that a navigation is requested but
|
||||
* not performed is sufficient (i.e. in focused tests of a single Composable screen, rather than
|
||||
* an actual navigation flow). Because the initial setting of the graph is also stubbed out, tests
|
||||
* of Composable screens that represent the host for a nested navigation graph should not try to
|
||||
* test any of that graph's visible contents.
|
||||
*/
|
||||
@Suppress("MaxLineLength")
|
||||
class FakeNavHostController : NavHostController(context = mockk()) {
|
||||
init {
|
||||
navigatorProvider = TestNavigatorProvider()
|
||||
navigatorProvider.addNavigator(ComposeNavigator())
|
||||
}
|
||||
|
||||
/**
|
||||
* A fake ID that may be used when testing "popping" to a particular graph ID.
|
||||
*/
|
||||
val graphId: Int = -1
|
||||
|
||||
/**
|
||||
* The current route (or `null` if no initial graph has been set and no navigation has been
|
||||
* performed).
|
||||
*
|
||||
* Note that this represents what the route **would be** if actual navigation were allowed to
|
||||
* be performed.
|
||||
*/
|
||||
private var currentRoute: String? = null
|
||||
|
||||
/**
|
||||
* Represents the parameters of the last known navigation attempt via a call to [navigate].
|
||||
*/
|
||||
private var lastNavigation: Navigation? = null
|
||||
|
||||
/**
|
||||
* A mocked-out internal graph. This exists purely to allow for some internal Compose logic
|
||||
* to complete without incident when rending a nav graph in a Composable.
|
||||
*/
|
||||
private val internalGraph =
|
||||
mockk<NavGraph>().apply {
|
||||
every { id } returns graphId
|
||||
}
|
||||
|
||||
override var graph: NavGraph
|
||||
get() = internalGraph
|
||||
set(value) {
|
||||
currentRoute = value.startDestinationRoute
|
||||
}
|
||||
|
||||
override fun navigate(
|
||||
request: NavDeepLinkRequest,
|
||||
navOptions: NavOptions?,
|
||||
) {
|
||||
navigate(
|
||||
request = request,
|
||||
navOptions = navOptions,
|
||||
navigatorExtras = null,
|
||||
)
|
||||
}
|
||||
|
||||
override fun navigate(
|
||||
request: NavDeepLinkRequest,
|
||||
navOptions: NavOptions?,
|
||||
navigatorExtras: Navigator.Extras?,
|
||||
) {
|
||||
lastNavigation = Navigation(
|
||||
request = request,
|
||||
navOptions = navOptions,
|
||||
navigatorExtras = navigatorExtras,
|
||||
)
|
||||
currentRoute = request.uri?.route
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts the [currentRoute] matches the given [route].
|
||||
*/
|
||||
fun assertCurrentRoute(route: String) {
|
||||
assertEquals(currentRoute, route)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts multiple aspects of the last navigation to have occurred.
|
||||
*/
|
||||
fun assertLastNavigation(
|
||||
route: String,
|
||||
navOptions: NavOptions? = null,
|
||||
navigatorExtras: Navigator.Extras? = null,
|
||||
) {
|
||||
assertEquals(currentRoute, route)
|
||||
assertEquals(lastNavigation?.navOptions, navOptions)
|
||||
assertEquals(lastNavigation?.navigatorExtras, navigatorExtras)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts the [lastNavigation] includes the given [navOptions].
|
||||
*/
|
||||
fun assertLastNavOptions(navOptions: NavOptions?) {
|
||||
assertEquals(lastNavigation?.navOptions, navOptions)
|
||||
}
|
||||
|
||||
data class Navigation(
|
||||
val request: NavDeepLinkRequest,
|
||||
val navOptions: NavOptions?,
|
||||
val navigatorExtras: Navigator.Extras?,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for converting a [Uri] to a "route" that we'd expect to see when calling
|
||||
* `NavHostController.currentDestination?.route`.
|
||||
*/
|
||||
private val Uri.route: String get() = "${this.path}".removePrefix("/")
|
||||
|
||||
/**
|
||||
* The following is borrowed directly from the TestNavigatorProvider of the compose testing
|
||||
* library.
|
||||
*
|
||||
* See https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigation/navigation-testing/src/main/java/androidx/navigation/testing/TestNavigatorProvider.kt?q=TestNavigatorProvider
|
||||
*/
|
||||
@Suppress("MaxLineLength")
|
||||
private class TestNavigatorProvider : NavigatorProvider() {
|
||||
|
||||
/**
|
||||
* A [Navigator] that only supports creating destinations.
|
||||
*/
|
||||
private val navigator = object : Navigator<NavDestination>() {
|
||||
override fun createDestination() = NavDestination("test")
|
||||
}
|
||||
|
||||
init {
|
||||
addNavigator(NavGraphNavigator(this))
|
||||
addNavigator("test", navigator)
|
||||
}
|
||||
|
||||
override fun <T : Navigator<out NavDestination>> getNavigator(name: String): T {
|
||||
return try {
|
||||
super.getNavigator(name)
|
||||
} catch (e: IllegalStateException) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
navigator as T
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.rootnav
|
||||
|
||||
import androidx.navigation.navOptions
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.FakeNavHostController
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class RootNavScreenTest : BaseComposeTest() {
|
||||
private val fakeNavHostController = FakeNavHostController()
|
||||
|
||||
private val expectedNavOptions = navOptions {
|
||||
// When changing root navigation state, pop everything else off the back stack:
|
||||
popUpTo(fakeNavHostController.graphId) {
|
||||
inclusive = false
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial route should be splash`() {
|
||||
val viewModel = mockk<RootNavViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { stateFlow } returns MutableStateFlow(RootNavState.Splash)
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
RootNavScreen(
|
||||
viewModel = viewModel,
|
||||
navController = fakeNavHostController,
|
||||
)
|
||||
}
|
||||
composeTestRule.runOnIdle {
|
||||
fakeNavHostController.assertCurrentRoute("splash")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when root nav destination changes, navigation should follow`() = runTest {
|
||||
val rootNavStateFlow = MutableStateFlow<RootNavState>(RootNavState.Splash)
|
||||
val viewModel = mockk<RootNavViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { stateFlow } returns rootNavStateFlow
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
RootNavScreen(
|
||||
viewModel = viewModel,
|
||||
navController = fakeNavHostController,
|
||||
)
|
||||
}
|
||||
composeTestRule.runOnIdle {
|
||||
fakeNavHostController.assertCurrentRoute("splash")
|
||||
}
|
||||
|
||||
// Make sure navigating to Auth works as expected:
|
||||
rootNavStateFlow.value = RootNavState.Auth
|
||||
composeTestRule.runOnIdle {
|
||||
fakeNavHostController.assertLastNavigation(
|
||||
route = "auth",
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
|
||||
// Make sure navigating to vault unlocked works as expected:
|
||||
rootNavStateFlow.value = RootNavState.VaultUnlocked
|
||||
composeTestRule.runOnIdle {
|
||||
fakeNavHostController.assertLastNavigation(
|
||||
route = "VaultUnlocked",
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue