Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions core/navigation/implementation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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 <T : Any> navigate(route: FeatureRoute<T>) = 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)
}
Expand All @@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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 <T : Any> navigate(
route: T,
navOptions: NavOptions? = null,
navigatorExtras: Navigator.Extras? = null,
)

fun popBackStack(): Boolean
}

fun createNavController(
navController: NavController,
): NavControllerProxy = object : NavControllerProxy {

override fun <T : Any> navigate(
route: T,
navOptions: NavOptions?,
navigatorExtras: Navigator.Extras?,
) = navController.navigate(route, navOptions, navigatorExtras)

override fun popBackStack(): Boolean =
navController.popBackStack()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -12,5 +13,5 @@ class FeatureRouterBinder(
) : Binder<FeatureRouter> {

override fun invoke(): FeatureRouter =
FeatureRouterImpl(navController = navHostController)
FeatureRouterImpl(navController = createNavController(navHostController))
}
Original file line number Diff line number Diff line change
@@ -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<NavControllerProxy> {
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<TestRoute>() {

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()
}
}
Loading