This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git

commit f1c5b6340dc76315da6fbe01958b570748084244
Author: James Bognar <[email protected]>
AuthorDate: Tue Aug 18 13:38:13 2026 -0400

    TODO-408/414: Add a themeable red tag/stage triad; make Theme.getTokens() 
iteration order deterministic
    
    TODO-408: adds a --jc-tag-red-* token triad (background/text/border) to 
Theme,
    completing the four-state status vocabulary (green/blue/neutral/red) the
    tag/stage pill styling needs. chrome.css gains the
    .tag.status.fail(ed) / .tag.stage.fail(ed) selector mapping onto it.
    
    TODO-414: Theme's constructor now copies the caller's token map with an
    explicit LinkedHashMap copy (cp/u helpers) instead of Map.copyOf.
    ConsoleChromeMixin emits getTokens()'s iteration order directly into the 
served
    stylesheet's :root{} block, and Map.copyOf's order is perturbed by a per-JVM
    salt -- two processes serving an identical token set could otherwise serve
    byte-different responses.
    
    The two are bundled because both edit Theme.java and both append to the same
    ConsoleChromeMixin_Test block; splitting them would produce two commits each
    smaller than the file-split overhead is worth. New Theme_TokenOrdering_Test
    covers TODO-414 on its own.
    
    Partial-file note: this commit contains only test methods n01-o02 of
    ConsoleChromeMixin_Test's post-backup block (red-triad completeness/theming/
    serving, then token-order determinism and cross-process byte-identity), plus
    the j03 pinned-token-count bump from 32 to 35 that the new triad forces.
    l01-m12 in the same file belong to a sibling commit's unrelated mount fix 
that
    happens to append to the same block -- see that commit's message.
---
 .../apache/juneau/rest/server/console/Theme.java   |  13 ++-
 .../resources/org/apache/juneau/console/chrome.css |   5 +
 .../server/console/ConsoleChromeMixin_Test.java    | 107 ++++++++++++++++++++-
 .../server/console/Theme_TokenOrdering_Test.java   |  96 ++++++++++++++++++
 4 files changed, 218 insertions(+), 3 deletions(-)

diff --git 
a/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/Theme.java
 
