/** * Playback Reporting service tests * * TRACES: UR-005, UR-019, UR-025 | DR-028, DR-047 */ import { describe, it, expect, beforeEach, vi } from "vitest"; import { reportPlaybackStart, reportPlaybackProgress, reportPlaybackStopped, markAsPlayed, } from "./playbackReporting"; vi.mock("@tauri-apps/api/core", () => ({ invoke: vi.fn(async (command: string) => { if (command.startsWith("storage_")) { return undefined; } return null; }), })); vi.mock("$lib/stores/auth", () => ({ auth: { getUserId: vi.fn(() => "user-123"), getRepository: vi.fn(() => ({ reportPlaybackStopped: vi.fn(async () => undefined), getItem: vi.fn(async (id: string) => ({ id, name: "Test Item", runTimeTicks: 100000000, })), })), }, })); describe("playback reporting service", () => { beforeEach(() => { vi.clearAllMocks(); }); describe("reportPlaybackStart", () => { it("should accept itemId and positionSeconds", async () => { await expect(reportPlaybackStart("item-123", 0)).resolves.toBeUndefined(); }); it("should accept optional contextType and contextId", async () => { await expect( reportPlaybackStart("item-123", 0, "container", "container-456") ).resolves.toBeUndefined(); }); it("should convert seconds to ticks", async () => { const { invoke } = await import("@tauri-apps/api/core"); const invokeSpy = vi.mocked(invoke); await reportPlaybackStart("item-123", 60); const call = invokeSpy.mock.calls.find( (c) => c[0] === "storage_update_playback_context" ); expect(call).toBeDefined(); expect(call![1]).toHaveProperty("positionTicks", 600000000); // 60 seconds }); it("should use single context by default", async () => { const { invoke } = await import("@tauri-apps/api/core"); const invokeSpy = vi.mocked(invoke); await reportPlaybackStart("item-123", 30); const call = invokeSpy.mock.calls.find( (c) => c[0] === "storage_update_playback_context" ); expect(call![1]).toHaveProperty("contextType", "single"); expect(call![1]).toHaveProperty("contextId", null); }); it("should include userId in command", async () => { const { invoke } = await import("@tauri-apps/api/core"); const invokeSpy = vi.mocked(invoke); await reportPlaybackStart("item-123", 0); const call = invokeSpy.mock.calls.find( (c) => c[0] === "storage_update_playback_context" ); expect(call![1]).toHaveProperty("userId", "user-123"); }); }); describe("reportPlaybackProgress", () => { it("should accept itemId and positionSeconds", async () => { await expect(reportPlaybackProgress("item-123", 30)).resolves.toBeUndefined(); }); it("should accept optional isPaused parameter", async () => { await expect(reportPlaybackProgress("item-123", 30, true)).resolves.toBeUndefined(); }); it("should update local progress only", async () => { const { invoke } = await import("@tauri-apps/api/core"); const invokeSpy = vi.mocked(invoke); await reportPlaybackProgress("item-123", 30); const call = invokeSpy.mock.calls.find( (c) => c[0] === "storage_update_playback_progress" ); expect(call).toBeDefined(); expect(call![1]).toHaveProperty("itemId", "item-123"); }); it("should convert seconds to ticks", async () => { const { invoke } = await import("@tauri-apps/api/core"); const invokeSpy = vi.mocked(invoke); await reportPlaybackProgress("item-123", 45); const call = invokeSpy.mock.calls.find( (c) => c[0] === "storage_update_playback_progress" ); expect(call![1]).toHaveProperty("positionTicks", 450000000); // 45 seconds }); }); describe("reportPlaybackStopped", () => { it("should accept itemId and positionSeconds", async () => { await expect(reportPlaybackStopped("item-123", 120)).resolves.toBeUndefined(); }); it("should update local progress", async () => { const { invoke } = await import("@tauri-apps/api/core"); const invokeSpy = vi.mocked(invoke); await reportPlaybackStopped("item-123", 120); const call = invokeSpy.mock.calls.find( (c) => c[0] === "storage_update_playback_progress" ); expect(call).toBeDefined(); }); it("should report to server via repository", async () => { const { auth } = await import("$lib/stores/auth"); const authModule = vi.mocked(auth); const mockRepo = { reportPlaybackStopped: vi.fn(async () => undefined), }; authModule.getRepository = vi.fn(() => mockRepo as any); await reportPlaybackStopped("item-123", 120); expect(mockRepo.reportPlaybackStopped).toHaveBeenCalled(); }); it("should convert seconds to ticks for server report", async () => { const { auth } = await import("$lib/stores/auth"); const authModule = vi.mocked(auth); const mockRepo = { reportPlaybackStopped: vi.fn(async () => undefined), }; authModule.getRepository = vi.fn(() => mockRepo as any); await reportPlaybackStopped("item-123", 90); expect(mockRepo.reportPlaybackStopped).toHaveBeenCalledWith( "item-123", 900000000 // 90 seconds in ticks ); }); it("should not report to server if positionSeconds is 0", async () => { const { auth } = await import("$lib/stores/auth"); const authModule = vi.mocked(auth); const mockRepo = { reportPlaybackStopped: vi.fn(async () => undefined), }; authModule.getRepository = vi.fn(() => mockRepo as any); await reportPlaybackStopped("item-123", 0); expect(mockRepo.reportPlaybackStopped).not.toHaveBeenCalled(); }); }); describe("markAsPlayed", () => { it("should mark item as played", async () => { const { invoke } = await import("@tauri-apps/api/core"); const invokeSpy = vi.mocked(invoke); await markAsPlayed("item-123"); const call = invokeSpy.mock.calls.find( (c) => c[0] === "storage_mark_played" ); expect(call).toBeDefined(); expect(call![1]).toHaveProperty("itemId", "item-123"); }); it("should report to server with full duration", async () => { const { auth } = await import("$lib/stores/auth"); const authModule = vi.mocked(auth); const mockRepo = { reportPlaybackStopped: vi.fn(async () => undefined), getItem: vi.fn(async () => ({ id: "item-123", name: "Item", runTimeTicks: 100000000, })), }; authModule.getRepository = vi.fn(() => mockRepo as any); await markAsPlayed("item-123"); expect(mockRepo.getItem).toHaveBeenCalledWith("item-123"); expect(mockRepo.reportPlaybackStopped).toHaveBeenCalledWith( "item-123", 100000000 ); }); it("should handle items without runTimeTicks", async () => { const { auth } = await import("$lib/stores/auth"); const authModule = vi.mocked(auth); const mockRepo = { reportPlaybackStopped: vi.fn(async () => undefined), getItem: vi.fn(async () => ({ id: "item-123", name: "Item", runTimeTicks: null, })), }; authModule.getRepository = vi.fn(() => mockRepo as any); await markAsPlayed("item-123"); expect(mockRepo.reportPlaybackStopped).not.toHaveBeenCalled(); }); }); });