rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

1package check
2
3import (
4 "fmt"
5 "math"
6 "regexp"
7 "slices"
8 "strconv"
9 "strings"
10 "testing"
11
12 "amadan.net/rastrillo/fiesta"
13)
14
15// The contrast gate in contrast_test.go measures token pairs. A button's
16// label does not sit on a token: it sits on the fill, under a face
17// gradient that lightens the top by --rst-depth-face and darkens the
18// bottom by the same, under a sheen while the pointer is on it, and
19// over a press that darkens by a wash plus a spot while it is held.
20// This file composes those points and holds each to
21// 4.5:1. The review computed 4.48:1 for a 15% sheen on the light
22// primary and the cap came down; if a value fails, lower the shade
23// or the sheen, never lighten the fill.
24
25func faceShadeShape(t *testing.T) {
26 t.Helper()
27 const shape = "linear-gradient(rgb(255 255 255 / var(--rst-depth-face, 0)), rgb(0 0 0 / var(--rst-depth-face, 0)))"
28 decls, _ := parseDeclarations(t, "tokens.css", string(fiesta.TokensCSS()))
29 if !slices.ContainsFunc(decls, func(d declaration) bool { return d.Property == "--rst-btn-face" && fullValue(d) == shape }) {
30 t.Fatal("tokens.css no longer shades a filled face with white at the top and black at the bottom by --rst-depth-face; this test measures that shape")
31 }
32}
33
34func alphaOf(t *testing.T, v string) float64 {
35 t.Helper()
36 v = strings.TrimSpace(v)
37 if v == "transparent" {
38 return 0
39 }
40 if strings.HasPrefix(v, "#") {
41 return 1
42 }
43 m := regexp.MustCompile(`rgba?\([^)]*?[,/]\s*([0-9.]+)\s*\)$`).FindStringSubmatch(v)
44 if m == nil {
45 t.Fatalf("cannot read an alpha from %q", v)
46 }
47 a, err := strconv.ParseFloat(m[1], 64)
48 if err != nil {
49 t.Fatal(err)
50 }
51 return a
52}
53
54func channelsOf(t *testing.T, v string) [3]float64 {
55 t.Helper()
56 v = strings.TrimSpace(v)
57 if v == "transparent" {
58 return [3]float64{}
59 }
60 if strings.HasPrefix(v, "#") {
61 r, g, b, err := parseHex(v)
62 if err != nil {
63 t.Fatal(err)
64 }
65 return [3]float64{float64(r), float64(g), float64(b)}
66 }
67 m := regexp.MustCompile(`rgba?\(\s*([0-9]+)[,\s]+([0-9]+)[,\s]+([0-9]+)`).FindStringSubmatch(v)
68 if m == nil {
69 t.Fatalf("cannot read channels from %q", v)
70 }
71 var out [3]float64
72 for i := range out {
73 n, _ := strconv.Atoi(m[i+1])
74 out[i] = float64(n)
75 }
76 return out
77}
78
79// over composites fg at alpha a over an opaque bg in sRGB, which is what
80// a gradient layer does, and returns hex for ContrastRatio.
81func over(t *testing.T, bg, fg string, a float64) string {
82 t.Helper()
83 B, F := channelsOf(t, bg), channelsOf(t, fg)
84 var out [3]uint8
85 for i := range out {
86 out[i] = uint8(math.Round(F[i]*a + B[i]*(1-a)))
87 }
88 return hexOf(out)
89}
90
91func hexChannels(t *testing.T, v string) string {
92 t.Helper()
93 c := channelsOf(t, v)
94 return fmt.Sprintf("#%02x%02x%02x", int(c[0]), int(c[1]), int(c[2]))
95}
96
97// pressNumber reads a number token, from the dark scheme blocks for
98// dark, and fails on anything that is not a number: a value that parsed
99// as 0 would make every press look safe. (Not "number": the package
100// imports golang.org/x/text/number.)
101func pressNumber(t *testing.T, theme, scheme, name string) float64 {
102 t.Helper()
103 v := themeTokens(t, theme)["light"][name]
104 if scheme == "dark" {
105 css, _ := fiesta.ThemeCSS(theme)
106 if dark, ok := schemeNumberBlocks(t, theme, string(css))[name]; ok {
107 v = dark
108 }
109 }
110 f, err := strconv.ParseFloat(v, 64)
111 if err != nil {
112 t.Fatalf("%s/%s %s = %q is not a number", theme, scheme, name, v)
113 }
114 return f
115}
116
117// TestButtonFacesClearContrastAtTheirLightestAndDarkestPoints holds
118// every composed face to 4.5:1.
119func TestButtonFacesClearContrastAtTheirLightestAndDarkestPoints(t *testing.T) {
120 faceShadeShape(t)
121 for _, theme := range fiesta.ThemeNames() {
122 for _, scheme := range []string{"light", "dark"} {
123 tok := resolvedTokens(t, theme)[scheme]
124 shade := pressNumber(t, theme, scheme, "--rst-depth-face")
125 check := func(label, text, face string) {
126 t.Helper()
127 r, err := ContrastRatio(text, face)
128 if err != nil {
129 t.Fatal(err)
130 }
131 if r < 4.5 {
132 t.Errorf("%s/%s %s: %s on the composed face %s = %.2f:1, want >= 4.5:1", theme, scheme, label, text, face, r)
133 }
134 t.Logf("%s/%s %s: %.2f:1", theme, scheme, label, r)
135 }
136 for _, v := range []struct{ name, fill, hover, text, wash, spot string }{
137 {"primary", tok["--rst-accent-fill"], tok["--rst-accent-fill-hover"], tok["--rst-on-accent-fill"], "--rst-press-wash", "--rst-press-spot"},
138 {"danger", tok["--rst-danger-fill"], tok["--rst-danger-fill-hover"], tok["--rst-on-danger-fill"], "--rst-press-wash", "--rst-press-spot"},
139 } {
140 lightest := over(t, over(t, v.fill, "#ffffff", shade), "#ffffff", alphaOf(t, tok["--rst-sheen-fill"]))
141 check(v.name+" lightest (top of the shade, full sheen)", v.text, lightest)
142 darkest := over(t, over(t, over(t, v.hover, "#000000", shade), "#000000", pressNumber(t, theme, scheme, v.wash)), "#000000", pressNumber(t, theme, scheme, v.spot))
143 check(v.name+" darkest (bottom of the shade, whole press)", v.text, darkest)
144 }
145 sheen := tok["--rst-sheen-plain"]
146 check("plain lightest (hovered, full sheen)", tok["--rst-control-text-hover"], over(t, tok["--rst-control-face-hover"], hexChannels(t, sheen), alphaOf(t, sheen)))
147 check("plain darkest (hovered, whole press)", tok["--rst-control-text-hover"], over(t, over(t, tok["--rst-control-face-hover"], "#000000", pressNumber(t, theme, scheme, "--rst-press-wash-plain")), "#000000", pressNumber(t, theme, scheme, "--rst-press-spot-plain")))
148 if tok["--rst-control-face-press"] != "transparent" {
149 check("ghost pressed", tok["--rst-text"], tok["--rst-control-face-press"])
150 }
151 }
152 }
153}
154