Files
jellytau/src/lib/services/skipReporting.test.ts
T
dtourolle eb76c96e94 feat(player): skipping an episode marks it watched, not paused
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
2026-07-25 09:20:56 +02:00

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