From 0fc33dfbd0f582109fae9e631597cd9ca55a2bc5 Mon Sep 17 00:00:00 2001 From: David Perez Date: Mon, 9 Nov 2026 15:07:44 +0000 Subject: [PATCH] [PM-45372] refactor: Migrate the Test Harness module to Navigation3 (#7508) --- testharness/build.gradle.kts | 1 - .../com/bitwarden/testharness/MainActivity.kt | 23 +-- .../feature/autofill/AutofillGraph.kt | 18 +++ .../autofill/AutofillPlaceholderNavigation.kt | 57 ++++---- .../createpasskey/CreatePasskeyNavigation.kt | 25 ++-- .../CreatePasswordNavigation.kt | 25 ++-- .../CredentialManagerGraph.kt | 48 +++++++ .../CredentialManagerListNavigation.kt | 135 +++++++++--------- .../getpasskey/GetPasskeyNavigation.kt | 25 ++-- .../getpassword/GetPasswordNavigation.kt | 25 ++-- .../GetPasswordOrPasskeyNavigation.kt | 25 ++-- .../feature/landing/LandingNavigation.kt | 18 +-- .../ui/platform/feature/main/MainGraph.kt | 17 +++ .../platform/feature/rootnav/RootNavGraph.kt | 33 +++++ .../feature/rootnav/RootNavNavigation.kt | 17 +-- .../platform/feature/rootnav/RootNavScreen.kt | 65 +++++---- .../feature/rootnav/RootNavScreenTest.kt | 73 +++++++--- 17 files changed, 366 insertions(+), 264 deletions(-) create mode 100644 testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/autofill/AutofillGraph.kt create mode 100644 testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/credentialmanager/CredentialManagerGraph.kt create mode 100644 testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/main/MainGraph.kt create mode 100644 testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/rootnav/RootNavGraph.kt diff --git a/testharness/build.gradle.kts b/testharness/build.gradle.kts index a0899e1b8c..b5671dd47b 100644 --- a/testharness/build.gradle.kts +++ b/testharness/build.gradle.kts @@ -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) diff --git a/testharness/src/main/kotlin/com/bitwarden/testharness/MainActivity.kt b/testharness/src/main/kotlin/com/bitwarden/testharness/MainActivity.kt index 25318e3240..28078c6049 100644 --- a/testharness/src/main/kotlin/com/bitwarden/testharness/MainActivity.kt +++ b/testharness/src/main/kotlin/com/bitwarden/testharness/MainActivity.kt @@ -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( + 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 } + }, + ) } } } diff --git a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/autofill/AutofillGraph.kt b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/autofill/AutofillGraph.kt new file mode 100644 index 0000000000..dd06294aff --- /dev/null +++ b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/autofill/AutofillGraph.kt @@ -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() +} diff --git a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/autofill/AutofillPlaceholderNavigation.kt b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/autofill/AutofillPlaceholderNavigation.kt index 248c17ec43..28bf2779b0 100644 --- a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/autofill/AutofillPlaceholderNavigation.kt +++ b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/autofill/AutofillPlaceholderNavigation.kt @@ -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.autofillGraph( onNavigateBack: () -> Unit, ) { - navigation( - startDestination = AutofillPlaceholderRoute, - ) { - autofillPlaceholderDestination( - onNavigateBack = onNavigateBack, + entry { + val navigationController = rememberNavigationController( + routes = arrayOf(AutofillGraph.AutofillPlaceholderRoute), + ) + BitwardenNavDisplay( + navigationController = navigationController, + entryProvider = entryProvider { + autofillPlaceholderDestination(onNavigateBack = onNavigateBack) + }, ) } } -private fun NavGraphBuilder.autofillPlaceholderDestination( +private fun EntryProviderScope.autofillPlaceholderDestination( onNavigateBack: () -> Unit, ) { - composableWithRootPushTransitions { + rootPushEntry { 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.navigateToAutofill() { + this.navigate(route = RootNavGraph.AutofillRoute) } diff --git a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/createpasskey/CreatePasskeyNavigation.kt b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/createpasskey/CreatePasskeyNavigation.kt index f17ca6770b..098188f1f2 100644 --- a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/createpasskey/CreatePasskeyNavigation.kt +++ b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/createpasskey/CreatePasskeyNavigation.kt @@ -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.createPasskeyDestination( onNavigateBack: () -> Unit, ) { - composableWithPushTransitions { + pushEntry { 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.navigateToCreatePasskey() { + this.navigate(route = CredentialManagerGraph.CreatePasskeyRoute) } diff --git a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/createpassword/CreatePasswordNavigation.kt b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/createpassword/CreatePasswordNavigation.kt index 5574adfcfd..4bcf141b68 100644 --- a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/createpassword/CreatePasswordNavigation.kt +++ b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/createpassword/CreatePasswordNavigation.kt @@ -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.createPasswordDestination( onNavigateBack: () -> Unit, ) { - composableWithPushTransitions { + pushEntry { 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.navigateToCreatePassword() { + this.navigate(route = CredentialManagerGraph.CreatePasswordRoute) } diff --git a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/credentialmanager/CredentialManagerGraph.kt b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/credentialmanager/CredentialManagerGraph.kt new file mode 100644 index 0000000000..84c946f236 --- /dev/null +++ b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/credentialmanager/CredentialManagerGraph.kt @@ -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() +} diff --git a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/credentialmanager/CredentialManagerListNavigation.kt b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/credentialmanager/CredentialManagerListNavigation.kt index 2a96a723f0..6f6aaaf571 100644 --- a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/credentialmanager/CredentialManagerListNavigation.kt +++ b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/credentialmanager/CredentialManagerListNavigation.kt @@ -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.credentialManagerGraph( onNavigateBack: () -> Unit, - navController: NavController, ) { - navigation( - startDestination = CredentialManagerListRoute, - ) { - credentialManagerListDestination( - onNavigateBack = onNavigateBack, - onNavigateToGetPassword = { - navController.navigateToGetPassword() - }, - onNavigateToCreatePassword = { - navController.navigateToCreatePassword() - }, - onNavigateToGetPasskey = { - navController.navigateToGetPasskey() - }, - onNavigateToCreatePasskey = { - navController.navigateToCreatePasskey() - }, - onNavigateToGetPasswordOrPasskey = { - navController.navigateToGetPasswordOrPasskey() - }, + entry { + val navigationController = rememberNavigationController( + 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.credentialManagerDestinations( + navigationController: NavigationController, + 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.credentialManagerListDestination( onNavigateBack: () -> Unit, onNavigateToGetPassword: () -> Unit, onNavigateToCreatePassword: () -> Unit, @@ -94,7 +95,7 @@ private fun NavGraphBuilder.credentialManagerListDestination( onNavigateToCreatePasskey: () -> Unit, onNavigateToGetPasswordOrPasskey: () -> Unit, ) { - composableWithRootPushTransitions { + rootPushEntry { 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.navigateToCredentialManager() { + this.navigate(route = RootNavGraph.CredentialManagerRoute) } diff --git a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/getpasskey/GetPasskeyNavigation.kt b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/getpasskey/GetPasskeyNavigation.kt index 57c749941a..87ed27e095 100644 --- a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/getpasskey/GetPasskeyNavigation.kt +++ b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/getpasskey/GetPasskeyNavigation.kt @@ -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.getPasskeyDestination( onNavigateBack: () -> Unit, ) { - composableWithPushTransitions { + pushEntry { 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.navigateToGetPasskey() { + this.navigate(route = CredentialManagerGraph.GetPasskeyRoute) } diff --git a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/getpassword/GetPasswordNavigation.kt b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/getpassword/GetPasswordNavigation.kt index fd8261d746..29160eb89d 100644 --- a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/getpassword/GetPasswordNavigation.kt +++ b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/getpassword/GetPasswordNavigation.kt @@ -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.getPasswordDestination( onNavigateBack: () -> Unit, ) { - composableWithPushTransitions { + pushEntry { 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.navigateToGetPassword() { + this.navigate(route = CredentialManagerGraph.GetPasswordRoute) } diff --git a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/getpasswordorpasskey/GetPasswordOrPasskeyNavigation.kt b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/getpasswordorpasskey/GetPasswordOrPasskeyNavigation.kt index 4f0653ffcc..86440576d3 100644 --- a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/getpasswordorpasskey/GetPasswordOrPasskeyNavigation.kt +++ b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/getpasswordorpasskey/GetPasswordOrPasskeyNavigation.kt @@ -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.getPasswordOrPasskeyDestination( onNavigateBack: () -> Unit, ) { - composableWithPushTransitions { + pushEntry { 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.navigateToGetPasswordOrPasskey() { + this.navigate(route = CredentialManagerGraph.GetPasswordOrPasskeyRoute) } diff --git a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/landing/LandingNavigation.kt b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/landing/LandingNavigation.kt index 49adeb2601..1aa1ec9eb3 100644 --- a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/landing/LandingNavigation.kt +++ b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/landing/LandingNavigation.kt @@ -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.landingDestination( onNavigateToAutofill: () -> Unit, onNavigateToCredentialManager: () -> Unit, ) { - composableWithRootPushTransitions { + rootPushEntry { LandingScreen( onNavigateToAutofill = onNavigateToAutofill, onNavigateToCredentialManager = onNavigateToCredentialManager, diff --git a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/main/MainGraph.kt b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/main/MainGraph.kt new file mode 100644 index 0000000000..adaa8f3a93 --- /dev/null +++ b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/main/MainGraph.kt @@ -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() +} diff --git a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/rootnav/RootNavGraph.kt b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/rootnav/RootNavGraph.kt new file mode 100644 index 0000000000..3bcf2c044a --- /dev/null +++ b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/rootnav/RootNavGraph.kt @@ -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() +} diff --git a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/rootnav/RootNavNavigation.kt b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/rootnav/RootNavNavigation.kt index 5ed8f62451..2ad073e877 100644 --- a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/rootnav/RootNavNavigation.kt +++ b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/rootnav/RootNavNavigation.kt @@ -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.rootNavDestination( onSplashScreenRemoved: () -> Unit, ) { - composable { + entry { RootNavScreen(onSplashScreenRemoved = onSplashScreenRemoved) } } diff --git a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/rootnav/RootNavScreen.kt b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/rootnav/RootNavScreen.kt index b7aede381f..f722f521a2 100644 --- a/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/rootnav/RootNavScreen.kt +++ b/testharness/src/main/kotlin/com/bitwarden/testharness/ui/platform/feature/rootnav/RootNavScreen.kt @@ -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 = rememberNavigationController( + routes = arrayOf(RootNavGraph.LandingRoute), + ), + entryProvider: (RootNavGraph) -> NavEntry = 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) -> NavEntry = entryProvider { + landingDestination( + onNavigateToAutofill = { navigationController.navigateToAutofill() }, + onNavigateToCredentialManager = { navigationController.navigateToCredentialManager() }, + ) + autofillGraph( + onNavigateBack = { navigationController.navigateBack() }, + ) + credentialManagerGraph( + onNavigateBack = { navigationController.navigateBack() }, + ) } diff --git a/testharness/src/test/kotlin/com/bitwarden/testharness/ui/platform/feature/rootnav/RootNavScreenTest.kt b/testharness/src/test/kotlin/com/bitwarden/testharness/ui/platform/feature/rootnav/RootNavScreenTest.kt index e770e31602..209787348e 100644 --- a/testharness/src/test/kotlin/com/bitwarden/testharness/ui/platform/feature/rootnav/RootNavScreenTest.kt +++ b/testharness/src/test/kotlin/com/bitwarden/testharness/ui/platform/feature/rootnav/RootNavScreenTest.kt @@ -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( + routes = arrayOf(RootNavGraph.LandingRoute), + ) private val rootNavStateFlow = MutableStateFlow(RootNavState.Splash) private val viewModel = mockk { 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(), + ) } } }