import { afterEach, beforeEach, Mock, vi } from 'vitest'; import { mock, MockProxy } from 'vitest-mock-extended'; // Type-safe Web Audio API mocks for tone tests. The real `GeneratedTone` // constructs an AudioContext directly; here we stub the global `AudioContext` // to return a deep-mocked instance whose `createOscillator()` / `createGain()` // factories return a *fresh* mock per call (not a shared one). This lets tests // assert against individual notes -- e.g. // `audio.oscillators[2].frequency.value` -- instead of having every call write // over the same observable state. interface AudioMocks { audioContext: MockProxy; audioContextCtor: Mock<[], MockProxy>; // Filled in the order `createOscillator()` / `createGain()` were called. oscillators: MockProxy[]; gains: MockProxy[]; // gainParams[i] is the AudioParam exposed by `gains[i].gain` -- kept as a // parallel array because `mock()` doesn't auto-populate the // AudioParam interface as a callable deep mock (we wire it up by hand). gainParams: MockProxy[]; } // Installs `AudioContext` mock on the global scope and resets it between tests; // restores real globals afterwards. Returns the live `audio` handle so tests // can read its fields after each `beforeEach` runs. // // Called once at module load. The `beforeEach`/`afterEach` calls inside this // helper register Vitest hooks at the file level -- Vitest picks them up just // as if they had been written at the top of the file -- so every test in the // file gets fresh mocks installed/torn down automatically. export const useAudioMocks = (): AudioMocks => { const audio = {} as AudioMocks; beforeEach(() => { audio.audioContext = mock(); audio.oscillators = []; audio.gains = []; audio.gainParams = []; vi.mocked(audio.audioContext.createOscillator).mockImplementation(() => { const oscillator = mock(); // `frequency` is a real AudioParam at runtime; the source assigns // `oscillator.frequency.value = freq` which must round-trip on read. Object.defineProperty(oscillator, 'frequency', { value: { value: 0 }, configurable: true, }); audio.oscillators.push(oscillator); return oscillator; }); vi.mocked(audio.audioContext.createGain).mockImplementation(() => { const gain = mock(); const gainParam = mock(); Object.defineProperty(gain, 'gain', { value: gainParam, configurable: true, }); audio.gains.push(gain); audio.gainParams.push(gainParam); return gain; }); // The source chains `.catch(...)` on the close() Promise. vi.mocked(audio.audioContext.close).mockResolvedValue(); // The base class reads `_currentTime` from this -- left as a deep-mock spy // by default it'd return a function, so anchor it at 0 for predictable // scheduling assertions. Object.defineProperty(audio.audioContext, 'currentTime', { value: 0, configurable: true, }); audio.audioContextCtor = vi.fn(() => audio.audioContext); vi.stubGlobal('AudioContext', audio.audioContextCtor); }); afterEach(() => { vi.unstubAllGlobals(); }); return audio; };