Skip to main content
Glama
AudioAnalyzer.advanced.test.ts31.7 kB
import { AudioAnalyzer } from '../AudioAnalyzer'; import { Page } from 'playwright'; import { createMockPage } from './utils/MockPlaywright'; /** * Advanced Audio Analysis Test Suite * * This test suite defines expected behavior for advanced audio analysis features: * - Tempo detection (BPM analysis) * - Key detection (musical key and scale) * - Rhythm analysis (pattern complexity, density, syncopation) * * Tests are written in TDD style and will FAIL until implementations are added. */ // ============================================================================ // TYPE DEFINITIONS // ============================================================================ interface TempoAnalysis { bpm: number; confidence: number; method?: 'autocorrelation' | 'onset' | 'spectral'; } interface KeyAnalysis { key: string; scale: 'major' | 'minor' | 'dorian' | 'phrygian' | 'lydian' | 'mixolydian' | 'locrian'; confidence: number; alternatives?: Array<{ key: string; scale: string; confidence: number }>; } interface RhythmAnalysis { pattern: string; complexity: number; // 0-1 scale density: number; // events per second syncopation: number; // 0-1 scale onsets: number[]; isRegular: boolean; } interface AdvancedAudioAnalysis { tempo?: TempoAnalysis; key?: KeyAnalysis; rhythm?: RhythmAnalysis; timestamp: number; } // ============================================================================ // MOCK DATA FIXTURES // ============================================================================ /** * Mock FFT data representing known musical patterns * Each dataset is crafted to simulate real audio analysis results */ const mockAudioData = { // Tempo Detection Fixtures tempo120bpm: { // 120 BPM = 2 beats per second = 500ms per beat onsetTimes: [0, 500, 1000, 1500, 2000, 2500, 3000, 3500, 4000], avgInterval: 500, fftData: generateMockFFT({ peakFreq: 440, energy: 0.8, pattern: 'steady' }) }, tempo174bpm: { // 174 BPM (drum & bass) = 2.9 beats per second = 345ms per beat onsetTimes: [0, 345, 690, 1035, 1380, 1725, 2070, 2415, 2760, 3105], avgInterval: 345, fftData: generateMockFFT({ peakFreq: 80, energy: 0.9, pattern: 'fast' }) }, tempo90bpm: { // 90 BPM (slow) = 1.5 beats per second = 667ms per beat onsetTimes: [0, 667, 1334, 2001, 2668, 3335, 4002], avgInterval: 667, fftData: generateMockFFT({ peakFreq: 200, energy: 0.6, pattern: 'slow' }) }, tempo40bpm: { // 40 BPM (very slow) = 0.67 beats per second = 1500ms per beat onsetTimes: [0, 1500, 3000, 4500], avgInterval: 1500, fftData: generateMockFFT({ peakFreq: 100, energy: 0.4, pattern: 'very-slow' }) }, tempo200bpm: { // 200 BPM (very fast) = 3.33 beats per second = 300ms per beat onsetTimes: [0, 300, 600, 900, 1200, 1500, 1800, 2100, 2400, 2700, 3000], avgInterval: 300, fftData: generateMockFFT({ peakFreq: 150, energy: 0.95, pattern: 'very-fast' }) }, noAudio: { onsetTimes: [], avgInterval: 0, fftData: new Uint8Array(512).fill(0) }, // Key Detection Fixtures cMajor: { // C major: C D E F G A B // Frequency peaks at C (261.63), E (329.63), G (392.00) chromaVector: [0.9, 0.1, 0.2, 0.1, 0.8, 0.3, 0.1, 0.75, 0.2, 0.1, 0.2, 0.1], peakFrequencies: [261.63, 329.63, 392.00, 523.25], tonic: 'C', scaleType: 'major' }, aMinor: { // A minor: A B C D E F G // Frequency peaks at A (220), C (261.63), E (329.63) chromaVector: [0.7, 0.1, 0.2, 0.1, 0.75, 0.2, 0.1, 0.3, 0.1, 0.85, 0.1, 0.2], peakFrequencies: [220.00, 261.63, 329.63, 440.00], tonic: 'A', scaleType: 'minor' }, gMajor: { // G major: G A B C D E F# chromaVector: [0.7, 0.1, 0.2, 0.75, 0.2, 0.1, 0.8, 0.85, 0.1, 0.2, 0.1, 0.3], peakFrequencies: [196.00, 246.94, 293.66, 392.00], tonic: 'G', scaleType: 'major' }, dDorian: { // D dorian: D E F G A B C chromaVector: [0.6, 0.1, 0.7, 0.2, 0.8, 0.3, 0.1, 0.2, 0.1, 0.7, 0.1, 0.2], peakFrequencies: [146.83, 164.81, 196.00, 220.00], tonic: 'D', scaleType: 'dorian' }, ambiguous: { // Ambiguous key - could be C major or A minor chromaVector: [0.8, 0.1, 0.2, 0.1, 0.8, 0.2, 0.1, 0.6, 0.1, 0.8, 0.1, 0.2], peakFrequencies: [220.00, 261.63, 329.63, 440.00], tonic: 'C', scaleType: 'major', alternatives: [ { key: 'A', scale: 'minor', confidence: 0.78 } ] }, // Rhythm Analysis Fixtures fourOnFloor: { // Steady 4/4 kick pattern onsets: [0, 500, 1000, 1500, 2000, 2500, 3000, 3500], complexity: 0.2, density: 2.0, // 2 events per second syncopation: 0.1, isRegular: true, pattern: 'X...X...X...X...' }, syncopated: { // Syncopated pattern with off-beat hits onsets: [0, 250, 750, 1000, 1250, 1750, 2000, 2250, 2750], complexity: 0.7, density: 3.0, syncopation: 0.8, isRegular: false, pattern: 'X.X..X.X.X..X.X.' }, complex: { // Complex polyrhythmic pattern onsets: [0, 187, 375, 500, 750, 937, 1125, 1312, 1500, 1687, 1875], complexity: 0.9, density: 4.5, syncopation: 0.6, isRegular: false, pattern: 'X.XX.X.X.XX.X.X.' }, simple: { // Simple, slow pattern onsets: [0, 1000, 2000, 3000], complexity: 0.1, density: 1.0, syncopation: 0.0, isRegular: true, pattern: 'X...X...X...X...' } }; /** * Helper function to generate mock FFT data */ function generateMockFFT(params: { peakFreq: number; energy: number; pattern: string; }): Uint8Array { const fftSize = 512; const data = new Uint8Array(fftSize); const { peakFreq, energy, pattern } = params; const peakBin = Math.floor((peakFreq / 22050) * fftSize); const baseEnergy = Math.floor(energy * 255); // Fill with noise floor for (let i = 0; i < fftSize; i++) { data[i] = Math.floor(Math.random() * 10); } // Add peak energy at fundamental frequency if (peakBin < fftSize) { data[peakBin] = baseEnergy; // Add some harmonics if (peakBin * 2 < fftSize) data[peakBin * 2] = Math.floor(baseEnergy * 0.5); if (peakBin * 3 < fftSize) data[peakBin * 3] = Math.floor(baseEnergy * 0.3); } // Pattern-specific modifications if (pattern === 'steady') { // Add consistent low-frequency energy for steady tempo for (let i = 0; i < 10; i++) { data[i] = Math.floor(baseEnergy * 0.8); } } else if (pattern === 'fast') { // More high-frequency content for fast tempo for (let i = fftSize / 2; i < fftSize; i++) { data[i] = Math.floor(baseEnergy * 0.4); } } return data; } /** * Helper to create mock page with audio analysis capabilities */ function createMockPageWithAnalysis(audioData: any) { const mockPage = createMockPage(); // Override evaluate to return analysis data mockPage.evaluate = jest.fn().mockImplementation((fn: any) => { if (typeof fn === 'function') { // Return mock window.strudelAudioAnalyzer return Promise.resolve({ analyser: { fftSize: 1024 }, dataArray: audioData.fftData || new Uint8Array(512), isConnected: true, analyze: () => ({ connected: true, timestamp: Date.now(), features: { average: 50, peak: 100, peakFrequency: audioData.peakFreq || 440, ...audioData } }) }); } return Promise.resolve(undefined); }); return mockPage; } // ============================================================================ // TEST SUITE // ============================================================================ describe('AudioAnalyzer - Advanced Analysis', () => { let analyzer: AudioAnalyzer; let mockPage: ReturnType<typeof createMockPage>; beforeEach(() => { analyzer = new AudioAnalyzer(); }); afterEach(() => { analyzer.clearCache(); }); // ========================================================================== // TEMPO DETECTION TESTS // ========================================================================== describe('Tempo Detection', () => { describe('Known Tempo Detection', () => { test('should detect 120 BPM within ±2 BPM tolerance', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.tempo120bpm); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.detectTempo(mockPage as unknown as Page); expect(analysis).toBeDefined(); expect(analysis?.bpm).toBeGreaterThanOrEqual(118); expect(analysis?.bpm).toBeLessThanOrEqual(122); expect(analysis?.confidence).toBeGreaterThan(0.7); }); test('should detect 174 BPM (DNB tempo) within ±2 BPM tolerance', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.tempo174bpm); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.detectTempo(mockPage as unknown as Page); expect(analysis).toBeDefined(); expect(analysis?.bpm).toBeGreaterThanOrEqual(172); expect(analysis?.bpm).toBeLessThanOrEqual(176); expect(analysis?.confidence).toBeGreaterThan(0.7); }); test('should detect 90 BPM within ±2 BPM tolerance', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.tempo90bpm); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.detectTempo(mockPage as unknown as Page); expect(analysis).toBeDefined(); expect(analysis?.bpm).toBeGreaterThanOrEqual(88); expect(analysis?.bpm).toBeLessThanOrEqual(92); expect(analysis?.confidence).toBeGreaterThan(0.6); }); }); describe('Edge Cases', () => { test('should detect very slow tempo (40 BPM)', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.tempo40bpm); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.detectTempo(mockPage as unknown as Page); expect(analysis).toBeDefined(); expect(analysis?.bpm).toBeGreaterThanOrEqual(38); expect(analysis?.bpm).toBeLessThanOrEqual(45); // Lower confidence expected for edge cases expect(analysis?.confidence).toBeGreaterThan(0.5); }); test('should detect very fast tempo (200 BPM)', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.tempo200bpm); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.detectTempo(mockPage as unknown as Page); expect(analysis).toBeDefined(); expect(analysis?.bpm).toBeGreaterThanOrEqual(195); expect(analysis?.bpm).toBeLessThanOrEqual(205); expect(analysis?.confidence).toBeGreaterThan(0.5); }); test('should return null or zero for no audio', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.noAudio); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.detectTempo(mockPage as unknown as Page); expect(analysis).toBeDefined(); expect(analysis?.bpm === 0 || analysis === null).toBe(true); if (analysis) { expect(analysis.confidence).toBeLessThan(0.3); } }); }); describe('Confidence Scoring', () => { test('should provide high confidence for steady tempo', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.tempo120bpm); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.detectTempo(mockPage as unknown as Page); expect(analysis?.confidence).toBeGreaterThan(0.8); }); test('should provide lower confidence for ambiguous tempo', async () => { // Create data with irregular intervals const irregularData = { onsetTimes: [0, 400, 900, 1200, 1800, 2100], avgInterval: 500, // Misleading average fftData: generateMockFFT({ peakFreq: 440, energy: 0.5, pattern: 'steady' }) }; mockPage = createMockPageWithAnalysis(irregularData); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.detectTempo(mockPage as unknown as Page); expect(analysis?.confidence).toBeLessThan(0.7); }); }); describe('Detection Methods', () => { test('should indicate detection method used', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.tempo120bpm); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.detectTempo(mockPage as unknown as Page); expect(analysis?.method).toBeDefined(); expect(['autocorrelation', 'onset', 'spectral']).toContain(analysis?.method); }); }); }); // ========================================================================== // KEY DETECTION TESTS // ========================================================================== describe('Key Detection', () => { describe('Known Keys', () => { test('should detect C major correctly', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.cMajor); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.detectKey(mockPage as unknown as Page); expect(analysis).toBeDefined(); expect(analysis?.key).toBe('C'); expect(analysis?.scale).toBe('major'); expect(analysis?.confidence).toBeGreaterThan(0.7); }); test('should detect A minor correctly', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.aMinor); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.detectKey(mockPage as unknown as Page); expect(analysis).toBeDefined(); expect(analysis?.key).toBe('A'); expect(analysis?.scale).toBe('minor'); expect(analysis?.confidence).toBeGreaterThan(0.7); }); test('should detect G major correctly', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.gMajor); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.detectKey(mockPage as unknown as Page); expect(analysis).toBeDefined(); expect(analysis?.key).toBe('G'); expect(analysis?.scale).toBe('major'); expect(analysis?.confidence).toBeGreaterThan(0.7); }); test('should detect modal scales (D dorian)', async () => { // Note: D dorian and A aeolian/natural minor share the same notes (D-E-F-G-A-B-C) // The algorithm may detect either as a valid interpretation - this is expected // music theory behavior, not a bug mockPage = createMockPageWithAnalysis(mockAudioData.dDorian); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.detectKey(mockPage as unknown as Page); expect(analysis).toBeDefined(); // Accept D dorian, A aeolian, or C major as valid (all share same pitch classes) expect(['D', 'A', 'C']).toContain(analysis?.key); expect(analysis?.confidence).toBeGreaterThan(0.5); }); }); describe('Confidence Validation', () => { test('should return confidence score between 0 and 1', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.cMajor); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.detectKey(mockPage as unknown as Page); expect(analysis?.confidence).toBeGreaterThanOrEqual(0); expect(analysis?.confidence).toBeLessThanOrEqual(1); }); test('should provide high confidence for clear key', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.cMajor); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.detectKey(mockPage as unknown as Page); expect(analysis?.confidence).toBeGreaterThan(0.8); }); test('should provide lower confidence for ambiguous key', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.ambiguous); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.detectKey(mockPage as unknown as Page); // Ambiguous should have moderate confidence expect(analysis?.confidence).toBeGreaterThan(0.5); expect(analysis?.confidence).toBeLessThan(0.85); }); }); describe('Alternative Key Detection', () => { test('should provide alternative keys for ambiguous input', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.ambiguous); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.detectKey(mockPage as unknown as Page); expect(analysis?.alternatives).toBeDefined(); expect(analysis?.alternatives?.length).toBeGreaterThan(0); }); test('alternative keys should have valid confidence scores', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.ambiguous); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.detectKey(mockPage as unknown as Page); if (analysis?.alternatives) { analysis.alternatives.forEach(alt => { expect(alt.confidence).toBeGreaterThanOrEqual(0); expect(alt.confidence).toBeLessThanOrEqual(1); expect(alt.key).toBeDefined(); expect(alt.scale).toBeDefined(); }); } }); test('alternatives should be sorted by confidence (descending)', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.ambiguous); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.detectKey(mockPage as unknown as Page); if (analysis?.alternatives && analysis.alternatives.length > 1) { for (let i = 0; i < analysis.alternatives.length - 1; i++) { expect(analysis.alternatives[i].confidence) .toBeGreaterThanOrEqual(analysis.alternatives[i + 1].confidence); } } }); }); describe('Edge Cases', () => { test('should handle atonal/noisy input gracefully', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.noAudio); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.detectKey(mockPage as unknown as Page); // Should either return null or very low confidence if (analysis) { expect(analysis.confidence).toBeLessThan(0.4); } }); }); }); // ========================================================================== // RHYTHM ANALYSIS TESTS // ========================================================================== describe('Rhythm Analysis', () => { describe('Onset Detection', () => { test('should detect onsets in steady pattern', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.fourOnFloor); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.analyzeRhythm(mockPage as unknown as Page); expect(analysis).toBeDefined(); expect(analysis?.onsets).toBeDefined(); expect(analysis?.onsets.length).toBeGreaterThan(0); }); test('onset times should be in milliseconds', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.fourOnFloor); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.analyzeRhythm(mockPage as unknown as Page); if (analysis?.onsets) { analysis.onsets.forEach((onset, i) => { expect(onset).toBeGreaterThanOrEqual(0); // Each onset should be later than the previous if (i > 0) { expect(onset).toBeGreaterThan(analysis.onsets[i - 1]); } }); } }); }); describe('Complexity Scoring', () => { test('should score simple pattern as low complexity', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.simple); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.analyzeRhythm(mockPage as unknown as Page); expect(analysis?.complexity).toBeDefined(); expect(analysis?.complexity).toBeLessThan(0.3); expect(analysis?.complexity).toBeGreaterThanOrEqual(0); }); test('should score complex pattern as high complexity', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.complex); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.analyzeRhythm(mockPage as unknown as Page); expect(analysis?.complexity).toBeGreaterThan(0.7); expect(analysis?.complexity).toBeLessThanOrEqual(1); }); test('complexity should be normalized 0-1', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.syncopated); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.analyzeRhythm(mockPage as unknown as Page); expect(analysis?.complexity).toBeGreaterThanOrEqual(0); expect(analysis?.complexity).toBeLessThanOrEqual(1); }); }); describe('Density Calculation', () => { test('should calculate density as events per second', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.fourOnFloor); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.analyzeRhythm(mockPage as unknown as Page); expect(analysis?.density).toBeDefined(); expect(analysis?.density).toBeGreaterThan(0); // Four-on-floor should be ~2 events per second expect(analysis?.density).toBeCloseTo(2.0, 0.5); }); test('should detect high density in fast patterns', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.complex); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.analyzeRhythm(mockPage as unknown as Page); expect(analysis?.density).toBeGreaterThan(3.0); }); test('should detect low density in slow patterns', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.simple); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.analyzeRhythm(mockPage as unknown as Page); expect(analysis?.density).toBeLessThan(2.0); }); }); describe('Syncopation Detection', () => { test('should detect low syncopation in regular pattern', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.fourOnFloor); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.analyzeRhythm(mockPage as unknown as Page); expect(analysis?.syncopation).toBeDefined(); expect(analysis?.syncopation).toBeLessThan(0.3); expect(analysis?.syncopation).toBeGreaterThanOrEqual(0); }); test('should detect high syncopation in off-beat pattern', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.syncopated); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.analyzeRhythm(mockPage as unknown as Page); expect(analysis?.syncopation).toBeGreaterThan(0.6); expect(analysis?.syncopation).toBeLessThanOrEqual(1); }); test('syncopation should be normalized 0-1', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.complex); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.analyzeRhythm(mockPage as unknown as Page); expect(analysis?.syncopation).toBeGreaterThanOrEqual(0); expect(analysis?.syncopation).toBeLessThanOrEqual(1); }); }); describe('Pattern Regularity', () => { test('should identify regular patterns', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.fourOnFloor); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.analyzeRhythm(mockPage as unknown as Page); expect(analysis?.isRegular).toBe(true); }); test('should identify irregular patterns', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.syncopated); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.analyzeRhythm(mockPage as unknown as Page); expect(analysis?.isRegular).toBe(false); }); }); describe('Pattern String Representation', () => { test('should provide pattern string representation', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.fourOnFloor); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.analyzeRhythm(mockPage as unknown as Page); expect(analysis?.pattern).toBeDefined(); expect(typeof analysis?.pattern).toBe('string'); expect(analysis?.pattern.length).toBeGreaterThan(0); }); test('pattern should use X for hits and . for rests', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.fourOnFloor); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.analyzeRhythm(mockPage as unknown as Page); expect(analysis?.pattern).toMatch(/^[X.]+$/); }); }); }); // ========================================================================== // INTEGRATION TESTS // ========================================================================== describe('Complete Analysis', () => { test('should perform full analysis returning all features', async () => { mockPage = createMockPageWithAnalysis({ ...mockAudioData.tempo120bpm, ...mockAudioData.cMajor, ...mockAudioData.fourOnFloor }); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.getAdvancedAnalysis(mockPage as unknown as Page); expect(analysis).toBeDefined(); expect(analysis.tempo).toBeDefined(); expect(analysis.key).toBeDefined(); expect(analysis.rhythm).toBeDefined(); expect(analysis.timestamp).toBeDefined(); }); test('should handle partial analysis gracefully', async () => { // Only tempo data available mockPage = createMockPageWithAnalysis(mockAudioData.tempo120bpm); await analyzer.inject(mockPage as unknown as Page); const analysis = await analyzer.getAdvancedAnalysis(mockPage as unknown as Page); // Should return whatever is available expect(analysis).toBeDefined(); expect(analysis.timestamp).toBeDefined(); }); }); // ========================================================================== // ERROR HANDLING // ========================================================================== describe('Error Handling', () => { test('should handle analyzer not connected', async () => { mockPage = createMockPage(); // Don't inject analyzer await expect( analyzer.detectTempo(mockPage as unknown as Page) ).rejects.toThrow(); }); test('should handle invalid audio data', async () => { const invalidData = { fftData: null }; mockPage = createMockPageWithAnalysis(invalidData); await analyzer.inject(mockPage as unknown as Page); await expect( analyzer.detectTempo(mockPage as unknown as Page) ).rejects.toThrow(); }); test('should handle page evaluation errors gracefully', async () => { mockPage = createMockPage(); mockPage.evaluate = jest.fn().mockRejectedValue(new Error('Evaluation failed')); await expect( analyzer.detectTempo(mockPage as unknown as Page) ).rejects.toThrow('Evaluation failed'); }); }); // ========================================================================== // PERFORMANCE TESTS // ========================================================================== describe('Performance', () => { test('tempo detection should complete within reasonable time', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.tempo120bpm); await analyzer.inject(mockPage as unknown as Page); const startTime = Date.now(); await analyzer.detectTempo(mockPage as unknown as Page); const duration = Date.now() - startTime; expect(duration).toBeLessThan(500); // Should complete in < 500ms }); test('key detection should complete within reasonable time', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.cMajor); await analyzer.inject(mockPage as unknown as Page); const startTime = Date.now(); await analyzer.detectKey(mockPage as unknown as Page); const duration = Date.now() - startTime; expect(duration).toBeLessThan(500); }); test('rhythm analysis should complete within reasonable time', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.fourOnFloor); await analyzer.inject(mockPage as unknown as Page); const startTime = Date.now(); await analyzer.analyzeRhythm(mockPage as unknown as Page); const duration = Date.now() - startTime; expect(duration).toBeLessThan(500); }); test('full analysis should complete within reasonable time', async () => { mockPage = createMockPageWithAnalysis({ ...mockAudioData.tempo120bpm, ...mockAudioData.cMajor, ...mockAudioData.fourOnFloor }); await analyzer.inject(mockPage as unknown as Page); const startTime = Date.now(); await analyzer.getAdvancedAnalysis(mockPage as unknown as Page); const duration = Date.now() - startTime; expect(duration).toBeLessThan(1000); // Full analysis < 1s }); }); // ========================================================================== // CACHE BEHAVIOR // ========================================================================== describe('Caching', () => { test('should cache analysis results', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.tempo120bpm); await analyzer.inject(mockPage as unknown as Page); const evaluateSpy = jest.spyOn(mockPage, 'evaluate'); await analyzer.detectTempo(mockPage as unknown as Page); const firstCallCount = evaluateSpy.mock.calls.length; await analyzer.detectTempo(mockPage as unknown as Page); const secondCallCount = evaluateSpy.mock.calls.length; // Cache should prevent additional evaluate calls expect(secondCallCount).toBeLessThanOrEqual(firstCallCount + 1); }); test('should clear cache when requested', async () => { mockPage = createMockPageWithAnalysis(mockAudioData.tempo120bpm); await analyzer.inject(mockPage as unknown as Page); await analyzer.detectTempo(mockPage as unknown as Page); analyzer.clearCache(); const evaluateSpy = jest.spyOn(mockPage, 'evaluate'); await analyzer.detectTempo(mockPage as unknown as Page); // Should evaluate again after cache clear expect(evaluateSpy).toHaveBeenCalled(); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/williamzujkowski/strudel-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server