Files
rond-with-you/internal/web/no_prompt_test.go
T
tamakyi 4015ddf373 时间框跳段 + 弹窗不再脏地址栏 + 统计脚本做成后台开关
时间框(flatpickr allowInput 的手输体验)
- flatpickr 把 <input type=datetime-local> 就地改成 type=text,原生分段编辑器被替换掉;
  allowInput 又是「整串解析」语义:实测 parseDate('2026') 只出年份、'202666' 才出月份,
  于是「年份很长才跳下一位」;'20260606' 还会被静默错解成 2030-12-06 且不报错。
- 新增 static/js/datetime-input.js:自定义 parseDate(纯数字只收 4/6/8/12/14 位,
  歧义与非法日期一律拒)+ 输入引导(段满即补分隔符,敲完 4 位年份立刻跳月份位)。
  按标准格式不做自动补零、不前滚;越界值原样保留交报错,不静默改成合法值。
- 三处页面(编辑/照片/专题)共用 attachPicker,配置与原先一致,模板净减 5 行。

弹窗
- window.prompt 会把提示文本与默认值写进地址栏并留进历史(confirm/alert 无此问题)。
- 新增 static/js/prompt-box.js 替代:原生 <dialog>.showModal(),遮罩/Esc/焦点齐全,
  旧浏览器降级成页面顶部一条。已换 report.html 复制链接兜底与 edit.html 活动改名。
- 门禁 no_prompt_test.go:静态扫模板禁止 prompt(,并钉住 JS 与 CSS 都在。

统计脚本
- 原来只能改 conf/app.ini(要重启),现在后台「站点设置」可开关 + 填写,默认关闭。
- cleanTrackScript 挡住注入面:开闭 <script> 标签数不配平(提前闭合会让页面散架)、
  src 的 javascript:/data: 伪协议、超长截断。注意不能用「出现 </script 就拒」当规则——
  百度统计、Umami 贴的就是完整 HTML,正常用法会被挡死。

门禁
- datetime-input.test.js:34 条断言(node 直跑,无依赖)。
- TestCleanTrackScript:逐条边界 + 定义性不变量(任何输入都不得产出含 </script 的非空结果)。
- 三项均做过变异验证,改坏实现都会被对应测试抓到。

实测:真实站点逐键 2→20→202→2026-→2026-06-→2026-06-06T;
统计脚本六个场景(默认关/只填不开/开+URL/后台与登录页不挂/危险输入/关掉恢复)全部符合预期。
全量 internal/web 204 例 0 skip,其余包全绿,gofmt 干净。
2026-10-08 18:27:59 +08:00

125 lines
4.5 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 (
"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():原生 <dialog> 居中弹层,
// 带遮罩、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, "<script") {
continue
}
if callRe.MatchString(stripComments(ln)) {
hits++
t.Errorf("%s 模板非脚本区还有 prompt 调用:%s\n"+
"换成 window.rondPrompt.ask()。", path, strings.TrimSpace(ln))
}
}
return nil
})
if err != nil {
t.Fatalf("遍历模板失败: %v", err)
}
if checked == 0 {
t.Fatal("一个内联 <script> 块都没扫到:模板目录或写法变了?")
}
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 也要在:<dialog> 的 ::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()
}