259 lines
7.8 KiB
TypeScript
259 lines
7.8 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 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');
|
|
});
|
|
});
|
|
|
|
describe('cleanup', () => {
|
|
it('should stop playback and clean up properly', () => {
|
|
// Mock a playing wavesurfer instance
|
|
const mockWavesurfer = {
|
|
isPlaying: vi.fn(() => true),
|
|
pause: vi.fn(),
|
|
unAll: vi.fn(),
|
|
destroy: vi.fn()
|
|
};
|
|
audioService['wavesurfer'] = mockWavesurfer;
|
|
|
|
audioService['currentPlayingSongId'] = 'song-123';
|
|
audioService['currentPlayingBandId'] = 'band-456';
|
|
|
|
audioService.cleanup();
|
|
|
|
expect(mockWavesurfer.pause).toHaveBeenCalled();
|
|
expect(mockWavesurfer.unAll).toHaveBeenCalled();
|
|
expect(mockWavesurfer.destroy).toHaveBeenCalled();
|
|
expect(audioService['wavesurfer']).toBeNull();
|
|
expect(audioService['currentPlayingSongId']).toBeNull();
|
|
expect(audioService['currentPlayingBandId']).toBeNull();
|
|
});
|
|
|
|
it('should handle cleanup when no wavesurfer instance exists', () => {
|
|
audioService['wavesurfer'] = null;
|
|
audioService['currentPlayingSongId'] = 'song-123';
|
|
|
|
expect(() => audioService.cleanup()).not.toThrow();
|
|
expect(audioService['currentPlayingSongId']).toBeNull();
|
|
});
|
|
});
|
|
|
|
}); |