import { describe, it, expect, beforeEach, vi } from 'vitest'; import { PitchWorklet, createPitchWorklet } from './worklet'; // Mock AudioWorklet API const mockWorkletNode = { port: { onmessage: null as any, postMessage: vi.fn(), }, connect: vi.fn(), disconnect: vi.fn(), }; const mockAudioContext = { audioWorklet: { addModule: vi.fn().mockResolvedValue(undefined), }, sampleRate: 44100, state: 'running', } as any; global.AudioWorkletNode = vi.fn(() => mockWorkletNode) as any; describe('PitchWorklet', () => { let worklet: PitchWorklet; let context: AudioContext; beforeEach(() => { context = mockAudioContext; worklet = new PitchWorklet(context, 2048, 1024); vi.clearAllMocks(); }); describe('initialization', () => { it('should initialize worklet', async () => { await worklet.initialize('/test/worklet.js'); expect(context.audioWorklet.addModule).toHaveBeenCalledWith('/test/worklet.js'); expect(AudioWorkletNode).toHaveBeenCalledWith( context, 'pitch-worklet-processor', { processorOptions: { frameSize: 2048, hopSize: 1024, }, } ); expect(worklet.isInitialized()).toBe(true); }); it('should use default worklet path', async () => { await worklet.initialize(); expect(context.audioWorklet.addModule).toHaveBeenCalledWith('/src/pitch-worklet.js'); }); }); describe('frame callbacks', () => { it('should register and call frame callback', async () => { await worklet.initialize(); const callback = vi.fn(); worklet.onFrame(callback); // Simulate frame message const frameData = new Float32Array(2048); const timestamp = 1.5; mockWorkletNode.port.onmessage({ data: { type: 'frame', frame: frameData.buffer, timestamp, }, } as MessageEvent); expect(callback).toHaveBeenCalled(); const [receivedFrame, receivedTimestamp] = callback.mock.calls[0]; expect(receivedFrame).toBeInstanceOf(Float32Array); expect(receivedFrame.length).toBe(2048); expect(receivedTimestamp).toBe(timestamp); }); it('should unregister callback', async () => { await worklet.initialize(); const callback = vi.fn(); const unsubscribe = worklet.onFrame(callback); unsubscribe(); mockWorkletNode.port.onmessage({ data: { type: 'frame', frame: new Float32Array(2048).buffer, timestamp: 1.0, }, } as MessageEvent); expect(callback).not.toHaveBeenCalled(); }); it('should support multiple callbacks', async () => { await worklet.initialize(); const callback1 = vi.fn(); const callback2 = vi.fn(); worklet.onFrame(callback1); worklet.onFrame(callback2); mockWorkletNode.port.onmessage({ data: { type: 'frame', frame: new Float32Array(2048).buffer, timestamp: 1.0, }, } as MessageEvent); expect(callback1).toHaveBeenCalled(); expect(callback2).toHaveBeenCalled(); }); }); describe('connect/disconnect', () => { it('should connect audio source', async () => { await worklet.initialize(); const source = { connect: vi.fn() } as any; worklet.connect(source); expect(source.connect).toHaveBeenCalledWith(mockWorkletNode); }); it('should throw if not initialized', () => { const source = { connect: vi.fn() } as any; expect(() => worklet.connect(source)).toThrow('not initialized'); }); it('should disconnect worklet', async () => { await worklet.initialize(); worklet.disconnect(); expect(mockWorkletNode.disconnect).toHaveBeenCalled(); }); }); describe('updateConfig', () => { it('should update configuration', async () => { await worklet.initialize(); worklet.updateConfig(4096, 2048); expect(mockWorkletNode.port.postMessage).toHaveBeenCalledWith({ type: 'config', frameSize: 4096, hopSize: 2048, }); }); it('should update partial configuration', async () => { await worklet.initialize(); worklet.updateConfig(undefined, 512); expect(mockWorkletNode.port.postMessage).toHaveBeenCalledWith({ type: 'config', frameSize: undefined, hopSize: 512, }); }); it('should throw if not initialized', () => { expect(() => worklet.updateConfig(2048)).toThrow('not initialized'); }); }); describe('getNode', () => { it('should return null when not initialized', () => { expect(worklet.getNode()).toBeNull(); }); it('should return worklet node when initialized', async () => { await worklet.initialize(); expect(worklet.getNode()).toBe(mockWorkletNode); }); }); describe('dispose', () => { it('should clean up resources', async () => { await worklet.initialize(); const callback = vi.fn(); worklet.onFrame(callback); worklet.dispose(); expect(mockWorkletNode.disconnect).toHaveBeenCalled(); expect(worklet.isInitialized()).toBe(false); // Callbacks should be cleared mockWorkletNode.port.onmessage({ data: { type: 'frame', frame: new Float32Array(2048).buffer, timestamp: 1.0, }, } as MessageEvent); expect(callback).not.toHaveBeenCalled(); }); }); }); describe('createPitchWorklet', () => { it('should create and initialize worklet', async () => { const worklet = await createPitchWorklet(mockAudioContext, { frameSize: 4096, hopSize: 2048, workletPath: '/custom/path.js', }); expect(worklet.isInitialized()).toBe(true); expect(mockAudioContext.audioWorklet.addModule).toHaveBeenCalledWith('/custom/path.js'); }); it('should use default options', async () => { const worklet = await createPitchWorklet(mockAudioContext); expect(worklet.isInitialized()).toBe(true); }); });