jellytau/src/lib/stores/lmsSync.test.ts
Duncan Tourolle f1d25c4f4d Add support for fusing/unfusing JellyLMS zones into synchronized
multi-room groups, addressed by MAC (derived from the `lms-{mac}` device id).
2026-06-26 19:27:37 +02:00

92 lines
3.4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { get } from "svelte/store";
import type { Session } from "$lib/api/types";
const mockInvoke = vi.fn();
vi.mock("$lib/api/bindings", () => ({
commands: {
lmsGetSyncGroups: () => mockInvoke("lms_get_sync_groups"),
lmsCreateSyncGroup: (masterMac: string, slaveMacs: string[]) =>
mockInvoke("lms_create_sync_group", { masterMac, slaveMacs }),
lmsUnsyncPlayer: (mac: string) => mockInvoke("lms_unsync_player", { mac }),
lmsDissolveSyncGroup: (masterMac: string) =>
mockInvoke("lms_dissolve_sync_group", { masterMac }),
},
}));
import { lmsSync, isLmsSession, macForSession } from "./lmsSync";
function session(deviceId: string | null): Session {
return { id: "s", deviceId } as unknown as Session;
}
describe("LMS session detection", () => {
it("detects LMS zones by the lms- device-id prefix", () => {
expect(isLmsSession(session("lms-aa:bb:cc:dd:ee:ff"))).toBe(true);
expect(isLmsSession(session("chromecast-123"))).toBe(false);
expect(isLmsSession(session(null))).toBe(false);
expect(isLmsSession(null)).toBe(false);
});
it("recovers the MAC from an LMS session, null otherwise", () => {
expect(macForSession(session("lms-aa:bb:cc:dd:ee:ff"))).toBe("aa:bb:cc:dd:ee:ff");
expect(macForSession(session("web-xyz"))).toBeNull();
expect(macForSession(null)).toBeNull();
});
});
describe("lmsSync store", () => {
beforeEach(() => {
mockInvoke.mockReset();
lmsSync.reset();
});
it("fusing preserves existing slaves and adds the new zone", async () => {
// Initial group: master M with slave A.
mockInvoke.mockResolvedValueOnce([{ masterMac: "M", masterName: "M", slaveMacs: ["A"], slaveNames: [] }]);
await lmsSync.refresh();
// create returns void; refresh after returns the updated group.
mockInvoke.mockResolvedValueOnce(undefined);
mockInvoke.mockResolvedValueOnce([{ masterMac: "M", masterName: "M", slaveMacs: ["A", "B"], slaveNames: [] }]);
await lmsSync.fuseZone("M", "B");
expect(mockInvoke).toHaveBeenCalledWith("lms_create_sync_group", {
masterMac: "M",
slaveMacs: ["A", "B"],
});
expect(get(lmsSync).groups[0].slaveMacs).toEqual(["A", "B"]);
});
it("decoupling a slave unsyncs just that player", async () => {
mockInvoke.mockResolvedValueOnce([{ masterMac: "M", masterName: "M", slaveMacs: ["A"], slaveNames: [] }]);
await lmsSync.refresh();
mockInvoke.mockResolvedValueOnce(undefined); // unsync
mockInvoke.mockResolvedValueOnce([{ masterMac: "M", masterName: "M", slaveMacs: [], slaveNames: [] }]);
await lmsSync.decoupleZone("A");
expect(mockInvoke).toHaveBeenCalledWith("lms_unsync_player", { mac: "A" });
});
it("decoupling the master dissolves the whole group", async () => {
mockInvoke.mockResolvedValueOnce([{ masterMac: "M", masterName: "M", slaveMacs: ["A"], slaveNames: [] }]);
await lmsSync.refresh();
mockInvoke.mockResolvedValueOnce(undefined); // dissolve
mockInvoke.mockResolvedValueOnce([]);
await lmsSync.decoupleZone("M");
expect(mockInvoke).toHaveBeenCalledWith("lms_dissolve_sync_group", { masterMac: "M" });
});
it("treats a missing plugin (refresh error) as no groups, not fatal", async () => {
mockInvoke.mockRejectedValueOnce(new Error("404"));
await lmsSync.refresh();
expect(get(lmsSync).groups).toEqual([]);
});
});