import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { AudioService } from '../src/services/audioService'; // Mock WaveSurfer function createMockWaveSurfer() { return { backend: { getAudioContext: vi.fn(() => ({ state: 'running', sampleRate: 44100, destination: { channelCount: 2 }, resume: vi.fn().mockResolvedValue(undefined), onstatechange: null })), ac: null, audioContext: null }, getAudioContext: vi.fn(), on: vi.fn(), load: vi.fn(), play: vi.fn(), pause: vi.fn(), getCurrentTime: vi.fn(() => 0), getDuration: vi.fn(() => 120), isPlaying: vi.fn(() => false), unAll: vi.fn(), destroy: vi.fn(), setTime: vi.fn() }; } function createMockAudioContext(state: 'suspended' | 'running' | 'closed' = 'running') { return { state, sampleRate: 44100, destination: { channelCount: 2 }, resume: vi.fn().mockResolvedValue(undefined), onstatechange: null }; } describe('AudioService', () => { let audioService: AudioService; let mockWaveSurfer: any; let mockAudioContext: any; beforeEach(() => { // Reset the singleton instance AudioService.resetInstance(); audioService = AudioService.getInstance(); mockWaveSurfer = createMockWaveSurfer(); mockAudioContext = createMockAudioContext(); // Mock window.AudioContext (globalThis as any).window = { AudioContext: vi.fn(() => mockAudioContext) as any }; }); afterEach(() => { vi.restoreAllMocks(); }); describe('setupAudioContext', () => { it('should successfully access audio context via backend.getAudioContext()', () => { audioService['setupAudioContext'](mockWaveSurfer); expect(mockWaveSurfer.backend.getAudioContext).toHaveBeenCalled(); expect(audioService['audioContext']).toBeDefined(); expect(audioService['audioContext'].state).toBe('running'); }); it('should fall back to ws.getAudioContext() if backend method fails', () => { const mockWaveSurferNoBackend = { ...mockWaveSurfer, backend: null, getAudioContext: vi.fn(() => mockAudioContext) }; audioService['setupAudioContext'](mockWaveSurferNoBackend); expect(mockWaveSurferNoBackend.getAudioContext).toHaveBeenCalled(); expect(audioService['audioContext']).toBeDefined(); }); it('should handle case when no audio context methods work but not throw error', () => { const mockWaveSurferNoMethods = { ...mockWaveSurfer, backend: { getAudioContext: null, ac: null, audioContext: null }, getAudioContext: null }; // Should not throw error - just continue without audio context audioService['setupAudioContext'](mockWaveSurferNoMethods); // Audio context should remain null in this case expect(audioService['audioContext']).toBeNull(); }); it('should handle suspended audio context by resuming it', () => { const suspendedContext = createMockAudioContext('suspended'); mockWaveSurfer.backend.getAudioContext.mockReturnValue(suspendedContext); audioService['setupAudioContext'](mockWaveSurfer); expect(suspendedContext.resume).toHaveBeenCalled(); }); it('should not throw error if audio context cannot be created - just continue', () => { global.window.AudioContext = vi.fn(() => { throw new Error('AudioContext creation failed'); }) as any; const mockWaveSurferNoMethods = { ...mockWaveSurfer, backend: { getAudioContext: null, ac: null, audioContext: null }, getAudioContext: null }; // Should not throw error - just continue without audio context expect(() => audioService['setupAudioContext'](mockWaveSurferNoMethods)) .not.toThrow(); expect(audioService['audioContext']).toBeNull(); }); }); describe('ensureAudioContext', () => { it('should return existing audio context if available', async () => { audioService['audioContext'] = mockAudioContext; const result = await audioService['ensureAudioContext'](); expect(result).toBe(mockAudioContext); }); it('should resume suspended audio context', async () => { const suspendedContext = createMockAudioContext('suspended'); audioService['audioContext'] = suspendedContext; const result = await audioService['ensureAudioContext'](); expect(suspendedContext.resume).toHaveBeenCalled(); expect(result).toBe(suspendedContext); }); it('should create new audio context if none exists', async () => { const result = await audioService['ensureAudioContext'](); expect(global.window.AudioContext).toHaveBeenCalled(); expect(result).toBeDefined(); expect(result.state).toBe('running'); }); it('should throw error if audio context creation fails', async () => { global.window.AudioContext = vi.fn(() => { throw new Error('Creation failed'); }) as any; await expect(audioService['ensureAudioContext']()) .rejects .toThrow('Audio context creation failed: Creation failed'); }); }); describe('getWaveSurferVersion', () => { it('should return WaveSurfer version if available', () => { audioService['wavesurfer'] = { version: '7.12.5' } as any; expect(audioService.getWaveSurferVersion()).toBe('7.12.5'); }); it('should return unknown if version not available', () => { audioService['wavesurfer'] = {} as any; expect(audioService.getWaveSurferVersion()).toBe('unknown'); }); it('should return null if no wavesurfer instance', () => { audioService['wavesurfer'] = null; expect(audioService.getWaveSurferVersion()).toBeNull(); }); }); describe('initializeAudioContext', () => { it('should initialize audio context successfully', async () => { const result = await audioService.initializeAudioContext(); expect(result).toBeDefined(); expect(result.state).toBe('running'); expect(audioService['audioContext']).toBe(result); }); it('should resume suspended audio context', async () => { const suspendedContext = createMockAudioContext('suspended'); global.window.AudioContext = vi.fn(() => suspendedContext) as any; const result = await audioService.initializeAudioContext(); expect(suspendedContext.resume).toHaveBeenCalled(); expect(result).toBe(suspendedContext); }); it('should handle audio context creation errors', async () => { global.window.AudioContext = vi.fn(() => { throw new Error('AudioContext creation failed'); }) as any; await expect(audioService.initializeAudioContext()) .rejects .toThrow('Failed to initialize audio context: AudioContext creation failed'); }); }); });