internal/httpd/chip_test.go

f327db6192d9a0877606a40385b24c0cda29fd2d
gitbay/internal/httpd/chip_test.go history · blame · raw

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}