Files
rond-with-you/internal/web/visitor_copy_test.go
tamakyi 5c7cf1a0e1 对访客公开的措辞按身份分支:访客不该看到「我的结论」
用户发现未登录也显示「推荐 & 踩雷(我的结论)」,访客没法解读「谁的我的」。
顺着把访客可见模板与 static/js 全过了一遍,同病共四处——都是**写模板时站在站长视角下笔、
而那一块又对访客公开**:

1. stats.html:标题 → {{if .IsAdmin}}我的结论{{else}}站长结论{{end}};
   副标题「只统计你自己…」→ 站长「你自己」/ 访客「站长」;两列「我推荐/我踩雷的地点」同理。
2. stats.html 访客块:「与我的结论互不影响:你标你的,访客投访客的」——对访客说不通,
   改成对两边都成立的「与上面的站长结论互不影响:各存各的表,谁也不会覆盖谁」。
3. home.html:「我的标签」→「足迹标签」(同级卡片全是中性的:足迹地图/城市足迹/天气足迹…)。
4. report.html:「这一年里,你第一次踏足的这些城市」→ 去掉人称(年报是站长的回顾)。

已经写对的照它学:place.html 的投票区早就是 {{if .IsAdmin}} 分支(站长「我的结论」、
访客「大家怎么说」),rond-map.js 的气泡同理;「含家/工作」chip 与一串「只有你自己能看到」
本来就在站长分支里。

门禁 TestVisitorPagesUseThirdPersonCopy:以**访客 ctx(无 user)**渲染 /、/stats、/report,
逐个禁用词断言,同时断言站长那份**仍旧说「我的」**(别把站长自己的视角也改掉);
前置需打开 marks_public(访客才看得到那一块),用完还原。

⚠️ 变异验证时踩了老坑:直接 `go test -run …` 因缺 ROND_TEST_CONF/ROND_SRC 会**静默 skip 并报 ok**
(0.17s 就 ok,一眼假绿)。改用 ./scripts/verify.sh 后变异被正确抓住
(visitor_copy_test.go:85 报「我的结论」),还原后转绿。

全量验收 PASS=339 FAIL=0 SKIP=0。
2026-10-01 02:15:49 +08:00

110 lines
4.6 KiB
Go
Raw Permalink 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 (
"context"
"database/sql"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// ---------- 措辞要分清「站长视角」与「访客视角」 ----------
//
// 用户实测反馈:未登录时统计页也显示「推荐 & 踩雷(我的结论)」,访客会困惑「谁的我的」。
// 这类毛病都出在同一处:**写模板时站在站长视角下笔**(我/你自己),而那一块又对访客公开。
// 地点页的投票区早就是按身份分支的(站长看「我的结论」、访客看「大家怎么说」),
// 这里把它做成回归用例:以**访客身份**取回页面,逐个钉死那些只有站长读得懂的说法。
// renderAsVisitor 以访客身份取回页面(ctx 里没有登录态)。
func renderAsVisitor(t *testing.T, f *movementFixture, h http.HandlerFunc, path string) string {
t.Helper()
rec := httptest.NewRecorder()
// 访客就是「没有 user 的 ctx」:IsAdmin() 看的是 p.User 是否为 nil
req := httptest.NewRequest("GET", path, nil).WithContext(context.Background())
h(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("访客取 %s 状态码 %d:%s", path, rec.Code, rec.Body.String())
}
body := rec.Body.String()
if !strings.HasSuffix(strings.TrimSpace(body), "</html>") {
t.Fatalf("访客取 %s 没渲染到底:…%s", path, body[len(body)-300:])
}
return body
}
// renderAsAdmin 以站长身份取回页面。
func renderAsAdmin(t *testing.T, f *movementFixture, h http.HandlerFunc, path string) string {
t.Helper()
rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", path, nil).WithContext(f.ctx)
h(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("站长取 %s 状态码 %d:%s", path, rec.Code, rec.Body.String())
}
return rec.Body.String()
}
// forceMarksPublic 把「推荐 / 踩雷对访客公开」打开——访客版页面里那一块才会渲染出来。
// 用例结束后还原设置(站点设置是全局一行,别把后面的用例带偏)。
func forceMarksPublic(t *testing.T, f *movementFixture) {
t.Helper()
var old sql.NullString
if err := f.pg.QueryRowContext(f.ctx, `SELECT value::text FROM settings WHERE key='site'`).Scan(&old); err != nil && err != sql.ErrNoRows {
t.Fatalf("读站点设置失败: %v", err)
}
if _, err := f.pg.ExecContext(f.ctx, `INSERT INTO settings (key, value)
VALUES ('site', '{"marks_public":true}'::jsonb)
ON CONFLICT (key) DO UPDATE SET value = jsonb_set(settings.value, '{marks_public}', 'true'::jsonb, true)`); err != nil {
t.Fatalf("打开「对访客公开」失败: %v", err)
}
t.Cleanup(func() {
if old.Valid {
f.pg.ExecContext(f.ctx, `UPDATE settings SET value=$1::jsonb WHERE key='site'`, old.String)
return
}
f.pg.ExecContext(f.ctx, `DELETE FROM settings WHERE key='site'`)
})
}
// visitorCopyForbidden 是**只有站长读得懂**的说法:访客看到「我 / 你自己」会一头雾水。
var visitorCopyForbidden = []string{"我的结论", "你自己", "我推荐的地点", "我踩雷的地点", "你标你的", "我的标签"}
func TestVisitorPagesUseThirdPersonCopy(t *testing.T) {
f := newMovementFixture(t)
forceMarksPublic(t, f)
// ① 统计页:站长仍旧说「我的」,访客一律第三人称
adminStats := renderAsAdmin(t, f, f.srv.statsPage, "/stats")
if !strings.Contains(adminStats, "我的结论") {
t.Error("站长自己看时应当说「我的结论」——按身份分支,不该把站长那份也改成第三人称")
}
visitorStats := renderAsVisitor(t, f, f.srv.statsPage, "/stats")
for _, bad := range visitorCopyForbidden {
if strings.Contains(visitorStats, bad) {
t.Errorf("访客版统计页出现了只有站长读得懂的说法:%q", bad)
}
}
if !strings.Contains(visitorStats, "站长结论") {
t.Error("访客版统计页应当用「站长结论」这类第三人称说法")
}
// 访客那一块自己得说清楚「谁投的」
if !strings.Contains(visitorStats, "访客点赞") {
t.Error("访客版统计页应当有独立的「访客点赞」一块")
}
// ② 首页:标签卡原先写「我的标签」,访客看不出是谁的
visitorHome := renderAsVisitor(t, f, f.srv.home, "/")
for _, bad := range visitorCopyForbidden {
if strings.Contains(visitorHome, bad) {
t.Errorf("访客版首页出现了只有站长读得懂的说法:%q", bad)
}
}
// ③ 年报:那是站长的年度回顾,不该对访客说「你这一年…」
visitorReport := renderAsVisitor(t, f, f.srv.reportPage, "/report")
if strings.Contains(visitorReport, "你第一次踏足") {
t.Error("访客版年报里把站长的一年说成了「你」的")
}
}