import { describe, it, expect } from "vitest"; import { afterRemoval, stepIndex } from "./playerQueue"; import type { Video } from "./api"; // "Delete" from inside the player (the owner asked for it: today the only way to drop a video from a // playlist is the Playlists module). The removal itself is the parent's — this is the half the player // owns, because only the player knows which item is playing: its launch queue is frozen at mount, so // nothing outside can take the item out for it. const v = (id: string): Video => ({ id }) as Video; const ids = (q: Video[]) => q.map((x) => x.id); describe("afterRemoval", () => { it("passes over a follower known dead, on the way it went", () => { const q = [v("a"), v("b"), v("c"), v("d")]; const next = afterRemoval(q, 1, "b", new Set(["c"])); expect(next).toMatchObject({ playingId: "d", way: 1 }); }); it("goes back over the dead ones when nothing playable followed", () => { const q = [v("a"), v("b"), v("c")]; expect(afterRemoval(q, 2, "c", new Set(["b"]))).toMatchObject({ playingId: "a", way: -1 }); // The last one going, nothing dead: the one before it, and that is a step back. expect(afterRemoval(q, 2, "c")).toMatchObject({ playingId: "b", way: -1 }); }); it("with none left that can play, takes the follower all the same", () => { const q = [v("a"), v("b"), v("c")]; expect(afterRemoval(q, 0, "a", new Set(["b", "c"]))).toMatchObject({ playingId: "b", way: 1, }); }); it("plays the item that followed the one you removed", () => { const q = [v("a"), v("b"), v("c")]; const next = afterRemoval(q, 1, "b"); expect(next).not.toBeNull(); expect(next!.playingId).toBe("c"); expect(ids(next!.queue)).toEqual(["a", "c"]); }); it("steps back to the new last item when the last one goes", () => { // There is no "next" after the end — falling off it would leave the player on nothing. const q = [v("a"), v("b"), v("c")]; const next = afterRemoval(q, 2, "c"); expect(next!.playingId).toBe("b"); expect(ids(next!.queue)).toEqual(["a", "b"]); }); it("drops the removed item from the queue, so prev/next cannot step back onto it", () => { const q = [v("a"), v("b"), v("c")]; expect(ids(afterRemoval(q, 0, "a")!.queue)).not.toContain("a"); }); it("says nothing is left when the list runs out (the player closes)", () => { expect(afterRemoval([v("only")], 0, "only")).toBeNull(); expect(afterRemoval([], 0, "gone")).toBeNull(); expect(afterRemoval(undefined, 0, "gone")).toBeNull(); }); it("survives an index that no longer points inside the shortened queue", () => { // The index comes from the frozen queue; nothing guarantees it is still in range afterwards. const q = [v("a"), v("b")]; const next = afterRemoval(q, 5, "b"); expect(next!.playingId).toBe("a"); }); it("is a no-op-shaped answer when the id is not in the queue at all", () => { const q = [v("a"), v("b")]; const next = afterRemoval(q, 0, "not-here"); expect(ids(next!.queue)).toEqual(["a", "b"]); expect(next!.playingId).toBe("a"); }); }); describe("stepIndex", () => { const q = [v("a"), v("b"), v("c"), v("d")]; const none = new Set(); it("steps to the neighbour that way", () => { expect(stepIndex(q, 1, 1, none)).toBe(2); expect(stepIndex(q, 1, -1, none)).toBe(0); }); it("passes over a video known dead", () => { expect(stepIndex(q, 3, -1, new Set(["c", "b"]))).toBe(0); expect(stepIndex(q, 0, 1, new Set(["b"]))).toBe(2); }); it("is -1 at the end, or when only dead videos are left that way", () => { expect(stepIndex(q, 3, 1, none)).toBe(-1); expect(stepIndex(q, 2, -1, new Set(["a", "b"]))).toBe(-1); }); it("wraps when asked, and never lands on where it started", () => { expect(stepIndex(q, 3, 1, none, true)).toBe(0); expect(stepIndex(q, 0, -1, new Set(["d"]), true)).toBe(2); expect(stepIndex(q, 1, 1, new Set(["a", "c", "d"]), true)).toBe(-1); }); });