用户报:把某行的导出值改成 MKPOICategoryCarRent、又手填了显示名「租车」, 保存成功、框里也回填了,页面却仍挂着「还没有中文名」。 两件事叠在一起: 1. 判断错了。Missing 只看内置表,没看有没有自定义覆盖 —— 填过了还照样提示。改成 categoryLabelMissing(v) && 没有覆盖。 补 TestCategoryLabelMissingRespectsOverride,并做变异验证(去掉那半个条件即报错)。 2. 值本身是拼错的。MKPOICategoryCarRent 不是真机的枚举,正确是 MKPOICategoryCarRental(少了个 al)——站内看不出任何异常,但导回手机后 那个地点显示不出类别。这正是加「显示名」时提醒过的风险,所以补一道防线: 编辑页把内置枚举名渲染给脚本,往 poi_category 写值的两个入口 (类别台账的「改名」、地点行内编辑的「自定义类别」)都过 okEnum(), 不在已知枚举里就 confirm,还能猜「你是不是想写 MKPOICategoryCarRental」。 中文值和已知枚举不打扰。 另外:这次用脚本往模板写 JS 时,\n 被多转义一层成了真换行,JS 单引号字符串 含裸换行 ⇒ 整块 <script> 语法错误、页面交互全哑,而模板解析测试照样通过。 新增 inline_script_test.go 扫所有内联脚本块(现在 31 个),字符串跨行就报错 并指出文件行号,已做变异验证。
151 lines
3.9 KiB
Go
151 lines
3.9 KiB
Go
package web
|
|
|
|
import (
|
|
"io/fs"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestInlineScriptHasNoMultilineStrings 守住「模板里的内联 JS 不许出现跨行字符串」。
|
|
//
|
|
// JS 的单引号字符串不能含裸换行;真出现一个,那一整块 <script> 会语法错误、
|
|
// **整段交互静默失效**——页面没有任何提示,控制台之外看不出来,
|
|
// 而 TestPageTemplatesParse 照样通过(模板本身是合法的)。
|
|
//
|
|
// 2026-09-20 就是这么栽的:用脚本改模板时 `\n` 被多转义了一层,写进去成了真换行,
|
|
// 当时如果这条测试在,一眼就能看到。
|
|
func TestInlineScriptHasNoMultilineStrings(t *testing.T) {
|
|
checked := 0
|
|
err := fs.WalkDir(tmplFS, "templates", func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil || d.IsDir() || filepath.Ext(path) != ".html" {
|
|
return err
|
|
}
|
|
raw, err := fs.ReadFile(tmplFS, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, blk := range scriptBlocks(string(raw)) {
|
|
checked++
|
|
if bad, ok := unterminatedString(blk.text); ok {
|
|
t.Errorf("%s:%d 内联 JS 里的字符串跨行了(第 %d 行起的这段):字符串不能含裸换行,"+
|
|
"否则整块 <script> 语法错误、页面上所有交互静默失效。\n%s",
|
|
path, blk.line+bad.line-1, blk.line+bad.line-1, strings.TrimSpace(bad.text))
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("遍历模板失败: %v", err)
|
|
}
|
|
if checked == 0 {
|
|
t.Fatal("一个内联 <script> 块都没扫到:模板目录或写法变了?")
|
|
}
|
|
t.Logf("扫了 %d 个内联脚本块", checked)
|
|
}
|
|
|
|
type scriptBlock struct {
|
|
line int // 块内容在整个文件里的起始行号
|
|
text string // <script> 与 </script> 之间的内容
|
|
}
|
|
|
|
// scriptBlocks 抠出每个 <script> 块的内容与它在文件里的起始行号。
|
|
func scriptBlocks(s string) []scriptBlock {
|
|
var out []scriptBlock
|
|
line := 1
|
|
for {
|
|
i := strings.Index(s, "<script")
|
|
if i < 0 {
|
|
break
|
|
}
|
|
line += strings.Count(s[:i], "\n")
|
|
j := strings.Index(s[i:], ">")
|
|
if j < 0 {
|
|
break
|
|
}
|
|
line += strings.Count(s[i:i+j+1], "\n")
|
|
rest := s[i+j+1:]
|
|
end := strings.Index(rest, "</script>")
|
|
if end < 0 {
|
|
break
|
|
}
|
|
start := line
|
|
out = append(out, scriptBlock{line: start, text: rest[:end]})
|
|
line += strings.Count(rest[:end], "\n") + strings.Count("</script>", "\n") + 1
|
|
s = rest[end:]
|
|
}
|
|
return out
|
|
}
|
|
|
|
// unterminatedString 找「单/双引号字符串里出现裸换行」的第一处。
|
|
// 跳过 // 行注释、/* */ 块注释、反引号模板串,以及 Go 模板动作 {{…}}
|
|
// (动作里可能带引号,不该被当成 JS 字符串的边界)。
|
|
func unterminatedString(js string) (scriptBlock, bool) {
|
|
var (
|
|
quote byte // 当前所在字符串的引号,0 表示不在字符串里
|
|
tick bool // 反引号模板串
|
|
blockC bool // /* */ 里
|
|
action int // 处于 {{…}} 里的深度
|
|
line = 1
|
|
)
|
|
for i := 0; i < len(js); i++ {
|
|
c := js[i]
|
|
if c == '\n' {
|
|
line++
|
|
if quote != 0 {
|
|
from := strings.LastIndex(js[:i], "\n") + 1
|
|
return scriptBlock{line: line, text: js[from:i]}, true
|
|
}
|
|
continue
|
|
}
|
|
if blockC {
|
|
if c == '*' && i+1 < len(js) && js[i+1] == '/' {
|
|
blockC = false
|
|
i++
|
|
}
|
|
continue
|
|
}
|
|
if action > 0 {
|
|
if c == '}' && i+1 < len(js) && js[i+1] == '}' {
|
|
action = 0
|
|
i++
|
|
}
|
|
continue
|
|
}
|
|
if tick {
|
|
if c == '`' {
|
|
tick = false
|
|
}
|
|
continue
|
|
}
|
|
if quote != 0 {
|
|
if c == '\\' {
|
|
i++
|
|
} else if c == quote {
|
|
quote = 0
|
|
}
|
|
continue
|
|
}
|
|
switch {
|
|
case c == '{' && i+1 < len(js) && js[i+1] == '{':
|
|
action++
|
|
i++
|
|
case c == '/' && i+1 < len(js) && js[i+1] == '/':
|
|
nl := strings.IndexByte(js[i:], '\n')
|
|
if nl < 0 {
|
|
return scriptBlock{}, false
|
|
}
|
|
line += strings.Count(js[i:i+nl], "\n")
|
|
i += nl
|
|
case c == '/' && i+1 < len(js) && js[i+1] == '*':
|
|
blockC = true
|
|
i++
|
|
case c == '`':
|
|
tick = true
|
|
case c == '\'' || c == '"':
|
|
quote = c
|
|
}
|
|
}
|
|
return scriptBlock{}, false
|
|
}
|