Files
gacha-event-tracker/test/lens.test.ts
T
Lucas WintherandClaude Opus 5 d70720e2af feat(filter): focus one game at a time
Switching games on and off says which games you play, and is set once.
It's the wrong tool for what a player of four games does while reading —
clear one game, move to the next — which cost two taps per game and left
the settings panel no longer describing what they play.

Focus is a lens over that filter, not a second filter: a bar at the top,
above everything it narrows, with a "next game" control that steps
through and ends by returning to all. It never touches hiddenGames, and
a focus on a game since switched off is ignored rather than obeyed, so
it can't strand you on a blank page whose cause is elsewhere. Each chip
carries that game's outstanding count, so a game with nothing waiting
says so before you visit it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-16 20:42:42 +02:00

134 lines
4.2 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import {
advanceFocus,
countByGame,
firstToExpire,
outstanding,
resolveFocus,
} from "../src/client/state/lens.ts";
import type { GameId } from "../src/shared/schema.ts";
const row = (id: string, game: GameId, msRemaining: number | null) => ({
event: { id, game },
clock: { msRemaining },
});
const none = () => false;
describe("outstanding", () => {
const rows = [
row("a", "genshin", 1000),
row("b", "hsr", 2000),
row("c", "zzz", 3000),
];
test("drops what the reader has finished", () => {
// The headline and the dailies strip both tell the reader what to do, and
// pointing at a job they already ticked off is the app arguing with them.
expect(outstanding(rows, (id) => id === "b", none).map((r) => r.event.id)).toEqual(
["a", "c"],
);
});
test("drops what they have ignored", () => {
expect(outstanding(rows, none, (id) => id === "a").map((r) => r.event.id)).toEqual(
["b", "c"],
);
});
test("done and ignored at once is still just gone", () => {
expect(outstanding(rows, (id) => id === "a", (id) => id === "a")).toHaveLength(2);
});
test("nothing outstanding is an empty list, not a null", () => {
expect(outstanding(rows, () => true, none)).toEqual([]);
});
});
describe("firstToExpire", () => {
test("takes the soonest, not the first row", () => {
// The list arrives sorted by whatever mode the reader chose. Under "doing
// first" its head is what they are partway through, which is not what a
// panel headed "next to expire" claims to be showing.
const rows = [
row("mid-run", "genshin", 9 * 86_400_000),
row("tonight", "hsr", 3 * 3_600_000),
];
expect(firstToExpire(rows)?.event.id).toBe("tonight");
});
test("an unannounced end is never the deadline while a real one exists", () => {
const rows = [row("unknown", "zzz", null), row("real", "wuwa", 5000)];
expect(firstToExpire(rows)?.event.id).toBe("real");
});
test("falls back to an unknown end when it is all there is", () => {
// It is still a live event and still worth showing; it is just not a
// countdown. Showing nothing would be worse.
expect(firstToExpire([row("unknown", "zzz", null)])?.event.id).toBe("unknown");
});
test("no rows is null rather than a crash", () => {
expect(firstToExpire([])).toBeNull();
});
});
describe("resolveFocus", () => {
const enabled: GameId[] = ["genshin", "hsr"];
test("a focus on a game they still play stands", () => {
expect(resolveFocus("hsr", enabled)).toBe("hsr");
});
test("a focus on a game they switched off is ignored, not obeyed", () => {
// Obeying it leaves a blank page whose cause is a setting in a panel at the
// bottom. The stored value is left alone, so switching the game back on
// puts them back where they were.
expect(resolveFocus("zzz", enabled)).toBeNull();
});
test("no focus is all games", () => {
expect(resolveFocus(null, enabled)).toBeNull();
});
});
describe("advanceFocus", () => {
const enabled: GameId[] = ["genshin", "hsr", "zzz"];
test("all games leads into the first one", () => {
expect(advanceFocus(null, enabled)).toBe("genshin");
});
test("steps through in order", () => {
expect(advanceFocus("genshin", enabled)).toBe("hsr");
expect(advanceFocus("hsr", enabled)).toBe("zzz");
});
test("the last game leads back out to all of them", () => {
// Not a wrap to the first: a rotation with no exit means the only way back
// to everything is finding the "All" chip, which is the thing the rotation
// was meant to save them.
expect(advanceFocus("zzz", enabled)).toBeNull();
});
test("a focus that is no longer enabled restarts the rotation", () => {
expect(advanceFocus("wuwa", enabled)).toBe("genshin");
});
test("no games to rotate through", () => {
expect(advanceFocus(null, [])).toBeNull();
});
});
describe("countByGame", () => {
test("counts per game and omits games with nothing", () => {
const counts = countByGame([
row("a", "genshin", 1),
row("b", "genshin", 2),
row("c", "hsr", 3),
]);
expect(counts).toEqual({ genshin: 2, hsr: 1 });
expect(counts.zzz).toBeUndefined();
});
});