oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1package docs
2
3import (
4 "net/http/httptest"
5 "os"
6 "strings"
7 "testing"
8)
9
10// The menu bar is a second surface onto the toolbar's command table,
11// and these tests exist because "second surface" is exactly the shape
12// that drifts: a command renamed in one place and not the other fails
13// silently in front of a person, as a menu item that does nothing when
14// clicked.
15
16func newMenuViewForTest() menuView {
17 return newMenuViewFor(true)
18}
19
20// newMenuViewFor builds either variant. The guest's File menu is one item
21// shorter, and the structure rules below (no leading divider, every form
22// carries an action) have to hold for it too — a shorter list is exactly
23// where a leading divider would appear.
24func newMenuViewFor(canCreate bool) menuView {
25 return newMenuView(httptest.NewRequest("GET", "/d/abc123", nil), "abc123", canCreate)
26}
27
28// Every command the menu names is implemented by one of the two modules
29// that implement commands — unless it is flagged as not built yet.
30//
31// This is the Go-model half of the same property
32// TestToolbarOptionValuesMatchTheBundle checks against the template.
33// Both are worth having: that one catches a command in the markup with
34// no implementation, this one catches a command in menu.go that never
35// reaches the markup at all, which is the case a template-only test
36// cannot see.
37func TestMenuCommandsExistInTheBundle(t *testing.T) {
38 toolbar, err := os.ReadFile("../../fixtures/toolbar.mjs")
39 if err != nil {
40 t.Fatal(err)
41 }
42 menu, err := os.ReadFile("../../fixtures/menu.mjs")
43 if err != nil {
44 t.Fatal(err)
45 }
46 // A third place a menu command can be implemented, and the reason
47 // is worth stating rather than looking like a widened net.
48 //
49 // toolbar.mjs holds commands that act on the editor's state; menu.mjs
50 // holds the ones that do not need it at all (Print). The View menu's
51 // paged/infinite pair is neither: it changes how the document is
52 // DRAWN, which is page-level state that editor-entry.js owns — it
53 // holds the mode, writes the cookie and configures the pagination
54 // plugin. Putting the pair in toolbar.mjs would mean the formatting
55 // bar's command table reaching up into the page's own view state.
56 entry, err := os.ReadFile("../../fixtures/editor-entry.js")
57 if err != nil {
58 t.Fatal(err)
59 }
60
61 v := newMenuViewForTest()
62 seen := 0
63 for _, name := range menuCommandNames(v) {
64 seen++
65 if strings.Contains(string(toolbar), name) ||
66 strings.Contains(string(menu), name) ||
67 strings.Contains(string(entry), name) {
68 continue
69 }
70 t.Errorf("menu.go offers %q, which none of fixtures/toolbar.mjs, fixtures/menu.mjs or fixtures/editor-entry.js implements", name)
71 }
72
73 // A guard on the guard. If the menu offered no commands at all the
74 // loop above would check nothing and report success. This repo has found
75 // several assertions unable to fail against the thing they name;
76 // the shape is always a check that cannot distinguish "no" from "I
77 // looked at nothing".
78 if seen == 0 {
79 t.Fatal("no menu commands to check, so this test verified nothing")
80 }
81}
82
83// The bar's shape. Each rule here is one way a menu item can be
84// malformed in a manner the Go compiler is happy with and a person is
85// not.
86func TestMenuStructureIsWellFormed(t *testing.T) {
87 // Both variants. The guest's File menu is one item shorter, and a
88 // shorter list is exactly where a leading divider appears — so the
89 // rules below have to hold for it, not only for a member's.
90 //
91 // The previous commit added newMenuViewFor and its comment saying so,
92 // and then never called it with false. The claim sat in a commit
93 // message for one round. (Re-review 4, finding 3.)
94 for _, canCreate := range []bool{true, false} {
95 t.Run(map[bool]string{true: "member", false: "guest"}[canCreate], func(t *testing.T) {
96 assertMenuWellFormed(t, newMenuViewFor(canCreate))
97 })
98 }
99}
100
101func assertMenuWellFormed(t *testing.T, v menuView) {
102 t.Helper()
103
104 if len(v.Menus) == 0 {
105 t.Fatal("the menu bar has no menus")
106 }
107 if v.Label == "" {
108 t.Error("the menu bar has no accessible name")
109 }
110
111 for _, m := range v.Menus {
112 if m.ID == "" || m.Title == "" {
113 t.Errorf("menu %q has no id or no title", m.ID)
114 }
115 if len(m.Items) == 0 {
116 t.Errorf("menu %q is empty — an empty menu is a title that opens onto nothing", m.ID)
117 }
118 // A menu that opens on a divider, or ends on one, has a rule
119 // against the panel's own edge where it separates nothing.
120 if m.Items[0].Kind == menuDivider {
121 t.Errorf("menu %q opens with a divider", m.ID)
122 }
123 if m.Items[len(m.Items)-1].Kind == menuDivider {
124 t.Errorf("menu %q ends with a divider", m.ID)
125 }
126
127 prevDivider := false
128 for _, it := range m.Items {
129 if it.Kind == menuDivider {
130 if prevDivider {
131 t.Errorf("menu %q has two dividers in a row", m.ID)
132 }
133 prevDivider = true
134 continue
135 }
136 prevDivider = false
137
138 if it.Label == "" {
139 t.Errorf("menu %q has an item with no label", m.ID)
140 }
141 switch it.Kind {
142 case menuCommand:
143 if it.Command == "" {
144 t.Errorf("menu %q: item %q is a command with no command name", m.ID, it.Label)
145 }
146 case menuLink:
147 if it.Href == "" {
148 t.Errorf("menu %q: item %q is a link with no href", m.ID, it.Label)
149 }
150 case menuForm:
151 if it.Action == "" {
152 t.Errorf("menu %q: item %q is a form with no action", m.ID, it.Label)
153 }
154 default:
155 t.Errorf("menu %q: item %q has unknown kind %q", m.ID, it.Label, it.Kind)
156 }
157 }
158 }
159}
160
161// The exports moved from two buttons pinned to the formatting bar into
162// the File menu, and the whole argument for <details> rather than a
163// scripted menubar was that they keep working with no JavaScript. This
164// checks the half of that claim the server owns: the links are still
165// rendered, still point at the same routes, and are still plain links
166// with a download attribute rather than anything a script has to
167// activate.
168func TestExportsAreScriptlessLinksInTheFileMenu(t *testing.T) {
169 v := newMenuViewForTest()
170
171 var file *menu
172 for i := range v.Menus {
173 if v.Menus[i].ID == "file" {
174 file = &v.Menus[i]
175 }
176 }
177 if file == nil {
178 t.Fatal("there is no File menu")
179 }
180
181 want := map[string]bool{
182 "/d/abc123/export.pdf": false,
183 "/d/abc123/export.md": false,
184 }
185 for _, it := range file.Items {
186 if _, ok := want[it.Href]; !ok {
187 continue
188 }
189 want[it.Href] = true
190 if it.Kind != menuLink {
191 t.Errorf("%s is a %q item; it must be a plain link or it stops working without JavaScript", it.Href, it.Kind)
192 }
193 if !it.Download {
194 t.Errorf("%s is missing the download attribute", it.Href)
195 }
196 }
197 for href, found := range want {
198 if !found {
199 t.Errorf("the File menu does not offer %s", href)
200 }
201 }
202}
203
204// Every label the bar renders came out of the catalog, not out of
205// menu.go as a literal. AGENTS.md: twelve base locales, and a
206// hardcoded English string is a bug.
207//
208// It works by rendering the view through a request with no locale
209// middleware, where rastrillo.T returns its key verbatim (documented
210// behaviour, and what a direct App.Handler() call in a test already
211// sees). So every label should come back looking like a key. A literal
212// would come back looking like English.
213func TestEveryMenuLabelIsACatalogKey(t *testing.T) {
214 v := newMenuViewForTest()
215
216 check := func(where, s string) {
217 if s == "" {
218 return
219 }
220 if !strings.HasPrefix(s, "docs.") {
221 t.Errorf("%s is the literal %q rather than a catalog key", where, s)
222 }
223 }
224
225 check("the menu bar's label", v.Label)
226 for _, m := range v.Menus {
227 check("menu "+m.ID+"'s title", m.Title)
228 for _, it := range m.Items {
229 if it.Kind == menuDivider {
230 continue
231 }
232 check("menu "+m.ID+"'s item", it.Label)
233 }
234 }
235}
236
237// Every catalog key the menu resolves is actually in the catalog.
238//
239// The test above proves the labels are keys; it cannot prove they are
240// keys that resolve, because an unresolved key returns itself and looks
241// exactly like a resolved one in that setting. A typo in a key is
242// therefore invisible to it — and visible to a person as raw
243// "docs.editor.menu.file.new" in their menu bar.
244func TestEveryMenuKeyIsInTheCatalog(t *testing.T) {
245 v := newMenuViewForTest()
246
247 check := func(key string) {
248 if key == "" {
249 return
250 }
251 if _, ok := Catalog[key]; !ok {
252 t.Errorf("the menu resolves %q, which is not in Catalog", key)
253 }
254 }
255
256 check(v.Label)
257 for _, m := range v.Menus {
258 check(m.Title)
259 for _, it := range m.Items {
260 if it.Kind == menuDivider {
261 continue
262 }
263 check(it.Label)
264 }
265 }
266}
267