internal/httpd/palette_test.go

c7cebb36f27f7d136074c5392f87231786a06d56
gitbay/internal/httpd/palette_test.go history · blame · raw

96 lines · 3031 bytes

 1package httpd
 2
 3import (
 4	"math"
 5	"testing"
 6
 7	"github.com/alecthomas/chroma/v2"
 8	"github.com/alecthomas/chroma/v2/styles"
 9)
10
11func channel(v float64) float64 {
12	if v <= 0.03928 {
13		return v / 12.92
14	}
15	return math.Pow((v+0.055)/1.055, 2.4)
16}
17
18func luminance(c chroma.Colour) float64 {
19	return 0.2126*channel(float64(c.Red())/255) +
20		0.7152*channel(float64(c.Green())/255) +
21		0.0722*channel(float64(c.Blue())/255)
22}
23
24func contrast(a, b chroma.Colour) float64 {
25	hi, lo := luminance(a), luminance(b)
26	if hi < lo {
27		hi, lo = lo, hi
28	}
29	return (hi + 0.05) / (lo + 0.05)
30}
31
32// TestSyntaxPaletteContrast holds both palettes to WCAG AA for body text
33// against every ground code sits on here: the page, a code block, and the
34// two diff tints. The chroma default (friendly) put 61 token/ground pairs
35// under the floor, which is why the styles are chosen rather than assumed.
36//
37// Tokens the stylesheet overrides after writing the palette are exempt,
38// since this test cannot see those rules: NameAttribute in light
39// (#6f5a21) and the line-number tokens in both (var(--muted)).
40func TestSyntaxPaletteContrast(t *testing.T) {
41	const floor = 4.5
42	// chroma's own line-number gutter renders on blob pages only — the diff
43	// supplies its own gutter — so those tokens are checked against the page
44	// and code grounds, never against a diff tint.
45	gutterOnly := map[chroma.TokenType]bool{
46		chroma.LineNumbers: true, chroma.LineNumbersTable: true,
47		chroma.LineHighlight: true, chroma.LineTable: true, chroma.LineTableTD: true,
48	}
49	cases := []struct {
50		style   string
51		grounds map[string]chroma.Colour
52		exempt  map[chroma.TokenType]bool
53	}{
54		{lightStyle, map[string]chroma.Colour{
55			"page": chroma.NewColour(0xff, 0xff, 0xff),
56			"code": chroma.NewColour(0xf5, 0xf5, 0xf5),
57			"add":  chroma.NewColour(0xe4, 0xf6, 0xea),
58			"del":  chroma.NewColour(0xfd, 0xea, 0xea),
59		}, map[chroma.TokenType]bool{chroma.NameAttribute: true}},
60		{darkStyle, map[string]chroma.Colour{
61			"page": chroma.NewColour(0x0a, 0x0a, 0x0a),
62			"code": chroma.NewColour(0x05, 0x05, 0x05),
63			"add":  chroma.NewColour(0x0d, 0x2a, 0x18),
64			"del":  chroma.NewColour(0x2c, 0x11, 0x13),
65		}, map[chroma.TokenType]bool{
66			chroma.LineNumbers: true, chroma.LineNumbersTable: true,
67		}},
68	}
69
70	for _, tc := range cases {
71		st := styles.Get(tc.style)
72		if st == nil || st.Name != tc.style {
73			t.Fatalf("chroma style %q is not available", tc.style)
74		}
75		for _, tt := range st.Types() {
76			// Whitespace markers are meant to be near-invisible, and the
77			// background entry is a ground, not text.
78			if tt == chroma.TextWhitespace || tt == chroma.Background || tc.exempt[tt] {
79				continue
80			}
81			e := st.Get(tt)
82			if !e.Colour.IsSet() {
83				continue
84			}
85			for name, ground := range tc.grounds {
86				if gutterOnly[tt] && (name == "add" || name == "del") {
87					continue
88				}
89				if got := contrast(e.Colour, ground); got < floor {
90					t.Errorf("%s: %s (%s) on %s is %.2f:1, want >= %.1f",
91						tc.style, tt, e.Colour, name, got, floor)
92				}
93			}
94		}
95	}
96}