Skip to content
Source
Sign inSign up

threadNotifications.test.ts

104 lines3.0 KB

1import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
2​
3const SOUND_SECONDS = 0.6;
4​
5const audio = { currentTime: 0, started: 0 };
6​
7class FakeAudioContext {
8 state = "running";
9 destination = {};
10 get currentTime() {
11 return audio.currentTime;
12 }
13 resume() {
14 return Promise.resolve();
15 }
16 decodeAudioData() {
17 return Promise.resolve({ duration: SOUND_SECONDS });
18 }
19 createBufferSource() {
20 return {
21 buffer: null,
22 connect() {},
23 start() {
24 audio.started += 1;
25 },
26 };
27 }
29​
30async function loadUnlocked() {
31 const notifications = await import("./threadNotifications");
32 notifications.unlockNotificationAudio();
33 return notifications;
35​
36beforeEach(() => {
37 vi.resetModules();
38 audio.currentTime = 0;
39 audio.started = 0;
40 vi.stubGlobal("AudioContext", FakeAudioContext);
41 vi.stubGlobal(
42 "fetch",
43 vi.fn(async () => ({ arrayBuffer: async () => new ArrayBuffer(0) })),
44 );
45});
46​
47afterEach(() => {
48 vi.unstubAllGlobals();
49});
50​
51describe("playNotificationSound", () => {
52 it("preloads both sounds on unlock and reuses them for playback", async () => {
53 const { unlockNotificationAudio, playNotificationSound } = await loadUnlocked();
54 expect(fetch).toHaveBeenCalledTimes(2);
55 unlockNotificationAudio();
56 await playNotificationSound("completion", () => true);
57 await playNotificationSound("input", () => true);
58 expect(fetch).toHaveBeenCalledTimes(2);
59 expect(audio.started).toBe(2);
60 });
61​
62 it("retries failed preloads on an alert rather than every gesture", async () => {
63 vi.mocked(fetch).mockRejectedValue(new Error("offline"));
64 const { unlockNotificationAudio, playNotificationSound } = await loadUnlocked();
65 await playNotificationSound("completion", () => true);
66 expect(fetch).toHaveBeenCalledTimes(2);
67​
68 unlockNotificationAudio();
69 expect(fetch).toHaveBeenCalledTimes(2);
70​
71 vi.mocked(fetch).mockResolvedValue(new Response(new ArrayBuffer(0)));
72 await playNotificationSound("completion", () => true);
73 expect(fetch).toHaveBeenCalledTimes(3);
74 expect(audio.started).toBe(1);
75 });
76​
77 it("plays one sound for threads that complete together", async () => {
78 const { playNotificationSound } = await loadUnlocked();
79​
80 await Promise.all(
81 Array.from({ length: 4 }, () => playNotificationSound("completion", () => true)),
82 );
83​
84 expect(audio.started).toBe(1);
85 });
86​
87 it("still plays an input alert over a completion sound", async () => {
88 const { playNotificationSound } = await loadUnlocked();
89 await playNotificationSound("completion", () => true);
90​
91 await playNotificationSound("input", () => true);
92​
93 expect(audio.started).toBe(2);
94 });
95​
96 it("leaves the slot free when a sound is turned off before it plays", async () => {
97 const { playNotificationSound } = await loadUnlocked();
98 await playNotificationSound("completion", () => false);
99​
100 await playNotificationSound("completion", () => true);
101​
102 expect(audio.started).toBe(1);
103 });
104});