PM-21252: Create mock NavHostController for navigation testing (#5159)

This commit is contained in:
David Perez
2025-05-08 19:58:03 +00:00
committed by GitHub
parent 564304616d
commit ed148c2089
7 changed files with 698 additions and 199 deletions
@@ -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
@@ -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
}
}
}
@@ -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,
)
@@ -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,
),
)