import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { NotificationType, NOTIFICATION_SOUNDS, type NotificationSound } from "./types"; // Mock HTMLAudioElement for soundPlayer tests class MockAudioElement { src: string = ""; preload: string = ""; volume: number = 1; constructor(src?: string) { if (src) this.src = src; } cloneNode(): MockAudioElement { const clone = new MockAudioElement(this.src); clone.volume = this.volume; return clone; } async play(): Promise { return Promise.resolve(); } } // Store original Audio before mocking const OriginalAudio = globalThis.Audio; describe("notifications", () => { describe("NotificationType enum", () => { it("has SUCCESS type", () => { expect(NotificationType.SUCCESS).toBe("success"); }); it("has ERROR type", () => { expect(NotificationType.ERROR).toBe("error"); }); it("has PERMISSION type", () => { expect(NotificationType.PERMISSION).toBe("permission"); }); it("has CONNECTION type", () => { expect(NotificationType.CONNECTION).toBe("connection"); }); it("has TASK_START type", () => { expect(NotificationType.TASK_START).toBe("task_start"); }); it("has ACHIEVEMENT type", () => { expect(NotificationType.ACHIEVEMENT).toBe("achievement"); }); it("has exactly 6 notification types", () => { const types = Object.values(NotificationType); expect(types.length).toBe(6); }); }); describe("NOTIFICATION_SOUNDS constant", () => { it("has sounds for all notification types", () => { Object.values(NotificationType).forEach((type) => { expect(NOTIFICATION_SOUNDS[type]).toBeDefined(); }); }); it("each sound has required properties", () => { Object.values(NOTIFICATION_SOUNDS).forEach((sound) => { expect(sound.type).toBeDefined(); expect(sound.filename).toBeDefined(); expect(sound.phrase).toBeDefined(); expect(typeof sound.filename).toBe("string"); expect(typeof sound.phrase).toBe("string"); expect(sound.filename.endsWith(".mp3")).toBe(true); }); }); it("SUCCESS sound has correct properties", () => { const sound = NOTIFICATION_SOUNDS[NotificationType.SUCCESS]; expect(sound.type).toBe(NotificationType.SUCCESS); expect(sound.filename).toBe("im-done.mp3"); expect(sound.phrase).toBe("I'm done!"); expect(sound.volume).toBe(0.7); }); it("ERROR sound has correct properties", () => { const sound = NOTIFICATION_SOUNDS[NotificationType.ERROR]; expect(sound.type).toBe(NotificationType.ERROR); expect(sound.filename).toBe("oh-no.mp3"); expect(sound.phrase).toBe("Oh no..."); expect(sound.volume).toBe(0.8); }); it("PERMISSION sound has correct properties", () => { const sound = NOTIFICATION_SOUNDS[NotificationType.PERMISSION]; expect(sound.type).toBe(NotificationType.PERMISSION); expect(sound.filename).toBe("access-please.mp3"); expect(sound.phrase).toBe("Access please!"); expect(sound.volume).toBe(0.9); }); it("CONNECTION sound has correct properties", () => { const sound = NOTIFICATION_SOUNDS[NotificationType.CONNECTION]; expect(sound.type).toBe(NotificationType.CONNECTION); expect(sound.filename).toBe("connected.mp3"); expect(sound.phrase).toBe("Connected!"); expect(sound.volume).toBe(0.7); }); it("TASK_START sound has correct properties", () => { const sound = NOTIFICATION_SOUNDS[NotificationType.TASK_START]; expect(sound.type).toBe(NotificationType.TASK_START); expect(sound.filename).toBe("working-on-it.mp3"); expect(sound.phrase).toBe("Working on it!"); expect(sound.volume).toBe(0.6); }); it("ACHIEVEMENT sound has correct properties", () => { const sound = NOTIFICATION_SOUNDS[NotificationType.ACHIEVEMENT]; expect(sound.type).toBe(NotificationType.ACHIEVEMENT); expect(sound.filename).toBe("achievement.mp3"); expect(sound.phrase).toBe("Achievement Get~!"); expect(sound.volume).toBe(0.8); }); it("all volumes are within valid range (0-1)", () => { Object.values(NOTIFICATION_SOUNDS).forEach((sound) => { if (sound.volume !== undefined) { expect(sound.volume).toBeGreaterThanOrEqual(0); expect(sound.volume).toBeLessThanOrEqual(1); } }); }); }); describe("NotificationSound interface", () => { it("can create a valid notification sound object", () => { const sound: NotificationSound = { type: NotificationType.SUCCESS, filename: "test-sound.mp3", phrase: "Test phrase", volume: 0.5, }; expect(sound.type).toBe(NotificationType.SUCCESS); expect(sound.filename).toBe("test-sound.mp3"); expect(sound.phrase).toBe("Test phrase"); expect(sound.volume).toBe(0.5); }); it("volume is optional", () => { const sound: NotificationSound = { type: NotificationType.ERROR, filename: "error.mp3", phrase: "Error occurred", }; expect(sound.volume).toBeUndefined(); }); }); describe("SoundPlayer class", () => { beforeEach(() => { // Mock Audio constructor globalThis.Audio = MockAudioElement as unknown as typeof Audio; }); afterEach(() => { // Restore original Audio globalThis.Audio = OriginalAudio; vi.resetModules(); }); it("can import soundPlayer singleton", async () => { const { soundPlayer } = await import("./soundPlayer"); expect(soundPlayer).toBeDefined(); }); it("setEnabled changes enabled state", async () => { const { soundPlayer } = await import("./soundPlayer"); soundPlayer.setEnabled(true); expect(soundPlayer.isEnabled()).toBe(true); soundPlayer.setEnabled(false); expect(soundPlayer.isEnabled()).toBe(false); }); it("starts disabled by default", async () => { // Need to reimport to get fresh instance behavior // But since it's a singleton, we just test the method const { soundPlayer } = await import("./soundPlayer"); // Reset to default state soundPlayer.setEnabled(false); expect(soundPlayer.isEnabled()).toBe(false); }); it("setGlobalVolume clamps values to 0-1 range", async () => { const { soundPlayer } = await import("./soundPlayer"); // Test that it doesn't throw on edge cases soundPlayer.setGlobalVolume(0); soundPlayer.setGlobalVolume(1); soundPlayer.setGlobalVolume(0.5); // Test clamping below 0 soundPlayer.setGlobalVolume(-0.5); // Test clamping above 1 soundPlayer.setGlobalVolume(1.5); }); it("play returns early when disabled", async () => { const { soundPlayer } = await import("./soundPlayer"); soundPlayer.setEnabled(false); // Should not throw when disabled await expect(soundPlayer.play(NotificationType.SUCCESS)).resolves.toBeUndefined(); }); it("play attempts to play when enabled", async () => { const { soundPlayer } = await import("./soundPlayer"); soundPlayer.setEnabled(true); // Should not throw await expect(soundPlayer.play(NotificationType.SUCCESS)).resolves.toBeUndefined(); }); }); describe("NotificationManager class", () => { beforeEach(() => { globalThis.Audio = MockAudioElement as unknown as typeof Audio; vi.resetModules(); }); afterEach(() => { globalThis.Audio = OriginalAudio; }); it("can import notificationManager singleton", async () => { vi.mock("@tauri-apps/api/core", () => ({ invoke: vi.fn().mockRejectedValue(new Error("Not available")), })); const { notificationManager } = await import("./notificationManager"); expect(notificationManager).toBeDefined(); }); it("has notifySuccess method", async () => { vi.mock("@tauri-apps/api/core", () => ({ invoke: vi.fn().mockRejectedValue(new Error("Not available")), })); const { notificationManager } = await import("./notificationManager"); expect(typeof notificationManager.notifySuccess).toBe("function"); }); it("has notifyError method", async () => { vi.mock("@tauri-apps/api/core", () => ({ invoke: vi.fn().mockRejectedValue(new Error("Not available")), })); const { notificationManager } = await import("./notificationManager"); expect(typeof notificationManager.notifyError).toBe("function"); }); it("has notifyPermission method", async () => { vi.mock("@tauri-apps/api/core", () => ({ invoke: vi.fn().mockRejectedValue(new Error("Not available")), })); const { notificationManager } = await import("./notificationManager"); expect(typeof notificationManager.notifyPermission).toBe("function"); }); it("has notifyConnection method", async () => { vi.mock("@tauri-apps/api/core", () => ({ invoke: vi.fn().mockRejectedValue(new Error("Not available")), })); const { notificationManager } = await import("./notificationManager"); expect(typeof notificationManager.notifyConnection).toBe("function"); }); it("has notifyTaskStart method", async () => { vi.mock("@tauri-apps/api/core", () => ({ invoke: vi.fn().mockRejectedValue(new Error("Not available")), })); const { notificationManager } = await import("./notificationManager"); expect(typeof notificationManager.notifyTaskStart).toBe("function"); }); it("has notify method", async () => { vi.mock("@tauri-apps/api/core", () => ({ invoke: vi.fn().mockRejectedValue(new Error("Not available")), })); const { notificationManager } = await import("./notificationManager"); expect(typeof notificationManager.notify).toBe("function"); }); }); describe("notification sounds file paths", () => { it("all sound files have valid paths", () => { Object.values(NOTIFICATION_SOUNDS).forEach((sound) => { // Check that filename doesn't contain path traversal expect(sound.filename).not.toContain(".."); expect(sound.filename).not.toContain("/"); expect(sound.filename).not.toContain("\\"); }); }); it("sound filenames are unique", () => { const filenames = Object.values(NOTIFICATION_SOUNDS).map((s) => s.filename); const uniqueFilenames = new Set(filenames); expect(uniqueFilenames.size).toBe(filenames.length); }); it("phrases are unique", () => { const phrases = Object.values(NOTIFICATION_SOUNDS).map((s) => s.phrase); const uniquePhrases = new Set(phrases); expect(uniquePhrases.size).toBe(phrases.length); }); }); });