Make the colours adjustable per operator profile

Six colour roles per design, per profile: surface, window surface, field interior,
text, accent and separator line. A change applies at once in every open window and
survives a restart once the settings are saved, like any other preference.

Three sources, later wins role by role: the shipped stylesheet from the classpath,
then the profile's own stylesheet file if there is one, then the overrides stored in
the profile's preferences. resolvePalette is a pure function and reports where every
role's colour came from, which the tab shows per field -- with two sources able to
decide one colour, that is the only thing keeping "why is this colour what it is"
answerable, and the spec calls it mandatory rather than decorative.

Four things this had to get right, each learned the hard way and each pinned:

A role is a selector and a property, not a property alone. Three of the six are
stated outside .root -- text on .label, the separator on .separator *.line -- and
-fx-background-color alone appears a dozen times in the evening sheet. Measured: the
shipped daylight sheet states no colour at all, the evening sheet states five and
implies the sixth.

Kst4ContestTheme reads the palette from the store's state and not from remember.
That read is what makes a change repaint every open window; remember(darkMode) held
the palette until somebody toggled day/evening, so a change reached nothing.

A derived colour follows its source's value, never its provenance. The root
profile's own stylesheet is the one copyResourceIfRequired writes at every startup --
the shipped sheet itself -- so layer 2 declares the accent on every existing
installation with nobody having chosen anything. Keying textAccent on "does this come
from a file" repainted the evening accent from green to blue on upgrade and made
"reset to shipped" disagree with "discard my changes".

The contrast guard measures where colours are actually drawn, read off Theme.kt's
Material mapping rather than off the role names: background and surface are both the
window surface, and primary is textAccent, not accent. It reports a pair only when it
is both under WCAG AA and worse than the shipped sheet -- the shipped sheets have
three pairs under 4.5 themselves, so an absolute floor would warn on an untouched
client and teach the operator to ignore warnings.

The way back out of an unreadable palette is deliberately overbuilt, because it is
the one failure that locks itself in. "Reset to the shipped colours" takes the
shipped palette as a required parameter and reads no composition local, so there is
no path by which it could be drawn in the colours it exists to undo; it looks out of
place under a customised palette, and that is what makes it findable. Ctrl+Z, Ctrl+D
and Ctrl+R reach the three ways back even with the caret in a colour field. A
mistyped colour is refused and the field snaps back.

The profile's stylesheet is the hand-editing route the copy under ~/.praktiKST has
been promising since it was first written and never read. It is read now. A button
writes the shipped template as a starting point and never replaces a file that is
already there. An unreadable, truncated or non-CSS file does not prevent startup --
the role falls back to the shipped design. A sheet stating -fx-base and no
-fx-background states the derived window surface too, the way JavaFX does, or the
most natural hand edit would leave every window the shipped light grey.

101 new tests. All of it runs headless.
This commit is contained in:
Rsclub2_2 committed 2026-10-05 22:36:01 +02:00
1 parent 83e23f55a3
commit 12b2d37ce2
39 files changed
+3373 -28

No files matched your search

