Files
rond-with-you/internal/web/wide_table_layout_test.go
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

235 lines
7.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package web
import (
"context"
"io/fs"
"net/http"
"net/http/httptest"
"regexp"
"strconv"
"strings"
"testing"
)
// TestWideTableCardsAreFullWidth 守住「宽表卡片必须整行」这条不变量。
//
// 这批表的 min-width 是 640 / 700,只要卡片被塞进 g2 的半宽列(视口 1440 时卡片只有 494px),
// 桌面端就得横滑 100+px 才看得全(/stats 三张、/report「南北两端」原来就是这样)。卡片能不能
// 放下只由它自己的宽度决定,所以宽表卡片一律不放进栅格——将来想给页面加 g2 排版时,这条会拦住。
//
// 同时钉住 CSS 侧的两个挂载点:.tw 得是容器、容器查询阈值得在(否则窄下来不会竖排,又会横滑)。
//
// ROND_TEST_CONF=conf/app-test.ini ROND_SRC=data/xxx.rondbackup go test ./internal/web -run WideTableCards -v
func TestWideTableCardsAreFullWidth(t *testing.T) {
f := newMovementFixture(t)
type target struct {
path string
h http.HandlerFunc
admin bool
}
tables := 0
for _, tg := range []target{
{"/stats", f.srv.statsPage, false},
{"/report", f.srv.reportPage, false},
{"/places", f.srv.placesPage, false},
{"/", f.srv.home, false},
{"/admin", f.srv.adminHome, true},
{"/admin/topics", f.srv.topicsAdminPage, true},
// 数据编辑页的三张表原来漏了 tb-stack:它们默认折叠在卡片里,探针量到的是 display:none,
// 「窄屏会不会横滑」这件事在默认态根本看不出来(站长 2026-09-19 就是这么反馈上来的)。
{"/admin/edit", f.srv.editPage, true},
{"/admin/photos", f.srv.adminPhotosPage, true},
} {
ctx := context.Background()
if tg.admin {
ctx = f.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
}
tables += checkWideTablesOutsideGrid(t, tg.path, w.Body.String())
}
if tables == 0 {
t.Fatal("一页 tb-stack 表都没渲染出来:模板里的类名是不是被改掉了")
}
css, err := fs.ReadFile(staticFS, "static/css/app.css")
if err != nil {
t.Fatalf("读不到 app.css: %v", err)
}
// 堆叠规则必须包在 @media 里,而且阈值要够宽:
// 卡片内容宽 ≈ 视口 − 93(页面 + 卡片的内边距),最宽的表 min-width 700 ⇒ 至少 792 才放得下。
// 这条断言挡的是两个真实踩过的坑:① 阈值 620 时视口 621~793 的卡片两头不靠、照样横滑;
// ② 换成 @container 后旧手机(iOS<16 / Chrome<105)整块规则失效,表格退化成横滑 388px。
clean := stripCSSComments(string(css))
blocks := cssMediaBlocks(clean)
found := 0
for _, loc := range regexp.MustCompile(`tb-stack`).FindAllStringIndex(clean, -1) {
found++
blk, ok := innermostMedia(blocks, loc[0])
if !ok {
t.Errorf("app.css 里有 tb-stack 规则不在任何 @media 里:旧手机的容器查询/自适应会失效(附近:%s)",
snippetAt(clean, loc[0]))
continue
}
if blk.maxW < 792 {
t.Errorf("tb-stack 规则所在的 @media 阈值只有 %dpx:视口 %d~792 的卡片放不下表格,又会横滑",
blk.maxW, blk.maxW+1)
}
}
if found == 0 {
t.Error("app.css 里一条 tb-stack 堆叠规则都没有:宽表在窄屏不会竖排")
}
t.Logf("检查了 %d 张 tb-stack 表、%d 条堆叠规则", tables, found)
}
// stripCSSComments 去掉 /* */ 注释:注释里出现的 @media / @container 字样会把解析带偏。
func stripCSSComments(s string) string {
var b strings.Builder
for i := 0; i < len(s); {
if i+1 < len(s) && s[i] == '/' && s[i+1] == '*' {
j := strings.Index(s[i+2:], "*/")
if j < 0 {
break
}
i += 2 + j + 2
b.WriteByte(' ')
continue
}
b.WriteByte(s[i])
i++
}
return b.String()
}
type cssMediaBlock struct{ start, end, maxW int }
// cssMediaBlocks 找出所有 @media 块的区间和它的 max-width 阈值(没有就记 -1)。
func cssMediaBlocks(css string) []cssMediaBlock {
var out []cssMediaBlock
headRe := regexp.MustCompile(`@media([^{]*)\{`)
widthRe := regexp.MustCompile(`max-width:\s*(\d+)px`)
for _, m := range headRe.FindAllStringSubmatchIndex(css, -1) {
maxW := -1
if w := widthRe.FindStringSubmatch(css[m[2]:m[3]]); w != nil {
maxW, _ = strconv.Atoi(w[1])
}
depth, end := 0, -1
for i := m[1] - 1; i < len(css); i++ {
switch css[i] {
case '{':
depth++
case '}':
depth--
if depth == 0 {
end = i
}
}
if end >= 0 {
break
}
}
if end > 0 {
out = append(out, cssMediaBlock{m[0], end, maxW})
}
}
return out
}
func innermostMedia(blocks []cssMediaBlock, pos int) (cssMediaBlock, bool) {
best := cssMediaBlock{}
ok := false
for _, b := range blocks {
if b.start < pos && pos < b.end && (!ok || b.start > best.start) {
best, ok = b, true
}
}
return best, ok
}
func snippetAt(s string, pos int) string {
lo, hi := pos-20, pos+30
if lo < 0 {
lo = 0
}
if hi > len(s) {
hi = len(s)
}
return strings.ReplaceAll(s[lo:hi], "\n", " ")
}
// checkWideTablesOutsideGrid 用 div 栈还原每个表格的祖先,确认两件事:
// ① 每张 tb-stack 表都不在任何 g2 网格里(半宽卡片会横滑);
// ② 凡是被 .tw 包住的表格都得带 tb-stack —— 否则窄屏没有任何堆叠规则可命中,照样横滑。
// 只靠「最近一个 class=\"grid g2\" 出现在前面」会被已闭合的网格误伤,所以按标签顺序配对。
// 返回这一页检查了几张堆叠表(数据为空时某些页面本来就没有表)。
func checkWideTablesOutsideGrid(t *testing.T, path, body string) int {
t.Helper()
tagRe := regexp.MustCompile(`(?s)<div\b[^>]*>|</div>|<table\b[^>]*>`)
classRe := regexp.MustCompile(`class="([^"]*)"`)
type frame struct{ g2, tw bool }
var stack []frame
seen := 0
for _, tag := range tagRe.FindAllString(body, -1) {
switch {
case strings.HasPrefix(tag, "</div"):
if len(stack) > 0 {
stack = stack[:len(stack)-1]
}
case strings.HasPrefix(tag, "<div"):
var f frame
if m := classRe.FindStringSubmatch(tag); m != nil {
for _, c := range strings.Fields(m[1]) {
switch c {
case "g2":
f.g2 = true
case "tw":
f.tw = true
}
}
}
stack = append(stack, f)
case strings.HasPrefix(tag, "<table"):
inTW, inG2 := false, false
for _, f := range stack {
inTW = inTW || f.tw
inG2 = inG2 || f.g2
}
if !strings.Contains(tag, "tb-stack") {
// 照片表由脚本逐格生成、列数固定,移动端走「隐藏第 3 列」那条路,暂不纳入堆叠。
if strings.Contains(tag, "photo-table") {
continue
}
// 判据从「在 .tw 里」扩成「只要用了 tb 这个宽表基类就得堆叠」:
// 漏了 .tw 包裹的宽表(/admin/fog/mask 的遮罩表就是这样)以前两条守卫都看不见。
usesTB := false
if m := classRe.FindStringSubmatch(tag); m != nil {
usesTB = strings.Contains(" "+m[1]+" ", " tb ")
}
if inTW || usesTB {
t.Errorf("%s:宽表没带 tb-stack(窄屏没有堆叠规则可命中,只能横滑):%s", path, tag)
}
continue
}
seen++
// 两个类是成对的:app.css 里每一条堆叠规则都写成 `.tw > table.tb.tb-stack …`。
// 只写 tb-stack 一条都命中不了,表格退回 `.tw > table.tb` 的 640px 底宽,
// 窄屏照旧横滑——而且横滑是在 .tw 里滚的,页面本身不溢出,肉眼很难发现
// (2026-09-20 加「活动与类别」卡片时就这么漏过一次)。
if m := classRe.FindStringSubmatch(tag); m == nil || !strings.Contains(" "+m[1]+" ", " tb ") {
t.Errorf("%s:tb-stack 表少了 tb 类(class=%q)——堆叠规则匹配不到,窄屏只能横滑", path, tag)
}
// 只有标了 tb-stack 的才算「宽表」;普通的窄表放进 g2 没问题,不该报。
if inG2 {
t.Errorf("%s:一张宽表被放在 g2 网格里(卡片只剩半宽,桌面端要横滑)", path)
}
}
}
return seen
}