mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-11-14 08:32:28 +01:00
Etappen 3 to 6a of the migration (129 commits). Every window the operator sees is Compose; the JavaFX ones still exist beside them so the two could be compared against a live session, which is the only way the differences came to light. The toolchain first, then the settings window with its eleven tabs and 72 real preferences; then the multi-column table as a component, and with it the rules it needs -- stable row identity, a selection that survives a refresh, column order the operator can drag, and a repaint when the rows change rather than when the list instance does. Then the update and monitor windows, and the main window: 22 station columns, the filter bar, the selected-station panel, the message tabs, the priority bar, the sked controls, the status indicators and the timeline, assembled behind a splitter because Compose has none. The map stops being a WebView. A Compose canvas projects WebMercator tiles itself, with the clustering in core and an asynchronous fetcher in place of the local tile proxy; derive() becomes a pure Kotlin reimplementation instead of a reflection call into com.sun.javafx. Most of the volume here is parity the operator would notice rather than new behaviour: desktop density instead of Material's phone minimums, colours taken from the JavaFX stylesheets instead of Material's palette, row dividers, band columns, grid-square labels and their colours, the home marker and antenna beam, pinch-to-zoom under Wayland, and layouts that survive a window narrower than the chart they stack. Two lessons are worth more than the diff. A control bound to a value Compose cannot observe writes through and never redraws -- that one shipped repeatedly with a green build, because no test drew anything. And a layout that clips instead of wrapping is worse than one that is merely too narrow: half a word is the worst available outcome.
227 lines
6.9 KiB
Kotlin
227 lines
6.9 KiB
Kotlin
package kst4contest.view.compose
|
|
|
|
import kst4contest.model.ChatMember
|
|
import org.junit.jupiter.api.Test
|
|
import org.junit.jupiter.api.Assertions.assertEquals
|
|
import org.junit.jupiter.api.Assertions.assertFalse
|
|
import org.junit.jupiter.api.Assertions.assertNull
|
|
import org.junit.jupiter.api.Assertions.assertTrue
|
|
|
|
/**
|
|
* The panel below the station list, for the station currently selected.
|
|
*
|
|
* Its nine "tag not qrv" marks are written back to the station and propagated to the
|
|
* active members, so getting one wrong hides a station from every list that filters on
|
|
* it. The sked controls create a real reminder. Both are worth pinning.
|
|
*/
|
|
class SelectedStationStateTest {
|
|
|
|
private class Session {
|
|
val propagated = mutableListOf<String>()
|
|
var skedsCreated = 0
|
|
var lastSkedMinutes: Int? = null
|
|
var lastSkedMode: String? = null
|
|
val failMarked = mutableListOf<String>()
|
|
val failReset = mutableListOf<String>()
|
|
|
|
fun state() = SelectedStationState(
|
|
propagateNotQrv = { propagated.add(it.callSign.orEmpty()) },
|
|
createSked = { _, minutes, _, mode ->
|
|
skedsCreated++
|
|
lastSkedMinutes = minutes
|
|
lastSkedMode = mode
|
|
true
|
|
},
|
|
markSkedFail = { failMarked.add(it.callSign.orEmpty()) },
|
|
resetSkedFail = { failReset.add(it.callSign.orEmpty()) },
|
|
)
|
|
}
|
|
|
|
private fun member(call: String) = ChatMember().apply { callSign = call }
|
|
|
|
@Test
|
|
fun `without a selection there is nothing to act on`() {
|
|
val state = Session().state()
|
|
|
|
assertNull(state.selected)
|
|
assertFalse(state.canCreateSked, "a sked needs a station to be with")
|
|
assertFalse(state.canLookUp, "there is no callsign to look up")
|
|
}
|
|
|
|
@Test
|
|
fun `tagging a band writes it to the station and propagates`() {
|
|
val session = Session()
|
|
val state = session.state()
|
|
val dn9apw = member("DN9APW")
|
|
state.select(dn9apw)
|
|
|
|
state.tagNotQrv(NotQrvBand.B_144, true)
|
|
|
|
assertFalse(dn9apw.isQrv144, "not QRV means the QRV flag goes false")
|
|
assertEquals(listOf("DN9APW"), session.propagated,
|
|
"every list filtering on QRV has to learn about it")
|
|
}
|
|
|
|
@Test
|
|
fun `tag not qrv all covers every band`() {
|
|
val session = Session()
|
|
val state = session.state()
|
|
val member = member("DO5AMF")
|
|
state.select(member)
|
|
|
|
state.tagNotQrvAll(true)
|
|
|
|
NotQrvBand.entries.forEach { band ->
|
|
assertTrue(state.isTaggedNotQrv(band), "band ${band.label}")
|
|
}
|
|
assertFalse(member.isQrv50)
|
|
assertFalse(member.isQrv10G)
|
|
}
|
|
|
|
@Test
|
|
fun `clearing tag not qrv all puts every band back`() {
|
|
val state = Session().state()
|
|
val member = member("DO5AMF")
|
|
state.select(member)
|
|
state.tagNotQrvAll(true)
|
|
|
|
state.tagNotQrvAll(false)
|
|
|
|
NotQrvBand.entries.forEach { band ->
|
|
assertFalse(state.isTaggedNotQrv(band), "band ${band.label}")
|
|
}
|
|
assertTrue(member.isQrv144)
|
|
}
|
|
|
|
@Test
|
|
fun `selecting another station shows that station's tags`() {
|
|
val state = Session().state()
|
|
val tagged = member("DN9APW").apply { isQrv432 = false }
|
|
val untagged = member("DO5AMF")
|
|
|
|
state.select(tagged)
|
|
assertTrue(state.isTaggedNotQrv(NotQrvBand.B_432))
|
|
|
|
state.select(untagged)
|
|
assertFalse(state.isTaggedNotQrv(NotQrvBand.B_432),
|
|
"the panel shows the selected station, not the one before it")
|
|
}
|
|
|
|
@Test
|
|
fun `a sked needs a selection and is created once`() {
|
|
val session = Session()
|
|
val state = session.state()
|
|
state.select(member("DN9APW"))
|
|
state.skedMinutes = 10
|
|
|
|
assertTrue(state.canCreateSked)
|
|
assertTrue(state.createSked())
|
|
|
|
assertEquals(1, session.skedsCreated)
|
|
assertEquals(10, session.lastSkedMinutes)
|
|
}
|
|
|
|
@Test
|
|
fun `creating a sked without a selection does nothing`() {
|
|
val session = Session()
|
|
val state = session.state()
|
|
|
|
assertFalse(state.createSked())
|
|
assertEquals(0, session.skedsCreated)
|
|
}
|
|
|
|
@Test
|
|
fun `the message filter is one choice, not four`() {
|
|
val state = Session().state()
|
|
|
|
state.messageFilter = SelectedMessageFilter.PM_TO_ME
|
|
assertEquals(SelectedMessageFilter.PM_TO_ME, state.messageFilter)
|
|
|
|
state.messageFilter = SelectedMessageFilter.PUBLIC
|
|
assertEquals(SelectedMessageFilter.PUBLIC, state.messageFilter,
|
|
"the JavaFX radio buttons were one toggle group; two cannot be on")
|
|
}
|
|
|
|
/**
|
|
* No filter is chosen until the operator chooses one. JavaFX has the
|
|
* setSelected(true) on "pm to me" commented out, so the group starts with nothing on,
|
|
* and starting on a filter would silently hide messages the operator never asked to
|
|
* hide.
|
|
*/
|
|
@Test
|
|
fun `no message filter is chosen to begin with`() {
|
|
assertNull(Session().state().messageFilter)
|
|
}
|
|
|
|
@Test
|
|
fun `the lookup addresses carry the selected callsign`() {
|
|
val state = Session().state()
|
|
state.select(member("DN9APW"))
|
|
|
|
assertTrue(state.qrzComUrl().endsWith("DN9APW"))
|
|
assertTrue(state.qrzCqUrl().endsWith("DN9APW"))
|
|
}
|
|
|
|
@Test
|
|
fun `the sked offers the minutes the JavaFX choice offered`() {
|
|
assertEquals(
|
|
listOf(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 20),
|
|
SelectedStationState.SKED_MINUTES,
|
|
)
|
|
assertEquals(5, Session().state().skedMinutes, "the default it selected")
|
|
}
|
|
|
|
@Test
|
|
fun `the sked offers the two modes`() {
|
|
assertEquals(listOf("SSB", "CW"), SelectedStationState.SKED_MODES)
|
|
}
|
|
|
|
@Test
|
|
fun `the reminder offers the three offset patterns`() {
|
|
assertEquals(listOf("2+1", "5+2+1", "10+5+2+1"), SelectedStationState.REMINDER_OFFSETS)
|
|
}
|
|
|
|
@Test
|
|
fun `the chosen mode reaches the sked`() {
|
|
val session = Session()
|
|
val state = session.state()
|
|
state.select(member("DN9APW"))
|
|
state.skedMode = "CW"
|
|
|
|
state.createSked()
|
|
|
|
assertEquals("CW", session.lastSkedMode)
|
|
}
|
|
|
|
@Test
|
|
fun `marking a sked as failed names the selected station`() {
|
|
val session = Session()
|
|
val state = session.state()
|
|
state.select(member("DN9APW"))
|
|
|
|
state.markSkedFail()
|
|
|
|
assertEquals(listOf("DN9APW"), session.failMarked)
|
|
}
|
|
|
|
@Test
|
|
fun `resetting the failure names it too`() {
|
|
val session = Session()
|
|
val state = session.state()
|
|
state.select(member("DO5AMF"))
|
|
|
|
state.resetSkedFail()
|
|
|
|
assertEquals(listOf("DO5AMF"), session.failReset)
|
|
}
|
|
|
|
@Test
|
|
fun `marking a failure without a selection does nothing`() {
|
|
val session = Session()
|
|
|
|
session.state().markSkedFail()
|
|
|
|
assertTrue(session.failMarked.isEmpty())
|
|
}
|
|
}
|