250 lines
7.4 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|