package web import ( "io/fs" "path/filepath" "regexp" "strings" "testing" ) // TestNoWindowPrompt 守住「页面里不许再出现 window.prompt」。 // // window.prompt 会把提示文本与默认值写进地址栏:Chrome 把它拼成 // `当前页?新的活动名称=吃饭` 这样的 URL 并留进历史记录——刷新即丢, // 分享链接还带着原文。confirm / alert 没有这个毛病,只有 prompt 有。 // // 2026-10-08 修掉了两处(report.html 复制链接的兜底、edit.html 活动改名), // 统一换成 static/js/prompt-box.js 的 rondPrompt.ask():原生 居中弹层, // 带遮罩、Esc 关闭、焦点自动进输入框,不碰地址栏;旧浏览器降级成页面顶部一条。 // // 只扫代码,不扫注释——注释里可以写「为什么不用 prompt」。 func TestNoWindowPrompt(t *testing.T) { // 匹配 prompt( 的调用形态,排除: // - 注释与字符串里出现的 "prompt"(stripped 之后再找) // - 别的对象上的方法(rondPrompt.ask 不是 prompt) callRe := regexp.MustCompile(`(?:^|[^\w.$])(?:window\s*\.\s*)?prompt\s*\(`) checked := 0 hits := 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++ code := stripComments(blk.text) for _, ln := range strings.Split(code, "\n") { if !callRe.MatchString(ln) { continue } hits++ t.Errorf("%s:%d 模板里还有 window.prompt 调用:%s\n"+ "它会把提示文本与默认值写进地址栏并留进历史(刷新即丢、分享带原文)。\n"+ "换成 window.rondPrompt.ask(title, value, {required/readonly})。", path, blk.line, strings.TrimSpace(ln)) } } // 内联属性(onclick="return confirm(...)")里也可能有 prompt for _, ln := range strings.Split(string(raw), "\n") { if strings.Contains(ln, " 块都没扫到:模板目录或写法变了?") } t.Logf("扫了 %d 个内联脚本块,命中 %d 处 prompt", checked, hits) } // TestPromptBoxScriptExists 保证 rondPrompt 真的存在。 // // 上面那条门禁只是「不许用 prompt」,若哪天 static/js/prompt-box.js 被删了、 // 模板里的 rondPrompt.ask 变成 undefined,页面上的改名/复制功能会在点击时 // 抛 TypeError —— 而模板解析测试照样通过。这条把「文件在 + 导出接口在」钉住。 func TestPromptBoxScriptExists(t *testing.T) { // staticFS 是 `//go:embed static`,所以路径要带 static/ 前缀 raw, err := fs.ReadFile(staticFS, "static/js/prompt-box.js") if err != nil { t.Fatalf("读不到 static/js/prompt-box.js: %v", err) } src := string(raw) if !strings.Contains(src, "rondPrompt") { t.Error("prompt-box.js 里没有导出 rondPrompt,模板里的 rondPrompt.ask 会是 undefined") } if !strings.Contains(src, "ask:") && !strings.Contains(src, "ask :") { t.Error("prompt-box.js 没有导出 ask 函数") } // CSS 也要在: 的 ::backdrop 遮罩与面板样式都在里面, // 缺了会变成「无遮罩的白板」,不算功能坏但会明显走样。 css, err := fs.ReadFile(staticFS, "static/css/app.css") if err != nil { t.Fatalf("读不到 static/css/app.css: %v", err) } if !strings.Contains(string(css), "dialog.prompt-box") { t.Error("app.css 里没有 dialog.prompt-box 样式,弹层会没有边框/圆角/遮罩") } if !strings.Contains(string(css), "prompt-fallback") { t.Error("app.css 里没有 prompt-fallback 样式,旧浏览器的降级形态会没样式") } } // stripComments 去掉 JS 行注释与块注释,避免注释里提到 prompt( 被误判。 func stripComments(s string) string { var b strings.Builder b.Grow(len(s)) lines := strings.Split(s, "\n") for i, ln := range lines { if i > 0 { b.WriteByte('\n') } // 粗略但够用:行注释、以及块注释的起止标记 if idx := strings.Index(ln, "//"); idx >= 0 { ln = ln[:idx] } ln = strings.ReplaceAll(ln, "/*", "") ln = strings.ReplaceAll(ln, "*/", "") b.WriteString(ln) } return b.String() }