feat: "show events that haven't started" governs both views
It was the board's alone, sitting between two app-wide filters and carrying a line of small print to explain why it was different. It is the same events answering the same question in either view, so it is now one switch: the board plots them, and the checklist keeps its "Not started yet" section. That means the section is off by default, which is a visible change for every existing reader — deliberate, and the same argument the board makes. This app answers what expires next; on fourteen lanes the queued patches are more rows than the thing they came for. `timelineUpcoming` becomes `showUpcoming`, because the old name would now be false, and `adoptRenamed` carries a stored answer across on load. Nothing would be lost by dropping it — `prefs` is one blob under one key, not a key space — but a reader who had switched the future on would find it off with no explanation, and they should not have to say a thing twice. A stored new name always wins, so it cannot overwrite a fresher answer with a stale one. Gating the section alone would have opened a hole: nothing running and everything held back rendered an empty column, because the "nothing to show" line keyed off there being no rows at all rather than none listed. It now counts what is held back and names the switch, as the board does. The split pills stay the board's: the checklist gives these a section with a heading either way, so there is nothing there to mix them into. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d3066aefdc
commit
2cb4dd214c
+16
-10
@@ -22,7 +22,7 @@ const PREFS: Prefs = {
|
||||
view: "soon",
|
||||
timelineDayWidth: 32,
|
||||
timelineGroup: "game",
|
||||
timelineUpcoming: false,
|
||||
showUpcoming: false,
|
||||
timelineSplitUpcoming: true,
|
||||
detectDaily: false,
|
||||
showCompleted: true,
|
||||
@@ -75,20 +75,26 @@ describe("Controls: what am I allowed to look at", () => {
|
||||
expect(html).toContain("Show events I've finished");
|
||||
});
|
||||
|
||||
test("it says the board is what it applies to", () => {
|
||||
// Sitting between two app-wide filters, a row that named no scope would
|
||||
// read as a promise about the whole app — and the checklist lists these
|
||||
// whatever this says.
|
||||
test("it names both views, because it reaches both", () => {
|
||||
// It began as the board's alone. Sitting between two app-wide filters, a
|
||||
// row that still said "on the timeline" would understate what a tick does.
|
||||
const html = render(PREFS);
|
||||
expect(html).toContain("On the timeline");
|
||||
expect(html).toContain("Not started yet");
|
||||
expect(html).toContain("timeline");
|
||||
});
|
||||
|
||||
test("the split pills say they are the board's alone", () => {
|
||||
// Unlike the row above them, these really are one view — the checklist
|
||||
// splits unstarted events into a section of their own either way.
|
||||
const html = render({ ...PREFS, showUpcoming: true });
|
||||
expect(html).toContain("On the timeline.");
|
||||
});
|
||||
|
||||
test("it reads its own preference and not a neighbour's", () => {
|
||||
// Both neighbours are on and this one is off, so a checkbox bound to the
|
||||
// wrong key shows up as the wrong count of ticks.
|
||||
const off = checkboxes(render(PREFS));
|
||||
const on = checkboxes(render({ ...PREFS, timelineUpcoming: true }));
|
||||
const on = checkboxes(render({ ...PREFS, showUpcoming: true }));
|
||||
expect(off.filter(Boolean)).toHaveLength(1);
|
||||
expect(on.filter(Boolean)).toHaveLength(2);
|
||||
});
|
||||
@@ -97,7 +103,7 @@ describe("Controls: what am I allowed to look at", () => {
|
||||
// A choice about arranging them is unanswerable with none on the board,
|
||||
// and a control that changes nothing visible is worse than none.
|
||||
expect(render(PREFS)).not.toContain("Mixed in");
|
||||
const on = render({ ...PREFS, timelineUpcoming: true });
|
||||
const on = render({ ...PREFS, showUpcoming: true });
|
||||
expect(on).toContain("In their own group");
|
||||
expect(on).toContain("Mixed in");
|
||||
});
|
||||
@@ -105,10 +111,10 @@ describe("Controls: what am I allowed to look at", () => {
|
||||
test("it is a pair of answers, not one answer and its absence", () => {
|
||||
// "Mixed in" is a different order, not a heading switched off, so both
|
||||
// states name themselves and the panel says which is on.
|
||||
const split = render({ ...PREFS, timelineUpcoming: true });
|
||||
const split = render({ ...PREFS, showUpcoming: true });
|
||||
const mixed = render({
|
||||
...PREFS,
|
||||
timelineUpcoming: true,
|
||||
showUpcoming: true,
|
||||
timelineSplitUpcoming: false,
|
||||
});
|
||||
const pressed = (html: string) =>
|
||||
|
||||
+41
-1
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { adoptNewLanes } from "../src/client/state/usePrefs.ts";
|
||||
import { adoptNewLanes, adoptRenamed } from "../src/client/state/usePrefs.ts";
|
||||
import type { LaneId } from "../src/shared/custom.ts";
|
||||
|
||||
/**
|
||||
@@ -66,3 +66,43 @@ describe("adoptNewLanes", () => {
|
||||
expect(patch?.hiddenGames).toEqual(["hsr", "fgo"]);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* A preference that changed its name.
|
||||
*
|
||||
* `timelineUpcoming` governed the board alone; `showUpcoming` governs the
|
||||
* checklist too. Nothing is lost by dropping the old name — `prefs` is one blob
|
||||
* under one key — but a reader who had switched the future on would find it off
|
||||
* again with no explanation, which is the same failure as forgetting their view.
|
||||
*/
|
||||
describe("adoptRenamed", () => {
|
||||
test("a reader's old answer is carried across", () => {
|
||||
expect(adoptRenamed({ timelineUpcoming: true })).toEqual({
|
||||
showUpcoming: true,
|
||||
});
|
||||
// Both directions: having said no is also an answer.
|
||||
expect(adoptRenamed({ timelineUpcoming: false })).toEqual({
|
||||
showUpcoming: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("the old name never overwrites a fresher one", () => {
|
||||
// Once written back under the new name, the leftover must not undo it —
|
||||
// otherwise the setting would spring back on every load.
|
||||
expect(
|
||||
adoptRenamed({ timelineUpcoming: true, showUpcoming: false }),
|
||||
).toEqual({ showUpcoming: false });
|
||||
});
|
||||
|
||||
test("it is dropped rather than carried into the stored object", () => {
|
||||
// Kept, it would be written straight back and outlive the migration.
|
||||
expect(
|
||||
Object.keys(adoptRenamed({ timelineUpcoming: true, sort: "doing" })),
|
||||
).toEqual(["sort", "showUpcoming"]);
|
||||
});
|
||||
|
||||
test("a reader with neither is left alone", () => {
|
||||
expect(adoptRenamed({ sort: "doing" })).toEqual({ sort: "doing" });
|
||||
expect(adoptRenamed({})).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user