import { describe, it, expect } from "vitest"; import type { MediaItem } from "$lib/api/types"; import { seasonAnchorId, seasonRedirectTarget, episodeFocusHref, seriesPlayHref, seriesPlayLabel, groupEpisodesBySeason, initialExpandedSeasons, } from "./seriesNavigation"; const SERIES = "series-1"; function ep(id: string, season: number | null, number: number | null): MediaItem { return { id, name: `S${season}E${number}`, kind: "episode", seriesId: SERIES, parentIndexNumber: season, indexNumber: number, durationMs: 1_000_000, } as unknown as MediaItem; } function seasonHeader(number: number, id = `season-${number}`): MediaItem { return { id, name: `Season ${number}`, kind: "season", seriesId: SERIES, indexNumber: number, } as unknown as MediaItem; } function withProgress(episode: MediaItem, fraction: number): MediaItem { return { ...episode, userData: { playbackPositionMs: (episode.durationMs ?? 0) * fraction }, } as MediaItem; } describe("seriesPlayHref", () => { // The reported bug: Play resolved the first *season* child and navigated to // /player/, which bounced back to the season-1 page. it("opens the current episode's focus view, never a season or the player", () => { const href = seriesPlayHref(SERIES, ep("s2e4", 2, 4)); expect(href).toBe("/library/series-1?episode=s2e4"); expect(href).not.toContain("/player/"); }); it("returns null for a series with no episodes so the button can hide", () => { expect(seriesPlayHref(SERIES, null)).toBeNull(); }); }); describe("seriesPlayLabel", () => { it("names the episode it will open", () => { expect(seriesPlayLabel(ep("s2e4", 2, 4))).toBe("Play S2E4"); }); it("says Resume for a part-watched episode", () => { expect(seriesPlayLabel(withProgress(ep("s2e4", 2, 4), 0.4))).toBe("Resume S2E4"); }); it("says Play for a barely-started or nearly-finished episode", () => { expect(seriesPlayLabel(withProgress(ep("s1e1", 1, 1), 0.001))).toBe("Play S1E1"); expect(seriesPlayLabel(withProgress(ep("s1e1", 1, 1), 0.99))).toBe("Play S1E1"); }); it("degrades to a bare verb when the numbering is unknown", () => { expect(seriesPlayLabel(ep("x", null, null))).toBe("Play"); expect(seriesPlayLabel(null)).toBe("Play"); }); }); describe("seasonRedirectTarget", () => { it("sends a season to its series, anchored at that season", () => { expect(seasonRedirectTarget(seasonHeader(3))).toBe("/library/series-1#season-3"); }); it("returns null when the series is unknown, so the caller can fall back", () => { const orphan = { ...seasonHeader(3), seriesId: undefined } as MediaItem; expect(seasonRedirectTarget(orphan)).toBeNull(); }); it("matches the anchor the season section renders", () => { expect(seasonRedirectTarget(seasonHeader(2))).toBe( `/library/${SERIES}#${seasonAnchorId(2)}` ); }); }); describe("episodeFocusHref", () => { it("opens an episode inside its series (never a bare episode page)", () => { expect(episodeFocusHref(ep("s1e2", 1, 2))).toBe("/library/series-1?episode=s1e2"); }); it("falls back to the bare item page when the series is unknown", () => { const orphan = { ...ep("lone", 1, 2), seriesId: undefined } as MediaItem; expect(episodeFocusHref(orphan)).toBe("/library/lone"); }); }); describe("groupEpisodesBySeason", () => { it("groups episodes under their season headers, in season order", () => { const seasons = [seasonHeader(2), seasonHeader(1)]; const episodes = [ep("s1e1", 1, 1), ep("s1e2", 1, 2), ep("s2e1", 2, 1)]; const grouped = groupEpisodesBySeason(seasons, episodes); expect(grouped.map((g) => g.season.indexNumber)).toEqual([1, 2]); expect(grouped[0].episodes.map((e) => e.id)).toEqual(["s1e1", "s1e2"]); expect(grouped[1].episodes.map((e) => e.id)).toEqual(["s2e1"]); }); it("puts specials after the numbered seasons", () => { const grouped = groupEpisodesBySeason( [seasonHeader(0), seasonHeader(1)], [ep("s0e1", 0, 1), ep("s1e1", 1, 1)] ); expect(grouped.map((g) => g.season.indexNumber)).toEqual([1, 0]); }); // A flat series: episodes hang off the series, no season folders exist. it("synthesizes headers when the server returned no seasons", () => { const grouped = groupEpisodesBySeason([], [ep("s1e1", 1, 1), ep("s2e1", 2, 1)]); expect(grouped.map((g) => g.season.name)).toEqual(["Season 1", "Season 2"]); expect(grouped.every((g) => g.season.kind === "season")).toBe(true); }); it("names a synthesized season 0 'Specials'", () => { const grouped = groupEpisodesBySeason([], [ep("s0e1", 0, 1)]); expect(grouped[0].season.name).toBe("Specials"); }); it("gives synthesized headers distinct ids so keyed #each blocks are stable", () => { const grouped = groupEpisodesBySeason([], [ep("s1e1", 1, 1), ep("s2e1", 2, 1)]); const ids = grouped.map((g) => g.season.id); expect(new Set(ids).size).toBe(ids.length); }); it("drops seasons that have no episodes", () => { const grouped = groupEpisodesBySeason( [seasonHeader(1), seasonHeader(2), seasonHeader(3)], [ep("s2e1", 2, 1)] ); expect(grouped.map((g) => g.season.indexNumber)).toEqual([2]); }); it("buckets season-less episodes into season 1 rather than losing them", () => { const grouped = groupEpisodesBySeason([], [ep("lone", null, 1)]); expect(grouped).toHaveLength(1); expect(grouped[0].episodes.map((e) => e.id)).toEqual(["lone"]); }); }); describe("initialExpandedSeasons", () => { const seasons = groupEpisodesBySeason( [seasonHeader(1), seasonHeader(2), seasonHeader(3)], [ ep("s1e1", 1, 1), ep("s2e1", 2, 1), ep("s2e2", 2, 2), ep("s3e1", 3, 1), ] ); it("expands only the season holding the current episode", () => { const expanded = initialExpandedSeasons(seasons, "s2e2"); expect([...expanded]).toEqual(["season-2"]); }); it("also expands the season of a ?episode= deep link", () => { const expanded = initialExpandedSeasons(seasons, "s1e1", "s3e1"); expect(expanded.has("season-1")).toBe(true); expect(expanded.has("season-3")).toBe(true); expect(expanded.has("season-2")).toBe(false); }); it("collapses nothing extra when current and focused share a season", () => { const expanded = initialExpandedSeasons(seasons, "s2e1", "s2e2"); expect([...expanded]).toEqual(["season-2"]); }); it("falls back to the first season when there is no current episode", () => { expect([...initialExpandedSeasons(seasons, null)]).toEqual(["season-1"]); }); it("falls back to the first season when the current episode is unknown here", () => { expect([...initialExpandedSeasons(seasons, "not-in-this-show")]).toEqual(["season-1"]); }); it("returns nothing for a series with no seasons", () => { expect(initialExpandedSeasons([], "s1e1").size).toBe(0); }); });