mirror of
https://github.com/bitwarden/android.git
synced 2026-07-29 03:53:48 -05:00
PM-21252: Create mock NavHostController for navigation testing (#5159)
This commit is contained in:
@@ -1,6 +1,3 @@
|
||||
// TODO: Add tests for this (PM-21252)
|
||||
@file:OmitFromCoverage
|
||||
|
||||
package com.x8bit.bitwarden.ui.platform.feature.rootnav
|
||||
|
||||
import androidx.activity.compose.LocalActivity
|
||||
@@ -16,7 +13,6 @@ import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.navOptions
|
||||
import com.bitwarden.core.annotation.OmitFromCoverage
|
||||
import com.bitwarden.ui.platform.theme.NonNullEnterTransitionProvider
|
||||
import com.bitwarden.ui.platform.theme.NonNullExitTransitionProvider
|
||||
import com.bitwarden.ui.platform.theme.RootTransitionProviders
|
||||
|
||||
-4
@@ -1,6 +1,3 @@
|
||||
// TODO: Add tests for this (PM-21252)
|
||||
@file:OmitFromCoverage
|
||||
|
||||
package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar
|
||||
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
@@ -22,7 +19,6 @@ import androidx.navigation.NavOptions
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.navOptions
|
||||
import com.bitwarden.core.annotation.OmitFromCoverage
|
||||
import com.bitwarden.ui.platform.theme.RootTransitionProviders
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.NavigationItem
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
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.NavigatorState
|
||||
import androidx.navigation.compose.ComposeNavigator
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
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())
|
||||
|
||||
val state = mockk<NavigatorState>(relaxed = true) {
|
||||
every { backStack } returns MutableStateFlow(emptyList())
|
||||
}
|
||||
navigatorProvider.navigators.forEach { (_, navigator) ->
|
||||
navigator.onAttach(state)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
every { startDestinationId } returns graphId
|
||||
every {
|
||||
findNode(resId = graphId)
|
||||
} returns mockk { every { id } returns graphId }
|
||||
every {
|
||||
findNodeComprehensive(resId = any(), lastVisited = any(), searchChildren = any())
|
||||
} returns mockk {
|
||||
every { id } returns graphId
|
||||
every { arguments } returns emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
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(route, currentRoute)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts multiple aspects of the last navigation to have occurred.
|
||||
*/
|
||||
fun assertLastNavigation(
|
||||
route: String,
|
||||
navOptions: NavOptions? = null,
|
||||
navigatorExtras: Navigator.Extras? = null,
|
||||
) {
|
||||
assertEquals(route, currentRoute)
|
||||
assertEquals(navOptions, lastNavigation?.navOptions)
|
||||
assertEquals(navigatorExtras, lastNavigation?.navigatorExtras)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts the [lastNavigation] includes the given [navOptions].
|
||||
*/
|
||||
fun assertLastNavOptions(navOptions: NavOptions?) {
|
||||
assertEquals(navOptions, lastNavigation?.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
|
||||
}
|
||||
}
|
||||
}
|
||||
+375
@@ -0,0 +1,375 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.rootnav
|
||||
|
||||
import androidx.navigation.navOptions
|
||||
import com.bitwarden.ui.platform.base.createMockNavHostController
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupAutofillRoute
|
||||
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupCompleteRoute
|
||||
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupUnlockRoute
|
||||
import com.x8bit.bitwarden.ui.auth.feature.auth.AuthGraphRoute
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationRoute
|
||||
import com.x8bit.bitwarden.ui.auth.feature.expiredregistrationlink.ExpiredRegistrationLinkRoute
|
||||
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.ResetPasswordRoute
|
||||
import com.x8bit.bitwarden.ui.auth.feature.setpassword.SetPasswordRoute
|
||||
import com.x8bit.bitwarden.ui.auth.feature.trusteddevice.TrustedDeviceGraphRoute
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.VaultUnlockRoute
|
||||
import com.x8bit.bitwarden.ui.auth.feature.welcome.WelcomeRoute
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.feature.splash.SplashRoute
|
||||
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.VaultUnlockedGraphRoute
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.AddSendRoute
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.ModeType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditMode
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditRoute
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.ItemListingType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingRoute
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
|
||||
class RootNavScreenTest : BaseComposeTest() {
|
||||
private val mockNavHostController = createMockNavHostController()
|
||||
private val rootNavStateFlow = MutableStateFlow<RootNavState>(RootNavState.Splash)
|
||||
private val viewModel = mockk<RootNavViewModel> {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { stateFlow } returns rootNavStateFlow
|
||||
}
|
||||
|
||||
private val expectedNavOptions = navOptions {
|
||||
// When changing root navigation state, pop everything else off the back stack:
|
||||
popUpTo(id = mockNavHostController.graph.id) {
|
||||
inclusive = false
|
||||
saveState = false
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = false
|
||||
}
|
||||
|
||||
private var isSplashScreenRemoved: Boolean = false
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
setContent {
|
||||
RootNavScreen(
|
||||
viewModel = viewModel,
|
||||
navController = mockNavHostController,
|
||||
onSplashScreenRemoved = { isSplashScreenRemoved = true },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial route should be splash`() {
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(
|
||||
route = SplashRoute,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when root nav destination changes, navigation should follow`() = runTest {
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(
|
||||
route = SplashRoute,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
assertFalse(isSplashScreenRemoved)
|
||||
|
||||
// Make sure navigating to Auth works as expected:
|
||||
rootNavStateFlow.value = RootNavState.Auth
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(
|
||||
route = AuthGraphRoute,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
assertTrue(isSplashScreenRemoved)
|
||||
|
||||
// Make sure navigating to Auth with the welcome route works as expected:
|
||||
rootNavStateFlow.value = RootNavState.AuthWithWelcome
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(
|
||||
route = WelcomeRoute,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure navigating to complete registration route works as expected:
|
||||
rootNavStateFlow.value = RootNavState.CompleteOngoingRegistration(
|
||||
email = "example@email.com",
|
||||
verificationToken = "verificationToken",
|
||||
fromEmail = true,
|
||||
timestamp = FIXED_CLOCK.millis(),
|
||||
)
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(
|
||||
route = CompleteRegistrationRoute(
|
||||
emailAddress = "example@email.com",
|
||||
verificationToken = "verificationToken",
|
||||
fromEmail = true,
|
||||
),
|
||||
navOptions = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure navigating to expired registration link route works as expected:
|
||||
rootNavStateFlow.value = RootNavState.ExpiredRegistrationLink
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(route = ExpiredRegistrationLinkRoute)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure navigating to vault locked works as expected:
|
||||
rootNavStateFlow.value = RootNavState.VaultLocked
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(
|
||||
route = VaultUnlockRoute.Standard,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure navigating to reset password works as expected:
|
||||
rootNavStateFlow.value = RootNavState.ResetPassword
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(
|
||||
route = ResetPasswordRoute,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure navigating to set password works as expected:
|
||||
rootNavStateFlow.value = RootNavState.SetPassword
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(
|
||||
route = SetPasswordRoute,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure navigating to set password works as expected:
|
||||
rootNavStateFlow.value = RootNavState.TrustedDevice
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(
|
||||
route = TrustedDeviceGraphRoute,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure navigating to vault unlocked works as expected:
|
||||
rootNavStateFlow.value = RootNavState.VaultUnlocked(activeUserId = "userId")
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(
|
||||
route = VaultUnlockedGraphRoute,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure navigating to vault unlocked for new totp works as expected:
|
||||
rootNavStateFlow.value = RootNavState.VaultUnlockedForNewTotp(activeUserId = "userId")
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(
|
||||
route = VaultItemListingRoute.AsRoot(
|
||||
type = ItemListingType.LOGIN,
|
||||
itemId = null,
|
||||
),
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure navigating to vault unlocked for new sends works as expected:
|
||||
rootNavStateFlow.value = RootNavState.VaultUnlockedForNewSend
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(
|
||||
route = AddSendRoute(
|
||||
type = ModeType.ADD,
|
||||
editSendId = null,
|
||||
),
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure navigating to vault unlocked for autofill save works as expected:
|
||||
rootNavStateFlow.value = RootNavState.VaultUnlockedForAutofillSave(
|
||||
autofillSaveItem = mockk(),
|
||||
)
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(
|
||||
route = VaultUnlockedGraphRoute,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
mockNavHostController.navigate(
|
||||
route = VaultAddEditRoute(
|
||||
vaultAddEditMode = VaultAddEditMode.ADD,
|
||||
vaultItemId = null,
|
||||
vaultItemCipherType = VaultItemCipherType.LOGIN,
|
||||
selectedFolderId = null,
|
||||
selectedCollectionId = null,
|
||||
),
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure navigating to vault unlocked for autofill works as expected:
|
||||
rootNavStateFlow.value = RootNavState.VaultUnlockedForAutofillSelection(
|
||||
activeUserId = "userId",
|
||||
type = AutofillSelectionData.Type.LOGIN,
|
||||
)
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(
|
||||
route = VaultUnlockedGraphRoute,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
mockNavHostController.navigate(
|
||||
route = VaultItemListingRoute.AsRoot(
|
||||
type = ItemListingType.LOGIN,
|
||||
itemId = null,
|
||||
),
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure navigating to vault unlocked for CreateCredentialRequest works as expected:
|
||||
rootNavStateFlow.value = RootNavState.VaultUnlockedForFido2Save(
|
||||
activeUserId = "activeUserId",
|
||||
createCredentialRequest = mockk(),
|
||||
)
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(
|
||||
route = VaultUnlockedGraphRoute,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
mockNavHostController.navigate(
|
||||
route = VaultItemListingRoute.AsRoot(
|
||||
type = ItemListingType.LOGIN,
|
||||
itemId = null,
|
||||
),
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure navigating to vault unlocked for Fido2Assertion works as expected:
|
||||
rootNavStateFlow.value = RootNavState.VaultUnlockedForFido2Assertion(
|
||||
activeUserId = "activeUserId",
|
||||
fido2CredentialAssertionRequest = mockk(),
|
||||
)
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(
|
||||
route = VaultUnlockedGraphRoute,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
mockNavHostController.navigate(
|
||||
route = VaultItemListingRoute.AsRoot(
|
||||
type = ItemListingType.LOGIN,
|
||||
itemId = null,
|
||||
),
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure navigating to vault unlocked for GetCredentialsRequest works as expected:
|
||||
rootNavStateFlow.value = RootNavState.VaultUnlockedForProviderGetCredentials(
|
||||
activeUserId = "activeUserId",
|
||||
getCredentialsRequest = mockk(),
|
||||
)
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(
|
||||
route = VaultUnlockedGraphRoute,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
mockNavHostController.navigate(
|
||||
route = VaultItemListingRoute.AsRoot(
|
||||
type = ItemListingType.LOGIN,
|
||||
itemId = null,
|
||||
),
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure navigating to account lock setup works as expected:
|
||||
rootNavStateFlow.value = RootNavState.OnboardingAccountLockSetup
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(
|
||||
route = SetupUnlockRoute.AsRoot,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure navigating to account autofill setup works as expected:
|
||||
rootNavStateFlow.value = RootNavState.OnboardingAutoFillSetup
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(
|
||||
route = SetupAutofillRoute.AsRoot,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure navigating to account setup complete works as expected:
|
||||
rootNavStateFlow.value = RootNavState.OnboardingStepsComplete
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(
|
||||
route = SetupCompleteRoute,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val FIXED_CLOCK: Clock = Clock.fixed(
|
||||
Instant.parse("2023-10-27T12:00:00Z"),
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar
|
||||
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.navigation.navOptions
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.ui.platform.base.createMockNavHostController
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.SettingsGraphRoute
|
||||
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorGraphRoute
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.SendGraphRoute
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultGraphRoute
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
|
||||
private val mockNavHostController = createMockNavHostController()
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<VaultUnlockedNavBarEvent>()
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
val viewModel = mockk<VaultUnlockedNavBarViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
}
|
||||
|
||||
private val expectedNavOptions = navOptions {
|
||||
// When changing root navigation state, pop everything else off the back stack:
|
||||
popUpTo(id = mockNavHostController.graph.id) {
|
||||
inclusive = false
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
setContent {
|
||||
VaultUnlockedNavBarScreen(
|
||||
viewModel = viewModel,
|
||||
navController = mockNavHostController,
|
||||
onNavigateToVaultAddItem = {},
|
||||
onNavigateToVaultItem = {},
|
||||
onNavigateToVaultEditItem = {},
|
||||
onNavigateToAddSend = {},
|
||||
onNavigateToEditSend = {},
|
||||
onNavigateToDeleteAccount = {},
|
||||
onNavigateToExportVault = {},
|
||||
onNavigateToFolders = {},
|
||||
onNavigateToPasswordHistory = {},
|
||||
onNavigateToPendingRequests = {},
|
||||
onNavigateToSearchVault = {},
|
||||
onNavigateToSearchSend = {},
|
||||
onNavigateToSetupAutoFillScreen = {},
|
||||
onNavigateToSetupUnlockScreen = {},
|
||||
onNavigateToImportLogins = {},
|
||||
onNavigateToAddFolderScreen = {},
|
||||
onNavigateToFlightRecorder = {},
|
||||
onNavigateToRecordedLogs = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vault tab click should send VaultTabClick action`() {
|
||||
composeTestRule.onNodeWithText(text = "My vault").performClick()
|
||||
verify { viewModel.trySendAction(VaultUnlockedNavBarAction.VaultTabClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToVaultScreen should navigate to VaultScreen`() {
|
||||
mutableEventFlow.tryEmit(
|
||||
VaultUnlockedNavBarEvent.NavigateToVaultScreen(
|
||||
labelRes = R.string.my_vault,
|
||||
contentDescRes = R.string.my_vault,
|
||||
),
|
||||
)
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(
|
||||
route = VaultGraphRoute,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToVaultScreen shortcut event should navigate to VaultScreen`() {
|
||||
mutableEventFlow.tryEmit(
|
||||
VaultUnlockedNavBarEvent.Shortcut.NavigateToVaultScreen(
|
||||
labelRes = R.string.my_vault,
|
||||
contentDescRes = R.string.my_vault,
|
||||
),
|
||||
)
|
||||
composeTestRule.runOnIdle {
|
||||
mockNavHostController.navigate(
|
||||
route = VaultGraphRoute,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToSettingsScreen shortcut event should navigate to SettingsScreen`() {
|
||||
mutableEventFlow.tryEmit(VaultUnlockedNavBarEvent.Shortcut.NavigateToSettingsScreen)
|
||||
composeTestRule.runOnIdle {
|
||||
verify {
|
||||
mockNavHostController.navigate(
|
||||
route = SettingsGraphRoute,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `send tab click should send SendTabClick action`() {
|
||||
composeTestRule.onNodeWithText(text = "Send").performClick()
|
||||
verify { viewModel.trySendAction(VaultUnlockedNavBarAction.SendTabClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToSendScreen should navigate to SendScreen`() {
|
||||
mutableEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToSendScreen)
|
||||
composeTestRule.runOnIdle {
|
||||
mockNavHostController.navigate(
|
||||
route = SendGraphRoute,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `generator tab click should send GeneratorTabClick action`() {
|
||||
composeTestRule.onNodeWithText(text = "Generator").performClick()
|
||||
verify { viewModel.trySendAction(VaultUnlockedNavBarAction.GeneratorTabClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToGeneratorScreen should navigate to GeneratorScreen`() {
|
||||
mutableEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToGeneratorScreen)
|
||||
composeTestRule.runOnIdle {
|
||||
mockNavHostController.navigate(
|
||||
route = GeneratorGraphRoute,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToGeneratorScreen shortcut event should navigate to GeneratorScreen`() {
|
||||
mutableEventFlow.tryEmit(VaultUnlockedNavBarEvent.Shortcut.NavigateToGeneratorScreen)
|
||||
composeTestRule.runOnIdle {
|
||||
mockNavHostController.navigate(
|
||||
route = GeneratorGraphRoute,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `settings tab click should send SendTabClick action`() {
|
||||
composeTestRule.onNodeWithText(text = "Settings").performClick()
|
||||
verify { viewModel.trySendAction(VaultUnlockedNavBarAction.SettingsTabClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToSettingsScreen should navigate to SettingsScreen`() {
|
||||
mutableEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToSettingsScreen)
|
||||
composeTestRule.runOnIdle {
|
||||
mockNavHostController.navigate(
|
||||
route = SettingsGraphRoute,
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vault nav bar should update according to state`() {
|
||||
composeTestRule.onNodeWithText(text = "My vault").assertExists()
|
||||
composeTestRule.onNodeWithText(text = "Vaults").assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.tryEmit(
|
||||
VaultUnlockedNavBarState(
|
||||
vaultNavBarLabelRes = R.string.vaults,
|
||||
vaultNavBarContentDescriptionRes = R.string.vaults,
|
||||
notificationState = VaultUnlockedNavBarNotificationState(
|
||||
settingsTabNotificationCount = 0,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
composeTestRule.onNodeWithText(text = "My vault").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithText(text = "Vaults").assertExists()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `settings tab notification count should update according to state and show correct count`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
notificationState = VaultUnlockedNavBarNotificationState(
|
||||
settingsTabNotificationCount = 1,
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "1", useUnmergedTree = true)
|
||||
.assertExists()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
notificationState = VaultUnlockedNavBarNotificationState(
|
||||
settingsTabNotificationCount = 0,
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "1", useUnmergedTree = true)
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_STATE = VaultUnlockedNavBarState(
|
||||
vaultNavBarLabelRes = R.string.my_vault,
|
||||
vaultNavBarContentDescriptionRes = R.string.my_vault,
|
||||
notificationState = VaultUnlockedNavBarNotificationState(
|
||||
settingsTabNotificationCount = 0,
|
||||
),
|
||||
)
|
||||
Reference in New Issue
Block a user