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.
257 lines
7.6 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|