import { AudioPlayerManager } from '../audio.player.manager' type AudioCallback = ((event: string) => void) | null type AudioPlayerCtorArgs = [ string, boolean, string | undefined, string | null | undefined, string | undefined, AudioCallback, ] type MockAudioPlayerInstance = { setCallback: ReturnType pauseAudio: ReturnType resetMsgId: ReturnType cacheBuffers: Array sourceBuffer: { abort: ReturnType } | undefined } const mockState = vi.hoisted(() => ({ instances: [] as MockAudioPlayerInstance[], })) const mockAudioPlayerConstructor = vi.hoisted(() => vi.fn()) const MockAudioPlayer = vi.hoisted(() => { return class MockAudioPlayerClass { setCallback = vi.fn() pauseAudio = vi.fn() resetMsgId = vi.fn() cacheBuffers = [new ArrayBuffer(1)] sourceBuffer = { abort: vi.fn() } constructor(...args: AudioPlayerCtorArgs) { mockAudioPlayerConstructor(...args) mockState.instances.push(this as unknown as MockAudioPlayerInstance) } } }) vi.mock('@/app/components/base/audio-btn/audio', () => ({ default: MockAudioPlayer, })) describe('AudioPlayerManager', () => { beforeEach(() => { vi.clearAllMocks() mockState.instances = [] Reflect.set(AudioPlayerManager, 'instance', undefined) }) describe('getInstance', () => { it('should return the same singleton instance across calls', () => { const first = AudioPlayerManager.getInstance() const second = AudioPlayerManager.getInstance() expect(first).toBe(second) }) }) describe('getAudioPlayer', () => { it('should create a new audio player when no existing player is cached', () => { const manager = AudioPlayerManager.getInstance() const callback = vi.fn() const result = manager.getAudioPlayer('/text-to-audio', false, 'msg-1', 'hello', 'en-US', callback) expect(mockAudioPlayerConstructor).toHaveBeenCalledTimes(1) expect(mockAudioPlayerConstructor).toHaveBeenCalledWith( '/text-to-audio', false, 'msg-1', 'hello', 'en-US', callback, ) expect(result).toBe(mockState.instances[0]) }) it('should reuse existing player and update callback when msg id is unchanged', () => { const manager = AudioPlayerManager.getInstance() const firstCallback = vi.fn() const secondCallback = vi.fn() const first = manager.getAudioPlayer('/text-to-audio', false, 'msg-1', 'hello', 'en-US', firstCallback) const second = manager.getAudioPlayer('/ignored', true, 'msg-1', 'ignored', 'fr-FR', secondCallback) expect(mockAudioPlayerConstructor).toHaveBeenCalledTimes(1) expect(first).toBe(second) expect(mockState.instances[0].setCallback).toHaveBeenCalledTimes(1) expect(mockState.instances[0].setCallback).toHaveBeenCalledWith(secondCallback) }) it('should cleanup existing player and create a new one when msg id changes', () => { const manager = AudioPlayerManager.getInstance() const callback = vi.fn() manager.getAudioPlayer('/text-to-audio', false, 'msg-1', 'hello', 'en-US', callback) const previous = mockState.instances[0] const next = manager.getAudioPlayer('/apps/1/text-to-audio', false, 'msg-2', 'world', 'en-US', callback) expect(previous.pauseAudio).toHaveBeenCalledTimes(1) expect(previous.cacheBuffers).toEqual([]) expect(previous.sourceBuffer?.abort).toHaveBeenCalledTimes(1) expect(mockAudioPlayerConstructor).toHaveBeenCalledTimes(2) expect(next).toBe(mockState.instances[1]) }) it('should swallow cleanup errors and still create a new player', () => { const manager = AudioPlayerManager.getInstance() const callback = vi.fn() manager.getAudioPlayer('/text-to-audio', false, 'msg-1', 'hello', 'en-US', callback) const previous = mockState.instances[0] previous.pauseAudio.mockImplementation(() => { throw new Error('cleanup failure') }) expect(() => { manager.getAudioPlayer('/apps/1/text-to-audio', false, 'msg-2', 'world', 'en-US', callback) }).not.toThrow() expect(previous.pauseAudio).toHaveBeenCalledTimes(1) expect(mockAudioPlayerConstructor).toHaveBeenCalledTimes(2) }) }) describe('resetMsgId', () => { it('should forward reset message id to the cached audio player when present', () => { const manager = AudioPlayerManager.getInstance() const callback = vi.fn() manager.getAudioPlayer('/text-to-audio', false, 'msg-1', 'hello', 'en-US', callback) manager.resetMsgId('msg-updated') expect(mockState.instances[0].resetMsgId).toHaveBeenCalledTimes(1) expect(mockState.instances[0].resetMsgId).toHaveBeenCalledWith('msg-updated') }) it('should not throw when resetting message id without an audio player', () => { const manager = AudioPlayerManager.getInstance() expect(() => manager.resetMsgId('msg-updated')).not.toThrow() }) }) })