initial commit
Some checks failed
ci / go (push) Has been cancelled
ci / admin-ui (push) Has been cancelled
ci / runtime-sdk (push) Has been cancelled
ci / helm (push) Has been cancelled
ci / openapi (push) Has been cancelled
ci / container (push) Has been cancelled
ci / release-config (push) Has been cancelled
ci / shared styling (push) Has been cancelled
ci / security (push) Has been cancelled
ci / all green (push) Has been cancelled
Some checks failed
ci / go (push) Has been cancelled
ci / admin-ui (push) Has been cancelled
ci / runtime-sdk (push) Has been cancelled
ci / helm (push) Has been cancelled
ci / openapi (push) Has been cancelled
ci / container (push) Has been cancelled
ci / release-config (push) Has been cancelled
ci / shared styling (push) Has been cancelled
ci / security (push) Has been cancelled
ci / all green (push) Has been cancelled
This commit is contained in:
commit
951dd8d26b
175 changed files with 50041 additions and 0 deletions
216
internal/tokenengine/color_test.go
Normal file
216
internal/tokenengine/color_test.go
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
package tokenengine
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseColorFormats(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
want Color
|
||||
}{
|
||||
{"hex6", "#1a2b3c", Color{RGB: RGB{R: 0x1a / 255.0, G: 0x2b / 255.0, B: 0x3c / 255.0, A: 1}, Raw: "#1a2b3c", Numeric: true}},
|
||||
{"hex3", "#abc", Color{RGB: RGB{R: 0xaa / 255.0, G: 0xbb / 255.0, B: 0xcc / 255.0, A: 1}, Raw: "#abc", Numeric: true}},
|
||||
{"hex8 alpha", "#00000080", Color{RGB: RGB{R: 0, G: 0, B: 0, A: 128 / 255.0}, Raw: "#00000080", Numeric: true}},
|
||||
{"rgb legacy comma", "rgb(255, 0, 0)", Color{RGB: RGB{R: 1, G: 0, B: 0, A: 1}, Raw: "rgb(255, 0, 0)", Numeric: true}},
|
||||
{"rgb modern space", "rgb(255 0 0)", Color{RGB: RGB{R: 1, G: 0, B: 0, A: 1}, Raw: "rgb(255 0 0)", Numeric: true}},
|
||||
{"rgba alpha", "rgba(0, 0, 0, 0.5)", Color{RGB: RGB{A: 0.5}, Raw: "rgba(0, 0, 0, 0.5)", Numeric: true}},
|
||||
{"hsl", "hsl(0, 100%, 50%)", Color{RGB: RGB{R: 1, G: 0, B: 0, A: 1}, Raw: "hsl(0, 100%, 50%)", Numeric: true}},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := ParseColor(tc.input)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseColor(%q) error: %v", tc.input, err)
|
||||
}
|
||||
if !got.Numeric {
|
||||
t.Fatalf("ParseColor(%q) returned a non-numeric colour", tc.input)
|
||||
}
|
||||
assertClose(t, got.R, tc.want.R, 0.002)
|
||||
assertClose(t, got.G, tc.want.G, 0.002)
|
||||
assertClose(t, got.B, tc.want.B, 0.002)
|
||||
assertClose(t, got.A, tc.want.A, 0.002)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseColorRejectsGarbage(t *testing.T) {
|
||||
for _, bad := range []string{"", "#", "#12", "#1234567", "rgb(1,2)", "#gggggg"} {
|
||||
if _, err := ParseColor(bad); err == nil {
|
||||
t.Errorf("ParseColor(%q) should have failed", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseColorPassesThroughModernSyntax(t *testing.T) {
|
||||
// These are valid CSS the engine cannot reason about numerically, but they
|
||||
// must round-trip rather than be rejected.
|
||||
for _, ok := range []string{"oklch(0.7 0.1 200)", "color(display-p3 1 0 0)", "light-dark(#fff, #000)", "currentColor", "transparent"} {
|
||||
got, err := ParseColor(ok)
|
||||
if err != nil {
|
||||
t.Errorf("ParseColor(%q) error: %v", ok, err)
|
||||
}
|
||||
if got.Numeric {
|
||||
t.Errorf("ParseColor(%q) should not be numeric", ok)
|
||||
}
|
||||
if got.Raw != ok {
|
||||
t.Errorf("ParseColor(%q).Raw = %q, want the original", ok, got.Raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestContrastRatioMatchesWCAGReferenceValues(t *testing.T) {
|
||||
// Reference values from the WCAG 2.1 relative-luminance definition.
|
||||
cases := []struct {
|
||||
a, b string
|
||||
want float64
|
||||
}{
|
||||
{"#ffffff", "#000000", 21},
|
||||
{"#ffffff", "#ffffff", 1},
|
||||
{"#000000", "#000000", 1},
|
||||
{"#777777", "#ffffff", 4.48},
|
||||
{"#767676", "#ffffff", 4.54}, // the canonical AA boundary grey
|
||||
}
|
||||
for _, tc := range cases {
|
||||
a, err := ParseColor(tc.a)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
b, err := ParseColor(tc.b)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
got := ContrastRatio(a, b)
|
||||
if math.Abs(got-tc.want) > 0.05 {
|
||||
t.Errorf("ContrastRatio(%s, %s) = %.3f, want %.3f", tc.a, tc.b, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBestForegroundPicksHigherContrast(t *testing.T) {
|
||||
light := Color{RGB: RGB{R: 1, G: 1, B: 1, A: 1}, Numeric: true}
|
||||
dark := Color{RGB: RGB{A: 1}, Numeric: true}
|
||||
|
||||
if got := BestForeground(Color{RGB: RGB{A: 1}, Numeric: true}); got.R != light.R {
|
||||
t.Errorf("foreground for black should be white, got %s", got.Hex())
|
||||
}
|
||||
if got := BestForeground(Color{RGB: RGB{R: 1, G: 1, B: 1, A: 1}, Numeric: true}); got.R != dark.R {
|
||||
t.Errorf("foreground for white should be black, got %s", got.Hex())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHexRoundTrip(t *testing.T) {
|
||||
for _, hex := range []string{"#000000", "#ffffff", "#4f46e5", "#4f46e580"} {
|
||||
c, err := ParseColor(hex)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
if got := c.Hex(); got != hex {
|
||||
t.Errorf("round trip of %q produced %q", hex, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMixProducesIntermediateColour(t *testing.T) {
|
||||
black, _ := ParseColor("#000000")
|
||||
white, _ := ParseColor("#ffffff")
|
||||
|
||||
mid := black.Mix(white, 0.5)
|
||||
if got := mid.Hex(); got != "#808080" && got != "#7f7f7f" {
|
||||
t.Errorf("50%% black/white mix = %s, want roughly #808080", got)
|
||||
}
|
||||
if got := black.Mix(white, 0).Hex(); got != "#000000" {
|
||||
t.Errorf("0%% mix = %s, want #000000", got)
|
||||
}
|
||||
if got := black.Mix(white, 1).Hex(); got != "#ffffff" {
|
||||
t.Errorf("100%% mix = %s, want #ffffff", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLightenDarkInvertsDirectionForDarkSchemes(t *testing.T) {
|
||||
base, _ := ParseColor("#4f46e5")
|
||||
|
||||
light := base.LightenDark(0.1, false)
|
||||
dark := base.LightenDark(0.1, true)
|
||||
|
||||
if light.RelativeLuminance() >= base.RelativeLuminance() {
|
||||
t.Error("light scheme emphasis should reduce luminance")
|
||||
}
|
||||
if dark.RelativeLuminance() <= base.RelativeLuminance() {
|
||||
t.Error("dark scheme emphasis should increase luminance")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateRampIsOrderedAndComplete(t *testing.T) {
|
||||
ramp := GenerateRamp(DefaultRampOptions())
|
||||
|
||||
shades := StandardPaletteShades()
|
||||
if len(ramp) != len(shades) {
|
||||
t.Fatalf("ramp has %d steps, want %d", len(ramp), len(shades))
|
||||
}
|
||||
|
||||
previous := math.Inf(1)
|
||||
for _, shade := range shades {
|
||||
leaf, ok := ramp[shade].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("shade %s is not a token leaf", shade)
|
||||
}
|
||||
value, ok := leaf[ValueKey].(string)
|
||||
if !ok {
|
||||
t.Fatalf("shade %s has no string value", shade)
|
||||
}
|
||||
col, err := ParseColor(value)
|
||||
if err != nil {
|
||||
t.Fatalf("shade %s value %q is not a colour: %v", shade, value, err)
|
||||
}
|
||||
luminance := col.RelativeLuminance()
|
||||
if luminance > previous {
|
||||
t.Errorf("shade %s is lighter than the preceding shade", shade)
|
||||
}
|
||||
previous = luminance
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateRampNormalisesOutOfRangeInput(t *testing.T) {
|
||||
opts := RampOptions{Hue: 400, Saturation: -20, Lightness: 999, HueSpread: 1000}.Normalize()
|
||||
|
||||
if opts.Hue < 0 || opts.Hue >= 360 {
|
||||
t.Errorf("hue = %f, want it wrapped into 0..360", opts.Hue)
|
||||
}
|
||||
if opts.Saturation != 0 {
|
||||
t.Errorf("saturation = %f, want it clamped to 0", opts.Saturation)
|
||||
}
|
||||
if opts.Lightness != 80 {
|
||||
t.Errorf("lightness = %f, want it clamped to 80", opts.Lightness)
|
||||
}
|
||||
if opts.HueSpread != 40 {
|
||||
t.Errorf("hueSpread = %f, want it clamped to 40", opts.HueSpread)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatNumberIsStable(t *testing.T) {
|
||||
cases := map[float64]string{
|
||||
0: "0",
|
||||
1: "1",
|
||||
1.5: "1.5",
|
||||
0.1250001: "0.125",
|
||||
-0.5: "-0.5",
|
||||
100: "100",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := FormatNumber(in); got != want {
|
||||
t.Errorf("FormatNumber(%v) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// assertClose fails the test when want and got differ by more than tolerance.
|
||||
func assertClose(t *testing.T, got, want, tolerance float64) {
|
||||
t.Helper()
|
||||
if math.Abs(got-want) > tolerance {
|
||||
t.Errorf("got %v, want %v (tolerance %v)", got, want, tolerance)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue