internal/httpd/chip_test.go
106 lines · 3370 bytes
1package httpd
2
3import (
4 "fmt"
5 "math"
6 "regexp"
7 "strconv"
8 "strings"
9 "testing"
10
11 "gitbay.org/gitbay/internal/web"
12)
13
14// relLum is WCAG relative luminance of a #rrggbb colour.
15func relLum(hex string) float64 {
16 lin := func(s string) float64 {
17 n, _ := strconv.ParseInt(s, 16, 32)
18 v := float64(n) / 255
19 if v <= 0.04045 {
20 return v / 12.92
21 }
22 return math.Pow((v+0.055)/1.055, 2.4)
23 }
24 return 0.2126*lin(hex[1:3]) + 0.7152*lin(hex[3:5]) + 0.0722*lin(hex[5:7])
25}
26
27// ground mixes a tenth of the chip colour into the canvas the way
28// color-mix(in srgb, chip 10%, canvas) does, and ratio is WCAG contrast.
29func ground(chip, canvas string) string {
30 ch := func(a, b string) string {
31 na, _ := strconv.ParseInt(a, 16, 32)
32 nb, _ := strconv.ParseInt(b, 16, 32)
33 return fmt.Sprintf("%02x", int(math.Round(0.1*float64(na)+0.9*float64(nb))))
34 }
35 return "#" + ch(chip[1:3], canvas[1:3]) + ch(chip[3:5], canvas[3:5]) + ch(chip[5:7], canvas[5:7])
36}
37
38func ratio(a, b string) float64 {
39 la, lb := relLum(a), relLum(b)
40 if la < lb {
41 la, lb = lb, la
42 }
43 return (la + 0.05) / (lb + 0.05)
44}
45
46// A label colour is 12px text on a ground mixed from itself, so it needs
47// 4.5:1 there — a ratio no one colour reaches on both the light and the
48// dark canvas. Each label carries a tone per scheme (#226).
49func TestChipTones(t *testing.T) {
50 cases := append([]string{}, labelPalette...)
51 cases = append(cases, "#ffff00", "#FFFFFF", "#000000", "#ffcccc", "#00ff00", "#101010", "#0000ff", "#8250df", "#bf5b16")
52 for _, in := range cases {
53 light, dark := chipTones(in)
54 if got := ratio(light, ground(light, chipCanvasLight)); got < 4.5 {
55 t.Errorf("chipTones(%s) light = %s: %.2f:1 on its ground", in, light, got)
56 }
57 if got := ratio(dark, ground(dark, chipCanvasDark)); got < 4.5 {
58 t.Errorf("chipTones(%s) dark = %s: %.2f:1 on its ground", in, dark, got)
59 }
60 }
61
62 // The hue survives the move wherever scaling can reach the ratio: a
63 // purple stays a purple, not a grey, in both schemes.
64 light, dark := chipTones("#8250df")
65 for _, tone := range []string{light, dark} {
66 chans := func(s string) (r, g, b int64) {
67 for i, p := range []*int64{&r, &g, &b} {
68 v, _ := strconv.ParseInt(s[1+2*i:3+2*i], 16, 32)
69 *p = v
70 }
71 return
72 }
73 r, g, b := chans(tone)
74 if !(b > r && r > g) {
75 t.Errorf("#8250df became %s: %d red, %d green, %d blue is no longer a purple", tone, r, g, b)
76 }
77 }
78
79 // A colour that already clears the ratio is left alone.
80 if light, _ := chipTones("#3b2178"); light != "#3b2178" {
81 t.Errorf("a colour that already passes on the light canvas moved to %s", light)
82 }
83}
84
85// The canvases the tones are computed against are the stylesheet's, in
86// both schemes: a token moves and these constants move with it.
87func TestChipCanvasMatchesStylesheet(t *testing.T) {
88 s := string(web.StyleCSS)
89 dark := strings.Index(s, "@media (prefers-color-scheme: dark)")
90 if dark < 0 {
91 t.Fatal("no dark media query in style.css")
92 }
93 re := regexp.MustCompile(`--canvas:\s*(#[0-9a-f]{6});`)
94 for _, c := range []struct{ scheme, in, want string }{
95 {"light", s[:dark], chipCanvasLight},
96 {"dark", s[dark:], chipCanvasDark},
97 } {
98 m := re.FindStringSubmatch(c.in)
99 if m == nil {
100 t.Fatalf("%s: no --canvas token", c.scheme)
101 }
102 if m[1] != c.want {
103 t.Errorf("%s: style.css --canvas is %s, the chip tones use %s", c.scheme, m[1], c.want)
104 }
105 }
106}