package guards import ( "regexp" "sort" "strings" "testing" ) // This is the guard for a bug that made every menu in the app open // against the right-hand edge of the viewport while both stylesheets // read as correct. // // rastrillo v0.23.0 places a dropdown panel with CSS anchor // positioning: `position: fixed`, `position-anchor: --rst-menu`, a // `position-area`, and `inset: auto` so that the area does the placing. // Its pre-anchor fallback — `position: absolute; top: 100%; // inset-inline-end: 0` — is still in the file for engines without it. // // Sheets overrides that fallback, because rastrillo's panel opens // leftward (right for the account menu it was written for) and a menu // bar opens rightward. The override was written as `inset-inline-start: // 0; inset-inline-end: auto` at `[data-menubar] [rst-dropdown-menu]`, // which is (0,2,0) — and rastrillo's `inset: auto` is (0,1,0). So ours // won inside the @supports branch too, where it means something else // entirely: an anchored, fixed panel with a resolved inline-start inset // takes the VIEWPORT as its containing block. Measured on 2026-09-03, // all six menus opened at x=1015 on a 1280px viewport, whichever word // was clicked. // // Nothing could see it. The Go suite passes, the CSS parses, every // token is real, and each of the two declarations is individually the // right thing to write. Only a browser measuring a rectangle finds it, // and CI has no browser. // // So the invariant is structural, and it is the one thing that was // missing: an override of rastrillo's PRE-ANCHOR insets must be paired // with an anchor-branch rule for the same selector that hands the // placing back. Adding a third menu-ish panel and styling it the // obvious way now fails here rather than in a screenshot nobody takes. const sheetsCSS = "internal/sheets/static/sheets.css" var ( // A rule head plus its body, non-greedy. sheets.css nests only // inside @supports, and stripBlocks below removes those first, so a // body never contains a brace. cssRule = regexp.MustCompile(`(?s)([^{}@]+)\{([^{}]*)\}`) // The @supports blocks, with their condition and their contents. cssSupports = regexp.MustCompile(`(?s)@supports([^{]*)\{(.*?\n\})\s*\n`) cssComment = regexp.MustCompile(`(?s)/\*.*?\*/`) insetLogical = regexp.MustCompile(`(?m)^\s*inset-inline-(start|end)\s*:`) insetAuto = regexp.MustCompile(`(?m)^\s*inset\s*:\s*auto\s*;`) // The declaration that actually does the placing. Requiring // `inset: auto` alone was the guard's own blind spot: a rule that // hands placement back and then says nothing about WHERE leaves // rastrillo's own area in force, which opens a menu bar's menus // leftward off the left edge of the window. positionArea = regexp.MustCompile(`(?m)^\s*position-area\s*:\s*\S`) ) // selectorsOverridingPanelInsets returns every selector in the given // CSS that targets a rastrillo dropdown panel and sets one of the // logical insets rastrillo's pre-anchor fallback uses. func selectorsOverridingPanelInsets(css string) []string { var out []string for _, m := range cssRule.FindAllStringSubmatch(css, -1) { sel := strings.Join(strings.Fields(m[1]), " ") if !strings.Contains(sel, "[rst-dropdown-menu]") { continue } if insetLogical.MatchString(m[2]) { out = append(out, sel) } } sort.Strings(out) return out } // anchorBranchSelectors returns every selector that appears inside an // @supports block testing position-area, hands placement back with // `inset: auto`, AND says where the panel goes. // // Both declarations are required. The first version checked only // `inset: auto`, so deleting `position-area: block-end span-inline-end` // — the one line that makes a menu bar open rightward — left this // green while removing exactly what the failure message tells the next // person to add. func anchorBranchSelectors(css string) map[string]bool { out := map[string]bool{} for _, blk := range cssSupports.FindAllStringSubmatch(css, -1) { if !strings.Contains(blk[1], "position-area") { continue } for _, m := range cssRule.FindAllStringSubmatch(blk[2], -1) { if !insetAuto.MatchString(m[2]) || !positionArea.MatchString(m[2]) { continue } out[strings.Join(strings.Fields(m[1]), " ")] = true } } return out } func TestEveryDropdownInsetOverrideHasAnAnchorBranch(t *testing.T) { css := cssComment.ReplaceAllString(readRepoFile(t, repoRoot(t), sheetsCSS), "") overrides := selectorsOverridingPanelInsets(css) if len(overrides) == 0 { t.Fatalf("guards: found no [rst-dropdown-menu] inset override in %s. "+ "Either the app stopped overriding rastrillo's placement — in which "+ "case delete this test and say why — or the parser above has gone "+ "blind and is now proving nothing.", sheetsCSS) } anchored := anchorBranchSelectors(css) if len(anchored) == 0 { t.Fatalf("guards: %s overrides %d dropdown panel inset(s) and declares no "+ "@supports (position-area …) branch at all. In Chromium those "+ "overrides do not adjust rastrillo's placement, they replace it with "+ "the viewport: every menu opens flush against one edge of the window "+ "whichever word was clicked.", sheetsCSS, len(overrides)) } for _, sel := range overrides { if !anchored[sel] { t.Errorf("%s sets inset-inline-* on %q but no @supports "+ "(position-area …) block resets `inset: auto` for that same "+ "selector.\n"+ "That override is written against rastrillo's PRE-ANCHOR "+ "fallback. Where anchor positioning exists it outranks "+ "rastrillo's own `inset: auto` on specificity — (0,2,0) beats "+ "(0,1,0) — and an anchored fixed panel with a resolved inset "+ "is placed against the viewport, not against the summary it "+ "hangs off.\n"+ "Pair it: keep this rule for the fallback, and add\n"+ " @supports (position-area: block-end) and "+ "(position-try-fallbacks: flip-block) {\n"+ " %s { inset: auto; position-area: block-end span-inline-end; }\n"+ " }", sheetsCSS, sel, sel) } } } // TestTheAnchorGuardReadsTheStylesheetItThinksItDoes is the mutation // check for the two parsers above. // // Both read CSS with a regex, which is the kind of check that silently // stops matching after a reformat and then passes forever. This feeds // them CSS with the bug present and requires them to see it, so a // parser that has gone blind fails here and names itself rather than // turning the test above into a no-op that nobody notices. func TestTheAnchorGuardReadsTheStylesheetItThinksItDoes(t *testing.T) { const broken = ` [data-menubar] [rst-dropdown-menu] { inset-inline-start: 0; inset-inline-end: auto; } ` if got := selectorsOverridingPanelInsets(broken); len(got) != 1 { t.Fatalf("the override parser did not see the exact shape of the shipped "+ "bug: got %v, want one selector", got) } if got := anchorBranchSelectors(broken); len(got) != 0 { t.Fatalf("the anchor-branch parser invented a branch that is not there: %v", got) } const fixed = broken + ` @supports (position-area: block-end) and (position-try-fallbacks: flip-block) { [data-menubar] [rst-dropdown-menu] { inset: auto; position-area: block-end span-inline-end; } } ` if got := anchorBranchSelectors(fixed); !got["[data-menubar] [rst-dropdown-menu]"] { t.Fatalf("the anchor-branch parser did not find the paired branch: %v", got) } // A branch that hands placement back and does not say where must // not count. This is the mutation the first version of the guard // could not see. const noArea = broken + ` @supports (position-area: block-end) and (position-try-fallbacks: flip-block) { [data-menubar] [rst-dropdown-menu] { inset: auto; } } ` if got := anchorBranchSelectors(noArea); len(got) != 0 { t.Fatalf("a branch with no position-area was accepted: %v", got) } // A branch guarded by some OTHER @supports condition must not count. const wrongCondition = broken + ` @supports (display: grid) { [data-menubar] [rst-dropdown-menu] { inset: auto; } } ` if got := anchorBranchSelectors(wrongCondition); len(got) != 0 { t.Fatalf("an @supports block that does not test position-area was accepted "+ "as the anchor branch: %v", got) } }