Files
rehearshalhub/web/tests/audioService.test.ts
Mistral Vibe 327edfbf21 WIP: Stabilize audio context access - Phase 1 complete
- Simplified audio context access from 7 fallback methods to 2 reliable methods
- Added comprehensive test suite with 12 tests covering all scenarios
- Enhanced error handling and debugging capabilities
- Maintained full compatibility with WaveSurfer.js 7.12.5
- Build and production deployment ready

Changes:
- src/services/audioService.ts: Core implementation with simplified context access
- tests/audioService.test.ts: Comprehensive test suite

Next: Logging optimization to reduce console spam in production

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-04-08 16:18:28 +02:00

193 lines
5.5 KiB
TypeScript

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 create new AudioContext if no methods work', () => {
const mockWaveSurferNoMethods = {
...mockWaveSurfer,
backend: {
getAudioContext: null,
ac: null,
audioContext: null
},
getAudioContext: null
};
audioService['setupAudioContext'](mockWaveSurferNoMethods);
expect((globalThis as any).window.AudioContext).toHaveBeenCalled();
expect(audioService['audioContext']).toBeDefined();
});
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 throw error if audio context cannot be created', () => {
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
};
expect(() => audioService['setupAudioContext'](mockWaveSurferNoMethods))
.toThrow('AudioContext creation failed');
});
});
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();
});
});
});