mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6962a4bfa1 | ||
|
|
94a1a4f7b7 | ||
|
|
dbf2229774 | ||
|
|
f2fc1b7b28 | ||
|
|
d600836e19 | ||
|
|
94bffe80b0 | ||
|
|
4260fbb096 | ||
|
|
3d5b55faa3 | ||
|
|
466f2f6088 | ||
|
|
14bd984327 | ||
|
|
d5c100903b |
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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,4 +4,20 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "com.rtbishop.look4sat.feature.mutual"
|
||||
}
|
||||
testOptions {
|
||||
unitTests {
|
||||
// Robolectric needs real Android resources for Compose UI tests.
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation(libs.test.junit4)
|
||||
testImplementation(libs.test.coroutines)
|
||||
testImplementation(libs.robolectric)
|
||||
testImplementation(libs.androidx.test.core)
|
||||
testImplementation(libs.compose.ui.test.junit4)
|
||||
testImplementation(libs.compose.navigation3)
|
||||
debugImplementation(libs.compose.debug.manifest)
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package com.rtbishop.look4sat.feature.mutual
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
@@ -54,11 +55,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
|
||||
@@ -77,6 +80,7 @@ import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
@Composable
|
||||
@@ -220,8 +224,24 @@ 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) }
|
||||
Log.d(TAG, "composed: scrollToTimeRange=${state.scrollToTimeRange} matchIndex=${if (state.errorMessage != null) 2 else 1} listReady=$listReady")
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.onGloballyPositioned {
|
||||
if (!listReady) {
|
||||
Log.d(TAG, "lazy list laid out, opening prefill scroll gate")
|
||||
listReady = true
|
||||
}
|
||||
},
|
||||
state = listState,
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
@@ -370,18 +390,43 @@ private fun MutualContent(
|
||||
}
|
||||
}
|
||||
|
||||
// Prefill from the map's grid-QSO dialog "Match" button: jump straight to
|
||||
// the time-range card. Index accounts for the optional error card at the
|
||||
// top (error = 0, station inputs = 1, time range = 2; else 1).
|
||||
// Prefill from the map's grid-QSO dialog "Match" button: once the page is
|
||||
// laid out, jump straight to the time-range card. Index accounts for the
|
||||
// optional error card at the top (error = 0, station inputs = 1, time
|
||||
// range = 2; else 1). scrollToItem is used instead of initializing the
|
||||
// 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) {
|
||||
listState.scrollToItem(matchSearchIndex)
|
||||
LaunchedEffect(state.scrollToTimeRange, matchSearchIndex, listReady) {
|
||||
Log.d(TAG, "scroll effect: scrollToTimeRange=${state.scrollToTimeRange} matchIndex=$matchSearchIndex listReady=$listReady")
|
||||
if (state.scrollToTimeRange && listReady) {
|
||||
// Keep trying until the scroll really lands: on a device the first
|
||||
// frame only contains the station cards + time-range card, which
|
||||
// can be shorter than the viewport (no scroll range), so a single
|
||||
// scrollToItem does nothing. When the async query results arrive
|
||||
// the list grows past one screen and the scroll becomes possible.
|
||||
for (attempt in 0 until 20) {
|
||||
Log.d(TAG, "attempting scrollToItem($matchSearchIndex) #$attempt")
|
||||
try {
|
||||
listState.scrollToItem(matchSearchIndex)
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "scrollToItem($matchSearchIndex) threw", t)
|
||||
break
|
||||
}
|
||||
if (listState.firstVisibleItemIndex == matchSearchIndex) {
|
||||
Log.d(TAG, "scroll landed at $matchSearchIndex on attempt #$attempt")
|
||||
break
|
||||
}
|
||||
delay(100)
|
||||
}
|
||||
viewModel.consumeScrollToTimeRange()
|
||||
Log.d(TAG, "scrollToTimeRange consumed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val TAG = "Look4SatMutual"
|
||||
|
||||
@Composable
|
||||
private fun MutualStatusChip(state: MutualUiState) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
|
||||
+14
-8
@@ -40,6 +40,8 @@ import kotlinx.coroutines.withContext
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val TAG = "Look4SatMutual"
|
||||
|
||||
data class MutualUiState(
|
||||
val stationALat: String = "",
|
||||
val stationALon: String = "",
|
||||
@@ -57,8 +59,8 @@ data class MutualUiState(
|
||||
val isUtc: Boolean = false,
|
||||
val errorMessage: String? = null,
|
||||
// One-shot flag set by prefillMatchFromGrid() (map grid → Match button):
|
||||
// the page scrolls to the time-range card, then consumeScrollToTimeRange()
|
||||
// clears it.
|
||||
// the page scrolls to the time-range card after the first layout, then
|
||||
// consumeScrollToTimeRange() clears it.
|
||||
val scrollToTimeRange: Boolean = false
|
||||
)
|
||||
|
||||
@@ -190,13 +192,14 @@ class MutualViewModel(
|
||||
/**
|
||||
* Pre-fill the match page for a target grid picked from the map's grid-QSO
|
||||
* dialog ("Match" button): set the opposite-station grid (with its
|
||||
* coordinates), reset the time range to 24h, flag the page to scroll
|
||||
* straight to the time-range card, and start the query immediately so
|
||||
* results are ready when the page opens.
|
||||
* coordinates), reset the time range to 24h, start the query immediately
|
||||
* so results are ready when the page opens, and flag the page to scroll
|
||||
* straight to the time-range card once it is laid out.
|
||||
*/
|
||||
fun prefillMatchFromGrid(grid: String) {
|
||||
val g = grid.trim().uppercase()
|
||||
val pos = qthToPosition(g)
|
||||
android.util.Log.d(TAG, "prefillMatchFromGrid('$g')")
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
stationBGrid = g,
|
||||
@@ -210,10 +213,11 @@ class MutualViewModel(
|
||||
}
|
||||
|
||||
fun consumeScrollToTimeRange() {
|
||||
android.util.Log.d(TAG, "consumeScrollToTimeRange")
|
||||
_uiState.update { it.copy(scrollToTimeRange = false) }
|
||||
}
|
||||
|
||||
fun queryMutualPasses() {
|
||||
fun queryMutualPasses(initialScrollIndex: Int = 0) {
|
||||
val state = _uiState.value
|
||||
|
||||
// Resolve positions from lat/lon or grid
|
||||
@@ -241,8 +245,10 @@ class MutualViewModel(
|
||||
)
|
||||
}
|
||||
// The results list is about to be replaced, so the scroll position must
|
||||
// not leak from the previous query's list.
|
||||
listScrollIndex = 0
|
||||
// not leak from the previous query's list. A prefill from the map
|
||||
// (grid-QSO dialog "Match" button) instead starts at the time-range
|
||||
// card so the page opens with it at the top.
|
||||
listScrollIndex = initialScrollIndex
|
||||
listScrollOffset = 0
|
||||
queryGeneration += 1
|
||||
|
||||
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
package com.rtbishop.look4sat.feature.mutual
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import com.rtbishop.look4sat.core.presentation.Screen
|
||||
import kotlinx.coroutines.delay
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.annotation.GraphicsMode
|
||||
|
||||
/**
|
||||
* Verifies the map grid-QSO dialog "Match" prefill: after
|
||||
* [MutualViewModel.prefillMatchFromGrid] the page opens with the time-range
|
||||
* card at the top and the 24h query auto-started.
|
||||
*
|
||||
* Note: `LazyListState(firstVisibleItemIndex = 1)` is deliberately NOT used to
|
||||
* position the page. In this Compose version (BOM 2026.06.01 / ui 1.11.x) the
|
||||
* constructor parameter is ignored — the list still starts at item 0 — while
|
||||
* `listState.scrollToItem(1)` from a LaunchedEffect lands correctly. See
|
||||
* [scrollToItemLandsAtTop] below, which guards the scrollToItem mechanism.
|
||||
*
|
||||
* Robolectric quirk: `assertIsNotDisplayed()` is unreliable here (a node
|
||||
* scrolled fully out of the viewport can still report "displayed"), so the
|
||||
* prefill test asserts the time-range card IS displayed (positive assertion)
|
||||
* rather than that the station card is NOT.
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
@Config(sdk = [34], qualifiers = "w411dp-h891dp-port")
|
||||
class MutualMatchPrefillScrollTest {
|
||||
|
||||
@get:Rule
|
||||
val composeRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun sanity_textRenders() {
|
||||
composeRule.setContent {
|
||||
MaterialTheme {
|
||||
Text("sanity-check-text")
|
||||
}
|
||||
}
|
||||
composeRule.onNodeWithText("sanity-check-text").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun scrollToItemLandsAtTop() {
|
||||
// Guards the mechanism the prefill relies on: scrollToItem(1) from a
|
||||
// LaunchedEffect moves item 1 to the top of the viewport.
|
||||
composeRule.setContent {
|
||||
MaterialTheme {
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
val state = remember { LazyListState(firstVisibleItemIndex = 0) }
|
||||
LaunchedEffect(Unit) { state.scrollToItem(1) }
|
||||
LazyColumn(state = state) {
|
||||
item { Text("min-item-0") }
|
||||
item { Text("min-item-1") }
|
||||
item { Text("min-item-2") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
composeRule.waitForIdle()
|
||||
composeRule.onNodeWithText("min-item-1").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun defaultOpen_showsStationInputsAtTop() {
|
||||
val vm = MutualViewModel(FakeSatelliteRepo(), FakeSettingsRepo())
|
||||
composeRule.setContent {
|
||||
MaterialTheme {
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
MutualScreen(viewModel = vm)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Without a prefill the list starts at item 0 (station inputs).
|
||||
composeRule.onNodeWithText("Your Station").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun prefillFromMap_opensAtTimeRangeCard() {
|
||||
val vm = MutualViewModel(FakeSatelliteRepo(), FakeSettingsRepo())
|
||||
// Same call the map grid-QSO "Match" button makes.
|
||||
vm.prefillMatchFromGrid("OL62")
|
||||
|
||||
composeRule.setContent {
|
||||
MaterialTheme {
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
MutualScreen(viewModel = vm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composeRule.waitForIdle()
|
||||
|
||||
// The time-range card must be visible at the top of the page after the
|
||||
// prefill scroll.
|
||||
composeRule.onNodeWithText("Time range").assertIsDisplayed()
|
||||
// The snapshotFlow write-back stores the scrolled position in the VM.
|
||||
// It equals 1 only if scrollToItem(1) REALLY scrolled the station card
|
||||
// out. If the list content is shorter than the viewport there is no
|
||||
// scroll range, scrollToItem cannot move, and this stays 0 — that is
|
||||
// exactly the device symptom ("page stays at the top").
|
||||
org.junit.Assert.assertEquals(
|
||||
"list must have actually scrolled to item 1 (content shorter than viewport?)",
|
||||
1, vm.listScrollIndex
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
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
|
||||
// the "no satellite data" guard (rather than never being triggered),
|
||||
// proving prefillMatchFromGrid kicks off queryMutualPasses.
|
||||
assertTrue(
|
||||
"auto-query should reach the satellite guard: gridA=${s.stationAGrid} " +
|
||||
"gridB=${s.stationBGrid} err=${s.errorMessage}",
|
||||
s.errorMessage?.contains("satellite", ignoreCase = true) == true
|
||||
)
|
||||
// ...and the target grid + 24h range pre-filled.
|
||||
org.junit.Assert.assertEquals("OL62", s.stationBGrid)
|
||||
org.junit.Assert.assertEquals(24, s.hoursAhead)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun navDisplayEntryAfterPrefill_scrollsToTimeRange() {
|
||||
// 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()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun navDisplay_switchToMutualEntry_scrollsToTimeRange() {
|
||||
// Closest device path: start on the Map entry, then push the Mutual
|
||||
// entry (what the map "Match" button does). NavDisplay runs a real
|
||||
// fade transition while the Mutual screen composes.
|
||||
val vm = MutualViewModel(FakeSatelliteRepo(), FakeSettingsRepo())
|
||||
vm.prefillMatchFromGrid("OL62")
|
||||
val backStackRef = mutableStateOf<NavBackStack<NavKey>?>(null)
|
||||
composeRule.setContent {
|
||||
val backStack = rememberNavBackStack(Screen.Map)
|
||||
backStackRef.value = backStack
|
||||
MaterialTheme {
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
onBack = { backStack.removeLastOrNull() },
|
||||
entryProvider = entryProvider {
|
||||
entry<Screen.Map> { Text("MAP PAGE") }
|
||||
entry<Screen.Mutual> { MutualScreen(viewModel = vm) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
composeRule.waitForIdle()
|
||||
// Push Mutual like onMatchGrid does, so the transition composes
|
||||
// MutualScreen with scrollToTimeRange already set.
|
||||
composeRule.runOnIdle { backStackRef.value?.add(Screen.Mutual) }
|
||||
composeRule.waitForIdle()
|
||||
composeRule.onNodeWithText("Time range").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun scrollRetriesUntilContentGrows() {
|
||||
// Reproduces the real-device mechanism: the first frame's content
|
||||
// (item 0 short) is shorter than the viewport, so scrollToItem(1) has
|
||||
// no range and cannot move. Then the content grows (async results)
|
||||
// past one screen; the retry loop must land on item 1.
|
||||
composeRule.setContent {
|
||||
MaterialTheme {
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
val tall = mutableStateOf(false)
|
||||
LaunchedEffect(Unit) { delay(300); tall.value = true }
|
||||
val state = remember { LazyListState() }
|
||||
LaunchedEffect(state) {
|
||||
// Same retry loop MutualScreen uses for the prefill.
|
||||
for (attempt in 0 until 20) {
|
||||
state.scrollToItem(1)
|
||||
if (state.firstVisibleItemIndex == 1) return@LaunchedEffect
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
LazyColumn(state = state) {
|
||||
item {
|
||||
if (tall.value) Spacer(modifier = Modifier.height(900.dp))
|
||||
else Spacer(modifier = Modifier.height(40.dp))
|
||||
}
|
||||
item { Text("target-item") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
composeRule.waitForIdle()
|
||||
composeRule.onNodeWithText("target-item").assertIsDisplayed()
|
||||
// Confirm the list really scrolled (station card equivalent is gone).
|
||||
// We can't read listState from here, so use the VM-style probe: none —
|
||||
// the target visible + viewport tall enough implies item 1 on top.
|
||||
}
|
||||
|
||||
@Test
|
||||
fun keepPositionAcrossTabs_thenMapMatchPrefill_scrollsToTimeRange() {
|
||||
// User hypothesis: the "keep scroll position across tab switches"
|
||||
// machinery (listState remember(queryGeneration) + snapshotFlow
|
||||
// write-back) interferes with the prefill auto-scroll. Reproduce the
|
||||
// full journey: first visit -> scroll a bit -> leave -> return
|
||||
// (position restored) -> leave -> enter via map Match button.
|
||||
val vm = MutualViewModel(FakeSatelliteRepo(), FakeSettingsRepo())
|
||||
val showMutual = mutableStateOf(true)
|
||||
|
||||
// First visit (e.g. bottom nav), user scrolls a little; the
|
||||
// snapshotFlow write-back stored index/offset in the VM.
|
||||
composeRule.setContent {
|
||||
MaterialTheme {
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
if (showMutual.value) MutualScreen(viewModel = vm)
|
||||
}
|
||||
}
|
||||
}
|
||||
composeRule.waitForIdle()
|
||||
vm.listScrollIndex = 0
|
||||
vm.listScrollOffset = 40
|
||||
|
||||
// Leave the page (tab switch destroys the composition).
|
||||
showMutual.value = false
|
||||
composeRule.waitForIdle()
|
||||
|
||||
// Re-enter: keep-position restores the scroll offset.
|
||||
showMutual.value = true
|
||||
composeRule.waitForIdle()
|
||||
|
||||
// Leave again, then enter via the map grid-QSO Match button.
|
||||
showMutual.value = false
|
||||
composeRule.waitForIdle()
|
||||
vm.prefillMatchFromGrid("OL62")
|
||||
showMutual.value = true
|
||||
composeRule.waitForIdle()
|
||||
|
||||
composeRule.onNodeWithText("Time range").assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
@@ -30,12 +30,15 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
@@ -145,11 +148,16 @@ private fun PassesScreen(
|
||||
onAccept = dismiss,
|
||||
titleFontSize = 18
|
||||
) { padding ->
|
||||
// The changelog is long; keep the dialog frame fixed and let the
|
||||
// text scroll inside it (matches the update-checker page pattern).
|
||||
Text(
|
||||
text = stringResource(R.string.pass_whatsnew_message),
|
||||
fontSize = 16.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.padding(horizontal = padding)
|
||||
modifier = Modifier
|
||||
.padding(horizontal = padding)
|
||||
.heightIn(max = 360.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[versions]
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
appVersionCode = "486"
|
||||
appVersionCode = "492"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
appVersionName = "4.4.7-ba7opf.12"
|
||||
appVersionName = "4.4.7-ba7opf.12.6"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
compileSdk = "37"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
@@ -33,6 +33,8 @@ other-okhttp = "5.4.0"
|
||||
other-osmdroid = "6.1.20"
|
||||
|
||||
test-junit4 = "4.13.2"
|
||||
robolectric = "4.16"
|
||||
androidx-test-core = "1.7.0"
|
||||
|
||||
androidTest-junit = "1.3.0"
|
||||
androidTest-espresso = "3.7.0"
|
||||
@@ -79,6 +81,9 @@ other-osmdroid = { module = "org.osmdroid:osmdroid-android", version.ref = "othe
|
||||
|
||||
test-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlin-coroutines" }
|
||||
test-junit4 = { module = "junit:junit", version.ref = "test-junit4" }
|
||||
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
|
||||
androidx-test-core = { module = "androidx.test:core-ktx", version.ref = "androidx-test-core" }
|
||||
compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
# Real org.json for unit tests (android.jar stub throws "not mocked")
|
||||
test-json = { module = "org.json:json", version = "20240303" }
|
||||
|
||||
|
||||
Reference in new issue
Block a user