First working POC
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
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("polling", () => {
|
||||
it.skip("should set isPolling to true when polling starts", async () => {
|
||||
const { sessions } = await import("./sessions");
|
||||
|
||||
// Mock the refresh to prevent actual API calls
|
||||
vi.spyOn(sessions, "refresh").mockResolvedValue();
|
||||
|
||||
// Note: startPolling is not yet implemented
|
||||
// sessions.startPolling(5000);
|
||||
// const state = get(sessions);
|
||||
|
||||
// expect(state.isPolling).toBe(true);
|
||||
});
|
||||
|
||||
it.skip("should set isPolling to false when polling stops", async () => {
|
||||
const { sessions } = await import("./sessions");
|
||||
|
||||
vi.spyOn(sessions, "refresh").mockResolvedValue();
|
||||
|
||||
// Note: startPolling/stopPolling are not yet implemented
|
||||
// sessions.startPolling(5000);
|
||||
// sessions.stopPolling();
|
||||
// const state = get(sessions);
|
||||
|
||||
// expect(state.isPolling).toBe(false);
|
||||
});
|
||||
|
||||
// Note: Cannot spy on internal refresh() function as it's not exported
|
||||
it.skip("should call refresh immediately when polling starts", async () => {
|
||||
const { sessions } = await import("./sessions");
|
||||
|
||||
const refreshSpy = vi.spyOn(sessions, "refresh").mockResolvedValue();
|
||||
|
||||
sessions.startPolling(5000);
|
||||
|
||||
expect(refreshSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// Note: Cannot spy on internal refresh() function as it's not exported
|
||||
it.skip("should call refresh at intervals", async () => {
|
||||
const { sessions } = await import("./sessions");
|
||||
|
||||
const refreshSpy = vi.spyOn(sessions, "refresh").mockResolvedValue();
|
||||
|
||||
sessions.startPolling(5000);
|
||||
|
||||
// Initial call
|
||||
expect(refreshSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Advance timers by 5 seconds
|
||||
await vi.advanceTimersByTime(5000);
|
||||
expect(refreshSpy).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Advance another 5 seconds
|
||||
await vi.advanceTimersByTime(5000);
|
||||
expect(refreshSpy).toHaveBeenCalledTimes(3);
|
||||
|
||||
sessions.stopPolling();
|
||||
});
|
||||
|
||||
// Note: Cannot spy on internal refresh() function as it's not exported
|
||||
it.skip("should stop previous polling when starting new polling", async () => {
|
||||
const { sessions } = await import("./sessions");
|
||||
|
||||
const refreshSpy = vi.spyOn(sessions, "refresh").mockResolvedValue();
|
||||
|
||||
sessions.startPolling(5000);
|
||||
await vi.advanceTimersByTime(5000);
|
||||
|
||||
const callsAfterFirst = refreshSpy.mock.calls.length;
|
||||
|
||||
// Start new polling - should stop the old one
|
||||
sessions.startPolling(3000);
|
||||
|
||||
// Advance by the old interval
|
||||
await vi.advanceTimersByTime(5000);
|
||||
|
||||
// Should have been called once for the new startPolling, and once after 3s
|
||||
expect(refreshSpy.mock.calls.length).toBeGreaterThan(callsAfterFirst);
|
||||
|
||||
sessions.stopPolling();
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user