The strip's Edit panel lists each game's chore, then its built-ins, then its events, each with Stop tracking, and Track · N offers everything a game could track and does not. Your own lists every built-in of the games that are on, untracked ones included and marked, so none is off with no way back. Renames Remove from dailies / Add an event to Stop tracking / Track. Co-Authored-By: Claude Opus 5.5 <[email protected]>
319 lines
12 KiB
TypeScript
319 lines
12 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import {
|
|
Dailies,
|
|
splitByCadence,
|
|
tasksShown,
|
|
type DailyItem,
|
|
} from "../src/client/components/Dailies.tsx";
|
|
import {
|
|
EditPanel,
|
|
laneGroups,
|
|
type StripEditor,
|
|
type StripEvent,
|
|
type StripStanding,
|
|
} from "../src/client/components/EditPanel.tsx";
|
|
import { GameMetaProvider } from "../src/client/state/gameMeta.tsx";
|
|
import { metaFor } from "../src/shared/games.ts";
|
|
import { goalSummary, type GoalStatus } from "../src/shared/goal.ts";
|
|
import type { Task } from "../src/shared/task.ts";
|
|
|
|
/**
|
|
* Editing the dailies strip from the strip itself.
|
|
*
|
|
* Setting up Genshin's “To Temper Thyself and Journey Far” — five days of seven,
|
|
* weeks from Monday — used to mean finding it in the list, opening its sheet,
|
|
* saying it repeats, then setting the goal. The strip's Edit panel does the
|
|
* same with the same handlers, and weekly and monthly tasks get a row of their
|
|
* own under the dailies that the reader can switch off.
|
|
*/
|
|
|
|
const NOW = Date.parse("2026-10-14T12:00:00.000Z");
|
|
|
|
function render(node: React.ReactElement): string {
|
|
return renderToStaticMarkup(
|
|
<GameMetaProvider value={(id) => metaFor(id, {})}>{node}</GameMetaProvider>,
|
|
);
|
|
}
|
|
|
|
const temper: StripEvent = {
|
|
id: "genshin:to-temper-thyself-and-journey-far:2026-08-10",
|
|
game: "genshin",
|
|
title: "To Temper Thyself and Journey Far",
|
|
goal: { period: "week", measure: "days", required: 5, weekStartsOn: 1 },
|
|
hasStoredGoal: true,
|
|
detected: false,
|
|
defaultWeekStartsOn: 1,
|
|
maxDays: 84,
|
|
};
|
|
const login: StripEvent = {
|
|
id: "hsr:gift-of-odyssey:2026-10-01",
|
|
game: "hsr",
|
|
title: "Gift of Odyssey",
|
|
goal: undefined,
|
|
hasStoredGoal: false,
|
|
detected: true,
|
|
defaultWeekStartsOn: 3,
|
|
maxDays: 7,
|
|
};
|
|
const abyss: StripEvent = {
|
|
id: "genshin:spiral-abyss:2026-10-01",
|
|
game: "genshin",
|
|
title: "Spiral Abyss",
|
|
goal: undefined,
|
|
hasStoredGoal: false,
|
|
detected: false,
|
|
defaultWeekStartsOn: 3,
|
|
maxDays: null,
|
|
};
|
|
|
|
function editor(over: Partial<StripEditor> = {}): StripEditor {
|
|
return {
|
|
inStrip: [temper, login],
|
|
addable: [abyss],
|
|
onGoal: () => {},
|
|
onDaily: () => {},
|
|
onOpen: () => {},
|
|
shown: { chores: true, weekly: true, monthly: true },
|
|
hasTasks: { weekly: true, monthly: false },
|
|
onShown: () => {},
|
|
standing: [],
|
|
onTrack: () => {},
|
|
...over,
|
|
};
|
|
}
|
|
|
|
describe("goalSummary", () => {
|
|
test("says what each goal asks for in a few words", () => {
|
|
expect(goalSummary(undefined)).toBe("No goal");
|
|
expect(goalSummary({ period: "week", measure: "days", required: 5, weekStartsOn: 1 })).toBe(
|
|
"5 days each week",
|
|
);
|
|
expect(goalSummary({ period: "week", measure: "days", required: 1, weekStartsOn: 1 })).toBe(
|
|
"1 day each week",
|
|
);
|
|
expect(goalSummary({ period: "event", measure: "days", required: 12 })).toBe(
|
|
"12 days across the event",
|
|
);
|
|
expect(
|
|
goalSummary({ period: "week", measure: "amount", target: 2100, weekStartsOn: 1 }),
|
|
).toBe("2,100 points each week");
|
|
expect(goalSummary({ period: "event", measure: "amount", target: 500 })).toBe(
|
|
"500 points across the event",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("laneGroups", () => {
|
|
test("the reader's lane order first, then stragglers, never re-sorted inside a lane", () => {
|
|
const zzz: StripEvent = { ...abyss, id: "z", game: "zzz", title: "Z" };
|
|
const groups = laneGroups(["hsr", "genshin"], [temper, zzz, login, abyss]);
|
|
expect(groups.map((g) => [g.game, g.events.map((e) => e.title)])).toEqual([
|
|
["hsr", ["Gift of Odyssey"]],
|
|
["genshin", ["To Temper Thyself and Journey Far", "Spiral Abyss"]],
|
|
["zzz", ["Z"]],
|
|
]);
|
|
});
|
|
|
|
test("a lane with nothing in it is dropped", () => {
|
|
expect(laneGroups(["hsr", "genshin"], [temper]).map((g) => g.game)).toEqual(["genshin"]);
|
|
});
|
|
});
|
|
|
|
describe("tasksShown", () => {
|
|
const AT = "2026-10-01T00:00:00.000Z";
|
|
const weekly: Task = {
|
|
id: "mytask:aaaaaa1111", game: "zzz", title: "Activity points",
|
|
period: { unit: "week", startsOn: 1 },
|
|
measure: { kind: "amount", target: 2100, unit: "points" }, at: AT, updatedAt: AT,
|
|
};
|
|
const monthly: Task = {
|
|
id: "mytask:bbbbbb2222", game: "zzz", title: "Shop reset",
|
|
period: { unit: "month", startsOn: 1 }, measure: { kind: "days", required: 1 },
|
|
at: AT, updatedAt: AT,
|
|
};
|
|
|
|
test("each switch hides its own period and nothing else", () => {
|
|
const all = [weekly, monthly];
|
|
expect(tasksShown(all, { weekly: true, monthly: true })).toEqual(all);
|
|
expect(tasksShown(all, { weekly: false, monthly: true })).toEqual([monthly]);
|
|
expect(tasksShown(all, { weekly: true, monthly: false })).toEqual([weekly]);
|
|
expect(tasksShown(all, { weekly: false, monthly: false })).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("splitByCadence", () => {
|
|
const item = (key: string, cadence: DailyItem["cadence"]): DailyItem => ({
|
|
key, game: "zzz", label: key, title: key, ariaLabel: key, today: "2026-10-14",
|
|
resetsIn: 0, notBefore: null, cadence,
|
|
});
|
|
|
|
test("today's things stay on top, weekly and monthly go under, order kept", () => {
|
|
const items = [item("chore", "day"), item("wk", "week"), item("ev", "event"),
|
|
item("mo", "month"), item("ev2", "day")];
|
|
const { today, longer } = splitByCadence(items);
|
|
expect(today.map((i) => i.key)).toEqual(["chore", "ev", "ev2"]);
|
|
expect(longer.map((i) => i.key)).toEqual(["wk", "mo"]);
|
|
});
|
|
});
|
|
|
|
describe("EditPanel", () => {
|
|
test("lists the strip's events by game, each with its goal in words", () => {
|
|
const html = render(<EditPanel games={["genshin", "hsr"]} editor={editor()} />);
|
|
expect(html).toContain("To Temper Thyself and Journey Far");
|
|
expect(html).toContain("5 days each week");
|
|
expect(html).toContain("Gift of Odyssey");
|
|
expect(html).toContain("No goal");
|
|
// Genshin's heading comes before Honkai's, as the reader ordered them.
|
|
expect(html.indexOf("Genshin Impact")).toBeLessThan(html.indexOf("Honkai: Star Rail"));
|
|
});
|
|
|
|
test("offers running events that are not in the strip yet, a game at a time", () => {
|
|
// Closed, a game says how many it could add and lists none of them: with
|
|
// every game on, the open list was a hundred pills deep.
|
|
const closed = render(<EditPanel games={["genshin", "hsr"]} editor={editor()} />);
|
|
expect(closed).toMatch(/Track · (<!-- -->)?1/);
|
|
expect(closed).not.toContain("Track Spiral Abyss");
|
|
const open = render(
|
|
<EditPanel games={["genshin", "hsr"]} editor={editor()} initialAdding="genshin" />,
|
|
);
|
|
expect(open).toMatch(/aria-label="Track Spiral Abyss"/);
|
|
});
|
|
|
|
test("a goal this version cannot read is said, not shown as none", () => {
|
|
const unread: StripEvent = { ...login, hasStoredGoal: true };
|
|
const html = render(
|
|
<EditPanel games={["hsr"]} editor={editor({ inStrip: [unread], addable: [] })} />,
|
|
);
|
|
expect(html).toContain("A goal this version can't read");
|
|
expect(html).not.toContain("No goal");
|
|
});
|
|
|
|
test("an open row carries the goal control, the sheet and the way out", () => {
|
|
const html = render(
|
|
<EditPanel games={["genshin"]} editor={editor({ inStrip: [temper], addable: [] })}
|
|
initialOpen={temper.id} />,
|
|
);
|
|
expect(html).toContain("Check-in goal");
|
|
expect(html).toContain("Open details");
|
|
// Same wording as the sheet, so the reader is told what goes with it.
|
|
expect(html).toContain("Stop tracking — removes the goal");
|
|
});
|
|
|
|
test("the switches are offered only for what exists", () => {
|
|
const html = render(<EditPanel games={["genshin"]} editor={editor()} />);
|
|
expect(html).toContain("Daily chores");
|
|
expect(html).toContain("Weekly tasks");
|
|
expect(html).not.toContain("Monthly tasks");
|
|
});
|
|
|
|
test("with nothing to add, it says where events come from", () => {
|
|
const html = render(
|
|
<EditPanel games={["genshin"]} editor={editor({ inStrip: [temper], addable: [] })} />,
|
|
);
|
|
expect(html).not.toContain('aria-label="Track');
|
|
expect(html).toContain("Everything running is tracked");
|
|
});
|
|
|
|
const chore: StripStanding = {
|
|
id: "dailies:genshin", game: "genshin", title: "Commissions, resin", kind: "chore", tracked: true,
|
|
};
|
|
const bosses: StripStanding = {
|
|
id: "routine:genshin:weekly-bosses", game: "genshin", title: "Weekly Bosses", kind: "routine", tracked: true,
|
|
};
|
|
const bargains: StripStanding = {
|
|
id: "routine:genshin:paimons-bargains", game: "genshin", title: "Paimon's Bargains", kind: "routine", tracked: false,
|
|
};
|
|
|
|
test("a game's chore and built-ins are listed with its events, each with Stop tracking", () => {
|
|
const html = render(
|
|
<EditPanel games={["genshin"]} editor={editor({ inStrip: [temper], addable: [], standing: [chore, bosses] })} />,
|
|
);
|
|
// Chore first, then built-ins, then the events — the order the strip draws.
|
|
const at = (t: string) => html.indexOf(t);
|
|
expect(at("Commissions, resin")).toBeLessThan(at("Weekly Bosses"));
|
|
expect(at("Weekly Bosses")).toBeLessThan(at("To Temper Thyself"));
|
|
expect(html).toContain('aria-label="Stop tracking Commissions, resin"');
|
|
expect(html).toContain('aria-label="Stop tracking Weekly Bosses"');
|
|
});
|
|
|
|
test("what a game could track and does not is counted under Track, events included", () => {
|
|
const ed = editor({ inStrip: [temper], addable: [abyss], standing: [{ ...chore, tracked: false }, bosses, bargains] });
|
|
const closed = render(<EditPanel games={["genshin"]} editor={ed} />);
|
|
expect(closed).toMatch(/Track · (<!-- -->)?3/);
|
|
expect(closed).not.toContain("Everything running is tracked");
|
|
const open = render(<EditPanel games={["genshin"]} editor={ed} initialAdding="genshin" />);
|
|
expect(open).toContain('aria-label="Track Commissions, resin"');
|
|
expect(open).toContain('aria-label="Track Paimon's Bargains"');
|
|
expect(open).toContain('aria-label="Track Spiral Abyss"');
|
|
expect(open).not.toContain('aria-label="Track Weekly Bosses"');
|
|
});
|
|
|
|
test("a game with only standing items still gets its group", () => {
|
|
const html = render(
|
|
<EditPanel games={["genshin", "hsr"]} editor={editor({ inStrip: [], addable: [], standing: [chore] })} />,
|
|
);
|
|
expect(html).toContain("Genshin Impact");
|
|
expect(html).not.toContain("Honkai: Star Rail");
|
|
});
|
|
});
|
|
|
|
describe("the strip with an editor", () => {
|
|
const AT = "2026-10-01T00:00:00.000Z";
|
|
const weekly: Task = {
|
|
id: "mytask:aaaaaa1111", game: "zzz", title: "Activity points",
|
|
period: { unit: "week", startsOn: 1 },
|
|
measure: { kind: "amount", target: 2100, unit: "points" }, at: AT, updatedAt: AT,
|
|
};
|
|
const status: GoalStatus = {
|
|
scope: "week", measure: "amount", periodStart: "2026-10-12", required: 2100, count: 0,
|
|
met: false, needed: 2100, open: 5, shortfall: false,
|
|
};
|
|
|
|
test("offers Edit beside Catch up", () => {
|
|
const html = render(
|
|
<Dailies games={["genshin"]} events={[]} region="europe" now={NOW} showChores
|
|
daysFor={() => []} onToggleDay={() => {}} edit={editor()} />,
|
|
);
|
|
expect(html).toContain("Catch up");
|
|
expect(html).toMatch(/>Edit</);
|
|
});
|
|
|
|
test("weekly and monthly tasks sit in their own row under today's", () => {
|
|
const html = render(
|
|
<Dailies games={["zzz"]} events={[]} region="europe" now={NOW} showChores
|
|
daysFor={() => []} onToggleDay={() => {}} tasks={[weekly]}
|
|
taskStatusFor={() => status} onSetAmount={() => {}} amountFor={() => undefined} />,
|
|
);
|
|
const heading = html.indexOf("This week & month");
|
|
expect(heading).toBeGreaterThan(-1);
|
|
expect(html.indexOf("ZZZ")).toBeLessThan(heading);
|
|
expect(html.indexOf("Activity points")).toBeGreaterThan(heading);
|
|
});
|
|
|
|
test("without weekly or monthly things there is no second row", () => {
|
|
const html = render(
|
|
<Dailies games={["zzz"]} events={[]} region="europe" now={NOW} showChores
|
|
daysFor={() => []} onToggleDay={() => {}} />,
|
|
);
|
|
expect(html).not.toContain("This week & month");
|
|
});
|
|
|
|
test("an empty strip still offers a way to add an event", () => {
|
|
const html = render(
|
|
<Dailies games={["genshin"]} events={[]} region="europe" now={NOW} showChores={false}
|
|
daysFor={() => []} onToggleDay={() => {}} edit={editor({ inStrip: [] })} />,
|
|
);
|
|
expect(html).toMatch(/>Edit</);
|
|
});
|
|
|
|
test("an empty strip with nothing to add stays away, as it did before", () => {
|
|
const html = render(
|
|
<Dailies games={["genshin"]} events={[]} region="europe" now={NOW} showChores={false}
|
|
daysFor={() => []} onToggleDay={() => {}}
|
|
edit={editor({ inStrip: [], addable: [], shown: { chores: false, weekly: true, monthly: true } })} />,
|
|
);
|
|
expect(html).toBe("");
|
|
});
|
|
});
|