diff --git a/core/navigation/implementation/build.gradle.kts b/core/navigation/implementation/build.gradle.kts index f5c36adc..ebe7a4f3 100644 --- a/core/navigation/implementation/build.gradle.kts +++ b/core/navigation/implementation/build.gradle.kts @@ -3,6 +3,8 @@ plugins { alias(libs.plugins.td.multiplatform.kotlin.inject.common) alias(libs.plugins.compose.multiplatform) alias(libs.plugins.compose.compiler) + alias(libs.plugins.td.multiplatform.mokkery) + alias(libs.plugins.kotlin.serialization) } kotlin { @@ -11,6 +13,17 @@ kotlin { } sourceSets { + getByName("androidHostTest") { + dependencies { + implementation(projects.core.testing.implementation) + } + } + commonTest { + dependencies { + implementation(projects.core.testing.gateway) + implementation(libs.kotlin.serialization.core) + } + } commonMain { dependencies { implementation(projects.core.navigation.gateway) diff --git a/core/navigation/implementation/src/androidHostTest/kotlin/com/multiplatform/td/core/navigation/composable/NavigationContextTest.kt b/core/navigation/implementation/src/androidHostTest/kotlin/com/multiplatform/td/core/navigation/composable/NavigationContextTest.kt new file mode 100644 index 00000000..065893d7 --- /dev/null +++ b/core/navigation/implementation/src/androidHostTest/kotlin/com/multiplatform/td/core/navigation/composable/NavigationContextTest.kt @@ -0,0 +1,73 @@ +package com.multiplatform.td.core.navigation.composable + +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.multiplatform.td.core.app.composable.LocalComponentStore +import com.multiplatform.td.core.app.composable.LocalNavController +import com.multiplatform.td.core.app.error.CompositionContextException +import com.multiplatform.td.core.app.inject.ComponentStore +import com.multiplatform.td.core.app.inject.ComponentStoreImpl +import com.multiplatform.td.core.testing.AbstractAndroidUnitTest +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.test.assertEquals +import kotlin.test.assertFails +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +internal class NavigationContextTest : AbstractAndroidUnitTest() { + + @Test + fun testNavigationContext() { + with(testRule) { + setScreen { + val componentStore = remember { ComponentStoreImpl() } + val navController = rememberNavController() + CompositionLocalProvider( + LocalComponentStore provides componentStore, + LocalNavController provides navController, + ) { + NavigationContext { + assertEquals(navController, LocalNavController.current) + assertEquals(componentStore, LocalComponentStore.current) + assertTrue { + val navComponent = LocalNavigationComponent.current + navComponent.navHostController == navController + } + } + } + } + } + } + + @Test + fun testNoNavControllerError() { + with(testRule) { + setScreen { + val error = assertFails { LocalNavController.current } + assertTrue { error is CompositionContextException } + assertEquals( + "compositionLocalOf { ${NavHostController::class.simpleName} } not provided, please provide value for it.", + error.message, + ) + } + } + } + + @Test + fun testNoComponentStoreError() { + with(testRule) { + setScreen { + val error = assertFails { LocalComponentStore.current } + assertTrue { error is CompositionContextException } + assertEquals( + "compositionLocalOf { ${ComponentStore::class.simpleName} } not provided, please provide value for it.", + error.message, + ) + } + } + } +} diff --git a/core/navigation/implementation/src/commonMain/kotlin/com/multiplatform/td/core/navigation/FeatureRouterImpl.kt b/core/navigation/implementation/src/commonMain/kotlin/com/multiplatform/td/core/navigation/FeatureRouterImpl.kt index d7e79c9c..7acd99c1 100644 --- a/core/navigation/implementation/src/commonMain/kotlin/com/multiplatform/td/core/navigation/FeatureRouterImpl.kt +++ b/core/navigation/implementation/src/commonMain/kotlin/com/multiplatform/td/core/navigation/FeatureRouterImpl.kt @@ -1,17 +1,16 @@ package com.multiplatform.td.core.navigation -import androidx.navigation.NavController import androidx.navigation.NavOptions import androidx.navigation.navOptions internal class FeatureRouterImpl( - private val navController: NavController, + private val navController: NavControllerProxy, ) : FeatureRouter { override fun navigate(route: FeatureRoute) = when { route.navOptions != null -> { - val navOptions = requireNotNull(route.navOptions).toOptions() - navController.navigate(route.route, navOptions) + val navOptions = requireNotNull(route.navOptions) + navigate(route, navOptions) } else -> navController.navigate(route.route) } @@ -26,15 +25,14 @@ internal class FeatureRouterImpl( override fun back() { navController.popBackStack() } - - private fun FeatureNavOptions.toOptions(): NavOptions = navOptions { - restoreState = this@toOptions.restoreState - launchSingleTop = this@toOptions.singleTop - this@toOptions.popUpTo?.let { route -> - popUpTo(route = route) { - inclusive = this@toOptions.inclusive - saveState = this@toOptions.saveState - } +} +internal fun FeatureNavOptions.toOptions(): NavOptions = navOptions { + restoreState = this@toOptions.restoreState + launchSingleTop = this@toOptions.singleTop + this@toOptions.popUpTo?.let { route -> + popUpTo(route = route) { + inclusive = this@toOptions.inclusive + saveState = this@toOptions.saveState } } } diff --git a/core/navigation/implementation/src/commonMain/kotlin/com/multiplatform/td/core/navigation/NavControllerProxy.kt b/core/navigation/implementation/src/commonMain/kotlin/com/multiplatform/td/core/navigation/NavControllerProxy.kt new file mode 100644 index 00000000..508de14c --- /dev/null +++ b/core/navigation/implementation/src/commonMain/kotlin/com/multiplatform/td/core/navigation/NavControllerProxy.kt @@ -0,0 +1,30 @@ +package com.multiplatform.td.core.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavOptions +import androidx.navigation.Navigator + +interface NavControllerProxy { + + fun navigate( + route: T, + navOptions: NavOptions? = null, + navigatorExtras: Navigator.Extras? = null, + ) + + fun popBackStack(): Boolean +} + +fun createNavController( + navController: NavController, +): NavControllerProxy = object : NavControllerProxy { + + override fun navigate( + route: T, + navOptions: NavOptions?, + navigatorExtras: Navigator.Extras?, + ) = navController.navigate(route, navOptions, navigatorExtras) + + override fun popBackStack(): Boolean = + navController.popBackStack() +} diff --git a/core/navigation/implementation/src/commonMain/kotlin/com/multiplatform/td/core/navigation/inject/binder/FeatureRouterBinder.kt b/core/navigation/implementation/src/commonMain/kotlin/com/multiplatform/td/core/navigation/inject/binder/FeatureRouterBinder.kt index a1f83725..2dbffd20 100644 --- a/core/navigation/implementation/src/commonMain/kotlin/com/multiplatform/td/core/navigation/inject/binder/FeatureRouterBinder.kt +++ b/core/navigation/implementation/src/commonMain/kotlin/com/multiplatform/td/core/navigation/inject/binder/FeatureRouterBinder.kt @@ -4,6 +4,7 @@ import androidx.navigation.NavHostController import com.multiplatform.td.core.injection.Binder import com.multiplatform.td.core.navigation.FeatureRouter import com.multiplatform.td.core.navigation.FeatureRouterImpl +import com.multiplatform.td.core.navigation.createNavController import me.tatarka.inject.annotations.Inject @Inject @@ -12,5 +13,5 @@ class FeatureRouterBinder( ) : Binder { override fun invoke(): FeatureRouter = - FeatureRouterImpl(navController = navHostController) + FeatureRouterImpl(navController = createNavController(navHostController)) } diff --git a/core/navigation/implementation/src/commonTest/kotlin/com/multiplatform/td/core/navigation/FeatureRouterImplTest.kt b/core/navigation/implementation/src/commonTest/kotlin/com/multiplatform/td/core/navigation/FeatureRouterImplTest.kt new file mode 100644 index 00000000..502381cd --- /dev/null +++ b/core/navigation/implementation/src/commonTest/kotlin/com/multiplatform/td/core/navigation/FeatureRouterImplTest.kt @@ -0,0 +1,110 @@ +package com.multiplatform.td.core.navigation + +import dev.mokkery.answering.returns +import dev.mokkery.every +import dev.mokkery.mock +import dev.mokkery.verify +import kotlinx.serialization.Serializable +import kotlin.test.Test + +internal class FeatureRouterImplTest { + + private val route = TestFeatureRoute.Route() + private val routeWithOption = TestFeatureRoute.RouteWithOptions() + + private val navController = mock { + every { + navigate( + route = route.route, + navOptions = null, + navigatorExtras = null, + ) + } returns Unit + every { + navigate( + route = routeWithOption.route, + navOptions = routeWithOption.navOptions.toOptions(), + navigatorExtras = null, + ) + } returns Unit + every { + popBackStack() + } returns true + } + + private val featureRouter = FeatureRouterImpl( + navController = navController, + ) + + @Test + fun `given route without options will call to nav controller`() { + featureRouter.navigate(route) + + verify { + navController.navigate( + route = route.route, + navOptions = null, + navigatorExtras = null, + ) + } + } + + @Test + fun `given route with options will call to nav controller`() { + featureRouter.navigate(routeWithOption) + + verify { + navController.navigate( + route = routeWithOption.route, + navOptions = routeWithOption.navOptions.toOptions(), + navigatorExtras = null, + ) + } + } + + @Test + fun `given back will call pop to nav controller`() { + featureRouter.back() + + verify { + navController.popBackStack() + } + } + + @Test + fun `given restart will call to nav controller`() { + featureRouter.restart(route) + + verify { + navController.navigate( + route = route.route, + navOptions = null, + navigatorExtras = null, + ) + } + } +} + +internal sealed interface TestRoute { + + @Serializable + data object Route : TestRoute +} + +internal sealed class TestFeatureRoute : FeatureRoute() { + + class Route : TestFeatureRoute() { + + override val route: TestRoute = TestRoute.Route + } + + class RouteWithOptions : TestFeatureRoute() { + + override val route: TestRoute = TestRoute.Route + + override val navOptions: FeatureNavOptions = FeatureNavOptions.Builder() + .singleTop(true) + .inclusive(true) + .build() + } +}