Formatting was configured but never enforced: `bun run format:check` reported 199 unformatted files and ran in no workflow and in no git hook, so .prettierrc (printWidth 100, trailing commas) described an intention rather than the tree. This is the one-time sweep that makes the check gateable. Whitespace and token-reflow only -- no behavioural change: `bun run check` reports 0 errors and all 1053 frontend tests pass before and after. Kept out of every other commit on purpose. A 199-file diff mixed with real changes is unreviewable, and the next commit turns format:check into a hard CI gate so this cannot silently accumulate again.
171 lines
6.1 KiB
TypeScript
171 lines
6.1 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import type { MediaItem } from "$lib/api/types";
|
|
import {
|
|
isCurrentEpisode,
|
|
adjacentEpisodes,
|
|
compareSeriesOrder,
|
|
stripCardLabel,
|
|
} from "./episodeStrip";
|
|
|
|
// Minimal episode factory — only the fields the strip logic reads.
|
|
function ep(id: string, season: number | null, number: number | null): MediaItem {
|
|
return {
|
|
id,
|
|
name: `S${season}E${number}`,
|
|
kind: "episode",
|
|
parentIndexNumber: season,
|
|
indexNumber: number,
|
|
} as unknown as MediaItem;
|
|
}
|
|
|
|
function season(n: number, count: number): MediaItem[] {
|
|
return Array.from({ length: count }, (_, i) => ep(`s${n}e${i + 1}`, n, i + 1));
|
|
}
|
|
|
|
describe("isCurrentEpisode", () => {
|
|
const current = ep("abc", 1, 3);
|
|
|
|
it("matches by id", () => {
|
|
expect(isCurrentEpisode(ep("abc", 9, 9), current)).toBe(true);
|
|
});
|
|
|
|
it("matches by season+episode number when id differs", () => {
|
|
expect(isCurrentEpisode(ep("other", 1, 3), current)).toBe(true);
|
|
});
|
|
|
|
it("does not match a different episode number", () => {
|
|
expect(isCurrentEpisode(ep("other", 1, 4), current)).toBe(false);
|
|
});
|
|
|
|
it("does NOT treat two number-less episodes as the same (the reported bug)", () => {
|
|
const a = ep("a", null, null);
|
|
const b = ep("b", null, null);
|
|
expect(isCurrentEpisode(a, b)).toBe(false);
|
|
});
|
|
|
|
it("does not match when only one side has numbers", () => {
|
|
expect(isCurrentEpisode(ep("a", null, null), current)).toBe(false);
|
|
expect(isCurrentEpisode(ep("a", 1, 3), ep("b", null, null))).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("adjacentEpisodes", () => {
|
|
it("returns just the current episode when there are no others", () => {
|
|
const current = ep("only", 1, 1);
|
|
expect(adjacentEpisodes(current, [])).toEqual([current]);
|
|
});
|
|
|
|
it("returns siblings, not just the current episode", () => {
|
|
const eps = season(1, 8);
|
|
const current = eps[2]; // S1E3
|
|
const strip = adjacentEpisodes(current, eps);
|
|
expect(strip.length).toBeGreaterThan(1);
|
|
expect(strip).toContain(current);
|
|
});
|
|
|
|
it("windows to 3 before and 6 after the current episode", () => {
|
|
const eps = season(1, 20);
|
|
const current = eps[9]; // S1E10, index 9
|
|
const strip = adjacentEpisodes(current, eps);
|
|
// start = max(0, 9-3)=6 (E7), end = min(20, 9+7)=16 → E7..E16 (10 items)
|
|
expect(strip.map((e) => e.indexNumber)).toEqual([7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
|
|
expect(strip).toContain(current);
|
|
});
|
|
|
|
// ux-flows §5B.2, "Cross-season continuity": the window spans the whole
|
|
// series in episode order, so it runs past a season boundary rather than
|
|
// dead-ending at the end of a season.
|
|
it("runs past the end of a season into the next one", () => {
|
|
const eps = [...season(1, 5), ...season(2, 5)];
|
|
const current = eps[4]; // S1E5 — the season finale
|
|
const strip = adjacentEpisodes(current, eps);
|
|
expect(strip.map((e) => e.id)).toEqual([
|
|
"s1e2",
|
|
"s1e3",
|
|
"s1e4",
|
|
"s1e5",
|
|
"s2e1",
|
|
"s2e2",
|
|
"s2e3",
|
|
"s2e4",
|
|
"s2e5",
|
|
]);
|
|
});
|
|
|
|
it("reaches back into the previous season from a season opener", () => {
|
|
const eps = [...season(1, 5), ...season(2, 5)];
|
|
const current = eps[5]; // S2E1
|
|
const strip = adjacentEpisodes(current, eps);
|
|
expect(strip.slice(0, 3).map((e) => e.id)).toEqual(["s1e3", "s1e4", "s1e5"]);
|
|
expect(strip[3].id).toBe("s2e1");
|
|
});
|
|
|
|
it("orders by season then episode, never interleaving seasons", () => {
|
|
const eps = [...season(2, 3), ...season(1, 3)]; // deliberately out of order
|
|
const current = eps[3]; // S1E1
|
|
const strip = adjacentEpisodes(current, eps);
|
|
expect(strip.map((e) => e.id)).toEqual(["s1e1", "s1e2", "s1e3", "s2e1", "s2e2", "s2e3"]);
|
|
});
|
|
|
|
it("sorts specials (season 0) after the numbered seasons", () => {
|
|
const eps = [...season(0, 2), ...season(1, 2)];
|
|
const current = eps[2]; // S1E1
|
|
const strip = adjacentEpisodes(current, eps);
|
|
expect(strip.map((e) => e.id)).toEqual(["s1e1", "s1e2", "s0e1", "s0e2"]);
|
|
});
|
|
|
|
it("splices in a directly-fetched episode absent from the list (ID mismatch)", () => {
|
|
const eps = season(1, 5);
|
|
// Focused episode has a different id than any in the list but same numbers.
|
|
const current = ep("fetched-directly", 1, 3);
|
|
const strip = adjacentEpisodes(current, eps);
|
|
// It should appear once, anchored at its numeric position, alongside siblings.
|
|
expect(strip.filter((e) => e.indexNumber === 3).length).toBe(1);
|
|
expect(strip.length).toBeGreaterThan(1);
|
|
});
|
|
|
|
it("falls back to the full list when the current season is unknown", () => {
|
|
const eps = season(1, 5);
|
|
const current = ep("mystery", null, 3); // no season number
|
|
const strip = adjacentEpisodes(current, eps);
|
|
expect(strip.length).toBeGreaterThan(1);
|
|
// Anchored at its episode number, not dumped at one end of the list.
|
|
expect(strip.indexOf(current)).toBeGreaterThan(0);
|
|
expect(strip.indexOf(current)).toBeLessThan(strip.length - 1);
|
|
});
|
|
});
|
|
|
|
describe("compareSeriesOrder", () => {
|
|
it("orders by season, then episode", () => {
|
|
expect(compareSeriesOrder(ep("a", 1, 9), ep("b", 2, 1))).toBeLessThan(0);
|
|
expect(compareSeriesOrder(ep("a", 2, 1), ep("b", 2, 2))).toBeLessThan(0);
|
|
expect(compareSeriesOrder(ep("a", 2, 2), ep("b", 2, 2))).toBe(0);
|
|
});
|
|
|
|
it("puts specials last", () => {
|
|
expect(compareSeriesOrder(ep("a", 0, 1), ep("b", 1, 1))).toBeGreaterThan(0);
|
|
expect(compareSeriesOrder(ep("a", 0, 1), ep("b", 9, 1))).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("falls back to episode number when a season is unknown", () => {
|
|
expect(compareSeriesOrder(ep("a", null, 2), ep("b", 1, 5))).toBeLessThan(0);
|
|
});
|
|
});
|
|
|
|
describe("stripCardLabel", () => {
|
|
const current = ep("cur", 2, 4);
|
|
|
|
it("shows a bare episode number within the current season", () => {
|
|
expect(stripCardLabel(ep("a", 2, 6), current)).toBe("6.");
|
|
});
|
|
|
|
it("shows SxEy once the card crosses a season boundary", () => {
|
|
expect(stripCardLabel(ep("a", 3, 1), current)).toBe("S3E1");
|
|
expect(stripCardLabel(ep("a", 1, 8), current)).toBe("S1E8");
|
|
});
|
|
|
|
it("degrades to the episode number when the season is unknown", () => {
|
|
expect(stripCardLabel(ep("a", null, 7), current)).toBe("7.");
|
|
});
|
|
});
|