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 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
}