// SonicForge Studio - chordTheory.js unit test (BIG_PHASE_3.md §6.4) // Node assert, không framework. Chạy: node app/static/js/services/chordTheory.test.js const assert = require('assert'); const ChordTheory = require('./chordTheory.js'); let fails = 0; function check(name, fn) { try { fn(); console.log('ok: ' + name); } catch (e) { fails++; console.error('FAIL: ' + name + ' — ' + e.message); } } function deep(arr, expected) { assert.deepStrictEqual(arr, expected); } // 1. Parse chord symbol → pitch array (octave 4 = C4=60) check('Cmaj7 = [60,64,67,71]', () => deep(ChordTheory.parseChordSymbol('Cmaj7'), [60, 64, 67, 71])); check('Am9 = [69,72,76,79,83]', () => deep(ChordTheory.parseChordSymbol('Am9'), [69, 72, 76, 79, 83])); check('Dm7 = [62,65,69,72]', () => deep(ChordTheory.parseChordSymbol('Dm7'), [62, 65, 69, 72])); check('G13 = [67,71,74,77,81,88]', () => deep(ChordTheory.parseChordSymbol('G13'), [67, 71, 74, 77, 81, 88])); check('Fadd9 = [65,69,72,79]', () => deep(ChordTheory.parseChordSymbol('Fadd9'), [65, 69, 72, 79])); // 2. Roman numeral progression với key C check('I-vi-IV-V key C → C-Am-F-G', () => { const prog = ChordTheory.progressionToChords('I-vi-IV-V', 'C', 'root'); deep(prog.map(c => c.name), ['C', 'Am', 'F', 'G']); deep(prog.map(c => c.pitches), [[60, 64, 67], [69, 72, 76], [65, 69, 72], [67, 71, 74]]); }); check('I-V-vi-IV key C → C-G-Am-F (preset Pop)', () => { const prog = ChordTheory.progressionToChords('I-V-vi-IV', 'C', 'root'); deep(prog.map(c => c.name), ['C', 'G', 'Am', 'F']); }); check('ii7-V7-Imaj7-VI7 key C → Dm7-G7-Cmaj7-A7', () => { const prog = ChordTheory.progressionToChords('ii7-V7-Imaj7-VI7', 'C', 'root'); deep(prog.map(c => c.name), ['Dm7', 'G7', 'Cmaj7', 'A7']); deep(prog[0].pitches, [62, 65, 69, 72]); // Dm7 deep(prog[2].pitches, [60, 64, 67, 71]); // Cmaj7 }); check('i7-iv7-v7-i7 key C → Cm7-Fm7-Gm7-Cm7', () => { const prog = ChordTheory.progressionToChords('i7-iv7-v7-i7', 'C', 'root'); deep(prog.map(c => c.name), ['Cm7', 'Fm7', 'Gm7', 'Cm7']); }); check('Imaj7-iii7-IVmaj7-V13 key C → Cmaj7-Em7-Fmaj7-G13', () => { const prog = ChordTheory.progressionToChords('Imaj7-iii7-IVmaj7-V13', 'C', 'root'); deep(prog.map(c => c.name), ['Cmaj7', 'Em7', 'Fmaj7', 'G13']); }); // 3. Voicing check('first inversion: root +12', () => deep(ChordTheory.applyVoicing([60, 64, 67, 71], 'first'), [64, 67, 71, 72])); check('drop2: second-highest -12', () => deep(ChordTheory.applyVoicing([60, 64, 67, 71], 'drop2'), [55, 60, 64, 71])); check('root position giữ nguyên', () => deep(ChordTheory.applyVoicing([60, 64, 67, 71], 'root'), [60, 64, 67, 71])); check('jazz drop2: Dm7-G7-Cmaj7-A7 ra đủ pitch', () => { const prog = ChordTheory.progressionToChords('ii7-V7-Imaj7-VI7', 'C', 'drop2'); deep(prog.map(c => c.name), ['Dm7', 'G7', 'Cmaj7', 'A7']); assert(prog.every(c => c.pitches.length === 4), 'mỗi chord 4 pitch'); }); // 4. Edge cases check('chuỗi chord name trực tiếp Cmaj7-Am9-Fadd9-G13', () => { const prog = ChordTheory.progressionToChords('Cmaj7-Am9-Fadd9-G13', 'C', 'root'); deep(prog.map(c => c.name), ['Cmaj7', 'Am9', 'Fadd9', 'G13']); deep(prog[0].pitches, [60, 64, 67, 71]); }); console.log(fails ? 'FAILED: ' + fails : 'ALL PASS'); process.exit(fails ? 1 : 0);