import { vi } from 'vitest'; /** * Подставной `Audio` для тестов саундборда: в jsdom `play()` не реализован, * поэтому звук подменяется целиком. Экземпляры собираются в массив — по ним * проверяют, что звук скачали, проиграли или остановили. */ export class FakeAudio { src: string; volume = 1; currentTime = 0; paused = true; playCalls = 0; pauseCalls = 0; /** Считаем и нативные, и «событийные» подписки: `ended` глушит звук. */ readonly listeners = new Map void>>(); constructor(src = '') { this.src = src; } addEventListener(type: string, handler: () => void): void { const set = this.listeners.get(type) ?? new Set<() => void>(); set.add(handler); this.listeners.set(type, set); } removeEventListener(type: string, handler: () => void): void { this.listeners.get(type)?.delete(handler); } play(): Promise { this.playCalls += 1; this.paused = false; return Promise.resolve(); } pause(): void { this.pauseCalls += 1; this.paused = true; } /** Сообщить о конце звука: движок снимает элемент с учёта. */ emit(type: string): void { for (const handler of [...(this.listeners.get(type) ?? [])]) { handler(); } } } /** Ставит подставной `Audio` и возвращает список созданных элементов. */ export function installFakeAudio(): FakeAudio[] { const instances: FakeAudio[] = []; class StubAudio extends FakeAudio { constructor(src = '') { super(src); instances.push(this); } } vi.stubGlobal('Audio', StubAudio); return instances; } /** * Подставной `AudioContext` для рингтона: jsdom не умеет Web Audio, поэтому * осцилляторы записываются, а не звучат. По ним видно, что сигнал запустили, * сколько раз и на каких частотах. */ export class FakeOscillator { type = 'sine'; frequency = { value: 0 }; startCalls = 0; stopCalls = 0; connect(): void { return undefined; } start(): void { this.startCalls += 1; } stop(): void { this.stopCalls += 1; } } export class FakeAudioContext { currentTime = 0; destination = {}; readonly oscillators: FakeOscillator[] = []; createOscillator(): FakeOscillator { const oscillator = new FakeOscillator(); this.oscillators.push(oscillator); return oscillator; } createGain(): { gain: Record void>; connect: () => void } { return { gain: { setValueAtTime: () => undefined, linearRampToValueAtTime: () => undefined }, connect: () => undefined, }; } } /** Ставит подставной `AudioContext` и возвращает последний созданный контекст. */ export function installFakeAudioContext(): { current: () => FakeAudioContext } { const created: FakeAudioContext[] = []; class StubAudioContext extends FakeAudioContext { constructor() { super(); created.push(this); } } vi.stubGlobal('AudioContext', StubAudioContext); return { current: () => { const last = created.at(-1); if (last === undefined) { throw new Error('AudioContext ещё не создавался'); } return last; }, }; }