The new reader app replaces the old UI: Checklist with a rail (focus pills, Next to expire, today's dailies, pinned routines), Timeline in a new and a classic look, Routines, Archive and a Settings area, in a Structured (default) and a Relaxed interface. It follows the approved Round 6 B and Round 7 mockups, corrected screen by screen with the owner. Reader data moves to a v2 store (gacha-tracker:v2:*). Existing v1 data is migrated on first load, a recovery copy is written first, and the v1 keys are never removed, so a rollback finds them unchanged. Redeem codes for Genshin arrive as their own feed. Also: live's urgency ramp, editable per theme; completion and section-complete fireworks; a returning-user tour; the design mockups, specs, plans, reviews and handoff record. Co-Authored-By: Claude Opus 5.5 <[email protected]>
62 lines
2.3 KiB
TypeScript
62 lines
2.3 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { mintAccountId } from "../src/client/state/reader/ids.ts";
|
|
import { actionMeta } from "../src/client/state/reader/actions.ts";
|
|
import { initializeReader } from "../src/client/state/reader/store.ts";
|
|
import { saveBackup, readBackups } from "../src/client/state/reader/backups.ts";
|
|
import { migrateLegacy } from "../src/client/state/reader/migration.ts";
|
|
import { READER_KEYS } from "../src/client/state/reader/schema.ts";
|
|
import { MemoryStorage } from "./helpers/reader.ts";
|
|
|
|
const at = "2026-10-16T12:00:00.000Z";
|
|
function withoutRandomUUID(run: () => void) {
|
|
const before = Object.getOwnPropertyDescriptor(crypto, "randomUUID");
|
|
Object.defineProperty(crypto, "randomUUID", {
|
|
configurable: true,
|
|
value: undefined,
|
|
});
|
|
try {
|
|
run();
|
|
} finally {
|
|
if (before) Object.defineProperty(crypto, "randomUUID", before);
|
|
else Reflect.deleteProperty(crypto, "randomUUID");
|
|
}
|
|
}
|
|
|
|
test("HTTP first-visit migration succeeds without randomUUID and retains legacy bytes", () => {
|
|
withoutRandomUUID(() => {
|
|
const storage = new MemoryStorage();
|
|
const raw = JSON.stringify({
|
|
"genshin:example:2026-10-01": { note: "Keep my progress", at },
|
|
});
|
|
storage.setItem("gacha-tracker:v1:progress", raw);
|
|
const result = initializeReader(storage, ["genshin"], at, mintAccountId);
|
|
expect(result.ok).toBe(true);
|
|
expect(storage.getItem("gacha-tracker:v1:progress")).toBe(raw);
|
|
expect(storage.getItem(READER_KEYS.reader)).not.toBeNull();
|
|
if (result.ok)
|
|
expect(
|
|
Object.values(result.state.data)[0]!.progress[
|
|
"genshin:example:2026-10-01"
|
|
],
|
|
).toHaveProperty("note", "Keep my progress");
|
|
});
|
|
});
|
|
|
|
test("HTTP edits receive distinct operation IDs without randomUUID", () => {
|
|
withoutRandomUUID(() => {
|
|
const first = actionMeta(),
|
|
second = actionMeta();
|
|
expect(first.opId).toMatch(/^[a-zA-Z0-9-]+$/);
|
|
expect(first.opId).not.toBe(second.opId);
|
|
});
|
|
});
|
|
|
|
test("HTTP local backups save normally without randomUUID", () => {
|
|
withoutRandomUUID(() => {
|
|
const storage = new MemoryStorage();
|
|
const state = migrateLegacy({}, ["genshin"], at, () => "account:one");
|
|
expect(saveBackup(storage, state, at)).toEqual({ ok: true });
|
|
expect(readBackups(storage)).toHaveLength(1);
|
|
});
|
|
});
|