- A press that begins while a field in the row is edited only ends the editing (rowOpen latches it at pointerdown); the card layout's row press is now tested. - A related video reset in the player forgets the session's stored position. - Removing the playing video passes over known-dead items and steps the way it went. - Prev/Next off only because of dead items stay focusable and say why (HU/EN). - The docked bar's scroller leaves room for focus rings. - A focus-opened About card is not closed by its own scroll into view. - EN "Clear watch progress" for the video reset, distinct from a page's "Reset view". - The signed-out title read goes to the page yt-dlp read; the retitle writes are conditional. - Tests: the cap of three errors, the bar's own Fullscreen, the group glyph, the backfill's reads.
97 lines
3.9 KiB
TypeScript
97 lines
3.9 KiB
TypeScript
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<string>();
|
|
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);
|
|
});
|
|
});
|