Skipping to the next episode left a mid-episode resume point behind, so the skipped episode reappeared in Continue Watching with a partial progress bar. Skipping means "done with this one", not "stopped here". - reportSkippedEpisode marks the outgoing episode played instead of reporting a stop position, and arms a one-shot suppression consumed by the player's stop handler, so VideoPlayer's post-navigation unmount stop report can't overwrite the 100% progress with the partial one. - Continue Watching drops resume entries superseded by Next Up: an in-progress episode whose series has a next-up entry strictly later in series order (season, then episode) is hidden from the Home and TV rows. Movies, series without a next-up entry, and items with unknown or mixed ordering are always kept. Adds UR-059, DR-088, DR-089. TRACES: UR-059 | DR-088, DR-089
89 lines
2.6 KiB
TypeScript
89 lines
2.6 KiB
TypeScript
/**
|
|
* Skip-to-next-episode reporting tests.
|
|
*
|
|
* Regression: pressing "skip to next episode" left the outgoing episode with a
|
|
* mid-episode resume position, so it showed a partial progress bar and offered
|
|
* to resume. A manual skip means the user is done with that episode — it must
|
|
* be recorded as fully watched.
|
|
*
|
|
* TRACES: UR-059, UR-025 | DR-088
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
|
|
const markAsPlayed = vi.fn(async (_itemId: string) => undefined);
|
|
const reportPlaybackStopped = vi.fn(
|
|
async (_itemId: string, _positionSeconds: number) => undefined
|
|
);
|
|
|
|
vi.mock("./playbackReporting", () => ({
|
|
markAsPlayed: (itemId: string) => markAsPlayed(itemId),
|
|
reportPlaybackStopped: (itemId: string, positionSeconds: number) =>
|
|
reportPlaybackStopped(itemId, positionSeconds),
|
|
}));
|
|
|
|
import {
|
|
shouldSuppressStopReport,
|
|
markSkipped,
|
|
reportSkippedEpisode,
|
|
resetSkipState,
|
|
} from "./skipReporting";
|
|
|
|
describe("skip reporting", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
resetSkipState();
|
|
});
|
|
|
|
describe("reportSkippedEpisode", () => {
|
|
it("marks the skipped episode as fully played", async () => {
|
|
await reportSkippedEpisode("ep-1");
|
|
|
|
expect(markAsPlayed).toHaveBeenCalledWith("ep-1");
|
|
});
|
|
|
|
it("does not stamp the mid-episode position as a resume point", async () => {
|
|
await reportSkippedEpisode("ep-1");
|
|
|
|
expect(reportPlaybackStopped).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("ignores a null item id", async () => {
|
|
await reportSkippedEpisode(null);
|
|
|
|
expect(markAsPlayed).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("shouldSuppressStopReport", () => {
|
|
it("suppresses the unmount stop report for the skipped episode", async () => {
|
|
await reportSkippedEpisode("ep-1");
|
|
|
|
// VideoPlayer.onDestroy fires after navigation with the mid-episode time.
|
|
expect(shouldSuppressStopReport("ep-1")).toBe(true);
|
|
});
|
|
|
|
it("only suppresses the episode that was actually skipped", async () => {
|
|
await reportSkippedEpisode("ep-1");
|
|
|
|
expect(shouldSuppressStopReport("ep-2")).toBe(false);
|
|
});
|
|
|
|
it("suppresses only once, so a later real stop still reports", async () => {
|
|
await reportSkippedEpisode("ep-1");
|
|
|
|
expect(shouldSuppressStopReport("ep-1")).toBe(true);
|
|
expect(shouldSuppressStopReport("ep-1")).toBe(false);
|
|
});
|
|
|
|
it("does not suppress when nothing was skipped", () => {
|
|
expect(shouldSuppressStopReport("ep-1")).toBe(false);
|
|
});
|
|
|
|
it("does not suppress a null item id", () => {
|
|
markSkipped("ep-1");
|
|
expect(shouldSuppressStopReport(null)).toBe(false);
|
|
});
|
|
});
|
|
});
|