mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-11-12 07:42:27 +01:00
Draw the menu bar in Compose, and give the drawing layer a net
What the removal left open (4 commits). Compose's MenuBar is a javax.swing.JMenuBar set on the window, so Swing draws it and no theme reaches it; with no look-and-feel configured that meant the default Metal menu above a themed window. The bar is drawn in the window now, except on macOS, where the system bar is where that platform expects it. Two renderers read one description -- mainMenuModel is a plain function of the state, which also gives the menus their first tests. Closing the main window quits the application again, and the window writes its size back to the profile; both had quietly stopped working when the last JavaFX stage went. Then Etappe 7. Four faults shipped in the settings-window branch alongside a green suite of 382 tests, because not one of those tests drew anything: a control that never redrew, a tab strip that clipped mid-word, a form taller than its window with no way down, and colours that came from Material rather than the stylesheet. One regression test per class, and the spec's acceptance criterion is met rather than assumed -- each fault was put back deliberately and the matching test watched to fail. The CI had never run the suite at all; it does now, headless. What that took was not Xvfb: skiko's native library links against libGL and only has to be loadable, since the drawing tests render in software. Without the package the whole Compose half of the suite dies with a message that reads like a missing display. Found by running the job under act rather than on GitHub. The six tests that open a real window skip headless by their own guard. The summary step counts from the result XML and fails on failures there, because this project's rule is never to infer a green suite from an exit code.
This commit is contained in:
13 files changed
+970
-782
No files matched your search
@@ -0,0 +1,207 @@
|
||||
package kst4contest.view.compose
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.hasSetTextAction
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.compose.ui.test.runComposeUiTest
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kst4contest.model.ChatPreferences
|
||||
import kst4contest.view.compose.tabs.MessageHandlingTab
|
||||
import kst4contest.view.compose.tabs.MessageHandlingTabState
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* The four ways the drawing layer went wrong while the build was green.
|
||||
*
|
||||
* Every one of these shipped in the branch at the same time as a passing suite of 382
|
||||
* tests, because not one of those tests drew anything. The state-level tests and the two
|
||||
* settings coverage nets answer "was the setter called"; these answer "did it reach the
|
||||
* screen", which is a different question and the one the operator asks.
|
||||
*
|
||||
* | Class | How it showed up |
|
||||
* |---|---|
|
||||
* | A control reads a non-observable value and never redraws | every keystroke in the settings was written through and not displayed |
|
||||
* | A layout clips instead of wrapping | the tab strip cut "Workedstn database" mid-word |
|
||||
* | Content is taller than the window and unreachable | a tab's form ended mid-way with nothing to say more was below |
|
||||
* | Colours do not come from the stylesheet | the window surface took `-fx-base` instead of `derive(-fx-base, 26.4%)`; the accent was Material violet |
|
||||
*
|
||||
* All four run without a display: `runComposeUiTest` reaches the semantics tree headless,
|
||||
* and the colour check reads the palette the theme hands out. That was measured before
|
||||
* these were written, because the spec left it open and a net that only works on a
|
||||
* developer's desk is worth saying so about.
|
||||
*/
|
||||
class DrawingLayerRegressionTest {
|
||||
|
||||
private fun themed(content: @Composable () -> Unit): @Composable () -> Unit = {
|
||||
Kst4ContestTheme(darkMode = false, baseFontSizeSp = 12f) { content() }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- class 1
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
@Test
|
||||
fun aTypedSettingAppearsOnScreenAndNotOnlyInThePreferences() = runComposeUiTest {
|
||||
val prefs = ChatPreferences()
|
||||
prefs.setMessageHandling_autoAnswerTextMainCat("")
|
||||
val state = MessageHandlingTabState(prefs)
|
||||
|
||||
// The real tab, not a field in isolation: the fault was in the wiring between them.
|
||||
setContent(themed { MessageHandlingTab(state) })
|
||||
|
||||
// MessageHandlingTab has exactly one text field, so this finder is unambiguous.
|
||||
onNode(hasSetTextAction()).performTextInput("qrv 144.300")
|
||||
|
||||
assertEquals(
|
||||
"qrv 144.300",
|
||||
prefs.getMessageHandling_autoAnswerTextMainCat(),
|
||||
"the value did not reach the preferences",
|
||||
)
|
||||
onNodeWithText("qrv 144.300").assertExists()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- class 2
|
||||
|
||||
private val tabTitles = listOf(
|
||||
"Station", "Log synch", "TRX synch", "Airscout", "Notification",
|
||||
"Message handling", "Beacon", "Shortcuts", "GUI", "Profiles",
|
||||
"Workedstn database",
|
||||
)
|
||||
|
||||
/**
|
||||
* The strip inside a box of a given width.
|
||||
*
|
||||
* The width is forced with a modifier rather than by sizing the test window, which is
|
||||
* what makes this a measurement instead of a does-it-crash check: the same content is
|
||||
* laid out twice and the two results are compared.
|
||||
*/
|
||||
private fun stripAt(widthDp: Int): @Composable () -> Unit = themed {
|
||||
Box(Modifier.width(widthDp.dp)) {
|
||||
SettingsTabStrip(
|
||||
tabs = tabTitles.map { title -> SettingsTab(title) { Text(title) } },
|
||||
selectedIndex = 0,
|
||||
onSelect = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
@Test
|
||||
fun theTabStripWrapsRatherThanSqueezingItsTitles() = runComposeUiTest {
|
||||
setContent(stripAt(WIDE_ENOUGH_FOR_EVERY_TAB))
|
||||
|
||||
val wide = onNodeWithText(LONGEST_TAB).fetchSemanticsNode()
|
||||
val wideWidth = wide.size.width
|
||||
val wideTop = wide.positionInRoot.y
|
||||
|
||||
assertTrue(wideWidth > 0, "the longest tab title was not laid out at all")
|
||||
|
||||
setContent(stripAt(TOO_NARROW_FOR_ONE_ROW))
|
||||
|
||||
val narrow = onNodeWithText(LONGEST_TAB).fetchSemanticsNode()
|
||||
|
||||
assertEquals(
|
||||
wideWidth,
|
||||
narrow.size.width,
|
||||
"the tab got narrower when the row did, which means it is being squeezed "
|
||||
+ "instead of wrapped — that is what cut '$LONGEST_TAB' mid-word",
|
||||
)
|
||||
assertTrue(
|
||||
narrow.positionInRoot.y > wideTop,
|
||||
"eleven tabs fitted on one line at $TOO_NARROW_FOR_ONE_ROW dp, so nothing "
|
||||
+ "was measured; pick a narrower width",
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- class 3
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
@Test
|
||||
fun aFormTallerThanTheWindowCanStillBeReached() = runComposeUiTest {
|
||||
val fieldCount = 40
|
||||
val lastLabel = "field $fieldCount"
|
||||
|
||||
setContent(
|
||||
themed {
|
||||
// The window's own scroll region, with content that cannot possibly fit.
|
||||
SettingsContent(
|
||||
tabs = listOf(
|
||||
SettingsTab("Tall") {
|
||||
Column {
|
||||
(1..fieldCount).forEach { Text("field $it", Modifier.height(40.dp)) }
|
||||
}
|
||||
},
|
||||
),
|
||||
notices = SettingsNotices(),
|
||||
buttons = {},
|
||||
close = {},
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
/*
|
||||
* performScrollTo fails if nothing in the chain scrolls. That is exactly the fault:
|
||||
* each tab used to scroll for itself, so content simply ended mid-form with no
|
||||
* scrollbar and no way down.
|
||||
*/
|
||||
onNodeWithText(lastLabel).performScrollTo().assertExists()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- class 4
|
||||
|
||||
@Test
|
||||
fun theWindowSurfaceIsDerivedFromTheBaseAndNotTheBaseItself() {
|
||||
val evening = JavaFxStylesheet.read("/KST4ContestDefaultEvening.css")
|
||||
|
||||
/*
|
||||
* The fault was taking -fx-base for the window surface. The sheets derive it, and
|
||||
* the two are visibly different greys — which is why the first Compose windows
|
||||
* looked flat against the JavaFX ones beside them.
|
||||
*/
|
||||
assertTrue(
|
||||
evening.windowBackground != evening.base,
|
||||
"the window surface is the raw base colour again",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theAccentComesFromTheStylesheetAndNotFromMaterial() {
|
||||
val evening = JavaFxStylesheet.read("/KST4ContestDefaultEvening.css")
|
||||
val day = JavaFxStylesheet.read("/KST4ContestDefaultDay.css")
|
||||
|
||||
/*
|
||||
* Material's default primary is a violet (#6650a4 family). The sheets name their
|
||||
* own accent, and the two sheets name different ones — so a palette that agreed
|
||||
* with Material, or a palette that was the same in both sheets, would mean the
|
||||
* stylesheet is not being read.
|
||||
*/
|
||||
val materialViolet = androidx.compose.ui.graphics.Color(0xFF6650A4)
|
||||
|
||||
assertTrue(evening.accent != materialViolet, "the accent is Material's, not the sheet's")
|
||||
assertTrue(day.accent != materialViolet, "the accent is Material's, not the sheet's")
|
||||
assertTrue(
|
||||
evening.accent != day.accent || evening.base != day.base,
|
||||
"both sheets produced the same palette, so neither was really read",
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val LONGEST_TAB = "Workedstn database"
|
||||
|
||||
/** Comfortably more than the eleven tabs need, so none of them wraps. */
|
||||
const val WIDE_ENOUGH_FOR_EVERY_TAB = 2000
|
||||
|
||||
/** Narrow enough that the row must break; the wrap is the point of the test. */
|
||||
const val TOO_NARROW_FOR_ONE_ROW = 300
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user