Update the Navigation component library (#5130)

This commit is contained in:
David Perez
2025-05-06 09:47:34 -05:00
committed by GitHub
parent 31de7fc331
commit e1f432ea5d
5 changed files with 9 additions and 510 deletions

View File

@@ -1,3 +1,6 @@
// TODO: Add tests for this (PM-21252)
@file:OmitFromCoverage
package com.x8bit.bitwarden.ui.platform.feature.rootnav
import androidx.activity.compose.LocalActivity
@@ -13,6 +16,7 @@ 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.x8bit.bitwarden.ui.auth.feature.accountsetup.SETUP_AUTO_FILL_AS_ROOT_ROUTE
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SETUP_COMPLETE_ROUTE
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SETUP_UNLOCK_AS_ROOT_ROUTE

View File

@@ -1,3 +1,6 @@
// TODO: Add tests for this (PM-21252)
@file:OmitFromCoverage
package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar
import androidx.compose.foundation.layout.WindowInsets
@@ -19,6 +22,7 @@ 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.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.components.model.NavigationItem
import com.x8bit.bitwarden.ui.platform.components.model.ScaffoldNavigationData

View File

@@ -1,259 +0,0 @@
package com.x8bit.bitwarden.ui.platform.feature.rootnav
import androidx.navigation.navOptions
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
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.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 fakeNavHostController = FakeNavHostController()
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(fakeNavHostController.graphId) {
inclusive = false
saveState = false
}
launchSingleTop = true
restoreState = false
}
private var isSplashScreenRemoved: Boolean = false
@Before
fun setup() {
setContent {
RootNavScreen(
viewModel = viewModel,
navController = fakeNavHostController,
onSplashScreenRemoved = { isSplashScreenRemoved = true },
)
}
}
@Test
fun `initial route should be splash`() {
composeTestRule.runOnIdle {
fakeNavHostController.assertCurrentRoute("splash")
}
}
@Test
fun `when root nav destination changes, navigation should follow`() = runTest {
composeTestRule.runOnIdle {
fakeNavHostController.assertCurrentRoute("splash")
}
assertFalse(isSplashScreenRemoved)
// Make sure navigating to Auth works as expected:
rootNavStateFlow.value = RootNavState.Auth
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "auth_graph",
navOptions = expectedNavOptions,
)
}
assertTrue(isSplashScreenRemoved)
// Make sure navigating to Auth with the welcome route works as expected:
rootNavStateFlow.value = RootNavState.AuthWithWelcome
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "welcome",
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 {
fakeNavHostController.assertLastNavigation(
route = "complete_registration/example@email.com/verificationToken/true",
)
}
// Make sure navigating to expired registration link route works as expected:
rootNavStateFlow.value = RootNavState.ExpiredRegistrationLink
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "expired_registration_link",
)
}
// Make sure navigating to vault locked works as expected:
rootNavStateFlow.value = RootNavState.VaultLocked
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "vault_unlock/STANDARD",
navOptions = expectedNavOptions,
)
}
// Make sure navigating to reset password works as expected:
rootNavStateFlow.value = RootNavState.ResetPassword
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "reset_password",
navOptions = expectedNavOptions,
)
}
// Make sure navigating to set password works as expected:
rootNavStateFlow.value = RootNavState.SetPassword
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "set_password",
navOptions = expectedNavOptions,
)
}
// Make sure navigating to set password works as expected:
rootNavStateFlow.value = RootNavState.TrustedDevice
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "trusted_device_graph",
navOptions = expectedNavOptions,
)
}
// Make sure navigating to vault unlocked works as expected:
rootNavStateFlow.value = RootNavState.VaultUnlocked(activeUserId = "userId")
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "vault_unlocked_graph",
navOptions = expectedNavOptions,
)
}
// Make sure navigating to vault unlocked for new totp works as expected:
rootNavStateFlow.value = RootNavState.VaultUnlockedForNewTotp(activeUserId = "userId")
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "vault_item_listing_as_root/login",
navOptions = expectedNavOptions,
)
}
// Make sure navigating to vault unlocked for new sends works as expected:
rootNavStateFlow.value = RootNavState.VaultUnlockedForNewSend
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "add_send_item/add",
navOptions = expectedNavOptions,
)
}
// Make sure navigating to vault unlocked for autofill save works as expected:
rootNavStateFlow.value = RootNavState.VaultUnlockedForAutofillSave(
autofillSaveItem = mockk(),
)
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "vault_add_edit_item/add",
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 {
fakeNavHostController.assertLastNavigation(
route = "vault_item_listing_as_root/login",
navOptions = expectedNavOptions,
)
}
// Make sure navigating to vault unlocked for Fido2Save works as expected:
rootNavStateFlow.value = RootNavState.VaultUnlockedForFido2Save(
activeUserId = "activeUserId",
fido2CreateCredentialRequest = mockk(),
)
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "vault_item_listing_as_root/login",
navOptions = expectedNavOptions,
)
}
// Make sure navigating to vault unlocked for Fido2Assertion works as expected:
rootNavStateFlow.value = RootNavState.VaultUnlockedForFido2Assertion(
activeUserId = "activeUserId",
fido2CredentialAssertionRequest = mockk(),
)
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "vault_item_listing_as_root/login",
navOptions = expectedNavOptions,
)
}
// Make sure navigating to vault unlocked for Fido2GetCredentials works as expected:
rootNavStateFlow.value = RootNavState.VaultUnlockedForFido2GetCredentials(
activeUserId = "activeUserId",
fido2GetCredentialsRequest = mockk(),
)
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "vault_item_listing_as_root/login",
navOptions = expectedNavOptions,
)
}
// Make sure navigating to account lock setup works as expected:
rootNavStateFlow.value = RootNavState.OnboardingAccountLockSetup
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "setup_unlock_as_root/true",
navOptions = expectedNavOptions,
)
}
// Make sure navigating to account autofill setup works as expected:
rootNavStateFlow.value = RootNavState.OnboardingAutoFillSetup
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "setup_auto_fill_as_root/true",
navOptions = expectedNavOptions,
)
}
// Make sure navigating to account setup complete works as expected:
rootNavStateFlow.value = RootNavState.OnboardingStepsComplete
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "setup_complete",
navOptions = expectedNavOptions,
)
}
}
}
private val FIXED_CLOCK: Clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)

