mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-11-09 14:48:37 +01:00
Apply live beacon settings and improve station map (#164)
Apply beacon checkbox changes at runtime and keep the aggregate status chip synchronized. Improve explicit station-map navigation, spatial filter overlays, overlay legend, selected-path contrast, and the documented map screenshot.
This commit is contained in:
1 parent
4d96822c0c
commit
d925c83412
61 files changed
+2686
-299
No files matched your search
@@ -35,7 +35,9 @@ class SettingsFieldCoverageTest {
|
||||
""
|
||||
}
|
||||
|
||||
val missing = expected.filterNot { composeSources.contains("$it(") }
|
||||
val missing = expected.filterNot {
|
||||
composeSources.contains("$it(") || composeSources.contains("::$it")
|
||||
}
|
||||
|
||||
assertTrue(missing.isEmpty(),
|
||||
"settings not written by any Compose source (${missing.size} of ${expected.size}):\n" +
|
||||
|
||||
@@ -70,12 +70,27 @@ class StationSelectionStateTest {
|
||||
val state = StationSelectionState()
|
||||
state.select(station("DL1ABC", 2), StationSelectionSource.STATION_TABLE)
|
||||
val revision = state.scrollRequestRevision
|
||||
val explicitRevision = state.explicitSelectionRevision
|
||||
val refreshed = station("DL1ABC", 2)
|
||||
|
||||
state.reconcile(refreshed)
|
||||
|
||||
assertSame(refreshed, state.selected)
|
||||
assertEquals(revision, state.scrollRequestRevision)
|
||||
assertEquals(explicitRevision, state.explicitSelectionRevision)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `roster reconciliation does not publish explicit navigation`() {
|
||||
val state = StationSelectionState()
|
||||
val requests = mutableListOf<Pair<ChatMember?, StationSelectionSource>>()
|
||||
state.explicitSelectionListener = { member, source -> requests += member to source }
|
||||
state.select(station("DL1ABC", 2), StationSelectionSource.PUBLIC_MESSAGE)
|
||||
|
||||
state.reconcile(station("DL1ABC", 2))
|
||||
|
||||
assertEquals(1, requests.size)
|
||||
assertEquals(StationSelectionSource.PUBLIC_MESSAGE, requests.single().second)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package kst4contest.view.compose
|
||||
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertHasClickAction
|
||||
import androidx.compose.ui.test.assertHasNoClickAction
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.runComposeUiTest
|
||||
import kst4contest.model.ThreadStateMessage
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
class StatusBarScreenTest {
|
||||
|
||||
@Test
|
||||
fun onlyAThreadWithARegisteredActionIsClickable() = runComposeUiTest {
|
||||
val buttons = ThreadStatusButtons()
|
||||
var beaconClicks = 0
|
||||
buttons.registerAction("MyBeacon") { beaconClicks++ }
|
||||
buttons.update("MyBeacon", status("off"))
|
||||
buttons.update("Cluster", status("on"))
|
||||
|
||||
setContent {
|
||||
Kst4ContestTheme(darkMode = false, baseFontSizeSp = 12f) {
|
||||
StatusBar(
|
||||
connectionState = null,
|
||||
connectionDetail = null,
|
||||
skedNotice = BlinkingNotice(),
|
||||
bandUpgradeNotice = BlinkingNotice(),
|
||||
threadButtons = buttons,
|
||||
showConnectionBadge = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
waitForIdle()
|
||||
|
||||
onNodeWithText("MyBeacon: off").assertHasClickAction().performClick()
|
||||
waitForIdle()
|
||||
assertEquals(1, beaconClicks)
|
||||
onNodeWithText("Cluster: on").assertHasNoClickAction()
|
||||
}
|
||||
|
||||
private fun status(description: String): ThreadStateMessage =
|
||||
ThreadStateMessage("worker", true, description, false).apply {
|
||||
runningInformationTextDescription = description
|
||||
}
|
||||
}
|
||||
@@ -407,6 +407,52 @@ class StatusIndicatorStateTest {
|
||||
assertEquals("Cluster: b", button.label)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an action registered before the first report reaches the created button`() {
|
||||
val buttons = ThreadStatusButtons()
|
||||
var clicks = 0
|
||||
buttons.registerAction("MyBeacon") { clicks++ }
|
||||
|
||||
buttons.update("MyBeacon", threadState(nick = "beacon", description = "off", info = "disabled"))
|
||||
buttons.buttons.single().activate()
|
||||
|
||||
assertEquals(1, clicks)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an action registered after a report reaches the existing button`() {
|
||||
val buttons = ThreadStatusButtons()
|
||||
var clicks = 0
|
||||
buttons.update("MyBeacon", threadState(nick = "beacon", description = "off", info = "disabled"))
|
||||
|
||||
buttons.registerAction("MyBeacon") { clicks++ }
|
||||
buttons.buttons.single().activate()
|
||||
|
||||
assertEquals(1, clicks)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a thread without an action cannot be activated`() {
|
||||
val buttons = ThreadStatusButtons()
|
||||
buttons.update("Cluster", threadState(nick = "cluster", description = "on", info = "running"))
|
||||
|
||||
assertFalse(buttons.buttons.single().activate())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a status provider replaces a stale worker snapshot when the UI applies it`() {
|
||||
val buttons = ThreadStatusButtons()
|
||||
var current = threadState(nick = "beacon", description = "on", info = "enabled")
|
||||
buttons.registerStatusProvider("MyBeacon") { current }
|
||||
val queuedSnapshot = current
|
||||
current = threadState(nick = "beacon", description = "off", info = "disabled")
|
||||
|
||||
buttons.update("MyBeacon", queuedSnapshot)
|
||||
|
||||
assertEquals("MyBeacon: off", buttons.buttons.single().label)
|
||||
assertEquals("disabled", buttons.buttons.single().tooltip)
|
||||
}
|
||||
|
||||
private fun threadState(nick: String, description: String, info: String): ThreadStateMessage =
|
||||
ThreadStateMessage(nick, true, info, false).apply {
|
||||
setRunningInformationTextDescription(description)
|
||||
|
||||
+68
-2
@@ -49,6 +49,8 @@ class ComposeStationMapDrawingTest {
|
||||
|
||||
private val workedGridBorder = 0x000000
|
||||
|
||||
private val selectedDotBorder = 0xFF9900
|
||||
|
||||
/*
|
||||
* An empty cache and a source that always fails, so that "tiles never arrive" holds by
|
||||
* construction. The default fetcher read the user's disk cache and the real tile
|
||||
@@ -71,6 +73,10 @@ class ComposeStationMapDrawingTest {
|
||||
ownLocator6: String = "",
|
||||
workedGrossFields: Set<String> = emptySet(),
|
||||
darkMode: Boolean = true,
|
||||
selectedTarget: MapCallsignRawSnapshot? = null,
|
||||
spatialFilterOverlay: SpatialFilterOverlayState? = null,
|
||||
antennaBeamWidthDeg: Double = 0.0,
|
||||
maxQrbKm: Double = 0.0,
|
||||
): BufferedImage {
|
||||
return onEventThread {
|
||||
val scene = ImageComposeScene(canvasWidth, canvasHeight, Density(1f)) {
|
||||
@@ -82,8 +88,10 @@ class ComposeStationMapDrawingTest {
|
||||
selectedMarkerCallsign = null,
|
||||
ownLocator6 = ownLocator6,
|
||||
antennaAzimuthDeg = 90.0,
|
||||
antennaBeamWidthDeg = 0.0,
|
||||
maxQrbKm = 0.0,
|
||||
antennaBeamWidthDeg = antennaBeamWidthDeg,
|
||||
maxQrbKm = maxQrbKm,
|
||||
selectedTarget = selectedTarget,
|
||||
spatialFilterOverlay = spatialFilterOverlay,
|
||||
onMarkerClicked = null,
|
||||
workedGrossFields = workedGrossFields,
|
||||
darkMode = darkMode,
|
||||
@@ -108,6 +116,16 @@ class ComposeStationMapDrawingTest {
|
||||
return count
|
||||
}
|
||||
|
||||
private fun changedPixels(before: BufferedImage, after: BufferedImage): Int {
|
||||
var changed = 0
|
||||
for (x in 0 until before.width) {
|
||||
for (y in 0 until before.height) {
|
||||
if (before.getRGB(x, y) != after.getRGB(x, y)) changed++
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
/** The widest horizontal run of a colour, which for a ring is its outer diameter. */
|
||||
private fun widestRun(image: BufferedImage, rgb: Int): Int {
|
||||
var widest = 0
|
||||
@@ -282,6 +300,54 @@ class ComposeStationMapDrawingTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `active spatial filters add a distinct overlay in both themes`() {
|
||||
val overlay = SpatialFilterOverlayState(
|
||||
qtfEnabled = true,
|
||||
qtfDegrees = 90.0,
|
||||
beamWidthDeg = 60.0,
|
||||
minQrbEnabled = true,
|
||||
minQrbKm = 50.0,
|
||||
maxQrbEnabled = true,
|
||||
maxQrbKm = 400.0,
|
||||
)
|
||||
|
||||
for (darkMode in listOf(false, true)) {
|
||||
val without = render(6f, markers = emptyList(), ownLocator6 = "JO50JP", darkMode = darkMode)
|
||||
val with = render(
|
||||
6f,
|
||||
markers = emptyList(),
|
||||
ownLocator6 = "JO50JP",
|
||||
darkMode = darkMode,
|
||||
spatialFilterOverlay = overlay,
|
||||
)
|
||||
|
||||
assertTrue(changedPixels(without, with) > 1_000) {
|
||||
"the spatial filter overlay was not visible in darkMode=$darkMode"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a filtered selected target is drawn without joining the normal marker list`() {
|
||||
val targetLocation = kst4contest.locatorUtils.Location("JO51AA")
|
||||
val target = station(
|
||||
"DL1HIDE",
|
||||
"JO51AA",
|
||||
targetLocation.latitude.toDegrees(),
|
||||
targetLocation.longitude.toDegrees(),
|
||||
)
|
||||
|
||||
val image = render(
|
||||
6f,
|
||||
markers = emptyList(),
|
||||
ownLocator6 = "JO50JP",
|
||||
selectedTarget = target,
|
||||
)
|
||||
|
||||
assertTrue(countColour(image, selectedDotBorder) > 0, "the separate selected marker is missing")
|
||||
}
|
||||
|
||||
/**
|
||||
* Every grid line has to be drawn once.
|
||||
*
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package kst4contest.view.compose.map
|
||||
|
||||
import kst4contest.model.ChatPreferences
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import java.nio.file.Path
|
||||
|
||||
class MapDisplaySettingsStateTest {
|
||||
|
||||
@TempDir
|
||||
lateinit var directory: Path
|
||||
|
||||
@Test
|
||||
fun `refresh publishes applied beam geometry from preferences`() {
|
||||
val preferences = ChatPreferences(directory.resolve("preferences.xml")).apply {
|
||||
stn_antennaBeamWidthDeg = 50.0
|
||||
stn_maxQRBDefault = 750.0
|
||||
}
|
||||
val state = MapDisplaySettingsState(preferences)
|
||||
|
||||
preferences.stn_antennaBeamWidthDeg = 72.0
|
||||
preferences.stn_maxQRBDefault = 1_100.0
|
||||
state.refreshFromPreferences()
|
||||
|
||||
assertEquals(72.0, state.antennaBeamWidthDeg)
|
||||
assertEquals(1_100.0, state.maxQrbKm)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package kst4contest.view.compose.map
|
||||
|
||||
import kst4contest.view.compose.StationIdentity
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class MapNavigationStateTest {
|
||||
|
||||
@Test
|
||||
fun `repeated station choices remain observable`() {
|
||||
val state = MapNavigationState()
|
||||
val identity = StationIdentity("DL1ABC", 2)
|
||||
|
||||
state.fitSelectedPath(identity)
|
||||
val firstRevision = state.requestRevision
|
||||
state.fitSelectedPath(identity)
|
||||
|
||||
assertTrue(state.requestRevision > firstRevision)
|
||||
assertEquals(MapNavigationRequest.FitSelectedPath(identity), state.request)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `latest explicit action replaces an older one`() {
|
||||
val state = MapNavigationState()
|
||||
|
||||
state.fitSelectedPath(StationIdentity("DL1ABC", 2))
|
||||
state.centerOnQtf(45.0)
|
||||
|
||||
assertEquals(MapNavigationRequest.CenterOnQtf(45.0), state.request)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invalid QTF values do not create requests`() {
|
||||
val state = MapNavigationState()
|
||||
|
||||
listOf(Double.NaN, Double.POSITIVE_INFINITY, -1.0, 360.1).forEach(state::centerOnQtf)
|
||||
|
||||
assertEquals(0L, state.requestRevision)
|
||||
assertEquals(null, state.request)
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package kst4contest.view.compose.map
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.runComposeUiTest
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kst4contest.view.compose.Kst4ContestTheme
|
||||
import kst4contest.view.i18n.LanguageStore
|
||||
import kst4contest.view.i18n.LocalStrings
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.Locale
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
class MapOverlayLegendScreenTest {
|
||||
|
||||
@Test
|
||||
fun germanEveningLegendWrapsBothItemsInsideANarrowHeader() = runComposeUiTest {
|
||||
var storedLanguage = "de"
|
||||
val languages = LanguageStore(
|
||||
storedLanguage = { storedLanguage },
|
||||
storeLanguage = { storedLanguage = it },
|
||||
systemDefault = Locale.GERMANY,
|
||||
)
|
||||
|
||||
try {
|
||||
setContent {
|
||||
CompositionLocalProvider(LocalStrings provides languages.strings) {
|
||||
Kst4ContestTheme(darkMode = true, baseFontSizeSp = 12f) {
|
||||
Box(Modifier.width(110.dp)) {
|
||||
MapOverlayLegend(
|
||||
showAntenna = true,
|
||||
showSpatialFilters = true,
|
||||
darkMode = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onNodeWithText("Antenne").assertIsDisplayed()
|
||||
onNodeWithText("Aktive Filter").assertIsDisplayed()
|
||||
|
||||
val antenna = onNodeWithTag(MAP_ANTENNA_LEGEND_TAG).fetchSemanticsNode().boundsInRoot
|
||||
val filters = onNodeWithTag(MAP_FILTER_LEGEND_TAG).fetchSemanticsNode().boundsInRoot
|
||||
val widthLimit = with(density) { 110.dp.toPx() }
|
||||
assertTrue(antenna.right <= widthLimit + 0.5f)
|
||||
assertTrue(filters.right <= widthLimit + 0.5f)
|
||||
assertTrue(filters.top >= antenna.bottom, "the legend items did not wrap at narrow width")
|
||||
} finally {
|
||||
runOnIdle { languages.use("en") }
|
||||
}
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package kst4contest.view.compose.map
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class SpatialFilterOverlayStateTest {
|
||||
|
||||
@Test
|
||||
fun `maximum QRB is inclusive and minimum QRB is exclusive`() {
|
||||
val state = overlay(minEnabled = true, minKm = 200.0, maxEnabled = true, maxKm = 500.0)
|
||||
|
||||
assertFalse(state.accepts(200.0, 90.0))
|
||||
assertTrue(state.accepts(200.1, 90.0))
|
||||
assertTrue(state.accepts(500.0, 90.0))
|
||||
assertFalse(state.accepts(500.1, 90.0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `QTF uses the same wrapped beamwidth semantics as the station filter`() {
|
||||
val state = overlay(qtf = true, direction = 0.0, beamWidth = 60.0)
|
||||
|
||||
assertTrue(state.accepts(300.0, 350.0))
|
||||
assertTrue(state.accepts(300.0, 30.0))
|
||||
assertFalse(state.accepts(300.0, 31.0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unusual full-circle beamwidth retains the existing station filter semantics`() {
|
||||
val state = overlay(qtf = true, direction = 0.0, beamWidth = 360.0)
|
||||
|
||||
assertTrue(state.accepts(300.0, 180.0))
|
||||
assertFalse(state.accepts(300.0, 0.0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `minimum above maximum is an empty distance range`() {
|
||||
val state = overlay(minEnabled = true, minKm = 500.0, maxEnabled = true, maxKm = 200.0)
|
||||
|
||||
assertTrue(state.hasEmptyDistanceRange)
|
||||
assertFalse(state.accepts(100.0, 0.0))
|
||||
assertFalse(state.accepts(300.0, 0.0))
|
||||
assertFalse(state.accepts(600.0, 0.0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `non-finite active values cannot be rendered as a misleading area`() {
|
||||
assertFalse(overlay(qtf = true, direction = Double.NaN).hasRenderableValues)
|
||||
assertFalse(overlay(minEnabled = true, minKm = Double.POSITIVE_INFINITY).hasRenderableValues)
|
||||
assertFalse(overlay(maxEnabled = true, maxKm = Double.NaN).hasRenderableValues)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `zero distance boundaries report only actually drawable geometry`() {
|
||||
assertFalse(overlay(maxEnabled = true, maxKm = 0.0).hasDrawableGeometry)
|
||||
assertFalse(overlay(qtf = true, maxEnabled = true, maxKm = 0.0).hasDrawableGeometry)
|
||||
assertFalse(
|
||||
overlay(minEnabled = true, minKm = 0.0, maxEnabled = true, maxKm = 0.0).hasDrawableGeometry
|
||||
)
|
||||
assertTrue(overlay(minEnabled = true, minKm = 0.0).hasDrawableGeometry)
|
||||
assertTrue(overlay(qtf = true).hasDrawableGeometry)
|
||||
assertTrue(overlay(minEnabled = true, minKm = 100.0, maxEnabled = true, maxKm = 0.0).hasDrawableGeometry)
|
||||
assertTrue(overlay(maxEnabled = true, maxKm = 500.0).hasDrawableGeometry)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `no spatial filter accepts finite positions`() {
|
||||
val state = overlay()
|
||||
|
||||
assertFalse(state.isActive)
|
||||
assertTrue(state.accepts(1000.0, 275.0))
|
||||
}
|
||||
|
||||
private fun overlay(
|
||||
qtf: Boolean = false,
|
||||
direction: Double = 0.0,
|
||||
beamWidth: Double = 60.0,
|
||||
minEnabled: Boolean = false,
|
||||
minKm: Double = 0.0,
|
||||
maxEnabled: Boolean = false,
|
||||
maxKm: Double = 0.0,
|
||||
) = SpatialFilterOverlayState(
|
||||
qtfEnabled = qtf,
|
||||
qtfDegrees = direction,
|
||||
beamWidthDeg = beamWidth,
|
||||
minQrbEnabled = minEnabled,
|
||||
minQrbKm = minKm,
|
||||
maxQrbEnabled = maxEnabled,
|
||||
maxQrbKm = maxKm,
|
||||
)
|
||||
}
|
||||
+187
-46
@@ -4,7 +4,6 @@ import androidx.compose.ui.test.ComposeUiTest
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertIsOff
|
||||
import androidx.compose.ui.test.assertIsOn
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.click
|
||||
import androidx.compose.ui.test.isToggleable
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
@@ -42,7 +41,6 @@ import kst4contest.view.compose.STATION_CONTEXT_TARGET_CHOOSER_TAG
|
||||
import kst4contest.view.compose.ThreadStatusButtons
|
||||
import kst4contest.view.compose.TimelineState
|
||||
import kst4contest.view.compose.TopPriorityState
|
||||
import kst4contest.view.map.StationMapClusterer
|
||||
import kst4contest.view.map.MapCallsignRawSnapshot
|
||||
import kst4contest.view.map.PathAnalysisResult
|
||||
import kst4contest.view.map.TileFetcher
|
||||
@@ -231,7 +229,10 @@ class StationMapWindowScreenTest {
|
||||
val dl1abc = station("DL1ABC", 2)
|
||||
state.selectedStation.select(dl1abc)
|
||||
val selectorCalls = mutableListOf<ChatMember?>()
|
||||
state.stationSelector = { member, _ -> selectorCalls += member }
|
||||
state.stationSelector = { member, source ->
|
||||
selectorCalls += member
|
||||
state.selectedStation.select(member, source)
|
||||
}
|
||||
val mapState = mapLookingAt("JO50JP", zoom = 15f)
|
||||
|
||||
show(state, listOf(dl1abc), mapState)
|
||||
@@ -240,11 +241,7 @@ class StationMapWindowScreenTest {
|
||||
waitForIdle()
|
||||
|
||||
assertEquals(listOf<ChatMember?>(null), selectorCalls, "reset did not go through the station selector")
|
||||
assertSame(
|
||||
dl1abc,
|
||||
state.selectedStation.selected,
|
||||
"reset cleared the panel's selection behind the selector's back",
|
||||
)
|
||||
assertEquals(null, state.selectedStation.selected)
|
||||
assertEquals(15f, mapState.zoom, "reset changed the zoom")
|
||||
|
||||
val home = Location(ownLocator)
|
||||
@@ -369,12 +366,9 @@ class StationMapWindowScreenTest {
|
||||
assertEquals(readsWhileEnabled, reads, "disabled Grid color still read worked fields")
|
||||
}
|
||||
|
||||
/**
|
||||
* The cc8c9020 rule: selecting a station pans the map to it and leaves the zoom alone.
|
||||
* The earlier Compose version recomputed the zoom here and threw zoom 15 out to about 7.
|
||||
*/
|
||||
/** An explicit selection fits both ends and still starts the existing path analysis. */
|
||||
@Test
|
||||
fun selectingAStationPansToItAndKeepsTheZoom() = runComposeUiTest {
|
||||
fun selectingAStationFitsTheCompletePath() = runComposeUiTest {
|
||||
val state = windowState(prefs())
|
||||
val dl1abc = station("DL1ABC", 2, locator = "JO50JP")
|
||||
val analysis = RecordingPathAnalysis()
|
||||
@@ -382,53 +376,49 @@ class StationMapWindowScreenTest {
|
||||
|
||||
show(state, listOf(dl1abc), mapState, analysis)
|
||||
|
||||
runOnIdle { state.selectedStation.select(dl1abc) }
|
||||
runOnIdle { state.selectStation(dl1abc, StationSelectionSource.STATION_TABLE) }
|
||||
waitForIdle()
|
||||
|
||||
val home = Location(ownLocator)
|
||||
val target = Location("JO50JP")
|
||||
assertEquals(target.longitude.toDegrees(), mapState.centerLon, 1e-9, "the map did not pan")
|
||||
assertEquals(target.latitude.toDegrees(), mapState.centerLat, 1e-9, "the map did not pan")
|
||||
assertEquals(15f, mapState.zoom, "selecting a station changed the zoom")
|
||||
assertTrue(mapState.centerLon in home.longitude.toDegrees()..target.longitude.toDegrees())
|
||||
assertTrue(mapState.centerLat in home.latitude.toDegrees()..target.latitude.toDegrees())
|
||||
assertTrue(mapState.zoom < 15f, "a path too long for zoom 15 was not fitted")
|
||||
assertEquals(listOf(dl1abc), analysis.requested, "the path analysis was not asked")
|
||||
}
|
||||
|
||||
/**
|
||||
* With grouping on, a selection zooms in only as far as it takes to break the station out
|
||||
* of its bubble, and never out: StationMapClusterer.zoomToRevealStations.
|
||||
*/
|
||||
/** Grouping never forces a path which already fits to zoom in. */
|
||||
@Test
|
||||
fun withGroupingOnASelectionOnlyZoomsInFarEnoughToRevealTheStation() = runComposeUiTest {
|
||||
fun groupingDoesNotZoomInWhileFittingASelectedPath() = runComposeUiTest {
|
||||
val prefs = prefs().apply { isGUIstationMapClusteringEnabled = true }
|
||||
val state = windowState(prefs)
|
||||
val dl1abc = station("DL1ABC", 2, locator = "JO50JP")
|
||||
val far = mapLookingAt(ownLocator, zoom = 5f)
|
||||
val dl1abc = station("DL1ABC", 2, locator = "JO40HB")
|
||||
val mapState = mapLookingAt(ownLocator, zoom = 5f)
|
||||
|
||||
show(state, listOf(dl1abc), far, RecordingPathAnalysis())
|
||||
show(state, listOf(dl1abc), mapState, RecordingPathAnalysis())
|
||||
|
||||
runOnIdle { state.selectedStation.select(dl1abc) }
|
||||
runOnIdle { state.selectStation(dl1abc, StationSelectionSource.STATION_TABLE) }
|
||||
waitForIdle()
|
||||
|
||||
assertEquals(StationMapClusterer.DISABLE_AT_ZOOM.toFloat(), far.zoom)
|
||||
assertEquals(5f, mapState.zoom)
|
||||
}
|
||||
|
||||
/** Without a running application there is nothing to ask, and the map stays put. */
|
||||
/** Camera navigation is independent of the optional path-analysis service. */
|
||||
@Test
|
||||
fun withoutAPathAnalysisASelectionLeavesTheMapWhereItIs() = runComposeUiTest {
|
||||
fun withoutAPathAnalysisASelectionStillFitsThePath() = runComposeUiTest {
|
||||
val state = windowState(prefs())
|
||||
val dl1abc = station("DL1ABC", 2, locator = "JO50JP")
|
||||
val mapState = mapLookingAt(ownLocator, zoom = 15f)
|
||||
val home = Location(ownLocator)
|
||||
var asked = 0
|
||||
|
||||
show(state, listOf(dl1abc), mapState, pathAnalysis = null, onPathAnalysisAsked = { asked++ })
|
||||
|
||||
runOnIdle { state.selectedStation.select(dl1abc) }
|
||||
runOnIdle { state.selectStation(dl1abc, StationSelectionSource.STATION_TABLE) }
|
||||
waitForIdle()
|
||||
|
||||
// Proves the selection effect ran, so the unchanged map is not a vacuous pass.
|
||||
assertEquals(1, asked, "the selection effect did not ask for a path analysis")
|
||||
assertEquals(home.longitude.toDegrees(), mapState.centerLon, 1e-9)
|
||||
assertEquals(15f, mapState.zoom)
|
||||
assertTrue(mapState.zoom < 15f)
|
||||
}
|
||||
|
||||
/** The two header switches write the profile, so the map opens the same way next time. */
|
||||
@@ -477,6 +467,54 @@ class StationMapWindowScreenTest {
|
||||
assertEquals(listOf(true, false), applied)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun headerLegendFollowsDrawableAntennaAndSpatialFilterOverlays() = runComposeUiTest {
|
||||
val prefs = prefs()
|
||||
val state = windowState(prefs)
|
||||
|
||||
show(state, emptyList(), MapState())
|
||||
|
||||
onNodeWithText("Antenna").assertExists()
|
||||
onNodeWithText("Active filters").assertDoesNotExist()
|
||||
|
||||
runOnIdle {
|
||||
state.stationFilter.maxQrbKm = 666.0
|
||||
state.stationFilter.maxQrbEnabled = true
|
||||
}
|
||||
waitForIdle()
|
||||
|
||||
onNodeWithText("Antenna").assertExists()
|
||||
onNodeWithText("Active filters").assertExists()
|
||||
|
||||
runOnIdle {
|
||||
prefs.stn_antennaBeamWidthDeg = 0.0
|
||||
prefs.stn_maxQRBDefault = 0.0
|
||||
state.refreshMapDisplaySettings()
|
||||
state.stationFilter.maxQrbEnabled = false
|
||||
}
|
||||
waitForIdle()
|
||||
|
||||
onNodeWithText("Antenna").assertDoesNotExist()
|
||||
onNodeWithText("Active filters").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun invalidSpatialFilterDoesNotClaimAVisibleOverlay() = runComposeUiTest {
|
||||
val state = windowState(prefs())
|
||||
state.stationFilter.maxQrbKm = 0.0
|
||||
state.stationFilter.maxQrbEnabled = true
|
||||
|
||||
show(state, emptyList(), MapState())
|
||||
|
||||
onNodeWithText("Antenna").assertExists()
|
||||
onNodeWithText("Active filters").assertDoesNotExist()
|
||||
|
||||
runOnIdle { state.stationFilter.maxQrbKm = Double.NaN }
|
||||
waitForIdle()
|
||||
|
||||
onNodeWithText("Active filters").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resetFiltersUsesTheSharedActionAndDoesNotResetTheView() = runComposeUiTest {
|
||||
val state = windowState(prefs())
|
||||
@@ -553,13 +591,9 @@ class StationMapWindowScreenTest {
|
||||
onNodeWithText("Showing 1 visible stations | filtered view active").assertExists()
|
||||
}
|
||||
|
||||
/**
|
||||
* A selected station the filters hide has no marker, and so nothing to analyse: no
|
||||
* request goes out and the analysis does not sit at "Loading" with an empty target (#21).
|
||||
* The visible station beside it is the control: selecting it does ask and does load.
|
||||
*/
|
||||
/** A filtered-out selection remains a separate target without entering the marker count. */
|
||||
@Test
|
||||
fun aSelectedStationTheFiltersHideGetsNoPathAnalysis() = runComposeUiTest {
|
||||
fun aSelectedStationTheFiltersHideKeepsItsTargetAndPathAnalysis() = runComposeUiTest {
|
||||
val state = windowState(prefs(pathAnalysisVisible = true))
|
||||
val hidden = station("DL1HIDE", 2, locator = "JO50JP").apply { qrb = 100.0 }
|
||||
val shown = station("DL2SHOW", 2, locator = "JO62QM").apply { qrb = 800.0 }
|
||||
@@ -568,21 +602,128 @@ class StationMapWindowScreenTest {
|
||||
state.stations.rowFilter = { state.stationFilter.acceptsQrb(it.qrb) }
|
||||
val analysis = RecordingPathAnalysis()
|
||||
val mapState = mapLookingAt(ownLocator, zoom = 9f)
|
||||
val home = Location(ownLocator)
|
||||
|
||||
show(state, listOf(hidden, shown), mapState, analysis)
|
||||
|
||||
runOnIdle { state.selectedStation.select(hidden) }
|
||||
runOnIdle { state.selectStation(hidden, StationSelectionSource.PRIORITY) }
|
||||
waitForIdle()
|
||||
|
||||
assertEquals(emptyList<ChatMember>(), analysis.requested, "a hidden station was analysed")
|
||||
onAllNodesWithText("Loading", substring = true).assertCountEquals(0)
|
||||
assertEquals(home.longitude.toDegrees(), mapState.centerLon, 1e-9, "the map panned to a hidden station")
|
||||
assertEquals(listOf(hidden), analysis.requested, "the selected hidden target was not analysed")
|
||||
assertEquals(true, onAllNodesWithText("Loading", substring = true).fetchSemanticsNodes().isNotEmpty())
|
||||
onNodeWithText("Showing 1 visible stations | filtered view active", substring = true).assertExists()
|
||||
|
||||
runOnIdle { state.selectedStation.select(shown) }
|
||||
runOnIdle { state.selectStation(shown, StationSelectionSource.PRIORITY) }
|
||||
waitForIdle()
|
||||
|
||||
assertEquals(listOf(shown), analysis.requested)
|
||||
assertEquals(listOf(hidden, shown), analysis.requested)
|
||||
assertEquals(true, onAllNodesWithText("Loading", substring = true).fetchSemanticsNodes().isNotEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun filteredSelectedMarkerWinsHitTestingWhenAnotherMarkerOverlapsIt() = runComposeUiTest {
|
||||
val state = windowState(prefs())
|
||||
val hidden = station("DL1HIDE", 2, locator = ownLocator).apply { qrb = 100.0 }
|
||||
val shown = station("DL2SHOW", 2, locator = ownLocator).apply { qrb = 800.0 }
|
||||
state.stationFilter.minQrbKm = 300.0
|
||||
state.stationFilter.minQrbEnabled = true
|
||||
state.stations.rowFilter = { state.stationFilter.acceptsQrb(it.qrb) }
|
||||
val selections = mutableListOf<ChatMember?>()
|
||||
state.stationSelector = { member, source ->
|
||||
selections += member
|
||||
state.selectedStation.select(member, source)
|
||||
}
|
||||
|
||||
show(state, listOf(hidden, shown), mapLookingAt(ownLocator, zoom = 12f))
|
||||
runOnIdle { state.selectStation(hidden, StationSelectionSource.PRIORITY) }
|
||||
waitForIdle()
|
||||
selections.clear()
|
||||
|
||||
onNodeWithTag(StationMapWindow.MAP_CANVAS_TAG).performMouseInput { click() }
|
||||
waitForIdle()
|
||||
|
||||
assertEquals(listOf(hidden), selections)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rosterReconciliationDoesNotOverwriteManualCameraMovement() = runComposeUiTest {
|
||||
val state = windowState(prefs())
|
||||
val selected = station("DL1ABC", 2, locator = "JO50JP")
|
||||
val mapState = mapLookingAt(ownLocator, zoom = 10f)
|
||||
show(state, listOf(selected), mapState, RecordingPathAnalysis())
|
||||
|
||||
runOnIdle { state.selectStation(selected, StationSelectionSource.PUBLIC_MESSAGE) }
|
||||
waitForIdle()
|
||||
runOnIdle {
|
||||
mapState.centerLon = 3.0
|
||||
mapState.centerLat = 48.0
|
||||
mapState.zoom = 11f
|
||||
state.stations.replaceRows(listOf(station("DL1ABC", 2, locator = "JO50JP")))
|
||||
}
|
||||
waitForIdle()
|
||||
|
||||
assertEquals(3.0, mapState.centerLon, 1e-9)
|
||||
assertEquals(48.0, mapState.centerLat, 1e-9)
|
||||
assertEquals(11f, mapState.zoom)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun explicitQtfNavigationPreservesZoomAndFilterResetDoesNotNavigate() = runComposeUiTest {
|
||||
val state = windowState(prefs())
|
||||
val mapState = mapLookingAt(ownLocator, zoom = 9.5f)
|
||||
show(state, emptyList(), mapState)
|
||||
|
||||
runOnIdle { state.applyExplicitQtfFilterDirection(45.0) }
|
||||
waitForIdle()
|
||||
val qtfLon = mapState.centerLon
|
||||
val qtfLat = mapState.centerLat
|
||||
assertEquals(9.5f, mapState.zoom)
|
||||
assertTrue(qtfLon > Location(ownLocator).longitude.toDegrees())
|
||||
assertTrue(qtfLat > Location(ownLocator).latitude.toDegrees())
|
||||
|
||||
runOnIdle { state.resetStationFilters() }
|
||||
waitForIdle()
|
||||
assertEquals(qtfLon, mapState.centerLon, 1e-9)
|
||||
assertEquals(qtfLat, mapState.centerLat, 1e-9)
|
||||
assertEquals(9.5f, mapState.zoom)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun QtfNavigationDoesNotRequireOrChangeManualAntennaHeading() = runComposeUiTest {
|
||||
val prefs = prefs().apply {
|
||||
setStn_useQtfFilterDirectionAsAntennaHeading(false)
|
||||
actualQTF.set(123.0)
|
||||
}
|
||||
val state = windowState(prefs)
|
||||
val mapState = mapLookingAt(ownLocator, zoom = 9.5f)
|
||||
show(state, emptyList(), mapState)
|
||||
|
||||
runOnIdle { state.applyExplicitQtfFilterDirection(45.0) }
|
||||
waitForIdle()
|
||||
|
||||
assertTrue(mapState.centerLon > Location(ownLocator).longitude.toDegrees())
|
||||
assertTrue(mapState.centerLat > Location(ownLocator).latitude.toDegrees())
|
||||
assertEquals(9.5f, mapState.zoom)
|
||||
assertEquals(123.0, prefs.actualQTF.get(), 1e-9)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selectingTheSameStationAgainRepeatsPathNavigation() = runComposeUiTest {
|
||||
val state = windowState(prefs())
|
||||
val selected = station("DL1ABC", 2, locator = "JO50JP")
|
||||
val mapState = mapLookingAt(ownLocator, zoom = 10f)
|
||||
show(state, listOf(selected), mapState, RecordingPathAnalysis())
|
||||
|
||||
runOnIdle { state.selectStation(selected, StationSelectionSource.TIMELINE) }
|
||||
waitForIdle()
|
||||
val firstCentre = mapState.centerLon to mapState.centerLat
|
||||
runOnIdle {
|
||||
mapState.centerLon = 2.0
|
||||
mapState.centerLat = 45.0
|
||||
state.selectStation(selected, StationSelectionSource.TIMELINE)
|
||||
}
|
||||
waitForIdle()
|
||||
|
||||
assertEquals(firstCentre.first, mapState.centerLon, 1e-9)
|
||||
assertEquals(firstCentre.second, mapState.centerLat, 1e-9)
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ class BeaconTabStateTest(@TempDir private val tempDir: Path) {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `beacon edits wait for apply, and the timer restarts only then`() {
|
||||
fun `enable changes immediately while template and interval wait for apply`() {
|
||||
val s = Session(draft = SettingsDraft())
|
||||
s.prefs.setBcn_beaconIntervalInMinutesMainCat(5)
|
||||
s.prefs.setBcn_beaconsEnabledMainCat(false)
|
||||
@@ -51,7 +51,7 @@ class BeaconTabStateTest(@TempDir private val tempDir: Path) {
|
||||
assertNull(state.commitBeaconTextMainCat("CQ MYCALL"))
|
||||
assertNull(state.commitBeaconInterval("7"))
|
||||
|
||||
assertFalse(s.prefs.isBcn_beaconsEnabledMainCat(), "an edit took effect before Apply")
|
||||
assertTrue(s.prefs.isBcn_beaconsEnabledMainCat(), "the runtime switch did not take effect immediately")
|
||||
assertEquals(5, s.prefs.getBcn_beaconIntervalInMinutesMainCat())
|
||||
assertEquals(0, s.timerRestarts, "the timer restarted before Apply")
|
||||
assertEquals(7, state.beaconIntervalMinutes, "the tab shows the edit")
|
||||
@@ -65,6 +65,18 @@ class BeaconTabStateTest(@TempDir private val tempDir: Path) {
|
||||
assertEquals(1, s.timerRestarts)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `enable changes do not become pending draft edits`() {
|
||||
val s = Session(draft = SettingsDraft())
|
||||
s.prefs.setBcn_beaconsEnabledMainCat(false)
|
||||
val state = s.state()
|
||||
|
||||
state.bcn_beaconsEnabledMainCat = true
|
||||
|
||||
assertTrue(s.prefs.isBcn_beaconsEnabledMainCat())
|
||||
assertFalse(s.draft.hasPendingChanges)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `discarded beacon edits leave the timer and the templates alone`() {
|
||||
val s = Session(draft = SettingsDraft())
|
||||
|
||||
@@ -162,8 +162,6 @@ class DraftedSettingsTableTest {
|
||||
}, { it.prefs.getLst_txtSnipList().snapshot() }),
|
||||
|
||||
// Beacon
|
||||
flag("beacon main", { it.beacon.bcn_beaconsEnabledMainCat }) { f, v -> f.beacon.bcn_beaconsEnabledMainCat = v },
|
||||
flag("beacon second", { it.beacon.bcn_beaconsEnabledSecondCat }) { f, v -> f.beacon.bcn_beaconsEnabledSecondCat = v },
|
||||
Case("beacon text main", { it.beacon.commitBeaconTextMainCat(it.beacon.beaconTextMainCat + " x") },
|
||||
{ it.prefs.getBcn_beaconTextMainCat() }),
|
||||
Case("beacon text second", { it.beacon.commitBeaconTextSecondCat(it.beacon.beaconTextSecondCat + " x") },
|
||||
|
||||
@@ -3,6 +3,7 @@ package kst4contest.view.compose.tabs
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertIsOff
|
||||
import androidx.compose.ui.test.assertIsOn
|
||||
import androidx.compose.ui.test.assertTextEquals
|
||||
import androidx.compose.ui.test.hasSetTextAction
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
@@ -190,6 +191,19 @@ class MessagingTabsScreenTest {
|
||||
assertEquals("cq %foo%", s.prefs.bcn_beaconTextMainCat)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anExternalBeaconToggleUpdatesTheOpenSettingsTab() = runComposeUiTest {
|
||||
val s = Beacon(preferences())
|
||||
s.prefs.isBcn_beaconsEnabledMainCat = false
|
||||
showTab { BeaconTab(s.state) { s.refusals += it } }
|
||||
checkBeside("Send the beacon in this category").assertIsOff()
|
||||
|
||||
runOnIdle { s.prefs.isBcn_beaconsEnabledMainCat = true }
|
||||
waitForIdle()
|
||||
|
||||
checkBeside("Send the beacon in this category").assertIsOn()
|
||||
}
|
||||
|
||||
/*
|
||||
* The tab reads the interval from a plain preference getter, so nothing recomposes it
|
||||
* after Enter commits. Compared with that stale value, leaving the field afterwards
|
||||
|
||||
+20
-20
@@ -107,14 +107,14 @@ class SettingsApplyScreenTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anEditTakesEffectOnlyWithApplyAndClose() = runComposeUiTest {
|
||||
fun beaconEnableTakesEffectImmediatelyWhileTheIntervalWaitsForApply() = runComposeUiTest {
|
||||
val w = Window()
|
||||
show(w)
|
||||
|
||||
toggle("Send the beacon in this category")
|
||||
typeAndCommit("Interval in whole minutes", "9")
|
||||
|
||||
assertFalse(w.prefs.isBcn_beaconsEnabledMainCat, "the box took effect before Apply")
|
||||
assertTrue(w.prefs.isBcn_beaconsEnabledMainCat, "the runtime switch did not take effect immediately")
|
||||
assertEquals(5, w.prefs.bcn_beaconIntervalInMinutesMainCat)
|
||||
assertEquals(0, w.restarts, "the timer restarted before Apply")
|
||||
|
||||
@@ -178,19 +178,19 @@ class SettingsApplyScreenTest {
|
||||
fun aRefusalAppliesNothingSoDiscardIsStillPossible() = runComposeUiTest {
|
||||
val w = Window()
|
||||
show(w)
|
||||
toggle("Send the beacon in this category")
|
||||
typeAndCommit("Interval in whole minutes", "9")
|
||||
type("Server port", "23x")
|
||||
|
||||
onNodeWithText(APPLY_AND_CLOSE).performClick()
|
||||
waitForIdle()
|
||||
|
||||
assertFalse(w.closed)
|
||||
assertFalse(w.prefs.isBcn_beaconsEnabledMainCat, "the valid edit went into effect despite the refusal")
|
||||
assertEquals(5, w.prefs.bcn_beaconIntervalInMinutesMainCat, "the valid edit went into effect despite the refusal")
|
||||
assertTrue(w.draft.hasPendingChanges)
|
||||
runOnIdle { w.notices.dismiss() }
|
||||
|
||||
runOnIdle { w.draft.discard() }
|
||||
assertFalse(w.prefs.isBcn_beaconsEnabledMainCat)
|
||||
assertEquals(5, w.prefs.bcn_beaconIntervalInMinutesMainCat)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -264,7 +264,7 @@ class SettingsApplyScreenTest {
|
||||
fun theXAsksAboutPendingEditsAndApplyAppliesThem() = runComposeUiTest {
|
||||
val w = Window()
|
||||
show(w)
|
||||
toggle("Send the beacon in this category")
|
||||
typeAndCommit("Interval in whole minutes", "9")
|
||||
|
||||
runOnIdle { w.x() }
|
||||
awaitNode(inDialog(PENDING_QUESTION))
|
||||
@@ -273,7 +273,7 @@ class SettingsApplyScreenTest {
|
||||
awaitNode(inDialog("Apply")).performClick()
|
||||
awaitGone(inDialog(PENDING_QUESTION))
|
||||
|
||||
assertTrue(w.prefs.isBcn_beaconsEnabledMainCat)
|
||||
assertEquals(9, w.prefs.bcn_beaconIntervalInMinutesMainCat)
|
||||
assertTrue(w.closed)
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ class SettingsApplyScreenTest {
|
||||
awaitNode(inDialog("Discard")).performClick()
|
||||
awaitGone(inDialog(PENDING_QUESTION))
|
||||
|
||||
assertFalse(w.prefs.isBcn_beaconsEnabledMainCat)
|
||||
assertTrue(w.prefs.isBcn_beaconsEnabledMainCat, "the immediate runtime switch was unexpectedly reverted")
|
||||
assertEquals(5, w.prefs.bcn_beaconIntervalInMinutesMainCat)
|
||||
assertEquals(0, w.restarts)
|
||||
assertTrue(w.closed)
|
||||
@@ -299,7 +299,7 @@ class SettingsApplyScreenTest {
|
||||
fun goingBackKeepsTheWindowAndTheEdits() = runComposeUiTest {
|
||||
val w = Window()
|
||||
show(w)
|
||||
toggle("Send the beacon in this category")
|
||||
typeAndCommit("Interval in whole minutes", "9")
|
||||
|
||||
runOnIdle { w.x() }
|
||||
awaitNode(inDialog("Back to the settings")).performClick()
|
||||
@@ -307,7 +307,7 @@ class SettingsApplyScreenTest {
|
||||
|
||||
assertFalse(w.closed)
|
||||
assertTrue(w.draft.hasPendingChanges)
|
||||
assertFalse(w.prefs.isBcn_beaconsEnabledMainCat)
|
||||
assertEquals(5, w.prefs.bcn_beaconIntervalInMinutesMainCat)
|
||||
checkBeside("Send the beacon in this category").assertExists()
|
||||
}
|
||||
|
||||
@@ -377,13 +377,13 @@ class SettingsApplyScreenTest {
|
||||
val w = Window()
|
||||
var wentOn = false
|
||||
show(w)
|
||||
toggle("Send the beacon in this category")
|
||||
typeAndCommit("Interval in whole minutes", "9")
|
||||
|
||||
runOnIdle { w.switchOrExit { wentOn = true } }
|
||||
awaitNode(inDialog("Apply")).performClick()
|
||||
awaitGone(inDialog(PENDING_QUESTION))
|
||||
|
||||
assertTrue(w.prefs.isBcn_beaconsEnabledMainCat)
|
||||
assertEquals(9, w.prefs.bcn_beaconIntervalInMinutesMainCat)
|
||||
assertTrue(wentOn)
|
||||
assertFalse(w.closed, "the window closed itself instead of leaving that to the teardown")
|
||||
}
|
||||
@@ -393,7 +393,7 @@ class SettingsApplyScreenTest {
|
||||
val w = Window()
|
||||
var wentOn = false
|
||||
show(w)
|
||||
toggle("Send the beacon in this category")
|
||||
typeAndCommit("Interval in whole minutes", "9")
|
||||
|
||||
runOnIdle { w.switchOrExit { wentOn = true } }
|
||||
awaitNode(inDialog("Back to the settings")).performClick()
|
||||
@@ -408,14 +408,14 @@ class SettingsApplyScreenTest {
|
||||
val w = Window()
|
||||
var wentOn = false
|
||||
show(w)
|
||||
toggle("Send the beacon in this category")
|
||||
typeAndCommit("Interval in whole minutes", "9")
|
||||
|
||||
runOnIdle { w.switchOrExit { wentOn = true } }
|
||||
awaitNode(inDialog("Discard")).performClick()
|
||||
awaitGone(inDialog(PENDING_QUESTION))
|
||||
|
||||
assertTrue(wentOn)
|
||||
assertFalse(w.prefs.isBcn_beaconsEnabledMainCat)
|
||||
assertEquals(5, w.prefs.bcn_beaconIntervalInMinutesMainCat)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -423,7 +423,7 @@ class SettingsApplyScreenTest {
|
||||
val w = Window()
|
||||
var wentOn = false
|
||||
show(w)
|
||||
toggle("Send the beacon in this category")
|
||||
typeAndCommit("Interval in whole minutes", "9")
|
||||
|
||||
runOnIdle { w.switchOrExit { wentOn = true } }
|
||||
awaitNode(inDialog(PENDING_QUESTION))
|
||||
@@ -432,7 +432,7 @@ class SettingsApplyScreenTest {
|
||||
awaitGone(inDialog(PENDING_QUESTION))
|
||||
|
||||
assertFalse(wentOn, "went on over a refused entry")
|
||||
assertFalse(w.prefs.isBcn_beaconsEnabledMainCat, "applied despite the refusal")
|
||||
assertEquals(5, w.prefs.bcn_beaconIntervalInMinutesMainCat, "applied despite the refusal")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -457,7 +457,7 @@ class SettingsApplyScreenTest {
|
||||
val w = Window()
|
||||
var wentOn = false
|
||||
show(w)
|
||||
toggle("Send the beacon in this category")
|
||||
typeAndCommit("Interval in whole minutes", "9")
|
||||
|
||||
runOnIdle { w.switchOrExit { wentOn = true } }
|
||||
awaitNode(inDialog(PENDING_QUESTION))
|
||||
@@ -477,7 +477,7 @@ class SettingsApplyScreenTest {
|
||||
fun anInvalidFieldStillLetsTheXDiscard() = runComposeUiTest {
|
||||
val w = Window()
|
||||
show(w)
|
||||
toggle("Send the beacon in this category")
|
||||
typeAndCommit("Interval in whole minutes", "9")
|
||||
type("Server port", "23x")
|
||||
|
||||
runOnIdle { w.x() }
|
||||
@@ -489,7 +489,7 @@ class SettingsApplyScreenTest {
|
||||
awaitGone(inDialog(PENDING_QUESTION))
|
||||
|
||||
assertTrue(w.closed)
|
||||
assertFalse(w.prefs.isBcn_beaconsEnabledMainCat, "something was applied")
|
||||
assertEquals(5, w.prefs.bcn_beaconIntervalInMinutesMainCat, "something was applied")
|
||||
}
|
||||
|
||||
/* The same for a switch or the exit, even when only the invalid field is unsettled. */
|
||||
|
||||
@@ -20,6 +20,8 @@ class StationTabStateTest(@param:TempDir private val tempDir: Path) {
|
||||
var secondApplied: ChatCategory? = null
|
||||
var secondWasApplied = false
|
||||
val secondRotatorCalls = mutableListOf<Unit>()
|
||||
var beaconStatusRefreshes = 0
|
||||
var mapDisplayRefreshes = 0
|
||||
val draft = SettingsDraft()
|
||||
val state = StationTabState(
|
||||
prefs,
|
||||
@@ -27,6 +29,8 @@ class StationTabStateTest(@param:TempDir private val tempDir: Path) {
|
||||
{ mainApplied = it },
|
||||
{ secondApplied = it; secondWasApplied = true },
|
||||
applySecondRotator = { secondRotatorCalls.add(Unit) },
|
||||
refreshBeaconStatus = { beaconStatusRefreshes++ },
|
||||
applyMapDisplaySettings = { mapDisplayRefreshes++ },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -305,6 +309,34 @@ class StationTabStateTest(@param:TempDir private val tempDir: Path) {
|
||||
assertEquals(true, state.loginToSecondChatEnabled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `applying the second chat switch refreshes aggregate beacon status`() {
|
||||
val session = Session()
|
||||
|
||||
session.state.loginToSecondChatEnabled = true
|
||||
assertEquals(0, session.beaconStatusRefreshes)
|
||||
|
||||
session.draft.apply()
|
||||
|
||||
assertEquals(1, session.beaconStatusRefreshes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `beam width and maximum QRB refresh the map once after both are applied`() {
|
||||
val session = Session()
|
||||
|
||||
session.state.stn_antennaBeamWidthDeg = 72.0
|
||||
session.state.stn_maxQRBDefault = 1_250.0
|
||||
assertEquals(0, session.mapDisplayRefreshes)
|
||||
|
||||
val outcome = session.draft.apply()
|
||||
|
||||
assertTrue(outcome.isClean, outcome.failures.toString())
|
||||
assertEquals(72.0, session.prefs.stn_antennaBeamWidthDeg, 0.0001)
|
||||
assertEquals(1_250.0, session.prefs.stn_maxQRBDefault, 0.0001)
|
||||
assertEquals(1, session.mapDisplayRefreshes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the own height above sea level reads empty while the elevation model decides`() {
|
||||
val s = Session()
|
||||
|
||||
Reference in new issue
Block a user