Files
rond-with-you/internal/web/fillup_test.go
T
tamakyi 824c1e3b3d 补全页加分页,每页 30 行
218 行在手机上堆成一屏一行有五万多像素长,分页后第 1 页只有八千多。

页码会夹紧:?page=999 停在最后一页而不是给空白页,解析不出来回第一页。
提交后留在同一页——处理完的行会从列表里消失,停在这儿才看得见剩下的。
翻页前若这一页有改过还没提交的下拉,先确认一次(预选的建议不算「改过」)。
2026-09-19 18:25:16 +08:00

236 lines
7.9 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 (
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
)
// fillupPlace 在测试数据集里造一个「没类别、名下到访全都没活动」的地点。
func fillupPlace(t *testing.T, f *movementFixture) (placeID int64, placePK int) {
t.Helper()
const pk = 670001
if err := f.pg.QueryRowContext(f.ctx, `INSERT INTO places (dataset_id, src_pk, name, lat, lon)
VALUES ($1,$2,'测试补全点',31.1,121.1) RETURNING id`, f.dsID, pk).Scan(&placeID); err != nil {
t.Fatalf("建地点失败: %v", err)
}
for _, vpk := range []int{670002, 670003} {
if _, err := f.pg.ExecContext(f.ctx, `INSERT INTO visits
(dataset_id, src_pk, place_id, arrival, departure)
VALUES ($1,$2,$3,'2026-01-01 10:00:00+08','2026-01-01 11:00:00+08')`,
f.dsID, vpk, placeID); err != nil {
t.Fatalf("建到访失败: %v", err)
}
}
t.Cleanup(func() {
f.pg.ExecContext(f.ctx, `DELETE FROM visits WHERE dataset_id=$1 AND place_id=$2`, f.dsID, placeID)
f.pg.ExecContext(f.ctx, `DELETE FROM places WHERE id=$1`, placeID)
})
return placeID, pk
}
func applyFillup(t *testing.T, f *movementFixture, form url.Values) string {
t.Helper()
rec := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/admin/fillup/apply", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
f.srv.adminFillupApply(rec, req.WithContext(f.ctx))
if rec.Code != http.StatusSeeOther {
t.Fatalf("补全应跳回页面: code=%d body=%s", rec.Code, rec.Body.String())
}
return rec.Header().Get("Location")
}
// 补活动是按**地点**批量做的:一次把它名下所有没活动的到访一起设,不是只改一条。
func TestFillupSetsActivityForWholePlace(t *testing.T) {
f := newMovementFixture(t)
placeID, _ := fillupPlace(t, f)
acts, err := f.srv.activityOptions(f.ctx, f.dsID)
if err != nil || len(acts) == 0 {
t.Fatalf("取活动失败: %v", err)
}
target := acts[0].Name
loc := applyFillup(t, f, url.Values{
"act_" + strconv.FormatInt(placeID, 10): {target},
})
if strings.Contains(loc, "err=") {
t.Fatalf("补全失败: %s", loc)
}
if n := f.scalar(t, `SELECT count(*) FROM visits WHERE dataset_id=$1 AND place_id=$2
AND activity_id IS NULL`, f.dsID, placeID); n != 0 {
t.Fatalf("该地点两条到访都该设上活动,还剩 %d 条没设", n)
}
var got string
if err := f.pg.QueryRowContext(f.ctx, `SELECT a.name FROM visits v JOIN activities a ON a.id=v.activity_id
WHERE v.dataset_id=$1 AND v.place_id=$2 LIMIT 1`, f.dsID, placeID).Scan(&got); err != nil {
t.Fatalf("读活动失败: %v", err)
}
if got != target {
t.Fatalf("活动应为 %q,实际 %q", target, got)
}
}
// 「只填空」:已经有值的一律不动,哪怕表单里填了别的。
func TestFillupNeverOverwrites(t *testing.T) {
f := newMovementFixture(t)
placeID, _ := fillupPlace(t, f)
// 先给一条到访设好活动,给地点设好类别
acts, _ := f.srv.activityOptions(f.ctx, f.dsID)
first := acts[0].Name
second := "补全测试活动"
if len(acts) > 1 {
second = acts[1].Name
}
if _, err := f.pg.ExecContext(f.ctx, `UPDATE visits SET activity_id=$1
WHERE dataset_id=$2 AND place_id=$3 AND src_pk=670002`,
acts[0].ID, f.dsID, placeID); err != nil {
t.Fatalf("预置活动失败: %v", err)
}
if _, err := f.pg.ExecContext(f.ctx, `UPDATE places SET poi_category='MKPOICategoryPark'
WHERE id=$1`, placeID); err != nil {
t.Fatalf("预置类别失败: %v", err)
}
applyFillup(t, f, url.Values{
"act_" + strconv.FormatInt(placeID, 10): {second},
"cat_" + strconv.FormatInt(placeID, 10): {"MKPOICategoryZoo"},
})
// 原来那条不能被改掉
var got string
if err := f.pg.QueryRowContext(f.ctx, `SELECT a.name FROM visits v JOIN activities a ON a.id=v.activity_id
WHERE v.dataset_id=$1 AND v.place_id=$2 AND v.src_pk=670002`, f.dsID, placeID).Scan(&got); err != nil {
t.Fatalf("读活动失败: %v", err)
}
if got != first {
t.Fatalf("已有活动的到访不该被改:期望 %q,实际 %q", first, got)
}
// 已经填过的类别也不能被覆盖
var cat string
if err := f.pg.QueryRowContext(f.ctx, `SELECT COALESCE(poi_category,'') FROM places WHERE id=$1`, placeID).Scan(&cat); err != nil {
t.Fatalf("读类别失败: %v", err)
}
if cat != "MKPOICategoryPark" {
t.Fatalf("已有类别不该被覆盖,实际 %q", cat)
}
// 但另一条原本没活动的到访应当补上:这个地点不该再有无活动的到访
if n := f.scalar(t, `SELECT count(*) FROM visits WHERE dataset_id=$1 AND place_id=$2
AND activity_id IS NULL`, f.dsID, placeID); n != 0 {
t.Fatalf("原本没活动的那条应当补上,还剩 %d 条没设", n)
}
}
// 填一个现有列表里没有的名字,应当自动建成新的活动类型(用户要的「匹配不上就新增」)。
func TestFillupCreatesUnknownActivity(t *testing.T) {
f := newMovementFixture(t)
placeID, _ := fillupPlace(t, f)
before := f.scalar(t, `SELECT count(*) FROM activities WHERE dataset_id=$1`, f.dsID)
applyFillup(t, f, url.Values{"act_" + strconv.FormatInt(placeID, 10): {"洗碗机测试活动"}})
if n := f.scalar(t, `SELECT count(*) FROM activities WHERE dataset_id=$1 AND name=$2`,
f.dsID, "洗碗机测试活动"); n != 1 {
t.Fatalf("该新建一个活动类型,实际 %d 个", n)
}
if n := f.scalar(t, `SELECT count(*) FROM activities WHERE dataset_id=$1`, f.dsID); n != before+1 {
t.Fatalf("活动总数应 +1,实际 %d(原 %d)", n, before)
}
t.Cleanup(func() {
f.pg.ExecContext(f.ctx, `DELETE FROM activities WHERE dataset_id=$1 AND name=$2`, f.dsID, "洗碗机测试活动")
})
}
func itoa(v int64) string {
return strings.TrimSpace(strings.ReplaceAll(strings.Trim(strings.Join([]string{}, ""), " "), " ", "")) + intToString(v)
}
func intToString(v int64) string {
if v == 0 {
return "0"
}
neg := v < 0
if neg {
v = -v
}
var b []byte
for v > 0 {
b = append([]byte{byte('0' + v%10)}, b...)
v /= 10
}
if neg {
return "-" + string(b)
}
return string(b)
}
func TestClampPage(t *testing.T) {
cases := []struct {
raw string
count int
want int
}{
{"", 8, 1},
{"1", 8, 1},
{"8", 8, 8},
{"999", 8, 8}, // 超了就停在最后一页,别给空白页
{"0", 8, 1},
{"-3", 8, 1},
{"abc", 8, 1},
{"3", 0, 3}, // 没有数据时不夹(总会在最后回到 1)
}
for _, c := range cases {
if got := clampPage(c.raw, c.count); got != c.want {
t.Errorf("clampPage(%q, %d) = %d,期望 %d", c.raw, c.count, got, c.want)
}
}
}
// 每页只渲染 30 行;页码越界时夹回最后一页。
func TestFillupPagination(t *testing.T) {
f := newMovementFixture(t)
// 造 35 个「缺类别」的地点,凑出两页
for i := 0; i < 35; i++ {
pk := 680000 + i
if _, err := f.pg.ExecContext(f.ctx, `INSERT INTO places (dataset_id, src_pk, name, lat, lon)
VALUES ($1,$2,$3,$4,$5)`, f.dsID, pk, "分页测试点", 30.0+float64(i)*0.001, 120.0); err != nil {
t.Fatalf("造地点失败: %v", err)
}
}
t.Cleanup(func() {
f.pg.ExecContext(f.ctx, `DELETE FROM places WHERE dataset_id=$1 AND src_pk BETWEEN 680000 AND 680099`, f.dsID)
})
countRows := func(url string) (rows, pageCount int) {
t.Helper()
rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", url, nil).WithContext(f.ctx)
f.srv.adminFillupPage(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("%s 渲染失败: %d", url, rec.Code)
}
body := rec.Body.String()
return strings.Count(body, `name="cat_`), strings.Count(body, `class="pager"`)
}
rows, pagers := countRows("/admin/fillup")
if rows != fillupPerPage {
t.Fatalf("第 1 页应有 %d 行,实际 %d", fillupPerPage, rows)
}
if pagers == 0 {
t.Error("多于 1 页时应出现分页控件")
}
if rows, _ := countRows("/admin/fillup?page=2"); rows == 0 || rows > fillupPerPage {
t.Fatalf("第 2 页行数异常: %d", rows)
}
// 越界页码要夹回最后一页,而不是给一页空白
if rows, _ := countRows("/admin/fillup?page=9999"); rows == 0 {
t.Error("越界页码应夹到最后一页,不该是空白页")
}
}