feat: added integration tests

This commit is contained in:
Xiaohan-Tian
2025-12-08 17:56:58 -08:00
parent c5ada0db63
commit 02a7a61b1d
8 changed files with 1036 additions and 10 deletions
@@ -0,0 +1,335 @@
/**
* Integration tests for command execution and undo/redo functionality
* Tests the complete flow: Command execution → Core model updates → UI state sync
*/
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { createRoot } from 'react-dom/client'
// Import core classes
import { KGCore } from '../../../core/KGCore'
import { KGProject } from '../../../core/KGProject'
import { KGMidiTrack } from '../../../core/track/KGMidiTrack'
import { KGMidiRegion } from '../../../core/region/KGMidiRegion'
import { KGMidiNote } from '../../../core/midi/KGMidiNote'
import { KGCommandHistory } from '../../../core/commands/KGCommandHistory'
// Import commands
import { CreateNoteCommand } from '../../../core/commands/note/CreateNoteCommand'
import { DeleteNotesCommand } from '../../../core/commands/note/DeleteNotesCommand'
import { AddTrackCommand } from '../../../core/commands/track/AddTrackCommand'
// Import store
import { useProjectStore } from '../../../stores/projectStore'
// Import test utilities
import '../../utils/setup-integration-tests'
describe('Command Execution Integration Tests', () => {
let testProject: KGProject
let testTrack: KGMidiTrack
let testRegion: KGMidiRegion
beforeEach(async () => {
// Create a real project with track and region for testing
testProject = new KGProject('Test Project')
testProject.setBpm(120)
testProject.setTimeSignature({ numerator: 4, denominator: 4 })
testProject.setKeySignature('C major')
testProject.setMaxBars(32)
// Create test track and region
testTrack = new KGMidiTrack('Test Track', 'acoustic_grand_piano')
testRegion = new KGMidiRegion('Test Region', 0, 16)
// Set up the project hierarchy
testTrack.addRegion(testRegion)
testProject.addTrack(testTrack)
// Initialize KGCore with test project
const core = KGCore.instance()
await core.initializeAsync()
core.setCurrentProject(testProject)
// Clear command history
KGCommandHistory.instance().clear()
// Initialize store with project
const { loadProject } = useProjectStore.getState()
await loadProject(testProject)
})
describe('Note Command Integration', () => {
it('should execute CreateNoteCommand and update both core model and store', async () => {
const regionId = testRegion.getId()
const initialNoteCount = testRegion.getNotes().length
// Create and execute command
const createCommand = new CreateNoteCommand(regionId, 0, 1, 60, 100)
const commandHistory = KGCommandHistory.instance()
// Execute command through command history (simulates real usage)
act(() => {
commandHistory.executeCommand(createCommand)
})
// Verify core model was updated
const updatedRegion = testProject.getTracks()[0].getRegions()[0] as KGMidiRegion
expect(updatedRegion.getNotes()).toHaveLength(initialNoteCount + 1)
const createdNote = updatedRegion.getNotes().find(note => note.getId() === createCommand.getNoteId())
expect(createdNote).toBeDefined()
expect(createdNote!.getPitch()).toBe(60)
expect(createdNote!.getStartBeat()).toBe(0)
expect(createdNote!.getEndBeat()).toBe(1)
// Verify command history state
expect(commandHistory.canUndo()).toBe(true)
expect(commandHistory.canRedo()).toBe(false)
expect(commandHistory.getUndoDescription()).toBe('Create note C4')
// Verify store undo/redo state is updated
const storeState = useProjectStore.getState()
expect(storeState.canUndo).toBe(true)
expect(storeState.canRedo).toBe(false)
})
it('should execute undo and restore previous state', async () => {
const regionId = testRegion.getId()
const initialNoteCount = testRegion.getNotes().length
// Create and execute command
const createCommand = new CreateNoteCommand(regionId, 2, 3, 64, 120) // E4
const commandHistory = KGCommandHistory.instance()
act(() => {
commandHistory.executeCommand(createCommand)
})
// Verify note was created
const regionAfterCreate = testProject.getTracks()[0].getRegions()[0] as KGMidiRegion
expect(regionAfterCreate.getNotes()).toHaveLength(initialNoteCount + 1)
// Execute undo
act(() => {
const undoSuccess = commandHistory.undo()
expect(undoSuccess).toBe(true)
})
// Verify core model was restored
const regionAfterUndo = testProject.getTracks()[0].getRegions()[0] as KGMidiRegion
expect(regionAfterUndo.getNotes()).toHaveLength(initialNoteCount)
// Verify the specific note was removed
const noteExists = regionAfterUndo.getNotes().some(note => note.getId() === createCommand.getNoteId())
expect(noteExists).toBe(false)
// Verify command history state
expect(commandHistory.canUndo()).toBe(false)
expect(commandHistory.canRedo()).toBe(true)
expect(commandHistory.getRedoDescription()).toBe('Create note E4')
})
it('should execute redo and restore forward state', async () => {
const regionId = testRegion.getId()
const initialNoteCount = testRegion.getNotes().length
// Create, execute, and undo a command
const createCommand = new CreateNoteCommand(regionId, 1, 2, 67, 110) // G4
const commandHistory = KGCommandHistory.instance()
act(() => {
commandHistory.executeCommand(createCommand)
commandHistory.undo()
})
// Verify we're back to initial state
expect(testRegion.getNotes()).toHaveLength(initialNoteCount)
// Execute redo
act(() => {
const redoSuccess = commandHistory.redo()
expect(redoSuccess).toBe(true)
})
// Verify core model was restored to post-create state
const regionAfterRedo = testProject.getTracks()[0].getRegions()[0] as KGMidiRegion
expect(regionAfterRedo.getNotes()).toHaveLength(initialNoteCount + 1)
// Verify the specific note was recreated
const recreatedNote = regionAfterRedo.getNotes().find(note => note.getId() === createCommand.getNoteId())
expect(recreatedNote).toBeDefined()
expect(recreatedNote!.getPitch()).toBe(67)
// Verify command history state
expect(commandHistory.canUndo()).toBe(true)
expect(commandHistory.canRedo()).toBe(false)
})
})
describe('Multiple Command Integration', () => {
it('should execute multiple commands and maintain history integrity', async () => {
const regionId = testRegion.getId()
const commandHistory = KGCommandHistory.instance()
// Execute multiple note creation commands
const command1 = new CreateNoteCommand(regionId, 0, 1, 60, 100) // C4
const command2 = new CreateNoteCommand(regionId, 1, 2, 64, 100) // E4
const command3 = new CreateNoteCommand(regionId, 2, 3, 67, 100) // G4
act(() => {
commandHistory.executeCommand(command1)
commandHistory.executeCommand(command2)
commandHistory.executeCommand(command3)
})
// Verify all notes were created
const region = testProject.getTracks()[0].getRegions()[0] as KGMidiRegion
expect(region.getNotes()).toHaveLength(3)
// Verify command history
expect(commandHistory.canUndo()).toBe(true)
expect(commandHistory.getUndoDescription()).toBe('Create note G4')
// Undo middle command by undoing twice
act(() => {
commandHistory.undo() // Remove G4
commandHistory.undo() // Remove E4
})
// Verify only first note remains
expect(region.getNotes()).toHaveLength(1)
expect(region.getNotes()[0].getPitch()).toBe(60) // C4
// Verify redo state
expect(commandHistory.canRedo()).toBe(true)
expect(commandHistory.getRedoDescription()).toBe('Create note E4')
})
it('should handle command execution with different command types', async () => {
const commandHistory = KGCommandHistory.instance()
const initialTrackCount = testProject.getTracks().length
// Execute track addition command
const addTrackCommand = new AddTrackCommand('Bass Track', 'acoustic_bass')
act(() => {
commandHistory.executeCommand(addTrackCommand)
})
// Verify track was added to core model
expect(testProject.getTracks()).toHaveLength(initialTrackCount + 1)
const newTrack = testProject.getTracks()[initialTrackCount] as KGMidiTrack
expect(newTrack.getName()).toBe('Bass Track')
expect(newTrack.getInstrument()).toBe('acoustic_bass')
// Add a note to the original region
const createNoteCommand = new CreateNoteCommand(testRegion.getId(), 0, 1, 48, 100) // C3
act(() => {
commandHistory.executeCommand(createNoteCommand)
})
// Verify both commands are in history
expect(commandHistory.canUndo()).toBe(true)
expect(commandHistory.getUndoDescription()).toBe('Create note C3')
// Undo both commands
act(() => {
commandHistory.undo() // Undo note creation
commandHistory.undo() // Undo track addition
})
// Verify both operations were undone
expect(testProject.getTracks()).toHaveLength(initialTrackCount)
const originalRegion = testProject.getTracks()[0].getRegions()[0] as KGMidiRegion
expect(originalRegion.getNotes()).toHaveLength(0)
})
})
describe('Error Handling Integration', () => {
it('should handle command execution errors gracefully', async () => {
const commandHistory = KGCommandHistory.instance()
// Try to create note in non-existent region
const invalidCommand = new CreateNoteCommand('invalid-region-id', 0, 1, 60, 100)
// Execute command - should not throw but should not add to history
act(() => {
commandHistory.executeCommand(invalidCommand)
})
// Verify command was not added to history due to execution failure
expect(commandHistory.canUndo()).toBe(false)
expect(commandHistory.getUndoDescription()).toBeNull()
// Verify project state unchanged
const region = testProject.getTracks()[0].getRegions()[0] as KGMidiRegion
expect(region.getNotes()).toHaveLength(0)
})
it('should handle undo failures gracefully', async () => {
const regionId = testRegion.getId()
const commandHistory = KGCommandHistory.instance()
// Create a command that will succeed initially
const createCommand = new CreateNoteCommand(regionId, 0, 1, 60, 100)
act(() => {
commandHistory.executeCommand(createCommand)
})
// Manually remove the region to cause undo to fail
testTrack.removeRegion(testRegion.getId())
// Try to undo - should fail gracefully
act(() => {
const undoSuccess = commandHistory.undo()
expect(undoSuccess).toBe(false)
})
// Verify command is still in undo stack after failed undo
expect(commandHistory.canUndo()).toBe(true)
})
})
describe('Store Integration', () => {
it('should keep store undo/redo state synchronized with command history', async () => {
const regionId = testRegion.getId()
const commandHistory = KGCommandHistory.instance()
const { refreshUndoRedoState } = useProjectStore.getState()
// Initial state
expect(useProjectStore.getState().canUndo).toBe(false)
expect(useProjectStore.getState().canRedo).toBe(false)
// Execute command
const createCommand = new CreateNoteCommand(regionId, 0, 1, 60, 100)
act(() => {
commandHistory.executeCommand(createCommand)
refreshUndoRedoState() // Simulate store sync
})
// Verify store state updated
let storeState = useProjectStore.getState()
expect(storeState.canUndo).toBe(true)
expect(storeState.canRedo).toBe(false)
expect(storeState.undoDescription).toBe('Create note C4')
expect(storeState.redoDescription).toBeNull()
// Execute undo
act(() => {
commandHistory.undo()
refreshUndoRedoState() // Simulate store sync
})
// Verify store state updated after undo
storeState = useProjectStore.getState()
expect(storeState.canUndo).toBe(false)
expect(storeState.canRedo).toBe(true)
expect(storeState.undoDescription).toBeNull()
expect(storeState.redoDescription).toBe('Create note C4')
})
})
})
@@ -0,0 +1,446 @@
/**
* Integration tests for project store synchronization with core models
* Tests the critical data flow: Store Actions → Core Models → UI State Updates
*/
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
import { act, renderHook } from '@testing-library/react'
// Import core classes
import { KGCore } from '../../../core/KGCore'
import { KGProject } from '../../../core/KGProject'
import { KGMidiTrack, type InstrumentType } from '../../../core/track/KGMidiTrack'
import { KGMidiRegion } from '../../../core/region/KGMidiRegion'
import { KGMidiNote } from '../../../core/midi/KGMidiNote'
// Import store
import { useProjectStore } from '../../../stores/projectStore'
// Import test utilities and mocks
import '../../utils/setup-integration-tests'
import { mockAudioInterface } from '../../mocks/audio-interface'
describe('Project Store Synchronization Integration Tests', () => {
let testProject: KGProject
beforeEach(async () => {
// Create a real project for testing
testProject = new KGProject('Sync Test Project')
testProject.setBpm(120)
testProject.setTimeSignature({ numerator: 4, denominator: 4 })
testProject.setKeySignature('C major')
testProject.setMaxBars(32)
// Initialize KGCore
const core = KGCore.instance()
await core.initializeAsync()
core.setCurrentProject(testProject)
// Reset store state
const store = useProjectStore.getState()
await store.loadProject(testProject)
})
afterEach(() => {
vi.clearAllMocks()
})
describe('Project Properties Synchronization', () => {
it('should sync BPM changes between store and core model', async () => {
const { setBpm } = useProjectStore.getState()
const newBpm = 140
// Execute store action
act(() => {
setBpm(newBpm)
})
// Verify core model was updated
expect(testProject.getBpm()).toBe(newBpm)
// Verify store state reflects change
const storeState = useProjectStore.getState()
expect(storeState.bpm).toBe(newBpm)
// Verify CSS custom property was updated
const cssValue = getComputedStyle(document.documentElement).getPropertyValue('--time-signature-numerator')
expect(cssValue).toBeTruthy() // CSS should be updated by store action
})
it('should sync time signature changes and update CSS properties', async () => {
const { setTimeSignature } = useProjectStore.getState()
const newTimeSignature = { numerator: 3, denominator: 4 }
act(() => {
setTimeSignature(newTimeSignature)
})
// Verify core model was updated
expect(testProject.getTimeSignature()).toEqual(newTimeSignature)
// Verify store state reflects change
const storeState = useProjectStore.getState()
expect(storeState.timeSignature).toEqual(newTimeSignature)
// Verify CSS custom property was updated for UI calculations
const cssValue = getComputedStyle(document.documentElement).getPropertyValue('--time-signature-numerator')
expect(cssValue.trim()).toBe('3')
})
it('should sync max bars changes and update layout CSS', async () => {
const { setMaxBars } = useProjectStore.getState()
const newMaxBars = 64
act(() => {
setMaxBars(newMaxBars)
})
// Verify core model was updated
expect(testProject.getMaxBars()).toBe(newMaxBars)
// Verify store state reflects change
const storeState = useProjectStore.getState()
expect(storeState.maxBars).toBe(newMaxBars)
// Verify CSS custom property was updated for layout
const cssValue = getComputedStyle(document.documentElement).getPropertyValue('--max-number-of-bars')
expect(cssValue.trim()).toBe('64')
})
it('should sync key signature changes', async () => {
const { setKeySignature } = useProjectStore.getState()
const newKeySignature = 'G major'
act(() => {
setKeySignature(newKeySignature)
})
// Verify core model was updated
expect(testProject.getKeySignature()).toBe(newKeySignature)
// Verify store state reflects change
const storeState = useProjectStore.getState()
expect(storeState.keySignature).toBe(newKeySignature)
})
})
describe('Track Management Synchronization', () => {
it('should sync track addition with core model and audio interface', async () => {
const { addTrack } = useProjectStore.getState()
const initialTrackCount = testProject.getTracks().length
// Execute store action
await act(async () => {
await addTrack()
})
// Verify core model was updated
expect(testProject.getTracks()).toHaveLength(initialTrackCount + 1)
const newTrack = testProject.getTracks()[initialTrackCount] as KGMidiTrack
expect(newTrack).toBeInstanceOf(KGMidiTrack)
expect(newTrack.getName()).toContain('Track')
// Verify store state reflects change
const storeState = useProjectStore.getState()
expect(storeState.tracks).toHaveLength(initialTrackCount + 1)
// Verify audio interface was notified
expect(mockAudioInterface.createTrackBus).toHaveBeenCalled()
})
it('should sync track removal with core model and audio interface', async () => {
// First add a track
const { addTrack, removeTrack } = useProjectStore.getState()
await act(async () => {
await addTrack()
})
const trackCountAfterAdd = testProject.getTracks().length
const trackToRemove = testProject.getTracks()[trackCountAfterAdd - 1]
// Remove the track
await act(async () => {
await removeTrack(trackCountAfterAdd - 1) // Remove last track
})
// Verify core model was updated
expect(testProject.getTracks()).toHaveLength(trackCountAfterAdd - 1)
// Verify store state reflects change
const storeState = useProjectStore.getState()
expect(storeState.tracks).toHaveLength(trackCountAfterAdd - 1)
// Verify audio interface was notified
expect(mockAudioInterface.removeTrackBus).toHaveBeenCalledWith(trackToRemove.getId())
})
it('should sync track instrument changes with audio interface', async () => {
// Add a track first
const { addTrack, setTrackInstrument } = useProjectStore.getState()
await act(async () => {
await addTrack()
})
const trackIndex = testProject.getTracks().length - 1
const track = testProject.getTracks()[trackIndex] as KGMidiTrack
const newInstrument: InstrumentType = 'electric_bass'
// Change track instrument
await act(async () => {
await setTrackInstrument(trackIndex, newInstrument)
})
// Verify core model was updated
expect(track.getInstrument()).toBe(newInstrument)
// Verify store state reflects change
const storeState = useProjectStore.getState()
const storeTrack = storeState.tracks[trackIndex] as KGMidiTrack
expect(storeTrack.getInstrument()).toBe(newInstrument)
// Verify audio interface was notified
expect(mockAudioInterface.setTrackInstrument).toHaveBeenCalledWith(track.getId(), newInstrument)
})
it('should sync track reordering with core model', async () => {
const { addTrack, reorderTracks } = useProjectStore.getState()
// Add two tracks
await act(async () => {
await addTrack() // Track at index 0
await addTrack() // Track at index 1
})
const track0Before = testProject.getTracks()[0]
const track1Before = testProject.getTracks()[1]
// Reorder tracks (move track 0 to position 1)
act(() => {
reorderTracks(0, 1)
})
// Verify core model track order changed
const track0After = testProject.getTracks()[0]
const track1After = testProject.getTracks()[1]
expect(track0After.getId()).toBe(track1Before.getId())
expect(track1After.getId()).toBe(track0Before.getId())
// Verify store state reflects change
const storeState = useProjectStore.getState()
expect(storeState.tracks[0].getId()).toBe(track1Before.getId())
expect(storeState.tracks[1].getId()).toBe(track0Before.getId())
})
})
describe('Playback State Synchronization', () => {
it('should sync playhead position with formatted time string', async () => {
const { setPlayheadPosition } = useProjectStore.getState()
const newPosition = 8.5 // beats
act(() => {
setPlayheadPosition(newPosition)
})
// Verify store state updated
const storeState = useProjectStore.getState()
expect(storeState.playheadPosition).toBe(newPosition)
// Verify formatted time string was updated
expect(storeState.currentTime).toBeDefined()
expect(storeState.currentTime).toContain('|') // Should contain BBB:B | mm:ss:mmm format
})
it('should sync playback state changes', async () => {
const { startPlaying, stopPlaying } = useProjectStore.getState()
// Start playing
await act(async () => {
await startPlaying()
})
// Verify store state updated
let storeState = useProjectStore.getState()
expect(storeState.isPlaying).toBe(true)
// Verify audio interface was called
expect(mockAudioInterface.startPlayback).toHaveBeenCalled()
// Stop playing
await act(async () => {
await stopPlaying()
})
// Verify store state updated
storeState = useProjectStore.getState()
expect(storeState.isPlaying).toBe(false)
// Verify audio interface was called
expect(mockAudioInterface.stopPlayback).toHaveBeenCalled()
})
})
describe('Selection State Synchronization', () => {
it('should sync selection state with core piano roll state', async () => {
const { setActiveRegionId, syncSelectionFromCore } = useProjectStore.getState()
// Add a track and region for testing
const testTrack = new KGMidiTrack('Test Track', 'acoustic_grand_piano')
const testRegion = new KGMidiRegion('Test Region', 0, 16)
testTrack.addRegion(testRegion)
testProject.addTrack(testTrack)
// Set active region
act(() => {
setActiveRegionId(testRegion.getId())
})
// Verify store state updated
let storeState = useProjectStore.getState()
expect(storeState.activeRegionId).toBe(testRegion.getId())
// Simulate core selection changes and sync
act(() => {
syncSelectionFromCore()
})
// Verify store selection state is synchronized
storeState = useProjectStore.getState()
expect(storeState.selectedNoteIds).toBeDefined()
expect(storeState.selectedRegionIds).toBeDefined()
})
it('should clear all selections and sync state', async () => {
const { clearAllSelections, setSelectedTrack } = useProjectStore.getState()
// Set some initial selection state
act(() => {
setSelectedTrack('test-track-id')
})
// Verify selection was set
let storeState = useProjectStore.getState()
expect(storeState.selectedTrackId).toBe('test-track-id')
// Clear all selections
act(() => {
clearAllSelections()
})
// Verify all selections were cleared
storeState = useProjectStore.getState()
expect(storeState.selectedTrackId).toBeNull()
expect(storeState.selectedNoteIds).toEqual([])
expect(storeState.selectedRegionIds).toEqual([])
})
})
describe('Piano Roll State Integration', () => {
it('should sync piano roll visibility and active region', async () => {
const { setShowPianoRoll, setActiveRegionId } = useProjectStore.getState()
// Add test region
const testTrack = new KGMidiTrack('Test Track', 'acoustic_grand_piano')
const testRegion = new KGMidiRegion('Test Region', 0, 16)
testTrack.addRegion(testRegion)
testProject.addTrack(testTrack)
// Show piano roll with active region
act(() => {
setActiveRegionId(testRegion.getId())
setShowPianoRoll(true)
})
// Verify store state updated
const storeState = useProjectStore.getState()
expect(storeState.showPianoRoll).toBe(true)
expect(storeState.activeRegionId).toBe(testRegion.getId())
})
})
describe('Error Handling and Edge Cases', () => {
it('should handle invalid track operations gracefully', async () => {
const { removeTrack } = useProjectStore.getState()
const initialTrackCount = testProject.getTracks().length
// Try to remove non-existent track
await act(async () => {
await removeTrack(999) // Invalid index
})
// Verify project state unchanged
expect(testProject.getTracks()).toHaveLength(initialTrackCount)
// Verify store state unchanged
const storeState = useProjectStore.getState()
expect(storeState.tracks).toHaveLength(initialTrackCount)
})
it('should handle concurrent state updates correctly', async () => {
const { setBpm, setMaxBars } = useProjectStore.getState()
// Execute multiple state updates concurrently
await act(async () => {
setBpm(140)
setMaxBars(64)
})
// Verify both updates were applied to core model
expect(testProject.getBpm()).toBe(140)
expect(testProject.getMaxBars()).toBe(64)
// Verify store state is consistent
const storeState = useProjectStore.getState()
expect(storeState.bpm).toBe(140)
expect(storeState.maxBars).toBe(64)
})
})
describe('Project Loading Integration', () => {
it('should completely sync store state when loading new project', async () => {
const { loadProject } = useProjectStore.getState()
// Create a new project with specific properties
const newProject = new KGProject('New Loaded Project')
newProject.setBpm(160)
newProject.setTimeSignature({ numerator: 6, denominator: 8 })
newProject.setKeySignature('D major')
newProject.setMaxBars(48)
// Add a track with region and notes
const track = new KGMidiTrack('Loaded Track', 'violin')
const region = new KGMidiRegion('Loaded Region', 0, 8)
const note = new KGMidiNote('test-note', 0, 1, 64, 100)
region.addNote(note)
track.addRegion(region)
newProject.addTrack(track)
// Load the new project
await act(async () => {
await loadProject(newProject)
})
// Verify store state completely matches new project
const storeState = useProjectStore.getState()
expect(storeState.projectName).toBe('New Loaded Project')
expect(storeState.bpm).toBe(160)
expect(storeState.timeSignature).toEqual({ numerator: 6, denominator: 8 })
expect(storeState.keySignature).toBe('D major')
expect(storeState.maxBars).toBe(48)
expect(storeState.tracks).toHaveLength(1)
// Verify core model is updated
const core = KGCore.instance()
expect(core.getCurrentProject()?.getName()).toBe('New Loaded Project')
// Verify CSS properties were updated
const timeSignatureCSS = getComputedStyle(document.documentElement).getPropertyValue('--time-signature-numerator')
expect(timeSignatureCSS.trim()).toBe('6')
const maxBarsCSS = getComputedStyle(document.documentElement).getPropertyValue('--max-number-of-bars')
expect(maxBarsCSS.trim()).toBe('48')
})
})
})
@@ -1,13 +1,13 @@
import { describe, it, expect, beforeEach, vi } from 'vitest' import { describe, it, expect, beforeEach, vi } from 'vitest'
import { plainToInstance } from 'class-transformer' import { plainToInstance } from 'class-transformer'
import { convertRegionToABCNotation } from './abcNotationUtil' import { convertRegionToABCNotation } from '../../../util/abcNotationUtil'
import { KGMidiRegion } from '../core/region/KGMidiRegion' import { KGMidiRegion } from '../../../core/region/KGMidiRegion'
import { KGMidiTrack } from '../core/track/KGMidiTrack' import { KGMidiTrack } from '../../../core/track/KGMidiTrack'
import { KGCore } from '../core/KGCore' import { KGCore } from '../../../core/KGCore'
import { KGProject } from '../core/KGProject' import { KGProject } from '../../../core/KGProject'
// Import the test fixture // Import the test fixture
import joyProjectData from '../test/fixtures/joy-project.json' import joyProjectData from '../../fixtures/joy-project.json'
// Helper function to load project using real class-transformer deserialization (same as UI) // Helper function to load project using real class-transformer deserialization (same as UI)
function loadProjectFromJSON(projectData: Record<string, unknown>): KGProject { function loadProjectFromJSON(projectData: Record<string, unknown>): KGProject {
+40
View File
@@ -0,0 +1,40 @@
/**
* Mock implementation of KGAudioInterface for integration tests
* Provides interface compatibility while avoiding actual audio operations
*/
import { vi } from 'vitest'
export const mockAudioInterface = {
// Audio context management
startAudioContext: vi.fn().mockResolvedValue(true),
isAudioContextStarted: vi.fn().mockReturnValue(true),
// Track management
createTrackBus: vi.fn().mockResolvedValue(undefined),
setTrackInstrument: vi.fn().mockResolvedValue(undefined),
setTrackVolume: vi.fn().mockReturnValue(undefined),
setTrackMuted: vi.fn().mockReturnValue(undefined),
setTrackSoloed: vi.fn().mockReturnValue(undefined),
removeTrackBus: vi.fn().mockReturnValue(undefined),
// Playback control
startPlayback: vi.fn().mockReturnValue(undefined),
stopPlayback: vi.fn().mockReturnValue(undefined),
pausePlayback: vi.fn().mockReturnValue(undefined),
setPlaybackPosition: vi.fn().mockReturnValue(undefined),
// Note scheduling
scheduleNote: vi.fn().mockReturnValue(undefined),
scheduleNotes: vi.fn().mockReturnValue(undefined),
clearScheduledNotes: vi.fn().mockReturnValue(undefined),
// Transport
getCurrentBeat: vi.fn().mockReturnValue(0),
setBpm: vi.fn().mockReturnValue(undefined),
// Singleton pattern
getInstance: vi.fn().mockReturnThis(),
}
// Mock the class constructor
export const mockKGAudioInterfaceClass = vi.fn(() => mockAudioInterface)
+55
View File
@@ -0,0 +1,55 @@
/**
* Mock implementation of IndexedDB for integration tests
* Provides in-memory storage that mimics IndexedDB interface
*/
import { vi } from 'vitest'
// In-memory storage for tests
const mockStorage = new Map<string, any>()
export const mockIndexedDB = {
openDB: vi.fn().mockImplementation(() => {
return Promise.resolve({
put: vi.fn().mockImplementation((storeName: string, data: any, key?: string) => {
const actualKey = key || data.id || 'default'
mockStorage.set(`${storeName}:${actualKey}`, data)
return Promise.resolve(actualKey)
}),
get: vi.fn().mockImplementation((storeName: string, key: string) => {
return Promise.resolve(mockStorage.get(`${storeName}:${key}`))
}),
getAll: vi.fn().mockImplementation((storeName: string) => {
const results: any[] = []
for (const [key, value] of mockStorage.entries()) {
if (key.startsWith(`${storeName}:`)) {
results.push(value)
}
}
return Promise.resolve(results)
}),
delete: vi.fn().mockImplementation((storeName: string, key: string) => {
mockStorage.delete(`${storeName}:${key}`)
return Promise.resolve()
}),
clear: vi.fn().mockImplementation((storeName: string) => {
for (const key of mockStorage.keys()) {
if (key.startsWith(`${storeName}:`)) {
mockStorage.delete(key)
}
}
return Promise.resolve()
}),
close: vi.fn().mockResolvedValue(undefined),
})
}),
}
// Helper function to clear mock storage between tests
export const clearMockStorage = () => {
mockStorage.clear()
}
+58
View File
@@ -0,0 +1,58 @@
/**
* Mock implementation of Tone.js for integration tests
* Provides interface compatibility while avoiding actual audio operations
*/
import { vi } from 'vitest'
// Mock Sampler class
export const mockSampler = {
triggerAttackRelease: vi.fn(),
triggerAttack: vi.fn(),
triggerRelease: vi.fn(),
dispose: vi.fn(),
loaded: true,
toDestination: vi.fn().mockReturnThis(),
connect: vi.fn().mockReturnThis(),
disconnect: vi.fn().mockReturnThis(),
set: vi.fn().mockReturnThis(),
get: vi.fn().mockReturnValue({}),
}
// Mock Transport
export const mockTransport = {
start: vi.fn(),
stop: vi.fn(),
pause: vi.fn(),
position: '0:0:0',
bpm: { value: 120 },
timeSignature: [4, 4],
state: 'stopped',
schedule: vi.fn(),
clear: vi.fn(),
cancel: vi.fn(),
}
// Mock Tone namespace
export const mockTone = {
Sampler: vi.fn().mockImplementation(() => mockSampler),
Transport: mockTransport,
Buffer: vi.fn().mockImplementation(() => ({
loaded: true,
duration: 1,
get: vi.fn(),
set: vi.fn(),
})),
ToneAudioBuffer: vi.fn().mockImplementation(() => ({
loaded: true,
duration: 1,
})),
start: vi.fn(),
getContext: vi.fn().mockReturnValue({
state: 'running',
resume: vi.fn().mockResolvedValue(undefined),
}),
context: {
state: 'running',
resume: vi.fn().mockResolvedValue(undefined),
},
}
+92
View File
@@ -0,0 +1,92 @@
/**
* Integration test setup utilities
* Common setup and teardown for integration tests
*/
import { beforeEach, afterEach, vi } from 'vitest'
import { mockAudioInterface } from '../mocks/audio-interface'
import { mockIndexedDB, clearMockStorage } from '../mocks/indexed-db'
import { mockTone } from '../mocks/tone-js'
// Global setup for integration tests
beforeEach(() => {
// Clear all mocks
vi.clearAllMocks()
// Clear mock storage
clearMockStorage()
// Mock external dependencies while keeping internal components real
vi.doMock('../../audio/KGAudioInterface', () => ({
KGAudioInterface: mockAudioInterface,
default: mockAudioInterface,
}))
vi.doMock('idb', () => mockIndexedDB)
vi.doMock('tone', () => mockTone)
// Mock browser APIs that might be used
Object.defineProperty(window, 'AudioContext', {
writable: true,
value: vi.fn().mockImplementation(() => ({
state: 'running',
resume: vi.fn().mockResolvedValue(undefined),
})),
})
Object.defineProperty(window, 'webkitAudioContext', {
writable: true,
value: window.AudioContext,
})
})
afterEach(() => {
// Clean up after each test
vi.restoreAllMocks()
clearMockStorage()
})
// Helper functions for integration tests
export const createMockProject = () => {
// Helper to create a basic project for testing
// This can be expanded as needed
return {
id: 'test-project',
name: 'Test Project',
bpm: 120,
timeSignature: { numerator: 4, denominator: 4 },
keySignature: 'C major',
maxBars: 32,
tracks: [],
}
}
export const createMockTrack = (name = 'Test Track') => {
return {
id: `track-${Date.now()}`,
name,
instrument: 'acoustic_grand_piano',
volume: 0.8,
regions: [],
}
}
export const createMockRegion = (name = 'Test Region') => {
return {
id: `region-${Date.now()}`,
name,
startBeat: 0,
endBeat: 4,
notes: [],
}
}
export const createMockNote = (pitch = 60, startBeat = 0, endBeat = 1) => {
return {
id: `note-${Date.now()}`,
pitch,
startBeat,
endBeat,
velocity: 100,
}
}
+4 -4
View File
@@ -11,9 +11,10 @@ export default defineConfig({
// Global test setup // Global test setup
setupFiles: ['./src/test/setup.ts'], setupFiles: ['./src/test/setup.ts'],
// Include unit test files co-located with source // Include unit test files co-located with source AND integration tests
include: [ include: [
'src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}' 'src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
'src/test/integration/**/*.integration.test.ts'
], ],
// Exclude directories // Exclude directories
@@ -21,8 +22,7 @@ export default defineConfig({
'node_modules', 'node_modules',
'dist', 'dist',
'.git', '.git',
'.cache', '.cache'
'tests/' // Exclude integration/e2e folder for now
], ],
// Enable global test functions (describe, it, expect) // Enable global test functions (describe, it, expect)