- 地图脚本: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 块一致。
201 lines
7.5 KiB
Go
201 lines
7.5 KiB
Go
package web
|
||
|
||
import (
|
||
"os"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// ---------- F5:地图脚本里的 localStorage 访问必须走兜底 ----------
|
||
//
|
||
// 隐私模式 / 禁用 Cookie 的浏览器里 localStorage 读写都会抛异常,裸调用会把整段脚本打断
|
||
// (地图白屏、图层不出来,且控制台之外看不出来)。统一走 lsGet/lsSet。
|
||
func TestMapLocalStorageGoesThroughGuard(t *testing.T) {
|
||
b, err := staticFS.ReadFile("static/js/rond-map.js")
|
||
if err != nil {
|
||
t.Fatalf("读取 rond-map.js 失败: %v", err)
|
||
}
|
||
src := string(b)
|
||
if !strings.Contains(src, "function lsGet(k)") || !strings.Contains(src, "function lsSet(k, v)") {
|
||
t.Fatal("rond-map.js 里找不到 lsGet/lsSet 兜底(写法变了,这条守卫失效)")
|
||
}
|
||
if n := strings.Count(src, "window.localStorage."); n != 2 {
|
||
t.Errorf("裸 localStorage 访问应只剩 lsGet/lsSet 里那 2 处,实际 %d 处"+
|
||
"(其余一律要走 lsGet/lsSet,否则隐私模式下整段脚本会抛异常)", n)
|
||
}
|
||
}
|
||
|
||
// ---------- F6:配置未就绪时的第二次 preview 不能丢点 ----------
|
||
func TestMapPreviewQueuesPendingPoints(t *testing.T) {
|
||
b, err := staticFS.ReadFile("static/js/rond-map.js")
|
||
if err != nil {
|
||
t.Fatalf("读取 rond-map.js 失败: %v", err)
|
||
}
|
||
src := string(b)
|
||
body := funcBody(src, "preview: function (opts)")
|
||
if body == "" {
|
||
t.Fatal("在 rond-map.js 里找不到 preview(写法变了,这条守卫失效)")
|
||
}
|
||
queue := strings.Index(body, "previewQueue[opts.id] = points;")
|
||
live := strings.Index(body, "if (live) {")
|
||
if queue < 0 || live < 0 || queue > live {
|
||
t.Error("preview:必须在 `if (live)` 之前就把本次的点记进 previewQueue," +
|
||
"否则配置未就绪时第二次 preview 的点会被静默丢掉")
|
||
}
|
||
if !strings.Contains(body, "if (previewQueue[opts.id]) points = previewQueue[opts.id];") {
|
||
t.Error("preview:配置回调里没把 previewQueue 排队的点取回来画")
|
||
}
|
||
if strings.Contains(src, "previewPending") {
|
||
t.Error("rond-map.js 里不该再有 previewPending(已由 previewQueue 取代)")
|
||
}
|
||
}
|
||
|
||
// ---------- F8:EXIF 全 0 时间不能变成 1899-11-30 ----------
|
||
func TestExifRejectsOutOfRangeDate(t *testing.T) {
|
||
b, err := staticFS.ReadFile("static/js/photo-exif.js")
|
||
if err != nil {
|
||
t.Fatalf("读取 photo-exif.js 失败: %v", err)
|
||
}
|
||
src := string(b)
|
||
guard := strings.Index(src, "Y >= 1900")
|
||
mk := strings.Index(src, "res.shot = new Date(Y, Mo - 1, D, H, Mi, S);")
|
||
flag := strings.Index(src, "res.hasTime = true;")
|
||
if guard < 0 || mk < 0 || flag < 0 || guard > mk || guard > flag {
|
||
t.Error("photo-exif.js:拍摄时间必须先过范围校验再赋给 res.shot / hasTime" +
|
||
"(否则「0000:00:00」会被 Date 归一成 1899-11-30,被当成到达时间写进库)")
|
||
}
|
||
}
|
||
|
||
// ---------- F9:findExifItemID 读 count 前必须卡住宽度 ----------
|
||
func TestExifItemIDChecksBoundsBeforeRead(t *testing.T) {
|
||
b, err := staticFS.ReadFile("static/js/photo-exif.js")
|
||
if err != nil {
|
||
t.Fatalf("读取 photo-exif.js 失败: %v", err)
|
||
}
|
||
body := funcBody(string(b), "function findExifItemID(")
|
||
if body == "" {
|
||
t.Fatal("在 photo-exif.js 里找不到 findExifItemID(写法变了,这条守卫失效)")
|
||
}
|
||
bounds := strings.Index(body, "p + 2 > u8.length")
|
||
read := strings.Index(body, "dv.getUint16(p)")
|
||
if bounds < 0 || read < 0 || bounds > read {
|
||
t.Error("findExifItemID:读 count 之前必须按 ver 卡住宽度(p+2 / p+4)——" +
|
||
"末尾恰好相等(start+4 == length)时旧守卫会放行,越界会抛 RangeError")
|
||
}
|
||
}
|
||
|
||
// ---------- F16:数据编辑页每个筛选表单都要带上全部筛选条件 ----------
|
||
//
|
||
// 三个表单各自只放自己那几个控件,其余条件靠隐藏域带着;漏一个,用户一提交
|
||
// 就会把别的列表的筛选悄悄清掉(原来「到访」表单只带 vq+back)。
|
||
func TestEditSearchFormsCarryAllFilters(t *testing.T) {
|
||
b, err := tmplFS.ReadFile("templates/admin/edit.html")
|
||
if err != nil {
|
||
t.Fatalf("读取 edit.html 失败: %v", err)
|
||
}
|
||
src := string(b)
|
||
const open = `<form method="get" action="/admin/edit"`
|
||
params := []string{`name="q"`, `name="src"`, `name="sus"`, `name="vq"`,
|
||
`name="mt"`, `name="mp"`, `name="mfrom"`, `name="mto"`}
|
||
n := 0
|
||
rest := src
|
||
for {
|
||
i := strings.Index(rest, open)
|
||
if i < 0 {
|
||
break
|
||
}
|
||
n++
|
||
rest = rest[i:]
|
||
j := strings.Index(rest, "</form>")
|
||
if j < 0 {
|
||
t.Fatalf("第 %d 个筛选表单没有收尾", n)
|
||
}
|
||
body := rest[:j]
|
||
for _, p := range params {
|
||
if !strings.Contains(body, p) {
|
||
t.Errorf("第 %d 个筛选表单少了 %s:一提交就会把别的列表的筛选清掉", n, p)
|
||
}
|
||
}
|
||
rest = rest[j:]
|
||
}
|
||
if n < 3 {
|
||
t.Fatalf("只找到 %d 个 /admin/edit 筛选表单,预期至少 3 个(写法变了,这条守卫失效)", n)
|
||
}
|
||
}
|
||
|
||
// ---------- F17:`safe` 这个现成的转义出口不该留着 ----------
|
||
func TestNoSafeEscapeFunc(t *testing.T) {
|
||
b, err := os.ReadFile(pkgFile(t, "view.go"))
|
||
if err != nil {
|
||
t.Fatalf("读取 view.go 失败: %v", err)
|
||
}
|
||
if strings.Contains(string(b), `"safe":`) {
|
||
t.Error("view.go 里不该再有 `safe` 模板函数(模板一处都没用,却是个现成的 HTML 注入出口)")
|
||
}
|
||
}
|
||
|
||
// ---------- F18:纯图标按钮要有 aria-label,图片要有 alt ----------
|
||
func TestPlaceImageControlsAreLabelled(t *testing.T) {
|
||
b, err := tmplFS.ReadFile("templates/place.html")
|
||
if err != nil {
|
||
t.Fatalf("读取 place.html 失败: %v", err)
|
||
}
|
||
src := string(b)
|
||
if !strings.Contains(src, `aria-label="删除这张图片"`) {
|
||
t.Error("place.html 模板渲染的删除按钮(×)缺 aria-label")
|
||
}
|
||
if !strings.Contains(src, `setAttribute('aria-label', '删除这张图片')`) {
|
||
t.Error("place.html 用 JS 新建的删除按钮(×)缺 aria-label")
|
||
}
|
||
if strings.Contains(src, `img.alt = ''`) {
|
||
t.Error("place.html 新建缩略图时 alt 不能留空")
|
||
}
|
||
}
|
||
|
||
// ---------- F14:窄屏导航链接的内边距别再写死一处(会被后面的触控块盖掉) ----------
|
||
func TestNavLinksNarrowRuleHasNoDeadPadding(t *testing.T) {
|
||
b, err := os.ReadFile(pkgFile(t, "static/css/app.css"))
|
||
if err != nil {
|
||
t.Fatalf("读取 app.css 失败: %v", err)
|
||
}
|
||
src := string(b)
|
||
if !strings.Contains(src, ".nav-links a { font-size: 13px; }") {
|
||
t.Fatal("找不到窄屏的 `.nav-links a { font-size: 13px; }`(写法变了,这条守卫失效)")
|
||
}
|
||
if strings.Contains(src, ".nav-links a { padding:") {
|
||
t.Error("窄屏 .nav-links a 又写了 padding:同特异性的内边距由后面「窄屏触控」那块给," +
|
||
"写在这里会被盖掉(死代码)")
|
||
}
|
||
}
|
||
|
||
// ---------- F10:补全页的 sessionStorage 读写都要包在 try 里 ----------
|
||
//
|
||
// 隐私模式下 sessionStorage 本身就抛,脏数据又会让 JSON.parse 抛;任何一处漏了 try,
|
||
// 整段脚本就从那一行断掉——下拉候选、草稿恢复全部哑掉,而且控制台之外看不出来。
|
||
func TestFillupSessionStorageIsGuarded(t *testing.T) {
|
||
b, err := tmplFS.ReadFile("templates/admin/fillup.html")
|
||
if err != nil {
|
||
t.Fatalf("读取 fillup.html 失败: %v", err)
|
||
}
|
||
src := string(b)
|
||
needles := []string{"JSON.parse(sessionStorage", "sessionStorage.setItem"}
|
||
total := 0
|
||
for _, n := range needles {
|
||
rest := src
|
||
for {
|
||
i := strings.Index(rest, n)
|
||
if i < 0 {
|
||
break
|
||
}
|
||
total++
|
||
if open := strings.LastIndex(rest[:i], "try {"); open < 0 || i-open > 160 {
|
||
t.Errorf("fillup.html:`%s` 前面没有紧挨着的 try {(隐私模式 / 脏数据会中断整段脚本)", n)
|
||
}
|
||
rest = rest[i+len(n):]
|
||
}
|
||
}
|
||
if total == 0 {
|
||
t.Fatal("fillup.html 里找不到 sessionStorage 读写(写法变了,这条守卫失效)")
|
||
}
|
||
}
|