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."); }); });