feat(routines): one game at a time in Catch up, with a way to reach it

Opening the panel gave one identical dropdown per game, naming none of them, at
the bottom of a page the Checklist did not link to. Named chips show one game's
titled rows and dated strips, and the Checklist's buttons land on the section.
The page-change scroll reset now skips these arrivals: a child's effect runs
first, so it was undoing the jump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-11-06 11:16:46 +01:00
1 parent 99af24e0d3
commit 3296784ca1
7 files changed
+493 -197

No files matched your search

+84
View File
@@ -280,3 +280,87 @@ test("pickAccount keeps a live selection, and falls back when it is gone", () =>
expect(pickAccount("gone", list)).toBe("b");
expect(pickAccount(null, list)).toBe("b");
});
// The section, as the Routines page draws it.
import { renderToStaticMarkup as render } from "react-dom/server";
import { RoutinesPage } from "../src/client/components/redesign/RoutinesPage.tsx";
import { DailyPanel } from "../src/client/components/redesign/DailyPanel.tsx";
const page = (state: ReaderState) =>
render(<RoutinesPage state={state} onAction={async () => ({ ok: true })} now={now} />);
function twoGames() {
let n = 0;
const state = migrateLegacy({}, ["genshin", "endfield"], at, () => `account:${++n}`);
state.prefs.gameOrder = ["genshin", "endfield"];
const byGame = (game: string) => Object.values(state.accounts).find((a) => a.game === game)!;
byGame("genshin").routines["dailies:genshin"] = pin;
byGame("endfield").routines[DAILY] = pin;
return { state, byGame };
}
const chips = (html: string) =>
[...html.matchAll(/class="focus-pill"[^>]*>([^<]+)/g)].map((m) => m[1]);
test("Routines has one Catch up heading and no nested dropdowns", () => {
const html = page(twoGames().state);
expect(html).not.toContain("recorded history");
expect(html.match(/<h2[^>]*>Catch up<\/h2>/g)).toHaveLength(1);
expect(html).not.toContain("routine-histor");
});
test("chips name the game and follow Game order", () => {
expect(chips(page(twoGames().state))).toEqual(["Genshin", "Endfield"]);
});
test("two servers of one game have distinct chips", () => {
const { state, byGame } = twoGames();
const first = byGame("endfield");
state.accounts["account:second"] = {
...first,
id: "account:second",
region: first.region === "asia" ? "europe" : "asia",
};
state.data["account:second"] = structuredClone(state.data[first.id]!);
const names = chips(page(state));
expect(new Set(names).size).toBe(names.length);
expect(names.filter((n) => n!.startsWith("Endfield"))).toHaveLength(2);
});
test("a day cell says weekday and date, and aria-pressed tracks the tick", () => {
const { state, byGame } = twoGames();
const a = byGame("genshin");
(state.data[a.id]!.daily[a.clocks[0]!.id] ??= {})["dailies:genshin"] = { days: ["2026-11-03"] };
const html = page(state);
expect(html).toMatch(/aria-label="Tue, 3 Nov"[^>]*aria-pressed="true"/);
expect(html).toMatch(/aria-label="Wed, 4 Nov"[^>]*aria-pressed="false"/);
});
test("the first game with something behind is chosen", () => {
const { state, byGame } = twoGames();
const a = byGame("endfield");
(state.data[a.id]!.daily[a.clocks[0]!.id] ??= {})[DAILY] = { days: ["2026-11-03"] };
const html = page(state);
expect(html).toContain("Daily missions");
expect(html).toMatch(/aria-pressed="true"[^>]*>Endfield/);
});
test("the Routines top bar offers Catch up", () => {
expect(page(twoGames().state)).toMatch(/class="btn"[^>]*>Catch up</);
});
test("Today's dailies offers Catch up beside Edit pins", () => {
const { state, byGame } = twoGames();
const a = byGame("endfield");
const rows = routineRows(state, a.id, now).filter((r) => r.id === DAILY);
const html = render(
<DailyPanel
rows={rows}
state={state}
now={now}
onAction={async () => ({ ok: true })}
onManage={() => {}}
onCatchUp={() => {}}
/>,
);
expect(html).toContain(">Catch up<");
expect(html).toContain(">Edit pins<");
});