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]>
121 lines
3.6 KiB
TypeScript
121 lines
3.6 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { migrateLegacy } from "../src/client/state/reader/migration.ts";
|
|
import { ReaderStateSchema } from "../src/client/state/reader/schema.ts";
|
|
const at = "2026-10-16T12:00:00.000Z";
|
|
const raw = {
|
|
"gacha-tracker:v1:prefs": JSON.stringify({
|
|
region: "europe",
|
|
view: "timeline",
|
|
theme: "light",
|
|
hiddenGames: ["hsr"],
|
|
gameOrder: ["genshin", "hsr"],
|
|
showRoutines: true,
|
|
routinesHidden: ["routine:genshin:weekly-bosses"],
|
|
}),
|
|
"gacha-tracker:v1:progress": JSON.stringify({
|
|
"genshin:test:2026-10-01": { status: "done", at, goal: { future: true } },
|
|
"foreign-id": { note: "keep", at },
|
|
}),
|
|
"gacha-tracker:v1:daily": JSON.stringify({
|
|
"dailies:genshin": { days: ["2026-10-15"], at },
|
|
}),
|
|
"gacha-tracker:v1:tasks": JSON.stringify({
|
|
"mytask:abcdef": {
|
|
id: "mytask:abcdef",
|
|
game: "genshin",
|
|
title: "Mine",
|
|
period: { unit: "week", startsOn: 1 },
|
|
measure: { kind: "days", required: 1 },
|
|
at,
|
|
updatedAt: at,
|
|
future: 42,
|
|
},
|
|
}),
|
|
"gacha-tracker:v1:amounts": JSON.stringify({
|
|
"mytask:abcdef": { "2026-10-12": { value: 7, at } },
|
|
}),
|
|
};
|
|
test("migration preserves view, region, game choices and exact history keys", () => {
|
|
let n = 0;
|
|
const state = migrateLegacy(
|
|
raw,
|
|
["genshin", "hsr"],
|
|
at,
|
|
() => `account:test${++n}`,
|
|
);
|
|
expect(ReaderStateSchema.safeParse(state).success).toBe(true);
|
|
expect(state.prefs.view).toBe("timeline");
|
|
expect(state.prefs.hiddenGames).toEqual(["hsr"]);
|
|
const account = Object.values(state.accounts).find(
|
|
(a) => a.game === "genshin",
|
|
)!;
|
|
expect(account.region).toBe("europe");
|
|
const data = state.data[account.id]!;
|
|
expect(data.progress["genshin:test:2026-10-01"]).toEqual({
|
|
status: "done",
|
|
at,
|
|
goal: { future: true },
|
|
});
|
|
expect(data.daily[account.clocks[0]!.id]?.["dailies:genshin"]).toEqual({
|
|
days: ["2026-10-15"],
|
|
at,
|
|
});
|
|
expect(data.amounts[account.clocks[0]!.id]?.["mytask:abcdef"]).toEqual({
|
|
"2026-10-12": { value: 7, at },
|
|
});
|
|
expect(account.routines["routine:genshin:weekly-bosses"]?.followed).toBe(
|
|
false,
|
|
);
|
|
expect(state.tasks["mytask:abcdef"]).toHaveProperty("future", 42);
|
|
expect(raw["gacha-tracker:v1:progress"]).toContain("foreign-id");
|
|
});
|
|
test("completion-only installs and games absent from feed retain their state", () => {
|
|
const state = migrateLegacy(
|
|
{
|
|
"gacha-tracker:v1:completions": JSON.stringify({
|
|
"zzz:old:2026-01-01": { at },
|
|
}),
|
|
},
|
|
[],
|
|
at,
|
|
() => "account:old",
|
|
);
|
|
const account = Object.values(state.accounts)[0]!;
|
|
expect(account.game).toBe("zzz");
|
|
expect(state.data[account.id]?.progress["zzz:old:2026-01-01"]).toEqual({
|
|
status: "done",
|
|
at,
|
|
});
|
|
});
|
|
test("malformed blobs cannot hide valid state in other legacy keys", () => {
|
|
const state = migrateLegacy(
|
|
{ ...raw, "gacha-tracker:v1:tasks": "broken" },
|
|
["genshin"],
|
|
at,
|
|
() => "account:one",
|
|
);
|
|
expect(state.tasks).toEqual({});
|
|
expect(state.data["account:one"]?.progress).toHaveProperty(
|
|
"genshin:test:2026-10-01",
|
|
);
|
|
});
|
|
test("new catalogue games remain switched off for a returning reader", () => {
|
|
let n = 0;
|
|
const s = migrateLegacy(
|
|
{
|
|
"gacha-tracker:v1:prefs": JSON.stringify({
|
|
knownGames: ["genshin"],
|
|
hiddenGames: [],
|
|
onboarded: true,
|
|
}),
|
|
},
|
|
["genshin", "hsr"],
|
|
at,
|
|
() => `account:new${++n}`,
|
|
);
|
|
expect(s.prefs.hiddenGames).toEqual(["hsr"]);
|
|
expect(
|
|
Object.values(s.accounts).find((a) => a.game === "hsr")?.intent.events,
|
|
).toBe(false);
|
|
});
|