package web import ( "io/fs" "path/filepath" "regexp" "strings" "testing" ) // tbStackExempt 是明确不参与窄屏堆叠的表格类名,必须写清理由。 var tbStackExempt = map[string]string{ "photo-table": "照片表由脚本逐格生成、列数固定,移动端走「隐藏第 3 列」那条路", } // TestAllTBTablesAreStackable 静态扫全部模板:只要用了 `tb` 这个宽表基类,就必须带 `tb-stack`。 // // 这是对 wide_table_layout_test 的补充——那条是**渲染后**按 DOM 栈判的,两种表能躲过去: // ① 运行期的数据为空(比如库里没有遮罩区),那张表压根没渲染; // ② 模板忘了写 .tw 包裹,判据里的「在 .tw 里」就不成立。 // /admin/fog/mask 的遮罩表两条都占了,2026-09-21 审计才发现。 func TestAllTBTablesAreStackable(t *testing.T) { tableRe := regexp.MustCompile(`]*class="([^"]*)"`) scanned, tables := 0, 0 err := fs.WalkDir(tmplFS, "templates", func(path string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() || filepath.Ext(path) != ".html" { return err } raw, err := fs.ReadFile(tmplFS, path) if err != nil { return err } scanned++ for _, m := range tableRe.FindAllStringSubmatch(string(raw), -1) { classes := strings.Fields(m[1]) if !containsStr(classes, "tb") { continue } tables++ if containsStr(classes, "tb-stack") { continue } exempt := false for _, c := range classes { if _, ok := tbStackExempt[c]; ok { exempt = true } } if exempt { continue } t.Errorf("%s:用了 tb 却没带 tb-stack(class=%q)——窄屏没有任何堆叠规则可命中,只能横滑", path, m[1]) } return nil }) if err != nil { t.Fatalf("遍历模板失败: %v", err) } if scanned == 0 || tables == 0 { t.Fatalf("没扫到模板/表格(模板目录或类名写法变了?):模板 %d 个、tb 表 %d 张", scanned, tables) } t.Logf("扫了 %d 个模板里的 %d 张 tb 表", scanned, tables) } // TestTemplatesHaveBalancedRows 静态钉住模板里 `` 与 `` 数量相等。 // // 浏览器会自己把漏掉的 `` 补上,页面看着完全正常——但 tb_stack_test 的行列校验 // 是按 `…` 配对的,一个没闭合的 `` 会让**那一行整行不被检查** // (2026-09-21 审计在 report.html 的「最南」行上发现,之前一直静默跳过)。 // // 先剥掉 的内容(含标签本身)。 func stripScriptBlocks(s string) string { var b strings.Builder for { i := strings.Index(s, "") if j < 0 { return b.String() } end := strings.Index(s[i+j:], "") if end < 0 { return b.String() } s = s[i+j+end+len(""):] } } func containsStr(list []string, want string) bool { for _, v := range list { if v == want { return true } } return false }