From 4d1a33a85097a2c8f572e9f1ce9984c48b2fc99c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Tue, 27 Jan 2026 15:05:43 +0100 Subject: [PATCH 1/5] feat: basic mock file --- .../react-native-audio-api/mock/package.json | 6 + packages/react-native-audio-api/package.json | 1 + .../src/core/StereoPannerNode.ts | 10 +- .../react-native-audio-api/src/defaults.ts | 22 +- .../react-native-audio-api/src/interfaces.ts | 22 +- .../react-native-audio-api/src/mock/index.ts | 1082 +++++++++++++++++ packages/react-native-audio-api/src/types.ts | 2 +- .../src/web-core/StereoPannerNode.tsx | 6 +- 8 files changed, 1120 insertions(+), 31 deletions(-) create mode 100644 packages/react-native-audio-api/mock/package.json create mode 100644 packages/react-native-audio-api/src/mock/index.ts 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..4357b3e79 --- /dev/null +++ b/packages/react-native-audio-api/mock/package.json @@ -0,0 +1,6 @@ +{ + "main": "../../lib/module/mock/react/index", + "module": "../../lib/module/mock/react/index", + "react-native": "../../src/mock/react/index", + "types": "../../lib/typescript/mock/react/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..fac41d2bd --- /dev/null +++ b/packages/react-native-audio-api/src/mock/index.ts @@ -0,0 +1,1082 @@ +import { + AudioContextOptions, + AudioRecorderCallbackOptions, + AudioRecorderFileOptions, + AudioRecorderStartOptions, + AudioWorkletRuntime, + BiquadFilterType, + ChannelCountMode, + ChannelInterpretation, + ContextState, + FileDirectory, + FileFormat, + FileInfo, + 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)); + } +} + +export 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; + } +} + +export 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 {} +} + +export 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 {} +} + +export 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; + } +} + +export 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 {} +} + +export class GainNodeMock extends AudioNodeMock { + readonly gain: AudioParamMock; + + constructor(context: BaseAudioContextMock, _options?: TGainOptions) { + super(context, {}); + this.gain = new AudioParamMock({}, context); + this.gain.value = 1; + } +} + +export class DelayNodeMock extends AudioNodeMock { + readonly delayTime: AudioParamMock; + + constructor(context: BaseAudioContextMock, _options?: TDelayOptions) { + super(context, {}); + this.delayTime = new AudioParamMock({}, context); + this.delayTime.maxValue = 1; + } +} + +export 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 {} +} + +export 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; + } +} + +export 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; + } +} + +export class StereoPannerNodeMock extends AudioNodeMock { + readonly pan: AudioParamMock; + + constructor(context: BaseAudioContextMock, _options?: TStereoPannerOptions) { + super(context, {}); + this.pan = new AudioParamMock({}, context); + } +} + +export 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 {} +} + +export class ConstantSourceNodeMock extends AudioScheduledSourceNodeMock { + readonly offset: AudioParamMock; + + constructor( + context: BaseAudioContextMock, + _options?: TConstantSourceOptions + ) { + super(context, {}); + this.offset = new AudioParamMock({}, context); + this.offset.value = 1; + } +} + +export 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; + } +} + +export class RecorderAdapterNodeMock extends AudioNodeMock { + public wasConnected: boolean = false; + + constructor(context: BaseAudioContextMock) { + super(context, {}); + } + + getNode(): Record { + return {}; + } +} + +export 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; + } +} + +export 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; + } +} + +export class WorkletNodeMock extends AudioNodeMock { + constructor( + context: BaseAudioContextMock, + _runtime: AudioWorkletRuntime, + _callback: (audioData: Array, channelCount: number) => void, + _bufferLength: number, + _inputChannelCount: number + ) { + super(context, {}); + } +} + +export class WorkletProcessingNodeMock extends AudioNodeMock { + constructor( + context: BaseAudioContextMock, + _runtime: AudioWorkletRuntime, + _callback: ( + inputData: Array, + outputData: Array, + framesToProcess: number, + currentTime: number + ) => void + ) { + super(context, {}); + } +} + +export class WorkletSourceNodeMock extends AudioScheduledSourceNodeMock { + constructor( + context: BaseAudioContextMock, + _runtime: AudioWorkletRuntime, + _callback: ( + audioData: Array, + framesToProcess: number, + currentTime: number, + startOffset: number + ) => void + ) { + super(context, {}); + } +} + +export class PeriodicWaveMock { + constructor( + _context: BaseAudioContextMock, + _options?: TPeriodicWaveOptions + ) {} +} + +export class AudioDestinationNodeMock extends AudioNodeMock { + public maxChannelCount: number = 2; + + constructor(context: BaseAudioContextMock) { + super(context, {}); + this.numberOfOutputs = 0; + } +} + +export class BaseAudioContextMock { + public destination: AudioDestinationNodeMock; + private _sampleRate: number = 44100; + private _currentTime: number = 0; + private _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); + } +} + +export class AudioContextMock extends BaseAudioContextMock { + constructor(options?: AudioContextOptions) { + super(options); + } + + close(): Promise { + return Promise.resolve(); + } + + resume(): Promise { + return Promise.resolve(); + } + + suspend(): Promise { + return Promise.resolve(); + } +} + +export 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, + }) + ); + } +} + +export 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; + } + } +} + +export const decodeAudioData = ( + _audioData: ArrayBuffer +): Promise => { + return Promise.resolve( + new AudioBufferMock({ + numberOfChannels: 2, + length: 44100, + sampleRate: 44100, + }) + ); +}; + +export const decodePCMInBase64 = ( + _base64Data: string +): Promise => { + return Promise.resolve( + new AudioBufferMock({ + numberOfChannels: 2, + length: 44100, + sampleRate: 44100, + }) + ); +}; + +export const changePlaybackSpeed = ( + buffer: AudioBufferMock, + _speed: number +): Promise => { + return Promise.resolve(buffer); +}; + +export 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 {} +} + +export class NotificationManagerMock { + static create(_options: Record): { + update: () => void; + destroy: () => void; + } { + return { + update: noop, + destroy: noop, + }; + } +} + +export class PlaybackNotificationManagerMock { + static create(_options: Record): { + update: () => void; + destroy: () => void; + } { + return { + update: noop, + destroy: noop, + }; + } +} + +export class RecordingNotificationManagerMock { + static create(_options: Record): { + update: () => void; + destroy: () => void; + } { + return { + update: noop, + destroy: noop, + }; + } +} + +let mockSystemVolumeValue = 0.5; +export const useSystemVolume = (): number => mockSystemVolumeValue; +export const setMockSystemVolume = (volume: number): void => { + mockSystemVolumeValue = volume; +}; + +export class FilePresetMock { + static get Low(): Record { + return {}; + } + + static get Medium(): Record { + return {}; + } + + static get High(): Record { + return {}; + } + + static get Lossless(): Record { + return {}; + } +} + +export class NotSupportedErrorMock extends Error { + constructor(message: string) { + super(message); + this.name = 'NotSupportedError'; + } +} + +export class InvalidAccessErrorMock extends Error { + constructor(message: string) { + super(message); + this.name = 'InvalidAccessError'; + } +} + +export class InvalidStateErrorMock extends Error { + constructor(message: string) { + super(message); + this.name = 'InvalidStateError'; + } +} + +export class IndexSizeErrorMock extends Error { + constructor(message: string) { + super(message); + this.name = 'IndexSizeError'; + } +} + +export class RangeErrorMock extends Error { + constructor(message: string) { + super(message); + this.name = 'RangeError'; + } +} + +export class AudioApiErrorMock extends Error { + constructor(message: string) { + super(message); + this.name = 'AudioApiError'; + } +} + +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 { FileDirectory, FileFormat }; + +export default { + AnalyserNode: AnalyserNodeMock, + AudioBuffer: AudioBufferMock, + AudioBufferQueueSourceNode: AudioBufferQueueSourceNodeMock, + AudioBufferSourceNode: AudioBufferSourceNodeMock, + AudioContext: AudioContextMock, + decodeAudioData, + decodePCMInBase64, + AudioDestinationNode: AudioDestinationNodeMock, + AudioNode: AudioNodeMock, + AudioParam: AudioParamMock, + AudioRecorder: AudioRecorderMock, + AudioScheduledSourceNode: AudioScheduledSourceNodeMock, + changePlaybackSpeed, + 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, + useSystemVolume, + setMockSystemVolume, + AudioManager: AudioManagerMock, + NotificationManager: NotificationManagerMock, + PlaybackNotificationManager: PlaybackNotificationManagerMock, + RecordingNotificationManager: RecordingNotificationManagerMock, + FilePreset: FilePresetMock, + NotSupportedError: NotSupportedErrorMock, + InvalidAccessError: InvalidAccessErrorMock, + InvalidStateError: InvalidStateErrorMock, + IndexSizeError: IndexSizeErrorMock, + RangeError: RangeErrorMock, + AudioApiError: AudioApiErrorMock, + 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, From 81b1f52512c0dac323b406ce73acecc8a839eaa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Tue, 27 Jan 2026 15:20:18 +0100 Subject: [PATCH 2/5] feat: slop testing --- .../__tests__/README.md | 189 +++++++ .../__tests__/integration.test.ts | 361 ++++++++++++ .../__tests__/jest.config.json | 9 + .../__tests__/mock.test.ts | 515 ++++++++++++++++++ .../react-native-audio-api/__tests__/setup.ts | 21 + .../react-native-audio-api/src/mock/index.ts | 8 +- 6 files changed, 1102 insertions(+), 1 deletion(-) create mode 100644 packages/react-native-audio-api/__tests__/README.md create mode 100644 packages/react-native-audio-api/__tests__/integration.test.ts create mode 100644 packages/react-native-audio-api/__tests__/jest.config.json create mode 100644 packages/react-native-audio-api/__tests__/mock.test.ts create mode 100644 packages/react-native-audio-api/__tests__/setup.ts diff --git a/packages/react-native-audio-api/__tests__/README.md b/packages/react-native-audio-api/__tests__/README.md new file mode 100644 index 000000000..13731b9d5 --- /dev/null +++ b/packages/react-native-audio-api/__tests__/README.md @@ -0,0 +1,189 @@ +# Mock Tests for React Native Audio API + +This directory contains comprehensive test suites for validating the mock implementations of the react-native-audio-api library. + +## Test Files + +### `mock.test.ts` + +Core unit tests that validate individual mock classes and their behavior: + +- AudioContext and OfflineAudioContext functionality +- Audio node creation and configuration +- AudioRecorder state management and workflows +- Custom nodes (RecorderAdapter, BufferQueue, Streamer, Worklet nodes) +- Utility functions (decoding, processing) +- System classes (AudioManager, NotificationManagers) +- Error handling and type correctness + +### `integration.test.ts` + +Integration tests demonstrating real-world usage scenarios: + +- Complete audio graph creation and connection +- Recording workflows with adapter nodes +- Offline audio processing and rendering +- Worklet-based custom processing +- Buffer queue management for seamless playback +- Audio streaming setup +- System integration (volume monitoring, notifications) +- Error condition handling +- File presets and configuration + +### `setup.ts` + +Jest setup file that configures the test environment: + +- Mock global objects required by the audio API +- Console output suppression for cleaner test output +- Test lifecycle management + +## Running Tests + +### Prerequisites + +Make sure you have Jest installed in your project: + +```bash +npm install --save-dev jest @types/jest +# or +yarn add --dev jest @types/jest +``` + +### Run All Tests + +```bash +# From the package root directory +npx jest --config __tests__/jest.config.json + +# Or add to your package.json scripts: +# "test:mocks": "jest --config __tests__/jest.config.json" +npm run test:mocks +``` + +### Run Specific Test Files + +```bash +# Run only unit tests +npx jest __tests__/mock.test.ts + +# Run only integration tests +npx jest __tests__/integration.test.ts +``` + +### Run with Coverage + +```bash +npx jest --config __tests__/jest.config.json --coverage +``` + +### Watch Mode for Development + +```bash +npx jest --config __tests__/jest.config.json --watch +``` + +## Test Coverage + +The tests cover: + +✅ **Core Audio API** + +- AudioContext lifecycle management +- Audio node creation and connection +- AudioParam manipulation +- Buffer management + +✅ **Custom React Native Extensions** + +- AudioRecorder with file output and callbacks +- RecorderAdapterNode connection management +- AudioBufferQueueSourceNode for seamless playback +- StreamerNode for network audio streams +- Worklet nodes for custom processing + +✅ **System Integration** + +- AudioManager for device interaction +- Notification managers for media controls +- Volume monitoring and system events +- Hook integration (useSystemVolume) + +✅ **Error Handling** + +- Custom error classes +- Proper error conditions and validation +- Connection state management + +✅ **Type Safety** + +- All mock implementations use proper TypeScript types +- No `any` or `object` types used +- Proper interface compliance + +## Usage in Your Tests + +### Basic Mock Setup + +```typescript +import * as MockAPI from '../src/mock'; + +// Replace the entire module +jest.mock('react-native-audio-api', () => require('./path/to/mock')); + +// Or import mocks directly +const context = new MockAPI.AudioContext(); +const recorder = new MockAPI.AudioRecorder(); +``` + +### Testing Audio Workflows + +```typescript +describe('Audio Processing', () => { + it('should create and connect audio nodes', () => { + const context = new MockAPI.AudioContext(); + const oscillator = context.createOscillator(); + const gain = context.createGain(); + + oscillator.connect(gain); + gain.connect(context.destination); + + expect(oscillator.frequency.value).toBe(440); + expect(gain.gain.value).toBe(1); + }); +}); +``` + +### Testing Recording + +```typescript +describe('Audio Recording', () => { + it('should handle recording workflow', () => { + const recorder = new MockAPI.AudioRecorder(); + + recorder.enableFileOutput({ format: MockAPI.FileFormat.M4A }); + recorder.start(); + expect(recorder.isRecording()).toBe(true); + + const result = recorder.stop(); + expect(result.status).toBe('success'); + }); +}); +``` + +## Contributing + +When adding new features to the mock implementations: + +1. Add corresponding unit tests in `mock.test.ts` +2. Add integration examples in `integration.test.ts` +3. Ensure all tests pass and maintain high coverage +4. Follow the existing test patterns and naming conventions + +## Notes + +- Tests run in Node.js environment, not React Native +- Global objects are mocked in setup.ts +- Console output is suppressed during tests for cleaner output +- All async operations use proper Promise patterns +- Mock implementations maintain state correctly for testing scenarios 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..1a904a1b3 --- /dev/null +++ b/packages/react-native-audio-api/__tests__/integration.test.ts @@ -0,0 +1,361 @@ +/** + * Integration tests demonstrating real-world usage scenarios of the mocks + */ + +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.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..8b6e5b211 --- /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..469009fa5 --- /dev/null +++ b/packages/react-native-audio-api/__tests__/mock.test.ts @@ -0,0 +1,515 @@ +/** + * Test suite for react-native-audio-api mocks + * This validates that the mock implementations behave correctly + */ + +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.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.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..f03cc6d23 --- /dev/null +++ b/packages/react-native-audio-api/__tests__/setup.ts @@ -0,0 +1,21 @@ +/** + * Jest setup file for mock tests + */ + +// Mock global objects that might be needed +global.createAudioContext = jest.fn(); +global.createAudioRecorder = jest.fn(); +global.AudioEventEmitter = {}; + +// 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(); +}); diff --git a/packages/react-native-audio-api/src/mock/index.ts b/packages/react-native-audio-api/src/mock/index.ts index fac41d2bd..3e5eea65b 100644 --- a/packages/react-native-audio-api/src/mock/index.ts +++ b/packages/react-native-audio-api/src/mock/index.ts @@ -482,6 +482,9 @@ export class StreamerNodeMock extends AudioScheduledSourceNodeMock { get streamPath(): string { return this._streamPath; } + + pause(): void {} + resume(): void {} } export class WorkletNodeMock extends AudioNodeMock { @@ -546,7 +549,7 @@ export class BaseAudioContextMock { public destination: AudioDestinationNodeMock; private _sampleRate: number = 44100; private _currentTime: number = 0; - private _state: ContextState = 'running'; + protected _state: ContextState = 'running'; constructor(options?: AudioContextOptions) { this.destination = new AudioDestinationNodeMock(this); @@ -689,14 +692,17 @@ export class AudioContextMock extends BaseAudioContextMock { } close(): Promise { + this._state = 'closed'; return Promise.resolve(); } resume(): Promise { + this._state = 'running'; return Promise.resolve(); } suspend(): Promise { + this._state = 'suspended'; return Promise.resolve(); } } From 9f831092ca2d692332736f820e612464af937bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Tue, 27 Jan 2026 16:44:14 +0100 Subject: [PATCH 3/5] fix: cleanup slop --- .../__tests__/README.md | 189 ------------------ .../{__tests__ => tests}/integration.test.ts | 4 +- .../{__tests__ => tests}/jest.config.json | 0 .../{__tests__ => tests}/mock.test.ts | 7 +- .../{__tests__ => tests}/setup.ts | 7 +- 5 files changed, 6 insertions(+), 201 deletions(-) delete mode 100644 packages/react-native-audio-api/__tests__/README.md rename packages/react-native-audio-api/{__tests__ => tests}/integration.test.ts (99%) rename packages/react-native-audio-api/{__tests__ => tests}/jest.config.json (100%) rename packages/react-native-audio-api/{__tests__ => tests}/mock.test.ts (99%) rename packages/react-native-audio-api/{__tests__ => tests}/setup.ts (78%) diff --git a/packages/react-native-audio-api/__tests__/README.md b/packages/react-native-audio-api/__tests__/README.md deleted file mode 100644 index 13731b9d5..000000000 --- a/packages/react-native-audio-api/__tests__/README.md +++ /dev/null @@ -1,189 +0,0 @@ -# Mock Tests for React Native Audio API - -This directory contains comprehensive test suites for validating the mock implementations of the react-native-audio-api library. - -## Test Files - -### `mock.test.ts` - -Core unit tests that validate individual mock classes and their behavior: - -- AudioContext and OfflineAudioContext functionality -- Audio node creation and configuration -- AudioRecorder state management and workflows -- Custom nodes (RecorderAdapter, BufferQueue, Streamer, Worklet nodes) -- Utility functions (decoding, processing) -- System classes (AudioManager, NotificationManagers) -- Error handling and type correctness - -### `integration.test.ts` - -Integration tests demonstrating real-world usage scenarios: - -- Complete audio graph creation and connection -- Recording workflows with adapter nodes -- Offline audio processing and rendering -- Worklet-based custom processing -- Buffer queue management for seamless playback -- Audio streaming setup -- System integration (volume monitoring, notifications) -- Error condition handling -- File presets and configuration - -### `setup.ts` - -Jest setup file that configures the test environment: - -- Mock global objects required by the audio API -- Console output suppression for cleaner test output -- Test lifecycle management - -## Running Tests - -### Prerequisites - -Make sure you have Jest installed in your project: - -```bash -npm install --save-dev jest @types/jest -# or -yarn add --dev jest @types/jest -``` - -### Run All Tests - -```bash -# From the package root directory -npx jest --config __tests__/jest.config.json - -# Or add to your package.json scripts: -# "test:mocks": "jest --config __tests__/jest.config.json" -npm run test:mocks -``` - -### Run Specific Test Files - -```bash -# Run only unit tests -npx jest __tests__/mock.test.ts - -# Run only integration tests -npx jest __tests__/integration.test.ts -``` - -### Run with Coverage - -```bash -npx jest --config __tests__/jest.config.json --coverage -``` - -### Watch Mode for Development - -```bash -npx jest --config __tests__/jest.config.json --watch -``` - -## Test Coverage - -The tests cover: - -✅ **Core Audio API** - -- AudioContext lifecycle management -- Audio node creation and connection -- AudioParam manipulation -- Buffer management - -✅ **Custom React Native Extensions** - -- AudioRecorder with file output and callbacks -- RecorderAdapterNode connection management -- AudioBufferQueueSourceNode for seamless playback -- StreamerNode for network audio streams -- Worklet nodes for custom processing - -✅ **System Integration** - -- AudioManager for device interaction -- Notification managers for media controls -- Volume monitoring and system events -- Hook integration (useSystemVolume) - -✅ **Error Handling** - -- Custom error classes -- Proper error conditions and validation -- Connection state management - -✅ **Type Safety** - -- All mock implementations use proper TypeScript types -- No `any` or `object` types used -- Proper interface compliance - -## Usage in Your Tests - -### Basic Mock Setup - -```typescript -import * as MockAPI from '../src/mock'; - -// Replace the entire module -jest.mock('react-native-audio-api', () => require('./path/to/mock')); - -// Or import mocks directly -const context = new MockAPI.AudioContext(); -const recorder = new MockAPI.AudioRecorder(); -``` - -### Testing Audio Workflows - -```typescript -describe('Audio Processing', () => { - it('should create and connect audio nodes', () => { - const context = new MockAPI.AudioContext(); - const oscillator = context.createOscillator(); - const gain = context.createGain(); - - oscillator.connect(gain); - gain.connect(context.destination); - - expect(oscillator.frequency.value).toBe(440); - expect(gain.gain.value).toBe(1); - }); -}); -``` - -### Testing Recording - -```typescript -describe('Audio Recording', () => { - it('should handle recording workflow', () => { - const recorder = new MockAPI.AudioRecorder(); - - recorder.enableFileOutput({ format: MockAPI.FileFormat.M4A }); - recorder.start(); - expect(recorder.isRecording()).toBe(true); - - const result = recorder.stop(); - expect(result.status).toBe('success'); - }); -}); -``` - -## Contributing - -When adding new features to the mock implementations: - -1. Add corresponding unit tests in `mock.test.ts` -2. Add integration examples in `integration.test.ts` -3. Ensure all tests pass and maintain high coverage -4. Follow the existing test patterns and naming conventions - -## Notes - -- Tests run in Node.js environment, not React Native -- Global objects are mocked in setup.ts -- Console output is suppressed during tests for cleaner output -- All async operations use proper Promise patterns -- Mock implementations maintain state correctly for testing scenarios diff --git a/packages/react-native-audio-api/__tests__/integration.test.ts b/packages/react-native-audio-api/tests/integration.test.ts similarity index 99% rename from packages/react-native-audio-api/__tests__/integration.test.ts rename to packages/react-native-audio-api/tests/integration.test.ts index 1a904a1b3..0422db627 100644 --- a/packages/react-native-audio-api/__tests__/integration.test.ts +++ b/packages/react-native-audio-api/tests/integration.test.ts @@ -1,6 +1,4 @@ -/** - * Integration tests demonstrating real-world usage scenarios of the mocks - */ +/* eslint-disable */ import * as MockAPI from '../src/mock'; diff --git a/packages/react-native-audio-api/__tests__/jest.config.json b/packages/react-native-audio-api/tests/jest.config.json similarity index 100% rename from packages/react-native-audio-api/__tests__/jest.config.json rename to packages/react-native-audio-api/tests/jest.config.json diff --git a/packages/react-native-audio-api/__tests__/mock.test.ts b/packages/react-native-audio-api/tests/mock.test.ts similarity index 99% rename from packages/react-native-audio-api/__tests__/mock.test.ts rename to packages/react-native-audio-api/tests/mock.test.ts index 469009fa5..2db9f277c 100644 --- a/packages/react-native-audio-api/__tests__/mock.test.ts +++ b/packages/react-native-audio-api/tests/mock.test.ts @@ -1,7 +1,4 @@ -/** - * Test suite for react-native-audio-api mocks - * This validates that the mock implementations behave correctly - */ +/* eslint-disable */ import * as MockAPI from '../src/mock'; @@ -258,7 +255,7 @@ describe('React Native Audio API Mocks', () => { }); describe('AudioRecorder', () => { - let recorder: MockAPI.AudioRecorder; + let recorder: MockAPI.AudioRecorderMock; beforeEach(() => { recorder = new MockAPI.AudioRecorder(); diff --git a/packages/react-native-audio-api/__tests__/setup.ts b/packages/react-native-audio-api/tests/setup.ts similarity index 78% rename from packages/react-native-audio-api/__tests__/setup.ts rename to packages/react-native-audio-api/tests/setup.ts index f03cc6d23..e382f5148 100644 --- a/packages/react-native-audio-api/__tests__/setup.ts +++ b/packages/react-native-audio-api/tests/setup.ts @@ -1,11 +1,10 @@ -/** - * Jest setup file for mock tests - */ +// __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 = {}; +global.AudioEventEmitter = {} as IAudioEventEmitter; // Set up global test environment beforeAll(() => { From 48796196dbbf508f45712a0956ba2b1291300c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Wed, 28 Jan 2026 15:12:44 +0100 Subject: [PATCH 4/5] fix: mocks setup --- .../react-native-audio-api/mock/package.json | 8 +- .../react-native-audio-api/src/mock/index.ts | 193 ++++++++++++------ .../tests/integration.test.ts | 2 +- .../tests/jest.config.json | 6 +- .../react-native-audio-api/tests/mock.test.ts | 6 +- 5 files changed, 146 insertions(+), 69 deletions(-) diff --git a/packages/react-native-audio-api/mock/package.json b/packages/react-native-audio-api/mock/package.json index 4357b3e79..d31d1d373 100644 --- a/packages/react-native-audio-api/mock/package.json +++ b/packages/react-native-audio-api/mock/package.json @@ -1,6 +1,6 @@ { - "main": "../../lib/module/mock/react/index", - "module": "../../lib/module/mock/react/index", - "react-native": "../../src/mock/react/index", - "types": "../../lib/typescript/mock/react/index.d.ts" + "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/src/mock/index.ts b/packages/react-native-audio-api/src/mock/index.ts index 3e5eea65b..62554fdd9 100644 --- a/packages/react-native-audio-api/src/mock/index.ts +++ b/packages/react-native-audio-api/src/mock/index.ts @@ -11,6 +11,7 @@ import { FileDirectory, FileFormat, FileInfo, + FilePresetType, OfflineAudioContextOptions, OscillatorType, OverSampleType, @@ -72,7 +73,7 @@ class MockAudioEventEmitter { } } -export class AudioParamMock { +class AudioParamMock { private _value: number = 0; public defaultValue: number = 0; public minValue: number = -3.4028235e38; @@ -135,7 +136,7 @@ export class AudioParamMock { } } -export class AudioBufferMock { +class AudioBufferMock { public sampleRate: number; public length: number; public duration: number; @@ -169,7 +170,7 @@ export class AudioBufferMock { ): void {} } -export class AudioNodeMock { +class AudioNodeMock { public context: BaseAudioContextMock; public numberOfInputs: number = 1; public numberOfOutputs: number = 1; @@ -193,7 +194,7 @@ export class AudioNodeMock { public disconnect(_destination?: AudioNodeMock): void {} } -export class AudioScheduledSourceNodeMock extends AudioNodeMock { +class AudioScheduledSourceNodeMock extends AudioNodeMock { private _onended: ((event: Event) => void) | null = null; constructor(context: BaseAudioContextMock, node: unknown) { @@ -212,7 +213,7 @@ export class AudioScheduledSourceNodeMock extends AudioNodeMock { } } -export class AnalyserNodeMock extends AudioNodeMock { +class AnalyserNodeMock extends AudioNodeMock { public fftSize: number = 2048; public frequencyBinCount: number = 1024; public minDecibels: number = -100; @@ -229,7 +230,7 @@ export class AnalyserNodeMock extends AudioNodeMock { getFloatTimeDomainData(_array: Float32Array): void {} } -export class GainNodeMock extends AudioNodeMock { +class GainNodeMock extends AudioNodeMock { readonly gain: AudioParamMock; constructor(context: BaseAudioContextMock, _options?: TGainOptions) { @@ -239,7 +240,7 @@ export class GainNodeMock extends AudioNodeMock { } } -export class DelayNodeMock extends AudioNodeMock { +class DelayNodeMock extends AudioNodeMock { readonly delayTime: AudioParamMock; constructor(context: BaseAudioContextMock, _options?: TDelayOptions) { @@ -249,7 +250,7 @@ export class DelayNodeMock extends AudioNodeMock { } } -export class BiquadFilterNodeMock extends AudioNodeMock { +class BiquadFilterNodeMock extends AudioNodeMock { private _type: BiquadFilterType = 'lowpass'; readonly frequency: AudioParamMock; readonly detune: AudioParamMock; @@ -283,7 +284,7 @@ export class BiquadFilterNodeMock extends AudioNodeMock { ): void {} } -export class ConvolverNodeMock extends AudioNodeMock { +class ConvolverNodeMock extends AudioNodeMock { private _buffer: AudioBufferMock | null = null; public normalize: boolean = true; @@ -300,7 +301,7 @@ export class ConvolverNodeMock extends AudioNodeMock { } } -export class WaveShaperNodeMock extends AudioNodeMock { +class WaveShaperNodeMock extends AudioNodeMock { private _curve: Float32Array | null = null; private _oversample: OverSampleType = 'none'; @@ -325,7 +326,7 @@ export class WaveShaperNodeMock extends AudioNodeMock { } } -export class StereoPannerNodeMock extends AudioNodeMock { +class StereoPannerNodeMock extends AudioNodeMock { readonly pan: AudioParamMock; constructor(context: BaseAudioContextMock, _options?: TStereoPannerOptions) { @@ -334,7 +335,7 @@ export class StereoPannerNodeMock extends AudioNodeMock { } } -export class OscillatorNodeMock extends AudioScheduledSourceNodeMock { +class OscillatorNodeMock extends AudioScheduledSourceNodeMock { private _type: OscillatorType = 'sine'; readonly frequency: AudioParamMock; readonly detune: AudioParamMock; @@ -357,7 +358,7 @@ export class OscillatorNodeMock extends AudioScheduledSourceNodeMock { public setPeriodicWave(_wave: PeriodicWaveMock): void {} } -export class ConstantSourceNodeMock extends AudioScheduledSourceNodeMock { +class ConstantSourceNodeMock extends AudioScheduledSourceNodeMock { readonly offset: AudioParamMock; constructor( @@ -370,7 +371,7 @@ export class ConstantSourceNodeMock extends AudioScheduledSourceNodeMock { } } -export class AudioBufferSourceNodeMock extends AudioScheduledSourceNodeMock { +class AudioBufferSourceNodeMock extends AudioScheduledSourceNodeMock { private _buffer: AudioBufferMock | null = null; private _loop: boolean = false; private _loopStart: number = 0; @@ -419,7 +420,7 @@ export class AudioBufferSourceNodeMock extends AudioScheduledSourceNodeMock { } } -export class RecorderAdapterNodeMock extends AudioNodeMock { +class RecorderAdapterNodeMock extends AudioNodeMock { public wasConnected: boolean = false; constructor(context: BaseAudioContextMock) { @@ -431,7 +432,7 @@ export class RecorderAdapterNodeMock extends AudioNodeMock { } } -export class AudioBufferQueueSourceNodeMock extends AudioScheduledSourceNodeMock { +class AudioBufferQueueSourceNodeMock extends AudioScheduledSourceNodeMock { private _onBufferEnded: ((event: { bufferId: string }) => void) | null = null; private eventEmitter = new MockAudioEventEmitter(); @@ -459,7 +460,7 @@ export class AudioBufferQueueSourceNodeMock extends AudioScheduledSourceNodeMock } } -export class StreamerNodeMock extends AudioScheduledSourceNodeMock { +class StreamerNodeMock extends AudioScheduledSourceNodeMock { private hasBeenSetup: boolean = false; private _streamPath: string = ''; @@ -487,7 +488,7 @@ export class StreamerNodeMock extends AudioScheduledSourceNodeMock { resume(): void {} } -export class WorkletNodeMock extends AudioNodeMock { +class WorkletNodeMock extends AudioNodeMock { constructor( context: BaseAudioContextMock, _runtime: AudioWorkletRuntime, @@ -499,7 +500,7 @@ export class WorkletNodeMock extends AudioNodeMock { } } -export class WorkletProcessingNodeMock extends AudioNodeMock { +class WorkletProcessingNodeMock extends AudioNodeMock { constructor( context: BaseAudioContextMock, _runtime: AudioWorkletRuntime, @@ -514,7 +515,7 @@ export class WorkletProcessingNodeMock extends AudioNodeMock { } } -export class WorkletSourceNodeMock extends AudioScheduledSourceNodeMock { +class WorkletSourceNodeMock extends AudioScheduledSourceNodeMock { constructor( context: BaseAudioContextMock, _runtime: AudioWorkletRuntime, @@ -529,14 +530,14 @@ export class WorkletSourceNodeMock extends AudioScheduledSourceNodeMock { } } -export class PeriodicWaveMock { +class PeriodicWaveMock { constructor( _context: BaseAudioContextMock, _options?: TPeriodicWaveOptions ) {} } -export class AudioDestinationNodeMock extends AudioNodeMock { +class AudioDestinationNodeMock extends AudioNodeMock { public maxChannelCount: number = 2; constructor(context: BaseAudioContextMock) { @@ -545,7 +546,7 @@ export class AudioDestinationNodeMock extends AudioNodeMock { } } -export class BaseAudioContextMock { +class BaseAudioContextMock { public destination: AudioDestinationNodeMock; private _sampleRate: number = 44100; private _currentTime: number = 0; @@ -686,7 +687,7 @@ export class BaseAudioContextMock { } } -export class AudioContextMock extends BaseAudioContextMock { +class AudioContextMock extends BaseAudioContextMock { constructor(options?: AudioContextOptions) { super(options); } @@ -707,7 +708,7 @@ export class AudioContextMock extends BaseAudioContextMock { } } -export class OfflineAudioContextMock extends BaseAudioContextMock { +class OfflineAudioContextMock extends BaseAudioContextMock { public length: number; constructor(options: OfflineAudioContextOptions) { @@ -726,7 +727,7 @@ export class OfflineAudioContextMock extends BaseAudioContextMock { } } -export class AudioRecorderMock { +class AudioRecorderMock { private _isRecording: boolean = false; private _isPaused: boolean = false; private _currentDuration: number = 0; @@ -847,9 +848,7 @@ export class AudioRecorderMock { } } -export const decodeAudioData = ( - _audioData: ArrayBuffer -): Promise => { +const decodeAudioData = (_audioData: ArrayBuffer): Promise => { return Promise.resolve( new AudioBufferMock({ numberOfChannels: 2, @@ -859,9 +858,7 @@ export const decodeAudioData = ( ); }; -export const decodePCMInBase64 = ( - _base64Data: string -): Promise => { +const decodePCMInBase64 = (_base64Data: string): Promise => { return Promise.resolve( new AudioBufferMock({ numberOfChannels: 2, @@ -871,14 +868,14 @@ export const decodePCMInBase64 = ( ); }; -export const changePlaybackSpeed = ( +const changePlaybackSpeed = ( buffer: AudioBufferMock, _speed: number ): Promise => { return Promise.resolve(buffer); }; -export class AudioManagerMock { +class AudioManagerMock { static getDevicePreferredSampleRate(): number { return 44100; } @@ -895,7 +892,7 @@ export class AudioManagerMock { static removeSystemEventListener(_listener: { remove: () => void }): void {} } -export class NotificationManagerMock { +class NotificationManagerMock { static create(_options: Record): { update: () => void; destroy: () => void; @@ -907,7 +904,7 @@ export class NotificationManagerMock { } } -export class PlaybackNotificationManagerMock { +class PlaybackNotificationManagerMock { static create(_options: Record): { update: () => void; destroy: () => void; @@ -919,7 +916,7 @@ export class PlaybackNotificationManagerMock { } } -export class RecordingNotificationManagerMock { +class RecordingNotificationManagerMock { static create(_options: Record): { update: () => void; destroy: () => void; @@ -932,71 +929,72 @@ export class RecordingNotificationManagerMock { } let mockSystemVolumeValue = 0.5; -export const useSystemVolume = (): number => mockSystemVolumeValue; -export const setMockSystemVolume = (volume: number): void => { +const useSystemVolume = (): number => mockSystemVolumeValue; +const setMockSystemVolume = (volume: number): void => { mockSystemVolumeValue = volume; }; -export class FilePresetMock { - static get Low(): Record { - return {}; +class FilePresetMock { + static get Low(): FilePresetType { + return {} as FilePresetType; } - static get Medium(): Record { - return {}; + static get Medium(): FilePresetType { + return {} as FilePresetType; } - static get High(): Record { - return {}; + static get High(): FilePresetType { + return {} as FilePresetType; } - static get Lossless(): Record { - return {}; + static get Lossless(): FilePresetType { + return {} as FilePresetType; } } -export class NotSupportedErrorMock extends Error { +class NotSupportedErrorMock extends Error { constructor(message: string) { super(message); this.name = 'NotSupportedError'; } } -export class InvalidAccessErrorMock extends Error { +class InvalidAccessErrorMock extends Error { constructor(message: string) { super(message); this.name = 'InvalidAccessError'; } } -export class InvalidStateErrorMock extends Error { +class InvalidStateErrorMock extends Error { constructor(message: string) { super(message); this.name = 'InvalidStateError'; } } -export class IndexSizeErrorMock extends Error { +class IndexSizeErrorMock extends Error { constructor(message: string) { super(message); this.name = 'IndexSizeError'; } } -export class RangeErrorMock extends Error { +class RangeErrorMock extends Error { constructor(message: string) { super(message); this.name = 'RangeError'; } } -export class AudioApiErrorMock extends Error { +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; @@ -1038,7 +1036,76 @@ export const IndexSizeError = IndexSizeErrorMock; export const RangeError = RangeErrorMock; export const AudioApiError = AudioApiErrorMock; -export { FileDirectory, FileFormat }; +// 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, @@ -1046,14 +1113,11 @@ export default { AudioBufferQueueSourceNode: AudioBufferQueueSourceNodeMock, AudioBufferSourceNode: AudioBufferSourceNodeMock, AudioContext: AudioContextMock, - decodeAudioData, - decodePCMInBase64, AudioDestinationNode: AudioDestinationNodeMock, AudioNode: AudioNodeMock, AudioParam: AudioParamMock, AudioRecorder: AudioRecorderMock, AudioScheduledSourceNode: AudioScheduledSourceNodeMock, - changePlaybackSpeed, BaseAudioContext: BaseAudioContextMock, BiquadFilterNode: BiquadFilterNodeMock, ConstantSourceNode: ConstantSourceNodeMock, @@ -1070,19 +1134,32 @@ export default { 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/tests/integration.test.ts b/packages/react-native-audio-api/tests/integration.test.ts index 0422db627..fbc2d19af 100644 --- a/packages/react-native-audio-api/tests/integration.test.ts +++ b/packages/react-native-audio-api/tests/integration.test.ts @@ -86,7 +86,7 @@ describe('Mock Integration Tests', () => { // Stop recording const stopResult = recorder.stop(); expect(stopResult.status).toBe('success'); - expect(stopResult.path).toBeDefined(); + expect((stopResult as { path?: string }).path).toBeDefined(); expect(recorder.isRecording()).toBe(false); }); diff --git a/packages/react-native-audio-api/tests/jest.config.json b/packages/react-native-audio-api/tests/jest.config.json index 8b6e5b211..46f9c8dab 100644 --- a/packages/react-native-audio-api/tests/jest.config.json +++ b/packages/react-native-audio-api/tests/jest.config.json @@ -1,9 +1,9 @@ { "preset": "react-native", "rootDir": "..", - "testMatch": ["**/__tests__/**/*.test.ts", "**/__tests__/**/*.test.tsx"], + "testMatch": ["**/tests/**/*.test.ts", "**/tests/**/*.test.tsx"], "collectCoverageFrom": ["src/mock/**/*.{ts,tsx}", "!src/mock/**/*.d.ts"], - "coverageDirectory": "__tests__/coverage", + "coverageDirectory": "tests/coverage", "coverageReporters": ["text", "lcov", "html"], - "setupFilesAfterEnv": ["/__tests__/setup.ts"] + "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 index 2db9f277c..a5a79919a 100644 --- a/packages/react-native-audio-api/tests/mock.test.ts +++ b/packages/react-native-audio-api/tests/mock.test.ts @@ -255,7 +255,7 @@ describe('React Native Audio API Mocks', () => { }); describe('AudioRecorder', () => { - let recorder: MockAPI.AudioRecorderMock; + let recorder: MockAPI.AudioRecorder; beforeEach(() => { recorder = new MockAPI.AudioRecorder(); @@ -275,7 +275,7 @@ describe('React Native Audio API Mocks', () => { }); expect(result.status).toBe('success'); - expect(result.path).toBeDefined(); + expect((result as { path?: string }).path).toBeDefined(); expect(recorder.options).toBeDefined(); }); @@ -294,7 +294,7 @@ describe('React Native Audio API Mocks', () => { const stopResult = recorder.stop(); expect(stopResult.status).toBe('success'); - expect(stopResult.path).toBeDefined(); + expect((stopResult as { path?: string }).path).toBeDefined(); expect(recorder.isRecording()).toBe(false); }); From c9af26714217442d2d4c27c39be784cd6c52908a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20S=C4=99k?= Date: Wed, 28 Jan 2026 15:34:12 +0100 Subject: [PATCH 5/5] feat: docs --- packages/audiodocs/docs/other/testing.mdx | 375 ++++++++++++++++++++++ packages/audiodocs/yarn.lock | 8 +- 2 files changed, 379 insertions(+), 4 deletions(-) create mode 100644 packages/audiodocs/docs/other/testing.mdx 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"