[PM-45372] refactor: Migrate the Test Harness module to Navigation3 (#7508)

This commit is contained in:
David Perez
2026-11-09 15:07:44 +00:00
committed by GitHub
parent db7d3c9498
commit 0fc33dfbd0
17 changed files with 366 additions and 264 deletions
-1
View File
@@ -94,7 +94,6 @@ dependencies {
implementation(libs.google.hilt.android)
ksp(libs.google.hilt.compiler)
implementation(libs.androidx.hilt.lifecycle.viewmodel.compose)
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
implementation(libs.androidx.navigation3.runtime)
implementation(libs.androidx.navigation3.ui)
@@ -7,11 +7,12 @@ import androidx.activity.viewModels
import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import androidx.navigation3.runtime.entryProvider
import com.bitwarden.testharness.ui.platform.feature.main.MainGraph
import com.bitwarden.testharness.ui.platform.feature.rootnav.RootNavScreen
import com.bitwarden.testharness.ui.platform.feature.rootnav.RootNavigationRoute
import com.bitwarden.testharness.ui.platform.feature.rootnav.rootNavDestination
import com.bitwarden.ui.platform.navigation.components.BitwardenNavDisplay
import com.bitwarden.ui.platform.navigation.util.rememberNavigationController
import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.bitwarden.ui.platform.util.setupEdgeToEdge
import dagger.hilt.android.AndroidEntryPoint
@@ -40,18 +41,20 @@ class MainActivity : ComponentActivity() {
setupEdgeToEdge(appThemeFlow = mainViewModel.stateFlow.map { it.theme })
setContent {
val navController = rememberNavController()
val navigationController = rememberNavigationController<MainGraph>(
routes = arrayOf(MainGraph.RootNavigationRoute),
)
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
BitwardenTheme(
theme = state.theme,
) {
NavHost(
navController = navController,
startDestination = RootNavigationRoute,
) {
rootNavDestination { shouldShowSplashScreen = false }
}
BitwardenNavDisplay(
navigationController = navigationController,
entryProvider = entryProvider {
rootNavDestination { shouldShowSplashScreen = false }
},
)
}
}
}
@@ -0,0 +1,18 @@
package com.bitwarden.testharness.ui.platform.feature.autofill
import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable
/**
* Represents the hierarchy of the Autofill test UI navigation graph and provides type-safe routes
* to each destination available in the graph.
*/
@Serializable
sealed class AutofillGraph : NavKey {
/**
* The type-safe route for the Autofill test placeholder screen, the root of the Autofill test
* flow.
*/
@Serializable
data object AutofillPlaceholderRoute : AutofillGraph()
}
@@ -2,48 +2,39 @@
package com.bitwarden.testharness.ui.platform.feature.autofill
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.navigation
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.entryProvider
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.ui.platform.base.util.composableWithRootPushTransitions
import kotlinx.serialization.Serializable
import com.bitwarden.testharness.ui.platform.feature.rootnav.RootNavGraph
import com.bitwarden.ui.platform.navigation.NavigationController
import com.bitwarden.ui.platform.navigation.components.BitwardenNavDisplay
import com.bitwarden.ui.platform.navigation.util.rememberNavigationController
import com.bitwarden.ui.platform.navigation.util.rootPushEntry
/**
* Autofill graph route - serves as the parent for all Autofill test flows.
* Add the Autofill test flow to the root navigation entries. The [RootNavGraph.AutofillRoute]
* hosts the [AutofillGraph].
*/
@Serializable
data object AutofillGraphRoute
/**
* Autofill test placeholder screen - the start destination of the graph.
*/
@Serializable
data object AutofillPlaceholderRoute
/**
* Add Autofill nav graph to the root nav graph.
*
* This graph contains the placeholder screen and can be expanded with
* additional autofill test screens as needed.
*/
fun NavGraphBuilder.autofillGraph(
fun EntryProviderScope<RootNavGraph>.autofillGraph(
onNavigateBack: () -> Unit,
) {
navigation<AutofillGraphRoute>(
startDestination = AutofillPlaceholderRoute,
) {
autofillPlaceholderDestination(
onNavigateBack = onNavigateBack,
entry<RootNavGraph.AutofillRoute> {
val navigationController = rememberNavigationController<AutofillGraph>(
routes = arrayOf(AutofillGraph.AutofillPlaceholderRoute),
)
BitwardenNavDisplay(
navigationController = navigationController,
entryProvider = entryProvider {
autofillPlaceholderDestination(onNavigateBack = onNavigateBack)
},
)
}
}
private fun NavGraphBuilder.autofillPlaceholderDestination(
private fun EntryProviderScope<AutofillGraph>.autofillPlaceholderDestination(
onNavigateBack: () -> Unit,
) {
composableWithRootPushTransitions<AutofillPlaceholderRoute> {
rootPushEntry<AutofillGraph.AutofillPlaceholderRoute> {
AutofillPlaceholderScreen(
onNavigateBack = onNavigateBack,
)
@@ -51,8 +42,8 @@ private fun NavGraphBuilder.autofillPlaceholderDestination(
}
/**
* Navigate to the Autofill graph.
* Navigate to the Autofill test flow.
*/
fun NavController.navigateToAutofillGraph(navOptions: NavOptions? = null) {
navigate(route = AutofillGraphRoute, navOptions = navOptions)
fun NavigationController<RootNavGraph>.navigateToAutofill() {
this.navigate(route = RootNavGraph.AutofillRoute)
}
@@ -2,26 +2,19 @@
package com.bitwarden.testharness.ui.platform.feature.createpasskey
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation3.runtime.EntryProviderScope
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
import kotlinx.serialization.Serializable
import com.bitwarden.testharness.ui.platform.feature.credentialmanager.CredentialManagerGraph
import com.bitwarden.ui.platform.navigation.NavigationController
import com.bitwarden.ui.platform.navigation.util.pushEntry
/**
* Create passkey test screen.
* Add Create Passkey destination to the navigation entries.
*/
@Serializable
data object CreatePasskeyRoute
/**
* Add Create Passkey destination to the nav graph.
*/
fun NavGraphBuilder.createPasskeyDestination(
fun EntryProviderScope<CredentialManagerGraph>.createPasskeyDestination(
onNavigateBack: () -> Unit,
) {
composableWithPushTransitions<CreatePasskeyRoute> {
pushEntry<CredentialManagerGraph.CreatePasskeyRoute> {
CreatePasskeyScreen(
onNavigateBack = onNavigateBack,
)
@@ -31,6 +24,6 @@ fun NavGraphBuilder.createPasskeyDestination(
/**
* Navigate to the Create Passkey test screen.
*/
fun NavController.navigateToCreatePasskey(navOptions: NavOptions? = null) {
navigate(route = CreatePasskeyRoute, navOptions = navOptions)
fun NavigationController<CredentialManagerGraph>.navigateToCreatePasskey() {
this.navigate(route = CredentialManagerGraph.CreatePasskeyRoute)
}
@@ -2,26 +2,19 @@
package com.bitwarden.testharness.ui.platform.feature.createpassword
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation3.runtime.EntryProviderScope
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
import kotlinx.serialization.Serializable
import com.bitwarden.testharness.ui.platform.feature.credentialmanager.CredentialManagerGraph
import com.bitwarden.ui.platform.navigation.NavigationController
import com.bitwarden.ui.platform.navigation.util.pushEntry
/**
* Create password test screen.
* Add Create Password destination to the navigation entries.
*/
@Serializable
data object CreatePasswordRoute
/**
* Add Create Password destination to the nav graph.
*/
fun NavGraphBuilder.createPasswordDestination(
fun EntryProviderScope<CredentialManagerGraph>.createPasswordDestination(
onNavigateBack: () -> Unit,
) {
composableWithPushTransitions<CreatePasswordRoute> {
pushEntry<CredentialManagerGraph.CreatePasswordRoute> {
CreatePasswordScreen(
onNavigateBack = onNavigateBack,
)
@@ -31,6 +24,6 @@ fun NavGraphBuilder.createPasswordDestination(
/**
* Navigate to the Create Password test screen.
*/
fun NavController.navigateToCreatePassword(navOptions: NavOptions? = null) {
navigate(route = CreatePasswordRoute, navOptions = navOptions)
fun NavigationController<CredentialManagerGraph>.navigateToCreatePassword() {
this.navigate(route = CredentialManagerGraph.CreatePasswordRoute)
}
@@ -0,0 +1,48 @@
package com.bitwarden.testharness.ui.platform.feature.credentialmanager
import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable
/**
* Represents the hierarchy of the Credential Manager test UI navigation graph and provides
* type-safe routes to each destination available in the graph.
*/
@Serializable
sealed class CredentialManagerGraph : NavKey {
/**
* The type-safe route for the Credential Manager test category list screen, the root of the
* Credential Manager test flow.
*/
@Serializable
data object CredentialManagerListRoute : CredentialManagerGraph()
/**
* The type-safe route for the Get Password test screen.
*/
@Serializable
data object GetPasswordRoute : CredentialManagerGraph()
/**
* The type-safe route for the Create Password test screen.
*/
@Serializable
data object CreatePasswordRoute : CredentialManagerGraph()
/**
* The type-safe route for the Get Passkey test screen.
*/
@Serializable
data object GetPasskeyRoute : CredentialManagerGraph()
/**
* The type-safe route for the Create Passkey test screen.
*/
@Serializable
data object CreatePasskeyRoute : CredentialManagerGraph()
/**
* The type-safe route for the Get Password or Passkey test screen.
*/
@Serializable
data object GetPasswordOrPasskeyRoute : CredentialManagerGraph()
}
@@ -2,10 +2,8 @@
package com.bitwarden.testharness.ui.platform.feature.credentialmanager
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.navigation
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.entryProvider
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.testharness.ui.platform.feature.createpasskey.createPasskeyDestination
import com.bitwarden.testharness.ui.platform.feature.createpasskey.navigateToCreatePasskey
@@ -17,76 +15,79 @@ import com.bitwarden.testharness.ui.platform.feature.getpassword.getPasswordDest
import com.bitwarden.testharness.ui.platform.feature.getpassword.navigateToGetPassword
import com.bitwarden.testharness.ui.platform.feature.getpasswordorpasskey.getPasswordOrPasskeyDestination
import com.bitwarden.testharness.ui.platform.feature.getpasswordorpasskey.navigateToGetPasswordOrPasskey
import com.bitwarden.ui.platform.base.util.composableWithRootPushTransitions
import kotlinx.serialization.Serializable
import com.bitwarden.testharness.ui.platform.feature.rootnav.RootNavGraph
import com.bitwarden.ui.platform.navigation.NavigationController
import com.bitwarden.ui.platform.navigation.components.BitwardenNavDisplay
import com.bitwarden.ui.platform.navigation.util.rememberNavigationController
import com.bitwarden.ui.platform.navigation.util.rootPushEntry
/**
* Credential Manager graph route - serves as the parent for all Credential Manager API test flows.
* Add the Credential Manager test flow to the root navigation entries. The
* [RootNavGraph.CredentialManagerRoute] hosts the [CredentialManagerGraph].
*/
@Serializable
data object CredentialManagerGraphRoute
/**
* Credential Manager test category list screen - the start destination of the graph.
*/
@Serializable
data object CredentialManagerListRoute
/**
* Add Credential Manager nav graph to the root nav graph.
*
* This graph contains the list screen and all nested credential manager API test screens.
*/
fun NavGraphBuilder.credentialManagerGraph(
fun EntryProviderScope<RootNavGraph>.credentialManagerGraph(
onNavigateBack: () -> Unit,
navController: NavController,
) {
navigation<CredentialManagerGraphRoute>(
startDestination = CredentialManagerListRoute,
) {
credentialManagerListDestination(
onNavigateBack = onNavigateBack,
onNavigateToGetPassword = {
navController.navigateToGetPassword()
},
onNavigateToCreatePassword = {
navController.navigateToCreatePassword()
},
onNavigateToGetPasskey = {
navController.navigateToGetPasskey()
},
onNavigateToCreatePasskey = {
navController.navigateToCreatePasskey()
},
onNavigateToGetPasswordOrPasskey = {
navController.navigateToGetPasswordOrPasskey()
},
entry<RootNavGraph.CredentialManagerRoute> {
val navigationController = rememberNavigationController<CredentialManagerGraph>(
routes = arrayOf(CredentialManagerGraph.CredentialManagerListRoute),
)
getPasswordDestination(
onNavigateBack = { navController.popBackStack() },
)
createPasswordDestination(
onNavigateBack = { navController.popBackStack() },
)
getPasskeyDestination(
onNavigateBack = { navController.popBackStack() },
)
createPasskeyDestination(
onNavigateBack = { navController.popBackStack() },
)
getPasswordOrPasskeyDestination(
onNavigateBack = { navController.popBackStack() },
BitwardenNavDisplay(
navigationController = navigationController,
entryProvider = entryProvider {
credentialManagerDestinations(
navigationController = navigationController,
onNavigateBack = onNavigateBack,
)
},
)
}
}
/**
* Add all the Credential Manager test destinations to the Credential Manager navigation entries.
*
* The [CredentialManagerGraph.CredentialManagerListRoute] is the root of the Credential Manager
* test flow, which contains the list screen and all nested credential manager API test screens.
*/
private fun EntryProviderScope<CredentialManagerGraph>.credentialManagerDestinations(
navigationController: NavigationController<CredentialManagerGraph>,
onNavigateBack: () -> Unit,
) {
credentialManagerListDestination(
onNavigateBack = onNavigateBack,
onNavigateToGetPassword = { navigationController.navigateToGetPassword() },
onNavigateToCreatePassword = { navigationController.navigateToCreatePassword() },
onNavigateToGetPasskey = { navigationController.navigateToGetPasskey() },
onNavigateToCreatePasskey = { navigationController.navigateToCreatePasskey() },
onNavigateToGetPasswordOrPasskey = {
navigationController.navigateToGetPasswordOrPasskey()
},
)
getPasswordDestination(
onNavigateBack = { navigationController.navigateBack() },
)
createPasswordDestination(
onNavigateBack = { navigationController.navigateBack() },
)
getPasskeyDestination(
onNavigateBack = { navigationController.navigateBack() },
)
createPasskeyDestination(
onNavigateBack = { navigationController.navigateBack() },
)
getPasswordOrPasskeyDestination(
onNavigateBack = { navigationController.navigateBack() },
)
}
@Suppress("LongParameterList")
private fun NavGraphBuilder.credentialManagerListDestination(
private fun EntryProviderScope<CredentialManagerGraph>.credentialManagerListDestination(
onNavigateBack: () -> Unit,
onNavigateToGetPassword: () -> Unit,
onNavigateToCreatePassword: () -> Unit,
@@ -94,7 +95,7 @@ private fun NavGraphBuilder.credentialManagerListDestination(
onNavigateToCreatePasskey: () -> Unit,
onNavigateToGetPasswordOrPasskey: () -> Unit,
) {
composableWithRootPushTransitions<CredentialManagerListRoute> {
rootPushEntry<CredentialManagerGraph.CredentialManagerListRoute> {
CredentialManagerListScreen(
onNavigateBack = onNavigateBack,
onNavigateToGetPassword = { onNavigateToGetPassword() },
@@ -107,8 +108,8 @@ private fun NavGraphBuilder.credentialManagerListDestination(
}
/**
* Navigate to the Credential Manager flow.
* Navigate to the Credential Manager test flow.
*/
fun NavController.navigateToCredentialManagerGraph(navOptions: NavOptions? = null) {
navigate(route = CredentialManagerGraphRoute, navOptions = navOptions)
fun NavigationController<RootNavGraph>.navigateToCredentialManager() {
this.navigate(route = RootNavGraph.CredentialManagerRoute)
}
@@ -2,26 +2,19 @@
package com.bitwarden.testharness.ui.platform.feature.getpasskey
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation3.runtime.EntryProviderScope
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
import kotlinx.serialization.Serializable
import com.bitwarden.testharness.ui.platform.feature.credentialmanager.CredentialManagerGraph
import com.bitwarden.ui.platform.navigation.NavigationController
import com.bitwarden.ui.platform.navigation.util.pushEntry
/**
* Get passkey test screen.
* Add Get Passkey destination to the navigation entries.
*/
@Serializable
data object GetPasskeyRoute
/**
* Add Get Passkey destination to the nav graph.
*/
fun NavGraphBuilder.getPasskeyDestination(
fun EntryProviderScope<CredentialManagerGraph>.getPasskeyDestination(
onNavigateBack: () -> Unit,
) {
composableWithPushTransitions<GetPasskeyRoute> {
pushEntry<CredentialManagerGraph.GetPasskeyRoute> {
GetPasskeyScreen(
onNavigateBack = onNavigateBack,
)
@@ -31,6 +24,6 @@ fun NavGraphBuilder.getPasskeyDestination(
/**
* Navigate to the Get Passkey test screen.
*/
fun NavController.navigateToGetPasskey(navOptions: NavOptions? = null) {
navigate(route = GetPasskeyRoute, navOptions = navOptions)
fun NavigationController<CredentialManagerGraph>.navigateToGetPasskey() {
this.navigate(route = CredentialManagerGraph.GetPasskeyRoute)
}
@@ -2,26 +2,19 @@
package com.bitwarden.testharness.ui.platform.feature.getpassword
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation3.runtime.EntryProviderScope
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
import kotlinx.serialization.Serializable
import com.bitwarden.testharness.ui.platform.feature.credentialmanager.CredentialManagerGraph
import com.bitwarden.ui.platform.navigation.NavigationController
import com.bitwarden.ui.platform.navigation.util.pushEntry
/**
* Get password test screen.
* Add Get Password destination to the navigation entries.
*/
@Serializable
data object GetPasswordRoute
/**
* Add Get Password destination to the nav graph.
*/
fun NavGraphBuilder.getPasswordDestination(
fun EntryProviderScope<CredentialManagerGraph>.getPasswordDestination(
onNavigateBack: () -> Unit,
) {
composableWithPushTransitions<GetPasswordRoute> {
pushEntry<CredentialManagerGraph.GetPasswordRoute> {
GetPasswordScreen(
onNavigateBack = onNavigateBack,
)
@@ -31,6 +24,6 @@ fun NavGraphBuilder.getPasswordDestination(
/**
* Navigate to the Get Password test screen.
*/
fun NavController.navigateToGetPassword(navOptions: NavOptions? = null) {
navigate(route = GetPasswordRoute, navOptions = navOptions)
fun NavigationController<CredentialManagerGraph>.navigateToGetPassword() {
this.navigate(route = CredentialManagerGraph.GetPasswordRoute)
}
@@ -2,26 +2,19 @@
package com.bitwarden.testharness.ui.platform.feature.getpasswordorpasskey
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation3.runtime.EntryProviderScope
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
import kotlinx.serialization.Serializable
import com.bitwarden.testharness.ui.platform.feature.credentialmanager.CredentialManagerGraph
import com.bitwarden.ui.platform.navigation.NavigationController
import com.bitwarden.ui.platform.navigation.util.pushEntry
/**
* Get Password or Passkey test screen.
* Add Get Password or Passkey destination to the navigation entries.
*/
@Serializable
data object GetPasswordOrPasskeyRoute
/**
* Add Get Password or Passkey destination to the nav graph.
*/
fun NavGraphBuilder.getPasswordOrPasskeyDestination(
fun EntryProviderScope<CredentialManagerGraph>.getPasswordOrPasskeyDestination(
onNavigateBack: () -> Unit,
) {
composableWithPushTransitions<GetPasswordOrPasskeyRoute> {
pushEntry<CredentialManagerGraph.GetPasswordOrPasskeyRoute> {
GetPasswordOrPasskeyScreen(
onNavigateBack = onNavigateBack,
)
@@ -31,6 +24,6 @@ fun NavGraphBuilder.getPasswordOrPasskeyDestination(
/**
* Navigate to the Get Password or Passkey test screen.
*/
fun NavController.navigateToGetPasswordOrPasskey(navOptions: NavOptions? = null) {
navigate(route = GetPasswordOrPasskeyRoute, navOptions = navOptions)
fun NavigationController<CredentialManagerGraph>.navigateToGetPasswordOrPasskey() {
this.navigate(route = CredentialManagerGraph.GetPasswordOrPasskeyRoute)
}
@@ -2,25 +2,19 @@
package com.bitwarden.testharness.ui.platform.feature.landing
import androidx.navigation.NavGraphBuilder
import androidx.navigation3.runtime.EntryProviderScope
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.ui.platform.base.util.composableWithRootPushTransitions
import kotlinx.serialization.Serializable
import com.bitwarden.testharness.ui.platform.feature.rootnav.RootNavGraph
import com.bitwarden.ui.platform.navigation.util.rootPushEntry
/**
* Landing screen - main entry point showing test category options.
* Add Landing destination to the navigation entries.
*/
@Serializable
data object LandingRoute
/**
* Add Landing destination to the nav graph.
*/
fun NavGraphBuilder.landingDestination(
fun EntryProviderScope<RootNavGraph>.landingDestination(
onNavigateToAutofill: () -> Unit,
onNavigateToCredentialManager: () -> Unit,
) {
composableWithRootPushTransitions<LandingRoute> {
rootPushEntry<RootNavGraph.LandingRoute> {
LandingScreen(
onNavigateToAutofill = onNavigateToAutofill,
onNavigateToCredentialManager = onNavigateToCredentialManager,
@@ -0,0 +1,17 @@
package com.bitwarden.testharness.ui.platform.feature.main
import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable
/**
* Represents the hierarchy of the Main UI navigation graph and provides type-safe routes to each
* destination available in the graph.
*/
@Serializable
sealed class MainGraph : NavKey {
/**
* The type-safe route for the root navigation screen.
*/
@Serializable
data object RootNavigationRoute : MainGraph()
}
@@ -0,0 +1,33 @@
package com.bitwarden.testharness.ui.platform.feature.rootnav
import androidx.navigation3.runtime.NavKey
import com.bitwarden.testharness.ui.platform.feature.autofill.AutofillGraph
import com.bitwarden.testharness.ui.platform.feature.credentialmanager.CredentialManagerGraph
import kotlinx.serialization.Serializable
/**
* Represents the hierarchy of the Root UI navigation graph and provides type-safe routes to each
* top-level section of the test harness.
*/
@Serializable
sealed class RootNavGraph : NavKey {
/**
* The type-safe route for the landing screen, the main entry point showing the test category
* options.
*/
@Serializable
data object LandingRoute : RootNavGraph()
/**
* The type-safe route for the Autofill test flow, which hosts the [AutofillGraph].
*/
@Serializable
data object AutofillRoute : RootNavGraph()
/**
* The type-safe route for the Credential Manager test flow, which hosts the
* [CredentialManagerGraph].
*/
@Serializable
data object CredentialManagerRoute : RootNavGraph()
}
@@ -2,24 +2,17 @@
package com.bitwarden.testharness.ui.platform.feature.rootnav
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import androidx.navigation3.runtime.EntryProviderScope
import com.bitwarden.annotation.OmitFromCoverage
import kotlinx.serialization.Serializable
import com.bitwarden.testharness.ui.platform.feature.main.MainGraph
/**
* The type-safe route for the root navigation screen.
* Add the root navigation screen to the navigation entries.
*/
@Serializable
data object RootNavigationRoute
/**
* Add the root navigation screen to the nav graph.
*/
fun NavGraphBuilder.rootNavDestination(
fun EntryProviderScope<MainGraph>.rootNavDestination(
onSplashScreenRemoved: () -> Unit,
) {
composable<RootNavigationRoute> {
entry<MainGraph.RootNavigationRoute> {
RootNavScreen(onSplashScreenRemoved = onSplashScreenRemoved)
}
}
@@ -5,24 +5,32 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay
import com.bitwarden.testharness.ui.platform.feature.autofill.autofillGraph
import com.bitwarden.testharness.ui.platform.feature.autofill.navigateToAutofillGraph
import com.bitwarden.testharness.ui.platform.feature.autofill.navigateToAutofill
import com.bitwarden.testharness.ui.platform.feature.credentialmanager.credentialManagerGraph
import com.bitwarden.testharness.ui.platform.feature.credentialmanager.navigateToCredentialManagerGraph
import com.bitwarden.testharness.ui.platform.feature.landing.LandingRoute
import com.bitwarden.testharness.ui.platform.feature.credentialmanager.navigateToCredentialManager
import com.bitwarden.testharness.ui.platform.feature.landing.landingDestination
import com.bitwarden.ui.platform.navigation.NavigationController
import com.bitwarden.ui.platform.navigation.components.BitwardenNavDisplay
import com.bitwarden.ui.platform.navigation.util.rememberNavigationController
/**
* Controls the root level [NavHost] for the test harness app.
* Controls the root level [NavDisplay] for the test harness app. Each route in the [RootNavGraph]
* represents a top-level section of the test harness, which may host its own nested navigation
* graph.
*/
@Suppress("LongMethod")
@Composable
fun RootNavScreen(
viewModel: RootNavViewModel = hiltViewModel(),
navController: NavHostController = rememberNavController(),
navigationController: NavigationController<RootNavGraph> = rememberNavigationController(
routes = arrayOf(RootNavGraph.LandingRoute),
),
entryProvider: (RootNavGraph) -> NavEntry<RootNavGraph> = entryProvider(
navigationController = navigationController,
),
onSplashScreenRemoved: () -> Unit,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
@@ -33,26 +41,23 @@ fun RootNavScreen(
}
}
NavHost(
navController = navController,
startDestination = LandingRoute,
) {
landingDestination(
onNavigateToAutofill = {
navController.navigateToAutofillGraph()
},
onNavigateToCredentialManager = {
navController.navigateToCredentialManagerGraph()
},
)
BitwardenNavDisplay(
navigationController = navigationController,
entryProvider = entryProvider,
)
}
autofillGraph(
onNavigateBack = { navController.popBackStack() },
)
credentialManagerGraph(
onNavigateBack = { navController.popBackStack() },
navController = navController,
)
}
private fun entryProvider(
navigationController: NavigationController<RootNavGraph>,
): (RootNavGraph) -> NavEntry<RootNavGraph> = entryProvider {
landingDestination(
onNavigateToAutofill = { navigationController.navigateToAutofill() },
onNavigateToCredentialManager = { navigationController.navigateToCredentialManager() },
)
autofillGraph(
onNavigateBack = { navigationController.navigateBack() },
)
credentialManagerGraph(
onNavigateBack = { navigationController.navigateBack() },
)
}
@@ -1,22 +1,28 @@
package com.bitwarden.testharness.ui.platform.feature.rootnav
import androidx.compose.material3.Text
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithText
import androidx.navigation3.runtime.NavEntry
import com.bitwarden.ui.platform.base.BaseComposeTest
import com.bitwarden.ui.platform.base.createMockNavHostController
import com.bitwarden.ui.platform.navigation.util.navigationControllerOf
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
/**
* Tests for the [RootNavScreen] composable in the testharness module.
*
* Verifies that root-level navigation correctly routes to different test flows
* based on the [RootNavState].
*/
class RootNavScreenTest : BaseComposeTest() {
private val mockNavHostController = createMockNavHostController()
private val navigationController = navigationControllerOf<RootNavGraph>(
routes = arrayOf(RootNavGraph.LandingRoute),
)
private val rootNavStateFlow = MutableStateFlow<RootNavState>(RootNavState.Splash)
private val viewModel = mockk<RootNavViewModel> {
every { eventFlow } returns emptyFlow()
@@ -30,7 +36,8 @@ class RootNavScreenTest : BaseComposeTest() {
setTestContent {
RootNavScreen(
viewModel = viewModel,
navController = mockNavHostController,
navigationController = navigationController,
entryProvider = { key -> NavEntry(key = key) { Text(text = key.toString()) } },
onSplashScreenRemoved = { isSplashScreenRemoved = true },
)
}
@@ -39,40 +46,68 @@ class RootNavScreenTest : BaseComposeTest() {
@Test
fun `initial state does not invoke splash screen removal`() {
composeTestRule.runOnIdle {
assert(!isSplashScreenRemoved)
assertFalse(isSplashScreenRemoved)
}
}
@Test
fun `splash screen callback is called when transitioning from Splash state`() {
composeTestRule.runOnIdle {
assert(!isSplashScreenRemoved)
assertFalse(isSplashScreenRemoved)
}
// Transition to Landing state
rootNavStateFlow.value = RootNavState.Landing
composeTestRule.runOnIdle {
assert(isSplashScreenRemoved)
assertTrue(isSplashScreenRemoved)
}
}
@Test
fun `nav host displays landing destination as start destination`() {
fun `nav display displays the landing destination as the initial destination`() {
composeTestRule
.onNodeWithText(text = RootNavGraph.LandingRoute.toString())
.assertIsDisplayed()
composeTestRule.runOnIdle {
// The NavHost is configured with LandingRoute as startDestination
assert(true)
assertEquals(
listOf(RootNavGraph.LandingRoute),
navigationController.backStack.toList(),
)
}
}
@Test
fun `nav host contains autofill and credential manager graphs`() {
fun `nav display displays the destination on top of the back stack`() {
navigationController.navigate(RootNavGraph.CredentialManagerRoute)
composeTestRule
.onNodeWithText(text = RootNavGraph.CredentialManagerRoute.toString())
.assertIsDisplayed()
composeTestRule.runOnIdle {
// The NavHost contains:
// - landingDestination()
// - autofillGraph()
// - credentialManagerGraph()
// Navigation to these graphs is triggered by user interaction in LandingScreen
assert(true)
assertEquals(
listOf(RootNavGraph.LandingRoute, RootNavGraph.CredentialManagerRoute),
navigationController.backStack.toList(),
)
}
}
@Test
fun `system back should remove the top destination from the back stack`() {
navigationController.navigate(RootNavGraph.AutofillRoute)
// Wait for the new destination to be displayed so the back handler has been enabled.
composeTestRule
.onNodeWithText(text = RootNavGraph.AutofillRoute.toString())
.assertIsDisplayed()
backDispatcher?.onBackPressed()
composeTestRule
.onNodeWithText(text = RootNavGraph.LandingRoute.toString())
.assertIsDisplayed()
composeTestRule.runOnIdle {
assertEquals(
listOf(RootNavGraph.LandingRoute),
navigationController.backStack.toList(),
)
}
}
}