test: add unit tests for notificationSound module
This commit is contained in:
155
src/lib/__tests__/notificationSound.test.ts
Normal file
155
src/lib/__tests__/notificationSound.test.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
/**
|
||||||
|
* @vitest-environment jsdom
|
||||||
|
*/
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock Web Audio API nodes
|
||||||
|
function createMockOscillator() {
|
||||||
|
return {
|
||||||
|
type: 'sine' as OscillatorType,
|
||||||
|
frequency: { value: 0 },
|
||||||
|
connect: vi.fn(),
|
||||||
|
start: vi.fn(),
|
||||||
|
stop: vi.fn(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMockGain() {
|
||||||
|
return {
|
||||||
|
gain: {
|
||||||
|
setValueAtTime: vi.fn(),
|
||||||
|
linearRampToValueAtTime: vi.fn(),
|
||||||
|
exponentialRampToValueAtTime: vi.fn(),
|
||||||
|
},
|
||||||
|
connect: vi.fn(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('playNotificationSound', () => {
|
||||||
|
let mockCtx: Record<string, unknown>;
|
||||||
|
let oscillators: ReturnType<typeof createMockOscillator>[];
|
||||||
|
let gains: ReturnType<typeof createMockGain>[];
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
let origAudioContext: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetModules();
|
||||||
|
|
||||||
|
oscillators = [];
|
||||||
|
gains = [];
|
||||||
|
|
||||||
|
mockCtx = {
|
||||||
|
currentTime: 0,
|
||||||
|
state: 'running',
|
||||||
|
destination: {},
|
||||||
|
resume: vi.fn(),
|
||||||
|
createOscillator: vi.fn(() => {
|
||||||
|
const osc = createMockOscillator();
|
||||||
|
oscillators.push(osc);
|
||||||
|
return osc;
|
||||||
|
}),
|
||||||
|
createGain: vi.fn(() => {
|
||||||
|
const gain = createMockGain();
|
||||||
|
gains.push(gain);
|
||||||
|
return gain;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
origAudioContext = (globalThis as any).AudioContext;
|
||||||
|
// Use a proper class so `new AudioContext()` works
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(globalThis as any).AudioContext = class {
|
||||||
|
constructor() {
|
||||||
|
return mockCtx;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (origAudioContext === undefined) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
delete (globalThis as any).AudioContext;
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(globalThis as any).AudioContext = origAudioContext;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getPlayFn() {
|
||||||
|
const mod = await import('../notificationSound');
|
||||||
|
return mod.playNotificationSound;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('creates two oscillators for the two-tone chime', async () => {
|
||||||
|
const play = await getPlayFn();
|
||||||
|
play();
|
||||||
|
expect(oscillators).toHaveLength(2);
|
||||||
|
expect(gains).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses correct frequencies (C5 and E5)', async () => {
|
||||||
|
const play = await getPlayFn();
|
||||||
|
play();
|
||||||
|
expect(oscillators[0].frequency.value).toBe(523.25);
|
||||||
|
expect(oscillators[1].frequency.value).toBe(659.25);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets oscillator type to sine', async () => {
|
||||||
|
const play = await getPlayFn();
|
||||||
|
play();
|
||||||
|
oscillators.forEach((osc) => {
|
||||||
|
expect(osc.type).toBe('sine');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('connects oscillators through gain nodes to destination', async () => {
|
||||||
|
const play = await getPlayFn();
|
||||||
|
play();
|
||||||
|
oscillators.forEach((osc, i) => {
|
||||||
|
expect(osc.connect).toHaveBeenCalledWith(gains[i]);
|
||||||
|
expect(gains[i].connect).toHaveBeenCalledWith(mockCtx.destination);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('starts and stops each oscillator', async () => {
|
||||||
|
const play = await getPlayFn();
|
||||||
|
play();
|
||||||
|
oscillators.forEach((osc) => {
|
||||||
|
expect(osc.start).toHaveBeenCalledTimes(1);
|
||||||
|
expect(osc.stop).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies gain envelope with the given volume', async () => {
|
||||||
|
const play = await getPlayFn();
|
||||||
|
play(0.5);
|
||||||
|
expect(gains[0].gain.linearRampToValueAtTime).toHaveBeenCalledWith(
|
||||||
|
0.5,
|
||||||
|
expect.any(Number),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses default volume of 0.3', async () => {
|
||||||
|
const play = await getPlayFn();
|
||||||
|
play();
|
||||||
|
expect(gains[0].gain.linearRampToValueAtTime).toHaveBeenCalledWith(
|
||||||
|
0.3,
|
||||||
|
expect.any(Number),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resumes suspended AudioContext', async () => {
|
||||||
|
mockCtx.state = 'suspended';
|
||||||
|
const play = await getPlayFn();
|
||||||
|
play();
|
||||||
|
expect(mockCtx.resume).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing when AudioContext is unavailable', async () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
delete (globalThis as any).AudioContext;
|
||||||
|
const play = await getPlayFn();
|
||||||
|
expect(() => play()).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user