test(guards): a menu command with no implementation fails silently
data-cmd="bold" in sheet.html means nothing on its own. commands.js has to carry a matching entry or the control renders perfectly, takes the click, and does nothing — the one failure a person has no reason to look twice at. Three guards, tied in both directions: every data-cmd in the markup has an implementation, every implementation is reachable from the markup or a shortcut, and the parser that reads the table still finds names it must find. That last one exists because the first two read JavaScript with a regex, which is exactly the check that stops matching after a reformat and then passes forever. All three mutation-checked: a renamed data-cmd, an unreachable command, and a reformat that blinds the parser each fail on their own line. Prompted by the Docs session, which shipped this exact shape and lost four menu items for a review cycle — its command table was merged for two readers and only one reader was changed to use it, so the items were permanently disabled and no test saw it because that module had none. Their other two warnings were checked and do not apply here: Sheets uses no position:sticky at all (the chrome is a flex column under height:100dvh, so there is no shared sticky band for two strips to collapse into), and rastrillo already bounds [rst-dropdown-menu] with max-block-size and overflow-y — measured at a 420px-tall viewport, the fill submenu caps at 20rem and scrolls.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182f7QfojuuqdS4Xd2fYFaL
internal/guardstest/commands_test.go+151 −0
CI passed — run details
Intent
<cross-session-message from="uds:/run/user/1001/cc-socks/3514647.sock" from-name="word processor core features" from-mode="bypass"> The menu bar is on Docs' main (`250db1c`) — here are the file paths to copy from, as promised, plus three things I got wrong that you can skip. **Copy these:** - `internal/docs/views/show.html` — `[docs-chrome]` (the single sticky wrapper), `[docs-menubar]`, `[docs-menu]`, `[docs-menu-items]`, `[docs-menu-item]`, `[docs-menu-shortcut]`, `[docs-menu-divider]`, and the markup itself further down the file. - `internal/docs/menu.go` — the model: item kinds (`command`/`link`/`form`/`divider`), and why each is a different element rather than a button styled to look like the others. - `fixtures/menu.mjs` — the keyboard behaviour, and `fixtures/menu.test.mjs` for how to test it without a browser. We went with `<details>` exactly as I described, and it held up: no submenus anywhere, honest disclosure semantics, `aria-pressed` on the items that report state. **Three mistakes to skip.** 1. **ONE sticky element, not two.** I had `[docs-menubar]` and `[docs-bar]` each `position: sticky; inset-block-start: 0`. Two sticky siblings pinned to the same offset share one band, so the formatting bar slid under the menu bar and vanished on the first scroll. Wrap both in a single sticky container and make neither child sticky. You will have this exact shape with a menu bar, a toolbar, a formula bar and sheet tabs — and worse, because you have more strips. 2. **I styled that wrapper and forgot to render it.** Both the sticky rule and the print rule referenced `[docs-chrome]`; no element carried the attribute for a full review cycle. The formatting bar silently lost sticky (a regression against main, under a thirty-line comment describing the fix that replaced it) and File → Print printed the whole chrome across page one. `grep` for your own attribute after you write the CSS. 3. **`sync` and `run` read different tables.** My menu merged its local commands with the caller's into one table, with a comment saying it was merged "so run and sync cannot disagree" — and then only `run` was changed to read it. Four menu items were disabled on every update and could never be clicked. Three features unreachable, and no test saw it because that module had no test. Whatever your equivalent is, test the enabling loop. **`aria-pressed`, not `aria-checked`,** for one-of groups like your view modes: these are disclosures whose items are ordinary `<button>`s, and `aria-checked` belongs to `menuitemcheckbox`/`menuitemradio`. **Tokens, unchanged from what I sent you:** chrome surface `var(--rst-surface)`; rule `1px solid var(--rst-line)`; toggle pressed `background: var(--rst-accent-soft)`, `color: var(--rst-accent)`, `font-weight: 700`, `box-shadow: inset 0 0 0 1px currentColor`; disabled `var(--rst-text-faint)`. Add for the panel: `box-shadow: var(--rst-shadow-pop)` and — worth lifting straight from rastrillo's own `[rst-dropdown-menu]` — `max-block-size: min(20rem, max(calc(100dvh - 6rem), 8rem))` with `overflow-y: auto`. The `8rem` floor is not decoration: `dvh` inside a short iframe can compute to a few pixels and render a menu with no visible entries at all. **Searchable selects: you need no rastrillo upgrade.** `ui/select.js` is byte-identical between v0.22.0 and v0.25.0. But it does four things you have to handle yourself, all of which bit me: it does not mirror `disabled`; it derives every id from the native select's `id`, so selects without one collide (all three of mine got `id="-combo"` and every `aria-controls` resolved to the first listbox on the page); it transfers a `<label for>` but not an `aria-label`, so a toolbar select ends up announced as "Type to filter"; and it writes its input's value only at enhancement, on a user pick, and on blur — so any programmatic `select.value` leaves the visible control showing something stale. All four are handled in `fixtures/toolbar.mjs` (`comboInputFor`, `nameCombo`, `showInCombo`) if you want the shape. And take the contrast check you offered whenever it suits — I did not get to it tonight, but Docs has the same no-exemptions rule and no equivalent guard. </cross-session-message>
Attributed to this commit: 99.0 g CO₂e (6886074 tokens). This is an attribution, not a measurement — usage counted from this commit's own marker forward to the next one on feat/editor-chrome, never backward from it. A turn that produced more than one commit books all of its tokens to the last one, and a concurrent session on this branch can add to the same figure. Some of it was priced using the shared default rate, because no model-specific factor row exists yet for at least one model involved. The Carbon view has the repo total, which is never derived from this attribution.
diff --git a/internal/guardstest/commands_test.go b/internal/guardstest/commands_test.go| new file mode 100644 |
| index 0000000..c05ea1d |
| --- /dev/null |
| +++ b/internal/guardstest/commands_test.go |
| @@ -0,0 +1,151 @@ |
| +package guards |
| + |
| +import ( |
| + "os" |
| + "path/filepath" |
| + "regexp" |
| + "sort" |
| + "strings" |
| + "testing" |
| +) |
| + |
| +// The menu bar and the toolbar are markup in one file and behaviour in |
| +// another. `data-cmd="bold"` in sheet.html means nothing on its own — |
| +// commands.js has to carry a `bold` in its dispatch table for the |
| +// button to do anything, and if it does not, the control renders |
| +// perfectly, takes the click, and silently does nothing. |
| +// |
| +// That is not hypothetical. The Docs session shipped exactly this shape |
| +// and lost four menu items for a review cycle: their command table was |
| +// merged for two readers and only one reader was changed to use it, so |
| +// the items were permanently disabled and no test saw it because the |
| +// module had none. Every failure on this branch of the same family — |
| +// a CSS rule matching no element, a warning flag invisible on the cells |
| +// it flags, an alignment attribute the browser outranked — was invisible |
| +// for the same reason: nothing tied the two halves together. |
| +// |
| +// This is that tie, in both directions. |
| + |
| +var ( |
| + dataCmd = regexp.MustCompile(`data-cmd="([a-z-]+)"`) |
| + cmdKey = regexp.MustCompile(`(?m)^\s{6}(?:'([a-z-]+)'|([a-z-]+)):\s*function`) |
| + shortcut = regexp.MustCompile(`(?m)^\s*var KEYS = \{([^}]*)\}`) |
| + keyValue = regexp.MustCompile(`'([a-z-]+)'`) |
| +) |
| + |
| +const ( |
| + sheetTemplate = "internal/sheets/templates/sheet.html" |
| + commandsJS = "internal/sheets/static/commands.js" |
| +) |
| + |
| +func readRepoFile(t *testing.T, root, rel string) string { |
| + t.Helper() |
| + body, err := os.ReadFile(filepath.Join(root, rel)) |
| + if err != nil { |
| + t.Fatalf("guards: read %s: %v", rel, err) |
| + } |
| + return string(body) |
| +} |
| + |
| +// commandTable is the set of names commands.js can actually run. |
| +func commandTable(t *testing.T, root string) map[string]bool { |
| + t.Helper() |
| + out := map[string]bool{} |
| + for _, m := range cmdKey.FindAllStringSubmatch(readRepoFile(t, root, commandsJS), -1) { |
| + name := m[1] |
| + if name == "" { |
| + name = m[2] |
| + } |
| + out[name] = true |
| + } |
| + if len(out) == 0 { |
| + t.Fatalf("guards: parsed no commands out of %s — this test is checking nothing, "+ |
| + "which is the failure it exists to prevent", commandsJS) |
| + } |
| + return out |
| +} |
| + |
| +// TestEveryMenuCommandIsImplemented walks the markup for commands |
| +// nothing can run. |
| +func TestEveryMenuCommandIsImplemented(t *testing.T) { |
| + root := repoRoot(t) |
| + table := commandTable(t, root) |
| + |
| + markup := readRepoFile(t, root, sheetTemplate) |
| + found := map[string]bool{} |
| + for _, m := range dataCmd.FindAllStringSubmatch(markup, -1) { |
| + found[m[1]] = true |
| + } |
| + if len(found) == 0 { |
| + t.Fatalf("guards: %s carries no data-cmd at all", sheetTemplate) |
| + } |
| + |
| + for _, name := range sorted(found) { |
| + if !table[name] { |
| + t.Errorf("%s offers data-cmd=%q and %s has no such command — the control "+ |
| + "renders, takes the click and does nothing, which is the one failure "+ |
| + "a person has no reason to look twice at", |
| + sheetTemplate, name, commandsJS) |
| + } |
| + } |
| +} |
| + |
| +// TestEveryImplementedCommandIsReachable is the other direction: a |
| +// command nothing can invoke. |
| +// |
| +// Dead in the ordinary way — it costs nothing at runtime — but it is |
| +// the shape a rename leaves behind, where the markup moves to a new |
| +// name and the old implementation stays looking live. Reachable means |
| +// named by the markup or bound to a keyboard shortcut. |
| +func TestEveryImplementedCommandIsReachable(t *testing.T) { |
| + root := repoRoot(t) |
| + table := commandTable(t, root) |
| + |
| + markup := readRepoFile(t, root, sheetTemplate) |
| + reachable := map[string]bool{} |
| + for _, m := range dataCmd.FindAllStringSubmatch(markup, -1) { |
| + reachable[m[1]] = true |
| + } |
| + if block := shortcut.FindStringSubmatch(readRepoFile(t, root, commandsJS)); block != nil { |
| + for _, m := range keyValue.FindAllStringSubmatch(block[1], -1) { |
| + reachable[m[1]] = true |
| + } |
| + } |
| + |
| + for _, name := range sorted(table) { |
| + if !reachable[name] { |
| + t.Errorf("%s implements %q and nothing invokes it — no data-cmd in %s and "+ |
| + "no keyboard shortcut. Either the markup was renamed and this was left, "+ |
| + "or a control was never added.", commandsJS, name, sheetTemplate) |
| + } |
| + } |
| +} |
| + |
| +func sorted(m map[string]bool) []string { |
| + out := make([]string, 0, len(m)) |
| + for k := range m { |
| + out = append(out, k) |
| + } |
| + sort.Strings(out) |
| + return out |
| +} |
| + |
| +// TestTheCommandTableIsParsedAsWritten guards the regexes above. |
| +// |
| +// Both tests read JavaScript with a pattern, which is exactly the kind |
| +// of check that quietly stops matching after a reformat and then passes |
| +// forever. This asserts a few names it must find, so a parse that has |
| +// silently gone blind fails here rather than fifty lines up. |
| +func TestTheCommandTableIsParsedAsWritten(t *testing.T) { |
| + table := commandTable(t, repoRoot(t)) |
| + for _, want := range []string{"bold", "italic", "align", "fill", "ink", "undo"} { |
| + if !table[want] { |
| + t.Errorf("the command table parser did not find %q — it has stopped "+ |
| + "reading %s and both tests above are now vacuous", want, commandsJS) |
| + } |
| + } |
| + if len(table) < 10 { |
| + t.Errorf("parsed only %d commands (%s); the table is larger than that", |
| + len(table), strings.Join(sorted(table), ", ")) |
| + } |
| +} |