Files
jellytau/src/lib/stores/sessions.test.ts
dtourolle ad48d89dfe chore(format): run prettier over src/ and scripts/
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.
2026-08-21 17:41:44 +02:00

257 lines
7.6 KiB
TypeScript

// Tests for sessions store
// TRACES: UR-010 | DR-037
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { get } from "svelte/store";
import type { Session } from "$lib/api/types";
// Mock the auth store
vi.mock("./auth", () => ({
auth: {
getRepository: vi.fn(() => ({
sessions: {
getSessions: vi.fn().mockResolvedValue([]),
},
})),
},
}));
describe("sessions store", () => {
// Mock session data
const mockSession1: Session = {
id: "session-1",
userId: "user-1",
userName: "Test User",
client: "Jellyfin Web",
deviceName: "Chrome Browser",
deviceId: "device-1",
applicationVersion: "10.8.0",
isActive: true,
supportsMediaControl: true,
supportsRemoteControl: true,
playState: {
positionTicks: 1000000000,
canSeek: true,
isPaused: false,
isMuted: false,
volumeLevel: 75,
repeatMode: "RepeatNone",
shuffleMode: "Sorted",
},
nowPlayingItem: {
id: "item-1",
name: "Test Song",
Type: "Audio",
runTimeTicks: null,
album: null,
albumId: null,
albumArtist: null,
artists: null,
imageTags: null,
primaryImageTag: null,
albumPrimaryImageTag: null,
},
playableMediaTypes: ["Audio", "Video"],
supportedCommands: ["PlayPause", "Stop", "Seek", "NextTrack", "PreviousTrack"],
};
const mockSession2: Session = {
id: "session-2",
userId: "user-1",
userName: "Test User",
client: "Jellyfin Mobile",
deviceName: "iPhone",
deviceId: "device-2",
applicationVersion: "1.0.0",
isActive: true,
supportsMediaControl: false,
supportsRemoteControl: false,
playState: null,
nowPlayingItem: null,
playableMediaTypes: [],
supportedCommands: [],
};
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
// Ensure window.setInterval and window.clearInterval are available
if (typeof window !== "undefined") {
global.window = window as any;
}
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("initial state", () => {
it("should have empty sessions initially", async () => {
// Import dynamically to get fresh store instance
const { sessions } = await import("./sessions");
const state = get(sessions);
expect(state.sessions).toEqual([]);
expect(state.selectedSessionId).toBeNull();
expect(state.isLoading).toBe(false);
expect(state.error).toBeNull();
expect(state.lastUpdated).toBeNull();
});
});
describe("selectSession", () => {
it("should select a session by ID", async () => {
const { sessions } = await import("./sessions");
sessions.selectSession("session-123");
const state = get(sessions);
expect(state.selectedSessionId).toBe("session-123");
});
it("should allow deselecting by passing null", async () => {
const { sessions } = await import("./sessions");
sessions.selectSession("session-123");
sessions.selectSession(null);
const state = get(sessions);
expect(state.selectedSessionId).toBeNull();
});
});
describe("derived stores", () => {
it("activeSessions should filter sessions with nowPlayingItem", async () => {
// This test would require mocking the store's internal state
// For a real implementation, you'd need to:
// 1. Mock the auth.getRepository().sessions.getSessions() call
// 2. Call sessions.refresh()
// 3. Then check the derived store
// Placeholder test structure:
const { activeSessions } = await import("./sessions");
const active = get(activeSessions);
// Initially empty
expect(active).toEqual([]);
});
it("selectedSession should return the selected session or null", async () => {
const { selectedSession } = await import("./sessions");
const selected = get(selectedSession);
// Initially null
expect(selected).toBeNull();
});
it("controllableSessions should filter sessions with supportsRemoteControl", async () => {
const { controllableSessions } = await import("./sessions");
const controllable = get(controllableSessions);
// Initially empty
expect(controllable).toEqual([]);
});
});
describe("refresh", () => {
it("should expose a refresh method for manual session fetching", async () => {
const { sessions } = await import("./sessions");
// The store uses event-driven updates via Tauri events,
// but also exposes refresh() for manual/on-demand fetching
expect(typeof sessions.refresh).toBe("function");
});
});
describe("command methods", () => {
it("sendPlayPause should call API and refresh", async () => {
const { sessions } = await import("./sessions");
// These would need proper mocking of the auth.getRepository() chain
// For now, we're documenting the expected behavior
// Mock implementation would be:
// vi.spyOn(auth, 'getRepository').mockReturnValue({
// sessions: {
// sendCommand: vi.fn().mockResolvedValue(undefined)
// }
// });
// await sessions.sendPlayPause('session-123');
// expect(mockSendCommand).toHaveBeenCalledWith('session-123', 'PlayPause');
});
it("sendStop should call API and refresh", async () => {
const { sessions } = await import("./sessions");
// Similar structure to sendPlayPause test
// Would verify sendCommand is called with 'Stop'
});
it("sendNext should call API and refresh", async () => {
const { sessions } = await import("./sessions");
// Would verify sendNextTrack is called
});
it("sendPrevious should call API and refresh", async () => {
const { sessions } = await import("./sessions");
// Would verify sendPreviousTrack is called
});
it("sendSeek should call API without immediate refresh", async () => {
const { sessions } = await import("./sessions");
// Would verify seek is called but refresh is NOT called
// (to avoid UI lag during seeking)
});
it("sendVolume should call API without immediate refresh", async () => {
const { sessions } = await import("./sessions");
// Would verify setVolume is called but refresh is NOT called
// (to avoid UI lag during volume changes)
});
it("sendToggleMute should call API and refresh", async () => {
const { sessions } = await import("./sessions");
// Would verify toggleMute is called and refresh is called
});
it("playOnSession should call API and refresh", async () => {
const { sessions } = await import("./sessions");
// Would verify playOnSession is called with correct parameters
});
});
describe("error handling", () => {
it("should set error state when refresh fails", async () => {
const { sessions } = await import("./sessions");
// Mock auth.getRepository() to throw an error
// const error = new Error("Network error");
// Mock implementation would set up the error
// await sessions.refresh();
// const state = get(sessions);
// expect(state.error).toBe("Network error");
// expect(state.isLoading).toBe(false);
});
it("should log errors to console when commands fail", async () => {
const { sessions } = await import("./sessions");
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
// Mock a failing command
// await expect(sessions.sendPlayPause('session-1')).rejects.toThrow();
// expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
});
});