oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1package sheets
2
3import (
4 "os"
5 "path/filepath"
6 "strconv"
7 "strings"
8 "testing"
9
10 "amadan.net/oficina/sheets/internal/sheets/xlsx"
11)
12
13// The lightnesses sheets.css resolves --sheet-fill-l and --sheet-ink-l
14// to, restated here.
15//
16// Restated, not shared, because CSS custom properties are not readable
17// from Go and the alternative — generating the stylesheet — would put a
18// build step in an app whose frontend rule is that there is none. So
19// this is a second place to remember, and TestLightnessesMatchTheStylesheet
20// below is what stops the two drifting: it greps sheets.css for the
21// declarations and fails if either differs.
22const (
23 fillLightnessLight = 0.92
24 fillLightnessDark = 0.30
25 inkLightnessLight = 0.42
26 inkLightnessDark = 0.80
27)
28
29// TestEverySwatchClearsAA is the gate on the palette this app OFFERS.
30//
31// The app warns about imported fills that pair with ink below 4.5:1
32// (contrast.go, spec §11.1). Shipping a picker whose own combinations
33// fail that floor would be this app committing the defect it complains
34// of — and it would fail silently, because a low-contrast cell renders
35// perfectly, it is just unreadable.
36//
37// Every ink against every fill, at both themes' lightnesses: 81 pairs
38// per theme, because a person may pick any ink on any fill and the
39// palette makes no combination harder to reach than another.
40//
41// **Ink on an UNFILLED cell is deliberately not gated here.** That pair
42// is ink against the theme's own background token, and checking it in
43// Go would mean copying rastrillo's palette hexes into this repo — a
44// coupling nothing watches, which would go stale the first time the
45// theme moved and would then be a test asserting the wrong thing while
46// passing. It is checked in the browser drive instead, against the
47// colour that actually paints.
48func TestEverySwatchClearsAA(t *testing.T) {
49 for _, theme := range []struct {
50 name string
51 fillL, inkL float64
52 }{
53 {"light", fillLightnessLight, inkLightnessLight},
54 {"dark", fillLightnessDark, inkLightnessDark},
55 } {
56 t.Run(theme.name, func(t *testing.T) {
57 for _, fill := range fillSwatches {
58 for _, ink := range inkSwatches {
59 fillHex := xlsx.IntentHex(theme.fillL, fill.C, fill.H)
60 inkHex := xlsx.IntentHex(theme.inkL, ink.C, ink.H)
61 ratio, ok := contrastRatio(fillHex, inkHex)
62 if !ok {
63 t.Fatalf("%s ink on %s fill: could not measure %q on %q",
64 ink.Name, fill.Name, inkHex, fillHex)
65 }
66 if ratio < aaFloor {
67 t.Errorf("%s ink on %s fill = %.2f:1, below AA's %.1f:1 "+
68 "(%s on %s) — lower the chroma or move the lightness apart",
69 ink.Name, fill.Name, ratio, aaFloor, inkHex, fillHex)
70 }
71 }
72 }
73 })
74 }
75}
76
77// TestLightnessesMatchTheStylesheet pins the four numbers above to the
78// four in sheets.css.
79//
80// They are declared in two places because CSS custom properties are not
81// readable from Go and the app has no build step to generate one from
82// the other — a rule its frontend section states outright. So the
83// duplication is deliberate and this is what makes it safe: change
84// --sheet-fill-l in the stylesheet without changing the constant here
85// and the contrast gate above would go on measuring the old value,
86// passing while the shipped palette failed.
87func TestLightnessesMatchTheStylesheet(t *testing.T) {
88 body, err := os.ReadFile(filepath.Join("static", "sheets.css"))
89 if err != nil {
90 t.Fatalf("read sheets.css: %v", err)
91 }
92 css := string(body)
93
94 for _, want := range []struct {
95 prop string
96 value float64
97 }{
98 {"--sheet-fill-l", fillLightnessLight},
99 {"--sheet-ink-l", inkLightnessLight},
100 {"--sheet-fill-l", fillLightnessDark},
101 {"--sheet-ink-l", inkLightnessDark},
102 } {
103 decl := want.prop + ": " + strconv.FormatFloat(want.value, 'f', 2, 64) + ";"
104 if !strings.Contains(css, decl) {
105 t.Errorf("sheets.css does not declare %q — the contrast gate in this file "+
106 "measures a lightness the stylesheet no longer uses", decl)
107 }
108 }
109
110 // Both dark blocks must exist. The media query alone leaves an
111 // explicit dark choice unstyled; the [data-theme="dark"] rule alone
112 // leaves "follow the OS" — the default — unstyled. layout.html
113 // writes NO attribute for auto, which is the mechanism, so the pair
114 // is required and neither is redundant.
115 for _, guard := range []string{
116 `:root:not([data-theme="light"])`,
117 `:root[data-theme="dark"]`,
118 } {
119 if !strings.Contains(css, guard) {
120 t.Errorf("sheets.css has no %s block, so one of the two ways a person "+
121 "can be in dark mode paints fills at the light lightness", guard)
122 }
123 }
124}
125