diff --git a/composeApp/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/App.kt b/composeApp/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/App.kt index 3c1d91059..a47a0c379 100644 --- a/composeApp/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/App.kt +++ b/composeApp/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/App.kt @@ -22,6 +22,7 @@ import com.livefast.eattrash.raccoonforfriendica.core.appearance.theme.AppTheme import com.livefast.eattrash.raccoonforfriendica.core.l10n.di.getL10nManager import com.livefast.eattrash.raccoonforfriendica.core.l10n.messages.ProvideStrings import com.livefast.eattrash.raccoonforfriendica.core.navigation.DrawerEvent +import com.livefast.eattrash.raccoonforfriendica.core.navigation.VoyagerNavigator import com.livefast.eattrash.raccoonforfriendica.core.navigation.di.getDrawerCoordinator import com.livefast.eattrash.raccoonforfriendica.core.navigation.di.getNavigationCoordinator import com.livefast.eattrash.raccoonforfriendica.core.utils.di.getCrashReportManager @@ -160,7 +161,8 @@ fun App(onLoadingFinished: (() -> Unit)? = null) { }, ) { navigator -> LaunchedEffect(navigationCoordinator) { - navigationCoordinator.setRootNavigator(navigator) + val adapter = VoyagerNavigator(navigator) + navigationCoordinator.setRootNavigator(adapter) } CurrentScreen() diff --git a/composeApp/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/resources/SharedResources.kt b/composeApp/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/resources/SharedResources.kt index 26cf0bf83..f6151e18c 100644 --- a/composeApp/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/resources/SharedResources.kt +++ b/composeApp/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/resources/SharedResources.kt @@ -67,6 +67,7 @@ internal class SharedResources : CoreResources { override fun getPlayerConfig(contentScale: ContentScale): PlayerConfig = PlayerConfig( isFullScreenEnabled = false, + isMute = true, videoFitMode = if (contentScale == ContentScale.Fit) { ScreenResize.FIT diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts index fa44942a2..5eb0be76d 100644 --- a/core/navigation/build.gradle.kts +++ b/core/navigation/build.gradle.kts @@ -6,6 +6,7 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.jetbrains.compose) alias(libs.plugins.compose.compiler) + alias(libs.plugins.mokkery) } @OptIn(ExperimentalKotlinGradlePluginApi::class) @@ -42,6 +43,15 @@ kotlin { implementation(projects.domain.content.data) } + val commonTest by getting { + dependencies { + dependencies { + implementation(kotlin("test")) + implementation(libs.kotlinx.coroutines.test) + implementation(libs.turbine) + } + } + } } } diff --git a/core/navigation/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/navigation/DefaultNavigationCoordinator.kt b/core/navigation/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/navigation/DefaultNavigationCoordinator.kt index d4dca3569..d935332df 100644 --- a/core/navigation/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/navigation/DefaultNavigationCoordinator.kt +++ b/core/navigation/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/navigation/DefaultNavigationCoordinator.kt @@ -2,7 +2,6 @@ package com.livefast.eattrash.raccoonforfriendica.core.navigation import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.navigator.Navigator import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -21,7 +20,7 @@ internal class DefaultNavigationCoordinator( override val canPop = MutableStateFlow(false) override val exitMessageVisible = MutableStateFlow(false) - private var rootNavigator: Navigator? = null + private var rootNavigator: NavigatorAdapter? = null private var bottomBarScrollConnection: NestedScrollConnection? = null private var canGoBackCallback: (() -> Boolean)? = null private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher) @@ -37,7 +36,7 @@ internal class DefaultNavigationCoordinator( } } - override fun setRootNavigator(navigator: Navigator) { + override fun setRootNavigator(navigator: NavigatorAdapter) { rootNavigator = navigator canPop.update { navigator.canPop } } diff --git a/core/navigation/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/navigation/NavigationCoordinator.kt b/core/navigation/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/navigation/NavigationCoordinator.kt index 1df349fd1..048a4ec2e 100644 --- a/core/navigation/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/navigation/NavigationCoordinator.kt +++ b/core/navigation/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/navigation/NavigationCoordinator.kt @@ -3,7 +3,6 @@ package com.livefast.eattrash.raccoonforfriendica.core.navigation import androidx.compose.runtime.Stable import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.navigator.Navigator import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -26,7 +25,7 @@ interface NavigationCoordinator { fun setExitMessageVisible(value: Boolean) - fun setRootNavigator(navigator: Navigator) + fun setRootNavigator(navigator: NavigatorAdapter) fun replace(screen: Screen) diff --git a/core/navigation/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/navigation/NavigatorAdapter.kt b/core/navigation/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/navigation/NavigatorAdapter.kt new file mode 100644 index 000000000..a5f648b42 --- /dev/null +++ b/core/navigation/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/navigation/NavigatorAdapter.kt @@ -0,0 +1,15 @@ +package com.livefast.eattrash.raccoonforfriendica.core.navigation + +import cafe.adriel.voyager.core.screen.Screen + +interface NavigatorAdapter { + val canPop: Boolean + + fun push(screen: Screen) + + fun pop() + + fun popUntilRoot() + + fun replace(screen: Screen) +} diff --git a/core/navigation/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/navigation/VoyagerNavigator.kt b/core/navigation/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/navigation/VoyagerNavigator.kt new file mode 100644 index 000000000..49bbbfa5e --- /dev/null +++ b/core/navigation/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/navigation/VoyagerNavigator.kt @@ -0,0 +1,27 @@ +package com.livefast.eattrash.raccoonforfriendica.core.navigation + +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.Navigator + +data class VoyagerNavigator( + val adaptee: Navigator, +) : NavigatorAdapter { + override val canPop: Boolean + get() = adaptee.canPop + + override fun push(screen: Screen) { + adaptee.push(screen) + } + + override fun pop() { + adaptee.pop() + } + + override fun popUntilRoot() { + adaptee.popUntilRoot() + } + + override fun replace(screen: Screen) { + adaptee.replace(screen) + } +} diff --git a/core/navigation/src/commonTest/kotlin/com/livefast/eattrash/raccoonforfriendica/core/navigation/DefaultDrawerCoordinatorTest.kt b/core/navigation/src/commonTest/kotlin/com/livefast/eattrash/raccoonforfriendica/core/navigation/DefaultDrawerCoordinatorTest.kt new file mode 100644 index 000000000..1d8c3d635 --- /dev/null +++ b/core/navigation/src/commonTest/kotlin/com/livefast/eattrash/raccoonforfriendica/core/navigation/DefaultDrawerCoordinatorTest.kt @@ -0,0 +1,63 @@ +package com.livefast.eattrash.raccoonforfriendica.core.navigation + +import app.cash.turbine.test +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class DefaultDrawerCoordinatorTest { + private val sut = DefaultDrawerCoordinator() + + @Test + fun `when toggleDrawer then event is emitted`() = + runTest { + launch { + sut.toggleDrawer() + } + + sut.events.test { + val evt = awaitItem() + assertEquals(DrawerEvent.Toggle, evt) + } + } + + @Test + fun `when closeDrawer then event is emitted`() = + runTest { + launch { + sut.closeDrawer() + } + + sut.events.test { + val evt = awaitItem() + assertEquals(DrawerEvent.Close, evt) + } + } + + @Test + fun `when setGesturesEnabled then state is updated`() = + runTest { + val initial = sut.gesturesEnabled.value + assertTrue(initial) + + sut.setGesturesEnabled(false) + + val value = sut.gesturesEnabled.value + assertFalse(value) + } + + @Test + fun `when sendEvent then event is emitted`() = + runTest { + launch { + sut.sendEvent(DrawerEvent.Toggle) + } + sut.events.test { + val evt = awaitItem() + assertEquals(DrawerEvent.Toggle, evt) + } + } +} diff --git a/core/navigation/src/commonTest/kotlin/com/livefast/eattrash/raccoonforfriendica/core/navigation/DefaultNavigationCoordinatorTest.kt b/core/navigation/src/commonTest/kotlin/com/livefast/eattrash/raccoonforfriendica/core/navigation/DefaultNavigationCoordinatorTest.kt new file mode 100644 index 000000000..9585017ba --- /dev/null +++ b/core/navigation/src/commonTest/kotlin/com/livefast/eattrash/raccoonforfriendica/core/navigation/DefaultNavigationCoordinatorTest.kt @@ -0,0 +1,206 @@ +package com.livefast.eattrash.raccoonforfriendica.core.navigation + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import app.cash.turbine.test +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import dev.mokkery.MockMode +import dev.mokkery.answering.returns +import dev.mokkery.every +import dev.mokkery.mock +import dev.mokkery.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@OptIn(ExperimentalCoroutinesApi::class) +class DefaultNavigationCoordinatorTest { + private val sut = + DefaultNavigationCoordinator( + dispatcher = UnconfinedTestDispatcher(), + ) + + @Test + fun `when setCanGoBackCallback then result is as expected`() = + runTest { + val callback = { true } + sut.setCanGoBackCallback(callback) + + val res = sut.getCanGoBackCallback() + assertEquals(callback, res) + } + + @Test + fun `when setBottomBarScrollConnection then result is as expected`() = + runTest { + val connection = mock(MockMode.autofill) + sut.setBottomBarScrollConnection(connection) + + val res = sut.getBottomBarScrollConnection() + assertEquals(connection, res) + } + + @Test + fun `when setCurrentSection then result is as expected`() { + val section = BottomNavigationSection.Explore + + sut.setCurrentSection(section) + + val res = sut.currentSection.value + assertEquals(section, res) + } + + @Test + fun `when setCurrentSection twice then onDoubleTabSelection is triggered`() = + runTest { + val section = BottomNavigationSection.Explore + sut.setCurrentSection(section) + launch { + delay(DELAY) + sut.setCurrentSection(section) + } + sut.onDoubleTabSelection.test { + val res = awaitItem() + assertEquals(section, res) + } + } + + @Test + fun `given navigator can pop when root navigator set then canPop is as expected`() = + runTest { + val initial = sut.canPop.value + assertFalse(initial) + val navigator = + mock(MockMode.autoUnit) { + every { canPop } returns true + } + + sut.setRootNavigator(navigator) + + val value = sut.canPop.value + assertTrue(value) + } + + @Test + fun `when setExitMessageVisible then value is as expected`() = + runTest { + val initial = sut.exitMessageVisible.value + assertFalse(initial) + + sut.setExitMessageVisible(true) + val value = sut.exitMessageVisible.value + assertTrue(value) + } + + @Test + fun `when push then interactions are as expected`() = + runTest { + val screen = + object : Screen { + override val key: ScreenKey = "new" + + @Composable + override fun Content() { + Box(modifier = Modifier.fillMaxSize()) + } + } + val navigator = + mock(MockMode.autoUnit) { + every { canPop } returns true + } + sut.setRootNavigator(navigator) + + launch { + sut.push(screen) + } + delay(DELAY) + + val canPop = sut.canPop.value + assertTrue(canPop) + verify { + navigator.push(screen) + } + } + + @Test + fun `when replace then interactions are as expected`() = + runTest { + val screen = + object : Screen { + override val key: ScreenKey = "new" + + @Composable + override fun Content() { + Box(modifier = Modifier.fillMaxSize()) + } + } + val navigator = + mock(MockMode.autoUnit) { + every { canPop } returns true + } + sut.setRootNavigator(navigator) + + launch { + sut.replace(screen) + } + delay(DELAY) + + val canPop = sut.canPop.value + assertTrue(canPop) + verify { + navigator.replace(screen) + } + } + + @Test + fun `when pop then interactions are as expected`() = + runTest { + val navigator = + mock(MockMode.autoUnit) { + every { canPop } returns false + } + sut.setRootNavigator(navigator) + + launch { + sut.pop() + } + advanceTimeBy(DELAY) + + val canPop = sut.canPop.value + assertFalse(canPop) + verify { + navigator.pop() + } + } + + @Test + fun `when popUntilRoot then interactions are as expected`() = + runTest { + val navigator = + mock(MockMode.autoUnit) { + every { canPop } returns false + } + sut.setRootNavigator(navigator) + + sut.popUntilRoot() + + verify { + navigator.popUntilRoot() + } + } + + companion object { + const val DELAY = 250L + } +}