package guards import ( "io/fs" "os" "path/filepath" "regexp" "strings" "testing" "amadan.net/oficina/estilo.git/chrome" ) // TestNoNestedExclusiveDetails is the guard on the one trap the menu // bar is built over. // // `name` on a
makes an exclusive accordion: opening one // member closes every other. That is exactly what a row of top-level // menus wants. It is exactly wrong for a submenu, which lives INSIDE a // grouped
— grouped, opening the submenu closes the parent it // is nested in, and the submenu goes down with it. // // The failure is silent and looks like a rendering glitch: the menu // blinks shut when you reach for the thing inside it. layout.html // already carries a long comment about the same trap on the shell menu, // which is how we know it is the kind of mistake that gets made twice. // // The check is structural rather than a name-by-name allowlist: any //
that CONTAINS another
fails, // whatever they are called. func TestNoNestedExclusiveDetails(t *testing.T) { root := repoRoot(t) files := templateFiles(t, root) if len(files) == 0 { t.Fatal("guards: found no templates; this test is not checking anything") } openTag := regexp.MustCompile(`(?i)]*>`) closeTag := regexp.MustCompile(`(?i)`) named := regexp.MustCompile(`(?i)\bname\s*=`) for _, rel := range files { body, err := os.ReadFile(filepath.Join(root, rel)) if err != nil { t.Fatalf("guards: read %s: %v", rel, err) } checkNesting(t, rel, string(body), openTag, closeTag, named) } } // checkNesting walks a template's
tags keeping a stack of // whether each open element carried a name. // // Tag-level and not a real parser, deliberately: these templates are // hand-written HTML with no self-closing
and no
// inside a comment or an attribute value, and a regex that can be read // in one sitting beats a dependency for a rule this narrow. If a // template ever grows a case this cannot see, the fix is to stop // writing that case. func checkNesting(t *testing.T, rel, body string, openTag, closeTag, named *regexp.Regexp) { t.Helper() type tag struct { named bool line int } var stack []tag for i, line := range strings.Split(body, "\n") { // A line may hold both an open and a close; the opens are // processed first, which is the order they appear in practice // (a one-line
…
is not written here). for _, m := range openTag.FindAllString(line, -1) { isNamed := named.MatchString(m) if isNamed { for _, parent := range stack { if parent.named { t.Errorf("%s:%d: a
is nested inside the one opened "+ "at line %d, which also carries name=. Grouped, opening the inner "+ "one CLOSES its parent and takes itself down with it — the menu "+ "blinks shut as you reach into it. A submenu carries no name.", rel, i+1, parent.line) } } } stack = append(stack, tag{named: isNamed, line: i + 1}) } for range closeTag.FindAllString(line, -1) { if len(stack) > 0 { stack = stack[:len(stack)-1] } } } } // TestTheMenusShareOneGroup pins the design TestNoNestedExclusiveDetails // protects the edges of: every top-level menu on the grid — File, Edit, // Insert, Format, Data — and every trigger in the bar above it — Menu, // the switcher, the account — is one exclusive group, so opening File // closes the account menu and the other way round. Sheets chose that on // purpose when both halves were rastrillo dropdowns in "rst-menus"; they // are estilo's now, and the group is estilo's one name, "estilo-menus" // (shared chrome spec §7.1). // // Two halves because either can drift alone: the menubar is this app's // markup, the bar is estilo's. func TestTheMenusShareOneGroup(t *testing.T) { const group = `name="estilo-menus"` sheet := readRepoFile(t, repoRoot(t), "internal/sheets/templates/sheet.html") titles := regexp.MustCompile(`
]*>`).FindAllString(sheet, -1) if len(titles) < 5 { t.Fatalf("found %d menubar menus in sheet.html, want File, Edit, Insert, Format and Data", len(titles)) } for _, d := range titles { if !strings.Contains(d, group) { t.Errorf("sheet.html: %s is not in the one menu group", d) } } for _, other := range regexp.MustCompile(`]*\bname="([^"]+)"`).FindAllStringSubmatch(sheet, -1) { if other[1] != "estilo-menus" && other[1] != "rst-tools" { t.Errorf("sheet.html: a menu joins group %q; the menubar and the bar share estilo-menus, and only the toolbar's pickers have their own", other[1]) } } bar, err := fs.ReadFile(chrome.Templates(), "templates/bar.html") if err != nil { t.Fatal(err) } account, err := fs.ReadFile(chrome.Templates(), "templates/account.html") if err != nil { t.Fatal(err) } switcher, err := fs.ReadFile(chrome.Templates(), "templates/switcher.html") if err != nil { t.Fatal(err) } for name, body := range map[string][]byte{"the Menu sheet": bar, "the account": account, "the switcher": switcher} { d := regexp.MustCompile(`
]*>`).FindString(string(body)) if d == "" || !strings.Contains(d, group) { t.Errorf("estilo's %s trigger is %q, not in the one menu group", name, d) } } }