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 "os"
5 "path/filepath"
6 "regexp"
7 "sort"
8 "strings"
9 "testing"
10)
11
12// The menu bar and the toolbar are markup in one file and behaviour in
13// another. `data-cmd="bold"` in sheet.html means nothing on its own —
14// commands.js has to carry a `bold` in its dispatch table for the
15// button to do anything, and if it does not, the control renders
16// perfectly, takes the click, and silently does nothing.
17//
18// That is not hypothetical. The Docs session shipped exactly this shape
19// and lost four menu items for a review cycle: their command table was
20// merged for two readers and only one reader was changed to use it, so
21// the items were permanently disabled and no test saw it because the
22// module had none. Every failure on this branch of the same family —
23// a CSS rule matching no element, a warning flag invisible on the cells
24// it flags, an alignment attribute the browser outranked — was invisible
25// for the same reason: nothing tied the two halves together.
26//
27// This is that tie, in both directions.
28
29var (
30 dataCmd = regexp.MustCompile(`data-cmd="([a-z-]+)"`)
31 cmdKey = regexp.MustCompile(`(?m)^\s{6}(?:'([a-z-]+)'|([a-z-]+)):\s*function`)
32 shortcut = regexp.MustCompile(`(?m)^\s*var KEYS = \{([^}]*)\}`)
33 keyValue = regexp.MustCompile(`'([a-z-]+)'`)
34)
35
36const (
37 sheetTemplate = "internal/sheets/templates/sheet.html"
38 commandsJS = "internal/sheets/static/commands.js"
39)
40
41func readRepoFile(t *testing.T, root, rel string) string {
42 t.Helper()
43 body, err := os.ReadFile(filepath.Join(root, rel))
44 if err != nil {
45 t.Fatalf("guards: read %s: %v", rel, err)
46 }
47 return string(body)
48}
49
50// commandTable is the set of names commands.js can actually run.
51func commandTable(t *testing.T, root string) map[string]bool {
52 t.Helper()
53 out := map[string]bool{}
54 for _, m := range cmdKey.FindAllStringSubmatch(readRepoFile(t, root, commandsJS), -1) {
55 name := m[1]
56 if name == "" {
57 name = m[2]
58 }
59 out[name] = true
60 }
61 if len(out) == 0 {
62 t.Fatalf("guards: parsed no commands out of %s — this test is checking nothing, "+
63 "which is the failure it exists to prevent", commandsJS)
64 }
65 return out
66}
67
68// TestEveryMenuCommandIsImplemented walks the markup for commands
69// nothing can run.
70func TestEveryMenuCommandIsImplemented(t *testing.T) {
71 root := repoRoot(t)
72 table := commandTable(t, root)
73
74 markup := readRepoFile(t, root, sheetTemplate)
75 found := map[string]bool{}
76 for _, m := range dataCmd.FindAllStringSubmatch(markup, -1) {
77 found[m[1]] = true
78 }
79 if len(found) == 0 {
80 t.Fatalf("guards: %s carries no data-cmd at all", sheetTemplate)
81 }
82
83 for _, name := range sorted(found) {
84 if !table[name] {
85 t.Errorf("%s offers data-cmd=%q and %s has no such command — the control "+
86 "renders, takes the click and does nothing, which is the one failure "+
87 "a person has no reason to look twice at",
88 sheetTemplate, name, commandsJS)
89 }
90 }
91}
92
93// TestEveryImplementedCommandIsReachable is the other direction: a
94// command nothing can invoke.
95//
96// Dead in the ordinary way — it costs nothing at runtime — but it is
97// the shape a rename leaves behind, where the markup moves to a new
98// name and the old implementation stays looking live. Reachable means
99// named by the markup or bound to a keyboard shortcut.
100func TestEveryImplementedCommandIsReachable(t *testing.T) {
101 root := repoRoot(t)
102 table := commandTable(t, root)
103
104 markup := readRepoFile(t, root, sheetTemplate)
105 reachable := map[string]bool{}
106 for _, m := range dataCmd.FindAllStringSubmatch(markup, -1) {
107 reachable[m[1]] = true
108 }
109 if block := shortcut.FindStringSubmatch(readRepoFile(t, root, commandsJS)); block != nil {
110 for _, m := range keyValue.FindAllStringSubmatch(block[1], -1) {
111 reachable[m[1]] = true
112 }
113 }
114
115 for _, name := range sorted(table) {
116 if !reachable[name] {
117 t.Errorf("%s implements %q and nothing invokes it — no data-cmd in %s and "+
118 "no keyboard shortcut. Either the markup was renamed and this was left, "+
119 "or a control was never added.", commandsJS, name, sheetTemplate)
120 }
121 }
122}
123
124func sorted(m map[string]bool) []string {
125 out := make([]string, 0, len(m))
126 for k := range m {
127 out = append(out, k)
128 }
129 sort.Strings(out)
130 return out
131}
132
133// TestTheCommandTableIsParsedAsWritten guards the regexes above.
134//
135// Both tests read JavaScript with a pattern, which is exactly the kind
136// of check that quietly stops matching after a reformat and then passes
137// forever. This asserts a few names it must find, so a parse that has
138// silently gone blind fails here rather than fifty lines up.
139func TestTheCommandTableIsParsedAsWritten(t *testing.T) {
140 table := commandTable(t, repoRoot(t))
141 for _, want := range []string{"bold", "italic", "align", "fill", "ink", "undo"} {
142 if !table[want] {
143 t.Errorf("the command table parser did not find %q — it has stopped "+
144 "reading %s and both tests above are now vacuous", want, commandsJS)
145 }
146 }
147 if len(table) < 10 {
148 t.Errorf("parsed only %d commands (%s); the table is larger than that",
149 len(table), strings.Join(sorted(table), ", "))
150 }
151}
152