diff --git a/packages/audiodocs/docs/other/testing.mdx b/packages/audiodocs/docs/other/testing.mdx new file mode 100644 index 000000000..febb96b2d --- /dev/null +++ b/packages/audiodocs/docs/other/testing.mdx @@ -0,0 +1,375 @@ + + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Testing + +React Native Audio API provides a comprehensive mock implementation to help you test your audio-related code without requiring actual audio hardware or platform-specific implementations. + +## Mock Implementation + +The mock implementation provides the same API surface as the real library but with no-op or simplified implementations that are perfect for unit testing. + +### Importing Mocks + + + + +```typescript +import * as MockAudioAPI from 'react-native-audio-api/mock'; + +// Or import specific components +import { AudioContext, AudioRecorder } from 'react-native-audio-api/mock'; +``` + + + + +```typescript +// In your test setup file +jest.mock('react-native-audio-api', () => + require('react-native-audio-api/mock') +); + +// Then in your tests +import { AudioContext, AudioRecorder } from 'react-native-audio-api'; +``` + + + + +## Basic Usage + +### Audio Context Testing + +```typescript +import { AudioContext } from 'react-native-audio-api/mock'; + +describe('Audio Graph Tests', () => { + it('should create and connect audio nodes', () => { + const context = new AudioContext(); + + // Create nodes + const oscillator = context.createOscillator(); + const gainNode = context.createGain(); + + // Configure properties + oscillator.frequency.value = 440; // A4 note + gainNode.gain.value = 0.5; // 50% volume + + // Connect the audio graph + oscillator.connect(gainNode); + gainNode.connect(context.destination); + + // Test the configuration + expect(oscillator.frequency.value).toBe(440); + expect(gainNode.gain.value).toBe(0.5); + }); + + it('should support context state management', async () => { + const context = new AudioContext(); + expect(context.state).toBe('running'); + + await context.suspend(); + expect(context.state).toBe('suspended'); + + await context.resume(); + expect(context.state).toBe('running'); + }); +}); +``` + +### Audio Recording Testing + +```typescript +import { AudioContext, AudioRecorder, FileFormat, FileDirectory } from 'react-native-audio-api/mock'; + +describe('Audio Recording Tests', () => { + it('should configure and control recording', () => { + const context = new AudioContext(); + const recorder = new AudioRecorder(); + + // Configure file output + const result = recorder.enableFileOutput({ + format: FileFormat.M4A, + channelCount: 2, + directory: FileDirectory.Document, + }); + + expect(result.status).toBe('success'); + + // Set up recording chain + const oscillator = context.createOscillator(); + const recorderAdapter = context.createRecorderAdapter(); + + oscillator.connect(recorderAdapter); + recorder.connect(recorderAdapter); + + // Test recording workflow + const startResult = recorder.start(); + expect(startResult.status).toBe('success'); + expect(recorder.isRecording()).toBe(true); + + const stopResult = recorder.stop(); + expect(stopResult.status).toBe('success'); + expect(recorder.isRecording()).toBe(false); + }); +}); +``` + +### Offline Audio Processing + +```typescript +import { OfflineAudioContext } from 'react-native-audio-api/mock'; + +describe('Offline Processing Tests', () => { + it('should render offline audio', async () => { + const offlineContext = new OfflineAudioContext({ + numberOfChannels: 2, + length: 44100, // 1 second at 44.1kHz + sampleRate: 44100, + }); + + // Create a simple tone + const oscillator = offlineContext.createOscillator(); + oscillator.frequency.value = 440; + oscillator.connect(offlineContext.destination); + + // Render the audio + const renderedBuffer = await offlineContext.startRendering(); + + expect(renderedBuffer.numberOfChannels).toBe(2); + expect(renderedBuffer.length).toBe(44100); + expect(renderedBuffer.sampleRate).toBe(44100); + }); +}); +``` + +## Advanced Testing Scenarios + +### Custom Worklet Testing + +```typescript +import { AudioContext, WorkletProcessingNode } from 'react-native-audio-api/mock'; + +describe('Worklet Tests', () => { + it('should create custom audio processing', () => { + const context = new AudioContext(); + + const processingCallback = jest.fn((inputData, outputData, framesToProcess) => { + // Mock audio processing logic + for (let channel = 0; channel < outputData.length; channel++) { + for (let i = 0; i < framesToProcess; i++) { + outputData[channel][i] = inputData[channel][i] * 0.5; // Simple gain + } + } + }); + + const workletNode = new WorkletProcessingNode( + context, + 'AudioRuntime', + processingCallback + ); + + expect(workletNode.context).toBe(context); + }); +}); +``` + +### Audio Streaming Testing + +```typescript +import { AudioContext } from 'react-native-audio-api/mock'; + +describe('Streaming Tests', () => { + it('should handle audio streaming', () => { + const context = new AudioContext(); + + const streamer = context.createStreamer({ + streamPath: 'https://example.com/audio-stream', + }); + + expect(streamer.streamPath).toBe('https://example.com/audio-stream'); + + // Test streaming controls + streamer.start(); + streamer.pause(); + streamer.resume(); + streamer.stop(); + }); +}); +``` + +### Error Handling Testing + +```typescript +import { + AudioRecorder, + NotSupportedError, + InvalidStateError +} from 'react-native-audio-api/mock'; + +describe('Error Handling Tests', () => { + it('should handle various error conditions', () => { + // Test error creation + expect(() => { + throw new NotSupportedError('Feature not supported'); + }).toThrow('Feature not supported'); + + // Test recorder connection errors + const recorder = new AudioRecorder(); + const context = new AudioContext(); + const adapter = context.createRecorderAdapter(); + + // First connection should work + recorder.connect(adapter); + + // Second connection should throw + expect(() => recorder.connect(adapter)).toThrow(); + }); +}); +``` + +## Mock Configuration + +### System Volume Testing + +```typescript +import { useSystemVolume, setMockSystemVolume, AudioManager } from 'react-native-audio-api/mock'; + +describe('System Integration Tests', () => { + it('should mock system audio management', () => { + // Test system sample rate + const preferredRate = AudioManager.getDevicePreferredSampleRate(); + expect(preferredRate).toBe(44100); + + // Test volume management + setMockSystemVolume(0.7); + const currentVolume = useSystemVolume(); + expect(currentVolume).toBe(0.7); + + // Test event listeners + const volumeCallback = jest.fn(); + const listener = AudioManager.addSystemEventListener( + 'volumeChange', + volumeCallback + ); + + expect(listener.remove).toBeDefined(); + listener.remove(); + }); +}); +``` + +### Audio Callback Testing + +```typescript +import { AudioRecorder } from 'react-native-audio-api/mock'; + +describe('Callback Tests', () => { + it('should handle audio data callbacks', () => { + const recorder = new AudioRecorder(); + const audioDataCallback = jest.fn(); + + const result = recorder.onAudioReady( + { + sampleRate: 44100, + bufferLength: 1024, + channelCount: 2, + }, + audioDataCallback + ); + + expect(result.status).toBe('success'); + + // Test callback cleanup + recorder.clearOnAudioReady(); + expect(() => recorder.clearOnAudioReady()).not.toThrow(); + }); +}); +``` + +## Type Safety + +The mock implementation provides full TypeScript support with the same types as the real library: + +```typescript +import type { AudioContext, AudioParam, GainNode } from 'react-native-audio-api/mock'; + +// All types are available and identical to the real implementation +function processAudioNode(node: GainNode): void { + node.gain.value = 0.5; +} +``` + +## Testing Best Practices + +1. **Isolate Audio Logic**: Test audio processing logic separately from UI components +2. **Mock External Dependencies**: Use mocks for file system, network, and platform-specific operations +3. **Test Error Scenarios**: Verify your code handles various error conditions gracefully +4. **Validate Audio Graph Structure**: Ensure nodes are connected correctly +5. **Test Async Operations**: Use proper async/await patterns for operations like rendering + +## Example Test Suite + +```typescript +import { + AudioContext, + AudioRecorder, + FileFormat, + decodeAudioData +} from 'react-native-audio-api/mock'; + +describe('Audio Application Tests', () => { + let context: AudioContext; + + beforeEach(() => { + context = new AudioContext(); + }); + + afterEach(() => { + // Clean up if needed + context.close(); + }); + + describe('Audio Graph', () => { + it('should create complex audio processing chain', () => { + const oscillator = context.createOscillator(); + const filter = context.createBiquadFilter(); + const delay = context.createDelay(); + const gainNode = context.createGain(); + + // Configure effects chain + filter.type = 'lowpass'; + filter.frequency.value = 2000; + delay.delayTime.value = 0.3; + gainNode.gain.value = 0.8; + + // Connect the chain + oscillator.connect(filter); + filter.connect(delay); + delay.connect(gainNode); + gainNode.connect(context.destination); + + // Verify configuration + expect(filter.type).toBe('lowpass'); + expect(delay.delayTime.value).toBe(0.3); + expect(gainNode.gain.value).toBe(0.8); + }); + }); + + describe('File Operations', () => { + it('should handle audio file processing', async () => { + const mockAudioData = new ArrayBuffer(1024); + + // Test audio decoding + const decodedBuffer = await decodeAudioData(mockAudioData); + expect(decodedBuffer.numberOfChannels).toBe(2); + expect(decodedBuffer.sampleRate).toBe(44100); + }); + }); +}); +``` + +The mock implementation provides a complete testing environment that allows you to thoroughly test your audio applications without requiring real audio hardware or complex setup. diff --git a/packages/audiodocs/yarn.lock b/packages/audiodocs/yarn.lock index a9d662725..947ea0fef 100644 --- a/packages/audiodocs/yarn.lock +++ b/packages/audiodocs/yarn.lock @@ -13232,10 +13232,10 @@ typescript-eslint@^8.18.0: "@typescript-eslint/typescript-estree" "8.41.0" "@typescript-eslint/utils" "8.41.0" -typescript@~5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" - integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== +typescript@~5.8.3: + version "5.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" + integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== ua-parser-js@^1.0.35: version "1.0.41" diff --git a/packages/react-native-audio-api/mock/package.json b/packages/react-native-audio-api/mock/package.json new file mode 100644 index 000000000..d31d1d373 --- /dev/null +++ b/packages/react-native-audio-api/mock/package.json @@ -0,0 +1,6 @@ +{ + "main": "../../lib/module/mock/index", + "module": "../../lib/module/mock/index", + "react-native": "../../src/mock/index", + "types": "../../lib/typescript/mock/index.d.ts" +} diff --git a/packages/react-native-audio-api/package.json b/packages/react-native-audio-api/package.json index 8816ac54e..f1ed96881 100644 --- a/packages/react-native-audio-api/package.json +++ b/packages/react-native-audio-api/package.json @@ -13,6 +13,7 @@ "files": [ "src/", "development/react/", + "mock/", "lib/", "common/", "android/src/main/AndroidManifest.xml", diff --git a/packages/react-native-audio-api/src/core/StereoPannerNode.ts b/packages/react-native-audio-api/src/core/StereoPannerNode.ts index 040e9f840..e0fc787be 100644 --- a/packages/react-native-audio-api/src/core/StereoPannerNode.ts +++ b/packages/react-native-audio-api/src/core/StereoPannerNode.ts @@ -1,6 +1,6 @@ +import { StereoPannerOptions } from '../defaults'; import { IStereoPannerNode } from '../interfaces'; -import { SteroPannerOptions } from '../defaults'; -import { TSteroPannerOptions } from '../types'; +import { TStereoPannerOptions } from '../types'; import AudioNode from './AudioNode'; import AudioParam from './AudioParam'; import BaseAudioContext from './BaseAudioContext'; @@ -8,9 +8,9 @@ import BaseAudioContext from './BaseAudioContext'; export default class StereoPannerNode extends AudioNode { readonly pan: AudioParam; - constructor(context: BaseAudioContext, options?: TSteroPannerOptions) { - const finalOptions: TSteroPannerOptions = { - ...SteroPannerOptions, + constructor(context: BaseAudioContext, options?: TStereoPannerOptions) { + const finalOptions: TStereoPannerOptions = { + ...StereoPannerOptions, ...options, }; const pan: IStereoPannerNode = diff --git a/packages/react-native-audio-api/src/defaults.ts b/packages/react-native-audio-api/src/defaults.ts index e40433ddb..cfda045e2 100644 --- a/packages/react-native-audio-api/src/defaults.ts +++ b/packages/react-native-audio-api/src/defaults.ts @@ -1,17 +1,17 @@ import { - TAudioNodeOptions, - TGainOptions, - TSteroPannerOptions, - TConvolverOptions, - TConstantSourceOptions, - TPeriodicWaveConstraints, TAnalyserOptions, - TBiquadFilterOptions, - TOscillatorOptions, - TBaseAudioBufferSourceOptions, - TAudioBufferSourceOptions, TAudioBufferOptions, + TAudioBufferSourceOptions, + TAudioNodeOptions, + TBaseAudioBufferSourceOptions, + TBiquadFilterOptions, + TConstantSourceOptions, + TConvolverOptions, TDelayOptions, + TGainOptions, + TOscillatorOptions, + TPeriodicWaveConstraints, + TStereoPannerOptions, TWaveShaperOptions, } from './types'; @@ -26,7 +26,7 @@ export const GainOptions: TGainOptions = { gain: 1, }; -export const SteroPannerOptions: TSteroPannerOptions = { +export const StereoPannerOptions: TStereoPannerOptions = { ...AudioNodeOptions, channelCountMode: 'clamped-max', pan: 0, diff --git a/packages/react-native-audio-api/src/interfaces.ts b/packages/react-native-audio-api/src/interfaces.ts index 24f05b93b..9f9df8a1d 100644 --- a/packages/react-native-audio-api/src/interfaces.ts +++ b/packages/react-native-audio-api/src/interfaces.ts @@ -10,21 +10,21 @@ import type { OscillatorType, OverSampleType, Result, - WindowType, - TGainOptions, - TSteroPannerOptions, - TConvolverOptions, - TConstantSourceOptions, TAnalyserOptions, - TBiquadFilterOptions, - TOscillatorOptions, - TBaseAudioBufferSourceOptions, - TAudioBufferSourceOptions, - TStreamerOptions, TAudioBufferOptions, + TAudioBufferSourceOptions, + TBaseAudioBufferSourceOptions, + TBiquadFilterOptions, + TConstantSourceOptions, + TConvolverOptions, TDelayOptions, + TGainOptions, TIIRFilterOptions, + TOscillatorOptions, + TStereoPannerOptions, + TStreamerOptions, TWaveShaperOptions, + WindowType, } from './types'; // IMPORTANT: use only IClass, because it is a part of contract between cpp host object and js layer @@ -82,7 +82,7 @@ export interface IBaseAudioContext { ): IConstantSourceNode; createGain(gainOptions: TGainOptions): IGainNode; createStereoPanner( - stereoPannerOptions: TSteroPannerOptions + stereoPannerOptions: TStereoPannerOptions ): IStereoPannerNode; createBiquadFilter: ( biquadFilterOptions: TBiquadFilterOptions diff --git a/packages/react-native-audio-api/src/mock/index.ts b/packages/react-native-audio-api/src/mock/index.ts new file mode 100644 index 000000000..62554fdd9 --- /dev/null +++ b/packages/react-native-audio-api/src/mock/index.ts @@ -0,0 +1,1165 @@ +import { + AudioContextOptions, + AudioRecorderCallbackOptions, + AudioRecorderFileOptions, + AudioRecorderStartOptions, + AudioWorkletRuntime, + BiquadFilterType, + ChannelCountMode, + ChannelInterpretation, + ContextState, + FileDirectory, + FileFormat, + FileInfo, + FilePresetType, + OfflineAudioContextOptions, + OscillatorType, + OverSampleType, + Result, + TAnalyserOptions, + TAudioBufferSourceOptions, + TBaseAudioBufferSourceOptions, + TBiquadFilterOptions, + TConstantSourceOptions, + TConvolverOptions, + TDelayOptions, + TGainOptions, + TOscillatorOptions, + TPeriodicWaveOptions, + TStereoPannerOptions, + TStreamerOptions, + TWaveShaperOptions, +} from '../types'; + +/* eslint-disable no-useless-constructor */ + +const noop = () => {}; + +class MockEventSubscription { + public subscriptionId: string = Number(1).toString(); + + remove = noop; +} + +class MockAudioEventEmitter { + private listeners: { + [event: string]: Array< + (event: Partial> | undefined) => void + >; + } = {}; + + addAudioEventListener( + event: string, + callback: (event: Partial> | undefined) => void + ): MockEventSubscription { + if (!this.listeners[event]) { + this.listeners[event] = []; + } + this.listeners[event].push(callback); + const subscription = new MockEventSubscription(); + + subscription.remove = () => { + const index = this.listeners[event]?.indexOf(callback); + if (index && index > -1) { + this.listeners[event].splice(index, 1); + } + }; + + return subscription; + } + + emit(event: string, data: Record): void { + this.listeners[event]?.forEach((callback) => callback(data)); + } +} + +class AudioParamMock { + private _value: number = 0; + public defaultValue: number = 0; + public minValue: number = -3.4028235e38; + public maxValue: number = 3.4028235e38; + + constructor(_audioParam: unknown, _context: BaseAudioContextMock) {} + + get value(): number { + return this._value; + } + + set value(newValue: number) { + this._value = newValue; + } + + public setValueAtTime(value: number, _startTime: number): AudioParamMock { + this._value = value; + return this; + } + + public linearRampToValueAtTime( + value: number, + _endTime: number + ): AudioParamMock { + this._value = value; + return this; + } + + public exponentialRampToValueAtTime( + value: number, + _endTime: number + ): AudioParamMock { + this._value = value; + return this; + } + + public setTargetAtTime( + target: number, + _startTime: number, + _timeConstant: number + ): AudioParamMock { + this._value = target; + return this; + } + + public setValueCurveAtTime( + _values: Float32Array, + _startTime: number, + _duration: number + ): AudioParamMock { + return this; + } + + public cancelScheduledValues(_startTime: number): AudioParamMock { + return this; + } + + public cancelAndHoldAtTime(_startTime: number): AudioParamMock { + return this; + } +} + +class AudioBufferMock { + public sampleRate: number; + public length: number; + public duration: number; + public numberOfChannels: number; + + constructor(options: { + numberOfChannels: number; + length: number; + sampleRate: number; + }) { + this.numberOfChannels = options.numberOfChannels; + this.length = options.length; + this.sampleRate = options.sampleRate; + this.duration = options.length / options.sampleRate; + } + + getChannelData(_channel: number): Float32Array { + return new Float32Array(this.length); + } + + copyFromChannel( + _destination: Float32Array, + _channelNumber: number, + _startInChannel?: number + ): void {} + + copyToChannel( + _source: Float32Array, + _channelNumber: number, + _startInChannel?: number + ): void {} +} + +class AudioNodeMock { + public context: BaseAudioContextMock; + public numberOfInputs: number = 1; + public numberOfOutputs: number = 1; + public channelCount: number = 2; + public channelCountMode: ChannelCountMode = 'max'; + public channelInterpretation: ChannelInterpretation = 'speakers'; + + constructor(context: BaseAudioContextMock, _node: unknown) { + this.context = context; + } + + public connect( + destination: AudioNodeMock | AudioParamMock + ): AudioNodeMock | void { + if (destination instanceof AudioParamMock) { + return; + } + return destination; + } + + public disconnect(_destination?: AudioNodeMock): void {} +} + +class AudioScheduledSourceNodeMock extends AudioNodeMock { + private _onended: ((event: Event) => void) | null = null; + + constructor(context: BaseAudioContextMock, node: unknown) { + super(context, node); + } + + public start(_when: number = 0): void {} + public stop(_when: number = 0): void {} + + public get onended(): ((event: Event) => void) | null { + return this._onended; + } + + public set onended(callback: ((event: Event) => void) | null) { + this._onended = callback; + } +} + +class AnalyserNodeMock extends AudioNodeMock { + public fftSize: number = 2048; + public frequencyBinCount: number = 1024; + public minDecibels: number = -100; + public maxDecibels: number = -30; + public smoothingTimeConstant: number = 0.8; + + constructor(context: BaseAudioContextMock, _options?: TAnalyserOptions) { + super(context, {}); + } + + getByteFrequencyData(_array: Uint8Array): void {} + getByteTimeDomainData(_array: Uint8Array): void {} + getFloatFrequencyData(_array: Float32Array): void {} + getFloatTimeDomainData(_array: Float32Array): void {} +} + +class GainNodeMock extends AudioNodeMock { + readonly gain: AudioParamMock; + + constructor(context: BaseAudioContextMock, _options?: TGainOptions) { + super(context, {}); + this.gain = new AudioParamMock({}, context); + this.gain.value = 1; + } +} + +class DelayNodeMock extends AudioNodeMock { + readonly delayTime: AudioParamMock; + + constructor(context: BaseAudioContextMock, _options?: TDelayOptions) { + super(context, {}); + this.delayTime = new AudioParamMock({}, context); + this.delayTime.maxValue = 1; + } +} + +class BiquadFilterNodeMock extends AudioNodeMock { + private _type: BiquadFilterType = 'lowpass'; + readonly frequency: AudioParamMock; + readonly detune: AudioParamMock; + readonly Q: AudioParamMock; + readonly gain: AudioParamMock; + + constructor(context: BaseAudioContextMock, _options?: TBiquadFilterOptions) { + super(context, {}); + this.frequency = new AudioParamMock({}, context); + this.detune = new AudioParamMock({}, context); + this.Q = new AudioParamMock({}, context); + this.gain = new AudioParamMock({}, context); + + this.frequency.value = 350; + this.Q.value = 1; + this.gain.value = 0; + } + + get type(): BiquadFilterType { + return this._type; + } + + set type(value: BiquadFilterType) { + this._type = value; + } + + getFrequencyResponse( + _frequencyHz: Float32Array, + _magResponse: Float32Array, + _phaseResponse: Float32Array + ): void {} +} + +class ConvolverNodeMock extends AudioNodeMock { + private _buffer: AudioBufferMock | null = null; + public normalize: boolean = true; + + constructor(context: BaseAudioContextMock, _options?: TConvolverOptions) { + super(context, {}); + } + + get buffer(): AudioBufferMock | null { + return this._buffer; + } + + set buffer(value: AudioBufferMock | null) { + this._buffer = value; + } +} + +class WaveShaperNodeMock extends AudioNodeMock { + private _curve: Float32Array | null = null; + private _oversample: OverSampleType = 'none'; + + constructor(context: BaseAudioContextMock, _options?: TWaveShaperOptions) { + super(context, {}); + } + + get curve(): Float32Array | null { + return this._curve; + } + + set curve(value: Float32Array | null) { + this._curve = value; + } + + get oversample(): OverSampleType { + return this._oversample; + } + + set oversample(value: OverSampleType) { + this._oversample = value; + } +} + +class StereoPannerNodeMock extends AudioNodeMock { + readonly pan: AudioParamMock; + + constructor(context: BaseAudioContextMock, _options?: TStereoPannerOptions) { + super(context, {}); + this.pan = new AudioParamMock({}, context); + } +} + +class OscillatorNodeMock extends AudioScheduledSourceNodeMock { + private _type: OscillatorType = 'sine'; + readonly frequency: AudioParamMock; + readonly detune: AudioParamMock; + + constructor(context: BaseAudioContextMock, _options?: TOscillatorOptions) { + super(context, {}); + this.frequency = new AudioParamMock({}, context); + this.detune = new AudioParamMock({}, context); + this.frequency.value = 440; + } + + get type(): OscillatorType { + return this._type; + } + + set type(value: OscillatorType) { + this._type = value; + } + + public setPeriodicWave(_wave: PeriodicWaveMock): void {} +} + +class ConstantSourceNodeMock extends AudioScheduledSourceNodeMock { + readonly offset: AudioParamMock; + + constructor( + context: BaseAudioContextMock, + _options?: TConstantSourceOptions + ) { + super(context, {}); + this.offset = new AudioParamMock({}, context); + this.offset.value = 1; + } +} + +class AudioBufferSourceNodeMock extends AudioScheduledSourceNodeMock { + private _buffer: AudioBufferMock | null = null; + private _loop: boolean = false; + private _loopStart: number = 0; + private _loopEnd: number = 0; + readonly playbackRate: AudioParamMock; + + constructor( + context: BaseAudioContextMock, + _options?: TAudioBufferSourceOptions + ) { + super(context, {}); + this.playbackRate = new AudioParamMock({}, context); + this.playbackRate.value = 1; + } + + get buffer(): AudioBufferMock | null { + return this._buffer; + } + + set buffer(value: AudioBufferMock | null) { + this._buffer = value; + } + + get loop(): boolean { + return this._loop; + } + + set loop(value: boolean) { + this._loop = value; + } + + get loopStart(): number { + return this._loopStart; + } + + set loopStart(value: number) { + this._loopStart = value; + } + + get loopEnd(): number { + return this._loopEnd; + } + + set loopEnd(value: number) { + this._loopEnd = value; + } +} + +class RecorderAdapterNodeMock extends AudioNodeMock { + public wasConnected: boolean = false; + + constructor(context: BaseAudioContextMock) { + super(context, {}); + } + + getNode(): Record { + return {}; + } +} + +class AudioBufferQueueSourceNodeMock extends AudioScheduledSourceNodeMock { + private _onBufferEnded: ((event: { bufferId: string }) => void) | null = null; + private eventEmitter = new MockAudioEventEmitter(); + + constructor( + context: BaseAudioContextMock, + _options?: TBaseAudioBufferSourceOptions + ) { + super(context, {}); + } + + enqueueBuffer(_buffer: AudioBufferMock): string { + return Math.random().toString(36).substr(2, 9); + } + + dequeueBuffer(_bufferId: string): void {} + clearBuffers(): void {} + pause(): void {} + + get onBufferEnded(): ((event: { bufferId: string }) => void) | null { + return this._onBufferEnded; + } + + set onBufferEnded(callback: ((event: { bufferId: string }) => void) | null) { + this._onBufferEnded = callback; + } +} + +class StreamerNodeMock extends AudioScheduledSourceNodeMock { + private hasBeenSetup: boolean = false; + private _streamPath: string = ''; + + constructor(context: BaseAudioContextMock, options?: TStreamerOptions) { + super(context, {}); + if (options?.streamPath) { + this.initialize(options.streamPath); + } + } + + initialize(streamPath: string): boolean { + if (this.hasBeenSetup) { + throw new Error('Node is already setup'); + } + this._streamPath = streamPath; + this.hasBeenSetup = true; + return true; + } + + get streamPath(): string { + return this._streamPath; + } + + pause(): void {} + resume(): void {} +} + +class WorkletNodeMock extends AudioNodeMock { + constructor( + context: BaseAudioContextMock, + _runtime: AudioWorkletRuntime, + _callback: (audioData: Array, channelCount: number) => void, + _bufferLength: number, + _inputChannelCount: number + ) { + super(context, {}); + } +} + +class WorkletProcessingNodeMock extends AudioNodeMock { + constructor( + context: BaseAudioContextMock, + _runtime: AudioWorkletRuntime, + _callback: ( + inputData: Array, + outputData: Array, + framesToProcess: number, + currentTime: number + ) => void + ) { + super(context, {}); + } +} + +class WorkletSourceNodeMock extends AudioScheduledSourceNodeMock { + constructor( + context: BaseAudioContextMock, + _runtime: AudioWorkletRuntime, + _callback: ( + audioData: Array, + framesToProcess: number, + currentTime: number, + startOffset: number + ) => void + ) { + super(context, {}); + } +} + +class PeriodicWaveMock { + constructor( + _context: BaseAudioContextMock, + _options?: TPeriodicWaveOptions + ) {} +} + +class AudioDestinationNodeMock extends AudioNodeMock { + public maxChannelCount: number = 2; + + constructor(context: BaseAudioContextMock) { + super(context, {}); + this.numberOfOutputs = 0; + } +} + +class BaseAudioContextMock { + public destination: AudioDestinationNodeMock; + private _sampleRate: number = 44100; + private _currentTime: number = 0; + protected _state: ContextState = 'running'; + + constructor(options?: AudioContextOptions) { + this.destination = new AudioDestinationNodeMock(this); + if (options?.sampleRate) { + this._sampleRate = options.sampleRate; + } + } + + get currentTime(): number { + return this._currentTime; + } + + get sampleRate(): number { + return this._sampleRate; + } + + get state(): ContextState { + return this._state; + } + + createBuffer( + numberOfChannels: number, + length: number, + sampleRate: number + ): AudioBufferMock { + return new AudioBufferMock({ numberOfChannels, length, sampleRate }); + } + + createPeriodicWave( + _real?: Float32Array, + _imag?: Float32Array, + _constraints?: { disableNormalization?: boolean } + ): PeriodicWaveMock { + return new PeriodicWaveMock(this); + } + + decodeAudioData(_audioData: ArrayBuffer): Promise { + return Promise.resolve( + new AudioBufferMock({ + numberOfChannels: 2, + length: 44100, + sampleRate: 44100, + }) + ); + } + + createAnalyser(options?: TAnalyserOptions): AnalyserNodeMock { + return new AnalyserNodeMock(this, options); + } + + createBiquadFilter(options?: TBiquadFilterOptions): BiquadFilterNodeMock { + return new BiquadFilterNodeMock(this, options); + } + + createBufferSource( + options?: TAudioBufferSourceOptions + ): AudioBufferSourceNodeMock { + return new AudioBufferSourceNodeMock(this, options); + } + + createChannelMerger(_numberOfInputs?: number): AudioNodeMock { + return new AudioNodeMock(this, {}); + } + + createChannelSplitter(_numberOfOutputs?: number): AudioNodeMock { + return new AudioNodeMock(this, {}); + } + + createConstantSource( + options?: TConstantSourceOptions + ): ConstantSourceNodeMock { + return new ConstantSourceNodeMock(this, options); + } + + createConvolver(options?: TConvolverOptions): ConvolverNodeMock { + return new ConvolverNodeMock(this, options); + } + + createDelay(options?: TDelayOptions): DelayNodeMock { + return new DelayNodeMock(this, options); + } + + createGain(options?: TGainOptions): GainNodeMock { + return new GainNodeMock(this, options); + } + + createOscillator(options?: TOscillatorOptions): OscillatorNodeMock { + return new OscillatorNodeMock(this, options); + } + + createStereoPanner(options?: TStereoPannerOptions): StereoPannerNodeMock { + return new StereoPannerNodeMock(this, options); + } + + createWaveShaper(options?: TWaveShaperOptions): WaveShaperNodeMock { + return new WaveShaperNodeMock(this, options); + } + + createRecorderAdapter(): RecorderAdapterNodeMock { + return new RecorderAdapterNodeMock(this); + } + + createBufferQueueSource( + options?: TBaseAudioBufferSourceOptions + ): AudioBufferQueueSourceNodeMock { + return new AudioBufferQueueSourceNodeMock(this, options); + } + + createStreamer(options?: TStreamerOptions): StreamerNodeMock { + return new StreamerNodeMock(this, options); + } + + createWorkletNode( + _shareableWorklet: Record, + _runOnUI: boolean, + _bufferLength: number, + _inputChannelCount: number + ): WorkletNodeMock { + return new WorkletNodeMock(this, 'AudioRuntime', noop, 0, 0); + } + + createWorkletProcessingNode( + _shareableWorklet: Record, + _runOnUI: boolean + ): WorkletProcessingNodeMock { + return new WorkletProcessingNodeMock(this, 'AudioRuntime', noop); + } + + createWorkletSourceNode( + _shareableWorklet: Record, + _runOnUI: boolean + ): WorkletSourceNodeMock { + return new WorkletSourceNodeMock(this, 'AudioRuntime', noop); + } +} + +class AudioContextMock extends BaseAudioContextMock { + constructor(options?: AudioContextOptions) { + super(options); + } + + close(): Promise { + this._state = 'closed'; + return Promise.resolve(); + } + + resume(): Promise { + this._state = 'running'; + return Promise.resolve(); + } + + suspend(): Promise { + this._state = 'suspended'; + return Promise.resolve(); + } +} + +class OfflineAudioContextMock extends BaseAudioContextMock { + public length: number; + + constructor(options: OfflineAudioContextOptions) { + super({ sampleRate: options.sampleRate }); + this.length = options.length; + } + + startRendering(): Promise { + return Promise.resolve( + new AudioBufferMock({ + numberOfChannels: 2, + length: this.length, + sampleRate: this.sampleRate, + }) + ); + } +} + +class AudioRecorderMock { + private _isRecording: boolean = false; + private _isPaused: boolean = false; + private _currentDuration: number = 0; + private _options: AudioRecorderFileOptions | null = null; + private isFileOutputEnabled: boolean = false; + private eventEmitter = new MockAudioEventEmitter(); + private onAudioReadySubscription: MockEventSubscription | null = null; + private onErrorSubscription: MockEventSubscription | null = null; + + constructor() {} + + enableFileOutput( + options?: Omit + ): Result<{ path: string }> { + this._options = options || {}; + this.isFileOutputEnabled = true; + return { status: 'success', path: '/mock/path/recordings' }; + } + + get options(): AudioRecorderFileOptions | null { + return this._options; + } + + disableFileOutput(): void { + this._options = null; + this.isFileOutputEnabled = false; + } + + start(options?: AudioRecorderStartOptions): Result<{ path: string }> { + this._isRecording = true; + this._isPaused = false; + const path = options?.fileNameOverride || 'recording.m4a'; + return { status: 'success', path }; + } + + stop(): Result { + this._isRecording = false; + this._isPaused = false; + this._currentDuration = 0; + return { + status: 'success', + path: '/mock/path/recording.m4a', + size: 12345, + duration: 5.0, + }; + } + + pause(): void { + this._isPaused = true; + } + + resume(): void { + this._isPaused = false; + } + + connect(_node: RecorderAdapterNodeMock): void { + if (_node.wasConnected) { + throw new Error('RecorderAdapterNode cannot be connected more than once'); + } + _node.wasConnected = true; + } + + disconnect(): void {} + + onAudioReady( + _options: AudioRecorderCallbackOptions, + callback: (event: Partial> | undefined) => void + ): Result<{}> { + if (this.onAudioReadySubscription) { + this.onAudioReadySubscription.remove(); + } + + this.onAudioReadySubscription = this.eventEmitter.addAudioEventListener( + 'audioReady', + callback + ); + + return { status: 'success' }; + } + + clearOnAudioReady(): void { + if (this.onAudioReadySubscription) { + this.onAudioReadySubscription.remove(); + this.onAudioReadySubscription = null; + } + } + + isRecording(): boolean { + return this._isRecording; + } + + isPaused(): boolean { + return this._isPaused; + } + + getCurrentDuration(): number { + return this._currentDuration; + } + + onError( + callback: (error: Record | undefined) => void + ): void { + if (this.onErrorSubscription) { + this.onErrorSubscription.remove(); + } + + this.onErrorSubscription = this.eventEmitter.addAudioEventListener( + 'recorderError', + callback + ); + } + + clearOnError(): void { + if (this.onErrorSubscription) { + this.onErrorSubscription.remove(); + this.onErrorSubscription = null; + } + } +} + +const decodeAudioData = (_audioData: ArrayBuffer): Promise => { + return Promise.resolve( + new AudioBufferMock({ + numberOfChannels: 2, + length: 44100, + sampleRate: 44100, + }) + ); +}; + +const decodePCMInBase64 = (_base64Data: string): Promise => { + return Promise.resolve( + new AudioBufferMock({ + numberOfChannels: 2, + length: 44100, + sampleRate: 44100, + }) + ); +}; + +const changePlaybackSpeed = ( + buffer: AudioBufferMock, + _speed: number +): Promise => { + return Promise.resolve(buffer); +}; + +class AudioManagerMock { + static getDevicePreferredSampleRate(): number { + return 44100; + } + + static observeVolumeChanges(_observe: boolean): void {} + + static addSystemEventListener( + _event: string, + _callback: (event: { value: number }) => void + ): { remove: () => void } { + return { remove: noop }; + } + + static removeSystemEventListener(_listener: { remove: () => void }): void {} +} + +class NotificationManagerMock { + static create(_options: Record): { + update: () => void; + destroy: () => void; + } { + return { + update: noop, + destroy: noop, + }; + } +} + +class PlaybackNotificationManagerMock { + static create(_options: Record): { + update: () => void; + destroy: () => void; + } { + return { + update: noop, + destroy: noop, + }; + } +} + +class RecordingNotificationManagerMock { + static create(_options: Record): { + update: () => void; + destroy: () => void; + } { + return { + update: noop, + destroy: noop, + }; + } +} + +let mockSystemVolumeValue = 0.5; +const useSystemVolume = (): number => mockSystemVolumeValue; +const setMockSystemVolume = (volume: number): void => { + mockSystemVolumeValue = volume; +}; + +class FilePresetMock { + static get Low(): FilePresetType { + return {} as FilePresetType; + } + + static get Medium(): FilePresetType { + return {} as FilePresetType; + } + + static get High(): FilePresetType { + return {} as FilePresetType; + } + + static get Lossless(): FilePresetType { + return {} as FilePresetType; + } +} + +class NotSupportedErrorMock extends Error { + constructor(message: string) { + super(message); + this.name = 'NotSupportedError'; + } +} + +class InvalidAccessErrorMock extends Error { + constructor(message: string) { + super(message); + this.name = 'InvalidAccessError'; + } +} + +class InvalidStateErrorMock extends Error { + constructor(message: string) { + super(message); + this.name = 'InvalidStateError'; + } +} + +class IndexSizeErrorMock extends Error { + constructor(message: string) { + super(message); + this.name = 'IndexSizeError'; + } +} + +class RangeErrorMock extends Error { + constructor(message: string) { + super(message); + this.name = 'RangeError'; + } +} + +class AudioApiErrorMock extends Error { + constructor(message: string) { + super(message); + this.name = 'AudioApiError'; + } +} + +// Export classes with original API names (for compatibility) +export const AnalyserNode = AnalyserNodeMock; +export const AudioBuffer = AudioBufferMock; +export const AudioBufferQueueSourceNode = AudioBufferQueueSourceNodeMock; +export const AudioBufferSourceNode = AudioBufferSourceNodeMock; +export const AudioContext = AudioContextMock; +export const AudioDestinationNode = AudioDestinationNodeMock; +export const AudioNode = AudioNodeMock; +export const AudioParam = AudioParamMock; +export const AudioRecorder = AudioRecorderMock; +export const AudioScheduledSourceNode = AudioScheduledSourceNodeMock; +export const BaseAudioContext = BaseAudioContextMock; +export const BiquadFilterNode = BiquadFilterNodeMock; +export const ConstantSourceNode = ConstantSourceNodeMock; +export const ConvolverNode = ConvolverNodeMock; +export const DelayNode = DelayNodeMock; +export const GainNode = GainNodeMock; +export const OfflineAudioContext = OfflineAudioContextMock; +export const OscillatorNode = OscillatorNodeMock; +export const RecorderAdapterNode = RecorderAdapterNodeMock; +export const StereoPannerNode = StereoPannerNodeMock; +export const StreamerNode = StreamerNodeMock; +export const WaveShaperNode = WaveShaperNodeMock; +export const WorkletNode = WorkletNodeMock; +export const WorkletProcessingNode = WorkletProcessingNodeMock; +export const WorkletSourceNode = WorkletSourceNodeMock; +export const PeriodicWave = PeriodicWaveMock; + +export const AudioManager = AudioManagerMock; +export const NotificationManager = NotificationManagerMock; +export const PlaybackNotificationManager = PlaybackNotificationManagerMock; +export const RecordingNotificationManager = RecordingNotificationManagerMock; + +export const FilePreset = FilePresetMock; + +export const NotSupportedError = NotSupportedErrorMock; +export const InvalidAccessError = InvalidAccessErrorMock; +export const InvalidStateError = InvalidStateErrorMock; +export const IndexSizeError = IndexSizeErrorMock; +export const RangeError = RangeErrorMock; +export const AudioApiError = AudioApiErrorMock; + +// Export functions +export { + changePlaybackSpeed, + decodeAudioData, + decodePCMInBase64, + setMockSystemVolume, + useSystemVolume, +}; + +// Type exports to allow using classes as types +export type AnalyserNode = AnalyserNodeMock; +export type AudioBuffer = AudioBufferMock; +export type AudioBufferQueueSourceNode = AudioBufferQueueSourceNodeMock; +export type AudioBufferSourceNode = AudioBufferSourceNodeMock; +export type AudioContext = AudioContextMock; +export type AudioDestinationNode = AudioDestinationNodeMock; +export type AudioNode = AudioNodeMock; +export type AudioParam = AudioParamMock; +export type AudioRecorder = AudioRecorderMock; +export type AudioScheduledSourceNode = AudioScheduledSourceNodeMock; +export type BaseAudioContext = BaseAudioContextMock; +export type BiquadFilterNode = BiquadFilterNodeMock; +export type ConstantSourceNode = ConstantSourceNodeMock; +export type ConvolverNode = ConvolverNodeMock; +export type DelayNode = DelayNodeMock; +export type GainNode = GainNodeMock; +export type OfflineAudioContext = OfflineAudioContextMock; +export type OscillatorNode = OscillatorNodeMock; +export type RecorderAdapterNode = RecorderAdapterNodeMock; +export type StereoPannerNode = StereoPannerNodeMock; +export type StreamerNode = StreamerNodeMock; +export type WaveShaperNode = WaveShaperNodeMock; +export type WorkletNode = WorkletNodeMock; +export type WorkletProcessingNode = WorkletProcessingNodeMock; +export type WorkletSourceNode = WorkletSourceNodeMock; +export type PeriodicWave = PeriodicWaveMock; + +// Export types and enums +export { + AudioContextOptions, + AudioRecorderCallbackOptions, + AudioRecorderFileOptions, + AudioRecorderStartOptions, + AudioWorkletRuntime, + BiquadFilterType, + ChannelCountMode, + ChannelInterpretation, + ContextState, + FileDirectory, + FileFormat, + FileInfo, + FilePresetType, + OfflineAudioContextOptions, + OscillatorType, + OverSampleType, + Result, + TAnalyserOptions, + TAudioBufferSourceOptions, + TBaseAudioBufferSourceOptions, + TBiquadFilterOptions, + TConstantSourceOptions, + TConvolverOptions, + TDelayOptions, + TGainOptions, + TOscillatorOptions, + TPeriodicWaveOptions, + TStereoPannerOptions, + TStreamerOptions, + TWaveShaperOptions, +}; + +export default { + AnalyserNode: AnalyserNodeMock, + AudioBuffer: AudioBufferMock, + AudioBufferQueueSourceNode: AudioBufferQueueSourceNodeMock, + AudioBufferSourceNode: AudioBufferSourceNodeMock, + AudioContext: AudioContextMock, + AudioDestinationNode: AudioDestinationNodeMock, + AudioNode: AudioNodeMock, + AudioParam: AudioParamMock, + AudioRecorder: AudioRecorderMock, + AudioScheduledSourceNode: AudioScheduledSourceNodeMock, + BaseAudioContext: BaseAudioContextMock, + BiquadFilterNode: BiquadFilterNodeMock, + ConstantSourceNode: ConstantSourceNodeMock, + ConvolverNode: ConvolverNodeMock, + DelayNode: DelayNodeMock, + GainNode: GainNodeMock, + OfflineAudioContext: OfflineAudioContextMock, + OscillatorNode: OscillatorNodeMock, + RecorderAdapterNode: RecorderAdapterNodeMock, + StereoPannerNode: StereoPannerNodeMock, + StreamerNode: StreamerNodeMock, + WaveShaperNode: WaveShaperNodeMock, + WorkletNode: WorkletNodeMock, + WorkletProcessingNode: WorkletProcessingNodeMock, + WorkletSourceNode: WorkletSourceNodeMock, + PeriodicWave: PeriodicWaveMock, + + // Functions + decodeAudioData, + decodePCMInBase64, + changePlaybackSpeed, + useSystemVolume, + setMockSystemVolume, + + // System classes + AudioManager: AudioManagerMock, + NotificationManager: NotificationManagerMock, + PlaybackNotificationManager: PlaybackNotificationManagerMock, + RecordingNotificationManager: RecordingNotificationManagerMock, + + // Utils + FilePreset: FilePresetMock, + + // Errors + NotSupportedError: NotSupportedErrorMock, + InvalidAccessError: InvalidAccessErrorMock, + InvalidStateError: InvalidStateErrorMock, + IndexSizeError: IndexSizeErrorMock, + RangeError: RangeErrorMock, + AudioApiError: AudioApiErrorMock, + + // Enums + FileDirectory, + FileFormat, +}; diff --git a/packages/react-native-audio-api/src/types.ts b/packages/react-native-audio-api/src/types.ts index 01a6a8de8..d1c89c012 100644 --- a/packages/react-native-audio-api/src/types.ts +++ b/packages/react-native-audio-api/src/types.ts @@ -119,7 +119,7 @@ export interface TGainOptions extends TAudioNodeOptions { gain?: number; } -export interface TSteroPannerOptions extends TAudioNodeOptions { +export interface TStereoPannerOptions extends TAudioNodeOptions { pan?: number; } diff --git a/packages/react-native-audio-api/src/web-core/StereoPannerNode.tsx b/packages/react-native-audio-api/src/web-core/StereoPannerNode.tsx index 5c968c8ff..b163ff191 100644 --- a/packages/react-native-audio-api/src/web-core/StereoPannerNode.tsx +++ b/packages/react-native-audio-api/src/web-core/StereoPannerNode.tsx @@ -1,14 +1,14 @@ -import BaseAudioContext from './BaseAudioContext'; +import { TStereoPannerOptions } from '../types'; import AudioNode from './AudioNode'; import AudioParam from './AudioParam'; -import { TSteroPannerOptions } from '../types'; +import BaseAudioContext from './BaseAudioContext'; export default class StereoPannerNode extends AudioNode { readonly pan: AudioParam; constructor( context: BaseAudioContext, - stereoPannerOptions?: TSteroPannerOptions + stereoPannerOptions?: TStereoPannerOptions ) { const pan = new globalThis.StereoPannerNode( context.context, diff --git a/packages/react-native-audio-api/tests/integration.test.ts b/packages/react-native-audio-api/tests/integration.test.ts new file mode 100644 index 000000000..fbc2d19af --- /dev/null +++ b/packages/react-native-audio-api/tests/integration.test.ts @@ -0,0 +1,359 @@ +/* eslint-disable */ + +import * as MockAPI from '../src/mock'; + +describe('Mock Integration Tests', () => { + describe('Audio Graph Creation', () => { + it('should create a complete audio graph with oscillator, gain, and destination', () => { + const context = new MockAPI.AudioContext(); + + // Create nodes + const oscillator = context.createOscillator(); + const gainNode = context.createGain(); + + // Configure oscillator + oscillator.type = 'sine'; + oscillator.frequency.value = 440; + + // Configure gain + gainNode.gain.value = 0.5; + + // Connect the graph + oscillator.connect(gainNode); + gainNode.connect(context.destination); + + // Verify the setup + expect(oscillator.type).toBe('sine'); + expect(oscillator.frequency.value).toBe(440); + expect(gainNode.gain.value).toBe(0.5); + expect(context.destination.numberOfOutputs).toBe(0); + }); + + it('should create an audio graph with effects chain', () => { + const context = new MockAPI.AudioContext(); + + const oscillator = context.createOscillator(); + const filter = context.createBiquadFilter(); + const delay = context.createDelay(); + const gainNode = context.createGain(); + + // Configure effects + filter.type = 'lowpass'; + filter.frequency.value = 2000; + delay.delayTime.value = 0.3; + gainNode.gain.value = 0.8; + + // Create effects chain + oscillator.connect(filter); + filter.connect(delay); + delay.connect(gainNode); + gainNode.connect(context.destination); + + expect(filter.type).toBe('lowpass'); + expect(delay.delayTime.value).toBe(0.3); + }); + }); + + describe('Audio Recording Workflow', () => { + it('should set up recording with adapter node', () => { + const context = new MockAPI.AudioContext(); + const recorder = new MockAPI.AudioRecorder(); + + // Configure file output + const configResult = recorder.enableFileOutput({ + format: MockAPI.FileFormat.M4A, + channelCount: 2, + directory: MockAPI.FileDirectory.Document, + }); + + expect(configResult.status).toBe('success'); + + // Create audio source + const oscillator = context.createOscillator(); + const recorderAdapter = context.createRecorderAdapter(); + + // Connect to recorder + oscillator.connect(recorderAdapter); + recorder.connect(recorderAdapter); + + // Start recording + const startResult = recorder.start({ + fileNameOverride: 'test-recording.m4a', + }); + expect(startResult.status).toBe('success'); + expect(recorder.isRecording()).toBe(true); + + // Stop recording + const stopResult = recorder.stop(); + expect(stopResult.status).toBe('success'); + expect((stopResult as { path?: string }).path).toBeDefined(); + expect(recorder.isRecording()).toBe(false); + }); + + it('should handle audio data callbacks during recording', () => { + const recorder = new MockAPI.AudioRecorder(); + const audioDataCallback = jest.fn(); + + const result = recorder.onAudioReady( + { + sampleRate: 44100, + bufferLength: 1024, + channelCount: 2, + }, + audioDataCallback + ); + + expect(result.status).toBe('success'); + + // Simulate clearing callback + recorder.clearOnAudioReady(); + + // Should not throw when clearing again + expect(() => recorder.clearOnAudioReady()).not.toThrow(); + }); + }); + + describe('Offline Audio Processing', () => { + it('should render offline audio context', async () => { + const offlineContext = new MockAPI.OfflineAudioContext({ + numberOfChannels: 2, + length: 44100, // 1 second at 44.1kHz + sampleRate: 44100, + }); + + // Create a simple tone + const oscillator = offlineContext.createOscillator(); + oscillator.frequency.value = 440; + oscillator.connect(offlineContext.destination); + + // Start rendering + const renderedBuffer = await offlineContext.startRendering(); + + expect(renderedBuffer).toBeInstanceOf(MockAPI.AudioBuffer); + expect(renderedBuffer.length).toBe(44100); + expect(renderedBuffer.sampleRate).toBe(44100); + expect(renderedBuffer.numberOfChannels).toBe(2); + }); + }); + + describe('Worklet Processing', () => { + it('should create and use worklet nodes for custom processing', () => { + const context = new MockAPI.AudioContext(); + + const processingCallback = jest.fn( + (inputData, outputData, framesToProcess) => { + // Mock audio processing logic + for (let channel = 0; channel < outputData.length; channel++) { + for (let i = 0; i < framesToProcess; i++) { + outputData[channel][i] = inputData[channel][i] * 0.5; // Simple gain + } + } + } + ); + + const workletNode = new MockAPI.WorkletProcessingNode( + context, + 'AudioRuntime', + processingCallback + ); + + expect(workletNode).toBeInstanceOf(MockAPI.WorkletProcessingNode); + expect(workletNode.context).toBe(context); + }); + + it('should create worklet source node for custom audio generation', () => { + const context = new MockAPI.AudioContext(); + + const sourceCallback = jest.fn( + (audioData, framesToProcess, currentTime) => { + // Mock audio generation logic + for (let channel = 0; channel < audioData.length; channel++) { + for (let i = 0; i < framesToProcess; i++) { + audioData[channel][i] = Math.sin(currentTime + i); // Simple sine wave + } + } + } + ); + + const workletSource = new MockAPI.WorkletSourceNode( + context, + 'UIRuntime', + sourceCallback + ); + + expect(workletSource).toBeInstanceOf(MockAPI.WorkletSourceNode); + expect(workletSource.context).toBe(context); + }); + }); + + describe('Buffer Queue Management', () => { + it('should manage audio buffer queue for seamless playback', () => { + const context = new MockAPI.AudioContext(); + const queueSource = context.createBufferQueueSource(); + + // Create multiple buffers + const buffer1 = context.createBuffer(2, 1024, 44100); + const buffer2 = context.createBuffer(2, 1024, 44100); + const buffer3 = context.createBuffer(2, 1024, 44100); + + // Enqueue buffers + const id1 = queueSource.enqueueBuffer(buffer1); + const id2 = queueSource.enqueueBuffer(buffer2); + const id3 = queueSource.enqueueBuffer(buffer3); + + expect(typeof id1).toBe('string'); + expect(typeof id2).toBe('string'); + expect(typeof id3).toBe('string'); + expect(id1).not.toBe(id2); + + // Set up buffer ended callback + const bufferEndedCallback = jest.fn(); + queueSource.onBufferEnded = bufferEndedCallback; + + // Dequeue specific buffer + queueSource.dequeueBuffer(id2); + + // Clear all buffers + queueSource.clearBuffers(); + + // Connect and start playback + queueSource.connect(context.destination); + queueSource.start(); + }); + }); + + describe('Streaming Audio', () => { + it('should set up audio streaming node', () => { + const context = new MockAPI.AudioContext(); + + // Create streamer with options + const streamer = context.createStreamer({ + streamPath: 'https://example.com/audio-stream', + }); + + expect(streamer.streamPath).toBe('https://example.com/audio-stream'); + + // Connect to output + streamer.connect(context.destination); + + // Control playback + streamer.start(); + streamer.pause(); + streamer.resume(); + streamer.stop(); + }); + + it('should handle streamer initialization errors', () => { + const context = new MockAPI.AudioContext(); + const streamer = context.createStreamer(); + + // First initialization should succeed + expect(streamer.initialize('http://stream1.com')).toBe(true); + + // Second initialization should fail + expect(() => streamer.initialize('http://stream2.com')).toThrow(); + }); + }); + + describe('System Integration', () => { + it('should integrate with system audio management', () => { + // Get preferred sample rate + const preferredRate = MockAPI.AudioManager.getDevicePreferredSampleRate(); + expect(preferredRate).toBe(44100); + + // Set up volume monitoring + const volumeCallback = jest.fn(); + const listener = MockAPI.AudioManager.addSystemEventListener( + 'volumeChange', + volumeCallback + ); + + MockAPI.AudioManager.observeVolumeChanges(true); + + // Simulate volume change + MockAPI.setMockSystemVolume(0.7); + const currentVolume = MockAPI.useSystemVolume(); + expect(currentVolume).toBe(0.7); + + // Clean up + MockAPI.AudioManager.observeVolumeChanges(false); + listener.remove(); + }); + + it('should create and manage notifications', () => { + const playbackNotification = MockAPI.PlaybackNotificationManager.create({ + title: 'Test Song', + artist: 'Test Artist', + artwork: 'https://example.com/artwork.jpg', + }); + + expect(playbackNotification.update).toBeDefined(); + expect(playbackNotification.destroy).toBeDefined(); + + // Update notification + playbackNotification.update(); + + // Clean up + playbackNotification.destroy(); + }); + }); + + describe('Error Handling', () => { + it('should handle various error conditions', () => { + // Test NotSupportedError + expect(() => { + throw new MockAPI.NotSupportedError('Feature not supported'); + }).toThrow('Feature not supported'); + + // Test InvalidStateError + expect(() => { + throw new MockAPI.InvalidStateError('Invalid state'); + }).toThrow('Invalid state'); + + // Test recorder connection errors + const recorder = new MockAPI.AudioRecorder(); + const context = new MockAPI.AudioContext(); + const adapter = context.createRecorderAdapter(); + + // First connection should work + recorder.connect(adapter); + + // Second connection should throw + expect(() => recorder.connect(adapter)).toThrow(); + }); + }); + + describe('File Presets and Configuration', () => { + it('should use different quality presets', () => { + const recorder = new MockAPI.AudioRecorder(); + + // Test with different presets + const lowQualityConfig = { + preset: MockAPI.FilePreset.Low, + format: MockAPI.FileFormat.M4A, + }; + + const highQualityConfig = { + preset: MockAPI.FilePreset.High, + format: MockAPI.FileFormat.Wav, + }; + + const losslessConfig = { + preset: MockAPI.FilePreset.Lossless, + format: MockAPI.FileFormat.Wav, + }; + + // Configure with different presets + recorder.enableFileOutput(lowQualityConfig); + expect(recorder.options?.format).toBe(MockAPI.FileFormat.M4A); + + recorder.disableFileOutput(); + recorder.enableFileOutput(highQualityConfig); + expect(recorder.options?.format).toBe(MockAPI.FileFormat.Wav); + + recorder.disableFileOutput(); + recorder.enableFileOutput(losslessConfig); + expect(recorder.options?.format).toBe(MockAPI.FileFormat.Wav); + }); + }); +}); diff --git a/packages/react-native-audio-api/tests/jest.config.json b/packages/react-native-audio-api/tests/jest.config.json new file mode 100644 index 000000000..46f9c8dab --- /dev/null +++ b/packages/react-native-audio-api/tests/jest.config.json @@ -0,0 +1,9 @@ +{ + "preset": "react-native", + "rootDir": "..", + "testMatch": ["**/tests/**/*.test.ts", "**/tests/**/*.test.tsx"], + "collectCoverageFrom": ["src/mock/**/*.{ts,tsx}", "!src/mock/**/*.d.ts"], + "coverageDirectory": "tests/coverage", + "coverageReporters": ["text", "lcov", "html"], + "setupFilesAfterEnv": ["/tests/setup.ts"] +} diff --git a/packages/react-native-audio-api/tests/mock.test.ts b/packages/react-native-audio-api/tests/mock.test.ts new file mode 100644 index 000000000..a5a79919a --- /dev/null +++ b/packages/react-native-audio-api/tests/mock.test.ts @@ -0,0 +1,512 @@ +/* eslint-disable */ + +import * as MockAPI from '../src/mock'; + +describe('React Native Audio API Mocks', () => { + describe('AudioContext', () => { + it('should create an AudioContext with default sample rate', () => { + const context = new MockAPI.AudioContext(); + expect(context.sampleRate).toBe(44100); + expect(context.currentTime).toBe(0); + expect(context.state).toBe('running'); + expect(context.destination).toBeInstanceOf(MockAPI.AudioDestinationNode); + }); + + it('should create an AudioContext with custom sample rate', () => { + const context = new MockAPI.AudioContext({ sampleRate: 48000 }); + expect(context.sampleRate).toBe(48000); + }); + + it('should support context state management', async () => { + const context = new MockAPI.AudioContext(); + expect(context.state).toBe('running'); + + await context.suspend(); + expect(context.state).toBe('suspended'); + + await context.resume(); + expect(context.state).toBe('running'); + + await context.close(); + expect(context.state).toBe('closed'); + }); + }); + + describe('OfflineAudioContext', () => { + it('should create an OfflineAudioContext with specified parameters', () => { + const context = new MockAPI.OfflineAudioContext({ + numberOfChannels: 2, + length: 44100, + sampleRate: 44100, + }); + + expect(context.sampleRate).toBe(44100); + expect(context.length).toBe(44100); + }); + + it('should start rendering and return an AudioBuffer', async () => { + const context = new MockAPI.OfflineAudioContext({ + numberOfChannels: 2, + length: 44100, + sampleRate: 44100, + }); + + const buffer = await context.startRendering(); + expect(buffer).toBeInstanceOf(MockAPI.AudioBuffer); + expect(buffer.numberOfChannels).toBe(2); + expect(buffer.length).toBe(44100); + expect(buffer.sampleRate).toBe(44100); + }); + }); + + describe('AudioBuffer', () => { + it('should create an AudioBuffer with correct properties', () => { + const buffer = new MockAPI.AudioBuffer({ + numberOfChannels: 2, + length: 1024, + sampleRate: 44100, + }); + + expect(buffer.numberOfChannels).toBe(2); + expect(buffer.length).toBe(1024); + expect(buffer.sampleRate).toBe(44100); + expect(buffer.duration).toBeCloseTo(1024 / 44100); + }); + + it('should provide channel data', () => { + const buffer = new MockAPI.AudioBuffer({ + numberOfChannels: 2, + length: 1024, + sampleRate: 44100, + }); + + const channelData = buffer.getChannelData(0); + expect(channelData).toBeInstanceOf(Float32Array); + expect(channelData.length).toBe(1024); + }); + }); + + describe('Audio Nodes', () => { + let context: MockAPI.AudioContext; + + beforeEach(() => { + context = new MockAPI.AudioContext(); + }); + + describe('GainNode', () => { + it('should create a GainNode with correct properties', () => { + const gainNode = context.createGain(); + expect(gainNode).toBeInstanceOf(MockAPI.GainNode); + expect(gainNode.gain).toBeInstanceOf(MockAPI.AudioParam); + expect(gainNode.gain.value).toBe(1); + }); + + it('should allow gain parameter manipulation', () => { + const gainNode = context.createGain(); + gainNode.gain.value = 0.5; + expect(gainNode.gain.value).toBe(0.5); + + const result = gainNode.gain.setValueAtTime(0.8, 0); + expect(result).toBe(gainNode.gain); + expect(gainNode.gain.value).toBe(0.8); + }); + }); + + describe('OscillatorNode', () => { + it('should create an OscillatorNode with correct properties', () => { + const oscillator = context.createOscillator(); + expect(oscillator).toBeInstanceOf(MockAPI.OscillatorNode); + expect(oscillator.frequency).toBeInstanceOf(MockAPI.AudioParam); + expect(oscillator.frequency.value).toBe(440); + expect(oscillator.type).toBe('sine'); + }); + + it('should allow type changes', () => { + const oscillator = context.createOscillator(); + oscillator.type = 'square'; + expect(oscillator.type).toBe('square'); + }); + + it('should support start/stop methods', () => { + const oscillator = context.createOscillator(); + expect(() => oscillator.start()).not.toThrow(); + expect(() => oscillator.stop()).not.toThrow(); + }); + + it('should support onended callback', () => { + const oscillator = context.createOscillator(); + const callback = jest.fn(); + oscillator.onended = callback; + expect(oscillator.onended).toBe(callback); + }); + }); + + describe('AnalyserNode', () => { + it('should create an AnalyserNode with correct properties', () => { + const analyser = context.createAnalyser(); + expect(analyser).toBeInstanceOf(MockAPI.AnalyserNode); + expect(analyser.fftSize).toBe(2048); + expect(analyser.frequencyBinCount).toBe(1024); + }); + }); + + describe('BiquadFilterNode', () => { + it('should create a BiquadFilterNode with correct properties', () => { + const filter = context.createBiquadFilter(); + expect(filter).toBeInstanceOf(MockAPI.BiquadFilterNode); + expect(filter.type).toBe('lowpass'); + expect(filter.frequency.value).toBe(350); + expect(filter.Q.value).toBe(1); + }); + }); + }); + + describe('Custom Nodes', () => { + let context: MockAPI.AudioContext; + + beforeEach(() => { + context = new MockAPI.AudioContext(); + }); + + describe('RecorderAdapterNode', () => { + it('should create a RecorderAdapterNode', () => { + const recorderAdapter = context.createRecorderAdapter(); + expect(recorderAdapter).toBeInstanceOf(MockAPI.RecorderAdapterNode); + expect(recorderAdapter.wasConnected).toBe(false); + }); + }); + + describe('AudioBufferQueueSourceNode', () => { + it('should create an AudioBufferQueueSourceNode', () => { + const queueSource = context.createBufferQueueSource(); + expect(queueSource).toBeInstanceOf(MockAPI.AudioBufferQueueSourceNode); + }); + + it('should support buffer queueing operations', () => { + const queueSource = context.createBufferQueueSource(); + const buffer = context.createBuffer(2, 1024, 44100); + + const bufferId = queueSource.enqueueBuffer(buffer); + expect(typeof bufferId).toBe('string'); + expect(bufferId.length).toBeGreaterThan(0); + + expect(() => queueSource.dequeueBuffer(bufferId)).not.toThrow(); + expect(() => queueSource.clearBuffers()).not.toThrow(); + }); + + it('should support onBufferEnded callback', () => { + const queueSource = context.createBufferQueueSource(); + const callback = jest.fn(); + queueSource.onBufferEnded = callback; + expect(queueSource.onBufferEnded).toBe(callback); + }); + }); + + describe('StreamerNode', () => { + it('should create a StreamerNode', () => { + const streamer = context.createStreamer(); + expect(streamer).toBeInstanceOf(MockAPI.StreamerNode); + }); + + it('should support initialization with stream path', () => { + const streamer = context.createStreamer(); + const result = streamer.initialize('http://example.com/stream'); + expect(result).toBe(true); + expect(streamer.streamPath).toBe('http://example.com/stream'); + }); + + it('should throw error on duplicate initialization', () => { + const streamer = context.createStreamer(); + streamer.initialize('http://example.com/stream'); + expect(() => streamer.initialize('http://example.com/other')).toThrow(); + }); + }); + + describe('WorkletNodes', () => { + it('should create WorkletNode', () => { + const workletNode = new MockAPI.WorkletNode( + context, + 'AudioRuntime', + () => {}, + 1024, + 2 + ); + expect(workletNode).toBeInstanceOf(MockAPI.WorkletNode); + }); + + it('should create WorkletProcessingNode', () => { + const processingNode = new MockAPI.WorkletProcessingNode( + context, + 'AudioRuntime', + () => {} + ); + expect(processingNode).toBeInstanceOf(MockAPI.WorkletProcessingNode); + }); + + it('should create WorkletSourceNode', () => { + const sourceNode = new MockAPI.WorkletSourceNode( + context, + 'AudioRuntime', + () => {} + ); + expect(sourceNode).toBeInstanceOf(MockAPI.WorkletSourceNode); + }); + }); + }); + + describe('AudioRecorder', () => { + let recorder: MockAPI.AudioRecorder; + + beforeEach(() => { + recorder = new MockAPI.AudioRecorder(); + }); + + it('should create an AudioRecorder with correct initial state', () => { + expect(recorder.isRecording()).toBe(false); + expect(recorder.isPaused()).toBe(false); + expect(recorder.getCurrentDuration()).toBe(0); + expect(recorder.options).toBeNull(); + }); + + it('should support file output configuration', () => { + const result = recorder.enableFileOutput({ + format: MockAPI.FileFormat.M4A, + channelCount: 2, + }); + + expect(result.status).toBe('success'); + expect((result as { path?: string }).path).toBeDefined(); + expect(recorder.options).toBeDefined(); + }); + + it('should support recording workflow', () => { + recorder.enableFileOutput(); + + const startResult = recorder.start(); + expect(startResult.status).toBe('success'); + expect(recorder.isRecording()).toBe(true); + + recorder.pause(); + expect(recorder.isPaused()).toBe(true); + + recorder.resume(); + expect(recorder.isPaused()).toBe(false); + + const stopResult = recorder.stop(); + expect(stopResult.status).toBe('success'); + expect((stopResult as { path?: string }).path).toBeDefined(); + expect(recorder.isRecording()).toBe(false); + }); + + it('should support RecorderAdapterNode connection', () => { + const context = new MockAPI.AudioContext(); + const adapter = context.createRecorderAdapter(); + + expect(() => recorder.connect(adapter)).not.toThrow(); + expect(adapter.wasConnected).toBe(true); + + // Should throw on duplicate connection + expect(() => recorder.connect(adapter)).toThrow(); + }); + + it('should support audio data callbacks', () => { + const callback = jest.fn(); + const result = recorder.onAudioReady( + { sampleRate: 44100, bufferLength: 1024, channelCount: 2 }, + callback + ); + + expect(result.status).toBe('success'); + + recorder.clearOnAudioReady(); + // Should not throw + }); + + it('should support error callbacks', () => { + const callback = jest.fn(); + expect(() => recorder.onError(callback)).not.toThrow(); + expect(() => recorder.clearOnError()).not.toThrow(); + }); + }); + + describe('Utility Functions', () => { + it('should decode audio data', async () => { + const arrayBuffer = new ArrayBuffer(1024); + const buffer = await MockAPI.decodeAudioData(arrayBuffer); + + expect(buffer).toBeInstanceOf(MockAPI.AudioBuffer); + expect(buffer.numberOfChannels).toBe(2); + expect(buffer.sampleRate).toBe(44100); + }); + + it('should decode PCM in base64', async () => { + const base64Data = 'SGVsbG8gV29ybGQ='; + const buffer = await MockAPI.decodePCMInBase64(base64Data); + + expect(buffer).toBeInstanceOf(MockAPI.AudioBuffer); + }); + + it('should change playback speed', async () => { + const context = new MockAPI.AudioContext(); + const inputBuffer = context.createBuffer(2, 1024, 44100); + const outputBuffer = await MockAPI.changePlaybackSpeed(inputBuffer, 1.5); + + expect(outputBuffer).toBe(inputBuffer); + }); + }); + + describe('System Classes', () => { + describe('AudioManager', () => { + it('should provide device preferred sample rate', () => { + const sampleRate = MockAPI.AudioManager.getDevicePreferredSampleRate(); + expect(sampleRate).toBe(44100); + }); + + it('should support volume change observation', () => { + expect(() => + MockAPI.AudioManager.observeVolumeChanges(true) + ).not.toThrow(); + expect(() => + MockAPI.AudioManager.observeVolumeChanges(false) + ).not.toThrow(); + }); + + it('should support system event listeners', () => { + const callback = jest.fn(); + const listener = MockAPI.AudioManager.addSystemEventListener( + 'volumeChange', + callback + ); + + expect(listener).toHaveProperty('remove'); + expect(typeof listener.remove).toBe('function'); + + expect(() => + MockAPI.AudioManager.removeSystemEventListener(listener) + ).not.toThrow(); + }); + }); + + describe('Notification Managers', () => { + it('should create notification managers', () => { + const options = { title: 'Test', artist: 'Test Artist' }; + + const notification = MockAPI.NotificationManager.create(options); + expect(notification).toHaveProperty('update'); + expect(notification).toHaveProperty('destroy'); + + const playback = MockAPI.PlaybackNotificationManager.create(options); + expect(playback).toHaveProperty('update'); + expect(playback).toHaveProperty('destroy'); + + const recording = MockAPI.RecordingNotificationManager.create(options); + expect(recording).toHaveProperty('update'); + expect(recording).toHaveProperty('destroy'); + }); + }); + }); + + describe('Hooks', () => { + it('should provide useSystemVolume hook', () => { + const volume = MockAPI.useSystemVolume(); + expect(typeof volume).toBe('number'); + expect(volume).toBe(0.5); + + MockAPI.setMockSystemVolume(0.8); + const newVolume = MockAPI.useSystemVolume(); + expect(newVolume).toBe(0.8); + }); + }); + + describe('File Presets', () => { + it('should provide file preset configurations', () => { + expect(MockAPI.FilePreset.Low).toBeDefined(); + expect(MockAPI.FilePreset.Medium).toBeDefined(); + expect(MockAPI.FilePreset.High).toBeDefined(); + expect(MockAPI.FilePreset.Lossless).toBeDefined(); + + expect(typeof MockAPI.FilePreset.Low).toBe('object'); + expect(typeof MockAPI.FilePreset.Medium).toBe('object'); + expect(typeof MockAPI.FilePreset.High).toBe('object'); + expect(typeof MockAPI.FilePreset.Lossless).toBe('object'); + }); + }); + + describe('Error Classes', () => { + it('should provide custom error classes', () => { + const notSupportedError = new MockAPI.NotSupportedError('Not supported'); + expect(notSupportedError).toBeInstanceOf(Error); + expect(notSupportedError.name).toBe('NotSupportedError'); + + const invalidAccessError = new MockAPI.InvalidAccessError( + 'Invalid access' + ); + expect(invalidAccessError).toBeInstanceOf(Error); + expect(invalidAccessError.name).toBe('InvalidAccessError'); + + const invalidStateError = new MockAPI.InvalidStateError('Invalid state'); + expect(invalidStateError).toBeInstanceOf(Error); + expect(invalidStateError.name).toBe('InvalidStateError'); + + const indexSizeError = new MockAPI.IndexSizeError('Index size error'); + expect(indexSizeError).toBeInstanceOf(Error); + expect(indexSizeError.name).toBe('IndexSizeError'); + + const rangeError = new MockAPI.RangeError('Range error'); + expect(rangeError).toBeInstanceOf(Error); + expect(rangeError.name).toBe('RangeError'); + + const audioApiError = new MockAPI.AudioApiError('Audio API error'); + expect(audioApiError).toBeInstanceOf(Error); + expect(audioApiError.name).toBe('AudioApiError'); + }); + }); + + describe('Constants', () => { + it('should export FileDirectory constants', () => { + expect(MockAPI.FileDirectory.Document).toBe(0); + expect(MockAPI.FileDirectory.Cache).toBe(1); + }); + + it('should export FileFormat constants', () => { + expect(MockAPI.FileFormat.Wav).toBe(0); + expect(MockAPI.FileFormat.Caf).toBe(1); + }); + }); + + describe('Node Connections', () => { + let context: MockAPI.AudioContext; + + beforeEach(() => { + context = new MockAPI.AudioContext(); + }); + + it('should support audio node connections', () => { + const oscillator = context.createOscillator(); + const gainNode = context.createGain(); + + const result = oscillator.connect(gainNode); + expect(result).toBe(gainNode); + + const result2 = gainNode.connect(context.destination); + expect(result2).toBe(context.destination); + }); + + it('should support AudioParam connections', () => { + const oscillator = context.createOscillator(); + const gainNode = context.createGain(); + + const result = oscillator.connect(gainNode.gain); + expect(result).toBeUndefined(); + }); + + it('should support disconnections', () => { + const oscillator = context.createOscillator(); + const gainNode = context.createGain(); + + oscillator.connect(gainNode); + expect(() => oscillator.disconnect()).not.toThrow(); + expect(() => oscillator.disconnect(gainNode)).not.toThrow(); + }); + }); +}); diff --git a/packages/react-native-audio-api/tests/setup.ts b/packages/react-native-audio-api/tests/setup.ts new file mode 100644 index 000000000..e382f5148 --- /dev/null +++ b/packages/react-native-audio-api/tests/setup.ts @@ -0,0 +1,20 @@ +// __tests__/setup.ts +import type { IAudioEventEmitter } from '../src/interfaces'; + +// Mock global objects that might be needed +global.createAudioContext = jest.fn(); +global.createAudioRecorder = jest.fn(); +global.AudioEventEmitter = {} as IAudioEventEmitter; + +// Set up global test environment +beforeAll(() => { + // Suppress console warnings for tests + jest.spyOn(console, 'warn').mockImplementation(() => {}); + jest.spyOn(console, 'error').mockImplementation(() => {}); +}); + +afterAll(() => { + // Restore console methods + (console.warn as jest.Mock).mockRestore(); + (console.error as jest.Mock).mockRestore(); +});