oficina / sheets Public

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

Plain git — no account needed to clone.

Download

Download this file

1package guards
2
3import (
4 "os"
5 "path/filepath"
6 "regexp"
7 "strings"
8 "testing"
9)
10
11// The vendored stylesheets, and ours.
12const (
13 vendorGrid = "internal/sheets/static/vendor/jspreadsheet.css"
14 vendorMenu = "internal/sheets/static/vendor/jsuites.css"
15 ourSheet = "internal/sheets/static/sheets.css"
16)
17
18// vendorPaint is every vendored rule that paints in a hard-coded colour
19// and that this app can actually render.
20//
21// sheets.css restated the vendored greys for the grid, the headers and
22// the range fill, and stopped there. It did not restate the four
23// highlight-* borders, the four copying-* borders, the four selection-*
24// borders, .jss_corner, or the selected row's header — so in dark mode
25// the range fill was drawn and its outline was not:
26//
27// - highlight-right/-bottom border 1px solid #000 → invisible on #111418.
28// - highlight-top/-left their border is overwritten by our own
29// `td { border-top-color: var(--rst-line) }`, one specificity step
30// higher, so the outline became an ordinary grid line — while their
31// decorative `box-shadow: -1px 0 #ccc` survived and became the one
32// bright edge on screen. A selection lit down its left side and
33// nowhere else.
34// - tr.selected > td:first-child #dcdcdc → a light slab in a dark
35// grid, carrying --rst-text-muted (#a8b0ba) at 1.60:1.
36// - .jss_corner a black knob ringed in white → reads inverted.
37// - jsuites' .jcontextmenu #fff with #555 text → a white card on a
38// dark grid, on every cell's right-click.
39//
40// The row header is the instructive one. sheets.css DID write an
41// override for it — `tbody > tr > td:first-child.selected` — but
42// Jspreadsheet puts `.selected` on the <tr>, not on its first cell, so
43// the rule matched nothing and looked like a fix. A CSS rule that
44// matches nothing fails exactly like a rule nobody wrote.
45//
46// Each entry is checked in BOTH directions: the vendored selector must
47// still exist (v5 renamed every `.jexcel` to `.jss_*` once already, and a
48// stale list like this one is how that rename goes unnoticed a second
49// time), and ours must restate it.
50//
51// Vendored rules with hard-coded colours that are deliberately NOT here.
52// Each reason is checkable against the code, and if one stops holding,
53// the rule belongs in the list:
54//
55// .readonly no column sets readOnly and nothing calls
56// setReadOnly; grid.js contains neither word.
57// .jss_freezed grid.js passes no freezeColumns or freezeRows.
58// .jss_pagination a sheet is one page; pagination is off.
59// .jupload, editors we do not enable — the cell editor is the
60// .jss_richtext plain text one.
61// .jss_content ::-webkit-scrollbar-track/-thumb, #eee and #666.
62// scrollbar Measured in Chromium: .jss_content computes
63// `overflow: visible` with scrollWidth ==
64// clientWidth, so it never has a scrollbar of its
65// own. The grid scrolls on OUR [data-sheet-grid]
66// (overflow: auto), whose bars the browser paints
67// from color-scheme.
68var vendorPaint = []struct {
69 file string // the vendored stylesheet it lives in
70 vendor string // must still exist in that stylesheet
71 restated string // sheets.css must carry this selector, in a RULE not a comment
72}{
73 {vendorGrid, ".jss_worksheet > thead > tr > td.selected", "> thead > tr > td.selected"},
74 {vendorGrid, ".jss_worksheet > tbody > tr.selected > td:first-child", "> tbody > tr.selected > td:first-child"},
75
76 {vendorGrid, ".jss_worksheet .highlight ", "> tbody > tr > td.highlight "},
77 {vendorGrid, ".jss_worksheet .highlight-top", "> tbody > tr > td.highlight-top"},
78 {vendorGrid, ".jss_worksheet .highlight-left", "> tbody > tr > td.highlight-left"},
79 {vendorGrid, ".jss_worksheet .highlight-right", "> tbody > tr > td.highlight-right"},
80 {vendorGrid, ".jss_worksheet .highlight-bottom", "> tbody > tr > td.highlight-bottom"},
81 {vendorGrid, ".jss_worksheet .highlight-selected", "> tbody > tr > td.highlight-selected"},
82 {vendorGrid, ".jss_worksheet > tbody > tr > td.highlight > a", "> tbody > tr > td.highlight > a"},
83
84 {vendorGrid, ".jss_worksheet .selection\n", "> tbody > tr > td.selection "},
85 {vendorGrid, ".jss_worksheet .selection-top", "> tbody > tr > td.selection-top"},
86 {vendorGrid, ".jss_worksheet .selection-bottom", "> tbody > tr > td.selection-bottom"},
87 {vendorGrid, ".jss_worksheet .selection-left", "> tbody > tr > td.selection-left"},
88 {vendorGrid, ".jss_worksheet .selection-right", "> tbody > tr > td.selection-right"},
89
90 {vendorGrid, ".jss_worksheet .copying-top", "> tbody > tr > td.copying-top"},
91 {vendorGrid, ".jss_worksheet .copying-bottom", "> tbody > tr > td.copying-bottom"},
92 {vendorGrid, ".jss_worksheet .copying-left", "> tbody > tr > td.copying-left"},
93 {vendorGrid, ".jss_worksheet .copying-right", "> tbody > tr > td.copying-right"},
94
95 {vendorGrid, ".jss_corner", "[data-sheet-grid] .jss_corner"},
96
97 {vendorMenu, ".jcontextmenu {", "[data-sheet-grid] .jcontextmenu {"},
98 {vendorMenu, ".jcontextmenu > div a", "[data-sheet-grid] .jcontextmenu > div a"},
99 {vendorMenu, ".jcontextmenu > div:hover", "[data-sheet-grid] .jcontextmenu > div:hover"},
100 {vendorMenu, ".jcontextmenu .jcontextmenu-disabled a", ".jcontextmenu .jcontextmenu-disabled a"},
101 {vendorMenu, ".jcontextmenu hr", "[data-sheet-grid] .jcontextmenu hr"},
102}
103
104// comments strips /* … */ so a selector NAMED in an explanatory comment
105// cannot stand in for the rule itself.
106//
107// This is not hypothetical tidying. The first draft of these tests
108// matched bare `.highlight-right` against the whole file, and the
109// paragraph above the rules contains the line
110// `.highlight-right/-bottom border: 1px solid #000` — so deleting the
111// actual rule left every guard green and the reported bug back. The
112// prose describing a bug matched the test looking for its fix.
113var comments = regexp.MustCompile(`(?s)/\*.*?\*/`)
114
115func rulesOnly(t *testing.T, rel string) string {
116 t.Helper()
117 body, err := os.ReadFile(filepath.Join(repoRoot(t), rel))
118 if err != nil {
119 t.Fatalf("guards: read %s: %v", rel, err)
120 }
121 // The vendored files are CRLF and ours is LF; normalise so an entry
122 // above can anchor on a line break without caring which.
123 return comments.ReplaceAllString(strings.ReplaceAll(string(body), "\r\n", "\n"), "")
124}
125
126func TestVendorPaintIsRestated(t *testing.T) {
127 ours := rulesOnly(t, ourSheet)
128 vendors := map[string]string{
129 vendorGrid: rulesOnly(t, vendorGrid),
130 vendorMenu: rulesOnly(t, vendorMenu),
131 }
132
133 for _, c := range vendorPaint {
134 if !strings.Contains(vendors[c.file], c.vendor) {
135 t.Errorf("%s: no rule for %q — the vendored library renamed or dropped it "+
136 "(v5 renamed every .jexcel to .jss_*). Re-derive this list from the "+
137 "vendored stylesheet; deleting the entry hides the rule, it does not fix it",
138 c.file, c.vendor)
139 continue
140 }
141 if !strings.Contains(ours, c.restated) {
142 t.Errorf("%s: %s is painted in a hard-coded colour by %s and no RULE here restates it — "+
143 "a literal does not follow the person's light/dark choice, so this surface "+
144 "is wrong in one of the two schemes. (Comments are stripped before this "+
145 "check: naming the selector in prose does not count.)",
146 ourSheet, c.restated, c.file)
147 }
148 }
149}
150
151// The vendored stylesheets are linked from sheet.html's head block,
152// which layout.html renders AFTER sheets.css — so the library is later
153// in source order and an EQUAL-specificity restatement loses. Ours has
154// to be strictly higher, which for the grid means carrying the same
155// `> tbody > tr > td` the grid-line rule carries. That is why every
156// `restated` string above is the full scoped selector rather than the
157// bare class: written short, the rule parses, uses a token, and paints
158// nothing.
159//
160// The limit of this check, stated so nobody trusts it further than it
161// goes: it compares strings, not specificity. If the grid-line rule at
162// the top of sheets.css ever gains a step of its own — an added class,
163// a `:not()`, a misused `:where()` — these selectors stay green and stop
164// winning. Recount by hand when you touch that rule.
165func TestVendorLinksLoadAfterOurs(t *testing.T) {
166 root := repoRoot(t)
167 layout, err := os.ReadFile(filepath.Join(root, "internal/sheets/templates/layout.html"))
168 if err != nil {
169 t.Skipf("guards: read layout.html: %v", err)
170 }
171 sheet, err := os.ReadFile(filepath.Join(root, "internal/sheets/templates/sheet.html"))
172 if err != nil {
173 t.Skipf("guards: read sheet.html: %v", err)
174 }
175 ours := strings.Index(string(layout), "sheets.css")
176 head := strings.Index(string(layout), `"head"`)
177 if ours < 0 || head < 0 {
178 t.Skip("guards: layout.html no longer links sheets.css before a head block")
179 }
180 if !strings.Contains(string(sheet), "jspreadsheet.css") {
181 t.Skip("guards: sheet.html no longer links the vendored stylesheet")
182 }
183 if ours > head {
184 t.Errorf("layout.html: sheets.css is linked after the head block that carries the "+
185 "vendored stylesheets (%d > %d). That reverses the load order these overrides "+
186 "were counted against — recheck every restatement in %s, since ties now go "+
187 "to us and some may be over-specified", ours, head, ourSheet)
188 }
189}
190
191// TestOurStylesheetHasNoColourLiterals is the other half of the token
192// rule, and the one the token-name check cannot make.
193//
194// csstokens_test.go proves every `var(--rst-…)` we use is defined. It
195// says nothing about a declaration that skips var() altogether:
196// `border-right-color: #7ba7ff` passes that check, passes the
197// restatement check above, and is wrong in light mode forever.
198func TestOurStylesheetHasNoColourLiterals(t *testing.T) {
199 css := rulesOnly(t, ourSheet)
200 // oklch() and color() are here because the app now paints cell fills
201 // with them. A LITERAL oklch — `oklch(0.92 0.08 145)` — is exactly
202 // the defect this test exists for and the original pattern could not
203 // see it. The legitimate uses all resolve a custom property, so the
204 // check is for a bare number where the lightness goes.
205 literal := regexp.MustCompile(`(?i)(#[0-9a-f]{3,8}\b|\brgba?\(|\bhsla?\(|` +
206 `\b(?:oklch|oklab|lch|lab|color)\(\s*[0-9.]|` +
207 `:\s*(red|blue|green|black|white|gray|grey|orange|yellow|purple)\b)`)
208 for i, line := range strings.Split(css, "\n") {
209 if m := literal.FindString(line); m != "" {
210 t.Errorf("%s:%d: colour literal %q — every colour here is a token, never a "+
211 "literal, because a literal cannot follow the person's light/dark choice. "+
212 "Line: %s", ourSheet, i+1, m, strings.TrimSpace(line))
213 }
214 }
215}
216