View File

@@ -1,250 +0,0 @@
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.x8bit.bitwarden.R
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 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 fakeNavHostController = FakeNavHostController()
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(fakeNavHostController.graphId) {
inclusive = false
saveState = true
}
launchSingleTop = true
restoreState = true
}
@Before
fun setup() {
composeTestRule.apply {
setContent {
VaultUnlockedNavBarScreen(
viewModel = viewModel,
navController = fakeNavHostController,
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("My vault").performClick()
verify { viewModel.trySendAction(VaultUnlockedNavBarAction.VaultTabClick) }
}
@Test
fun `NavigateToVaultScreen should navigate to VaultScreen`() {
mutableEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToSendScreen)
composeTestRule.runOnIdle { fakeNavHostController.assertCurrentRoute("send_graph") }
mutableEventFlow.tryEmit(
VaultUnlockedNavBarEvent.NavigateToVaultScreen(
labelRes = R.string.my_vault,
contentDescRes = R.string.my_vault,
),
)
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "vault_graph",
navOptions = expectedNavOptions,
)
}
}
@Test
fun `NavigateToVaultScreen shortcut event should navigate to VaultScreen`() {
mutableEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToSendScreen)
composeTestRule.runOnIdle { fakeNavHostController.assertCurrentRoute("send_graph") }
mutableEventFlow.tryEmit(
VaultUnlockedNavBarEvent.Shortcut.NavigateToVaultScreen(
labelRes = R.string.my_vault,
contentDescRes = R.string.my_vault,
),
)
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "vault_graph",
navOptions = expectedNavOptions,
)
}
}
@Test
fun `NavigateToSettingsScreen shortcut event should navigate to SettingsScreen`() {
mutableEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToSendScreen)
composeTestRule.runOnIdle { fakeNavHostController.assertCurrentRoute("send_graph") }
mutableEventFlow.tryEmit(
VaultUnlockedNavBarEvent.Shortcut.NavigateToSettingsScreen,
)
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "settings_graph",
navOptions = expectedNavOptions,
)
}
}
@Test
fun `send tab click should send SendTabClick action`() {
composeTestRule.onNodeWithText("Send").performClick()
verify { viewModel.trySendAction(VaultUnlockedNavBarAction.SendTabClick) }
}
@Test
fun `NavigateToSendScreen should navigate to SendScreen`() {
composeTestRule.apply {
runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") }
mutableEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToSendScreen)
runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "send_graph",
navOptions = expectedNavOptions,
)
}
}
}
@Test
fun `generator tab click should send GeneratorTabClick action`() {
composeTestRule.onNodeWithText("Generator").performClick()
verify { viewModel.trySendAction(VaultUnlockedNavBarAction.GeneratorTabClick) }
}
@Test
fun `NavigateToGeneratorScreen should navigate to GeneratorScreen`() {
composeTestRule.apply {
runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") }
mutableEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToGeneratorScreen)
runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "generator_graph",
navOptions = expectedNavOptions,
)
}
}
}
@Test
fun `NavigateToGeneratorScreen shortcut event should navigate to GeneratorScreen`() {
composeTestRule.apply {
runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") }
mutableEventFlow.tryEmit(VaultUnlockedNavBarEvent.Shortcut.NavigateToGeneratorScreen)
runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "generator_graph",
navOptions = expectedNavOptions,
)
}
}
}
@Test
fun `settings tab click should send SendTabClick action`() {
composeTestRule.onNodeWithText("Settings").performClick()
verify { viewModel.trySendAction(VaultUnlockedNavBarAction.SettingsTabClick) }
}
@Test
fun `NavigateToSettingsScreen should navigate to SettingsScreen`() {
composeTestRule.apply {
runOnIdle { fakeNavHostController.assertCurrentRoute("vault_graph") }
mutableEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToSettingsScreen)
runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "settings_graph",
navOptions = expectedNavOptions,
)
}
}
}
@Test
fun `vault nav bar should update according to state`() {
composeTestRule.onNodeWithText("My vault").assertExists()
composeTestRule.onNodeWithText("Vaults").assertDoesNotExist()
mutableStateFlow.tryEmit(
VaultUnlockedNavBarState(
vaultNavBarLabelRes = R.string.vaults,
vaultNavBarContentDescriptionRes = R.string.vaults,
notificationState = VaultUnlockedNavBarNotificationState(
settingsTabNotificationCount = 0,
),
),
)
composeTestRule.onNodeWithText("My vault").assertDoesNotExist()
composeTestRule.onNodeWithText("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("1", useUnmergedTree = true)
.assertExists()
mutableStateFlow.update {
it.copy(
notificationState = VaultUnlockedNavBarNotificationState(
settingsTabNotificationCount = 0,
),
)
}
composeTestRule
.onNodeWithText("1", useUnmergedTree = true)
.assertDoesNotExist()
}
}
private val DEFAULT_STATE = VaultUnlockedNavBarState(
vaultNavBarLabelRes = R.string.my_vault,
vaultNavBarContentDescriptionRes = R.string.my_vault,
notificationState = VaultUnlockedNavBarNotificationState(
settingsTabNotificationCount = 0,
),
)

View File

@@ -18,7 +18,7 @@ androidxCore = "1.16.0"
androidxCredentials = "1.5.0"
androidxHiltNavigationCompose = "1.2.0"
androidxLifecycle = "2.8.7"
androidxNavigation = "2.8.0"
androidxNavigation = "2.8.9"
androidxRoom = "2.7.1"
androidXSecurityCrypto = "1.1.0-alpha06"
androidxSplash = "1.1.0-rc01"