- 地图脚本:localStorage 读写走 lsGet/lsSet 兜底(隐私模式下裸访问会打断整段脚本); preview() 在 /api/map-config 还没回来时不丢点(previewQueue 在 if (live) 之前排队); 异步回包一律发序号作废,取消后回来的包不再画 - photo-exif.js:EXIF 时间全 0 不再变成 1899-11-30 写进到访时间;读 count 前先按版本卡宽度 - 数据编辑页:三个筛选表单都把其余条件用隐藏域原样带上,提交不再把别的列表的筛选清掉 - 补全页:sessionStorage 的读写一律包 try(脏数据/隐私模式都会抛) - 窄屏:删掉被后面同特异性规则盖掉的死内边距;宽表堆叠类名成对、判据扩到所有 tb 表 - 无障碍:图片删除按钮补 aria-label,访客画廊图片补 alt - report.html 补上漏掉的 </tr>,fog-mask 的宽表带 tb-stack 本批不碰数据库与导出路径:往返包 823/323/8/30/791/557/2246 全等、 坐标偏差 0.000000000、时间偏差 0s、迷雾 8701 块一致。
136 lines
4.0 KiB
Go
136 lines
4.0 KiB
Go
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(`<table\b[^>]*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 静态钉住模板里 `<tr>` 与 `</tr>` 数量相等。
|
||
//
|
||
// 浏览器会自己把漏掉的 `</tr>` 补上,页面看着完全正常——但 tb_stack_test 的行列校验
|
||
// 是按 `<tr>…</tr>` 配对的,一个没闭合的 `<tr>` 会让**那一行整行不被检查**
|
||
// (2026-09-21 审计在 report.html 的「最南」行上发现,之前一直静默跳过)。
|
||
//
|
||
// 先剥掉 <script> 块:照片表的行是脚本拼字符串生成的,那边的 `<tr>` 不算数。
|
||
func TestTemplatesHaveBalancedRows(t *testing.T) {
|
||
checked := 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
|
||
}
|
||
body := stripScriptBlocks(string(raw))
|
||
open := len(regexp.MustCompile(`<tr\b`).FindAllString(body, -1))
|
||
close := strings.Count(body, "</tr>")
|
||
if open == 0 {
|
||
return nil
|
||
}
|
||
checked++
|
||
if open != close {
|
||
t.Errorf("%s:<tr> 有 %d 个、</tr> 只有 %d 个 —— 缺口那一行会被 tb_stack_test 静默跳过",
|
||
path, open, close)
|
||
}
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("遍历模板失败: %v", err)
|
||
}
|
||
if checked == 0 {
|
||
t.Fatal("一个含表格行的模板都没扫到:模板目录变了?")
|
||
}
|
||
t.Logf("检查了 %d 个含表格行的模板", checked)
|
||
}
|
||
|
||
// stripScriptBlocks 去掉所有 <script>…</script> 的内容(含标签本身)。
|
||
func stripScriptBlocks(s string) string {
|
||
var b strings.Builder
|
||
for {
|
||
i := strings.Index(s, "<script")
|
||
if i < 0 {
|
||
b.WriteString(s)
|
||
return b.String()
|
||
}
|
||
b.WriteString(s[:i])
|
||
j := strings.Index(s[i:], ">")
|
||
if j < 0 {
|
||
return b.String()
|
||
}
|
||
end := strings.Index(s[i+j:], "</script>")
|
||
if end < 0 {
|
||
return b.String()
|
||
}
|
||
s = s[i+j+end+len("</script>"):]
|
||
}
|
||
}
|
||
|
||
func containsStr(list []string, want string) bool {
|
||
for _, v := range list {
|
||
if v == want {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|