217 lines
6.5 KiB
Go
217 lines
6.5 KiB
Go
|
|
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)
|
||
|
|
}
|
||
|
|
}
|