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))
}
}
}