Files
mangabaka-seeder/test/view-model.test.ts
T
Lucas WintherandClaude Opus 5.5 17ecebc4a4 feat(web): Obi Shelf design in a console layout, with a timeline view; record DESIGN.md
The user chose the direction over two mockup rounds (samples/mockups, gitignored):
the Obi Shelf's shelf and colours, Graphite Console side panels, the Annotated
Edition timeline as a second view, and warm off-colour grounds in both themes.
Selects look editable; labels are source-neutral. impeccable finish review: ship.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 01:38:00 +02:00

74 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, test } from "bun:test";
import type { ReviewCard } from "../src/core/session";
import { GRANULARITY_LABELS, cardFacts, defaultHost, hostChoices, seenBar, seenSpan, stageText, timelineGroups } from "../src/web/view-model";
import { inspected } from "./helpers/covers";
test("hosts: server first when it has room; hidden without storage; disabled when full", () => {
const ids = (c: ReturnType<typeof hostChoices>) => c.map((h) => `${h.id}${h.disabled ? "(disabled)" : ""}`);
expect(ids(hostChoices({ storage: true, storageFull: false }, {}, { catboxNeedsServer: true }))).toEqual(["server", "litterbox", "catbox", "direct"]);
expect(ids(hostChoices({ storage: false, storageFull: false }, {}, { catboxNeedsServer: true }))).toEqual(["litterbox", "catbox", "direct"]);
const full = hostChoices({ storage: true, storageFull: true }, {}, { catboxNeedsServer: true });
expect(ids(full)).toEqual(["server(disabled)", "litterbox", "catbox", "direct"]);
expect(defaultHost(full)).toBe("litterbox");
expect(ids(hostChoices(null, {}, { catboxNeedsServer: true }))).toEqual(["litterbox", "catbox(disabled)", "direct"]);
expect(ids(hostChoices(null, {}, { catboxNeedsServer: false }))).toEqual(["litterbox", "catbox", "direct"]);
});
test("litterbox carries the review-time warning and its chosen lifetime", () => {
const litterbox = hostChoices(null, { litterboxTime: "24h" }, { catboxNeedsServer: false })[0]!;
expect(litterbox.lifetime).toBe("24 hours");
expect(litterbox.warning).toMatch(/review can take longer/);
});
test("stage text with and without progress", () => {
expect(stageText("history")).toBe("Looking through the Wayback Machine…");
expect(stageText("history", 3, 24)).toBe("Looking through the Wayback Machine… 3/24");
});
test("granularity labels", () => {
expect(GRANULARITY_LABELS).toEqual({ 1: "every month", 3: "every 3 months", 6: "every 6 months", 12: "every year" });
});
test("card facts: size, shape, when seen, archive-only", () => {
const card = (cover: ReturnType<typeof inspected>): ReviewCard =>
({ cover, selected: true, type: cover.type, note: "", contentRating: "safe", hostedUrl: null, error: null });
expect(cardFacts(card(inspected({ bytes: new Uint8Array(352813) })))).toEqual(["480×623", "345 KB", "on the page now"]);
expect(cardFacts(card(inspected({
width: 720, height: 1230, shape: "tall", live: false, bytes: new Uint8Array(2048),
candidate: { current: false, seenAt: "20230401000000", lastSeenAt: "20260301000000" },
})))).toEqual(["720×1230", "2 KB", "landing-page art", "seen 2023-04 – 2026-03", "only on archive.org"]);
});
describe("seen ranges and the timeline", () => {
const now = new Date(Date.UTC(2026, 9, 15)); // 2026-10
const card = (candidate: NonNullable<Parameters<typeof inspected>[0]>["candidate"]): ReviewCard => {
const cover = inspected({ candidate });
return { cover, selected: true, type: cover.type, note: "", contentRating: "safe", hostedUrl: null, error: null };
};
const old = card({ current: false, seenAt: "20150312000000", lastSeenAt: "20150901000000" });
const mid = card({ current: false, seenAt: "20190301000000", lastSeenAt: "20220901000000" });
const live = card({ current: true });
const liveArchived = card({ current: true, seenAt: "20230401000000" });
test("the span runs from the earliest sighting to now", () => {
expect(seenSpan([live.cover, old.cover, mid.cover], now)).toEqual({ from: 2015 * 12 + 2, to: 2026 * 12 + 9 });
expect(seenSpan([live.cover], now)).toEqual({ from: 2026 * 12 + 9, to: 2026 * 12 + 9 });
});
test("bars are drawn to scale; current covers reach the right edge", () => {
const span = seenSpan([old.cover, live.cover], now);
const total = span.to - span.from + 1;
expect(seenBar(old.cover, span, now)).toEqual({ left: 0, width: (7 / total) * 100 });
const b = seenBar(liveArchived.cover, span, now);
expect(b.left + b.width).toBeCloseTo(100, 5);
expect(seenBar(live.cover, span, now).width).toBeGreaterThanOrEqual(1.5);
});
test("the timeline groups covers by the year they were last seen, newest first", () => {
const groups = timelineGroups([old, live, mid, liveArchived], now);
expect(groups.map((g) => g.year)).toEqual([2026, 2022, 2015]);
expect(groups[0]!.cards).toEqual([live, liveArchived]);
expect(groups[1]!.cards).toEqual([mid]);
});
});