From dd8d7577b37cc55ff0099de1a7337e8445d1652b Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Fri, 28 Aug 2026 05:43:04 +0200 Subject: [PATCH] Watch the pref reach the screen, not just the function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review pointed out that `dailyGroups` was well covered and the wiring to it was not: passing the wrong pref from App, binding the checkbox to a neighbour, or hardcoding `true` in Dailies each shipped with the suite green. Three of those are now caught by rendering the strip and the settings panel rather than the function behind them — verified by performing the mutations, not by reading the code. The fourth, App handing Dailies the wrong pref, is still uncovered: no test renders App, and closing it needs a feed and a click library this project does not have. Also pins the default. Flipping that one line silently empties Today's dailies for every existing reader, which is the whole argument of the comment above it, and nothing was watching — `defaults` is exported so the value and the upgrade path can both be asserted. And DATA-MODEL's enumeration of the prefs blob had gone stale; AGENTS.md names it as the doc to update when a stored key moves. Co-Authored-By: Claude Opus 5 (1M context) --- docs/DATA-MODEL.md | 5 ++-- src/client/state/usePrefs.ts | 2 +- test/controls.test.tsx | 12 ++++++++ test/prefs.test.ts | 19 ++++++++++++- test/views.test.tsx | 53 +++++++++++++++++++++++++++++++++++- 5 files changed, 86 insertions(+), 5 deletions(-) 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"); + }); +});