From 3b816c729979d99f89066d7c13c6befb3d67de2b Mon Sep 17 00:00:00 2001 From: Dward Date: Thu, 11 Dec 2025 22:40:40 +0800 Subject: [PATCH] fix: LoginScreen does not appear immediately and you can see NotesScreen for a second ### What: - When the user is logged out the app briefly shows the NotesScreen before redirecting to LoginScreen ### Why: - The initial screen configured in navigation is NotesScreen ### How: - Dynamically choose start destination based on login state - Add instrumentation tests to ensure correct start destination --- noty-android/app/composeapp/build.gradle.kts | 3 + .../composeapp/navigation/NavigationTest.kt | 87 +++++++++++++++++++ .../composeapp/navigation/NotyNavigation.kt | 9 +- .../noty/composeapp/ui/MainActivity.kt | 9 +- noty-android/gradle/libs.versions.toml | 1 + 5 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 noty-android/app/composeapp/src/androidTest/java/dev/shreyaspatil/noty/composeapp/navigation/NavigationTest.kt diff --git a/noty-android/app/composeapp/build.gradle.kts b/noty-android/app/composeapp/build.gradle.kts index c5f79336..3d730d31 100644 --- a/noty-android/app/composeapp/build.gradle.kts +++ b/noty-android/app/composeapp/build.gradle.kts @@ -190,6 +190,9 @@ dependencies { // WorkManager for testing androidTestImplementation(libs.androidx.work.testing) + // Navigation for testing + androidTestImplementation(libs.androidx.navigation.testing) + // Leak Canary // Uncomment this when have to check for leaks // debugImplementation(libs.leakcanary) diff --git a/noty-android/app/composeapp/src/androidTest/java/dev/shreyaspatil/noty/composeapp/navigation/NavigationTest.kt b/noty-android/app/composeapp/src/androidTest/java/dev/shreyaspatil/noty/composeapp/navigation/NavigationTest.kt new file mode 100644 index 00000000..cc93ffeb --- /dev/null +++ b/noty-android/app/composeapp/src/androidTest/java/dev/shreyaspatil/noty/composeapp/navigation/NavigationTest.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2020 Shreyas Patil + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.shreyaspatil.noty.composeapp.navigation + +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.navigation.NavHostController +import androidx.navigation.compose.ComposeNavigator +import androidx.navigation.testing.TestNavHostController +import androidx.test.ext.junit.rules.ActivityScenarioRule +import dagger.hilt.android.testing.HiltAndroidTest +import dev.shreyaspatil.noty.composeapp.NotyScreenTest +import dev.shreyaspatil.noty.composeapp.ui.MainActivity +import dev.shreyaspatil.noty.composeapp.ui.Screen +import dev.shreyaspatil.noty.composeapp.ui.theme.LocalUiInDarkMode +import dev.shreyaspatil.noty.core.session.SessionManager +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class NavigationTest : NotyScreenTest() { + + @Inject + lateinit var sessionManager: SessionManager + + @Before + fun setUp() { + inject() + // Mock fake authentication + sessionManager.saveToken("Bearer ABCD") + } + + @Test + fun startDestination_isLoginScreen_whenLoggedOut() = runTest { + val isLoggedIn = false + val expectedRoute = Screen.Login.route + + testStartDestination(isLoggedIn, expectedRoute) + } + + @Test + fun startDestination_isNotesScreen_whenLoggedIn() = runTest { + val isLoggedIn = true + val expectedRoute = Screen.Notes.route + + testStartDestination(isLoggedIn, expectedRoute) + } + + private fun AndroidComposeTestRule, MainActivity>.testStartDestination( + isLoggedIn: Boolean, + expectedRoute: String + ) { + lateinit var navController: NavHostController + + setNotyContent { + CompositionLocalProvider(LocalUiInDarkMode provides true) { + navController = TestNavHostController(LocalContext.current) + navController.navigatorProvider.addNavigator(ComposeNavigator()) + + NotyNavigation( + isLoggedIn = isLoggedIn, + navController = navController + ) + } + } + + waitForIdle() + assertEquals(expectedRoute, navController.currentDestination?.route) + } +} diff --git a/noty-android/app/composeapp/src/main/java/dev/shreyaspatil/noty/composeapp/navigation/NotyNavigation.kt b/noty-android/app/composeapp/src/main/java/dev/shreyaspatil/noty/composeapp/navigation/NotyNavigation.kt index b1b1fa6d..4b0ceafa 100644 --- a/noty-android/app/composeapp/src/main/java/dev/shreyaspatil/noty/composeapp/navigation/NotyNavigation.kt +++ b/noty-android/app/composeapp/src/main/java/dev/shreyaspatil/noty/composeapp/navigation/NotyNavigation.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController +import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -43,8 +44,10 @@ import dev.shreyaspatil.noty.view.viewmodel.NoteDetailViewModel const val NOTY_NAV_HOST_ROUTE = "noty-main-route" @Composable -fun NotyNavigation() { - val navController = rememberNavController() +fun NotyNavigation( + isLoggedIn: Boolean, + navController: NavHostController = rememberNavController(), +) { NavHost( navController, @@ -54,7 +57,7 @@ fun NotyNavigation() { .statusBarsPadding() .navigationBarsPadding() .displayCutoutPadding(), - startDestination = Screen.Notes.route, + startDestination = if (isLoggedIn) Screen.Notes.route else Screen.Login.route, route = NOTY_NAV_HOST_ROUTE, enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start, tween(700)) }, exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Start, tween(700)) }, diff --git a/noty-android/app/composeapp/src/main/java/dev/shreyaspatil/noty/composeapp/ui/MainActivity.kt b/noty-android/app/composeapp/src/main/java/dev/shreyaspatil/noty/composeapp/ui/MainActivity.kt index 34d5dad4..6842c5e5 100644 --- a/noty-android/app/composeapp/src/main/java/dev/shreyaspatil/noty/composeapp/ui/MainActivity.kt +++ b/noty-android/app/composeapp/src/main/java/dev/shreyaspatil/noty/composeapp/ui/MainActivity.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.core.view.WindowInsetsControllerCompat +import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope @@ -36,7 +37,9 @@ import dev.shreyaspatil.noty.composeapp.R import dev.shreyaspatil.noty.composeapp.navigation.NotyNavigation import dev.shreyaspatil.noty.composeapp.ui.theme.LocalUiInDarkMode import dev.shreyaspatil.noty.composeapp.ui.theme.NotyTheme +import dev.shreyaspatil.noty.composeapp.utils.collectState import dev.shreyaspatil.noty.core.preference.PreferenceManager +import dev.shreyaspatil.noty.view.viewmodel.HomeViewModel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import javax.inject.Inject @@ -59,10 +62,14 @@ class MainActivity : ComponentActivity() { @Composable private fun NotyMain() { val darkMode by rememberUiMode() + + val homeViewModel: HomeViewModel = hiltViewModel() + val state by homeViewModel.collectState() + CompositionLocalProvider(LocalUiInDarkMode provides darkMode) { NotyTheme(darkTheme = LocalUiInDarkMode.current) { Surface { - NotyNavigation() + NotyNavigation(state.isLoggedIn == true) } } } diff --git a/noty-android/gradle/libs.versions.toml b/noty-android/gradle/libs.versions.toml index 035bc6a1..b87d51fd 100644 --- a/noty-android/gradle/libs.versions.toml +++ b/noty-android/gradle/libs.versions.toml @@ -134,6 +134,7 @@ junit5-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version. junit5-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit5" } junit5-params = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "junit5" } junit5-vintage = { group = "org.junit.vintage", name = "junit-vintage-engine", version.ref = "junit5" } +androidx-navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "composeNav"} androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidJUnit" } androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso" } androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidTestCore" }