mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-10-18 21:34:03 +02:00
* test: draw and click the in-window menu row (#107) Pins what MainMenuModelTest cannot see: every item picked through the drawn row runs its own action, the enabled states and the away and window-toggle labels follow their state while the menu is down, and a palette or language change reaches the row on screen. Named regressions:fe4215bc(menu actions hung on deleted JavaFX MenuItems) and83e23f55(unthemed Swing menu bar), both in main as2a5c89b9. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: draw the station list with its real row key (#107) The station row key moves from an inline Java lambda into StationColumns.rowKey, unchanged in its text, so the list can be tested with the key the application really uses. The screen test pins three regressions on the drawn table: the same callsign in two categories collided under one key and sorting dropped the filters (both21251377), and the AP column sorted by its text (ab282162, #87). It also pins that rows replaced after the first draw reach the screen and that the selection follows a station by key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: draw the monitor window's live tables (#107) MonitorContent becomes internal so its body can be drawn headless. The screen test pins the defects the port shipped in this window (cc8c9020, in main as2a5c89b9): rows arriving while the window is open, also after a redraw that was not about the rows, must reach both tables, and two messages that read alike stay two rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: draw and click the station map window's body (#107) StationMapWindow.show now hands its body to an internal Content composable that takes the tile fetcher, the member roster and the map state as parameters, with the old inline values as defaults, and the map canvas carries a test tag. The screen test pins the map selection fixed in21251377: a marker click and Reset view go through the central station selector, a click keeps the category variant already selected and prepares "/cq CALL", and neither changes the zoom. The header switches write the profile. Tiles never arrive: an empty temporary cache and a failing source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: draw the Station, Log synch, TRX synch and Airscout tabs (#107) Each tab is composed headless and checked for its sections and key controls, and editing a control through the drawn form must write through to ChatPreferences and redraw. Refused entries stay on screen as typed and leave the stored value alone. A small helper finds a control by the label beside it, the way the operator finds it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: draw the Message handling, GUI, Beacon and Notification tabs (#107) Each tab is composed headless with its sections and key controls, and edits through the drawn controls reach ChatPreferences and the application callbacks: the auto answer for both categories, the debug-to-file switchc8ce0e98restored, the startup design and the language, the beacon templates and interval, the cluster server, the fallback band, the test spot and the monitored callsign dialog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: draw the Shortcuts and Worked database tabs (#107) The shortcut and snippet lists show what the profile holds, and an edit or an added entry made in the drawn list reaches the roster and the refresh hook the button row depends on (the hook that was called into thin air beforefe4215bc). The worked database lists its rows, re-reads them on refresh, asks before a reset and leaves the lists alone when the database reset fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: draw the Profiles and Colours tabs (#107) Profiles: the default profile and every action are on screen, deleting it is refused after the confirmation, a cancelled New dialog creates nothing, and switching hands the selected profile to the application. Only a temporary registry is touched. Colours: the six roles show the resolved colour, a typed colour is committed into the store, a mistyped one is refused and put back, the shipped reset undoes a change, and the stylesheet export writes once into a temporary profile directory and never over an existing file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor: give the map's path analysis a seam and tag the monitoring field (#107) StationMapWindow.Content takes a MapPathAnalysis supplier. The default looks the running application's reachability service up on every selection, exactly as the inline chat-controller lookup did, and null still means: no pan, no analysis. This lets a test run the zoom rule on selection thatcc8c9020fixed. The callsign field of the add-to-monitoring dialog in NotificationTab carries a test tag, so a test types into it by name rather than by position. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: isolate and harden the screen tests after review (#107) - No screen test builds ChatPreferences() any more: that constructor resolves the real user profile and seeds preferences.xml there. Each class uses ChatPreferences(Path) under a @TempDir. - Compose state changed from the test thread (roster refresh, language reset) goes through runOnIdle. - Dialogs and dropdowns are waited for with waitUntil and found inside their overlay (isDialog / isPopup) instead of by tree order; the monitored-callsign field is found by its test tag. - The menu test now asserts Disconnect when online and Connect when offline instead of skipping one. - The map test runs the zoom-on-selection rule through the new seam, closes its tile fetcher after each test and keeps its cache in the @TempDir. - StationColumns.rowKey is pinned directly: two categories, two keys. - The menu colour is sampled further inside the row, and the station list click picks its row by position. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: prove the map's selection effect ran when no analysis is available (#107) Review follow-up. The no-analysis test only asserted that the map stayed put, which would also hold if the selection effect never ran; it now counts the analysis lookup. The effect reads the lookup through rememberUpdatedState, so a caller passing a new lambda is not served a stale one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
189 lines
7.5 KiB
Kotlin
189 lines
7.5 KiB
Kotlin
package kst4contest.view.compose
|
|
|
|
import kst4contest.model.AirPlane
|
|
import kst4contest.model.AirPlaneReflectionInfo
|
|
import kst4contest.model.ChatMember
|
|
import org.junit.jupiter.api.Test
|
|
import org.junit.jupiter.api.Assertions.assertDoesNotThrow
|
|
import org.junit.jupiter.api.Assertions.assertEquals
|
|
import org.junit.jupiter.api.Assertions.assertNotNull
|
|
import org.junit.jupiter.api.Assertions.assertNull
|
|
import org.junit.jupiter.api.Assertions.assertTrue
|
|
|
|
/**
|
|
* The 22 columns of the station list.
|
|
*
|
|
* The ids are the operator's stored layout — renaming one loses their column widths
|
|
* without a word — and these rows come from the network, so every value has to survive
|
|
* a half-filled station.
|
|
*
|
|
* "wkdany" sits at the END of the worked group, not the start. Pinned to the JavaFX
|
|
* table, which adds the band sub-columns under `isStn_bandActiveXX()` and only then
|
|
* `workedCol.getColumns().add(wkdAny_subcol)` (`Kst4ContestApplication.java:2496`).
|
|
*
|
|
* A band cell's text comes from the `formatBand` the caller supplies, because the
|
|
* JavaFX cell factory distinguishes worked from new from unavailable; the production
|
|
* caller passes `formatBandCellStatus` (`:10460`).
|
|
*/
|
|
class StationColumnsTest {
|
|
|
|
private fun columns() = StationColumns.all(
|
|
tropoOf = { "" },
|
|
priorityScoreOf = { 0.0 },
|
|
formatBand = { _, _, worked -> if (worked) "X" else "" },
|
|
)
|
|
|
|
@Test
|
|
fun `the ids are the ones the stored layout uses`() {
|
|
assertEquals(
|
|
listOf(
|
|
"callsign", "name", "qra", "qrb", "qtf", "qrg", "airscout", "tropo",
|
|
"score", "activity", "band-50", "band-70", "band-144", "band-432",
|
|
"band-1296", "band-2320", "band-3400", "band-5760", "band-10g",
|
|
"worked-any", "not-qrv", "category",
|
|
),
|
|
columns().map { it.id },
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `the headings are the ones the operator reads`() {
|
|
assertEquals(
|
|
listOf(
|
|
"Callsign", "Name", "QRA", "QRB", "QTF", "QRG", "AP [minutes / pot%]",
|
|
"Tropo", "Score", "Act", "50", "70", "144", "432", "23",
|
|
"13", "9", "6", "3", "wkdany", "NOT QRV @", "Category",
|
|
),
|
|
columns().map { it.title() },
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `the worked columns are grouped under one heading`() {
|
|
// Without the group, ten columns headed "50 70 144 432 23 13 9 6 3 wkdany"
|
|
// say nothing about what they mean. The JavaFX table put them under "worked",
|
|
// and added wkdany last.
|
|
val grouped = columns().filter { it.group == "worked" }.map { it.id }
|
|
|
|
assertEquals(
|
|
listOf("band-50", "band-70", "band-144", "band-432", "band-1296",
|
|
"band-2320", "band-3400", "band-5760", "band-10g", "worked-any"),
|
|
grouped,
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `columns outside that group carry no group`() {
|
|
assertNull(columns().first { it.id == "callsign" }.group)
|
|
assertNull(columns().first { it.id == "category" }.group)
|
|
}
|
|
|
|
@Test
|
|
fun `a half-filled station reads as empty, not as a crash`() {
|
|
val bare = ChatMember()
|
|
|
|
columns().forEach { column ->
|
|
assertDoesNotThrow({ column.value(bare) }, "column ${column.id}")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun `a band column shows a cross only when that band was worked`() {
|
|
val worked144 = ChatMember().apply { isWorked144 = true }
|
|
|
|
val band144 = columns().first { it.id == "band-144" }
|
|
val band432 = columns().first { it.id == "band-432" }
|
|
|
|
assertEquals("X", band144.value(worked144))
|
|
assertEquals("", band432.value(worked144))
|
|
}
|
|
|
|
@Test
|
|
fun `the NOT QRV column names the bands the station is not on`() {
|
|
/*
|
|
* Every QRV flag starts true — a station is assumed to be on every band until
|
|
* it says otherwise — so a fresh row leaves this column empty and it fills as
|
|
* bands are marked off. Listing the QRV bands instead would put a full column
|
|
* on almost every row and mean the opposite of its heading.
|
|
*/
|
|
val member = ChatMember().apply {
|
|
isQrv432 = false
|
|
isQrv1240 = false
|
|
}
|
|
|
|
val notQrv = columns().first { it.id == "not-qrv" }.value(member)
|
|
|
|
assertTrue(notQrv.contains("70cm"), "432 was marked off")
|
|
assertTrue(notQrv.contains("23cm"), "1240 too")
|
|
assertTrue(!notQrv.contains("2m"), "144 was never marked off, so it is not listed")
|
|
}
|
|
|
|
@Test
|
|
fun `a station on every band has an empty NOT QRV column`() {
|
|
assertEquals("", columns().first { it.id == "not-qrv" }.value(ChatMember()))
|
|
}
|
|
|
|
/**
|
|
* Issue #87: the AP column had no comparator and fell back to its text, so "13 (75%)"
|
|
* sorted before "9 (75%)". Same shape as DataTableStateTest's "a column with a
|
|
* comparator sorts by it, not by its text", with the real columns.
|
|
*/
|
|
@Test
|
|
fun `airscout sorts numerically`() {
|
|
val columns = columns()
|
|
assertNotNull(columns.first { it.id == "airscout" }.comparator, "the AP column sorts by its own key")
|
|
|
|
val table = DataTableState(columns = columns, rowKey = RowKeys.byValue<ChatMember> { it.callSign.orEmpty() })
|
|
table.replaceRows(listOf(stationWithAeroplane("DL13ABC", minutes = 13), stationWithAeroplane("DL9ABC", minutes = 9)))
|
|
|
|
table.toggleSort("airscout")
|
|
|
|
assertEquals(listOf("DL9ABC", "DL13ABC"), table.visibleRows.map { it.callSign },
|
|
"9 before 13 numerically; by text it would be the other way round")
|
|
}
|
|
|
|
private fun stationWithAeroplane(call: String, minutes: Int): ChatMember = ChatMember().apply {
|
|
setCallSign(call)
|
|
airPlaneReflectInfo = AirPlaneReflectionInfo().apply {
|
|
risingAirplanes = mutableListOf(AirPlane().apply { arrivingDurationMinutes = minutes; potential = 75 })
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun `tropo and score come from outside the station`() {
|
|
val columns = StationColumns.all(
|
|
tropoOf = { "12 dB" },
|
|
priorityScoreOf = { 250.0 },
|
|
formatBand = { _, _, _ -> "" },
|
|
)
|
|
val member = ChatMember()
|
|
|
|
assertEquals("12 dB", columns.first { it.id == "tropo" }.value(member),
|
|
"reachability is computed by the controller, not carried by the row")
|
|
assertEquals("250", columns.first { it.id == "score" }.value(member))
|
|
}
|
|
|
|
/**
|
|
* The key Kst4ContestApplication binds the station list to (#107). The category is part
|
|
* of the chat identity, so the same callsign in two categories is two keys; keyed by the
|
|
* callsign alone the two rows collided (21251377). The exact text is pinned because it
|
|
* was moved out of an inline Java lambda and must read the same as that lambda did.
|
|
*/
|
|
@Test
|
|
fun `the row key separates the categories of one callsign`() {
|
|
fun member(call: String?, category: Int?) = ChatMember().apply {
|
|
callSign = call
|
|
chatCategory = category?.let { kst4contest.model.ChatCategory(it) }
|
|
}
|
|
|
|
assertEquals("DL1ABC|2", StationColumns.rowKey(member("DL1ABC", 2)))
|
|
assertEquals("DL1ABC|3", StationColumns.rowKey(member("DL1ABC", 3)))
|
|
assertTrue(
|
|
StationColumns.rowKey(member("DL1ABC", 2)) != StationColumns.rowKey(member("DL1ABC", 3)),
|
|
"two categories of one callsign share a key",
|
|
)
|
|
assertEquals("DL1ABC|", StationColumns.rowKey(member("DL1ABC", null)))
|
|
assertEquals("null|2", StationColumns.rowKey(member(null, 2)))
|
|
}
|
|
}
|