Files
tamakyi d5f6dfa381 审计修复:前端(模板 / CSS / JS 与无障碍)
- 地图脚本: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 块一致。
2026-09-24 20:37:57 +08:00

112 lines
4.1 KiB
Go

package web
import (
"context"
"net/http"
"net/http/httptest"
"regexp"
"strconv"
"strings"
"testing"
)
// TestStackedTablesHaveLabels 守住「窄屏宽表变一屏一行」这条改动的核心不变量:
// 每张打了 tb-stack 的表,窄屏会把表头藏起来、改成「字段名 + 值」竖排,字段名只能取自
// td 的 data-label。少写一个标签,那一格在手机上就是没有名字的裸值——而桌面端一切正常,
// 很难发现(这轮就漏过两次:一张表的中间几行、后台两张表的「状态 / 对访客 / 操作」)。
// 顺带钉住每行格子数 == 表头列数,否则说明模板结构歪了。
//
// ROND_TEST_CONF=conf/app-test.ini ROND_SRC=data/xxx.rondbackup go test ./internal/web -run StackedTables -v
func TestStackedTablesHaveLabels(t *testing.T) {
f := newMovementFixture(t)
var topicID int64
if err := f.pg.QueryRowContext(f.ctx, `SELECT id FROM topics WHERE dataset_id=$1 ORDER BY id LIMIT 1`,
f.dsID).Scan(&topicID); err != nil {
topicID = 0 // 这个数据集没有专题,就少查一页
}
type target struct {
path string
h http.HandlerFunc
admin bool
}
targets := []target{
{"/places", f.srv.placesPage, false},
{"/stats", f.srv.statsPage, false},
{"/", f.srv.home, false},
{"/report", f.srv.reportPage, false},
{"/admin", f.srv.adminHome, true},
{"/admin/topics", f.srv.topicsAdminPage, true},
// 数据编辑页的三张表默认折叠在卡片里(探针量到的是 display:none),最容易漏标
{"/admin/edit", f.srv.editPage, true},
// 遮罩表原来既没 tb-stack 也没 .tw,两条守卫都看不见(数据库里没有遮罩时这页也不渲染表)
{"/admin/fog/mask", f.srv.fogMaskPage, true},
}
if topicID > 0 {
targets = append(targets, target{"/topics/" + strconv.FormatInt(topicID, 10), f.srv.topicPage, false})
}
tableRe := regexp.MustCompile(`(?s)<table[^>]*class="[^"]*tb-stack[^"]*"[^>]*>.*?</table>`)
rowRe := regexp.MustCompile(`(?s)<tr[^>]*>(.*?)</tr>`)
tdRe := regexp.MustCompile(`(?s)<td([^>]*)>`)
thRe := regexp.MustCompile(`<th[\s>]`)
thTextRe := regexp.MustCompile(`(?s)<th[^>]*>(.*?)</th>`)
tagStripRe := regexp.MustCompile(`<[^>]*>`)
total := 0
for _, tg := range targets {
ctx := context.Background()
if tg.admin {
ctx = f.ctx // 夹具的 ctx 里已经注入登录态
}
w := httptest.NewRecorder()
tg.h(w, httptest.NewRequest("GET", tg.path, nil).WithContext(ctx))
if w.Code != 200 {
t.Logf("%s 渲染 code=%d,跳过", tg.path, w.Code)
continue
}
for ti, tb := range tableRe.FindAllString(w.Body.String(), -1) {
total++
head := ""
if i, j := strings.Index(tb, "<thead>"), strings.Index(tb, "</thead>"); i >= 0 && j > i {
head = tb[i:j]
}
cols := len(thRe.FindAllString(head, -1))
// 表头为空的列(如三张表末尾那个只放「编辑」的操作格)不需要名字:窄屏堆叠时
// 它整行铺开,硬加个标签反而把按钮/表单挤窄。判据跟着表头走,两边自然一致。
blank := make([]bool, cols)
if ths := thTextRe.FindAllStringSubmatch(head, -1); len(ths) == cols {
for i, th := range ths {
blank[i] = strings.TrimSpace(tagStripRe.ReplaceAllString(th[1], "")) == ""
}
}
for ri, row := range rowRe.FindAllString(tb, -1) {
if !strings.Contains(row, "<td") {
continue
}
cells := tdRe.FindAllStringSubmatch(row, -1)
if cols > 0 && len(cells) != cols {
t.Errorf("%s 第 %d 张表第 %d 行有 %d 格,表头 %d 列", tg.path, ti+1, ri+1, len(cells), cols)
}
for ci, c := range cells {
if ci == 0 {
continue // 首格是这一行的主角,故意不带标签
}
if ci < len(blank) && blank[ci] {
continue // 表头本来就是空的
}
if !strings.Contains(c[1], "data-label=") {
t.Errorf("%s 第 %d 张表第 %d 行第 %d 格没有 data-label(窄屏会变成没有名字的裸值)",
tg.path, ti+1, ri+1, ci+1)
}
}
}
}
}
if total == 0 {
t.Fatal("一页 tb-stack 表都没找到:模板里的类名是不是被改掉了")
}
t.Logf("检查了 %d 张 tb-stack 表", total)
}