fix(ui): enforce accessible theme contrast

This commit is contained in:
sechmachine
2026-08-15 00:30:01 +07:00
parent 6d30b426aa
commit 3343745632
4 changed files with 151 additions and 8 deletions
@@ -6,6 +6,8 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
@@ -70,6 +72,28 @@ class UiContractWebTest {
assertTrue(themeBootstrap.contains("matchMedia('(prefers-color-scheme: dark)')"));
}
@Test
void themeTokensMeetTextFocusAndMeaningfulBoundaryContrast() throws Exception {
String css = new ClassPathResource("static/assets/app.css")
.getContentAsString(StandardCharsets.UTF_8);
String light = section(css, ":root\\{color-scheme:light;([^}]*)}");
String dark = section(css, ":root\\[data-theme=dark]\\{color-scheme:dark;([^}]*)}");
assertContrast(light, "ink", "canvas", 4.5);
assertContrast(light, "muted", "panel", 4.5);
assertContrast(light, "subtle", "sidebar", 4.5);
assertContrast(light, "border", "canvas", 3.0);
assertContrast(light, "border-strong", "panel", 3.0);
assertContrast(light, "focus", "canvas", 3.0);
assertContrast(dark, "ink", "canvas", 4.5);
assertContrast(dark, "muted", "panel", 4.5);
assertContrast(dark, "subtle", "sidebar", 4.5);
assertContrast(dark, "border", "panel", 3.0);
assertContrast(dark, "border-strong", "panel", 3.0);
assertContrast(dark, "focus", "panel", 3.0);
}
@Test
@WithMockUser(username = "admin@example.test", roles = "ADMIN")
void sharedComponentsExposeAccessibleFormsStatusAndEmptyState() throws Exception {
@@ -100,4 +124,48 @@ class UiContractWebTest {
return "test/components-consumer";
}
}
private static String section(String css, String expression) {
Matcher matcher = Pattern.compile(expression).matcher(css);
assertTrue(matcher.find(), () -> "Missing CSS token section: " + expression);
return matcher.group(1);
}
private static void assertContrast(String section, String foreground, String background, double minimum) {
double ratio = contrast(color(section, foreground), color(section, background));
assertTrue(ratio >= minimum,
() -> foreground + " / " + background + " contrast " + ratio + " is below " + minimum);
}
private static String color(String section, String name) {
Matcher matcher = Pattern.compile("--" + Pattern.quote(name) + ":(#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?)")
.matcher(section);
assertTrue(matcher.find(), () -> "Missing CSS color token: " + name);
String value = matcher.group(1);
if (value.length() == 4) {
return "#" + value.charAt(1) + value.charAt(1)
+ value.charAt(2) + value.charAt(2)
+ value.charAt(3) + value.charAt(3);
}
return value;
}
private static double contrast(String first, String second) {
double lighter = Math.max(luminance(first), luminance(second));
double darker = Math.min(luminance(first), luminance(second));
return (lighter + 0.05) / (darker + 0.05);
}
private static double luminance(String hex) {
double red = linear(Integer.parseInt(hex.substring(1, 3), 16) / 255.0);
double green = linear(Integer.parseInt(hex.substring(3, 5), 16) / 255.0);
double blue = linear(Integer.parseInt(hex.substring(5, 7), 16) / 255.0);
return 0.2126 * red + 0.7152 * green + 0.0722 * blue;
}
private static double linear(double component) {
return component <= 0.04045
? component / 12.92
: Math.pow((component + 0.055) / 1.055, 2.4);
}
}