From d303ef4651db4ede1c5ebd29a133a8bd65fdcf3b Mon Sep 17 00:00:00 2001 From: Gert van Niekerk Date: Sun, 23 Aug 2026 21:47:09 +0200 Subject: [PATCH] fix(android): preserve More in back navigation --- .../app/src/main/java/com/noop/ui/AppRoot.kt | 7 +-- .../com/noop/ui/MoreNavigationContractTest.kt | 54 +++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 android/app/src/test/java/com/noop/ui/MoreNavigationContractTest.kt diff --git a/android/app/src/main/java/com/noop/ui/AppRoot.kt b/android/app/src/main/java/com/noop/ui/AppRoot.kt index 12a367d71e..352d1102a1 100644 --- a/android/app/src/main/java/com/noop/ui/AppRoot.kt +++ b/android/app/src/main/java/com/noop/ui/AppRoot.kt @@ -446,9 +446,10 @@ fun AppRoot(viewModel: AppViewModel = viewModel()) { } composable(Destination.TestCentre.route) { TestCentreScreen(viewModel) } // The "More" page — the iOS More tab's twin: a navigated ScreenScaffold page hosting the - // full grouped destination list (was a pull-up sheet). A row navigates top-level. + // full grouped destination list (was a pull-up sheet). A row pushes its destination so + // Android Back returns to More instead of skipping straight to Today. composable(Destination.More.route) { - MoreScreen(onNavigate = { nav.navigateTopLevel(it) }) + MoreScreen(onNavigate = { nav.navigate(it) }) } } } @@ -588,7 +589,7 @@ fun AppRoot(viewModel: AppViewModel = viewModel()) { // ([drawerGroups]) inside a [ScreenScaffold], with the exact section-header + row styling the sheet // used (uppercase [Overline] group labels, icon + label [NavigationDrawerItem] rows) — now with a // trailing chevron so each row reads as a navigation push, matching the iOS disclosure rows. Tapping a -// row navigates top-level; there is no sheet to dismiss. The floating bottom bar stays visible because +// row pushes its destination; there is no sheet to dismiss. The floating bottom bar stays visible because // this is just another NavHost destination under the same Scaffold. /** The full grouped destination list as a navigated page (the iOS More tab's twin). */ diff --git a/android/app/src/test/java/com/noop/ui/MoreNavigationContractTest.kt b/android/app/src/test/java/com/noop/ui/MoreNavigationContractTest.kt new file mode 100644 index 0000000000..594a5b40d5 --- /dev/null +++ b/android/app/src/test/java/com/noop/ui/MoreNavigationContractTest.kt @@ -0,0 +1,54 @@ +package com.noop.ui + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Assume.assumeTrue +import org.junit.Test +import java.io.File + +/** + * Pins the Android More-page navigation contract. More is a real NavHost destination, so each row must + * push its destination onto the current stack. Treating a row like a bottom-tab switch pops More back to + * Today before opening the destination, which makes Android Back skip More entirely. + * + * This source audit is deliberately paired with the existing navigation wiring: it proves every generated + * [MoreRow] still flows through the shared push callback while the actual bottom bar retains the separate + * top-level state-save/restore policy. It follows the source-locating pattern used by + * [ResolvedSeriesCallSiteAuditTest]. + */ +class MoreNavigationContractTest { + + /** Locate AppRoot.kt from Gradle, IDE, or repository-root test working directories. */ + private fun appRootSource(): File? { + val userDir = File(System.getProperty("user.dir") ?: ".") + return listOf( + File(userDir, "src/main/java/com/noop/ui/AppRoot.kt"), + File(userDir, "app/src/main/java/com/noop/ui/AppRoot.kt"), + File(userDir, "android/app/src/main/java/com/noop/ui/AppRoot.kt"), + ).firstOrNull { it.isFile } + } + + @Test + fun moreRowsPushWhileBottomTabsRemainTopLevel() { + val sourceFile = appRootSource() + assumeTrue("AppRoot.kt not reachable from ${System.getProperty("user.dir")}", sourceFile != null) + val source = sourceFile!!.readText().replace(Regex("\\s+"), " ") + + assertTrue( + "More destinations must be pushed so Back returns to More", + source.contains("MoreScreen(onNavigate = { nav.navigate(it) })"), + ) + assertFalse( + "More destinations must not clear the stack through navigateTopLevel", + source.contains("MoreScreen(onNavigate = { nav.navigateTopLevel(it) })"), + ) + assertTrue( + "Every generated More row must keep using the shared navigation callback", + source.contains("MoreRow(dest = dest, onClick = { onNavigate(dest.route) })"), + ) + assertTrue( + "Bottom-tab selections must retain top-level state save/restore", + source.contains("if (dest.route != currentRoute) nav.navigateTopLevel(dest.route)"), + ) + } +}