mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-10-26 16:15:51 +01: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>
This commit is contained in:
1 parent
2db71ee864
commit
4b10d96091
17 files changed
+2299
-22
No files matched your search
@@ -75,8 +75,9 @@ object MonitorWindow {
|
||||
}
|
||||
}
|
||||
|
||||
/** Internal rather than private so the window's body can be drawn headless in a test. */
|
||||
@Composable
|
||||
private fun MonitorContent(
|
||||
internal fun MonitorContent(
|
||||
clusterTable: DataTableState<ClusterMessage>,
|
||||
qsoTable: DataTableState<ChatMessage>,
|
||||
) {
|
||||
|
||||
@@ -26,6 +26,19 @@ object StationColumns {
|
||||
/** The heading the ten worked columns sit under. */
|
||||
const val WORKED_GROUP = "worked"
|
||||
|
||||
/**
|
||||
* The identity a station row is drawn and selected under: the callsign and the chat
|
||||
* category. The category is part of it because the same callsign can be in two chats at
|
||||
* once, and AGENTS.md makes the category part of the chat identity; keyed by callsign
|
||||
* alone, the two rows collided (fixed in 21251377).
|
||||
*
|
||||
* Kept here rather than inline at the one call site so the station list can be tested
|
||||
* with the key it really uses. The text is exactly what the inline Java expression
|
||||
* produced, including "null" for a missing callsign.
|
||||
*/
|
||||
fun rowKey(member: ChatMember): String =
|
||||
"${member.callSign}|${member.chatCategory?.categoryNumber ?: ""}"
|
||||
|
||||
fun all(
|
||||
tropoOf: (ChatMember) -> String,
|
||||
priorityScoreOf: (ChatMember) -> Double,
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -59,8 +60,6 @@ object StationMapWindow {
|
||||
heightDp = heightDp,
|
||||
onResized = onResized,
|
||||
) { _ ->
|
||||
val strings = LocalStrings.current
|
||||
|
||||
val window = this.window
|
||||
androidx.compose.runtime.LaunchedEffect(window) { window?.minimumSize = java.awt.Dimension(900, 600) }
|
||||
val tileFetcher = remember { TileFetcher() }
|
||||
@@ -74,6 +73,37 @@ object StationMapWindow {
|
||||
onDispose { tileFetcher.close() }
|
||||
}
|
||||
|
||||
Content(mainWindowState = mainWindowState, tileFetcher = tileFetcher)
|
||||
} // host.show content
|
||||
}
|
||||
|
||||
/**
|
||||
* The window's body, without the window.
|
||||
*
|
||||
* Split out of [show] so it can be drawn headless in a test, with a tile fetcher that
|
||||
* stays off the network and the user's tile cache, a roster that is not the running
|
||||
* application's, and a [MapState] the test can read. The defaults are exactly what
|
||||
* [show] used inline before the split.
|
||||
*
|
||||
* @param chatMembers the roster a marker click looks its station up in; a marker stands
|
||||
* for a raw callsign, and this is where its category variants come from
|
||||
* @param mapState where the map is looking
|
||||
* @param pathAnalysis asked each time the selection changes; null means there is no
|
||||
* running application, and then the map neither pans nor analyses, exactly as
|
||||
* when no chat controller existed before the split
|
||||
*/
|
||||
@Composable
|
||||
internal fun Content(
|
||||
mainWindowState: MainWindowState,
|
||||
tileFetcher: TileFetcher,
|
||||
chatMembers: () -> List<kst4contest.model.ChatMember> = {
|
||||
ApplicationRuntimeLauncher.getCurrent()?.chatController?.lst_chatMemberList?.snapshot().orEmpty()
|
||||
},
|
||||
mapState: MapState = remember { MapState() },
|
||||
pathAnalysis: () -> MapPathAnalysis? = ::runtimePathAnalysis,
|
||||
) {
|
||||
val strings = LocalStrings.current
|
||||
|
||||
/*
|
||||
* The fetcher is plain Java state, so nothing about it recomposes on its
|
||||
* own. Polled rather than wired through a callback: this drives one line of
|
||||
@@ -118,7 +148,9 @@ object StationMapWindow {
|
||||
}
|
||||
|
||||
var pathAnalysisResult by remember { mutableStateOf<PathAnalysisResult?>(null) }
|
||||
val mapState = remember { MapState() }
|
||||
|
||||
// Read through updated state so a caller passing a new lambda is never served a stale one.
|
||||
val currentPathAnalysis by rememberUpdatedState(pathAnalysis)
|
||||
|
||||
LaunchedEffect(selectedMember) {
|
||||
if (selectedMember == null) {
|
||||
@@ -126,8 +158,8 @@ object StationMapWindow {
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
val chatController = ApplicationRuntimeLauncher.getCurrent()?.chatController
|
||||
if (chatController != null) {
|
||||
val analysis = currentPathAnalysis()
|
||||
if (analysis != null) {
|
||||
val selectedSnapshot = markers.find { it.callSignRaw() == selectedMember.callSignRaw }
|
||||
|
||||
if (selectedSnapshot != null && selectedSnapshot.hasUsablePosition()) {
|
||||
@@ -155,13 +187,7 @@ object StationMapWindow {
|
||||
selectedSnapshot?.callSignRaw() ?: ""
|
||||
)
|
||||
|
||||
val overrideBand: kst4contest.model.Band? = null
|
||||
|
||||
chatController.reachabilityService.requestPathAnalysisForMap(
|
||||
selectedMember,
|
||||
selectedSnapshot,
|
||||
overrideBand
|
||||
) { result ->
|
||||
analysis.request(selectedMember, selectedSnapshot) { result ->
|
||||
pathAnalysisResult = result
|
||||
}
|
||||
}
|
||||
@@ -277,10 +303,8 @@ object StationMapWindow {
|
||||
maxQrbKm = maxQrbKm,
|
||||
darkMode = isDarkTheme,
|
||||
onMarkerClicked = { snapshot ->
|
||||
val chatController = ApplicationRuntimeLauncher.getCurrent()?.chatController
|
||||
val variants = chatController?.lst_chatMemberList?.snapshot()
|
||||
?.filter { it.callSignRaw == snapshot.callSignRaw() }
|
||||
.orEmpty()
|
||||
val variants = chatMembers()
|
||||
.filter { it.callSignRaw == snapshot.callSignRaw() }
|
||||
/*
|
||||
* A marker stands for a raw callsign, not a category. Keep the
|
||||
* category already selected when it is one of the variants.
|
||||
@@ -298,7 +322,7 @@ object StationMapWindow {
|
||||
mainWindowState.chatInput.prepareCq(member.callSign ?: "", forceOverwrite = true)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
modifier = Modifier.fillMaxSize().testTag(MAP_CANVAS_TAG)
|
||||
)
|
||||
|
||||
MapOverlayControls(
|
||||
@@ -361,8 +385,10 @@ object StationMapWindow {
|
||||
} // main layout column
|
||||
} // window column
|
||||
} // Surface
|
||||
} // host.show content
|
||||
}
|
||||
} // Content
|
||||
|
||||
/** Marks the map canvas so a test can click a marker on it. */
|
||||
internal const val MAP_CANVAS_TAG = "station-map-canvas"
|
||||
|
||||
@JvmStatic
|
||||
fun applyDarkMode(darkMode: Boolean) {
|
||||
@@ -397,3 +423,34 @@ object StationMapWindow {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks for the path analysis of the station selected on the map.
|
||||
*
|
||||
* The seam between the map window and the running application: the window pans to the
|
||||
* station and starts the analysis only when there is one to ask, which a test supplies
|
||||
* without building a chat controller.
|
||||
*/
|
||||
internal fun interface MapPathAnalysis {
|
||||
fun request(
|
||||
member: kst4contest.model.ChatMember,
|
||||
snapshot: kst4contest.view.map.MapCallsignRawSnapshot?,
|
||||
onResult: (PathAnalysisResult) -> Unit,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The running application's reachability service, or null before there is one.
|
||||
*
|
||||
* Looked up on every call, as the window did inline: a profile switch replaces the runtime.
|
||||
* No band override -- the map always analyses on the automatically resolved band.
|
||||
*/
|
||||
private fun runtimePathAnalysis(): MapPathAnalysis? {
|
||||
val chatController = ApplicationRuntimeLauncher.getCurrent()?.chatController ?: return null
|
||||
|
||||
return MapPathAnalysis { member, snapshot, onResult ->
|
||||
chatController.reachabilityService.requestPathAnalysisForMap(member, snapshot, null) { result ->
|
||||
onResult(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import kst4contest.model.Band
|
||||
import kst4contest.view.compose.DataTable
|
||||
import kst4contest.view.compose.Form
|
||||
@@ -134,6 +135,7 @@ fun NotificationTab(state: NotificationTabState, onRefused: (String) -> Unit) {
|
||||
onValueChange = { entered = it },
|
||||
singleLine = true,
|
||||
label = { Text(strings.notificationCallsign) },
|
||||
modifier = Modifier.testTag(MONITORED_CALLSIGN_FIELD_TAG),
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -149,3 +151,6 @@ fun NotificationTab(state: NotificationTabState, onRefused: (String) -> Unit) {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Marks the callsign field of the add-to-monitoring dialog, so a test can type into it. */
|
||||
internal const val MONITORED_CALLSIGN_FIELD_TAG = "monitored-callsign-field"
|
||||
Reference in new issue
Block a user