feat(frontend): reading stats store (TDD) + session heartbeat hook (B0)

This commit is contained in:
2026-09-16 15:22:38 +08:00
parent 6aefe62a9f
commit 996f492083
4 changed files with 176 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
import { describe, expect, it } from "vitest";
import { createLocalStats, dayKey, recordInto, streakOf, weeklySum, type Buckets } from "../src/lib/readingStats";
describe("dayKey", () => {
it("本地时区 YYYY-MM-DD", () => {
expect(dayKey(new Date(2026, 0, 5))).toBe("2026-01-05");
expect(dayKey(new Date(2026, 10, 23))).toBe("2026-11-23");
});
});
describe("recordInto", () => {
it("不可变累加", () => {
const b: Buckets = { "2026-01-01": 60 };
const nb = recordInto(b, "2026-01-01", 30);
expect(nb["2026-01-01"]).toBe(90);
expect(b["2026-01-01"]).toBe(60); // 原桶不变
expect(recordInto(nb, "2026-01-02", 5)["2026-01-02"]).toBe(5);
expect(recordInto(nb, "2026-01-03", -5)).toEqual({ "2026-01-01": 90, "2026-01-03": 0 }); // 负值钳 0
});
});
describe("weeklySum", () => {
it("近 7 天含今天、缺日补 0、合计", () => {
const today = new Date(2026, 8, 16); // 2026-09-16(周三)
const b: Buckets = { "2026-09-16": 120, "2026-09-12": 300 };
const w = weeklySum(b, today);
expect(w.perDay).toHaveLength(7);
expect(w.perDay[6]).toEqual({ day: "2026-09-16", seconds: 120 });
expect(w.perDay[2]).toEqual({ day: "2026-09-12", seconds: 300 });
expect(w.perDay[0].seconds).toBe(0);
expect(w.total).toBe(420);
});
});
describe("streakOf", () => {
it("今天有记录 → 连续天数", () => {
const today = new Date(2026, 8, 16);
const b: Buckets = { "2026-09-16": 10, "2026-09-15": 10, "2026-09-14": 10 };
expect(streakOf(b, today)).toBe(3);
});
it("今天无但昨天有 → 连续天数(含昨天)", () => {
const today = new Date(2026, 8, 16);
const b: Buckets = { "2026-09-15": 10, "2026-09-14": 10 };
expect(streakOf(b, today)).toBe(2);
});
it("断档即止", () => {
const today = new Date(2026, 8, 16);
const b: Buckets = { "2026-09-15": 10, "2026-09-13": 10 };
expect(streakOf(b, today)).toBe(1);
});
it("空 → 0", () => {
const today = new Date(2026, 8, 16);
expect(streakOf({}, today)).toBe(0);
expect(streakOf({ "2026-09-14": 10 }, today)).toBe(0);
});
});
describe("createLocalStats", () => {
it("record→weekly/streak 走 localStorage(内存桩)", () => {
const mem: Record<string, string> = {};
globalThis.localStorage ??= {
getItem: (k: string) => (k in mem ? mem[k] : null),
setItem: (k: string, v: string) => (mem[k] = String(v)),
clear: () => Object.keys(mem).forEach((k) => delete mem[k]),
} as unknown as typeof localStorage;
globalThis.localStorage.clear();
const s = createLocalStats();
s.record(30);
s.record(20);
const w = s.weekly();
expect(w.perDay.at(-1)?.seconds).toBe(50);
expect(s.streak()).toBe(1);
s.record(0); // 无时间不落盘
expect(s.weekly().total).toBe(50);
});
});