Compare commits

..
Author SHA1 Message Date
atsunatsu dbf2229774 chore: bump version to 4.4.7-ba7opf.12.4 (versionCode 490) 2026-09-22 14:49:32 +08:00
atsunatsu f2fc1b7b28 test(mutual): portrait-size Robolectric + NavDisplay entry scenario
Run the scroll tests at 411x891dp portrait (device-like layout) and add
a NavDisplay entryProvider scenario that composes MutualScreen the way
the app does, so the prefill scroll is exercised end-to-end.
2026-09-22 14:49:04 +08:00
atsunatsu d600836e19 fix(mutual): gate prefill scroll on real list layout (onGloballyPositioned)
On a device the Mutual entry is composed inside the NavDisplay transition;
LaunchedEffect could run before the LazyColumn is measurable and the
scrollToItem then never lands, leaving the page at the top. Only scroll
once onGloballyPositioned confirms the list has actually been laid out.
2026-09-22 14:49:00 +08:00
atsunatsu 94bffe80b0 chore: bump version to 4.4.7-ba7opf.12.3 (versionCode 489) 2026-09-22 13:13:11 +08:00
atsunatsu 4260fbb096 fix(nav): map Match prefill pushes Mutual onto the stack
The grid-QSO dialog Match button now adds Mutual on top of the Map entry
instead of clearing the stack, so the system back gesture pops back to
the map page (the page the user came from). Bottom-nav tab switches keep
their replace semantics.
2026-09-22 13:13:08 +08:00
5 changed files with 56 additions and 11 deletions

No files matched your search

@@ -248,10 +248,15 @@ fun MainScreen(
mapFilterViewModel = mapFilterViewModel,
onMatchGrid = { grid ->
// Grid-QSO dialog "Match" button: pre-fill the
// match page for that grid and switch to its tab.
// match page for that grid and open it.
mutualViewModel.prefillMatchFromGrid(grid)
while (backStack.size > 1) backStack.removeAt(backStack.size - 1)
backStack.add(Screen.Mutual)
// Push Mutual on top of the Map entry instead of
// replacing the stack (bottom-nav style): the
// system back gesture then pops back to the map
// page, which is the page the user came from.
if (backStack.lastOrNull() !is Screen.Mutual) {
backStack.add(Screen.Mutual)
}
}
)
}
+1
View File
@@ -18,5 +18,6 @@ dependencies {
testImplementation(libs.robolectric)
testImplementation(libs.androidx.test.core)
testImplementation(libs.compose.ui.test.junit4)
testImplementation(libs.compose.navigation3)
debugImplementation(libs.compose.debug.manifest)
}
@@ -54,11 +54,13 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -220,8 +222,18 @@ private fun MutualContent(
}
}
// Set once the list has actually been laid out. The prefill scroll only
// fires after this, because on a real device the Mutual entry appears
// through the NavDisplay transition and LaunchedEffect alone can run
// before the list is measurable (the scroll then never lands and the
// page stays at the top). onGloballyPositioned fires on the first real
// layout, so the subsequent scrollToItem always has a measured list.
var listReady by remember { mutableStateOf(false) }
LazyColumn(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.onGloballyPositioned { listReady = true },
state = listState,
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
@@ -377,8 +389,8 @@ private fun MutualContent(
// LazyListState at index 1: the constructor parameter is ignored by this
// Compose version (verified in tests), while scrollToItem lands correctly.
val matchSearchIndex = if (state.errorMessage != null) 2 else 1
LaunchedEffect(state.scrollToTimeRange, matchSearchIndex) {
if (state.scrollToTimeRange) {
LaunchedEffect(state.scrollToTimeRange, matchSearchIndex, listReady) {
if (state.scrollToTimeRange && listReady) {
listState.scrollToItem(matchSearchIndex)
viewModel.consumeScrollToTimeRange()
}
@@ -12,6 +12,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import com.rtbishop.look4sat.core.presentation.Screen
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
@@ -38,7 +42,7 @@ import org.robolectric.annotation.GraphicsMode
*/
@RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(sdk = [34])
@Config(sdk = [34], qualifiers = "w411dp-h891dp-port")
class MutualMatchPrefillScrollTest {
@get:Rule
@@ -114,8 +118,7 @@ class MutualMatchPrefillScrollTest {
fun prefillAutoStartsQueryAndFillsState() {
val vm = MutualViewModel(FakeSatelliteRepo(), FakeSettingsRepo())
vm.prefillMatchFromGrid("OL62")
val s = vm.uiState.value
// Query auto-started: with the empty-satellite fake the query reaches
val s = vm.uiState.value // Query auto-started: with the empty-satellite fake the query reaches
// the "no satellite data" guard (rather than never being triggered),
// proving prefillMatchFromGrid kicks off queryMutualPasses.
assertTrue(
@@ -127,4 +130,28 @@ class MutualMatchPrefillScrollTest {
org.junit.Assert.assertEquals("OL62", s.stationBGrid)
org.junit.Assert.assertEquals(24, s.hoursAhead)
}
@Test
fun navDisplayEntryAfterPrefill_scrollsToTimeRange() {
// Closest to the real device path: the Mutual screen composed inside a
// NavDisplay entry (back stack [Mutual]) right after prefillMatchFromGrid.
val vm = MutualViewModel(FakeSatelliteRepo(), FakeSettingsRepo())
vm.prefillMatchFromGrid("OL62")
composeRule.setContent {
val backStack = rememberNavBackStack(Screen.Mutual)
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryProvider = entryProvider {
entry<Screen.Mutual> { MutualScreen(viewModel = vm) }
}
)
}
}
}
composeRule.waitForIdle()
composeRule.onNodeWithText("Time range").assertIsDisplayed()
}
}
+2 -2
View File
@@ -1,8 +1,8 @@
[versions]
#noinspection UnusedVersionCatalogEntry
appVersionCode = "488"
appVersionCode = "490"
#noinspection UnusedVersionCatalogEntry
appVersionName = "4.4.7-ba7opf.12.2"
appVersionName = "4.4.7-ba7opf.12.4"
#noinspection UnusedVersionCatalogEntry
compileSdk = "37"
#noinspection UnusedVersionCatalogEntry