mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-11-08 22:38:36 +01:00
fix: apply beacon switches at runtime
This commit is contained in:
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
|
||||
|
||||
+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,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()
|
||||
|
||||
Reference in new issue
Block a user