jellytau/src/lib/stores/sessions.test.ts
Duncan Tourolle 3a9c126dfe
Some checks failed
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 14s
🏗️ Build and Test JellyTau / Build Android APK (push) Has been skipped
Traceability Validation / Check Requirement Traces (push) Failing after 1s
Fix warnings and update tracability
2026-02-28 20:54:25 +01:00

250 lines
7.4 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",
serverId: "server-1",
},
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();
});
});
});