@@ -1,5 +1,6 @@
package kst4contest.controller;
import kst4contest.ApplicationConstants;
import kst4contest.model.ChatPreferences;
import kst4contest.model.OperatorProfile;
import kst4contest.model.OperatorProfileSelection;
@@ -88,6 +89,34 @@ public final class OperatorProfilePaths {
return profileRelativeDirectory(profile) + "/" + DBController.DATABASE_FILE;
}
/**
* Returns the stylesheet file name relative to the application directory.
*
* <p>The operator's own stylesheet for one design, which is the middle of the three
* palette layers. Shaped like the preferences and the worked database: the root profile
* keeps the historic flat name, every other profile gets it inside its own directory.</p>
*
* <p>The root profile's name is deliberately the one copyResourceIfRequired already
* writes at startup. That copy has never been read by anything; reading it is what gives
* the operator the hand-editing route the file has been promising all along.</p>
*
* @param profile profile to resolve
* @param darkMode true for the evening design, false for the daylight one
* @return relative stylesheet file name
*/
public static String paletteRelativeFileName(final OperatorProfile profile, final boolean darkMode) {
final String fileName = darkMode
? ApplicationConstants.STYLECSSFILE_DEFAULT_EVENING
: ApplicationConstants.STYLECSSFILE_DEFAULT_DAYLIGHT;
if (profile.isRootProfile()) {
return fileName;
}
return profileRelativeDirectory(profile) + "/" + fileName;
}
/**
* Resolves a profile descriptor into the runtime selection used during startup.
*
@@ -50,7 +50,13 @@ public class ChatPreferences {
/**
* Bump this when you change the XML schema written by {@link #writePreferencesToXmlFile()}.
* Bump this when a change to the XML schema needs a reader to act on the version.
* <p>
* Adding an element does not: reading must stay backwards compatible, every setting falls
* back to its own default, and nothing in this application branches on the number -- the
* only use is a best-effort warning when a file is newer than the application. Release
* 1.50 added the two palette-override elements and deliberately left this at 7. A bump
* that no reader acts on only invites the next person to bump it again.
* <p>
* Reading must stay backwards compatible: missing/unknown tags should fall back to defaults.
*/
@@ -382,6 +388,17 @@ public class ChatPreferences {
boolean guiOptions_showGrossFieldWorkedHintInBandColumns = true; // show "o" (grid square already worked on this band) in the band columns
boolean guiOptions_showFreshCallHintInBandColumns = true; // show "a" (band available, call not worked on any band yet) instead of always "B+" in the band columns
/**
* The operator's colour overrides for the daylight design, encoded by PaletteOverrides.
* Per profile, like every other preference: the file this is written to is the profile's
* own. One string rather than six elements, because this file is written element by
* element by hand and twelve more would be twelve write blocks and twelve read blocks.
*/
private String guiOptions_paletteOverridesDay = "";
/** The same for the evening design. Two designs, two independent sets. */
private String guiOptions_paletteOverridesEvening = "";
private double[] GUIstationMapStageSceneSizeHW = new double[] { 1000, 800 };
private double[] GUIstationMapStagePositionXY = new double[] { Double.NaN, Double.NaN };
private boolean GUIstationMapPathAnalysisVisible = true;
@@ -773,6 +790,44 @@ public class ChatPreferences {
this.guiOptions_showFreshCallHintInBandColumns = guiOptions_showFreshCallHintInBandColumns;
}
/**
* Returns the colour overrides of the daylight design.
*
* @return the encoded overrides, empty when the operator has changed nothing
*/
public String getGuiOptions_paletteOverridesDay() {
return guiOptions_paletteOverridesDay;
}
/**
* Sets the colour overrides of the daylight design.
*
* @param guiOptions_paletteOverridesDay the encoded overrides; null becomes empty
*/
public void setGuiOptions_paletteOverridesDay(String guiOptions_paletteOverridesDay) {
this.guiOptions_paletteOverridesDay =
guiOptions_paletteOverridesDay == null ? "" : guiOptions_paletteOverridesDay;
}
/**
* Returns the colour overrides of the evening design.
*
* @return the encoded overrides, empty when the operator has changed nothing
*/
public String getGuiOptions_paletteOverridesEvening() {
return guiOptions_paletteOverridesEvening;
}
/**
* Sets the colour overrides of the evening design.
*
* @param guiOptions_paletteOverridesEvening the encoded overrides; null becomes empty
*/
public void setGuiOptions_paletteOverridesEvening(String guiOptions_paletteOverridesEvening) {
this.guiOptions_paletteOverridesEvening =
guiOptions_paletteOverridesEvening == null ? "" : guiOptions_paletteOverridesEvening;
}
public boolean isGuiOptions_defaultFilterPmToOther() {
return guiOptions_defaultFilterPmToOther;
}
@@ -2086,6 +2141,14 @@ public class ChatPreferences {
guiOptions_showFreshCallHintInBandColumns.setTextContent(this.isGuiOptions_showFreshCallHintInBandColumns()+"");
guiSaveableOptions.appendChild(guiOptions_showFreshCallHintInBandColumns);
Element guiOptions_paletteOverridesDay = doc.createElement("guiOptions_paletteOverridesDay");
guiOptions_paletteOverridesDay.setTextContent(this.getGuiOptions_paletteOverridesDay());
guiSaveableOptions.appendChild(guiOptions_paletteOverridesDay);
Element guiOptions_paletteOverridesEvening = doc.createElement("guiOptions_paletteOverridesEvening");
guiOptions_paletteOverridesEvening.setTextContent(this.getGuiOptions_paletteOverridesEvening());
guiSaveableOptions.appendChild(guiOptions_paletteOverridesEvening);
Element guiOptions_darkModeActive = doc.createElement("guiOptions_darkModeActive");
guiOptions_darkModeActive.setTextContent(this.GUI_darkModeActive + "");
guiSaveableOptions.appendChild(guiOptions_darkModeActive);
@@ -3160,6 +3223,10 @@ public class ChatPreferences {
this.setGuiOptions_showGrossFieldWorkedHintInBandColumns(getBoolean(guiSaveableOptionsEl, this.isGuiOptions_showGrossFieldWorkedHintInBandColumns(), "guiOptions_showGrossFieldWorkedHintInBandColumns"));
this.setGuiOptions_showFreshCallHintInBandColumns(getBoolean(guiSaveableOptionsEl, this.isGuiOptions_showFreshCallHintInBandColumns(), "guiOptions_showFreshCallHintInBandColumns"));
// Added in release 1.50: the operator's own colours, one string per design.
this.setGuiOptions_paletteOverridesDay(getText(guiSaveableOptionsEl, this.getGuiOptions_paletteOverridesDay(), "guiOptions_paletteOverridesDay"));
this.setGuiOptions_paletteOverridesEvening(getText(guiSaveableOptionsEl, this.getGuiOptions_paletteOverridesEvening(), "guiOptions_paletteOverridesEvening"));
// Added in later versions: dark mode flags
this.GUI_darkModeActive = getBoolean(guiSaveableOptionsEl, this.GUI_darkModeActive, "guiOptions_darkModeActive");
this.GUI_darkModeActiveByDefault = getBoolean(guiSaveableOptionsEl, this.GUI_darkModeActiveByDefault, "guiOptions_darkModeActiveByDefault");
@@ -0,0 +1,120 @@
package kst4contest.model;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
/**
* A set of palette overrides, encoded as one string.
*
* <p>One string per design rather than twelve XML elements: the preferences file is written
* element by element by hand, and twelve more would be twelve write blocks, twelve read
* blocks and twelve fields in a class that is already three thousand lines long.</p>
*
* <p>Reading is deliberately forgiving and never throws. The input is not always this
* version's own: a hand-edited XML, a file from an older release, or a write interrupted
* half way. An unknown key or an unparsable colour is skipped and the rest survives --
* losing one colour is a nuisance, losing the readability of the client is not.</p>
*/
public final class PaletteOverrides {
private static final Pattern SIX_DIGIT_HEX = Pattern.compile("^#[0-9a-fA-F]{6}$");
private static final String PAIR_SEPARATOR = ";";
private static final String KEY_VALUE_SEPARATOR = "=";
private PaletteOverrides() {
// Utility class.
}
/**
* Reads a stored set of overrides.
*
* @param stored the stored string, may be null or malformed
* @return the roles actually named with a usable colour, never null
*/
public static Map<PaletteRole, String> parse(final String stored) {
Map<PaletteRole, String> overrides = new LinkedHashMap<>();
if (stored == null || stored.isBlank()) {
return overrides;
}
for (String pair : stored.split(PAIR_SEPARATOR)) {
int separator = pair.indexOf(KEY_VALUE_SEPARATOR);
if (separator <= 0) {
continue;
}
String key = pair.substring(0, separator).trim();
String value = pair.substring(separator + 1).trim();
if (!isValidColour(value)) {
continue;
}
for (PaletteRole role : PaletteRole.values()) {
if (role.key().equals(key)) {
overrides.put(role, normalise(value));
break;
}
}
}
return overrides;
}
/**
* Writes a set of overrides for storage.
*
* @param overrides the roles to store; entries with an unusable colour are left out
* @return the string to store, empty when there is nothing to store
*/
public static String format(final Map<PaletteRole, String> overrides) {
if (overrides == null || overrides.isEmpty()) {
return "";
}
StringBuilder stored = new StringBuilder();
for (PaletteRole role : PaletteRole.values()) {
String colour = overrides.get(role);
if (!isValidColour(colour)) {
continue;
}
if (stored.length() > 0) {
stored.append(PAIR_SEPARATOR);
}
stored.append(role.key()).append(KEY_VALUE_SEPARATOR).append(normalise(colour));
}
return stored.toString();
}
/**
* Returns whether a string is a colour this application can use.
*
* <p>Six hexadecimal digits with a leading hash, and nothing else. The stylesheets use
* named colours and functions too, but those are the shipped sheets' business; what an
* operator types has one form so that what they typed is what they get.</p>
*
* @param colour the text to check, may be null
* @return true when it names a colour
*/
public static boolean isValidColour(final String colour) {
return colour != null && SIX_DIGIT_HEX.matcher(colour.trim()).matches();
}
private static String normalise(final String colour) {
return colour.trim().toUpperCase(Locale.ROOT);
}
}
@@ -0,0 +1,97 @@
package kst4contest.model;
/**
* A colour the operator may set, and the stylesheet declaration it corresponds to.
*
* <p>In core and therefore free of any toolkit: a role is a name and three strings, and the
* colour itself travels as text. Turning text into a drawable colour is the user
* interface's job.</p>
*
* <p>A declaration is a selector and a property together. Three of the six roles are stated
* outside {@code .root} by the shipped sheets, and {@code -fx-background-color} alone appears
* a dozen times in one of them -- on scroll bars, buttons and toggle buttons. Without the
* selector, half the roles would never be found and the rest would be found in the wrong
* places.</p>
*
* <p>All three strings are part of a file format. The selector and the property are what a
* hand-edited stylesheet writes; {@link #key()} is what the stored overrides are written
* under. Renaming any of them silently drops a colour an operator chose.</p>
*/
public enum PaletteRole {
/** The control surface; the window surface is normally a {@code derive} of this. */
SURFACE(".root", "-fx-base", "surface"),
/** The window's own area. Neither shipped sheet states it, so both derive it. */
WINDOW_SURFACE(".root", "-fx-background", "windowSurface"),
/** Inside a text field or a list. */
FIELD_INTERIOR(".root", "-fx-control-inner-background", "fieldInterior"),
/** Label text. Stated on {@code .label} and not on {@code .root}. */
TEXT(".label", "-fx-text-fill", "text"),
ACCENT(".root", "-fx-accent", "accent"),
/** The separator's line, whose selector is what tells it from a scroll bar. */
SEPARATOR(".separator *.line", "-fx-background-color", "separator");
private final String selector;
private final String cssProperty;
private final String key;
PaletteRole(final String selector, final String cssProperty, final String key) {
this.selector = selector;
this.cssProperty = cssProperty;
this.key = key;
}
/**
* Returns the stylesheet selector this role is stated on.
*
* @return selector
*/
public String selector() {
return selector;
}
/**
* Returns the stylesheet property this role is read from.
*
* @return CSS property name
*/
public String cssProperty() {
return cssProperty;
}
/**
* Returns the name this role is stored under in the preferences.
*
* @return storage key
*/
public String key() {
return key;
}
/**
* Returns the role a stylesheet declaration belongs to.
*
* @param selector a selector from a stylesheet, may be null
* @param cssProperty a property name from a stylesheet, may be null
* @return the role, or null for the hundreds of declarations that are not ours
*/
public static PaletteRole of(final String selector, final String cssProperty) {
if (selector == null || cssProperty == null) {
return null;
}
for (PaletteRole role : values()) {
if (role.selector.equals(selector) && role.cssProperty.equals(cssProperty)) {
return role;
}
}
return null;
}
}
@@ -78,4 +78,32 @@ class OperatorProfilePathsTest {
assertNotEquals(OperatorProfilePaths.ROOT_PROFILE_ID,
OperatorProfilePaths.toProfileId("default", Set.of()));
}
@Test
void theRootProfileKeepsTheHistoricFlatStylesheetNames() {
OperatorProfile root = OperatorProfilePaths.buildRootProfile("Default");
assertEquals(
"KST4ContestDefaultDay.css",
OperatorProfilePaths.paletteRelativeFileName(root, false));
assertEquals(
"KST4ContestDefaultEvening.css",
OperatorProfilePaths.paletteRelativeFileName(root, true));
}
@Test
void anotherProfileGetsItsStylesheetInItsOwnDirectory() {
/*
* The same shape the preferences and the worked database already take. A different
* shape here would have one profile reading another profile's colours.
*/
OperatorProfile other = new OperatorProfile("OP2", "DN9APW", false, false);
assertEquals(
"profiles/OP2/KST4ContestDefaultDay.css",
OperatorProfilePaths.paletteRelativeFileName(other, false));
assertEquals(
"profiles/OP2/KST4ContestDefaultEvening.css",
OperatorProfilePaths.paletteRelativeFileName(other, true));
}
}
@@ -0,0 +1,46 @@
package kst4contest.model;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* The two designs' colour overrides are stored independently.
*
* <p>That is the part worth pinning: two designs sharing one storage mechanism is exactly
* the shape in which a change to the evening palette quietly drags the daylight one with
* it.</p>
*/
class ChatPreferencesPaletteTest {
@Test
void theTwoDesignsAreStoredIndependently() {
ChatPreferences prefs = new ChatPreferences();
prefs.setGuiOptions_paletteOverridesDay("surface=#FFFFFF");
prefs.setGuiOptions_paletteOverridesEvening("surface=#000000");
assertEquals("surface=#FFFFFF", prefs.getGuiOptions_paletteOverridesDay());
assertEquals("surface=#000000", prefs.getGuiOptions_paletteOverridesEvening());
}
@Test
void anUnsetOverrideReadsAsEmptyAndNotAsNull() {
// A preferences file from an older release has no such element at all.
ChatPreferences prefs = new ChatPreferences();
assertEquals("", prefs.getGuiOptions_paletteOverridesDay());
assertEquals("", prefs.getGuiOptions_paletteOverridesEvening());
}
@Test
void aNullSetterValueBecomesEmpty() {
ChatPreferences prefs = new ChatPreferences();
prefs.setGuiOptions_paletteOverridesDay(null);
prefs.setGuiOptions_paletteOverridesEvening(null);
assertEquals("", prefs.getGuiOptions_paletteOverridesDay());
assertEquals("", prefs.getGuiOptions_paletteOverridesEvening());
}
}
@@ -0,0 +1,101 @@
package kst4contest.model;
import org.junit.jupiter.api.Test;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* How a set of colour overrides survives in the preferences XML.
*
* <p>One string per design rather than twelve elements. The parsing is where an operator's
* stored colours are either kept or quietly lost, and the input is not always ours: a
* hand-edited XML, a file written by an older version, or a half-finished write.</p>
*/
class PaletteOverridesTest {
@Test
void anEmptyStoreMeansNoOverridesRatherThanAFailure() {
assertTrue(PaletteOverrides.parse(null).isEmpty());
assertTrue(PaletteOverrides.parse("").isEmpty());
assertTrue(PaletteOverrides.parse(" ").isEmpty());
}
@Test
void onlyTheRolesActuallyNamedComeBack() {
Map<PaletteRole, String> parsed = PaletteOverrides.parse("surface=#ECECEC");
// The whole point of the layering: one named role overrides one role.
assertEquals(1, parsed.size());
assertEquals("#ECECEC", parsed.get(PaletteRole.SURFACE));
}
@Test
void aRoundTripKeepsEveryRole() {
Map<PaletteRole, String> original = new LinkedHashMap<>();
for (PaletteRole role : PaletteRole.values()) {
original.put(role, "#10203" + role.ordinal());
}
assertEquals(original, PaletteOverrides.parse(PaletteOverrides.format(original)));
}
@Test
void anUnknownKeyIsSkippedAndTheRestSurvives() {
// An older or newer version may have written a role this one does not know.
Map<PaletteRole, String> parsed =
PaletteOverrides.parse("surface=#ECECEC;chrome=#123456;accent=#0096C9");
assertEquals(2, parsed.size());
assertEquals("#ECECEC", parsed.get(PaletteRole.SURFACE));
assertEquals("#0096C9", parsed.get(PaletteRole.ACCENT));
}
@Test
void anUnparsableColourIsSkippedRatherThanStored() {
/*
* This is the difference between an operator losing one colour and an operator
* losing the readability of the whole client: a bad value must not become black.
*/
Map<PaletteRole, String> parsed =
PaletteOverrides.parse("surface=rot;accent=#0096C9;text=#GGGGGG;separator=");
assertEquals(1, parsed.size());
assertEquals("#0096C9", parsed.get(PaletteRole.ACCENT));
}
@Test
void garbageIsSurvivedWithoutAnException() {
// Half-written files exist. None of these may throw.
for (String garbage : new String[] {
";;;", "=", "surface", "surface=", "=#ECECEC", "surface==#ECECEC",
"surface=#ECECEC;", "\n\t", "a=b=c",
}) {
PaletteOverrides.parse(garbage);
}
}
@Test
void whatCountsAsAColour() {
assertTrue(PaletteOverrides.isValidColour("#ECECEC"));
assertTrue(PaletteOverrides.isValidColour("#ececec"));
assertTrue(PaletteOverrides.isValidColour(" #ECECEC "));
assertFalse(PaletteOverrides.isValidColour(null));
assertFalse(PaletteOverrides.isValidColour(""));
assertFalse(PaletteOverrides.isValidColour("ECECEC"));
assertFalse(PaletteOverrides.isValidColour("#ECECE"));
assertFalse(PaletteOverrides.isValidColour("#ECECECE"));
assertFalse(PaletteOverrides.isValidColour("#GGGGGG"));
assertFalse(PaletteOverrides.isValidColour("rot"));
}
@Test
void formattingAnEmptyMapGivesAnEmptyString() {
assertEquals("", PaletteOverrides.format(new LinkedHashMap<>()));
}
}
@@ -0,0 +1,87 @@
package kst4contest.model;
import org.junit.jupiter.api.Test;
import java.util.HashSet;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* The six roles the operator may set.
*
* <p>A role is a selector and a property, not a property alone: the shipped sheets state
* three of the six outside `.root`, and `-fx-background-color` on its own appears a dozen
* times in one sheet. The selector is what tells the separator's line from a scroll bar.</p>
*
* <p>All three strings are part of a file format -- two of them the operator's own
* hand-edited stylesheet, the third the preferences XML. Renaming any of them silently
* drops a colour somebody chose, which is why they are pinned here rather than inlined.</p>
*/
class PaletteRoleTest {
@Test
void thereAreSixRolesAndEachNamesOneDeclaration() {
assertEquals(6, PaletteRole.values().length);
Set<String> declarations = new HashSet<>();
for (PaletteRole role : PaletteRole.values()) {
assertTrue(role.cssProperty().startsWith("-fx-"), role + " has no CSS property");
assertTrue(role.selector().startsWith("."), role + " has no selector");
assertTrue(
declarations.add(role.selector() + "|" + role.cssProperty()),
role + " names the same declaration as another role");
}
}
@Test
void theThreeRolesOutsideRootKeepTheirOwnSelectors() {
/*
* These three are the reason a role carries a selector at all. The values are the
* ones JavaFxStylesheet.read already uses, so the two must not drift apart.
*/
assertEquals(".label", PaletteRole.TEXT.selector());
assertEquals("-fx-text-fill", PaletteRole.TEXT.cssProperty());
assertEquals(".separator *.line", PaletteRole.SEPARATOR.selector());
assertEquals("-fx-background-color", PaletteRole.SEPARATOR.cssProperty());
assertEquals(".root", PaletteRole.SURFACE.selector());
}
@Test
void theStorageKeysAreDistinctAndStable() {
// These strings land in the operator's preferences XML. Renaming one drops a colour.
assertEquals("surface", PaletteRole.SURFACE.key());
assertEquals("windowSurface", PaletteRole.WINDOW_SURFACE.key());
assertEquals("fieldInterior", PaletteRole.FIELD_INTERIOR.key());
assertEquals("text", PaletteRole.TEXT.key());
assertEquals("accent", PaletteRole.ACCENT.key());
assertEquals("separator", PaletteRole.SEPARATOR.key());
}
@Test
void aDeclarationResolvesBackToItsRole() {
assertEquals(PaletteRole.SURFACE, PaletteRole.of(".root", "-fx-base"));
assertEquals(PaletteRole.TEXT, PaletteRole.of(".label", "-fx-text-fill"));
}
@Test
void theSamePropertyUnderAnotherSelectorIsNotThatRole() {
/*
* The whole point of carrying the selector. Both sheets set -fx-text-fill on a
* dozen selectors and -fx-background-color on more; only one of each is a role.
*/
assertNull(PaletteRole.of(".button:hover", "-fx-text-fill"));
assertNull(PaletteRole.of(".scroll-bar", "-fx-background-color"));
}
@Test
void anUnknownDeclarationIsNotARole() {
assertNull(PaletteRole.of(".root", "-fx-font-size"));
assertNull(PaletteRole.of("", ""));
assertNull(PaletteRole.of(null, null));
}
}