fix: apply beacon switches at runtime

This commit is contained in:
Marc Froehlich committed 2026-11-04 01:09:26 +01:00
1 parent 4d96822c0c
commit b8bb86977f
30 files changed
+903 -133

No files matched your search

@@ -3278,6 +3278,16 @@ public class Kst4ContestApplication implements StatusUpdateListener, SettingsHos
}),
newJoined,
reachability);
composeMainWindowState.getThreadButtons().registerAction(
ChatController.BEACON_STATUS_SOURCE,
() -> {
chatcontroller.toggleUsableBeacons();
return kotlin.Unit.INSTANCE;
});
composeMainWindowState.getThreadButtons().registerStatusProvider(
ChatController.BEACON_STATUS_SOURCE,
chatcontroller::currentBeaconStatus);
chatcontroller.refreshBeaconStatus();
/*
* The feed, not the window construction, owns the timeline's data from here.
@@ -1,7 +1,11 @@
package kst4contest.view.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import kst4contest.observe.ObservableValue
import java.util.function.Consumer
@@ -35,3 +39,19 @@ class ObservedValue<T>(private val source: ObservableValue<T>) {
source.removeListener(follower)
}
}
/** Observes a domain value only for the lifetime of the current composition. */
@Composable
internal fun <T> ObservableValue<T>.observeAsState(): State<T?> {
val state = remember(this) { mutableStateOf(get()) }
DisposableEffect(this) {
val follower = Consumer<T> { updated -> state.value = updated }
addListener(follower)
// Close the gap between the initial read and listener registration.
state.value = get()
onDispose { removeListener(follower) }
}
return state
}
@@ -148,6 +148,7 @@ fun buildSettingsTabs(
applySecondCategory = { category -> controller.setChatCategorySecondChat(category) },
connectionState = connectionState,
applySecondRotator = { controller.reconfigureSecondRotator(); host.refreshSo2rState() },
refreshBeaconStatus = controller::refreshBeaconStatus,
)
val notificationState = NotificationTabState(
@@ -185,6 +186,8 @@ fun buildSettingsTabs(
draft,
validateBeaconTemplate = { template -> controller.validateBeaconTemplate(template) },
restartBeaconTimer = { controller.restartBeaconTimer() },
setMainBeaconEnabled = controller::setMainBeaconEnabled,
setSecondBeaconEnabled = controller::setSecondBeaconEnabled,
mainCategoryName = categoryName(controller.chatCategoryMain, kst4contest.view.i18n.CurrentStrings.get().settingsCategoryMainFallback),
secondCategoryName = categoryName(controller.chatCategorySecondChat, kst4contest.view.i18n.CurrentStrings.get().settingsCategorySecondFallback),
)
@@ -7,6 +7,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.TooltipArea
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ExperimentalLayoutApi
@@ -25,6 +26,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -39,11 +41,9 @@ import kst4contest.controller.On4KstConnectionState
* only the indicators are left here — which is also what finally makes the bar wrap
* properly on a narrow window instead of pushing the badges out of sight.
*
* Nothing in this bar is clickable. The three indicators were mouse-transparent and
* non-focusable in JavaFX and stay that way: the operator is typing into the chat, and a
* status badge must never take the keyboard. The background-thread buttons were ordinary
* focusable Buttons there, with no action attached — they are drawn as plain labels here,
* which is what they always behaved like.
* The connection and notice indicators remain non-clickable. A background-thread status is
* actionable only when the application explicitly registers an action for that source; the
* beacon uses this for its quick switch, while every other worker remains a plain label.
*/
@OptIn(ExperimentalLayoutApi::class)
@Composable
@@ -206,10 +206,16 @@ private fun ThreadStatusButtonView(button: ThreadStatusButton) {
val foreground = if (button.highlighted) scheme.onPrimary else scheme.onSurfaceVariant
TooltipArea(tooltip = { TooltipCard(button.tooltip) }) {
val actionModifier = if (button.hasAction) {
Modifier.clickable(role = Role.Button) { button.activate() }
} else {
Modifier
}
Box(
modifier = Modifier
.defaultMinSize(minHeight = 22.dp)
.background(background, NOTICE_SHAPE)
.then(actionModifier)
.padding(NOTICE_PADDING),
contentAlignment = Alignment.Center,
) {
@@ -176,20 +176,34 @@ object StatusIndicators {
class ThreadStatusButtons {
private val bySource = LinkedHashMap<String, ThreadStatusButton>()
private val actions = LinkedHashMap<String, () -> Unit>()
private val statusProviders = LinkedHashMap<String, () -> ThreadStateMessage>()
private val ordered = mutableStateListOf<ThreadStatusButton>()
val buttons: List<ThreadStatusButton> get() = ordered
fun update(sourceName: String, message: ThreadStateMessage) {
val effectiveMessage = statusProviders[sourceName]?.invoke() ?: message
val button = bySource.getOrPut(sourceName) {
ThreadStatusButton(sourceName).also { ordered.add(it) }
ThreadStatusButton(sourceName, actions[sourceName]).also { ordered.add(it) }
}
button.report(
label = "$sourceName: ${message.runningInformationTextDescription}",
tooltip = message.runningInformation.orEmpty(),
label = "$sourceName: ${effectiveMessage.runningInformationTextDescription}",
tooltip = effectiveMessage.runningInformation.orEmpty(),
)
}
/** Gives one status source an explicit action without changing any other source. */
fun registerAction(sourceName: String, action: () -> Unit) {
actions[sourceName] = action
bySource[sourceName]?.setAction(action)
}
/** Resolves a status only when its queued UI update is actually applied. */
fun registerStatusProvider(sourceName: String, provider: () -> ThreadStateMessage) {
statusProviders[sourceName] = provider
}
companion object {
/** How long the flash stays on; JavaFX used a 0.2 s PauseTransition. */
const val HIGHLIGHT_MILLIS = 200L
@@ -197,7 +211,10 @@ class ThreadStatusButtons {
}
/** One background thread's button. */
class ThreadStatusButton(val sourceName: String) {
class ThreadStatusButton(
val sourceName: String,
initialAction: (() -> Unit)? = null,
) {
var label: String by mutableStateOf(sourceName)
private set
@@ -212,6 +229,12 @@ class ThreadStatusButton(val sourceName: String) {
var revision: Int by mutableStateOf(0)
private set
var action: (() -> Unit)? by mutableStateOf(initialAction)
private set
val hasAction: Boolean
get() = action != null
internal fun report(label: String, tooltip: String) {
this.label = label
this.tooltip = tooltip
@@ -222,4 +245,15 @@ class ThreadStatusButton(val sourceName: String) {
fun clearHighlight() {
highlighted = false
}
internal fun setAction(action: () -> Unit) {
this.action = action
}
/** Runs the registered action and reports whether this status is actionable. */
fun activate(): Boolean {
val current = action ?: return false
current()
return true
}
}
@@ -4,9 +4,11 @@ import kst4contest.view.i18n.LocalStrings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kst4contest.view.compose.Form
import kst4contest.view.compose.observeAsState
/**
* The beacon tab: one CQ template per chat category and the interval they share.
@@ -18,12 +20,14 @@ import kst4contest.view.compose.Form
@Composable
fun BeaconTab(state: BeaconTabState, onRefused: (String) -> Unit) {
val strings = LocalStrings.current
val mainEnabled by state.mainBeaconEnabled.observeAsState()
val secondEnabled by state.secondBeaconEnabled.observeAsState()
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Form.section(strings.beaconSectionFor(state.mainCategoryName)) {
Form.check(strings.beaconEnabled, state.bcn_beaconsEnabledMainCat) {
Form.check(strings.beaconEnabled, mainEnabled == true) {
state.bcn_beaconsEnabledMainCat = it
}
Form.committed(
@@ -36,7 +40,7 @@ fun BeaconTab(state: BeaconTabState, onRefused: (String) -> Unit) {
}
Form.section(strings.beaconSectionFor(state.secondCategoryName)) {
Form.check(strings.beaconEnabled, state.bcn_beaconsEnabledSecondCat) {
Form.check(strings.beaconEnabled, secondEnabled == true) {
state.bcn_beaconsEnabledSecondCat = it
}
Form.committed(
@@ -12,8 +12,9 @@ import kst4contest.view.compose.setting
* The two commit methods are the ports of `applyBeaconTextSetting` and
* `applySharedBeaconInterval`. Both returned nothing and opened an alert; here they
* return the refusal text instead, so the tab decides how to show it and a test can
* read it. An accepted value is staged in the window's [SettingsDraft] and takes effect
* when the operator applies it; a refused one never gets there.
* read it. Accepted templates and intervals are staged in the window's [SettingsDraft]
* and take effect when the operator applies them. The two enable switches deliberately
* bypass the draft so the running timer sees their values immediately.
*
* @param validateBeaconTemplate throws IllegalArgumentException with the reason,
* the contract of `ChatController.validateBeaconTemplate`.
@@ -27,15 +28,20 @@ class BeaconTabState(
private val restartBeaconTimer: () -> Unit,
val mainCategoryName: String,
val secondCategoryName: String,
private val setMainBeaconEnabled: (Boolean) -> Unit = prefs::setBcn_beaconsEnabledMainCat,
private val setSecondBeaconEnabled: (Boolean) -> Unit = prefs::setBcn_beaconsEnabledSecondCat,
) {
var bcn_beaconsEnabledMainCat: Boolean by draft.setting(
{ prefs.isBcn_beaconsEnabledMainCat() },
) { prefs.setBcn_beaconsEnabledMainCat(it) }
val mainBeaconEnabled = prefs.bcnBeaconsEnabledMainCatProperty()
val secondBeaconEnabled = prefs.bcnBeaconsEnabledSecondCatProperty()
var bcn_beaconsEnabledSecondCat: Boolean by draft.setting(
{ prefs.isBcn_beaconsEnabledSecondCat() },
) { prefs.setBcn_beaconsEnabledSecondCat(it) }
var bcn_beaconsEnabledMainCat: Boolean
get() = prefs.isBcn_beaconsEnabledMainCat()
set(value) = setMainBeaconEnabled(value)
var bcn_beaconsEnabledSecondCat: Boolean
get() = prefs.isBcn_beaconsEnabledSecondCat()
set(value) = setSecondBeaconEnabled(value)
private var textMainCat: String by draft.setting(
{ prefs.getBcn_beaconTextMainCat() },
@@ -69,7 +75,7 @@ class BeaconTabState(
*
* The clamp is not cosmetic: a stored zero would otherwise be displayed as a
* valid setting while `ChatController` sends at the minimum anyway
* (ChatController:3425). The JavaFX field applied the same `Math.max` when it
* when scheduling the timer. The JavaFX field applied the same `Math.max` when it
* restored the value after a rejected entry.
*/
val beaconIntervalMinutes: Int
@@ -44,6 +44,8 @@ class StationTabState(
* TRX synch tab through [SECOND_ROTATOR], so it runs once per apply.
*/
private val applySecondRotator: () -> Unit = {},
/** Recomputes status whose available-count depends on the second-chat switch. */
private val refreshBeaconStatus: () -> Unit = {},
) {
var loginChatCategoryMain: ChatCategory?
@@ -234,7 +236,10 @@ class StationTabState(
* pickers it governs. It decides what the Connect button does, so it belongs with
* the login settings and not with the display options.
*/
var loginToSecondChatEnabled: Boolean by draft.setting({ prefs.isLoginToSecondChatEnabled() }) { prefs.setLoginToSecondChatEnabled(it) }
var loginToSecondChatEnabled: Boolean by draft.setting(
{ prefs.isLoginToSecondChatEnabled() },
effect = { refreshBeaconStatus() },
) { prefs.setLoginToSecondChatEnabled(it) }
/**
* Whether the login fields may be edited. The JavaFX window disabled callsign,
@@ -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" +
@@ -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)
@@ -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
@@ -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,7 @@ class StationTabStateTest(@param:TempDir private val tempDir: Path) {
var secondApplied: ChatCategory? = null
var secondWasApplied = false
val secondRotatorCalls = mutableListOf<Unit>()
var beaconStatusRefreshes = 0
val draft = SettingsDraft()
val state = StationTabState(
prefs,
@@ -27,6 +28,7 @@ class StationTabStateTest(@param:TempDir private val tempDir: Path) {
{ mainApplied = it },
{ secondApplied = it; secondWasApplied = true },
applySecondRotator = { secondRotatorCalls.add(Unit) },
refreshBeaconStatus = { beaconStatusRefreshes++ },
)
}
@@ -305,6 +307,18 @@ 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 `the own height above sea level reads empty while the elevation model decides`() {
val s = Session()