feat: added unit tests for KGMidiRegion and abcNotationUtil; update all deprecated method plainToClass to plainToInstance.
This commit is contained in:
@@ -196,6 +196,11 @@ body {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.export-dropdown .quant-dropdown {
|
||||
width: 250px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Main content */
|
||||
.main-content {
|
||||
display: flex;
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
FaCog
|
||||
} from 'react-icons/fa';
|
||||
import { KGProject, type KeySignature } from '../core/KGProject';
|
||||
import { plainToClass, instanceToPlain } from 'class-transformer';
|
||||
import { plainToInstance, instanceToPlain } from 'class-transformer';
|
||||
import { FaPencil, FaCopy, FaPaste, FaTrash } from 'react-icons/fa6';
|
||||
import { KGMainContentState } from '../core/state/KGMainContentState';
|
||||
import { regionDeleteManager } from '../util/regionDeleteUtil';
|
||||
@@ -315,9 +315,9 @@ const Toolbar: React.FC = () => {
|
||||
const projectData = JSON.parse(fileContent);
|
||||
|
||||
// Deserialize the project data using class-transformer (same as KGStorage)
|
||||
const deserializedResult = plainToClass(KGProject, projectData);
|
||||
const deserializedResult = plainToInstance(KGProject, projectData);
|
||||
|
||||
// Handle case where plainToClass might return an array
|
||||
// Handle case where plainToInstance might return an array
|
||||
const deserializedProject = Array.isArray(deserializedResult)
|
||||
? deserializedResult[0] || null
|
||||
: deserializedResult;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { openDB } from 'idb'
|
||||
import type { IDBPDatabase } from 'idb'
|
||||
import { plainToClass, instanceToPlain } from 'class-transformer'
|
||||
import { plainToInstance, instanceToPlain } from 'class-transformer'
|
||||
import { DB_CONSTANTS } from '../../constants/coreConstants'
|
||||
|
||||
export interface StorageEntry {
|
||||
@@ -96,7 +96,7 @@ export class KGStorage {
|
||||
return null
|
||||
}
|
||||
|
||||
const instance = plainToClass(classType, entry.data)
|
||||
const instance = plainToInstance(classType, entry.data)
|
||||
const loadedInstance = Array.isArray(instance) ? instance[0] || null : instance
|
||||
|
||||
if (loadedInstance && typeof (loadedInstance as { setName?: (projectName: string) => void }).setName === 'function') {
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { KGMidiRegion } from './KGMidiRegion'
|
||||
import { KGRegion } from './KGRegion'
|
||||
import { KGMidiNote } from '../midi/KGMidiNote'
|
||||
import { createMockMidiNote } from '../../test/utils/mock-data'
|
||||
|
||||
describe('KGMidiRegion', () => {
|
||||
let region: KGMidiRegion
|
||||
|
||||
beforeEach(() => {
|
||||
region = new KGMidiRegion(
|
||||
'test-region-1',
|
||||
'test-track-1',
|
||||
0,
|
||||
'Test Region',
|
||||
0,
|
||||
4
|
||||
)
|
||||
})
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create a MIDI region with correct properties', () => {
|
||||
const testRegion = new KGMidiRegion(
|
||||
'region-1',
|
||||
'track-1',
|
||||
2,
|
||||
'My Region',
|
||||
4,
|
||||
8
|
||||
)
|
||||
|
||||
expect(testRegion.getId()).toBe('region-1')
|
||||
expect(testRegion.getTrackId()).toBe('track-1')
|
||||
expect(testRegion.getTrackIndex()).toBe(2)
|
||||
expect(testRegion.getName()).toBe('My Region')
|
||||
expect(testRegion.getStartFromBeat()).toBe(4)
|
||||
expect(testRegion.getLength()).toBe(8)
|
||||
expect(testRegion.getNotes()).toEqual([])
|
||||
})
|
||||
|
||||
it('should use default values for optional parameters', () => {
|
||||
const defaultRegion = new KGMidiRegion('region-1', 'track-1', 0, 'Default Region')
|
||||
|
||||
expect(defaultRegion.getStartFromBeat()).toBe(0)
|
||||
expect(defaultRegion.getLength()).toBe(0)
|
||||
expect(defaultRegion.getNotes()).toEqual([])
|
||||
})
|
||||
|
||||
it('should set the correct type identifier', () => {
|
||||
expect(region.getCurrentType()).toBe('KGMidiRegion')
|
||||
})
|
||||
})
|
||||
|
||||
describe('note management', () => {
|
||||
let note1: KGMidiNote
|
||||
let note2: KGMidiNote
|
||||
let note3: KGMidiNote
|
||||
|
||||
beforeEach(() => {
|
||||
note1 = createMockMidiNote({ id: 'note-1', pitch: 60, startBeat: 0, endBeat: 1 })
|
||||
note2 = createMockMidiNote({ id: 'note-2', pitch: 64, startBeat: 1, endBeat: 2 })
|
||||
note3 = createMockMidiNote({ id: 'note-3', pitch: 67, startBeat: 2, endBeat: 3 })
|
||||
})
|
||||
|
||||
describe('addNote', () => {
|
||||
it('should add a single note to empty region', () => {
|
||||
region.addNote(note1)
|
||||
|
||||
const notes = region.getNotes()
|
||||
expect(notes).toHaveLength(1)
|
||||
expect(notes[0]).toBe(note1)
|
||||
})
|
||||
|
||||
it('should add multiple notes to region', () => {
|
||||
region.addNote(note1)
|
||||
region.addNote(note2)
|
||||
region.addNote(note3)
|
||||
|
||||
const notes = region.getNotes()
|
||||
expect(notes).toHaveLength(3)
|
||||
expect(notes).toContain(note1)
|
||||
expect(notes).toContain(note2)
|
||||
expect(notes).toContain(note3)
|
||||
})
|
||||
|
||||
it('should maintain note order when adding', () => {
|
||||
region.addNote(note1)
|
||||
region.addNote(note2)
|
||||
region.addNote(note3)
|
||||
|
||||
const notes = region.getNotes()
|
||||
expect(notes[0]).toBe(note1)
|
||||
expect(notes[1]).toBe(note2)
|
||||
expect(notes[2]).toBe(note3)
|
||||
})
|
||||
|
||||
it('should allow adding the same note multiple times', () => {
|
||||
region.addNote(note1)
|
||||
region.addNote(note1)
|
||||
|
||||
const notes = region.getNotes()
|
||||
expect(notes).toHaveLength(2)
|
||||
expect(notes[0]).toBe(note1)
|
||||
expect(notes[1]).toBe(note1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeNote', () => {
|
||||
beforeEach(() => {
|
||||
region.addNote(note1)
|
||||
region.addNote(note2)
|
||||
region.addNote(note3)
|
||||
})
|
||||
|
||||
it('should remove note by ID', () => {
|
||||
region.removeNote('note-2')
|
||||
|
||||
const notes = region.getNotes()
|
||||
expect(notes).toHaveLength(2)
|
||||
expect(notes).toContain(note1)
|
||||
expect(notes).toContain(note3)
|
||||
expect(notes).not.toContain(note2)
|
||||
})
|
||||
|
||||
it('should handle removing non-existent note gracefully', () => {
|
||||
const initialLength = region.getNotes().length
|
||||
|
||||
region.removeNote('non-existent-note')
|
||||
|
||||
expect(region.getNotes()).toHaveLength(initialLength)
|
||||
})
|
||||
|
||||
it('should remove all instances when note ID appears multiple times', () => {
|
||||
// Add another note with same ID as note1
|
||||
const duplicateNote = createMockMidiNote({ id: 'note-1', pitch: 72, startBeat: 3, endBeat: 4 })
|
||||
region.addNote(duplicateNote)
|
||||
|
||||
expect(region.getNotes()).toHaveLength(4)
|
||||
|
||||
region.removeNote('note-1')
|
||||
|
||||
const notes = region.getNotes()
|
||||
expect(notes).toHaveLength(2)
|
||||
expect(notes).toContain(note2)
|
||||
expect(notes).toContain(note3)
|
||||
expect(notes).not.toContain(note1)
|
||||
expect(notes).not.toContain(duplicateNote)
|
||||
})
|
||||
|
||||
it('should handle removing from empty region', () => {
|
||||
const emptyRegion = new KGMidiRegion('empty', 'track', 0, 'Empty Region')
|
||||
|
||||
expect(() => emptyRegion.removeNote('note-1')).not.toThrow()
|
||||
expect(emptyRegion.getNotes()).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getNotes', () => {
|
||||
it('should return empty array for new region', () => {
|
||||
const notes = region.getNotes()
|
||||
expect(notes).toEqual([])
|
||||
expect(notes).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should return all notes in region', () => {
|
||||
region.addNote(note1)
|
||||
region.addNote(note2)
|
||||
|
||||
const notes = region.getNotes()
|
||||
expect(notes).toHaveLength(2)
|
||||
expect(notes).toEqual([note1, note2])
|
||||
})
|
||||
|
||||
it('should return a reference to the internal notes array', () => {
|
||||
region.addNote(note1)
|
||||
const notes1 = region.getNotes()
|
||||
const notes2 = region.getNotes()
|
||||
|
||||
expect(notes1).toBe(notes2) // Same reference
|
||||
})
|
||||
})
|
||||
|
||||
describe('setNotes', () => {
|
||||
it('should replace all notes with new array', () => {
|
||||
region.addNote(note1)
|
||||
region.addNote(note2)
|
||||
|
||||
expect(region.getNotes()).toHaveLength(2)
|
||||
|
||||
region.setNotes([note3])
|
||||
|
||||
const notes = region.getNotes()
|
||||
expect(notes).toHaveLength(1)
|
||||
expect(notes[0]).toBe(note3)
|
||||
})
|
||||
|
||||
it('should allow setting empty notes array', () => {
|
||||
region.addNote(note1)
|
||||
region.addNote(note2)
|
||||
|
||||
region.setNotes([])
|
||||
|
||||
expect(region.getNotes()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should accept notes array with multiple notes', () => {
|
||||
const newNotes = [note1, note2, note3]
|
||||
region.setNotes(newNotes)
|
||||
|
||||
const retrievedNotes = region.getNotes()
|
||||
expect(retrievedNotes).toHaveLength(3)
|
||||
expect(retrievedNotes).toEqual(newNotes)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('inheritance from KGRegion', () => {
|
||||
it('should inherit all base region properties', () => {
|
||||
expect(region.getId()).toBe('test-region-1')
|
||||
expect(region.getTrackId()).toBe('test-track-1')
|
||||
expect(region.getTrackIndex()).toBe(0)
|
||||
expect(region.getName()).toBe('Test Region')
|
||||
expect(region.getStartFromBeat()).toBe(0)
|
||||
expect(region.getLength()).toBe(4)
|
||||
})
|
||||
|
||||
it('should inherit selection functionality', () => {
|
||||
expect(region.isSelected()).toBe(false)
|
||||
|
||||
region.select()
|
||||
expect(region.isSelected()).toBe(true)
|
||||
|
||||
region.deselect()
|
||||
expect(region.isSelected()).toBe(false)
|
||||
})
|
||||
|
||||
it('should inherit setters from base class', () => {
|
||||
region.setName('Updated Region')
|
||||
expect(region.getName()).toBe('Updated Region')
|
||||
|
||||
region.setStartFromBeat(8)
|
||||
expect(region.getStartFromBeat()).toBe(8)
|
||||
|
||||
region.setLength(12)
|
||||
expect(region.getLength()).toBe(12)
|
||||
})
|
||||
})
|
||||
|
||||
describe('type identification', () => {
|
||||
it('should return correct current type', () => {
|
||||
expect(region.getCurrentType()).toBe('KGMidiRegion')
|
||||
})
|
||||
|
||||
it('should return correct root type', () => {
|
||||
expect(region.getRootType()).toBe('KGRegion')
|
||||
})
|
||||
|
||||
it('should be instanceof both KGMidiRegion and KGRegion', () => {
|
||||
expect(region).toBeInstanceOf(KGMidiRegion)
|
||||
expect(region).toBeInstanceOf(KGRegion)
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases and error handling', () => {
|
||||
it('should handle notes with overlapping time ranges', () => {
|
||||
const overlappingNote1 = createMockMidiNote({ id: 'overlap-1', pitch: 60, startBeat: 0, endBeat: 2 })
|
||||
const overlappingNote2 = createMockMidiNote({ id: 'overlap-2', pitch: 64, startBeat: 1, endBeat: 3 })
|
||||
|
||||
region.addNote(overlappingNote1)
|
||||
region.addNote(overlappingNote2)
|
||||
|
||||
const notes = region.getNotes()
|
||||
expect(notes).toHaveLength(2)
|
||||
expect(notes).toContain(overlappingNote1)
|
||||
expect(notes).toContain(overlappingNote2)
|
||||
})
|
||||
|
||||
it('should handle notes with same pitch but different timing', () => {
|
||||
const sameNote1 = createMockMidiNote({ id: 'same-1', pitch: 60, startBeat: 0, endBeat: 1 })
|
||||
const sameNote2 = createMockMidiNote({ id: 'same-2', pitch: 60, startBeat: 2, endBeat: 3 })
|
||||
|
||||
region.addNote(sameNote1)
|
||||
region.addNote(sameNote2)
|
||||
|
||||
expect(region.getNotes()).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should handle notes outside region boundaries', () => {
|
||||
// Region is from beat 0 to 4, but note extends beyond
|
||||
const outsideNote = createMockMidiNote({ id: 'outside', pitch: 60, startBeat: 3, endBeat: 6 })
|
||||
|
||||
region.addNote(outsideNote)
|
||||
|
||||
const notes = region.getNotes()
|
||||
expect(notes).toHaveLength(1)
|
||||
expect(notes[0]).toBe(outsideNote)
|
||||
// Note: The region doesn't enforce boundary constraints - that's application logic
|
||||
})
|
||||
|
||||
it('should handle zero-length region', () => {
|
||||
const zeroRegion = new KGMidiRegion('zero', 'track', 0, 'Zero Length', 0, 0)
|
||||
const note = createMockMidiNote({ id: 'note', pitch: 60, startBeat: 0, endBeat: 1 })
|
||||
|
||||
zeroRegion.addNote(note)
|
||||
|
||||
expect(zeroRegion.getNotes()).toHaveLength(1)
|
||||
expect(zeroRegion.getLength()).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('data consistency', () => {
|
||||
it('should maintain note references correctly', () => {
|
||||
const originalNote = createMockMidiNote({ id: 'ref-test', pitch: 60, startBeat: 0, endBeat: 1 })
|
||||
|
||||
region.addNote(originalNote)
|
||||
const retrievedNote = region.getNotes()[0]
|
||||
|
||||
expect(retrievedNote).toBe(originalNote) // Same reference
|
||||
|
||||
// Modify original note
|
||||
originalNote.setPitch(64)
|
||||
expect(retrievedNote.getPitch()).toBe(64) // Should reflect change
|
||||
})
|
||||
|
||||
it('should handle concurrent modifications correctly', () => {
|
||||
const notes = [
|
||||
createMockMidiNote({ id: 'concurrent-1', pitch: 60 }),
|
||||
createMockMidiNote({ id: 'concurrent-2', pitch: 64 }),
|
||||
createMockMidiNote({ id: 'concurrent-3', pitch: 67 })
|
||||
]
|
||||
|
||||
// Add notes
|
||||
notes.forEach(note => region.addNote(note))
|
||||
expect(region.getNotes()).toHaveLength(3)
|
||||
|
||||
// Remove middle note
|
||||
region.removeNote('concurrent-2')
|
||||
expect(region.getNotes()).toHaveLength(2)
|
||||
|
||||
// Add new note
|
||||
const newNote = createMockMidiNote({ id: 'concurrent-4', pitch: 70 })
|
||||
region.addNote(newNote)
|
||||
expect(region.getNotes()).toHaveLength(3)
|
||||
|
||||
// Verify final state
|
||||
const finalNotes = region.getNotes()
|
||||
expect(finalNotes).toContain(notes[0]) // concurrent-1
|
||||
expect(finalNotes).not.toContain(notes[1]) // concurrent-2 (removed)
|
||||
expect(finalNotes).toContain(notes[2]) // concurrent-3
|
||||
expect(finalNotes).toContain(newNote) // concurrent-4
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,398 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { KGMidiTrack, type InstrumentType } from './KGMidiTrack'
|
||||
import { KGTrack, TrackType } from './KGTrack'
|
||||
import { KGMidiRegion } from '../region/KGMidiRegion'
|
||||
import { createMockMidiRegion } from '../../test/utils/mock-data'
|
||||
|
||||
describe('KGMidiTrack', () => {
|
||||
let track: KGMidiTrack
|
||||
|
||||
beforeEach(() => {
|
||||
track = new KGMidiTrack()
|
||||
})
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create track with default values', () => {
|
||||
const defaultTrack = new KGMidiTrack()
|
||||
|
||||
expect(defaultTrack.getName()).toBe('Untitled MIDI Track')
|
||||
expect(defaultTrack.getId()).toBe(0)
|
||||
expect(defaultTrack.getType()).toBe(TrackType.MIDI)
|
||||
expect(defaultTrack.getInstrument()).toBe('acoustic_grand_piano')
|
||||
expect(defaultTrack.getVolume()).toBe(0.8) // DEFAULT_TRACK_VOLUME
|
||||
expect(defaultTrack.getRegions()).toEqual([])
|
||||
})
|
||||
|
||||
it('should create track with custom parameters', () => {
|
||||
const customTrack = new KGMidiTrack('My Piano Track', 5, 'electric_piano_1', 0.6)
|
||||
|
||||
expect(customTrack.getName()).toBe('My Piano Track')
|
||||
expect(customTrack.getId()).toBe(5)
|
||||
expect(customTrack.getType()).toBe(TrackType.MIDI)
|
||||
expect(customTrack.getInstrument()).toBe('electric_piano_1')
|
||||
expect(customTrack.getVolume()).toBe(0.6)
|
||||
})
|
||||
|
||||
it('should set correct type identifier', () => {
|
||||
expect(track.getCurrentType()).toBe('KGMidiTrack')
|
||||
})
|
||||
|
||||
it('should inherit from KGTrack', () => {
|
||||
expect(track).toBeInstanceOf(KGMidiTrack)
|
||||
expect(track).toBeInstanceOf(KGTrack)
|
||||
})
|
||||
})
|
||||
|
||||
describe('instrument management', () => {
|
||||
describe('getInstrument', () => {
|
||||
it('should return default instrument when not set', () => {
|
||||
expect(track.getInstrument()).toBe('acoustic_grand_piano')
|
||||
})
|
||||
|
||||
it('should return current instrument', () => {
|
||||
const customTrack = new KGMidiTrack('Test', 0, 'electric_guitar_clean')
|
||||
expect(customTrack.getInstrument()).toBe('electric_guitar_clean')
|
||||
})
|
||||
|
||||
it('should provide backward compatibility for undefined instrument', () => {
|
||||
// This tests the backward compatibility mentioned in the code
|
||||
expect(track.getInstrument()).toBe('acoustic_grand_piano')
|
||||
})
|
||||
})
|
||||
|
||||
describe('setInstrument', () => {
|
||||
it('should update instrument', () => {
|
||||
track.setInstrument('violin')
|
||||
expect(track.getInstrument()).toBe('violin')
|
||||
})
|
||||
|
||||
it('should handle different instrument types', () => {
|
||||
const instruments: InstrumentType[] = [
|
||||
'acoustic_grand_piano',
|
||||
'electric_piano_1',
|
||||
'electric_guitar_clean',
|
||||
'acoustic_bass',
|
||||
'violin',
|
||||
'trumpet',
|
||||
'flute'
|
||||
]
|
||||
|
||||
instruments.forEach(instrument => {
|
||||
track.setInstrument(instrument)
|
||||
expect(track.getInstrument()).toBe(instrument)
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle rapid instrument changes', () => {
|
||||
track.setInstrument('piano')
|
||||
track.setInstrument('guitar')
|
||||
track.setInstrument('violin')
|
||||
|
||||
expect(track.getInstrument()).toBe('violin')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('region management', () => {
|
||||
let region1: KGMidiRegion
|
||||
let region2: KGMidiRegion
|
||||
let region3: KGMidiRegion
|
||||
|
||||
beforeEach(() => {
|
||||
region1 = createMockMidiRegion({
|
||||
id: 'region-1',
|
||||
trackId: track.getId().toString(),
|
||||
name: 'Region 1',
|
||||
startFromBeat: 0,
|
||||
length: 4
|
||||
})
|
||||
region2 = createMockMidiRegion({
|
||||
id: 'region-2',
|
||||
trackId: track.getId().toString(),
|
||||
name: 'Region 2',
|
||||
startFromBeat: 4,
|
||||
length: 4
|
||||
})
|
||||
region3 = createMockMidiRegion({
|
||||
id: 'region-3',
|
||||
trackId: track.getId().toString(),
|
||||
name: 'Region 3',
|
||||
startFromBeat: 8,
|
||||
length: 4
|
||||
})
|
||||
})
|
||||
|
||||
describe('setRegions', () => {
|
||||
it('should set regions array', () => {
|
||||
const regions = [region1, region2]
|
||||
track.setRegions(regions)
|
||||
|
||||
expect(track.getRegions()).toHaveLength(2)
|
||||
expect(track.getRegions()).toEqual(regions)
|
||||
})
|
||||
|
||||
it('should replace existing regions', () => {
|
||||
track.setRegions([region1])
|
||||
expect(track.getRegions()).toHaveLength(1)
|
||||
|
||||
track.setRegions([region2, region3])
|
||||
expect(track.getRegions()).toHaveLength(2)
|
||||
expect(track.getRegions()).toContain(region2)
|
||||
expect(track.getRegions()).toContain(region3)
|
||||
expect(track.getRegions()).not.toContain(region1)
|
||||
})
|
||||
|
||||
it('should accept empty array', () => {
|
||||
track.setRegions([region1, region2])
|
||||
track.setRegions([])
|
||||
|
||||
expect(track.getRegions()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should enforce KGMidiRegion type', () => {
|
||||
const regions: KGMidiRegion[] = [region1, region2]
|
||||
track.setRegions(regions)
|
||||
|
||||
const retrievedRegions = track.getRegions()
|
||||
retrievedRegions.forEach(region => {
|
||||
expect(region).toBeInstanceOf(KGMidiRegion)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('inherited region methods', () => {
|
||||
beforeEach(() => {
|
||||
track.setRegions([region1, region2])
|
||||
})
|
||||
|
||||
it('should inherit addRegion method', () => {
|
||||
track.addRegion(region3)
|
||||
|
||||
const regions = track.getRegions()
|
||||
expect(regions).toHaveLength(3)
|
||||
expect(regions).toContain(region3)
|
||||
})
|
||||
|
||||
it('should inherit removeRegion method', () => {
|
||||
track.removeRegion('region-1')
|
||||
|
||||
const regions = track.getRegions()
|
||||
expect(regions).toHaveLength(1)
|
||||
expect(regions).not.toContain(region1)
|
||||
expect(regions).toContain(region2)
|
||||
})
|
||||
|
||||
it('should inherit getRegions method', () => {
|
||||
const regions = track.getRegions()
|
||||
expect(regions).toHaveLength(2)
|
||||
expect(regions).toContain(region1)
|
||||
expect(regions).toContain(region2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('inheritance from KGTrack', () => {
|
||||
it('should inherit all base track properties', () => {
|
||||
const customTrack = new KGMidiTrack('Test Track', 42, 'violin', 0.9)
|
||||
|
||||
expect(customTrack.getName()).toBe('Test Track')
|
||||
expect(customTrack.getId()).toBe(42)
|
||||
expect(customTrack.getType()).toBe(TrackType.MIDI)
|
||||
expect(customTrack.getVolume()).toBe(0.9)
|
||||
})
|
||||
|
||||
it('should inherit base track setters', () => {
|
||||
track.setName('Updated Track')
|
||||
expect(track.getName()).toBe('Updated Track')
|
||||
|
||||
track.setVolume(0.5)
|
||||
expect(track.getVolume()).toBe(0.5)
|
||||
|
||||
track.setTrackIndex(3)
|
||||
expect(track.getTrackIndex()).toBe(3)
|
||||
})
|
||||
|
||||
it('should inherit volume controls', () => {
|
||||
expect(track.getVolume()).toBe(0.8) // Default volume
|
||||
|
||||
track.setVolume(0.5)
|
||||
expect(track.getVolume()).toBe(0.5)
|
||||
|
||||
track.setVolume(1.0)
|
||||
expect(track.getVolume()).toBe(1.0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('type identification', () => {
|
||||
it('should return correct current type', () => {
|
||||
expect(track.getCurrentType()).toBe('KGMidiTrack')
|
||||
})
|
||||
|
||||
it('should return correct root type', () => {
|
||||
expect(track.getRootType()).toBe('KGTrack')
|
||||
})
|
||||
|
||||
it('should have MIDI track type', () => {
|
||||
expect(track.getType()).toBe(TrackType.MIDI)
|
||||
})
|
||||
})
|
||||
|
||||
describe('instrument type validation', () => {
|
||||
it('should handle all valid General MIDI instruments', () => {
|
||||
// Test a selection of valid GM instruments
|
||||
const validInstruments: InstrumentType[] = [
|
||||
'acoustic_grand_piano',
|
||||
'electric_piano_1',
|
||||
'electric_piano_2',
|
||||
'electric_guitar_clean',
|
||||
'electric_guitar_muted',
|
||||
'acoustic_bass',
|
||||
'electric_bass_finger',
|
||||
'violin',
|
||||
'viola',
|
||||
'cello',
|
||||
'contrabass',
|
||||
'trumpet',
|
||||
'trombone',
|
||||
'french_horn',
|
||||
'flute',
|
||||
'clarinet',
|
||||
'soprano_sax',
|
||||
'alto_sax'
|
||||
]
|
||||
|
||||
validInstruments.forEach(instrument => {
|
||||
expect(() => {
|
||||
track.setInstrument(instrument)
|
||||
expect(track.getInstrument()).toBe(instrument)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('track state consistency', () => {
|
||||
it('should maintain consistent state after multiple operations', () => {
|
||||
// Setup initial state
|
||||
track.setName('Piano Track')
|
||||
track.setInstrument('acoustic_grand_piano')
|
||||
track.setVolume(0.7)
|
||||
|
||||
const regions = [
|
||||
createMockMidiRegion({ id: 'r1', trackId: '0', name: 'Intro' }),
|
||||
createMockMidiRegion({ id: 'r2', trackId: '0', name: 'Verse' })
|
||||
]
|
||||
track.setRegions(regions)
|
||||
|
||||
// Verify initial state
|
||||
expect(track.getName()).toBe('Piano Track')
|
||||
expect(track.getInstrument()).toBe('acoustic_grand_piano')
|
||||
expect(track.getVolume()).toBe(0.7)
|
||||
expect(track.getRegions()).toHaveLength(2)
|
||||
|
||||
// Modify state
|
||||
track.setInstrument('electric_piano_1')
|
||||
track.addRegion(createMockMidiRegion({
|
||||
id: 'r3',
|
||||
trackId: '0',
|
||||
name: 'Chorus'
|
||||
}))
|
||||
|
||||
// Verify modified state
|
||||
expect(track.getName()).toBe('Piano Track')
|
||||
expect(track.getInstrument()).toBe('electric_piano_1')
|
||||
expect(track.getVolume()).toBe(0.7)
|
||||
expect(track.getRegions()).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('should handle region-track relationship correctly', () => {
|
||||
const region = createMockMidiRegion({
|
||||
id: 'test-region',
|
||||
trackId: track.getId().toString(),
|
||||
name: 'Test Region'
|
||||
})
|
||||
|
||||
track.addRegion(region)
|
||||
|
||||
// Verify region is in track
|
||||
expect(track.getRegions()).toContain(region)
|
||||
|
||||
// Find region manually since getRegionById doesn't exist
|
||||
const foundRegion = track.getRegions().find(r => r.getId() === 'test-region')
|
||||
expect(foundRegion).toBe(region)
|
||||
|
||||
// Verify region properties
|
||||
expect(region.getTrackId()).toBe(track.getId().toString())
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases and error handling', () => {
|
||||
it('should handle empty track name', () => {
|
||||
const emptyNameTrack = new KGMidiTrack('', 0, 'piano')
|
||||
expect(emptyNameTrack.getName()).toBe('')
|
||||
})
|
||||
|
||||
it('should handle negative track ID', () => {
|
||||
const negativeIdTrack = new KGMidiTrack('Test', -1, 'piano')
|
||||
expect(negativeIdTrack.getId()).toBe(-1)
|
||||
})
|
||||
|
||||
it('should handle volume boundaries', () => {
|
||||
track.setVolume(0.0)
|
||||
expect(track.getVolume()).toBe(0.0)
|
||||
|
||||
track.setVolume(1.0)
|
||||
expect(track.getVolume()).toBe(1.0)
|
||||
|
||||
// Volume outside normal range (should still work)
|
||||
track.setVolume(1.5)
|
||||
expect(track.getVolume()).toBe(1.5)
|
||||
})
|
||||
|
||||
it('should handle large number of regions', () => {
|
||||
const manyRegions = Array.from({ length: 100 }, (_, i) =>
|
||||
createMockMidiRegion({
|
||||
id: `region-${i}`,
|
||||
trackId: track.getId().toString(),
|
||||
name: `Region ${i}`
|
||||
})
|
||||
)
|
||||
|
||||
track.setRegions(manyRegions)
|
||||
expect(track.getRegions()).toHaveLength(100)
|
||||
|
||||
// Should be able to find any region
|
||||
const foundRegion50 = track.getRegions().find(r => r.getId() === 'region-50')
|
||||
const foundRegion99 = track.getRegions().find(r => r.getId() === 'region-99')
|
||||
|
||||
expect(foundRegion50).toBeDefined()
|
||||
expect(foundRegion99).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('class-transformer compatibility', () => {
|
||||
it('should have proper type annotations for serialization', () => {
|
||||
// Verify the class has the necessary decorators for serialization
|
||||
expect(track.getCurrentType()).toBe('KGMidiTrack')
|
||||
|
||||
// Test that default instrument fallback works
|
||||
const instrument = track.getInstrument()
|
||||
expect(instrument).toBe('acoustic_grand_piano')
|
||||
})
|
||||
|
||||
it('should maintain regions type after serialization simulation', () => {
|
||||
const regions = [
|
||||
createMockMidiRegion({ id: 'r1', trackId: '0' }),
|
||||
createMockMidiRegion({ id: 'r2', trackId: '0' })
|
||||
]
|
||||
|
||||
track.setRegions(regions)
|
||||
|
||||
// Simulate what happens during serialization/deserialization
|
||||
const retrievedRegions = track.getRegions()
|
||||
expect(retrievedRegions).toHaveLength(2)
|
||||
retrievedRegions.forEach(region => {
|
||||
expect(region).toBeInstanceOf(KGMidiRegion)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Vendored
+299
@@ -0,0 +1,299 @@
|
||||
{
|
||||
"name": "joy",
|
||||
"maxBars": 32,
|
||||
"currentBars": 0,
|
||||
"timeSignature": {
|
||||
"numerator": 4,
|
||||
"denominator": 4
|
||||
},
|
||||
"bpm": 125,
|
||||
"keySignature": "C major",
|
||||
"projectStructureVersion": 1,
|
||||
"tracks": [
|
||||
{
|
||||
"__type": "KGMidiTrack",
|
||||
"name": "Melody",
|
||||
"id": 1,
|
||||
"trackIndex": 0,
|
||||
"type": "MIDI",
|
||||
"volume": 0.8,
|
||||
"regions": [
|
||||
{
|
||||
"__type": "KGMidiRegion",
|
||||
"id": "KGMidiRegion_1755134493138_iufo370n9",
|
||||
"trackId": "1",
|
||||
"trackIndex": 0,
|
||||
"name": "Melody Region 1",
|
||||
"startFromBeat": 0,
|
||||
"length": 32,
|
||||
"selected": true,
|
||||
"notes": [
|
||||
{
|
||||
"id": "KGMidiNote_1755134524628_dfmfpzoa9",
|
||||
"startBeat": 0,
|
||||
"endBeat": 1,
|
||||
"pitch": 64,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134528480_2uml69rkg",
|
||||
"startBeat": 1,
|
||||
"endBeat": 2,
|
||||
"pitch": 64,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134530480_4r34d57ig",
|
||||
"startBeat": 2,
|
||||
"endBeat": 3,
|
||||
"pitch": 65,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134531066_zahyopqtg",
|
||||
"startBeat": 3,
|
||||
"endBeat": 4,
|
||||
"pitch": 67,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134531647_65jgmc4m2",
|
||||
"startBeat": 4,
|
||||
"endBeat": 5,
|
||||
"pitch": 67,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134532248_mnd24k18e",
|
||||
"startBeat": 5,
|
||||
"endBeat": 6,
|
||||
"pitch": 65,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134532815_506vblsde",
|
||||
"startBeat": 6,
|
||||
"endBeat": 7,
|
||||
"pitch": 64,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134533716_ah1hw0kfe",
|
||||
"startBeat": 7,
|
||||
"endBeat": 8,
|
||||
"pitch": 62,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134534566_m8bunoc6t",
|
||||
"startBeat": 8,
|
||||
"endBeat": 9,
|
||||
"pitch": 60,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134536265_7qeciq0m6",
|
||||
"startBeat": 9,
|
||||
"endBeat": 10,
|
||||
"pitch": 60,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134537068_o6bb5y7ha",
|
||||
"startBeat": 10,
|
||||
"endBeat": 11,
|
||||
"pitch": 62,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134537752_ya958b30e",
|
||||
"startBeat": 11,
|
||||
"endBeat": 12,
|
||||
"pitch": 64,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134538401_bzer7pq8h",
|
||||
"startBeat": 12,
|
||||
"endBeat": 13.5,
|
||||
"pitch": 64,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134541568_so9s6pa1u",
|
||||
"startBeat": 13.5,
|
||||
"endBeat": 14,
|
||||
"pitch": 62,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134545236_r9xnsoggt",
|
||||
"startBeat": 14,
|
||||
"endBeat": 16,
|
||||
"pitch": 62,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134561530_idvq2hr0t",
|
||||
"startBeat": 16,
|
||||
"endBeat": 17,
|
||||
"pitch": 64,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134561530_i6za508pa",
|
||||
"startBeat": 17,
|
||||
"endBeat": 18,
|
||||
"pitch": 64,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134561530_0kru65kt6",
|
||||
"startBeat": 18,
|
||||
"endBeat": 19,
|
||||
"pitch": 65,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134561530_ayj4954ko",
|
||||
"startBeat": 19,
|
||||
"endBeat": 20,
|
||||
"pitch": 67,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134561530_5toqdpq5n",
|
||||
"startBeat": 20,
|
||||
"endBeat": 21,
|
||||
"pitch": 67,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134561530_yilvvambd",
|
||||
"startBeat": 21,
|
||||
"endBeat": 22,
|
||||
"pitch": 65,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134561530_r8w8tr21a",
|
||||
"startBeat": 22,
|
||||
"endBeat": 23,
|
||||
"pitch": 64,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134561530_d8t91xm5l",
|
||||
"startBeat": 23,
|
||||
"endBeat": 24,
|
||||
"pitch": 62,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134561530_hk0q0ow9q",
|
||||
"startBeat": 24,
|
||||
"endBeat": 25,
|
||||
"pitch": 60,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134561530_gnl53ldaz",
|
||||
"startBeat": 25,
|
||||
"endBeat": 26,
|
||||
"pitch": 60,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134561531_53kdyl7u5",
|
||||
"startBeat": 26,
|
||||
"endBeat": 27,
|
||||
"pitch": 62,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134561531_les717v12",
|
||||
"startBeat": 27,
|
||||
"endBeat": 28,
|
||||
"pitch": 64,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134561531_owdko4big",
|
||||
"startBeat": 28,
|
||||
"endBeat": 29.5,
|
||||
"pitch": 62,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134561531_b04zbsgnf",
|
||||
"startBeat": 29.5,
|
||||
"endBeat": 30,
|
||||
"pitch": 60,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"id": "KGMidiNote_1755134561531_jgvrlacer",
|
||||
"startBeat": 30,
|
||||
"endBeat": 32,
|
||||
"pitch": 60,
|
||||
"velocity": 127,
|
||||
"selected": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"instrument": "acoustic_grand_piano"
|
||||
},
|
||||
{
|
||||
"__type": "KGMidiTrack",
|
||||
"name": "Pad Chord",
|
||||
"id": 2,
|
||||
"trackIndex": 1,
|
||||
"type": "MIDI",
|
||||
"volume": 1,
|
||||
"regions": [
|
||||
{
|
||||
"__type": "KGMidiRegion",
|
||||
"id": "KGMidiRegion_1755326973407_9hsnkcgwr",
|
||||
"trackId": "2",
|
||||
"trackIndex": 1,
|
||||
"name": "Pad Chord Region 1",
|
||||
"startFromBeat": 0,
|
||||
"length": 32,
|
||||
"selected": true,
|
||||
"notes": []
|
||||
}
|
||||
],
|
||||
"instrument": "pad_1_new_age"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { plainToInstance } from 'class-transformer'
|
||||
import { convertRegionToABCNotation } from './abcNotationUtil'
|
||||
import { KGMidiRegion } from '../core/region/KGMidiRegion'
|
||||
import { KGMidiTrack } from '../core/track/KGMidiTrack'
|
||||
import { KGCore } from '../core/KGCore'
|
||||
import { KGProject } from '../core/KGProject'
|
||||
|
||||
// Import the test fixture
|
||||
import joyProjectData from '../test/fixtures/joy-project.json'
|
||||
|
||||
// Helper function to load project using real class-transformer deserialization (same as UI)
|
||||
function loadProjectFromJSON(projectData: Record<string, unknown>): KGProject {
|
||||
// Use the exact same deserialization process as the UI (Toolbar.tsx handleKGStudioJSONImport)
|
||||
const deserializedResult = plainToInstance(KGProject, projectData)
|
||||
|
||||
// Handle case where plainToInstance might return an array (same as UI)
|
||||
const deserializedProject = Array.isArray(deserializedResult)
|
||||
? deserializedResult[0] || null
|
||||
: deserializedResult
|
||||
|
||||
if (!deserializedProject) {
|
||||
throw new Error("Failed to deserialize project data")
|
||||
}
|
||||
|
||||
return deserializedProject
|
||||
}
|
||||
|
||||
describe('abcNotationUtil - Integration Tests with Real Project Data', () => {
|
||||
let joyProject: KGProject
|
||||
|
||||
beforeEach(() => {
|
||||
// Simulate the exact same process as Toolbar.tsx handleKGStudioJSONImport
|
||||
// First parse as JSON (simulating file.text() -> JSON.parse())
|
||||
const fileContent = JSON.stringify(joyProjectData)
|
||||
const projectData = JSON.parse(fileContent)
|
||||
|
||||
// Then deserialize using class-transformer (same as UI)
|
||||
joyProject = loadProjectFromJSON(projectData)
|
||||
|
||||
// Mock KGCore to return the loaded project (same as production flow)
|
||||
vi.spyOn(KGCore, 'instance').mockReturnValue({
|
||||
getCurrentProject: () => joyProject
|
||||
} as unknown as KGCore)
|
||||
})
|
||||
|
||||
describe('convertRegionToABCNotation with real project data', () => {
|
||||
it('should convert joy project melody region to exact ABC notation', () => {
|
||||
// Get the melody track and region using real project structure
|
||||
const melodyTrack = joyProject.getTracks()[0] as KGMidiTrack
|
||||
expect(melodyTrack.getName()).toBe('Melody')
|
||||
expect(melodyTrack.getRegions()).toHaveLength(1)
|
||||
|
||||
const melodyRegion = melodyTrack.getRegions()[0] as KGMidiRegion
|
||||
expect(melodyRegion.getName()).toBe('Melody Region 1')
|
||||
expect(melodyRegion.getNotes()).toHaveLength(30) // Verify we have all 30 notes
|
||||
|
||||
// Convert to ABC notation using real region data
|
||||
const result = convertRegionToABCNotation(melodyRegion, 0, 32)
|
||||
|
||||
// Expected ABC notation output (exactly as provided)
|
||||
const expectedABCNotation = `X:1
|
||||
T:Melody Region 1
|
||||
M:4/4
|
||||
L:1/4
|
||||
Q:1/4=125
|
||||
K:C
|
||||
E E F G | G F E D | C C D E | E3/2 D1/2 D2 | E E F G | G F E D | C C D E | D3/2 C1/2 C2 |`
|
||||
|
||||
// Verify exact match
|
||||
expect(result).toBe(expectedABCNotation)
|
||||
})
|
||||
|
||||
it('should handle empty pad chord region from real project', () => {
|
||||
// Get the pad chord track and region
|
||||
const padTrack = joyProject.getTracks()[1] as KGMidiTrack
|
||||
expect(padTrack.getName()).toBe('Pad Chord')
|
||||
expect(padTrack.getInstrument()).toBe('pad_1_new_age')
|
||||
|
||||
const padRegion = padTrack.getRegions()[0] as KGMidiRegion
|
||||
expect(padRegion.getName()).toBe('Pad Chord Region 1')
|
||||
expect(padRegion.getNotes()).toHaveLength(0) // Empty region
|
||||
|
||||
// Convert empty region to ABC notation
|
||||
const result = convertRegionToABCNotation(padRegion, 0, 32)
|
||||
|
||||
// Verify it contains rest notation and proper headers
|
||||
expect(result).toBeDefined()
|
||||
expect(result).toContain('T:Pad Chord Region 1')
|
||||
expect(result).toContain('M:4/4')
|
||||
expect(result).toContain('Q:1/4=125')
|
||||
expect(result).toContain('K:C')
|
||||
expect(result).toContain('z') // Should contain rest notation
|
||||
})
|
||||
|
||||
it('should use correct project settings from real project data', () => {
|
||||
// Verify project settings are loaded correctly
|
||||
expect(joyProject.getName()).toBe('joy')
|
||||
expect(joyProject.getBpm()).toBe(125)
|
||||
expect(joyProject.getTimeSignature()).toEqual({ numerator: 4, denominator: 4 })
|
||||
expect(joyProject.getKeySignature()).toBe('C major')
|
||||
expect(joyProject.getMaxBars()).toBe(32)
|
||||
|
||||
// These settings should be reflected in ABC notation headers
|
||||
const melodyTrack = joyProject.getTracks()[0] as KGMidiTrack
|
||||
const melodyRegion = melodyTrack.getRegions()[0] as KGMidiRegion
|
||||
const result = convertRegionToABCNotation(melodyRegion, 0, 32)
|
||||
|
||||
expect(result).toContain('Q:1/4=125') // BPM
|
||||
expect(result).toContain('M:4/4') // Time signature
|
||||
expect(result).toContain('K:C') // Key signature
|
||||
})
|
||||
|
||||
it('should handle real note timing and pitches correctly', () => {
|
||||
const melodyTrack = joyProject.getTracks()[0] as KGMidiTrack
|
||||
const melodyRegion = melodyTrack.getRegions()[0] as KGMidiRegion
|
||||
const notes = melodyRegion.getNotes()
|
||||
|
||||
// Verify some key notes from the real data
|
||||
expect(notes[0].getPitch()).toBe(64) // First note is E (64)
|
||||
expect(notes[0].getStartBeat()).toBe(0)
|
||||
expect(notes[0].getEndBeat()).toBe(1)
|
||||
|
||||
expect(notes[3].getPitch()).toBe(67) // Fourth note is G (67)
|
||||
expect(notes[3].getStartBeat()).toBe(3)
|
||||
expect(notes[3].getEndBeat()).toBe(4)
|
||||
|
||||
// Verify fractional timing note (beat 12-13.5)
|
||||
const fractionalNote = notes.find(note => note.getEndBeat() === 13.5)
|
||||
expect(fractionalNote).toBeDefined()
|
||||
expect(fractionalNote!.getPitch()).toBe(64) // E
|
||||
expect(fractionalNote!.getStartBeat()).toBe(12)
|
||||
|
||||
// Convert and verify these real timings are reflected in ABC notation
|
||||
const result = convertRegionToABCNotation(melodyRegion, 0, 32)
|
||||
expect(result).toContain('E3/2 D1/2') // Fractional timing should appear in ABC
|
||||
})
|
||||
|
||||
it('should handle multiple tracks with different instruments', () => {
|
||||
const tracks = joyProject.getTracks()
|
||||
expect(tracks).toHaveLength(2)
|
||||
|
||||
// Verify track properties from real project
|
||||
const melodyTrack = tracks[0] as KGMidiTrack
|
||||
expect(melodyTrack.getName()).toBe('Melody')
|
||||
expect(melodyTrack.getInstrument()).toBe('acoustic_grand_piano')
|
||||
expect(melodyTrack.getVolume()).toBe(0.8)
|
||||
|
||||
const padTrack = tracks[1] as KGMidiTrack
|
||||
expect(padTrack.getName()).toBe('Pad Chord')
|
||||
expect(padTrack.getInstrument()).toBe('pad_1_new_age')
|
||||
expect(padTrack.getVolume()).toBe(1)
|
||||
|
||||
// Both tracks should be processable for ABC notation
|
||||
const melodyRegion = melodyTrack.getRegions()[0] as KGMidiRegion
|
||||
const padRegion = padTrack.getRegions()[0] as KGMidiRegion
|
||||
|
||||
const melodyABC = convertRegionToABCNotation(melodyRegion, 0, 32)
|
||||
const padABC = convertRegionToABCNotation(padRegion, 0, 32)
|
||||
|
||||
expect(melodyABC).toContain('T:Melody Region 1')
|
||||
expect(padABC).toContain('T:Pad Chord Region 1')
|
||||
})
|
||||
|
||||
it('should verify complete deserialization hierarchy', () => {
|
||||
// Test that class-transformer properly restored the entire object hierarchy
|
||||
expect(joyProject).toBeInstanceOf(KGProject)
|
||||
|
||||
const tracks = joyProject.getTracks()
|
||||
tracks.forEach(track => {
|
||||
expect(track).toBeInstanceOf(KGMidiTrack)
|
||||
|
||||
const regions = track.getRegions()
|
||||
regions.forEach(region => {
|
||||
expect(region).toBeInstanceOf(KGMidiRegion)
|
||||
|
||||
const notes = (region as KGMidiRegion).getNotes()
|
||||
notes.forEach(note => {
|
||||
// Notes should have proper methods and properties
|
||||
expect(typeof note.getId()).toBe('string')
|
||||
expect(typeof note.getPitch()).toBe('number')
|
||||
expect(typeof note.getStartBeat()).toBe('number')
|
||||
expect(typeof note.getEndBeat()).toBe('number')
|
||||
expect(typeof note.getVelocity()).toBe('number')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Verify type identifiers are preserved
|
||||
tracks.forEach(track => {
|
||||
expect((track as KGMidiTrack).getCurrentType()).toBe('KGMidiTrack')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases with real project data', () => {
|
||||
it('should handle partial region conversion', () => {
|
||||
const melodyTrack = joyProject.getTracks()[0] as KGMidiTrack
|
||||
const melodyRegion = melodyTrack.getRegions()[0] as KGMidiRegion
|
||||
|
||||
// Test converting only first 8 beats (2 bars)
|
||||
const result = convertRegionToABCNotation(melodyRegion, 0, 8)
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result).toContain('T:Melody Region 1')
|
||||
// Should only contain the first 2 bars of music
|
||||
expect(result).not.toContain('D3/2 C1/2 C2') // This appears later in the song
|
||||
})
|
||||
|
||||
it('should handle mid-region start point', () => {
|
||||
const melodyTrack = joyProject.getTracks()[0] as KGMidiTrack
|
||||
const melodyRegion = melodyTrack.getRegions()[0] as KGMidiRegion
|
||||
|
||||
// Test converting from beat 16 to 24 (second half of melody)
|
||||
const result = convertRegionToABCNotation(melodyRegion, 16, 24)
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result).toContain('T:Melody Region 1')
|
||||
// Should start from the second repetition
|
||||
const lines = result.split('\n')
|
||||
const musicLine = lines[lines.length - 1] // Last line contains the music
|
||||
expect(musicLine).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user