移动端有 7 张表要横滑 300+px 才看得全,滑着还对不上表头(地点库 8 列、统计 3 张、专题详情、 首页天气、后台数据集表与数据说明、专题管理、报告页 2 张)。给这些表加 tb-stack: 窄屏隐藏 thead、每行一屏一块、每格「字段名 + 值」竖排,值多行时用悬挂缩进。 - 字段名取自 td 的 data-label(首格是这行主角,故意不带标签;操作格给「操作」)—— 这样改列顺序也不会串标签,比把名字写进 CSS 的 nth-child 稳。 - 表格结构、链接、表单、按钮一律没动(只是隐藏 thead + 改 display),桌面与平板仍是原表。 - 断点只用 @media (max-width:620px):试过容器查询(能顺手治掉「宽屏里 g2 窄卡片也要横滑」), 但实测会把 /stats 三个卡片撑高 1300px,太臃肿,不划算。 实测:390/620 十张表需横滑全部为 0、每行每格都有标签;621/700/1440 的横滑量与改动前一致 (桌面零变化);128 个链接/按钮滚动到可见后逐个点测均可点中,无裁切;全站审计 390 站长/访客 + 320 + 1440 溢出全 0、0 JS 报错,小触控与小字数与改动前逐页一致(320 下露外元素 /places 277→2、 /stats 114→12、/admin 46→4,剩下的都是有意的横滑容器)。 新增回归 internal/web/tb_stack_test.go:渲染 7 个页面、逐行核对「每个非首格都有 data-label」 且「格子数 == 表头列数」(变异验证过:去掉一个标签立刻 FAIL)。
95 lines
3.1 KiB
Go
95 lines
3.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},
|
|
}
|
|
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>]`)
|
|
|
|
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))
|
|
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 !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)
|
|
}
|