Files
rond-with-you/internal/web/wide_table_layout_test.go
T
tamakyi 9e01923244 宽表在窄屏真的不横滑了:堆叠判据退回媒体查询,后台三张表补上
真机反馈「手机上还是要左右滑」,两个根因:

1. 上一版把堆叠判据写成了 @container。它按卡片自己的宽度判断更准,但要
   iOS 16 / Chrome 105 才支持——旧手机把整块规则当不认识的东西丢掉,表格
   完全不堆叠、退化成横滑 328~388px,比改之前还糟。改回 @media,阈值从
   620 抬到 800:卡片内容宽 ≈ 视口 − 93,最宽的表 min-width 700,视口 793
   以上才放得下,620 那档会让 621~792 两头不靠。

2. /admin/edit 的地点/到访/行程三张表压根没加 tb-stack。它们默认折叠在
   data-card 里,探针在默认态量到的是 display:none,前两轮审计都没扫到。

顺带修掉三处被这次改动顶出来的问题:
- 宽表卡片不放 g2:/stats /report 有几张表被塞进半宽卡片,视口 1440 时
  卡片只有 494px,桌面端照样要横滑 100+px。改成整行(只动含表的那些网格,
  「推荐 & 踩雷」「类型分布 + 一天中的分布」这类没有表的仍保持两列)。
- 操作格的 inline white-space:nowrap 会继承进行内编辑表单,堆叠态下
  「已选:#403 …」那一行不换行、把卡片顶破。挪成 .ops 类,窄屏放开。
- 行内编辑表单的 min-width 改成 --efw 变量(inline 的 min-width 压不过
  CSS 覆盖),堆叠态置 0 让它自然铺满。

导航装不下时右缘渐隐一下,提示还能往右滑;滑到末端自动收掉。用 mask 而不是
盖一层渐变伪元素——伪元素会把链接文字一起遮住。

回归:tb_stack_test.go 补 /admin/edit(12 张表),表头为空的列(如末尾操作格)
不再要求 data-label;新增 wide_table_layout_test.go 守住「宽表卡片必须整行」+
「凡被 .tw 包住的表必须带 tb-stack」+「堆叠规则必须在 max-width≥792 的 @media 里」。
2026-09-19 02:04:03 +08:00

219 lines
6.9 KiB
Go
Raw 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 inTW && !strings.Contains(tag, "photo-table") {
t.Errorf("%s:.tw 里的表格没带 tb-stack(窄屏没有堆叠规则可命中,只能横滑)", path)
}
continue
}
seen++
// 只有标了 tb-stack 的才算「宽表」;普通的窄表放进 g2 没问题,不该报。
if inG2 {
t.Errorf("%s:一张宽表被放在 g2 网格里(卡片只剩半宽,桌面端要横滑)", path)
}
}
}
return seen
}