import { beforeEach, describe, expect, it } from 'vitest'; import { Note } from 'tonal'; import { KGCore } from '../core/KGCore'; import type { ChordGuideData } from '../core/ChordGuideTypes'; import { getMatchingChordGuideChordsForPitch, resolveChordGuideItems } from './chordGuideDataUtil'; import { getChordPitchClasses, parseChordSymbol } from './chordUtil'; import chordGuideDataJson from '../../public/resources/modes/chord_guide.json'; const chordGuideData = chordGuideDataJson as ChordGuideData; function getExpectedPitchClassesFromNotes(notes: string[]): number[] { return notes.map((note) => { const parsed = Note.get(note); if (parsed.empty || parsed.chroma === undefined) { throw new Error(`Unable to convert note "${note}" to pitch class`); } return parsed.chroma; }); } describe('chordGuideDataUtil', () => { beforeEach(() => { KGCore.CHORD_GUIDE_DATA = chordGuideData; KGCore.FUNCTIONAL_CHORDS_DATA = { ionian: { name: 'Broken', steps: [2, 2, 2, 2, 2, 1, 1], T: [], S: [], D: [], chords: {} }, }; }); it('parses every chord symbol used by chord_guide.json', () => { const allItems = [ ...chordGuideData.ionian.T, ...chordGuideData.ionian.S, ...chordGuideData.ionian.D, ...chordGuideData.aeolian.T, ...chordGuideData.aeolian.S, ...chordGuideData.aeolian.D, ]; for (const item of allItems) { const parsed = parseChordSymbol(item.name); expect(parsed, item.name).not.toBeNull(); expect(parsed?.symbol).toBe(item.name); } }); it('maps every chord guide symbol to the corresponding notes declared in chord_guide.json', () => { const allItems = [ ...chordGuideData.ionian.T, ...chordGuideData.ionian.S, ...chordGuideData.ionian.D, ...chordGuideData.aeolian.T, ...chordGuideData.aeolian.S, ...chordGuideData.aeolian.D, ]; for (const item of allItems) { const actualPitchClasses = getChordPitchClasses(item.name).map((pitch) => ((pitch % 12) + 12) % 12); const expectedPitchClasses = getExpectedPitchClassesFromNotes(item.notes); expect( actualPitchClasses, `${item.name} should resolve to ${item.notes.join(' ')}` ).toEqual(expectedPitchClasses); } }); it('resolves ionian tonic chords in C major from the new chord guide data', () => { const result = resolveChordGuideItems('C major', 'ionian', 'T'); expect(result[0]).toMatchObject({ name: 'C', roman: 'I', notes: ['C', 'E', 'G'], resolvedNotes: ['C', 'E', 'G'], pitchClasses: [0, 4, 7], }); expect(result.find((item) => item.name === 'Am')?.pitchClasses).toEqual([9, 12, 16]); }); it('transposes ionian tonic chords away from C major', () => { const result = resolveChordGuideItems('D major', 'ionian', 'T'); expect(result[0]).toMatchObject({ name: 'C', resolvedNotes: ['D', 'F#', 'A'], pitchClasses: [2, 6, 9], }); expect(result.find((item) => item.name === 'Am')?.resolvedNotes).toEqual(['B', 'D', 'F#']); }); it('uses A minor as the aeolian reference tonic', () => { const result = resolveChordGuideItems('A minor', 'aeolian', 'T'); expect(result[0]).toMatchObject({ name: 'Am', resolvedNotes: ['A', 'C', 'E'], pitchClasses: [9, 12, 16], }); expect(result.find((item) => item.name === 'Amadd9')?.resolvedNotes).toEqual(['A', 'C', 'E', 'B']); }); it('transposes aeolian tonic chords away from A minor', () => { const result = resolveChordGuideItems('E minor', 'aeolian', 'T'); expect(result[0]).toMatchObject({ name: 'Am', resolvedNotes: ['E', 'G', 'B'], pitchClasses: [4, 7, 11], }); expect(result.find((item) => item.name === 'C')?.resolvedNotes).toEqual(['G', 'B', 'D']); }); it('matches hover chords without depending on functional chord config', () => { const result = getMatchingChordGuideChordsForPitch(60, 'C major', 'ionian', 'T'); expect(result[0]).toEqual([0, 4, 7]); expect(result.some((chord) => chord.includes(0))).toBe(true); }); });