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