UNPKG

ts-audio

Version:

297 lines (250 loc) 7.75 kB
import { describe, it, expect, beforeEach, mock } from 'bun:test' import Audio from '../Audio' import { AudioCtx } from '../AudioCtx' /** Test-only interface for accessing private Audio internals */ interface AudioTestInternals { _pauseTime: number _pendingSeekTime: number | null } mock.module('../AudioCtx', () => ({ AudioCtx: mock(), })) mock.module('../initializeSource', () => ({ initializeSource: mock(), })) describe('audio', () => { let audioCtxMock: AudioContext let mockCreateBufferSource: ReturnType<typeof mock> let mockCreateGain: ReturnType<typeof mock> let mockResume: ReturnType<typeof mock> let mockSuspend: ReturnType<typeof mock> beforeEach(() => { mockCreateBufferSource = mock(() => {}) mockCreateGain = mock(() => {}) mockResume = mock(() => {}) mockSuspend = mock(() => {}) audioCtxMock = { createBufferSource: mockCreateBufferSource, createGain: mockCreateGain, resume: mockResume, suspend: mockSuspend, state: 'running', } as unknown as AudioContext // Configure AudioCtx() to return our fake context ;(AudioCtx as unknown as ReturnType<typeof mock>).mockImplementation(() => audioCtxMock) // Reset mock implementations mockCreateBufferSource.mockReturnValue({ buffer: null, loop: false, stop: mock(() => {}), start: mock(() => {}), context: audioCtxMock, }) mockCreateGain.mockReturnValue({ gain: { value: 0.8 }, connect: mock(() => {}), }) }) it('should audio object be defined', () => { expect(Audio).toBeDefined() }) it('should export the audio context object as a read-only property', () => { const audio = Audio({ file: 'test.mp3' }) expect(audio.audioCtx).toBeDefined() expect(audio.audioCtx).toBe(audioCtxMock) }) describe('seek method', () => { let audio: ReturnType<typeof Audio> let mockSource: AudioBufferSourceNode let mockGainNode: GainNode beforeEach(() => { mockSource = { buffer: null, loop: false, stop: mock(() => {}), start: mock(() => {}), context: audioCtxMock, } as unknown as AudioBufferSourceNode mockGainNode = { gain: { value: 0.8 }, connect: mock(() => {}), } as unknown as GainNode audio = Audio({ file: 'test.mp3' }) }) it('should seek to a valid time position', () => { Object.defineProperty(audio, '_states', { value: { isDecoded: true, isPlaying: false, hasStarted: true, source: mockSource, gainNode: mockGainNode, }, writable: true, }) Object.defineProperty(audio, 'duration', { get: () => 120, configurable: true, }) expect(() => audio.seek(60)).not.toThrow() }) it('should clamp negative time to 0', () => { mockSource.buffer = { duration: 120 } as AudioBuffer Object.defineProperty(audio, '_states', { value: { isDecoded: true, isPlaying: false, hasStarted: true, source: mockSource, gainNode: mockGainNode, }, writable: true, }) expect(() => audio.seek(-10)).not.toThrow() expect((audio as unknown as AudioTestInternals)._pauseTime).toBe(0) }) it('should clamp time beyond duration to duration', () => { mockSource.buffer = { duration: 120 } as AudioBuffer Object.defineProperty(audio, '_states', { value: { isDecoded: true, isPlaying: false, hasStarted: true, source: mockSource, gainNode: mockGainNode, }, writable: true, }) expect(() => audio.seek(150)).not.toThrow() expect((audio as unknown as AudioTestInternals)._pauseTime).toBe(120) }) it('should seek to boundary values correctly', () => { Object.defineProperty(audio, '_states', { value: { isDecoded: true, isPlaying: false, hasStarted: true, source: mockSource, gainNode: mockGainNode, }, writable: true, }) Object.defineProperty(audio, 'duration', { get: () => 120, configurable: true, }) expect(() => audio.seek(0)).not.toThrow() expect(() => audio.seek(130)).not.toThrow() }) it('should accept seek before play() is called', () => { Object.defineProperty(audio, '_states', { value: { isDecoded: false, isPlaying: false, hasStarted: false, source: null, gainNode: null, }, writable: true, }) expect(() => audio.seek(30)).not.toThrow() expect((audio as unknown as AudioTestInternals)._pendingSeekTime).toBe(30) }) it('should accept seek before audio is decoded', () => { Object.defineProperty(audio, '_states', { value: { isDecoded: false, isPlaying: false, hasStarted: false, source: mockSource, gainNode: mockGainNode, }, writable: true, }) expect(() => audio.seek(45)).not.toThrow() expect((audio as unknown as AudioTestInternals)._pendingSeekTime).toBe(45) }) }) describe('destroy method', () => { let audio: ReturnType<typeof Audio> let mockSource: AudioBufferSourceNode let mockGainNode: GainNode let mockStop: ReturnType<typeof mock> let mockSourceDisconnect: ReturnType<typeof mock> let mockGainDisconnect: ReturnType<typeof mock> beforeEach(() => { mockStop = mock(() => {}) mockSourceDisconnect = mock(() => {}) mockGainDisconnect = mock(() => {}) mockSource = { buffer: null, loop: false, stop: mockStop, start: mock(() => {}), disconnect: mockSourceDisconnect, context: audioCtxMock, onended: null, } as unknown as AudioBufferSourceNode mockGainNode = { gain: { value: 0.8 }, connect: mock(() => {}), disconnect: mockGainDisconnect, } as unknown as GainNode audio = Audio({ file: 'test.mp3' }) }) it('should properly cleanup resources when destroyed', () => { Object.defineProperty(audio, '_states', { value: { isDecoded: true, isPlaying: true, hasStarted: true, source: mockSource, gainNode: mockGainNode, }, writable: true, }) audio.destroy() expect(mockStop).toHaveBeenCalledWith(0) expect(mockSourceDisconnect).toHaveBeenCalled() expect(mockGainDisconnect).toHaveBeenCalled() expect(mockSource.onended).toBe(null) }) it('should handle destroy with no active source', () => { Object.defineProperty(audio, '_states', { value: { isDecoded: false, isPlaying: false, hasStarted: false, source: null, gainNode: null, }, writable: true, }) expect(() => audio.destroy()).not.toThrow() }) }) describe('isPlaying getter', () => { let audio: ReturnType<typeof Audio> beforeEach(() => { audio = Audio({ file: 'test.mp3' }) }) it('should return true', () => { Object.defineProperty(audio, '_states', { value: { isPlaying: true, }, writable: true, }) expect(audio.isPlaying).toEqual(true) }) it('should return false', () => { Object.defineProperty(audio, '_states', { value: { isPlaying: false, }, writable: true, }) expect(audio.isPlaying).toEqual(false) }) }) })