package web import ( "bytes" "html/template" "path/filepath" "strings" "testing" "time" ) // TestPageTemplatesParse 把每个页面模板都解析一遍。 // 模板是启动时编译的(见 New),语法错会让整个服务起不来; // 这个测试把它提前到 go test 阶段,不用真起一次服务才发现。 func TestPageTemplatesParse(t *testing.T) { for key, def := range pageFiles { if _, err := template.New(filepath.Base(def.files[0])).Funcs(templateFuncs()). ParseFS(tmplFS, def.files...); err != nil { t.Errorf("解析模板 %s 失败: %v", key, err) } } } // TestLayoutFooterToleratesMissingLastVisit 钉住页脚在「数据集一条到访都没有」时也渲染得完。 // // last_visit_at 可以为 NULL(refreshDatasetAgg 用 LEFT JOIN 求 max,零到访就写 NULL), // 而 d 的形参是 time.Time、实参是 *time.Time:非 nil 时模板会自动解一层指针,**nil 时报 // "dereference of nil pointer" 并中断渲染**。render() 只把错误记进日志,客户端拿到的是 // HTTP 200 加一个「页脚之后整段不存在」的页面——滚动恢复、地图脚本引入全丢, // 服务端一点动静都没有。所以这里断言「有收尾的 」,而不只是断言没报错。 func TestLayoutFooterToleratesMissingLastVisit(t *testing.T) { base, err := template.New("layout.html").Funcs(templateFuncs()).ParseFS(tmplFS, "templates/layout.html") if err != nil { t.Fatalf("解析 layout.html: %v", err) } tpl, err := base.Parse(`{{define "content"}}正文{{end}}`) if err != nil { t.Fatalf("补 content 块: %v", err) } last := time.Date(2026, 9, 21, 12, 0, 0, 0, time.Local) cases := []struct { name string ds *Dataset want string }{ {"没有数据集", nil, ""}, {"数据集里一条到访都没有", &Dataset{Name: "空数据集"}, ""}, {"正常", &Dataset{Name: "有数据", Last: &last}, "数据截至 2026-09-21"}, } for _, c := range cases { var buf bytes.Buffer if err := tpl.ExecuteTemplate(&buf, "layout", Page{Nav: "home", Title: "测", Dataset: c.ds}); err != nil { t.Fatalf("%s: 渲染失败: %v", c.name, err) } got := buf.String() if !strings.Contains(got, "") { t.Errorf("%s: 页面被截断(没有收尾的 )", c.name) } if c.want == "" { if strings.Contains(got, "数据截至") { t.Errorf("%s: 没有可信的截止时间时不该渲染「数据截至」", c.name) } } else if !strings.Contains(got, c.want) { t.Errorf("%s: 页脚应含 %q", c.name, c.want) } } }