b/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/Theme.java
index 83a74fb51a..e6d42d9391 100644
--- 
a/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/Theme.java
+++ 
b/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/Theme.java
@@ -97,6 +97,9 @@ public final class Theme {
                .token("--jc-tag-neutral-bg", "#e2e3e5")
                .token("--jc-tag-neutral-text", "#383d41")
                .token("--jc-tag-neutral-border", "#c6c8ca")
+               .token("--jc-tag-red-bg", "#f8d7da")
+               .token("--jc-tag-red-text", "#721c24")
+               .token("--jc-tag-red-border", "#f5c6cb")
                .build();
 
        private final String name;
@@ -104,7 +107,11 @@ public final class Theme {
 
        private Theme(String name, Map<String,String> tokens) {
                this.name = name;
-               this.tokens = Map.copyOf(tokens);
+               // Insertion-ordered rather than Map.copyOf: ConsoleChromeMixin 
emits this map's iteration order directly as
+               // the served stylesheet's :root{} declarations, and 
Map.copyOf's order is perturbed by a per-JVM salt - which
+               // would make the response body differ between two processes 
serving an identical token set.  cp(Map) is
+               // contractually a LinkedHashMap copy, so it preserves that 
order; do not swap it for an unordered copy.
+               this.tokens = u(cp(tokens));
        }
 
        /**
@@ -133,7 +140,9 @@ public final class Theme {
        /**
         * Returns this theme's token overrides.
         *
-        * @return An immutable map of token name (e.g. {@code "--jc-accent"}) 
to CSS value. Never <jk>null</jk>.
+        * @return
+        *      An immutable map of token name (e.g. {@code "--jc-accent"}) to 
CSS value, iterating in the order the tokens
+        *      were declared on the builder. Never <jk>null</jk>.
         */
        public Map<String,String> getTokens() { return tokens; }
 
diff --git 
a/juneau-rest/juneau-rest-server-console-ui/src/main/resources/org/apache/juneau/console/chrome.css
 
b/juneau-rest/juneau-rest-server-console-ui/src/main/resources/org/apache/juneau/console/chrome.css
index 737c09b967..342ba26819 100644
--- 
a/juneau-rest/juneau-rest-server-console-ui/src/main/resources/org/apache/juneau/console/chrome.css
+++ 
b/juneau-rest/juneau-rest-server-console-ui/src/main/resources/org/apache/juneau/console/chrome.css
@@ -346,6 +346,11 @@ a:hover { text-decoration: underline; }
 .tag.stage.unknown,
 .tag.stage.archived { background-color: var(--jc-tag-neutral-bg); color: 
var(--jc-tag-neutral-text); border-color: var(--jc-tag-neutral-border); }
 
+.tag.status.fail,
+.tag.status.failed,
+.tag.stage.fail,
+.tag.stage.failed { background-color: var(--jc-tag-red-bg); color: 
var(--jc-tag-red-text); border-color: var(--jc-tag-red-border); }
+
 /* ==========================================================================
    Data table (IRS / DataTables style)
    ========================================================================== 
*/
diff --git 
a/juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin_Test.java
 
b/juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin_Test.java
index c91e5e0aca..7e645b72c8 100644
--- 
a/juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin_Test.java
+++ 
b/juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin_Test.java
@@ -497,8 +497,12 @@ class ConsoleChromeMixin_Test extends TestBase {
                // A0 must not add a --jc-logo or --jc-page-bg-image token - 
the logo/page-bg mechanism is deliberately
                // NOT part of the Theme token model (finding 4 of the design 
doc). If this count ever changes, it must be
                // a DIFFERENT, deliberate change to Theme.OPEN - not a side 
effect of the asset feature.
-               assertEquals(32, Theme.OPEN.getTokens().size());
+               //
+               // 35 = the original 32, plus the three-token red tag triad 
added so a four-state status vocabulary has a
+               // fourth colour. Bumping this number is only ever correct 
alongside a reviewed edit to Theme.OPEN itself.
+               assertEquals(35, Theme.OPEN.getTokens().size());
                assertFalse(Theme.OPEN.getTokens().containsKey("--jc-logo"));
+               
assertFalse(Theme.OPEN.getTokens().containsKey("--jc-page-bg-image"));
        }
 
        
//-----------------------------------------------------------------------------------------------------------------
@@ -716,6 +720,107 @@ class ConsoleChromeMixin_Test extends TestBase {
                assertEquals(2, MOUNT_CACHE_MIXIN.debugBuildCount(), "expected 
exactly one assembly per distinct mount");
        }
 
+       
//-----------------------------------------------------------------------------------------------------------------
+       // n) Tag colour palette: the red triad that makes a four-state 
(pass/warn/fail/unknown) vocabulary expressible
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       /** Every colour family in the tag palette, in the order Theme.OPEN 
declares them. */
+       private static final List<String> TAG_PALETTE = List.of("green", 
"blue", "amber", "neutral", "red");
+
+       /** The three properties every tag colour family covers - a family 
missing one of them cannot paint a whole pill. */
+       private static final List<String> TAG_TRIAD_PROPERTIES = List.of("bg", 
"text", "border");
+
+       @Test void n01_everyTagColourFamily_isACompleteTriad_includingRed() {
+               for (var colour : TAG_PALETTE)
+                       for (var property : TAG_TRIAD_PROPERTIES) {
+                               var name = "--jc-tag-" + colour + '-' + 
property;
+                               
assertTrue(Theme.OPEN.getTokens().containsKey(name), () -> "Theme.OPEN is 
missing tag token '" + name + "'");
+                       }
+       }
+
+       @Test void 
n02_redTriadValues_areLiteralHexColours_andSurviveTheValueGrammar() {
+               for (var property : TAG_TRIAD_PROPERTIES) {
+                       var value = Theme.OPEN.getTokens().get("--jc-tag-red-" 
+ property);
+                       assertNotNull(value, () -> "no value for --jc-tag-red-" 
+ property);
+                       assertTrue(value.matches("#[0-9a-f]{6}"), () -> 
"--jc-tag-red-" + property + " is not a 6-digit hex colour like its siblings: " 
+ value);
+                       assertEquals(value, 
CssValueGrammar.normalizeAndValidate(value));
+               }
+       }
+
+       @Test void n03_chromeCss_mapsAFailValueOntoTheRedTriad() throws 
IOException {
+               var css = readChromeCss();
+               assertTrue(css.contains(".tag.status.fail"), () -> "no 
.tag.status.fail mapping rule, css:\n" + css);
+               for (var property : TAG_TRIAD_PROPERTIES)
+                       assertTrue(css.contains("var(--jc-tag-red-" + property 
+ ")"), () -> "--jc-tag-red-" + property + " is defined but never consumed");
+       }
+
+       @Test void 
n04_servedChromeCss_emitsTheRedTriad_alongsideTheOtherFourFamilies() throws 
Exception {
+               var body = bodyOf(MockRestClient.buildLax(DefaultHost.class));
+               for (var colour : TAG_PALETTE)
+                       for (var property : TAG_TRIAD_PROPERTIES) {
+                               var declaration = "--jc-tag-" + colour + '-' + 
property + ':';
+                               assertTrue(body.contains(declaration), () -> 
"served chrome.css never declares '" + declaration + "', body:\n" + body);
+                       }
+       }
+
+       private static final Theme RED_OVERRIDE_THEME = 
Theme.create("red-override")
+               .token("--jc-tag-red-bg", "#ffe0e2")
+               .token("--jc-tag-red-text", "#5a0f14")
+               .token("--jc-tag-red-border", "#f0b3b7")
+               .build();
+
+       @Rest(mixins=ConsoleChromeMixin.class)
+       public static class RedOverrideHost extends BasicRestServlet {
+               private static final long serialVersionUID = 1L;
+               @Bean public ConsoleChromeMixin console() { return 
ConsoleChromeMixin.create().theme(RED_OVERRIDE_THEME).build(); }
+       }
+
+       @Test void 
n05_redTriad_isThemeable_throughTheSameApiAsEveryOtherTriad() throws Exception {
+               var body = 
bodyOf(MockRestClient.buildLax(RedOverrideHost.class));
+               assertEquals(2, countRootBlocks(body), () -> "expected 
Theme.OPEN block + the override block, body:\n" + body);
+               assertTrue(body.contains("--jc-tag-red-bg:#ffe0e2;"), () -> 
"red bg override not emitted, body:\n" + body);
+               assertTrue(body.contains("--jc-tag-red-text:#5a0f14;"), () -> 
"red text override not emitted, body:\n" + body);
+               assertTrue(body.contains("--jc-tag-red-border:#f0b3b7;"), () -> 
"red border override not emitted, body:\n" + body);
+       }
+
+       /**
+        * The point of the red family: a four-state {@code pass}/{@code 
warn}/{@code fail}/{@code unknown} vocabulary
+        * has to reach four distinct colours. Sharing one between {@code warn} 
and {@code fail} would make a check that
+        * could not run indistinguishable from one that passed with a caveat.
+        */
+       @Test void n06_fourStateStatusVocabulary_resolvesToFourDistinctFills() {
+               var fills = new LinkedHashSet<String>();
+               for (var colour : List.of("green", "amber", "red", "neutral")) {
+                       var fill = Theme.OPEN.getTokens().get("--jc-tag-" + 
colour + "-bg");
+                       assertNotNull(fill, () -> "no fill for the '" + colour 
+ "' family");
+                       fills.add(fill);
+               }
+               assertEquals(4, fills.size(), () -> "pass/warn/fail/unknown 
collapse onto fewer than four fills: " + fills);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // o) Deterministic token emission: the :root{} block must be 
byte-stable for a given token set
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       /**
+        * Pins that the emitted declaration order is exactly {@link 
Theme#getTokens()}'s iteration order, i.e. that
+        * the theme's ordering guarantee reaches the wire rather than being 
re-bucketed on the way out. The guarantee
+        * itself - that the iteration order is the declaration order and not a 
per-JVM hash order, which is what makes
+        * the response byte-stable enough to ever carry an {@code ETag} - is 
proved in {@code Theme_TokenOrdering_Test}.
+        */
+       @Test void 
o01_openBlockDeclarationOrder_matchesThemeOpensDeclarationOrder() throws 
Exception {
+               var block = 
firstRootBlock(bodyOf(MockRestClient.buildLax(DefaultHost.class)));
+               var emitted = new ArrayList<String>();
+               var m = 
Pattern.compile("(--jc-[a-z0-9-]+)\\s*:").matcher(block);
+               while (m.find())
+                       emitted.add(m.group(1));
+               assertEquals(new ArrayList<>(Theme.OPEN.getTokens().keySet()), 
emitted);
+       }
+
+       @Test void 
o02_twoIndependentlyBuiltMixinsWithTheSameTheme_serveByteIdenticalBodies() 
throws Exception {
+               
assertEquals(bodyOf(MockRestClient.buildLax(DefaultHost.class)), 
bodyOf(MockRestClient.buildLax(DefaultHost.class)));
+       }
+
        
//-----------------------------------------------------------------------------------------------------------------
        // Test helpers
        
//-----------------------------------------------------------------------------------------------------------------
diff --git 
a/juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/Theme_TokenOrdering_Test.java
 
b/juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/Theme_TokenOrdering_Test.java
new file mode 100644
index 0000000000..17ec6a4491
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/Theme_TokenOrdering_Test.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.console;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * {@link Theme#getTokens()} must iterate in the order the tokens were 
declared, because
+ * {@code ConsoleChromeMixin} writes that iteration straight out as the served 
stylesheet's {@code :root{}}
+ * declarations - so a hash-ordered map makes the response body differ, byte 
for byte, between two processes
+ * serving an identical token set.
+ *
+ * <p>
+ * The load-bearing assertion is (a01)'s <i>insertion</i>-order check, not 
(a03)'s repeat-build check. A
+ * {@code java.util.ImmutableCollections} map perturbs its probe sequence with 
a per-JVM salt, so its order is
+ * arbitrary but <i>fixed</i> within one process: a test that only builds the 
same theme twice and compares
+ * cannot see the defect at all.
+ */
+class Theme_TokenOrdering_Test extends TestBase {
+
+       /** Declared in reverse-alphabetical order so a map that happened to 
sort its keys would not pass by accident. */
+       private static final List<String> NAMES = List.of(
+               "--jc-zulu", "--jc-yankee", "--jc-xray", "--jc-whiskey", 
"--jc-victor", "--jc-uniform",
+               "--jc-tango", "--jc-sierra", "--jc-romeo", "--jc-quebec", 
"--jc-papa", "--jc-oscar");
+
+       private static Theme themeWithNames() {
+               var b = Theme.create("ordering");
+               for (var i = 0; i < NAMES.size(); i++)
+                       b.token(NAMES.get(i), String.format("#%06x", i));
+               return b.build();
+       }
+
+       @Test void a01_getTokens_iteratesInDeclarationOrder() {
+               assertEquals(NAMES, new 
ArrayList<>(themeWithNames().getTokens().keySet()));
+       }
+
+       @Test void 
a02_themeOpen_iteratesInDeclarationOrder_soTheTagPaletteStaysGrouped() {
+               var names = new ArrayList<>(Theme.OPEN.getTokens().keySet());
+               assertEquals("--jc-font", names.get(0), () -> "Theme.OPEN's 
first declared token is not first, order: " + names);
+               var tags = names.stream().filter(x -> 
x.startsWith("--jc-tag-")).toList();
+               assertEquals(
+                       List.of(
+                               "--jc-tag-green-bg", "--jc-tag-green-text", 
"--jc-tag-green-border",
+                               "--jc-tag-blue-bg", "--jc-tag-blue-text", 
"--jc-tag-blue-border",
+                               "--jc-tag-amber-bg", "--jc-tag-amber-text", 
"--jc-tag-amber-border",
+                               "--jc-tag-neutral-bg", "--jc-tag-neutral-text", 
"--jc-tag-neutral-border",
+                               "--jc-tag-red-bg", "--jc-tag-red-text", 
"--jc-tag-red-border"),
+                       tags);
+       }
+
+       @Test void a03_repeatedBuildsOfTheSameTokenSet_produceTheSameOrder() {
+               assertEquals(new 
ArrayList<>(themeWithNames().getTokens().keySet()), new 
ArrayList<>(themeWithNames().getTokens().keySet()));
+       }
+
+       @Test void a04_reDeclaringAToken_updatesInPlace_withoutMovingIt() {
+               var theme = Theme.create("ordering")
+                       .token("--jc-accent", "#111111")
+                       .token("--jc-link", "#222222")
+                       .token("--jc-accent", "#333333")
+                       .build();
+               assertEquals(List.of("--jc-accent", "--jc-link"), new 
ArrayList<>(theme.getTokens().keySet()));
+               assertEquals("#333333", theme.getTokens().get("--jc-accent"));
+       }
+
+       @Test void a05_getTokens_isStillUnmodifiable() {
+               var tokens = Theme.OPEN.getTokens();
+               assertThrows(UnsupportedOperationException.class, () -> 
tokens.put("--jc-injected", "#000000"));
+       }
+
+       /** Mutating the builder after {@code build()} must not reach through 
into the built theme's copy. */
+       @Test void a06_builtTheme_isDecoupledFromLaterBuilderMutation() {
+               var b = Theme.create("ordering").token("--jc-accent", 
"#111111");
+               var theme = b.build();
+               b.token("--jc-link", "#222222");
+               assertEquals(List.of("--jc-accent"), new 
ArrayList<>(theme.getTokens().keySet()));
+       }
+}

Reply via email to