diff --git a/docs/DATA-MODEL.md b/docs/DATA-MODEL.md index 9f92dbe..133fb06 100644 --- a/docs/DATA-MODEL.md +++ b/docs/DATA-MODEL.md @@ -220,8 +220,9 @@ Namespaced, versioned, and small. Nothing here ever goes to the server. "gacha-tracker:v1:prefs" // { region, hiddenGames[], knownGames[]?, gameOrder[]?, // focusGame, sort, view, // timelineDayWidth, timelineGroup, showUpcoming, - // timelineSplitUpcoming, detectDaily, showCompleted, - // showIgnored, theme, regionConfirmed, onboarded } + // timelineSplitUpcoming, detectDaily, showChores, + // showCompleted, showIgnored, theme, regionConfirmed, + // onboarded } // timelineDayWidth is px per day on the board, stored as the // measurement rather than a step number and read through // snapDayWidth — so a value from an older ladder still opens diff --git a/src/client/state/usePrefs.ts b/src/client/state/usePrefs.ts index ae9eb84..6a1aa5b 100644 --- a/src/client/state/usePrefs.ts +++ b/src/client/state/usePrefs.ts @@ -186,7 +186,7 @@ export interface Prefs { onboarded: boolean; } -function defaults(): Prefs { +export function defaults(): Prefs { return { region: guessRegion(), hiddenGames: [], diff --git a/test/controls.test.tsx b/test/controls.test.tsx index 89afa2a..6afc1fc 100644 --- a/test/controls.test.tsx +++ b/test/controls.test.tsx @@ -275,3 +275,15 @@ describe("Controls: the game order editor", () => { ); }); }); + +describe("the chores checkbox reports its own pref", () => { + test("off, one fewer box is ticked", () => { + // Counting alone can no longer separate the two always-on boxes from each + // other — with `showCompleted` also true, a checkbox bound to the wrong one + // of them keeps the same total. Flipping this pref specifically is what + // distinguishes them. + const on = checkboxes(render(PREFS)).filter(Boolean).length; + const off = checkboxes(render({ ...PREFS, showChores: false })).filter(Boolean).length; + expect(off).toBe(on - 1); + }); +}); diff --git a/test/prefs.test.ts b/test/prefs.test.ts index edd6588..31c1305 100644 --- a/test/prefs.test.ts +++ b/test/prefs.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { adoptNewLanes, adoptRenamed } from "../src/client/state/usePrefs.ts"; +import { adoptNewLanes, adoptRenamed, defaults } from "../src/client/state/usePrefs.ts"; import type { LaneId } from "../src/shared/custom.ts"; /** @@ -106,3 +106,20 @@ describe("adoptRenamed", () => { expect(adoptRenamed({})).toEqual({}); }); }); + +describe("defaults that a reader would notice losing", () => { + test("the chores are on until someone says otherwise", () => { + // Flipping this one line silently empties Today's dailies for every + // existing reader — it is the whole argument of the comment above it, and + // nothing else in the suite was watching it. + expect(defaults().showChores).toBe(true); + }); + + test("a stored blob without the key keeps the default", () => { + // The upgrade path. Prefs saved before this key existed must not read as + // "off" — `{...defaults(), ...stored}` is what guarantees that, and JSON + // cannot carry an `undefined` that would override it. + const stored = { region: "europe", detectDaily: false } as const; + expect({ ...defaults(), ...stored }.showChores).toBe(true); + }); +}); diff --git a/test/views.test.tsx b/test/views.test.tsx index 3b64798..a9ed28f 100644 --- a/test/views.test.tsx +++ b/test/views.test.tsx @@ -8,7 +8,7 @@ import { startMarkers, Timeline, } from "../src/client/components/Timeline.tsx"; -import { CatchUpPanel, dailyGroups } from "../src/client/components/Dailies.tsx"; +import { CatchUpPanel, Dailies, dailyGroups } from "../src/client/components/Dailies.tsx"; import { Welcome } from "../src/client/components/Welcome.tsx"; import { timelineLanes } from "../src/client/state/lanes.ts"; import { GameMetaProvider } from "../src/client/state/gameMeta.tsx"; @@ -1088,3 +1088,54 @@ describe("dailyGroups with the chores switched off", () => { ]); }); }); + +describe("the chores toggle reaches the screen", () => { + // `dailyGroups` is pure and well covered, but nothing tied `prefs.showChores` + // to what renders. Review proved four plausible breaks shipped green — + // including App passing the wrong pref, and Dailies hardcoding `true` — so + // these assert the rendered strip rather than the function behind it. + const NOW = Date.parse("2026-08-17T12:00:00.000Z"); + const repeating = { + id: "e1", + game: "genshin", + title: "Lantern Rite login", + type: "login", + summary: null, + startsAt: "2026-08-10T00:00:00.000Z", + startPrecision: "day", + endsAt: null, + endPrecision: "unknown", + regionScoped: false, + regionEnds: null, + sourceUrl: "https://example.test", + } as never; + + const strip = (showChores: boolean) => + render( + []} + onToggleDay={() => {}} + />, + ); + + test("on, the invented chore is on screen", () => { + const html = strip(true); + expect(html).toContain("Commissions, resin"); + expect(html).toContain("Lantern Rite login"); + }); + + test("off, the chore is gone and the event and its count are not", () => { + // The count is asserted too: it derives from the items, so a chore hidden + // from view but still counted would leave the reader chasing a tick that + // is not there. + const html = strip(false); + expect(html).not.toContain("Commissions, resin"); + expect(html).toContain("Lantern Rite login"); + expect(html).toContain("0/1"); + }); +});