feat: add R as shortcut of recording button; fixed linter errors.
This commit is contained in:
+113
-113
@@ -1,186 +1,186 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
beatsToBar,
|
||||
pitchToNoteNameString,
|
||||
pitchToNoteName,
|
||||
pianoRollIndexToPitch,
|
||||
noteNameToPitch
|
||||
} from './midiUtil'
|
||||
} from './midiUtil';
|
||||
|
||||
describe('midiUtil', () => {
|
||||
describe('beatsToBar', () => {
|
||||
it('should convert beats to bar position object', () => {
|
||||
const result1 = beatsToBar(0, { numerator: 4, denominator: 4 })
|
||||
expect(result1.bar).toBe(0)
|
||||
expect(result1.beatInBar).toBe(0)
|
||||
const result1 = beatsToBar(0, { numerator: 4, denominator: 4 });
|
||||
expect(result1.bar).toBe(0);
|
||||
expect(result1.beatInBar).toBe(0);
|
||||
|
||||
const result2 = beatsToBar(4, { numerator: 4, denominator: 4 })
|
||||
expect(result2.bar).toBe(1)
|
||||
expect(result2.beatInBar).toBe(0)
|
||||
const result2 = beatsToBar(4, { numerator: 4, denominator: 4 });
|
||||
expect(result2.bar).toBe(1);
|
||||
expect(result2.beatInBar).toBe(0);
|
||||
|
||||
const result3 = beatsToBar(8, { numerator: 4, denominator: 4 })
|
||||
expect(result3.bar).toBe(2)
|
||||
expect(result3.beatInBar).toBe(0)
|
||||
})
|
||||
const result3 = beatsToBar(8, { numerator: 4, denominator: 4 });
|
||||
expect(result3.bar).toBe(2);
|
||||
expect(result3.beatInBar).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle different time signatures', () => {
|
||||
const result1 = beatsToBar(0, { numerator: 3, denominator: 4 })
|
||||
expect(result1.bar).toBe(0)
|
||||
expect(result1.beatInBar).toBe(0)
|
||||
const result1 = beatsToBar(0, { numerator: 3, denominator: 4 });
|
||||
expect(result1.bar).toBe(0);
|
||||
expect(result1.beatInBar).toBe(0);
|
||||
|
||||
const result2 = beatsToBar(3, { numerator: 3, denominator: 4 })
|
||||
expect(result2.bar).toBe(1)
|
||||
expect(result2.beatInBar).toBe(0)
|
||||
})
|
||||
const result2 = beatsToBar(3, { numerator: 3, denominator: 4 });
|
||||
expect(result2.bar).toBe(1);
|
||||
expect(result2.beatInBar).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle fractional beats', () => {
|
||||
const result1 = beatsToBar(2.5, { numerator: 4, denominator: 4 })
|
||||
expect(result1.bar).toBe(0)
|
||||
expect(result1.beatInBar).toBe(2.5)
|
||||
const result1 = beatsToBar(2.5, { numerator: 4, denominator: 4 });
|
||||
expect(result1.bar).toBe(0);
|
||||
expect(result1.beatInBar).toBe(2.5);
|
||||
|
||||
const result2 = beatsToBar(4.5, { numerator: 4, denominator: 4 })
|
||||
expect(result2.bar).toBe(1)
|
||||
expect(result2.beatInBar).toBe(0.5)
|
||||
})
|
||||
})
|
||||
const result2 = beatsToBar(4.5, { numerator: 4, denominator: 4 });
|
||||
expect(result2.bar).toBe(1);
|
||||
expect(result2.beatInBar).toBe(0.5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pitchToNoteNameString', () => {
|
||||
it('should convert MIDI pitch to note name with octave', () => {
|
||||
expect(pitchToNoteNameString(60)).toBe('C4') // Middle C
|
||||
expect(pitchToNoteNameString(61)).toBe('C#4') // C# above middle C
|
||||
expect(pitchToNoteNameString(59)).toBe('B3') // B below middle C
|
||||
expect(pitchToNoteNameString(72)).toBe('C5') // C one octave above middle C
|
||||
expect(pitchToNoteNameString(48)).toBe('C3') // C one octave below middle C
|
||||
})
|
||||
expect(pitchToNoteNameString(60)).toBe('C4'); // Middle C
|
||||
expect(pitchToNoteNameString(61)).toBe('C#4'); // C# above middle C
|
||||
expect(pitchToNoteNameString(59)).toBe('B3'); // B below middle C
|
||||
expect(pitchToNoteNameString(72)).toBe('C5'); // C one octave above middle C
|
||||
expect(pitchToNoteNameString(48)).toBe('C3'); // C one octave below middle C
|
||||
});
|
||||
|
||||
it('should handle edge cases', () => {
|
||||
expect(pitchToNoteNameString(0)).toBe('C-1') // Lowest MIDI note
|
||||
expect(pitchToNoteNameString(127)).toBe('G9') // Highest MIDI note
|
||||
})
|
||||
expect(pitchToNoteNameString(0)).toBe('C-1'); // Lowest MIDI note
|
||||
expect(pitchToNoteNameString(127)).toBe('G9'); // Highest MIDI note
|
||||
});
|
||||
|
||||
it('should handle all chromatic notes', () => {
|
||||
const expectedNotes = ['C4', 'C#4', 'D4', 'D#4', 'E4', 'F4', 'F#4', 'G4', 'G#4', 'A4', 'A#4', 'B4']
|
||||
const expectedNotes = ['C4', 'C#4', 'D4', 'D#4', 'E4', 'F4', 'F#4', 'G4', 'G#4', 'A4', 'A#4', 'B4'];
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
expect(pitchToNoteNameString(60 + i)).toBe(expectedNotes[i])
|
||||
expect(pitchToNoteNameString(60 + i)).toBe(expectedNotes[i]);
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
describe('pitchToNoteName', () => {
|
||||
it('should convert pitch to note name object', () => {
|
||||
const result60 = pitchToNoteName(60) // Middle C
|
||||
expect(result60.note).toBe('C')
|
||||
expect(result60.octave).toBe(4)
|
||||
const result60 = pitchToNoteName(60); // Middle C
|
||||
expect(result60.note).toBe('C');
|
||||
expect(result60.octave).toBe(4);
|
||||
|
||||
const result61 = pitchToNoteName(61) // C#
|
||||
expect(result61.note).toBe('C#')
|
||||
expect(result61.octave).toBe(4)
|
||||
})
|
||||
const result61 = pitchToNoteName(61); // C#
|
||||
expect(result61.note).toBe('C#');
|
||||
expect(result61.octave).toBe(4);
|
||||
});
|
||||
|
||||
it('should wrap around for different octaves', () => {
|
||||
const result60 = pitchToNoteName(60)
|
||||
const result72 = pitchToNoteName(72)
|
||||
const result84 = pitchToNoteName(84)
|
||||
const result60 = pitchToNoteName(60);
|
||||
const result72 = pitchToNoteName(72);
|
||||
const result84 = pitchToNoteName(84);
|
||||
|
||||
expect(result60.note).toBe('C')
|
||||
expect(result72.note).toBe('C')
|
||||
expect(result84.note).toBe('C')
|
||||
expect(result60.note).toBe('C');
|
||||
expect(result72.note).toBe('C');
|
||||
expect(result84.note).toBe('C');
|
||||
|
||||
expect(result60.octave).toBe(4)
|
||||
expect(result72.octave).toBe(5)
|
||||
expect(result84.octave).toBe(6)
|
||||
})
|
||||
})
|
||||
expect(result60.octave).toBe(4);
|
||||
expect(result72.octave).toBe(5);
|
||||
expect(result84.octave).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pianoRollIndexToPitch', () => {
|
||||
it('should convert piano roll row index to MIDI pitch', () => {
|
||||
// This function likely maps visual rows to MIDI pitches
|
||||
// The exact mapping depends on your implementation
|
||||
const result = pianoRollIndexToPitch(10)
|
||||
expect(typeof result).toBe('number')
|
||||
expect(result).toBeGreaterThanOrEqual(0)
|
||||
expect(result).toBeLessThanOrEqual(127)
|
||||
})
|
||||
const result = pianoRollIndexToPitch(10);
|
||||
expect(typeof result).toBe('number');
|
||||
expect(result).toBeGreaterThanOrEqual(0);
|
||||
expect(result).toBeLessThanOrEqual(127);
|
||||
});
|
||||
|
||||
it('should return different pitches for different indices', () => {
|
||||
const pitch1 = pianoRollIndexToPitch(0)
|
||||
const pitch2 = pianoRollIndexToPitch(1)
|
||||
expect(pitch1).not.toBe(pitch2)
|
||||
})
|
||||
})
|
||||
const pitch1 = pianoRollIndexToPitch(0);
|
||||
const pitch2 = pianoRollIndexToPitch(1);
|
||||
expect(pitch1).not.toBe(pitch2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('noteNameToPitch', () => {
|
||||
it('should convert note names to MIDI pitch', () => {
|
||||
expect(noteNameToPitch('C4')).toBe(60) // Middle C
|
||||
expect(noteNameToPitch('C#4')).toBe(61) // C# above middle C
|
||||
expect(noteNameToPitch('D4')).toBe(62) // D above middle C
|
||||
})
|
||||
expect(noteNameToPitch('C4')).toBe(60); // Middle C
|
||||
expect(noteNameToPitch('C#4')).toBe(61); // C# above middle C
|
||||
expect(noteNameToPitch('D4')).toBe(62); // D above middle C
|
||||
});
|
||||
|
||||
it('should handle different octaves', () => {
|
||||
expect(noteNameToPitch('C3')).toBe(48) // C below middle C
|
||||
expect(noteNameToPitch('C5')).toBe(72) // C above middle C
|
||||
})
|
||||
expect(noteNameToPitch('C3')).toBe(48); // C below middle C
|
||||
expect(noteNameToPitch('C5')).toBe(72); // C above middle C
|
||||
});
|
||||
|
||||
it('should handle sharps', () => {
|
||||
expect(noteNameToPitch('C#4')).toBe(61)
|
||||
expect(noteNameToPitch('F#4')).toBe(66)
|
||||
expect(noteNameToPitch('G#4')).toBe(68)
|
||||
})
|
||||
expect(noteNameToPitch('C#4')).toBe(61);
|
||||
expect(noteNameToPitch('F#4')).toBe(66);
|
||||
expect(noteNameToPitch('G#4')).toBe(68);
|
||||
});
|
||||
|
||||
it('should handle invalid note names', () => {
|
||||
expect(() => noteNameToPitch('Db4')).toThrow('Invalid note name: Db4') // Flats not supported
|
||||
expect(() => noteNameToPitch('H4')).toThrow('Invalid note name: H4') // Invalid note
|
||||
expect(() => noteNameToPitch('C')).toThrow('Invalid note name: C') // Missing octave
|
||||
})
|
||||
})
|
||||
expect(() => noteNameToPitch('Db4')).toThrow('Invalid note name: Db4'); // Flats not supported
|
||||
expect(() => noteNameToPitch('H4')).toThrow('Invalid note name: H4'); // Invalid note
|
||||
expect(() => noteNameToPitch('C')).toThrow('Invalid note name: C'); // Missing octave
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases and error handling', () => {
|
||||
it('should handle negative values gracefully', () => {
|
||||
expect(() => pitchToNoteNameString(-1)).not.toThrow()
|
||||
expect(() => beatsToBar(-1, { numerator: 4, denominator: 4 })).not.toThrow()
|
||||
})
|
||||
expect(() => pitchToNoteNameString(-1)).not.toThrow();
|
||||
expect(() => beatsToBar(-1, { numerator: 4, denominator: 4 })).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle very large values', () => {
|
||||
expect(() => pitchToNoteNameString(200)).not.toThrow()
|
||||
expect(() => beatsToBar(1000, { numerator: 4, denominator: 4 })).not.toThrow()
|
||||
})
|
||||
expect(() => pitchToNoteNameString(200)).not.toThrow();
|
||||
expect(() => beatsToBar(1000, { numerator: 4, denominator: 4 })).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle zero values', () => {
|
||||
expect(pitchToNoteNameString(0)).toBeDefined()
|
||||
expect(beatsToBar(0, { numerator: 4, denominator: 4 })).toBeDefined()
|
||||
})
|
||||
})
|
||||
expect(pitchToNoteNameString(0)).toBeDefined();
|
||||
expect(beatsToBar(0, { numerator: 4, denominator: 4 })).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mathematical consistency', () => {
|
||||
it('should maintain pitch relationships', () => {
|
||||
// One octave = 12 semitones - note names should be the same
|
||||
const baseNote = pitchToNoteName(60)
|
||||
const octaveNote = pitchToNoteName(72)
|
||||
expect(baseNote.note).toBe(octaveNote.note) // Both should be 'C'
|
||||
expect(octaveNote.octave).toBe(baseNote.octave + 1) // Octave should be one higher
|
||||
})
|
||||
const baseNote = pitchToNoteName(60);
|
||||
const octaveNote = pitchToNoteName(72);
|
||||
expect(baseNote.note).toBe(octaveNote.note); // Both should be 'C'
|
||||
expect(octaveNote.octave).toBe(baseNote.octave + 1); // Octave should be one higher
|
||||
});
|
||||
|
||||
it('should maintain beat-to-bar relationships', () => {
|
||||
const timeSignature = { numerator: 4, denominator: 4 }
|
||||
const timeSignature = { numerator: 4, denominator: 4 };
|
||||
|
||||
// Should increment bar by 1 for each complete measure
|
||||
for (let beat = 0; beat < 20; beat += 4) {
|
||||
const expectedBar = Math.floor(beat / 4)
|
||||
const result = beatsToBar(beat, timeSignature)
|
||||
expect(result.bar).toBe(expectedBar)
|
||||
expect(result.beatInBar).toBe(0) // Should be at start of bar
|
||||
const expectedBar = Math.floor(beat / 4);
|
||||
const result = beatsToBar(beat, timeSignature);
|
||||
expect(result.bar).toBe(expectedBar);
|
||||
expect(result.beatInBar).toBe(0); // Should be at start of bar
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
it('should maintain note name to pitch conversion consistency', () => {
|
||||
// Converting pitch to note name and back should be consistent
|
||||
const originalPitch = 60
|
||||
const noteObj = pitchToNoteName(originalPitch)
|
||||
const noteName = `${noteObj.note}${noteObj.octave}`
|
||||
const convertedPitch = noteNameToPitch(noteName)
|
||||
const originalPitch = 60;
|
||||
const noteObj = pitchToNoteName(originalPitch);
|
||||
const noteName = `${noteObj.note}${noteObj.octave}`;
|
||||
const convertedPitch = noteNameToPitch(noteName);
|
||||
|
||||
expect(convertedPitch).toBe(originalPitch)
|
||||
})
|
||||
})
|
||||
})
|
||||
expect(convertedPitch).toBe(originalPitch);
|
||||
});
|
||||
});
|
||||
});
|
||||
+292
-292
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import {
|
||||
getRootNoteFromKeySignature,
|
||||
noteNameToPitchClass,
|
||||
@@ -9,208 +9,208 @@ import {
|
||||
getMatchingChordsForPitch,
|
||||
generatePianoGridBackground,
|
||||
validateFunctionalChordsJSON
|
||||
} from './scaleUtil'
|
||||
import { KGCore } from '../core/KGCore'
|
||||
import type { KeySignature } from '../core/KGProject'
|
||||
import functionalChordsData from '../../public/resources/modes/functional_chords.json'
|
||||
} from './scaleUtil';
|
||||
import { KGCore } from '../core/KGCore';
|
||||
import type { KeySignature } from '../core/KGProject';
|
||||
import functionalChordsData from '../../public/resources/modes/functional_chords.json';
|
||||
|
||||
describe('scaleUtil', () => {
|
||||
// Setup: Mock KGCore with real chord data
|
||||
beforeEach(() => {
|
||||
// Use real functional chords data from JSON file (includes name, steps, and chord data)
|
||||
KGCore.FUNCTIONAL_CHORDS_DATA = functionalChordsData
|
||||
})
|
||||
KGCore.FUNCTIONAL_CHORDS_DATA = functionalChordsData;
|
||||
});
|
||||
|
||||
describe('getRootNoteFromKeySignature', () => {
|
||||
it('should extract root note from C major', () => {
|
||||
expect(getRootNoteFromKeySignature('C major')).toBe('C')
|
||||
})
|
||||
expect(getRootNoteFromKeySignature('C major')).toBe('C');
|
||||
});
|
||||
|
||||
it('should extract root note from F# minor', () => {
|
||||
expect(getRootNoteFromKeySignature('F# minor')).toBe('F#')
|
||||
})
|
||||
expect(getRootNoteFromKeySignature('F# minor')).toBe('F#');
|
||||
});
|
||||
|
||||
it('should extract root note from Bb major', () => {
|
||||
expect(getRootNoteFromKeySignature('Bb major')).toBe('Bb')
|
||||
})
|
||||
expect(getRootNoteFromKeySignature('Bb major')).toBe('Bb');
|
||||
});
|
||||
|
||||
it('should handle Db major', () => {
|
||||
expect(getRootNoteFromKeySignature('Db major')).toBe('Db')
|
||||
})
|
||||
expect(getRootNoteFromKeySignature('Db major')).toBe('Db');
|
||||
});
|
||||
|
||||
it('should default to C for invalid format', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
expect(getRootNoteFromKeySignature('Invalid' as KeySignature)).toBe('C')
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid key signature format'))
|
||||
warnSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
expect(getRootNoteFromKeySignature('Invalid' as KeySignature)).toBe('C');
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid key signature format'));
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('noteNameToPitchClass', () => {
|
||||
it('should convert C to 0', () => {
|
||||
expect(noteNameToPitchClass('C')).toBe(0)
|
||||
})
|
||||
expect(noteNameToPitchClass('C')).toBe(0);
|
||||
});
|
||||
|
||||
it('should convert C# to 1', () => {
|
||||
expect(noteNameToPitchClass('C#')).toBe(1)
|
||||
})
|
||||
expect(noteNameToPitchClass('C#')).toBe(1);
|
||||
});
|
||||
|
||||
it('should convert Db to 1', () => {
|
||||
expect(noteNameToPitchClass('Db')).toBe(1)
|
||||
})
|
||||
expect(noteNameToPitchClass('Db')).toBe(1);
|
||||
});
|
||||
|
||||
it('should convert D to 2', () => {
|
||||
expect(noteNameToPitchClass('D')).toBe(2)
|
||||
})
|
||||
expect(noteNameToPitchClass('D')).toBe(2);
|
||||
});
|
||||
|
||||
it('should convert E to 4', () => {
|
||||
expect(noteNameToPitchClass('E')).toBe(4)
|
||||
})
|
||||
expect(noteNameToPitchClass('E')).toBe(4);
|
||||
});
|
||||
|
||||
it('should convert F to 5', () => {
|
||||
expect(noteNameToPitchClass('F')).toBe(5)
|
||||
})
|
||||
expect(noteNameToPitchClass('F')).toBe(5);
|
||||
});
|
||||
|
||||
it('should convert F# to 6', () => {
|
||||
expect(noteNameToPitchClass('F#')).toBe(6)
|
||||
})
|
||||
expect(noteNameToPitchClass('F#')).toBe(6);
|
||||
});
|
||||
|
||||
it('should convert G to 7', () => {
|
||||
expect(noteNameToPitchClass('G')).toBe(7)
|
||||
})
|
||||
expect(noteNameToPitchClass('G')).toBe(7);
|
||||
});
|
||||
|
||||
it('should convert A to 9', () => {
|
||||
expect(noteNameToPitchClass('A')).toBe(9)
|
||||
})
|
||||
expect(noteNameToPitchClass('A')).toBe(9);
|
||||
});
|
||||
|
||||
it('should convert Bb to 10', () => {
|
||||
expect(noteNameToPitchClass('Bb')).toBe(10)
|
||||
})
|
||||
expect(noteNameToPitchClass('Bb')).toBe(10);
|
||||
});
|
||||
|
||||
it('should convert B to 11', () => {
|
||||
expect(noteNameToPitchClass('B')).toBe(11)
|
||||
})
|
||||
expect(noteNameToPitchClass('B')).toBe(11);
|
||||
});
|
||||
|
||||
it('should default to 0 for invalid note', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
expect(noteNameToPitchClass('X')).toBe(0)
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid note name'))
|
||||
warnSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
expect(noteNameToPitchClass('X')).toBe(0);
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid note name'));
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getModeSteps', () => {
|
||||
it('should return ionian steps', () => {
|
||||
expect(getModeSteps('ionian')).toEqual([2, 2, 1, 2, 2, 2, 1])
|
||||
})
|
||||
expect(getModeSteps('ionian')).toEqual([2, 2, 1, 2, 2, 2, 1]);
|
||||
});
|
||||
|
||||
it('should return dorian steps', () => {
|
||||
expect(getModeSteps('dorian')).toEqual([2, 1, 2, 2, 2, 1, 2])
|
||||
})
|
||||
expect(getModeSteps('dorian')).toEqual([2, 1, 2, 2, 2, 1, 2]);
|
||||
});
|
||||
|
||||
it('should default to ionian for invalid mode', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
expect(getModeSteps('invalid')).toEqual([2, 2, 1, 2, 2, 2, 1])
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Mode not found'))
|
||||
warnSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
expect(getModeSteps('invalid')).toEqual([2, 2, 1, 2, 2, 2, 1]);
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Mode not found'));
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getScalePitchClasses', () => {
|
||||
it('should return C major scale pitch classes', () => {
|
||||
const steps = [2, 2, 1, 2, 2, 2, 1]
|
||||
const result = getScalePitchClasses('C', steps)
|
||||
expect(result).toEqual([0, 2, 4, 5, 7, 9, 11]) // C D E F G A B
|
||||
})
|
||||
const steps = [2, 2, 1, 2, 2, 2, 1];
|
||||
const result = getScalePitchClasses('C', steps);
|
||||
expect(result).toEqual([0, 2, 4, 5, 7, 9, 11]); // C D E F G A B
|
||||
});
|
||||
|
||||
it('should return D major scale pitch classes', () => {
|
||||
const steps = [2, 2, 1, 2, 2, 2, 1]
|
||||
const result = getScalePitchClasses('D', steps)
|
||||
expect(result).toEqual([2, 4, 6, 7, 9, 11, 1]) // D E F# G A B C#
|
||||
})
|
||||
const steps = [2, 2, 1, 2, 2, 2, 1];
|
||||
const result = getScalePitchClasses('D', steps);
|
||||
expect(result).toEqual([2, 4, 6, 7, 9, 11, 1]); // D E F# G A B C#
|
||||
});
|
||||
|
||||
it('should return F# dorian scale pitch classes', () => {
|
||||
const steps = [2, 1, 2, 2, 2, 1, 2]
|
||||
const result = getScalePitchClasses('F#', steps)
|
||||
expect(result).toEqual([6, 8, 9, 11, 1, 3, 4]) // F# G# A B C# D# E
|
||||
})
|
||||
})
|
||||
const steps = [2, 1, 2, 2, 2, 1, 2];
|
||||
const result = getScalePitchClasses('F#', steps);
|
||||
expect(result).toEqual([6, 8, 9, 11, 1, 3, 4]); // F# G# A B C# D# E
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSuitableChords', () => {
|
||||
it('should return tonic chords for C major ionian', () => {
|
||||
const result = getSuitableChords('C major', 'ionian', 'T')
|
||||
expect(result).toHaveProperty('I')
|
||||
expect(result).toHaveProperty('vi')
|
||||
expect(result).toHaveProperty('iii')
|
||||
expect(result['I']).toEqual(['C', 'E', 'G'])
|
||||
expect(result['vi']).toEqual(['A', 'C', 'E'])
|
||||
})
|
||||
const result = getSuitableChords('C major', 'ionian', 'T');
|
||||
expect(result).toHaveProperty('I');
|
||||
expect(result).toHaveProperty('vi');
|
||||
expect(result).toHaveProperty('iii');
|
||||
expect(result['I']).toEqual(['C', 'E', 'G']);
|
||||
expect(result['vi']).toEqual(['A', 'C', 'E']);
|
||||
});
|
||||
|
||||
it('should return subdominant chords for C major ionian', () => {
|
||||
const result = getSuitableChords('C major', 'ionian', 'S')
|
||||
expect(result).toHaveProperty('IV')
|
||||
expect(result).toHaveProperty('ii')
|
||||
expect(result['IV']).toEqual(['F', 'A', 'C'])
|
||||
expect(result['ii']).toEqual(['D', 'F', 'A'])
|
||||
})
|
||||
const result = getSuitableChords('C major', 'ionian', 'S');
|
||||
expect(result).toHaveProperty('IV');
|
||||
expect(result).toHaveProperty('ii');
|
||||
expect(result['IV']).toEqual(['F', 'A', 'C']);
|
||||
expect(result['ii']).toEqual(['D', 'F', 'A']);
|
||||
});
|
||||
|
||||
it('should return dominant chords for C major ionian', () => {
|
||||
const result = getSuitableChords('C major', 'ionian', 'D')
|
||||
expect(result).toHaveProperty('V')
|
||||
expect(result).toHaveProperty('V7')
|
||||
expect(result['V']).toEqual(['G', 'B', 'D'])
|
||||
expect(result['V7']).toEqual(['G', 'B', 'D', 'F'])
|
||||
})
|
||||
const result = getSuitableChords('C major', 'ionian', 'D');
|
||||
expect(result).toHaveProperty('V');
|
||||
expect(result).toHaveProperty('V7');
|
||||
expect(result['V']).toEqual(['G', 'B', 'D']);
|
||||
expect(result['V7']).toEqual(['G', 'B', 'D', 'F']);
|
||||
});
|
||||
|
||||
it('should transpose chords for D major ionian', () => {
|
||||
const result = getSuitableChords('D major', 'ionian', 'T')
|
||||
expect(result['I']).toEqual(['D', 'F#', 'A'])
|
||||
expect(result['vi']).toEqual(['B', 'D', 'F#'])
|
||||
})
|
||||
const result = getSuitableChords('D major', 'ionian', 'T');
|
||||
expect(result['I']).toEqual(['D', 'F#', 'A']);
|
||||
expect(result['vi']).toEqual(['B', 'D', 'F#']);
|
||||
});
|
||||
|
||||
it('should transpose chords for F# major ionian', () => {
|
||||
const result = getSuitableChords('F# major', 'ionian', 'T')
|
||||
expect(result['I']).toEqual(['F#', 'A#', 'C#'])
|
||||
})
|
||||
const result = getSuitableChords('F# major', 'ionian', 'T');
|
||||
expect(result['I']).toEqual(['F#', 'A#', 'C#']);
|
||||
});
|
||||
|
||||
it('should return empty object for invalid mode', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const result = getSuitableChords('C major', 'invalid', 'T')
|
||||
expect(result).toEqual({})
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('No functional chords found'))
|
||||
warnSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const result = getSuitableChords('C major', 'invalid', 'T');
|
||||
expect(result).toEqual({});
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('No functional chords found'));
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getChordNotesInKey', () => {
|
||||
it('should return I chord notes in C major ionian', () => {
|
||||
const result = getChordNotesInKey('I', 'C major', 'ionian')
|
||||
expect(result).toEqual(['C', 'E', 'G'])
|
||||
})
|
||||
const result = getChordNotesInKey('I', 'C major', 'ionian');
|
||||
expect(result).toEqual(['C', 'E', 'G']);
|
||||
});
|
||||
|
||||
it('should return V7 chord notes in C major ionian', () => {
|
||||
const result = getChordNotesInKey('V7', 'C major', 'ionian')
|
||||
expect(result).toEqual(['G', 'B', 'D', 'F'])
|
||||
})
|
||||
const result = getChordNotesInKey('V7', 'C major', 'ionian');
|
||||
expect(result).toEqual(['G', 'B', 'D', 'F']);
|
||||
});
|
||||
|
||||
it('should transpose to D major', () => {
|
||||
const result = getChordNotesInKey('I', 'D major', 'ionian')
|
||||
expect(result).toEqual(['D', 'F#', 'A'])
|
||||
})
|
||||
const result = getChordNotesInKey('I', 'D major', 'ionian');
|
||||
expect(result).toEqual(['D', 'F#', 'A']);
|
||||
});
|
||||
|
||||
it('should return empty array for invalid chord symbol', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const result = getChordNotesInKey('invalid', 'C major', 'ionian')
|
||||
expect(result).toEqual([])
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Chord symbol not found'))
|
||||
warnSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const result = getChordNotesInKey('invalid', 'C major', 'ionian');
|
||||
expect(result).toEqual([]);
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Chord symbol not found'));
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMatchingChordsForPitch', () => {
|
||||
describe('valid inputs', () => {
|
||||
it('should return matching chords for C (pitch 60) in C major ionian tonic', () => {
|
||||
const result = getMatchingChordsForPitch(60, 'C major', 'ionian', 'T')
|
||||
const result = getMatchingChordsForPitch(60, 'C major', 'ionian', 'T');
|
||||
|
||||
// C major ionian T chords from JSON: I ["C","E","G"], vi ["A","C","E"], iii ["E","G","B"], I⁶ ["E","G","C"]
|
||||
// Pitch 60 = C (pitch class 0)
|
||||
@@ -222,11 +222,11 @@ describe('scaleUtil', () => {
|
||||
[0, 4, 7], // I chord (C-E-G): C is root
|
||||
[-3, 0, 4], // vi chord (A-C-E): C is 2nd, offset applied
|
||||
[-8, -5, 0] // I⁶ chord (E-G-C): C is 3rd, offset applied
|
||||
])
|
||||
})
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return matching chords for E (pitch 64) in C major ionian tonic', () => {
|
||||
const result = getMatchingChordsForPitch(64, 'C major', 'ionian', 'T')
|
||||
const result = getMatchingChordsForPitch(64, 'C major', 'ionian', 'T');
|
||||
|
||||
// Pitch 64 = E (pitch class 4)
|
||||
// Expected matches prioritized by position:
|
||||
@@ -239,19 +239,19 @@ describe('scaleUtil', () => {
|
||||
[4, 7, 12], // I⁶ chord (E-G-C): E is root
|
||||
[0, 4, 7], // I chord (C-E-G): E is 2nd
|
||||
[-3, 0, 4] // vi chord (A-C-E): E is 3rd, offset applied
|
||||
])
|
||||
})
|
||||
]);
|
||||
});
|
||||
|
||||
it('should prioritize root matches over other positions', () => {
|
||||
const result = getMatchingChordsForPitch(60, 'C major', 'ionian', 'T') // C
|
||||
const result = getMatchingChordsForPitch(60, 'C major', 'ionian', 'T'); // C
|
||||
// First chord should have C as root (position 0)
|
||||
if (result.length > 0) {
|
||||
expect(result[0][0] % 12).toBe(0) // First note of first chord should be C
|
||||
expect(result[0][0] % 12).toBe(0); // First note of first chord should be C
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
it('should return matching chords for F (pitch 65) in C major ionian subdominant', () => {
|
||||
const result = getMatchingChordsForPitch(65, 'C major', 'ionian', 'S') // F
|
||||
const result = getMatchingChordsForPitch(65, 'C major', 'ionian', 'S'); // F
|
||||
|
||||
// Pitch 65 = F (pitch class 5)
|
||||
// C major ionian S chords from JSON: IV ["F","A","C"], ii ["D","F","A"], vi ["A","C","E"], IV⁶ ["A","C","F"]
|
||||
@@ -263,11 +263,11 @@ describe('scaleUtil', () => {
|
||||
[5, 9, 12], // IV chord (F-A-C): F is root, offset applied
|
||||
[2, 5, 9], // ii chord (D-F-A): F is 2nd
|
||||
[-3, 0, 5] // IV⁶ chord (A-C-F): F is 3rd, offset applied
|
||||
])
|
||||
})
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return matching chords for G (pitch 67) in C major ionian dominant', () => {
|
||||
const result = getMatchingChordsForPitch(67, 'C major', 'ionian', 'D') // G
|
||||
const result = getMatchingChordsForPitch(67, 'C major', 'ionian', 'D'); // G
|
||||
|
||||
// Pitch 67 = G (pitch class 7)
|
||||
// C major ionian D chords from JSON: V ["G","B","D"], V7 ["G","B","D","F"], vii° ["B","D","F"], ♭II ["Db","F","Ab"]
|
||||
@@ -277,122 +277,122 @@ describe('scaleUtil', () => {
|
||||
expect(result).toEqual([
|
||||
[7, 11, 14], // V chord (G-B-D): G is root, offset applied
|
||||
[7, 11, 14, 17] // V7 chord (G-B-D-F): G is root, offset applied
|
||||
])
|
||||
})
|
||||
]);
|
||||
});
|
||||
|
||||
it('should work with different modes (dorian)', () => {
|
||||
const result = getMatchingChordsForPitch(60, 'C major', 'dorian', 'T')
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
})
|
||||
const result = getMatchingChordsForPitch(60, 'C major', 'dorian', 'T');
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should transpose correctly for D major', () => {
|
||||
const result = getMatchingChordsForPitch(62, 'D major', 'ionian', 'T') // D
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
expect(result.some(chord => chord.includes(2))).toBe(true) // Contains D (pitch class 2)
|
||||
})
|
||||
const result = getMatchingChordsForPitch(62, 'D major', 'ionian', 'T'); // D
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
expect(result.some(chord => chord.includes(2))).toBe(true); // Contains D (pitch class 2)
|
||||
});
|
||||
|
||||
it('should handle all MIDI pitch ranges (low)', () => {
|
||||
const result = getMatchingChordsForPitch(24, 'C major', 'ionian', 'T') // C1
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
})
|
||||
const result = getMatchingChordsForPitch(24, 'C major', 'ionian', 'T'); // C1
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle all MIDI pitch ranges (high)', () => {
|
||||
const result = getMatchingChordsForPitch(108, 'C major', 'ionian', 'T') // C8
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
})
|
||||
})
|
||||
const result = getMatchingChordsForPitch(108, 'C major', 'ionian', 'T'); // C8
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should return empty array for invalid mode', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const result = getMatchingChordsForPitch(60, 'C major', 'invalid', 'T')
|
||||
expect(result).toEqual([])
|
||||
warnSpy.mockRestore()
|
||||
})
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const result = getMatchingChordsForPitch(60, 'C major', 'invalid', 'T');
|
||||
expect(result).toEqual([]);
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should handle pitch class calculations correctly', () => {
|
||||
// Test that pitch 60 (C4) and pitch 72 (C5) both match C chords
|
||||
const result1 = getMatchingChordsForPitch(60, 'C major', 'ionian', 'T')
|
||||
const result2 = getMatchingChordsForPitch(72, 'C major', 'ionian', 'T')
|
||||
expect(result1.length).toBe(result2.length) // Same chords match
|
||||
})
|
||||
const result1 = getMatchingChordsForPitch(60, 'C major', 'ionian', 'T');
|
||||
const result2 = getMatchingChordsForPitch(72, 'C major', 'ionian', 'T');
|
||||
expect(result1.length).toBe(result2.length); // Same chords match
|
||||
});
|
||||
|
||||
it('should return pitch classes in ascending order', () => {
|
||||
const result = getMatchingChordsForPitch(60, 'C major', 'ionian', 'T')
|
||||
const result = getMatchingChordsForPitch(60, 'C major', 'ionian', 'T');
|
||||
result.forEach(chord => {
|
||||
for (let i = 1; i < chord.length; i++) {
|
||||
expect(chord[i]).toBeGreaterThan(chord[i - 1])
|
||||
expect(chord[i]).toBeGreaterThan(chord[i - 1]);
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply octave offset correctly for pitch classes >= 12', () => {
|
||||
const result = getMatchingChordsForPitch(60, 'C major', 'ionian', 'T')
|
||||
const result = getMatchingChordsForPitch(60, 'C major', 'ionian', 'T');
|
||||
// All pitch classes should be < 12 after offset
|
||||
result.forEach(chord => {
|
||||
chord.forEach(pitchClass => {
|
||||
expect(pitchClass).toBeLessThan(24) // Allowing for extended range
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
expect(pitchClass).toBeLessThan(24); // Allowing for extended range
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('pitch class calculations', () => {
|
||||
it('should correctly convert hover pitch to pitch class', () => {
|
||||
// C4 (60) should match same chords as C5 (72)
|
||||
const result1 = getMatchingChordsForPitch(60, 'C major', 'ionian', 'T')
|
||||
const result2 = getMatchingChordsForPitch(72, 'C major', 'ionian', 'T')
|
||||
expect(result1).toEqual(result2)
|
||||
})
|
||||
const result1 = getMatchingChordsForPitch(60, 'C major', 'ionian', 'T');
|
||||
const result2 = getMatchingChordsForPitch(72, 'C major', 'ionian', 'T');
|
||||
expect(result1).toEqual(result2);
|
||||
});
|
||||
|
||||
it('should maintain ascending pitch order in results', () => {
|
||||
const result = getMatchingChordsForPitch(64, 'C major', 'ionian', 'T')
|
||||
const result = getMatchingChordsForPitch(64, 'C major', 'ionian', 'T');
|
||||
result.forEach(chord => {
|
||||
for (let i = 1; i < chord.length; i++) {
|
||||
expect(chord[i]).toBeGreaterThan(chord[i - 1])
|
||||
expect(chord[i]).toBeGreaterThan(chord[i - 1]);
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration with helper functions', () => {
|
||||
it('should work with getSuitableChords', () => {
|
||||
const suitableChords = getSuitableChords('C major', 'ionian', 'T')
|
||||
expect(Object.keys(suitableChords).length).toBeGreaterThan(0)
|
||||
const suitableChords = getSuitableChords('C major', 'ionian', 'T');
|
||||
expect(Object.keys(suitableChords).length).toBeGreaterThan(0);
|
||||
|
||||
const matchingChords = getMatchingChordsForPitch(60, 'C major', 'ionian', 'T')
|
||||
expect(matchingChords.length).toBeGreaterThan(0)
|
||||
})
|
||||
const matchingChords = getMatchingChordsForPitch(60, 'C major', 'ionian', 'T');
|
||||
expect(matchingChords.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should work with noteNameToPitchClass', () => {
|
||||
const cPitch = noteNameToPitchClass('C')
|
||||
const result = getMatchingChordsForPitch(60, 'C major', 'ionian', 'T')
|
||||
expect(result.some(chord => chord.some(pc => pc % 12 === cPitch))).toBe(true)
|
||||
})
|
||||
const cPitch = noteNameToPitchClass('C');
|
||||
const result = getMatchingChordsForPitch(60, 'C major', 'ionian', 'T');
|
||||
expect(result.some(chord => chord.some(pc => pc % 12 === cPitch))).toBe(true);
|
||||
});
|
||||
|
||||
it('should respect KGCore.FUNCTIONAL_CHORDS_DATA structure', () => {
|
||||
// Verify the data has the expected structure
|
||||
const data = KGCore.FUNCTIONAL_CHORDS_DATA
|
||||
expect(data).toHaveProperty('ionian')
|
||||
expect(data['ionian']).toHaveProperty('T')
|
||||
expect(data['ionian']).toHaveProperty('chords')
|
||||
})
|
||||
})
|
||||
})
|
||||
const data = KGCore.FUNCTIONAL_CHORDS_DATA;
|
||||
expect(data).toHaveProperty('ionian');
|
||||
expect(data['ionian']).toHaveProperty('T');
|
||||
expect(data['ionian']).toHaveProperty('chords');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('generatePianoGridBackground', () => {
|
||||
it('should generate CSS background for C major ionian', () => {
|
||||
const result = generatePianoGridBackground('ionian', 'C major')
|
||||
expect(result).toContain('linear-gradient')
|
||||
expect(typeof result).toBe('string')
|
||||
})
|
||||
const result = generatePianoGridBackground('ionian', 'C major');
|
||||
expect(result).toContain('linear-gradient');
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
|
||||
it('should generate different backgrounds for different modes', () => {
|
||||
const ionian = generatePianoGridBackground('ionian', 'C major')
|
||||
const dorian = generatePianoGridBackground('dorian', 'C major')
|
||||
expect(ionian).not.toBe(dorian)
|
||||
})
|
||||
})
|
||||
const ionian = generatePianoGridBackground('ionian', 'C major');
|
||||
const dorian = generatePianoGridBackground('dorian', 'C major');
|
||||
expect(ionian).not.toBe(dorian);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateFunctionalChordsJSON', () => {
|
||||
const validJSON = `{
|
||||
@@ -434,14 +434,14 @@ describe('scaleUtil', () => {
|
||||
"♭VII": ["Bb", "D", "F"]
|
||||
}
|
||||
}
|
||||
}`
|
||||
}`;
|
||||
|
||||
describe('valid JSON', () => {
|
||||
it('should validate correct JSON with ionian and aeolian', () => {
|
||||
const result = validateFunctionalChordsJSON(validJSON)
|
||||
expect(result.valid).toBe(true)
|
||||
expect(result.errors).toEqual([])
|
||||
})
|
||||
const result = validateFunctionalChordsJSON(validJSON);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should accept empty T/S/D arrays', () => {
|
||||
const json = `{
|
||||
@@ -453,10 +453,10 @@ describe('scaleUtil', () => {
|
||||
"D": [],
|
||||
"chords": {}
|
||||
}
|
||||
}`
|
||||
const result = validateFunctionalChordsJSON(json)
|
||||
expect(result.valid).toBe(true)
|
||||
})
|
||||
}`;
|
||||
const result = validateFunctionalChordsJSON(json);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept mode names with underscores, dashes, and spaces', () => {
|
||||
const json = `{
|
||||
@@ -468,10 +468,10 @@ describe('scaleUtil', () => {
|
||||
"D": [],
|
||||
"chords": {}
|
||||
}
|
||||
}`
|
||||
const result = validateFunctionalChordsJSON(json)
|
||||
expect(result.valid).toBe(true)
|
||||
})
|
||||
}`;
|
||||
const result = validateFunctionalChordsJSON(json);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept notes with sharp and flat', () => {
|
||||
const json = `{
|
||||
@@ -485,10 +485,10 @@ describe('scaleUtil', () => {
|
||||
"I": ["C#", "Eb", "F#"]
|
||||
}
|
||||
}
|
||||
}`
|
||||
const result = validateFunctionalChordsJSON(json)
|
||||
expect(result.valid).toBe(true)
|
||||
})
|
||||
}`;
|
||||
const result = validateFunctionalChordsJSON(json);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept chord symbols with special characters', () => {
|
||||
const json = `{
|
||||
@@ -505,24 +505,24 @@ describe('scaleUtil', () => {
|
||||
"♭II": ["Db", "F", "Ab"]
|
||||
}
|
||||
}
|
||||
}`
|
||||
const result = validateFunctionalChordsJSON(json)
|
||||
expect(result.valid).toBe(true)
|
||||
})
|
||||
})
|
||||
}`;
|
||||
const result = validateFunctionalChordsJSON(json);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid JSON', () => {
|
||||
it('should reject malformed JSON', () => {
|
||||
const result = validateFunctionalChordsJSON('{ invalid json }')
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors).toContain('Invalid JSON format')
|
||||
})
|
||||
const result = validateFunctionalChordsJSON('{ invalid json }');
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain('Invalid JSON format');
|
||||
});
|
||||
|
||||
it('should reject JSON array as root', () => {
|
||||
const result = validateFunctionalChordsJSON('[]')
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors).toContain('Root must be an object')
|
||||
})
|
||||
const result = validateFunctionalChordsJSON('[]');
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain('Root must be an object');
|
||||
});
|
||||
|
||||
it('should reject missing ionian mode', () => {
|
||||
const json = `{
|
||||
@@ -534,11 +534,11 @@ describe('scaleUtil', () => {
|
||||
"D": [],
|
||||
"chords": {}
|
||||
}
|
||||
}`
|
||||
const result = validateFunctionalChordsJSON(json)
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors).toContain('Missing required mode: "ionian"')
|
||||
})
|
||||
}`;
|
||||
const result = validateFunctionalChordsJSON(json);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain('Missing required mode: "ionian"');
|
||||
});
|
||||
|
||||
it('should reject invalid mode name', () => {
|
||||
const json = `{
|
||||
@@ -550,11 +550,11 @@ describe('scaleUtil', () => {
|
||||
"D": [],
|
||||
"chords": {}
|
||||
}
|
||||
}`
|
||||
const result = validateFunctionalChordsJSON(json)
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.some(e => e.includes('"name" must be a string'))).toBe(true)
|
||||
})
|
||||
}`;
|
||||
const result = validateFunctionalChordsJSON(json);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('"name" must be a string'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject steps with wrong number of elements', () => {
|
||||
const json = `{
|
||||
@@ -566,11 +566,11 @@ describe('scaleUtil', () => {
|
||||
"D": [],
|
||||
"chords": {}
|
||||
}
|
||||
}`
|
||||
const result = validateFunctionalChordsJSON(json)
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.some(e => e.includes('must contain exactly 7 integers'))).toBe(true)
|
||||
})
|
||||
}`;
|
||||
const result = validateFunctionalChordsJSON(json);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('must contain exactly 7 integers'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject steps that do not sum to 12', () => {
|
||||
const json = `{
|
||||
@@ -582,11 +582,11 @@ describe('scaleUtil', () => {
|
||||
"D": [],
|
||||
"chords": {}
|
||||
}
|
||||
}`
|
||||
const result = validateFunctionalChordsJSON(json)
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.some(e => e.includes('must sum to 12'))).toBe(true)
|
||||
})
|
||||
}`;
|
||||
const result = validateFunctionalChordsJSON(json);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('must sum to 12'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject non-integer steps', () => {
|
||||
const json = `{
|
||||
@@ -598,11 +598,11 @@ describe('scaleUtil', () => {
|
||||
"D": [],
|
||||
"chords": {}
|
||||
}
|
||||
}`
|
||||
const result = validateFunctionalChordsJSON(json)
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.some(e => e.includes('must contain only integers'))).toBe(true)
|
||||
})
|
||||
}`;
|
||||
const result = validateFunctionalChordsJSON(json);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('must contain only integers'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject invalid chord symbol in T/S/D', () => {
|
||||
const json = `{
|
||||
@@ -616,11 +616,11 @@ describe('scaleUtil', () => {
|
||||
"invalid123": ["C", "E", "G"]
|
||||
}
|
||||
}
|
||||
}`
|
||||
const result = validateFunctionalChordsJSON(json)
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.some(e => e.includes('Invalid chord symbol'))).toBe(true)
|
||||
})
|
||||
}`;
|
||||
const result = validateFunctionalChordsJSON(json);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('Invalid chord symbol'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject chord referenced but not defined', () => {
|
||||
const json = `{
|
||||
@@ -632,11 +632,11 @@ describe('scaleUtil', () => {
|
||||
"D": [],
|
||||
"chords": {}
|
||||
}
|
||||
}`
|
||||
const result = validateFunctionalChordsJSON(json)
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.some(e => e.includes('referenced in T/S/D but not defined'))).toBe(true)
|
||||
})
|
||||
}`;
|
||||
const result = validateFunctionalChordsJSON(json);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('referenced in T/S/D but not defined'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject invalid note name', () => {
|
||||
const json = `{
|
||||
@@ -650,11 +650,11 @@ describe('scaleUtil', () => {
|
||||
"I": ["C", "X", "G"]
|
||||
}
|
||||
}
|
||||
}`
|
||||
const result = validateFunctionalChordsJSON(json)
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.some(e => e.includes('Invalid note'))).toBe(true)
|
||||
})
|
||||
}`;
|
||||
const result = validateFunctionalChordsJSON(json);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('Invalid note'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject note with invalid accidental', () => {
|
||||
const json = `{
|
||||
@@ -668,11 +668,11 @@ describe('scaleUtil', () => {
|
||||
"I": ["C##", "E", "G"]
|
||||
}
|
||||
}
|
||||
}`
|
||||
const result = validateFunctionalChordsJSON(json)
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.some(e => e.includes('Invalid note'))).toBe(true)
|
||||
})
|
||||
}`;
|
||||
const result = validateFunctionalChordsJSON(json);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('Invalid note'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject lowercase note names', () => {
|
||||
const json = `{
|
||||
@@ -686,12 +686,12 @@ describe('scaleUtil', () => {
|
||||
"I": ["c", "e", "g"]
|
||||
}
|
||||
}
|
||||
}`
|
||||
const result = validateFunctionalChordsJSON(json)
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.some(e => e.includes('Invalid note'))).toBe(true)
|
||||
})
|
||||
})
|
||||
}`;
|
||||
const result = validateFunctionalChordsJSON(json);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('Invalid note'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error messages', () => {
|
||||
it('should provide descriptive error messages for multiple errors', () => {
|
||||
@@ -704,11 +704,11 @@ describe('scaleUtil', () => {
|
||||
"D": [],
|
||||
"chords": {}
|
||||
}
|
||||
}`
|
||||
const result = validateFunctionalChordsJSON(json)
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.length).toBeGreaterThan(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}`;
|
||||
const result = validateFunctionalChordsJSON(json);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.length).toBeGreaterThan(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+162
-162
@@ -1,316 +1,316 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { parseTimeSignature, getTimeSignatureErrorMessage, beatsToTimeString, formatLocalDateTime } from './timeUtil'
|
||||
import { TIME_CONSTANTS } from '../constants/coreConstants'
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { parseTimeSignature, getTimeSignatureErrorMessage, beatsToTimeString, formatLocalDateTime } from './timeUtil';
|
||||
import { TIME_CONSTANTS } from '../constants/coreConstants';
|
||||
|
||||
describe('timeUtil', () => {
|
||||
describe('parseTimeSignature', () => {
|
||||
it('should parse valid time signatures', () => {
|
||||
expect(parseTimeSignature('4/4')).toEqual({ numerator: 4, denominator: 4 })
|
||||
expect(parseTimeSignature('3/4')).toEqual({ numerator: 3, denominator: 4 })
|
||||
expect(parseTimeSignature('6/8')).toEqual({ numerator: 6, denominator: 8 })
|
||||
expect(parseTimeSignature('12/8')).toEqual({ numerator: 12, denominator: 8 })
|
||||
expect(parseTimeSignature('2/4')).toEqual({ numerator: 2, denominator: 4 })
|
||||
})
|
||||
expect(parseTimeSignature('4/4')).toEqual({ numerator: 4, denominator: 4 });
|
||||
expect(parseTimeSignature('3/4')).toEqual({ numerator: 3, denominator: 4 });
|
||||
expect(parseTimeSignature('6/8')).toEqual({ numerator: 6, denominator: 8 });
|
||||
expect(parseTimeSignature('12/8')).toEqual({ numerator: 12, denominator: 8 });
|
||||
expect(parseTimeSignature('2/4')).toEqual({ numerator: 2, denominator: 4 });
|
||||
});
|
||||
|
||||
it('should handle whitespace around input', () => {
|
||||
expect(parseTimeSignature(' 4/4 ')).toEqual({ numerator: 4, denominator: 4 })
|
||||
expect(parseTimeSignature(' 3/4 ')).toEqual({ numerator: 3, denominator: 4 })
|
||||
expect(parseTimeSignature('\t6/8\n')).toEqual({ numerator: 6, denominator: 8 })
|
||||
})
|
||||
expect(parseTimeSignature(' 4/4 ')).toEqual({ numerator: 4, denominator: 4 });
|
||||
expect(parseTimeSignature(' 3/4 ')).toEqual({ numerator: 3, denominator: 4 });
|
||||
expect(parseTimeSignature('\t6/8\n')).toEqual({ numerator: 6, denominator: 8 });
|
||||
});
|
||||
|
||||
it('should return null for invalid formats', () => {
|
||||
expect(parseTimeSignature('4')).toBeNull()
|
||||
expect(parseTimeSignature('4/4/4')).toBeNull()
|
||||
expect(parseTimeSignature('4-4')).toBeNull()
|
||||
expect(parseTimeSignature('4:4')).toBeNull()
|
||||
expect(parseTimeSignature('')).toBeNull()
|
||||
expect(parseTimeSignature('/')).toBeNull()
|
||||
expect(parseTimeSignature('4/')).toBeNull()
|
||||
expect(parseTimeSignature('/4')).toBeNull()
|
||||
})
|
||||
expect(parseTimeSignature('4')).toBeNull();
|
||||
expect(parseTimeSignature('4/4/4')).toBeNull();
|
||||
expect(parseTimeSignature('4-4')).toBeNull();
|
||||
expect(parseTimeSignature('4:4')).toBeNull();
|
||||
expect(parseTimeSignature('')).toBeNull();
|
||||
expect(parseTimeSignature('/')).toBeNull();
|
||||
expect(parseTimeSignature('4/')).toBeNull();
|
||||
expect(parseTimeSignature('/4')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for non-numeric values', () => {
|
||||
expect(parseTimeSignature('a/4')).toBeNull()
|
||||
expect(parseTimeSignature('4/b')).toBeNull()
|
||||
expect(parseTimeSignature('x/y')).toBeNull()
|
||||
expect(parseTimeSignature('a/4')).toBeNull();
|
||||
expect(parseTimeSignature('4/b')).toBeNull();
|
||||
expect(parseTimeSignature('x/y')).toBeNull();
|
||||
// Note: parseInt('4.5') returns 4, so these will parse as integers
|
||||
// Testing the actual behavior of parseInt
|
||||
expect(parseTimeSignature('4.5/4')).toEqual({ numerator: 4, denominator: 4 })
|
||||
expect(parseTimeSignature('4/4.5')).toEqual({ numerator: 4, denominator: 4 })
|
||||
})
|
||||
expect(parseTimeSignature('4.5/4')).toEqual({ numerator: 4, denominator: 4 });
|
||||
expect(parseTimeSignature('4/4.5')).toEqual({ numerator: 4, denominator: 4 });
|
||||
});
|
||||
|
||||
it('should return null for numerators not in available list', () => {
|
||||
// Assuming TIME_CONSTANTS has specific available numerators
|
||||
expect(parseTimeSignature('99/4')).toBeNull()
|
||||
expect(parseTimeSignature('0/4')).toBeNull()
|
||||
expect(parseTimeSignature('-1/4')).toBeNull()
|
||||
})
|
||||
expect(parseTimeSignature('99/4')).toBeNull();
|
||||
expect(parseTimeSignature('0/4')).toBeNull();
|
||||
expect(parseTimeSignature('-1/4')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for denominators not in available list', () => {
|
||||
// Assuming TIME_CONSTANTS has specific available denominators
|
||||
expect(parseTimeSignature('4/99')).toBeNull()
|
||||
expect(parseTimeSignature('4/0')).toBeNull()
|
||||
expect(parseTimeSignature('4/-1')).toBeNull()
|
||||
})
|
||||
expect(parseTimeSignature('4/99')).toBeNull();
|
||||
expect(parseTimeSignature('4/0')).toBeNull();
|
||||
expect(parseTimeSignature('4/-1')).toBeNull();
|
||||
});
|
||||
|
||||
it('should validate against TIME_CONSTANTS available values', () => {
|
||||
// Test that function actually uses TIME_CONSTANTS for validation
|
||||
const validNumerator = TIME_CONSTANTS.AVAILABLE_TIME_SIGNATURE_NUMERATORS[0]
|
||||
const validDenominator = TIME_CONSTANTS.AVAILABLE_TIME_SIGNATURE_DENOMINATORS[0]
|
||||
const invalidNumerator = 999 // Assuming this is not in the available list
|
||||
const invalidDenominator = 999 // Assuming this is not in the available list
|
||||
const validNumerator = TIME_CONSTANTS.AVAILABLE_TIME_SIGNATURE_NUMERATORS[0];
|
||||
const validDenominator = TIME_CONSTANTS.AVAILABLE_TIME_SIGNATURE_DENOMINATORS[0];
|
||||
const invalidNumerator = 999; // Assuming this is not in the available list
|
||||
const invalidDenominator = 999; // Assuming this is not in the available list
|
||||
|
||||
expect(parseTimeSignature(`${validNumerator}/${validDenominator}`)).not.toBeNull()
|
||||
expect(parseTimeSignature(`${invalidNumerator}/${validDenominator}`)).toBeNull()
|
||||
expect(parseTimeSignature(`${validNumerator}/${invalidDenominator}`)).toBeNull()
|
||||
})
|
||||
})
|
||||
expect(parseTimeSignature(`${validNumerator}/${validDenominator}`)).not.toBeNull();
|
||||
expect(parseTimeSignature(`${invalidNumerator}/${validDenominator}`)).toBeNull();
|
||||
expect(parseTimeSignature(`${validNumerator}/${invalidDenominator}`)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTimeSignatureErrorMessage', () => {
|
||||
it('should return a formatted error message with available options', () => {
|
||||
const message = getTimeSignatureErrorMessage()
|
||||
const message = getTimeSignatureErrorMessage();
|
||||
|
||||
expect(message).toContain('Invalid time signature format')
|
||||
expect(message).toContain('numerator/denominator')
|
||||
expect(message).toContain('Available numerators:')
|
||||
expect(message).toContain('Available denominators:')
|
||||
expect(message).toContain('Examples: 4/4, 3/4, 6/8, 12/8')
|
||||
})
|
||||
expect(message).toContain('Invalid time signature format');
|
||||
expect(message).toContain('numerator/denominator');
|
||||
expect(message).toContain('Available numerators:');
|
||||
expect(message).toContain('Available denominators:');
|
||||
expect(message).toContain('Examples: 4/4, 3/4, 6/8, 12/8');
|
||||
});
|
||||
|
||||
it('should include actual available values from TIME_CONSTANTS', () => {
|
||||
const message = getTimeSignatureErrorMessage()
|
||||
const message = getTimeSignatureErrorMessage();
|
||||
|
||||
// Check that it includes values from TIME_CONSTANTS
|
||||
TIME_CONSTANTS.AVAILABLE_TIME_SIGNATURE_NUMERATORS.forEach(numerator => {
|
||||
expect(message).toContain(numerator.toString())
|
||||
})
|
||||
expect(message).toContain(numerator.toString());
|
||||
});
|
||||
|
||||
TIME_CONSTANTS.AVAILABLE_TIME_SIGNATURE_DENOMINATORS.forEach(denominator => {
|
||||
expect(message).toContain(denominator.toString())
|
||||
})
|
||||
})
|
||||
expect(message).toContain(denominator.toString());
|
||||
});
|
||||
});
|
||||
|
||||
it('should be a consistent message format', () => {
|
||||
const message1 = getTimeSignatureErrorMessage()
|
||||
const message2 = getTimeSignatureErrorMessage()
|
||||
const message1 = getTimeSignatureErrorMessage();
|
||||
const message2 = getTimeSignatureErrorMessage();
|
||||
|
||||
expect(message1).toBe(message2)
|
||||
})
|
||||
})
|
||||
expect(message1).toBe(message2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('beatsToTimeString', () => {
|
||||
it('should format beats to BBB:B | mm:ss:mmm format', () => {
|
||||
// Test basic 4/4 time signature
|
||||
expect(beatsToTimeString(0, 120, { numerator: 4, denominator: 4 }))
|
||||
.toBe('001:1 | 00:00:000')
|
||||
.toBe('001:1 | 00:00:000');
|
||||
|
||||
expect(beatsToTimeString(4, 120, { numerator: 4, denominator: 4 }))
|
||||
.toBe('002:1 | 00:02:000')
|
||||
.toBe('002:1 | 00:02:000');
|
||||
|
||||
expect(beatsToTimeString(8, 120, { numerator: 4, denominator: 4 }))
|
||||
.toBe('003:1 | 00:04:000')
|
||||
})
|
||||
.toBe('003:1 | 00:04:000');
|
||||
});
|
||||
|
||||
it('should handle different time signatures correctly', () => {
|
||||
// 3/4 time signature - 3 beats per bar
|
||||
expect(beatsToTimeString(0, 120, { numerator: 3, denominator: 4 }))
|
||||
.toBe('001:1 | 00:00:000')
|
||||
.toBe('001:1 | 00:00:000');
|
||||
|
||||
expect(beatsToTimeString(3, 120, { numerator: 3, denominator: 4 }))
|
||||
.toBe('002:1 | 00:01:500')
|
||||
.toBe('002:1 | 00:01:500');
|
||||
|
||||
expect(beatsToTimeString(6, 120, { numerator: 3, denominator: 4 }))
|
||||
.toBe('003:1 | 00:03:000')
|
||||
.toBe('003:1 | 00:03:000');
|
||||
|
||||
// 6/8 time signature - 6 beats per bar
|
||||
expect(beatsToTimeString(0, 120, { numerator: 6, denominator: 8 }))
|
||||
.toBe('001:1 | 00:00:000')
|
||||
.toBe('001:1 | 00:00:000');
|
||||
|
||||
expect(beatsToTimeString(6, 120, { numerator: 6, denominator: 8 }))
|
||||
.toBe('002:1 | 00:03:000')
|
||||
})
|
||||
.toBe('002:1 | 00:03:000');
|
||||
});
|
||||
|
||||
it('should handle different BPM values correctly', () => {
|
||||
// 60 BPM - 1 beat per second
|
||||
expect(beatsToTimeString(1, 60, { numerator: 4, denominator: 4 }))
|
||||
.toBe('001:2 | 00:01:000')
|
||||
.toBe('001:2 | 00:01:000');
|
||||
|
||||
expect(beatsToTimeString(4, 60, { numerator: 4, denominator: 4 }))
|
||||
.toBe('002:1 | 00:04:000')
|
||||
.toBe('002:1 | 00:04:000');
|
||||
|
||||
// 240 BPM - 4 beats per second
|
||||
expect(beatsToTimeString(1, 240, { numerator: 4, denominator: 4 }))
|
||||
.toBe('001:2 | 00:00:250')
|
||||
.toBe('001:2 | 00:00:250');
|
||||
|
||||
expect(beatsToTimeString(4, 240, { numerator: 4, denominator: 4 }))
|
||||
.toBe('002:1 | 00:01:000')
|
||||
})
|
||||
.toBe('002:1 | 00:01:000');
|
||||
});
|
||||
|
||||
it('should handle fractional beats correctly', () => {
|
||||
expect(beatsToTimeString(1.5, 120, { numerator: 4, denominator: 4 }))
|
||||
.toBe('001:2 | 00:00:750')
|
||||
.toBe('001:2 | 00:00:750');
|
||||
|
||||
expect(beatsToTimeString(2.25, 120, { numerator: 4, denominator: 4 }))
|
||||
.toBe('001:3 | 00:01:125')
|
||||
.toBe('001:3 | 00:01:125');
|
||||
|
||||
expect(beatsToTimeString(4.75, 120, { numerator: 4, denominator: 4 }))
|
||||
.toBe('002:1 | 00:02:375')
|
||||
})
|
||||
.toBe('002:1 | 00:02:375');
|
||||
});
|
||||
|
||||
it('should pad numbers correctly in output format', () => {
|
||||
// Test bar padding (BBB format)
|
||||
expect(beatsToTimeString(0, 120, { numerator: 4, denominator: 4 }))
|
||||
.toMatch(/^001:/)
|
||||
.toMatch(/^001:/);
|
||||
|
||||
expect(beatsToTimeString(40, 120, { numerator: 4, denominator: 4 }))
|
||||
.toMatch(/^011:/) // Bar 11
|
||||
.toMatch(/^011:/); // Bar 11
|
||||
|
||||
expect(beatsToTimeString(396, 120, { numerator: 4, denominator: 4 }))
|
||||
.toMatch(/^100:/) // Bar 100
|
||||
.toMatch(/^100:/); // Bar 100
|
||||
|
||||
// Test time padding (mm:ss:mmm format)
|
||||
expect(beatsToTimeString(1, 60, { numerator: 4, denominator: 4 }))
|
||||
.toMatch(/\| 00:01:000$/)
|
||||
.toMatch(/\| 00:01:000$/);
|
||||
|
||||
expect(beatsToTimeString(75, 60, { numerator: 4, denominator: 4 }))
|
||||
.toMatch(/\| 01:15:000$/) // 1 minute 15 seconds
|
||||
})
|
||||
.toMatch(/\| 01:15:000$/); // 1 minute 15 seconds
|
||||
});
|
||||
|
||||
it('should handle large beat values', () => {
|
||||
const result = beatsToTimeString(1000, 120, { numerator: 4, denominator: 4 })
|
||||
expect(result).toMatch(/^251:1 \| \d{2}:\d{2}:\d{3}$/)
|
||||
})
|
||||
const result = beatsToTimeString(1000, 120, { numerator: 4, denominator: 4 });
|
||||
expect(result).toMatch(/^251:1 \| \d{2}:\d{2}:\d{3}$/);
|
||||
});
|
||||
|
||||
it('should handle edge case of zero BPM gracefully', () => {
|
||||
// This might cause division by zero, should handle gracefully
|
||||
expect(() => beatsToTimeString(1, 0, { numerator: 4, denominator: 4 }))
|
||||
.not.toThrow()
|
||||
})
|
||||
.not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle negative beats', () => {
|
||||
// Edge case - negative beats may produce negative values in time format
|
||||
const result = beatsToTimeString(-1, 120, { numerator: 4, denominator: 4 })
|
||||
expect(result).toBeDefined()
|
||||
expect(typeof result).toBe('string')
|
||||
const result = beatsToTimeString(-1, 120, { numerator: 4, denominator: 4 });
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe('string');
|
||||
// The function may produce negative time values for negative beats
|
||||
expect(result).toMatch(/^\d{3}:\d \| -?\d+:-?\d+:-?\d+$/)
|
||||
})
|
||||
})
|
||||
expect(result).toMatch(/^\d{3}:\d \| -?\d+:-?\d+:-?\d+$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatLocalDateTime', () => {
|
||||
let mockDate: Date
|
||||
let mockDate: Date;
|
||||
|
||||
beforeEach(() => {
|
||||
// Use a fixed date for consistent testing
|
||||
mockDate = new Date('2025-08-21T19:57:11.123Z')
|
||||
})
|
||||
mockDate = new Date('2025-08-21T19:57:11.123Z');
|
||||
});
|
||||
|
||||
it('should format date with correct structure', () => {
|
||||
const result = formatLocalDateTime(mockDate)
|
||||
const result = formatLocalDateTime(mockDate);
|
||||
|
||||
// Should contain date parts
|
||||
expect(result).toMatch(/\d{4}/) // Year
|
||||
expect(result).toMatch(/\d{2}/) // Month/day/hour/minute/second
|
||||
expect(result).toContain(':') // Time separator
|
||||
expect(result).toMatch(/GMT[+-]\d+|UTC|[A-Z]{3,4}/) // Timezone
|
||||
})
|
||||
expect(result).toMatch(/\d{4}/); // Year
|
||||
expect(result).toMatch(/\d{2}/); // Month/day/hour/minute/second
|
||||
expect(result).toContain(':'); // Time separator
|
||||
expect(result).toMatch(/GMT[+-]\d+|UTC|[A-Z]{3,4}/); // Timezone
|
||||
});
|
||||
|
||||
it('should use 24-hour format', () => {
|
||||
const morningDate = new Date('2025-08-21T09:30:00Z')
|
||||
const eveningDate = new Date('2025-08-21T21:30:00Z')
|
||||
const morningDate = new Date('2025-08-21T09:30:00Z');
|
||||
const eveningDate = new Date('2025-08-21T21:30:00Z');
|
||||
|
||||
const morningResult = formatLocalDateTime(morningDate)
|
||||
const eveningResult = formatLocalDateTime(eveningDate)
|
||||
const morningResult = formatLocalDateTime(morningDate);
|
||||
const eveningResult = formatLocalDateTime(eveningDate);
|
||||
|
||||
// Should not contain AM/PM indicators
|
||||
expect(morningResult).not.toMatch(/AM|PM/i)
|
||||
expect(eveningResult).not.toMatch(/AM|PM/i)
|
||||
})
|
||||
expect(morningResult).not.toMatch(/AM|PM/i);
|
||||
expect(eveningResult).not.toMatch(/AM|PM/i);
|
||||
});
|
||||
|
||||
it('should include timezone information', () => {
|
||||
const result = formatLocalDateTime(mockDate)
|
||||
const result = formatLocalDateTime(mockDate);
|
||||
|
||||
// Should contain some timezone indicator
|
||||
expect(result).toMatch(/GMT[+-]\d+|UTC|[A-Z]{3,4}|\+\d{4}|-\d{4}/)
|
||||
})
|
||||
expect(result).toMatch(/GMT[+-]\d+|UTC|[A-Z]{3,4}|\+\d{4}|-\d{4}/);
|
||||
});
|
||||
|
||||
it('should handle different dates consistently', () => {
|
||||
const dates = [
|
||||
new Date('2025-01-01T00:00:00Z'),
|
||||
new Date('2025-06-15T12:30:45Z'),
|
||||
new Date('2025-12-31T23:59:59Z')
|
||||
]
|
||||
];
|
||||
|
||||
dates.forEach(date => {
|
||||
const result = formatLocalDateTime(date)
|
||||
expect(result).toBeDefined()
|
||||
expect(typeof result).toBe('string')
|
||||
expect(result.length).toBeGreaterThan(10)
|
||||
})
|
||||
})
|
||||
const result = formatLocalDateTime(date);
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result.length).toBeGreaterThan(10);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle edge dates', () => {
|
||||
const edgeDates = [
|
||||
new Date('1970-01-01T00:00:00Z'), // Unix epoch
|
||||
new Date('2038-01-19T03:14:07Z'), // Near 32-bit timestamp limit
|
||||
new Date('2100-12-31T23:59:59Z') // Future date
|
||||
]
|
||||
];
|
||||
|
||||
edgeDates.forEach(date => {
|
||||
expect(() => formatLocalDateTime(date)).not.toThrow()
|
||||
const result = formatLocalDateTime(date)
|
||||
expect(typeof result).toBe('string')
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
expect(() => formatLocalDateTime(date)).not.toThrow();
|
||||
const result = formatLocalDateTime(date);
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be consistent for the same date', () => {
|
||||
const result1 = formatLocalDateTime(mockDate)
|
||||
const result2 = formatLocalDateTime(mockDate)
|
||||
const result1 = formatLocalDateTime(mockDate);
|
||||
const result2 = formatLocalDateTime(mockDate);
|
||||
|
||||
expect(result1).toBe(result2)
|
||||
})
|
||||
expect(result1).toBe(result2);
|
||||
});
|
||||
|
||||
it('should handle leap year dates', () => {
|
||||
const leapYearDate = new Date('2024-02-29T12:00:00Z') // Leap year
|
||||
const leapYearDate = new Date('2024-02-29T12:00:00Z'); // Leap year
|
||||
|
||||
expect(() => formatLocalDateTime(leapYearDate)).not.toThrow()
|
||||
const result = formatLocalDateTime(leapYearDate)
|
||||
expect(result).toContain('2024')
|
||||
expect(result).toContain('02')
|
||||
expect(result).toContain('29')
|
||||
})
|
||||
})
|
||||
expect(() => formatLocalDateTime(leapYearDate)).not.toThrow();
|
||||
const result = formatLocalDateTime(leapYearDate);
|
||||
expect(result).toContain('2024');
|
||||
expect(result).toContain('02');
|
||||
expect(result).toContain('29');
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration tests', () => {
|
||||
it('should work together for typical DAW workflow', () => {
|
||||
// Parse time signature
|
||||
const timeSignature = parseTimeSignature('4/4')
|
||||
expect(timeSignature).not.toBeNull()
|
||||
const timeSignature = parseTimeSignature('4/4');
|
||||
expect(timeSignature).not.toBeNull();
|
||||
|
||||
// Use parsed time signature in time formatting
|
||||
const timeString = beatsToTimeString(16, 120, timeSignature!)
|
||||
expect(timeString).toBe('005:1 | 00:08:000')
|
||||
const timeString = beatsToTimeString(16, 120, timeSignature!);
|
||||
expect(timeString).toBe('005:1 | 00:08:000');
|
||||
|
||||
// Format current time
|
||||
const now = new Date()
|
||||
const formattedTime = formatLocalDateTime(now)
|
||||
expect(formattedTime).toBeDefined()
|
||||
})
|
||||
const now = new Date();
|
||||
const formattedTime = formatLocalDateTime(now);
|
||||
expect(formattedTime).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle error cases gracefully in workflow', () => {
|
||||
// Invalid time signature should not break workflow
|
||||
const invalidTimeSignature = parseTimeSignature('invalid')
|
||||
expect(invalidTimeSignature).toBeNull()
|
||||
const invalidTimeSignature = parseTimeSignature('invalid');
|
||||
expect(invalidTimeSignature).toBeNull();
|
||||
|
||||
// Get error message for user feedback
|
||||
const errorMessage = getTimeSignatureErrorMessage()
|
||||
expect(errorMessage).toContain('Invalid time signature')
|
||||
const errorMessage = getTimeSignatureErrorMessage();
|
||||
expect(errorMessage).toContain('Invalid time signature');
|
||||
|
||||
// Fallback to default time signature
|
||||
const fallbackTimeSignature = { numerator: 4, denominator: 4 }
|
||||
const timeString = beatsToTimeString(8, 120, fallbackTimeSignature)
|
||||
expect(timeString).toBe('003:1 | 00:04:000')
|
||||
})
|
||||
})
|
||||
})
|
||||
const fallbackTimeSignature = { numerator: 4, denominator: 4 };
|
||||
const timeString = beatsToTimeString(8, 120, fallbackTimeSignature);
|
||||
expect(timeString).toBe('003:1 | 00:04:000');
|
||||
});
|
||||
});
|
||||
});
|
||||
+204
-204
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { extractXMLFromString, wrapXmlBlocksInContent } from './xmlUtil'
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { extractXMLFromString, wrapXmlBlocksInContent } from './xmlUtil';
|
||||
import {
|
||||
longTextWithAddNotes,
|
||||
longTextWithReadMusic,
|
||||
@@ -9,323 +9,323 @@ import {
|
||||
malformedXml,
|
||||
noXmlContent,
|
||||
emptyAndWhitespaceXml
|
||||
} from '../test/fixtures/xml-samples'
|
||||
} from '../test/fixtures/xml-samples';
|
||||
|
||||
describe('xmlUtil', () => {
|
||||
describe('extractXMLFromString', () => {
|
||||
it('should extract simple XML blocks', () => {
|
||||
const input = `Here is some text with <test>content</test> and more text.`
|
||||
const result = extractXMLFromString(input)
|
||||
const input = `Here is some text with <test>content</test> and more text.`;
|
||||
const result = extractXMLFromString(input);
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toBe('<test>content</test>')
|
||||
})
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBe('<test>content</test>');
|
||||
});
|
||||
|
||||
it('should extract multiple XML blocks', () => {
|
||||
const input = `<first>content1</first> some text <second>content2</second>`
|
||||
const result = extractXMLFromString(input)
|
||||
const input = `<first>content1</first> some text <second>content2</second>`;
|
||||
const result = extractXMLFromString(input);
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0]).toBe('<first>content1</first>')
|
||||
expect(result[1]).toBe('<second>content2</second>')
|
||||
})
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toBe('<first>content1</first>');
|
||||
expect(result[1]).toBe('<second>content2</second>');
|
||||
});
|
||||
|
||||
it('should extract XML blocks with nested elements', () => {
|
||||
const input = `<outer><inner>nested content</inner></outer>`
|
||||
const result = extractXMLFromString(input)
|
||||
const input = `<outer><inner>nested content</inner></outer>`;
|
||||
const result = extractXMLFromString(input);
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toBe('<outer><inner>nested content</inner></outer>')
|
||||
})
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBe('<outer><inner>nested content</inner></outer>');
|
||||
});
|
||||
|
||||
it('should extract XML blocks with attributes', () => {
|
||||
const input = `<tag attr="value" id="123">content</tag>`
|
||||
const result = extractXMLFromString(input)
|
||||
const input = `<tag attr="value" id="123">content</tag>`;
|
||||
const result = extractXMLFromString(input);
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toBe('<tag attr="value" id="123">content</tag>')
|
||||
})
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBe('<tag attr="value" id="123">content</tag>');
|
||||
});
|
||||
|
||||
it('should handle multiline XML blocks', () => {
|
||||
const input = `<multiline>
|
||||
<line1>content1</line1>
|
||||
<line2>content2</line2>
|
||||
</multiline>`
|
||||
const result = extractXMLFromString(input)
|
||||
</multiline>`;
|
||||
const result = extractXMLFromString(input);
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toContain('<multiline>')
|
||||
expect(result[0]).toContain('<line1>content1</line1>')
|
||||
expect(result[0]).toContain('<line2>content2</line2>')
|
||||
expect(result[0]).toContain('</multiline>')
|
||||
})
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toContain('<multiline>');
|
||||
expect(result[0]).toContain('<line1>content1</line1>');
|
||||
expect(result[0]).toContain('<line2>content2</line2>');
|
||||
expect(result[0]).toContain('</multiline>');
|
||||
});
|
||||
|
||||
it('should extract XML from complex nested content fixture', () => {
|
||||
const result = extractXMLFromString(nestedXmlContent)
|
||||
const result = extractXMLFromString(nestedXmlContent);
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toContain('<container>')
|
||||
expect(result[0]).toContain('<inner_element>')
|
||||
expect(result[0]).toContain('<deep_nested>')
|
||||
expect(result[0]).toContain('<value>Test Content</value>')
|
||||
expect(result[0]).toContain('</container>')
|
||||
})
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toContain('<container>');
|
||||
expect(result[0]).toContain('<inner_element>');
|
||||
expect(result[0]).toContain('<deep_nested>');
|
||||
expect(result[0]).toContain('<value>Test Content</value>');
|
||||
expect(result[0]).toContain('</container>');
|
||||
});
|
||||
|
||||
it('should extract multiple XML blocks from fixture', () => {
|
||||
const result = extractXMLFromString(multipleXmlBlocks)
|
||||
const result = extractXMLFromString(multipleXmlBlocks);
|
||||
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result[0]).toContain('<add_notes>')
|
||||
expect(result[0]).toContain('</add_notes>')
|
||||
expect(result[1]).toContain('<read_music>')
|
||||
expect(result[1]).toContain('</read_music>')
|
||||
expect(result[2]).toContain('<modify_tempo>')
|
||||
expect(result[2]).toContain('</modify_tempo>')
|
||||
})
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0]).toContain('<add_notes>');
|
||||
expect(result[0]).toContain('</add_notes>');
|
||||
expect(result[1]).toContain('<read_music>');
|
||||
expect(result[1]).toContain('</read_music>');
|
||||
expect(result[2]).toContain('<modify_tempo>');
|
||||
expect(result[2]).toContain('</modify_tempo>');
|
||||
});
|
||||
|
||||
it('should extract XML with attributes from fixture', () => {
|
||||
const result = extractXMLFromString(xmlWithAttributes)
|
||||
const result = extractXMLFromString(xmlWithAttributes);
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toContain('region_id="main"')
|
||||
expect(result[0]).toContain('track="melody"')
|
||||
expect(result[0]).toContain('id="1"')
|
||||
expect(result[0]).toContain('velocity="127"')
|
||||
})
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toContain('region_id="main"');
|
||||
expect(result[0]).toContain('track="melody"');
|
||||
expect(result[0]).toContain('id="1"');
|
||||
expect(result[0]).toContain('velocity="127"');
|
||||
});
|
||||
|
||||
it('should handle the long text with add_notes fixture (Case 1)', () => {
|
||||
const result = extractXMLFromString(longTextWithAddNotes)
|
||||
const result = extractXMLFromString(longTextWithAddNotes);
|
||||
|
||||
expect(result).toHaveLength(2) // Contains thinking tag and add_notes block
|
||||
expect(result).toHaveLength(2); // Contains thinking tag and add_notes block
|
||||
|
||||
// Find the add_notes block
|
||||
const addNotesBlock = result.find(block => block.includes('<add_notes>'))
|
||||
expect(addNotesBlock).toBeDefined()
|
||||
expect(addNotesBlock).toContain('<notes>')
|
||||
expect(addNotesBlock).toContain('<note>')
|
||||
expect(addNotesBlock).toContain('<pitch>C4</pitch>')
|
||||
expect(addNotesBlock).toContain('<start_beat>0</start_beat>')
|
||||
expect(addNotesBlock).toContain('<length>4</length>')
|
||||
expect(addNotesBlock).toContain('</add_notes>')
|
||||
const addNotesBlock = result.find(block => block.includes('<add_notes>'));
|
||||
expect(addNotesBlock).toBeDefined();
|
||||
expect(addNotesBlock).toContain('<notes>');
|
||||
expect(addNotesBlock).toContain('<note>');
|
||||
expect(addNotesBlock).toContain('<pitch>C4</pitch>');
|
||||
expect(addNotesBlock).toContain('<start_beat>0</start_beat>');
|
||||
expect(addNotesBlock).toContain('<length>4</length>');
|
||||
expect(addNotesBlock).toContain('</add_notes>');
|
||||
|
||||
// Should contain all 12 notes
|
||||
const noteMatches = addNotesBlock!.match(/<note>/g)
|
||||
expect(noteMatches).toHaveLength(12)
|
||||
})
|
||||
const noteMatches = addNotesBlock!.match(/<note>/g);
|
||||
expect(noteMatches).toHaveLength(12);
|
||||
});
|
||||
|
||||
it('should handle the long text with read_music fixture (Case 2)', () => {
|
||||
const result = extractXMLFromString(longTextWithReadMusic)
|
||||
const result = extractXMLFromString(longTextWithReadMusic);
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result).toHaveLength(2);
|
||||
|
||||
// First XML block (inside thinking tag)
|
||||
expect(result[0]).toContain('<read_music>')
|
||||
expect(result[0]).toContain('<start_beat>0</start_beat>')
|
||||
expect(result[0]).toContain('<length>32</length>')
|
||||
expect(result[0]).toContain('</read_music>')
|
||||
expect(result[0]).toContain('<read_music>');
|
||||
expect(result[0]).toContain('<start_beat>0</start_beat>');
|
||||
expect(result[0]).toContain('<length>32</length>');
|
||||
expect(result[0]).toContain('</read_music>');
|
||||
|
||||
// Second XML block (at the end)
|
||||
expect(result[1]).toContain('<read_music>')
|
||||
expect(result[1]).toContain('<start_beat>0</start_beat>')
|
||||
expect(result[1]).toContain('<length>32</length>')
|
||||
expect(result[1]).toContain('</read_music>')
|
||||
})
|
||||
expect(result[1]).toContain('<read_music>');
|
||||
expect(result[1]).toContain('<start_beat>0</start_beat>');
|
||||
expect(result[1]).toContain('<length>32</length>');
|
||||
expect(result[1]).toContain('</read_music>');
|
||||
});
|
||||
|
||||
it('should handle malformed XML gracefully', () => {
|
||||
const result = extractXMLFromString(malformedXml)
|
||||
const result = extractXMLFromString(malformedXml);
|
||||
|
||||
// Should extract valid XML blocks (ignores malformed ones)
|
||||
expect(result.length).toBeGreaterThanOrEqual(1)
|
||||
expect(result.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Find the definitely valid block
|
||||
const validBlock = result.find(block => block.includes('<valid_tag>'))
|
||||
expect(validBlock).toBeDefined()
|
||||
expect(validBlock).toContain('<content>This is valid</content>')
|
||||
})
|
||||
const validBlock = result.find(block => block.includes('<valid_tag>'));
|
||||
expect(validBlock).toBeDefined();
|
||||
expect(validBlock).toContain('<content>This is valid</content>');
|
||||
});
|
||||
|
||||
it('should return empty array for content with no XML', () => {
|
||||
const result = extractXMLFromString(noXmlContent)
|
||||
const result = extractXMLFromString(noXmlContent);
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
expect(result).toHaveLength(0);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle empty and whitespace XML', () => {
|
||||
const result = extractXMLFromString(emptyAndWhitespaceXml)
|
||||
const result = extractXMLFromString(emptyAndWhitespaceXml);
|
||||
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result[0]).toBe('<empty_tag></empty_tag>')
|
||||
expect(result[1]).toContain('<whitespace_tag>')
|
||||
expect(result[1]).toContain('</whitespace_tag>')
|
||||
expect(result[2]).toContain('<mixed_content>')
|
||||
expect(result[2]).toContain('Some text with spaces')
|
||||
expect(result[2]).toContain('</mixed_content>')
|
||||
})
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0]).toBe('<empty_tag></empty_tag>');
|
||||
expect(result[1]).toContain('<whitespace_tag>');
|
||||
expect(result[1]).toContain('</whitespace_tag>');
|
||||
expect(result[2]).toContain('<mixed_content>');
|
||||
expect(result[2]).toContain('Some text with spaces');
|
||||
expect(result[2]).toContain('</mixed_content>');
|
||||
});
|
||||
|
||||
it('should handle XML with underscores and hyphens in tag names', () => {
|
||||
const input = `<tag_name>content</tag_name> and <tag-name>content</tag-name>`
|
||||
const result = extractXMLFromString(input)
|
||||
const input = `<tag_name>content</tag_name> and <tag-name>content</tag-name>`;
|
||||
const result = extractXMLFromString(input);
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0]).toBe('<tag_name>content</tag_name>')
|
||||
expect(result[1]).toBe('<tag-name>content</tag-name>')
|
||||
})
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toBe('<tag_name>content</tag_name>');
|
||||
expect(result[1]).toBe('<tag-name>content</tag-name>');
|
||||
});
|
||||
|
||||
it('should handle self-closing tags (not currently supported)', () => {
|
||||
const input = `<self-closing /> and <normal>content</normal>`
|
||||
const result = extractXMLFromString(input)
|
||||
const input = `<self-closing /> and <normal>content</normal>`;
|
||||
const result = extractXMLFromString(input);
|
||||
|
||||
// Current implementation doesn't support self-closing tags
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toBe('<normal>content</normal>')
|
||||
})
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBe('<normal>content</normal>');
|
||||
});
|
||||
|
||||
it('should trim whitespace around extracted XML', () => {
|
||||
const input = ` <tag>content</tag> `
|
||||
const result = extractXMLFromString(input)
|
||||
const input = ` <tag>content</tag> `;
|
||||
const result = extractXMLFromString(input);
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toBe('<tag>content</tag>')
|
||||
})
|
||||
})
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBe('<tag>content</tag>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('wrapXmlBlocksInContent', () => {
|
||||
it('should wrap single XML block in fenced code block', () => {
|
||||
const input = `Here is <test>content</test> in text.`
|
||||
const result = wrapXmlBlocksInContent(input)
|
||||
const input = `Here is <test>content</test> in text.`;
|
||||
const result = wrapXmlBlocksInContent(input);
|
||||
|
||||
expect(result).toBe('Here is ```xml\n<test>content</test>\n``` in text.')
|
||||
})
|
||||
expect(result).toBe('Here is ```xml\n<test>content</test>\n``` in text.');
|
||||
});
|
||||
|
||||
it('should wrap multiple XML blocks', () => {
|
||||
const input = `<first>content1</first> text <second>content2</second>`
|
||||
const result = wrapXmlBlocksInContent(input)
|
||||
const input = `<first>content1</first> text <second>content2</second>`;
|
||||
const result = wrapXmlBlocksInContent(input);
|
||||
|
||||
expect(result).toContain('```xml\n<first>content1</first>\n```')
|
||||
expect(result).toContain('```xml\n<second>content2</second>\n```')
|
||||
})
|
||||
expect(result).toContain('```xml\n<first>content1</first>\n```');
|
||||
expect(result).toContain('```xml\n<second>content2</second>\n```');
|
||||
});
|
||||
|
||||
it('should return original content when no XML blocks present', () => {
|
||||
const input = noXmlContent
|
||||
const result = wrapXmlBlocksInContent(input)
|
||||
const input = noXmlContent;
|
||||
const result = wrapXmlBlocksInContent(input);
|
||||
|
||||
expect(result).toBe(input)
|
||||
})
|
||||
expect(result).toBe(input);
|
||||
});
|
||||
|
||||
it('should handle empty input', () => {
|
||||
expect(wrapXmlBlocksInContent('')).toBe('')
|
||||
expect(wrapXmlBlocksInContent(null as any)).toBeNull()
|
||||
expect(wrapXmlBlocksInContent(undefined as any)).toBeUndefined()
|
||||
})
|
||||
expect(wrapXmlBlocksInContent('')).toBe('');
|
||||
expect(wrapXmlBlocksInContent(null as any)).toBeNull();
|
||||
expect(wrapXmlBlocksInContent(undefined as any)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should wrap XML blocks from multipleXmlBlocks fixture', () => {
|
||||
const result = wrapXmlBlocksInContent(multipleXmlBlocks)
|
||||
const result = wrapXmlBlocksInContent(multipleXmlBlocks);
|
||||
|
||||
expect(result).toContain('```xml\n<add_notes>')
|
||||
expect(result).toContain('</add_notes>\n```')
|
||||
expect(result).toContain('```xml\n<read_music>')
|
||||
expect(result).toContain('</read_music>\n```')
|
||||
expect(result).toContain('```xml\n<modify_tempo>')
|
||||
expect(result).toContain('</modify_tempo>\n```')
|
||||
expect(result).toContain('```xml\n<add_notes>');
|
||||
expect(result).toContain('</add_notes>\n```');
|
||||
expect(result).toContain('```xml\n<read_music>');
|
||||
expect(result).toContain('</read_music>\n```');
|
||||
expect(result).toContain('```xml\n<modify_tempo>');
|
||||
expect(result).toContain('</modify_tempo>\n```');
|
||||
|
||||
// Should preserve the surrounding text
|
||||
expect(result).toContain('Here\'s how to add multiple musical elements:')
|
||||
expect(result).toContain('First, let\'s add some notes:')
|
||||
expect(result).toContain('Then we can read the current music:')
|
||||
})
|
||||
expect(result).toContain('Here\'s how to add multiple musical elements:');
|
||||
expect(result).toContain('First, let\'s add some notes:');
|
||||
expect(result).toContain('Then we can read the current music:');
|
||||
});
|
||||
|
||||
it('should wrap the long add_notes XML block (Case 1)', () => {
|
||||
const result = wrapXmlBlocksInContent(longTextWithAddNotes)
|
||||
const result = wrapXmlBlocksInContent(longTextWithAddNotes);
|
||||
|
||||
expect(result).toContain('```xml\n<add_notes>')
|
||||
expect(result).toContain('</add_notes>\n```')
|
||||
expect(result).toContain('```xml\n<add_notes>');
|
||||
expect(result).toContain('</add_notes>\n```');
|
||||
|
||||
// Should preserve the thinking content and other text
|
||||
expect(result).toContain('<thinking>')
|
||||
expect(result).toContain('Perfect! I can see this is a beautiful')
|
||||
expect(result).toContain('Let me start by adding the harmony')
|
||||
})
|
||||
expect(result).toContain('<thinking>');
|
||||
expect(result).toContain('Perfect! I can see this is a beautiful');
|
||||
expect(result).toContain('Let me start by adding the harmony');
|
||||
});
|
||||
|
||||
it('should wrap the long read_music XML blocks (Case 2)', () => {
|
||||
const result = wrapXmlBlocksInContent(longTextWithReadMusic)
|
||||
const result = wrapXmlBlocksInContent(longTextWithReadMusic);
|
||||
|
||||
// Should contain two wrapped XML blocks
|
||||
const fencedBlocks = result.match(/```xml\n<read_music>[\s\S]*?<\/read_music>\n```/g)
|
||||
expect(fencedBlocks).toHaveLength(2)
|
||||
const fencedBlocks = result.match(/```xml\n<read_music>[\s\S]*?<\/read_music>\n```/g);
|
||||
expect(fencedBlocks).toHaveLength(2);
|
||||
|
||||
// Should preserve the thinking content and other text
|
||||
expect(result).toContain('<thinking>')
|
||||
expect(result).toContain('I\'ll help you create a pad harmony track')
|
||||
expect(result).toContain('Let me first read the existing music')
|
||||
})
|
||||
expect(result).toContain('<thinking>');
|
||||
expect(result).toContain('I\'ll help you create a pad harmony track');
|
||||
expect(result).toContain('Let me first read the existing music');
|
||||
});
|
||||
|
||||
it('should handle nested XML correctly', () => {
|
||||
const result = wrapXmlBlocksInContent(nestedXmlContent)
|
||||
const result = wrapXmlBlocksInContent(nestedXmlContent);
|
||||
|
||||
expect(result).toContain('```xml\n<container>')
|
||||
expect(result).toContain('<inner_element>')
|
||||
expect(result).toContain('<deep_nested>')
|
||||
expect(result).toContain('<value>Test Content</value>')
|
||||
expect(result).toContain('</container>\n```')
|
||||
})
|
||||
expect(result).toContain('```xml\n<container>');
|
||||
expect(result).toContain('<inner_element>');
|
||||
expect(result).toContain('<deep_nested>');
|
||||
expect(result).toContain('<value>Test Content</value>');
|
||||
expect(result).toContain('</container>\n```');
|
||||
});
|
||||
|
||||
it('should preserve XML block integrity when wrapping', () => {
|
||||
const input = `Text before\n<complex>\n <nested>value</nested>\n <another>content</another>\n</complex>\nText after`
|
||||
const result = wrapXmlBlocksInContent(input)
|
||||
const input = `Text before\n<complex>\n <nested>value</nested>\n <another>content</another>\n</complex>\nText after`;
|
||||
const result = wrapXmlBlocksInContent(input);
|
||||
|
||||
expect(result).toBe(`Text before\n\`\`\`xml\n<complex>\n <nested>value</nested>\n <another>content</another>\n</complex>\n\`\`\`\nText after`)
|
||||
})
|
||||
expect(result).toBe(`Text before\n\`\`\`xml\n<complex>\n <nested>value</nested>\n <another>content</another>\n</complex>\n\`\`\`\nText after`);
|
||||
});
|
||||
|
||||
it('should handle duplicate XML blocks correctly', () => {
|
||||
const input = `<same>content</same> and then <same>content</same> again`
|
||||
const result = wrapXmlBlocksInContent(input)
|
||||
const input = `<same>content</same> and then <same>content</same> again`;
|
||||
const result = wrapXmlBlocksInContent(input);
|
||||
|
||||
// Both instances should be wrapped
|
||||
const wrappedBlocks = result.match(/```xml\n<same>content<\/same>\n```/g)
|
||||
expect(wrappedBlocks).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
const wrappedBlocks = result.match(/```xml\n<same>content<\/same>\n```/g);
|
||||
expect(wrappedBlocks).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases and error handling', () => {
|
||||
it('should handle very large XML blocks', () => {
|
||||
const largeContent = 'x'.repeat(10000)
|
||||
const input = `<large>${largeContent}</large>`
|
||||
const largeContent = 'x'.repeat(10000);
|
||||
const input = `<large>${largeContent}</large>`;
|
||||
|
||||
const extracted = extractXMLFromString(input)
|
||||
expect(extracted).toHaveLength(1)
|
||||
expect(extracted[0]).toContain(largeContent)
|
||||
const extracted = extractXMLFromString(input);
|
||||
expect(extracted).toHaveLength(1);
|
||||
expect(extracted[0]).toContain(largeContent);
|
||||
|
||||
const wrapped = wrapXmlBlocksInContent(input)
|
||||
expect(wrapped).toContain('```xml\n<large>')
|
||||
expect(wrapped).toContain('</large>\n```')
|
||||
})
|
||||
const wrapped = wrapXmlBlocksInContent(input);
|
||||
expect(wrapped).toContain('```xml\n<large>');
|
||||
expect(wrapped).toContain('</large>\n```');
|
||||
});
|
||||
|
||||
it('should handle XML with special characters', () => {
|
||||
const input = `<tag>Content with < > & " '</tag>`
|
||||
const input = `<tag>Content with < > & " '</tag>`;
|
||||
|
||||
const extracted = extractXMLFromString(input)
|
||||
expect(extracted).toHaveLength(1)
|
||||
expect(extracted[0]).toContain('< > & " '')
|
||||
const extracted = extractXMLFromString(input);
|
||||
expect(extracted).toHaveLength(1);
|
||||
expect(extracted[0]).toContain('< > & " '');
|
||||
|
||||
const wrapped = wrapXmlBlocksInContent(input)
|
||||
expect(wrapped).toContain('```xml\n<tag>Content with < > & " '</tag>\n```')
|
||||
})
|
||||
const wrapped = wrapXmlBlocksInContent(input);
|
||||
expect(wrapped).toContain('```xml\n<tag>Content with < > & " '</tag>\n```');
|
||||
});
|
||||
|
||||
it('should handle XML with CDATA sections', () => {
|
||||
const input = `<tag><![CDATA[Some content with <special> chars]]></tag>`
|
||||
const input = `<tag><![CDATA[Some content with <special> chars]]></tag>`;
|
||||
|
||||
const extracted = extractXMLFromString(input)
|
||||
expect(extracted).toHaveLength(1)
|
||||
expect(extracted[0]).toContain('<![CDATA[')
|
||||
expect(extracted[0]).toContain(']]>')
|
||||
})
|
||||
const extracted = extractXMLFromString(input);
|
||||
expect(extracted).toHaveLength(1);
|
||||
expect(extracted[0]).toContain('<![CDATA[');
|
||||
expect(extracted[0]).toContain(']]>');
|
||||
});
|
||||
|
||||
it('should handle mixed content with partial XML-like text', () => {
|
||||
const input = `This < is not XML and neither > is this <incomplete and <valid>content</valid> is valid`
|
||||
const input = `This < is not XML and neither > is this <incomplete and <valid>content</valid> is valid`;
|
||||
|
||||
const extracted = extractXMLFromString(input)
|
||||
expect(extracted).toHaveLength(1)
|
||||
expect(extracted[0]).toBe('<valid>content</valid>')
|
||||
})
|
||||
})
|
||||
})
|
||||
const extracted = extractXMLFromString(input);
|
||||
expect(extracted).toHaveLength(1);
|
||||
expect(extracted[0]).toBe('<valid>content</valid>');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user