Formatting was configured but never enforced: `bun run format:check` reported 199 unformatted files and ran in no workflow and in no git hook, so .prettierrc (printWidth 100, trailing commas) described an intention rather than the tree. This is the one-time sweep that makes the check gateable. Whitespace and token-reflow only -- no behavioural change: `bun run check` reports 0 errors and all 1053 frontend tests pass before and after. Kept out of every other commit on purpose. A 199-file diff mixed with real changes is unreviewable, and the next commit turns format:check into a hard CI gate so this cannot silently accumulate again.
226 lines
7.2 KiB
TypeScript
226 lines
7.2 KiB
TypeScript
/**
|
|
* 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",
|
|
durationMs: 10000,
|
|
})),
|
|
})),
|
|
},
|
|
}));
|
|
|
|
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("positionMs", 60000); // 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("positionMs", 45000); // 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 milliseconds 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",
|
|
90000, // 90 seconds in ms
|
|
);
|
|
});
|
|
|
|
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",
|
|
durationMs: 10000,
|
|
})),
|
|
};
|
|
authModule.getRepository = vi.fn(() => mockRepo as any);
|
|
|
|
await markAsPlayed("item-123");
|
|
|
|
expect(mockRepo.getItem).toHaveBeenCalledWith("item-123");
|
|
expect(mockRepo.reportPlaybackStopped).toHaveBeenCalledWith("item-123", 10000);
|
|
});
|
|
|
|
it("should handle items without durationMs", 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",
|
|
durationMs: null,
|
|
})),
|
|
};
|
|
authModule.getRepository = vi.fn(() => mockRepo as any);
|
|
|
|
await markAsPlayed("item-123");
|
|
|
|
expect(mockRepo.reportPlaybackStopped).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|