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 "regexp"
5 "sort"
6 "strings"
7 "testing"
8)
9
10// This is the guard for a bug that made every menu in the app open
11// against the right-hand edge of the viewport while both stylesheets
12// read as correct.
13//
14// rastrillo v0.23.0 places a dropdown panel with CSS anchor
15// positioning: `position: fixed`, `position-anchor: --rst-menu`, a
16// `position-area`, and `inset: auto` so that the area does the placing.
17// Its pre-anchor fallback — `position: absolute; top: 100%;
18// inset-inline-end: 0` — is still in the file for engines without it.
19//
20// Sheets overrides that fallback, because rastrillo's panel opens
21// leftward (right for the account menu it was written for) and a menu
22// bar opens rightward. The override was written as `inset-inline-start:
23// 0; inset-inline-end: auto` at `[data-menubar] [rst-dropdown-menu]`,
24// which is (0,2,0) — and rastrillo's `inset: auto` is (0,1,0). So ours
25// won inside the @supports branch too, where it means something else
26// entirely: an anchored, fixed panel with a resolved inline-start inset
27// takes the VIEWPORT as its containing block. Measured on 2026-09-03,
28// all six menus opened at x=1015 on a 1280px viewport, whichever word
29// was clicked.
30//
31// Nothing could see it. The Go suite passes, the CSS parses, every
32// token is real, and each of the two declarations is individually the
33// right thing to write. Only a browser measuring a rectangle finds it,
34// and CI has no browser.
35//
36// So the invariant is structural, and it is the one thing that was
37// missing: an override of rastrillo's PRE-ANCHOR insets must be paired
38// with an anchor-branch rule for the same selector that hands the
39// placing back. Adding a third menu-ish panel and styling it the
40// obvious way now fails here rather than in a screenshot nobody takes.
41const sheetsCSS = "internal/sheets/static/sheets.css"
42
43var (
44 // A rule head plus its body, non-greedy. sheets.css nests only
45 // inside @supports, and stripBlocks below removes those first, so a
46 // body never contains a brace.
47 cssRule = regexp.MustCompile(`(?s)([^{}@]+)\{([^{}]*)\}`)
48 // The @supports blocks, with their condition and their contents.
49 cssSupports = regexp.MustCompile(`(?s)@supports([^{]*)\{(.*?\n\})\s*\n`)
50 cssComment = regexp.MustCompile(`(?s)/\*.*?\*/`)
51 insetLogical = regexp.MustCompile(`(?m)^\s*inset-inline-(start|end)\s*:`)
52 insetAuto = regexp.MustCompile(`(?m)^\s*inset\s*:\s*auto\s*;`)
53 // The declaration that actually does the placing. Requiring
54 // `inset: auto` alone was the guard's own blind spot: a rule that
55 // hands placement back and then says nothing about WHERE leaves
56 // rastrillo's own area in force, which opens a menu bar's menus
57 // leftward off the left edge of the window.
58 positionArea = regexp.MustCompile(`(?m)^\s*position-area\s*:\s*\S`)
59)
60
61// selectorsOverridingPanelInsets returns every selector in the given
62// CSS that targets a rastrillo dropdown panel and sets one of the
63// logical insets rastrillo's pre-anchor fallback uses.
64func selectorsOverridingPanelInsets(css string) []string {
65 var out []string
66 for _, m := range cssRule.FindAllStringSubmatch(css, -1) {
67 sel := strings.Join(strings.Fields(m[1]), " ")
68 if !strings.Contains(sel, "[rst-dropdown-menu]") {
69 continue
70 }
71 if insetLogical.MatchString(m[2]) {
72 out = append(out, sel)
73 }
74 }
75 sort.Strings(out)
76 return out
77}
78
79// anchorBranchSelectors returns every selector that appears inside an
80// @supports block testing position-area, hands placement back with
81// `inset: auto`, AND says where the panel goes.
82//
83// Both declarations are required. The first version checked only
84// `inset: auto`, so deleting `position-area: block-end span-inline-end`
85// — the one line that makes a menu bar open rightward — left this
86// green while removing exactly what the failure message tells the next
87// person to add.
88func anchorBranchSelectors(css string) map[string]bool {
89 out := map[string]bool{}
90 for _, blk := range cssSupports.FindAllStringSubmatch(css, -1) {
91 if !strings.Contains(blk[1], "position-area") {
92 continue
93 }
94 for _, m := range cssRule.FindAllStringSubmatch(blk[2], -1) {
95 if !insetAuto.MatchString(m[2]) || !positionArea.MatchString(m[2]) {
96 continue
97 }
98 out[strings.Join(strings.Fields(m[1]), " ")] = true
99 }
100 }
101 return out
102}
103
104func TestEveryDropdownInsetOverrideHasAnAnchorBranch(t *testing.T) {
105 css := cssComment.ReplaceAllString(readRepoFile(t, repoRoot(t), sheetsCSS), "")
106
107 overrides := selectorsOverridingPanelInsets(css)
108 if len(overrides) == 0 {
109 t.Fatalf("guards: found no [rst-dropdown-menu] inset override in %s. "+
110 "Either the app stopped overriding rastrillo's placement — in which "+
111 "case delete this test and say why — or the parser above has gone "+
112 "blind and is now proving nothing.", sheetsCSS)
113 }
114
115 anchored := anchorBranchSelectors(css)
116 if len(anchored) == 0 {
117 t.Fatalf("guards: %s overrides %d dropdown panel inset(s) and declares no "+
118 "@supports (position-area …) branch at all. In Chromium those "+
119 "overrides do not adjust rastrillo's placement, they replace it with "+
120 "the viewport: every menu opens flush against one edge of the window "+
121 "whichever word was clicked.", sheetsCSS, len(overrides))
122 }
123
124 for _, sel := range overrides {
125 if !anchored[sel] {
126 t.Errorf("%s sets inset-inline-* on %q but no @supports "+
127 "(position-area …) block resets `inset: auto` for that same "+
128 "selector.\n"+
129 "That override is written against rastrillo's PRE-ANCHOR "+
130 "fallback. Where anchor positioning exists it outranks "+
131 "rastrillo's own `inset: auto` on specificity — (0,2,0) beats "+
132 "(0,1,0) — and an anchored fixed panel with a resolved inset "+
133 "is placed against the viewport, not against the summary it "+
134 "hangs off.\n"+
135 "Pair it: keep this rule for the fallback, and add\n"+
136 " @supports (position-area: block-end) and "+
137 "(position-try-fallbacks: flip-block) {\n"+
138 " %s { inset: auto; position-area: block-end span-inline-end; }\n"+
139 " }",
140 sheetsCSS, sel, sel)
141 }
142 }
143}
144
145// TestTheAnchorGuardReadsTheStylesheetItThinksItDoes is the mutation
146// check for the two parsers above.
147//
148// Both read CSS with a regex, which is the kind of check that silently
149// stops matching after a reformat and then passes forever. This feeds
150// them CSS with the bug present and requires them to see it, so a
151// parser that has gone blind fails here and names itself rather than
152// turning the test above into a no-op that nobody notices.
153func TestTheAnchorGuardReadsTheStylesheetItThinksItDoes(t *testing.T) {
154 const broken = `
155[data-menubar] [rst-dropdown-menu] {
156 inset-inline-start: 0;
157 inset-inline-end: auto;
158}
159`
160 if got := selectorsOverridingPanelInsets(broken); len(got) != 1 {
161 t.Fatalf("the override parser did not see the exact shape of the shipped "+
162 "bug: got %v, want one selector", got)
163 }
164 if got := anchorBranchSelectors(broken); len(got) != 0 {
165 t.Fatalf("the anchor-branch parser invented a branch that is not there: %v", got)
166 }
167
168 const fixed = broken + `
169@supports (position-area: block-end) and (position-try-fallbacks: flip-block) {
170 [data-menubar] [rst-dropdown-menu] {
171 inset: auto;
172 position-area: block-end span-inline-end;
173 }
174}
175`
176 if got := anchorBranchSelectors(fixed); !got["[data-menubar] [rst-dropdown-menu]"] {
177 t.Fatalf("the anchor-branch parser did not find the paired branch: %v", got)
178 }
179
180 // A branch that hands placement back and does not say where must
181 // not count. This is the mutation the first version of the guard
182 // could not see.
183 const noArea = broken + `
184@supports (position-area: block-end) and (position-try-fallbacks: flip-block) {
185 [data-menubar] [rst-dropdown-menu] {
186 inset: auto;
187 }
188}
189`
190 if got := anchorBranchSelectors(noArea); len(got) != 0 {
191 t.Fatalf("a branch with no position-area was accepted: %v", got)
192 }
193
194 // A branch guarded by some OTHER @supports condition must not count.
195 const wrongCondition = broken + `
196@supports (display: grid) {
197 [data-menubar] [rst-dropdown-menu] {
198 inset: auto;
199 }
200}
201`
202 if got := anchorBranchSelectors(wrongCondition); len(got) != 0 {
203 t.Fatalf("an @supports block that does not test position-area was accepted "+
204 "as the anchor branch: %v", got)
205 }
206}
207