package guards import ( "os" "path/filepath" "regexp" "strings" "testing" ) // The vendored stylesheets, and ours. const ( vendorGrid = "internal/sheets/static/vendor/jspreadsheet.css" vendorMenu = "internal/sheets/static/vendor/jsuites.css" ourSheet = "internal/sheets/static/sheets.css" ) // vendorPaint is every vendored rule that paints in a hard-coded colour // and that this app can actually render. // // sheets.css restated the vendored greys for the grid, the headers and // the range fill, and stopped there. It did not restate the four // highlight-* borders, the four copying-* borders, the four selection-* // borders, .jss_corner, or the selected row's header — so in dark mode // the range fill was drawn and its outline was not: // // - highlight-right/-bottom border 1px solid #000 → invisible on #111418. // - highlight-top/-left their border is overwritten by our own // `td { border-top-color: var(--rst-line) }`, one specificity step // higher, so the outline became an ordinary grid line — while their // decorative `box-shadow: -1px 0 #ccc` survived and became the one // bright edge on screen. A selection lit down its left side and // nowhere else. // - tr.selected > td:first-child #dcdcdc → a light slab in a dark // grid, carrying --rst-text-muted (#a8b0ba) at 1.60:1. // - .jss_corner a black knob ringed in white → reads inverted. // - jsuites' .jcontextmenu #fff with #555 text → a white card on a // dark grid, on every cell's right-click. // // The row header is the instructive one. sheets.css DID write an // override for it — `tbody > tr > td:first-child.selected` — but // Jspreadsheet puts `.selected` on the , not on its first cell, so // the rule matched nothing and looked like a fix. A CSS rule that // matches nothing fails exactly like a rule nobody wrote. // // Each entry is checked in BOTH directions: the vendored selector must // still exist (v5 renamed every `.jexcel` to `.jss_*` once already, and a // stale list like this one is how that rename goes unnoticed a second // time), and ours must restate it. // // Vendored rules with hard-coded colours that are deliberately NOT here. // Each reason is checkable against the code, and if one stops holding, // the rule belongs in the list: // // .readonly no column sets readOnly and nothing calls // setReadOnly; grid.js contains neither word. // .jss_freezed grid.js passes no freezeColumns or freezeRows. // .jss_pagination a sheet is one page; pagination is off. // .jupload, editors we do not enable — the cell editor is the // .jss_richtext plain text one. // .jss_content ::-webkit-scrollbar-track/-thumb, #eee and #666. // scrollbar Measured in Chromium: .jss_content computes // `overflow: visible` with scrollWidth == // clientWidth, so it never has a scrollbar of its // own. The grid scrolls on OUR [data-sheet-grid] // (overflow: auto), whose bars the browser paints // from color-scheme. var vendorPaint = []struct { file string // the vendored stylesheet it lives in vendor string // must still exist in that stylesheet restated string // sheets.css must carry this selector, in a RULE not a comment }{ {vendorGrid, ".jss_worksheet > thead > tr > td.selected", "> thead > tr > td.selected"}, {vendorGrid, ".jss_worksheet > tbody > tr.selected > td:first-child", "> tbody > tr.selected > td:first-child"}, {vendorGrid, ".jss_worksheet .highlight ", "> tbody > tr > td.highlight "}, {vendorGrid, ".jss_worksheet .highlight-top", "> tbody > tr > td.highlight-top"}, {vendorGrid, ".jss_worksheet .highlight-left", "> tbody > tr > td.highlight-left"}, {vendorGrid, ".jss_worksheet .highlight-right", "> tbody > tr > td.highlight-right"}, {vendorGrid, ".jss_worksheet .highlight-bottom", "> tbody > tr > td.highlight-bottom"}, {vendorGrid, ".jss_worksheet .highlight-selected", "> tbody > tr > td.highlight-selected"}, {vendorGrid, ".jss_worksheet > tbody > tr > td.highlight > a", "> tbody > tr > td.highlight > a"}, {vendorGrid, ".jss_worksheet .selection\n", "> tbody > tr > td.selection "}, {vendorGrid, ".jss_worksheet .selection-top", "> tbody > tr > td.selection-top"}, {vendorGrid, ".jss_worksheet .selection-bottom", "> tbody > tr > td.selection-bottom"}, {vendorGrid, ".jss_worksheet .selection-left", "> tbody > tr > td.selection-left"}, {vendorGrid, ".jss_worksheet .selection-right", "> tbody > tr > td.selection-right"}, {vendorGrid, ".jss_worksheet .copying-top", "> tbody > tr > td.copying-top"}, {vendorGrid, ".jss_worksheet .copying-bottom", "> tbody > tr > td.copying-bottom"}, {vendorGrid, ".jss_worksheet .copying-left", "> tbody > tr > td.copying-left"}, {vendorGrid, ".jss_worksheet .copying-right", "> tbody > tr > td.copying-right"}, {vendorGrid, ".jss_corner", "[data-sheet-grid] .jss_corner"}, {vendorMenu, ".jcontextmenu {", "[data-sheet-grid] .jcontextmenu {"}, {vendorMenu, ".jcontextmenu > div a", "[data-sheet-grid] .jcontextmenu > div a"}, {vendorMenu, ".jcontextmenu > div:hover", "[data-sheet-grid] .jcontextmenu > div:hover"}, {vendorMenu, ".jcontextmenu .jcontextmenu-disabled a", ".jcontextmenu .jcontextmenu-disabled a"}, {vendorMenu, ".jcontextmenu hr", "[data-sheet-grid] .jcontextmenu hr"}, } // comments strips /* … */ so a selector NAMED in an explanatory comment // cannot stand in for the rule itself. // // This is not hypothetical tidying. The first draft of these tests // matched bare `.highlight-right` against the whole file, and the // paragraph above the rules contains the line // `.highlight-right/-bottom border: 1px solid #000` — so deleting the // actual rule left every guard green and the reported bug back. The // prose describing a bug matched the test looking for its fix. var comments = regexp.MustCompile(`(?s)/\*.*?\*/`) func rulesOnly(t *testing.T, rel string) string { t.Helper() body, err := os.ReadFile(filepath.Join(repoRoot(t), rel)) if err != nil { t.Fatalf("guards: read %s: %v", rel, err) } // The vendored files are CRLF and ours is LF; normalise so an entry // above can anchor on a line break without caring which. return comments.ReplaceAllString(strings.ReplaceAll(string(body), "\r\n", "\n"), "") } func TestVendorPaintIsRestated(t *testing.T) { ours := rulesOnly(t, ourSheet) vendors := map[string]string{ vendorGrid: rulesOnly(t, vendorGrid), vendorMenu: rulesOnly(t, vendorMenu), } for _, c := range vendorPaint { if !strings.Contains(vendors[c.file], c.vendor) { t.Errorf("%s: no rule for %q — the vendored library renamed or dropped it "+ "(v5 renamed every .jexcel to .jss_*). Re-derive this list from the "+ "vendored stylesheet; deleting the entry hides the rule, it does not fix it", c.file, c.vendor) continue } if !strings.Contains(ours, c.restated) { t.Errorf("%s: %s is painted in a hard-coded colour by %s and no RULE here restates it — "+ "a literal does not follow the person's light/dark choice, so this surface "+ "is wrong in one of the two schemes. (Comments are stripped before this "+ "check: naming the selector in prose does not count.)", ourSheet, c.restated, c.file) } } } // The vendored stylesheets are linked from sheet.html's head block, // which layout.html renders AFTER sheets.css — so the library is later // in source order and an EQUAL-specificity restatement loses. Ours has // to be strictly higher, which for the grid means carrying the same // `> tbody > tr > td` the grid-line rule carries. That is why every // `restated` string above is the full scoped selector rather than the // bare class: written short, the rule parses, uses a token, and paints // nothing. // // The limit of this check, stated so nobody trusts it further than it // goes: it compares strings, not specificity. If the grid-line rule at // the top of sheets.css ever gains a step of its own — an added class, // a `:not()`, a misused `:where()` — these selectors stay green and stop // winning. Recount by hand when you touch that rule. func TestVendorLinksLoadAfterOurs(t *testing.T) { root := repoRoot(t) layout, err := os.ReadFile(filepath.Join(root, "internal/sheets/templates/layout.html")) if err != nil { t.Skipf("guards: read layout.html: %v", err) } sheet, err := os.ReadFile(filepath.Join(root, "internal/sheets/templates/sheet.html")) if err != nil { t.Skipf("guards: read sheet.html: %v", err) } ours := strings.Index(string(layout), "sheets.css") head := strings.Index(string(layout), `"head"`) if ours < 0 || head < 0 { t.Skip("guards: layout.html no longer links sheets.css before a head block") } if !strings.Contains(string(sheet), "jspreadsheet.css") { t.Skip("guards: sheet.html no longer links the vendored stylesheet") } if ours > head { t.Errorf("layout.html: sheets.css is linked after the head block that carries the "+ "vendored stylesheets (%d > %d). That reverses the load order these overrides "+ "were counted against — recheck every restatement in %s, since ties now go "+ "to us and some may be over-specified", ours, head, ourSheet) } } // TestOurStylesheetHasNoColourLiterals is the other half of the token // rule, and the one the token-name check cannot make. // // csstokens_test.go proves every `var(--rst-…)` we use is defined. It // says nothing about a declaration that skips var() altogether: // `border-right-color: #7ba7ff` passes that check, passes the // restatement check above, and is wrong in light mode forever. func TestOurStylesheetHasNoColourLiterals(t *testing.T) { css := rulesOnly(t, ourSheet) // oklch() and color() are here because the app now paints cell fills // with them. A LITERAL oklch — `oklch(0.92 0.08 145)` — is exactly // the defect this test exists for and the original pattern could not // see it. The legitimate uses all resolve a custom property, so the // check is for a bare number where the lightness goes. literal := regexp.MustCompile(`(?i)(#[0-9a-f]{3,8}\b|\brgba?\(|\bhsla?\(|` + `\b(?:oklch|oklab|lch|lab|color)\(\s*[0-9.]|` + `:\s*(red|blue|green|black|white|gray|grey|orange|yellow|purple)\b)`) for i, line := range strings.Split(css, "\n") { if m := literal.FindString(line); m != "" { t.Errorf("%s:%d: colour literal %q — every colour here is a token, never a "+ "literal, because a literal cannot follow the person's light/dark choice. "+ "Line: %s", ourSheet, i+1, m, strings.TrimSpace(line)) } } }