Make it mine's copying was inline in App and untested; it is now a pure function with a test, so a change to either store can't make the copy silently arrive empty. The question's buttons now say what they answer when heard out of context. The prefs block and the spec now match what shipped. Co-Authored-By: Claude Opus 5.5 <[email protected]>
116 lines
4.7 KiB
TypeScript
116 lines
4.7 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import {
|
|
activeRoutines,
|
|
ROUTINES,
|
|
RoutineSchema,
|
|
routinesFor,
|
|
routineToAsk,
|
|
mineFrom,
|
|
copyRoutineState,
|
|
} from "../src/shared/routines.ts";
|
|
import { restorePrefsValue } from "../src/client/state/usePrefs.ts";
|
|
import { taskStatus } from "../src/shared/task.ts";
|
|
|
|
/**
|
|
* Weekly tasks the app ships for a game. Each is a claim about a game we did
|
|
* not write, so each must say where it came from and as of when — the bar
|
|
* `dailyTasks` in games.ts is held to.
|
|
*/
|
|
describe("ROUTINES", () => {
|
|
test("every one parses, and ids are unique", () => {
|
|
for (const r of ROUTINES) expect(RoutineSchema.safeParse(r).success).toBe(true);
|
|
expect(new Set(ROUTINES.map((r) => r.id)).size).toBe(ROUTINES.length);
|
|
});
|
|
|
|
test("every one names its game in its id, and cites an https source with a date", () => {
|
|
for (const r of ROUTINES) {
|
|
expect(r.id.split(":")[1]).toBe(r.game);
|
|
expect(r.source.startsWith("https://")).toBe(true);
|
|
expect(r.asOf).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
expect(r.checked).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
}
|
|
});
|
|
|
|
test("a routine without a source is refused", () => {
|
|
const { source: _s, ...bare } = ROUTINES[0]!;
|
|
expect(RoutineSchema.safeParse(bare).success).toBe(false);
|
|
});
|
|
|
|
test("the first two, as sourced", () => {
|
|
expect(routinesFor("zzz").map((r) => [r.title, r.measure])).toEqual([
|
|
["Ridu Weekly", { kind: "amount", target: 1300, unit: "points" }],
|
|
]);
|
|
expect(routinesFor("endfield").map((r) => [r.title, r.measure])).toEqual([
|
|
["Weekly Routine", { kind: "amount", target: 10, unit: "points" }],
|
|
]);
|
|
expect(routinesFor("genshin")).toEqual([]);
|
|
});
|
|
|
|
test("a routine has a status like any task", () => {
|
|
const s = taskStatus({
|
|
task: routinesFor("zzz")[0]!, region: "europe",
|
|
now: Date.parse("2026-10-15T12:00:00Z"), logged: [],
|
|
amounts: { "2026-10-12": { value: 900, at: "2026-10-15T12:00:00.000Z" } },
|
|
});
|
|
expect(s).toMatchObject({ scope: "week", count: 900, required: 1300 });
|
|
});
|
|
});
|
|
|
|
describe("which built-ins are on", () => {
|
|
test("only for games the reader said yes to", () => {
|
|
expect(activeRoutines(undefined, new Set()).map((r) => r.id)).toEqual([]);
|
|
expect(activeRoutines(["zzz"], new Set()).map((r) => r.id)).toEqual(["routine:zzz:ridu-weekly"]);
|
|
});
|
|
|
|
test("one the reader made their own is hidden while the copy exists", () => {
|
|
expect(activeRoutines(["zzz", "endfield"], new Set(["routine:zzz:ridu-weekly"])).map((r) => r.id))
|
|
.toEqual(["routine:endfield:weekly-routine"]);
|
|
});
|
|
});
|
|
|
|
describe("which game to ask about", () => {
|
|
test("the first game on screen with built-ins, not asked and not on", () => {
|
|
expect(routineToAsk(["genshin", "zzz", "endfield"], undefined, undefined)).toBe("zzz");
|
|
expect(routineToAsk(["genshin", "zzz", "endfield"], undefined, ["zzz"])).toBe("endfield");
|
|
expect(routineToAsk(["zzz"], ["zzz"], undefined)).toBeNull();
|
|
expect(routineToAsk(["genshin"], undefined, undefined)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("restoring prefs with the new fields", () => {
|
|
test("junk in the arrays is sanitised, and absent stays absent", () => {
|
|
const r = restorePrefsValue({ routineGames: ["zzz", 7], routinesAsked: "zzz" });
|
|
expect(r.routineGames).toEqual(["zzz"]);
|
|
expect(r.routinesAsked).toBeUndefined();
|
|
expect(restorePrefsValue({}).routineGames).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("making a built-in your own", () => {
|
|
test("copies its shape and remembers where it came from", () => {
|
|
const ridu = routinesFor("zzz")[0]!;
|
|
expect(mineFrom(ridu)).toEqual({
|
|
game: "zzz", title: "Ridu Weekly", period: { unit: "week", startsOn: 1 },
|
|
measure: { kind: "amount", target: 1300, unit: "points" }, from: "routine:zzz:ridu-weekly",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("copying a built-in's record to the reader's copy", () => {
|
|
const logs = { "routine:zzz:ridu-weekly": { days: ["2026-10-12"], at: "2026-10-12T10:00:00.000Z" } };
|
|
const amounts = { "routine:zzz:ridu-weekly": { "2026-10-12": { value: 900, at: "2026-10-13T10:00:00.000Z" } } };
|
|
|
|
test("ticks and numbers land under the new id; the original's are not touched", () => {
|
|
const copy = copyRoutineState(logs, amounts, "routine:zzz:ridu-weekly", "mytask:abcdef1234");
|
|
expect(copy.daily).toEqual({ "mytask:abcdef1234": logs["routine:zzz:ridu-weekly"] });
|
|
expect(copy.amounts).toEqual({ "mytask:abcdef1234": amounts["routine:zzz:ridu-weekly"] });
|
|
expect(Object.keys(copy.daily)).not.toContain("routine:zzz:ridu-weekly");
|
|
});
|
|
|
|
test("nothing recorded, nothing copied", () => {
|
|
expect(copyRoutineState({}, {}, "routine:zzz:ridu-weekly", "mytask:abcdef1234")).toEqual({
|
|
daily: {}, amounts: {},
|
|
});
|
|
});
|
|
});
|