"In their own group" is the shape of a patch — what is on now, and what is queued behind it. "Mixed in" is the question a Gantt chart exists for: what runs out first, whether or not it has opened. On real data that is not a cosmetic difference — mixed, five events currently running sort below an Arknights rerun that has not started, because it closes before they do. Which is why it is a pair of pills and not a checkbox: neither answer is the absence of the other, and "mixed in" is a different order rather than the heading switched off. It sits under the row that puts the future on the board at all, and only while that row is ticked — a choice about arranging unstarted events is unanswerable with none of them on screen. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
130 lines
4.8 KiB
TypeScript
130 lines
4.8 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import { Controls } from "../src/client/components/Controls.tsx";
|
|
import { GameMetaProvider } from "../src/client/state/gameMeta.tsx";
|
|
import { metaFor } from "../src/shared/games.ts";
|
|
import type { Prefs } from "../src/client/state/usePrefs.ts";
|
|
|
|
/**
|
|
* The settings panel's view filters.
|
|
*
|
|
* Three rows answer the same question — *what am I allowed to look at?* — and
|
|
* they are the only place two of them can be reached from, so what they are
|
|
* bound to is worth pinning. A checkbox wired to the wrong preference is
|
|
* invisible in a diff and obvious only to the reader it happens to.
|
|
*/
|
|
|
|
const PREFS: Prefs = {
|
|
region: "europe",
|
|
hiddenGames: [],
|
|
focusGame: null,
|
|
sort: "ending",
|
|
view: "soon",
|
|
timelineDayWidth: 32,
|
|
timelineGroup: "game",
|
|
timelineUpcoming: false,
|
|
timelineSplitUpcoming: true,
|
|
detectDaily: false,
|
|
showCompleted: true,
|
|
showIgnored: false,
|
|
theme: "dark",
|
|
regionConfirmed: true,
|
|
onboarded: true,
|
|
};
|
|
|
|
function render(prefs: Prefs, ignoredCount = 0): string {
|
|
return renderToStaticMarkup(
|
|
<GameMetaProvider value={(id) => metaFor(id, {})}>
|
|
<Controls
|
|
games={["genshin", "hsr"]}
|
|
prefs={prefs}
|
|
onToggleGame={() => {}}
|
|
onUpdate={() => {}}
|
|
ignoredCount={ignoredCount}
|
|
onExport={() => {}}
|
|
onImport={() => {}}
|
|
own={{
|
|
games: {},
|
|
events: {},
|
|
lanes: ["genshin", "hsr"],
|
|
onAddGame: () => {},
|
|
onEditGame: () => {},
|
|
onRemoveGame: () => ({ removed: true, blockedBy: 0 }),
|
|
onAddEvent: () => {},
|
|
}}
|
|
/>
|
|
</GameMetaProvider>,
|
|
);
|
|
}
|
|
|
|
/** The nth checkbox's `checked` attribute, in document order. */
|
|
function checkboxes(html: string): boolean[] {
|
|
return [...html.matchAll(/<input type="checkbox"[^>]*>/g)].map((m) =>
|
|
m[0].includes('checked=""'),
|
|
);
|
|
}
|
|
|
|
describe("Controls: what am I allowed to look at", () => {
|
|
test("the unstarted-events switch is here, in the reader's words", () => {
|
|
// It used to be a pill in the board's own header, next to the stacking and
|
|
// scale controls. Those two reshape what is already on the board; this one
|
|
// decides what is on it at all, which is the question the two rows beside
|
|
// it answer.
|
|
const html = render(PREFS);
|
|
expect(html).toContain("Show events that haven't started");
|
|
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.
|
|
const html = render(PREFS);
|
|
expect(html).toContain("On the timeline");
|
|
expect(html).toContain("Not started yet");
|
|
});
|
|
|
|
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 }));
|
|
expect(off.filter(Boolean)).toHaveLength(1);
|
|
expect(on.filter(Boolean)).toHaveLength(2);
|
|
});
|
|
|
|
test("how unstarted events sit on the board is offered only when they are", () => {
|
|
// 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 });
|
|
expect(on).toContain("In their own group");
|
|
expect(on).toContain("Mixed in");
|
|
});
|
|
|
|
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 mixed = render({
|
|
...PREFS,
|
|
timelineUpcoming: true,
|
|
timelineSplitUpcoming: false,
|
|
});
|
|
const pressed = (html: string) =>
|
|
[...html.matchAll(/aria-pressed="true"[^>]*>([^<]+)</g)].map((m) => m[1]);
|
|
expect(pressed(split)).toContain("In their own group");
|
|
expect(pressed(mixed)).toContain("Mixed in");
|
|
// And the line under them describes the answer that is actually on.
|
|
expect(mixed).toContain("One deadline order");
|
|
expect(split).not.toContain("One deadline order");
|
|
});
|
|
|
|
test("the ignored row appears only once something is ignored", () => {
|
|
// Nothing to restore means nothing to offer — the row would be a filter
|
|
// over an empty set.
|
|
expect(render(PREFS)).not.toContain("I'm ignoring");
|
|
expect(render(PREFS, 3)).toContain("Show the 3 events I'm");
|
|
});
|
|
});
|