import { describe, it, expect, beforeEach, vi } from 'vitest'; import { CreateNoteCommand } from './CreateNoteCommand'; import { KGCore } from '../../KGCore'; import { KGMidiNote } from '../../midi/KGMidiNote'; import { createMockProject, createMockMidiTrack, createMockMidiRegion } from '../../../test/utils/mock-data'; // Mock the KGCore singleton vi.mock('../../KGCore', () => ({ KGCore: { instance: vi.fn() } })); // Mock generateUniqueId utility vi.mock('../../../util/miscUtil', () => ({ generateUniqueId: vi.fn().mockReturnValue('mock-note-id') })); // Import the mocked function properly const { generateUniqueId } = await import('../../../util/miscUtil'); interface MockCore { getCurrentProject: ReturnType } describe('CreateNoteCommand', () => { let mockCore: MockCore; let mockProject: ReturnType; let mockTrack: ReturnType; let mockRegion: ReturnType; let command: CreateNoteCommand; beforeEach(() => { // Create test data mockRegion = createMockMidiRegion({ id: 'test-region', trackId: 'test-track', name: 'Test Region' }); mockTrack = createMockMidiTrack({ id: 1, name: 'Test Track', regions: [mockRegion] }); mockProject = createMockProject({ name: 'Test Project', tracks: [mockTrack] }); // Mock KGCore methods mockCore = { getCurrentProject: vi.fn().mockReturnValue(mockProject) }; vi.mocked(KGCore.instance).mockReturnValue(mockCore as unknown as KGCore); // Create command command = new CreateNoteCommand( 'test-region', // regionId 0, // startBeat 1, // endBeat 60, // pitch (middle C) 80 // velocity ); }); describe('constructor', () => { it('should create command with correct parameters', () => { const cmd = new CreateNoteCommand('region-1', 2, 4, 72, 100, 'custom-id'); expect(cmd).toBeInstanceOf(CreateNoteCommand); // We can't directly test private properties, but we can test execution }); it('should generate unique ID when not provided', () => { new CreateNoteCommand('region-1', 0, 1, 60, 80); // The generateUniqueId mock should have been called expect(generateUniqueId).toHaveBeenCalledWith('KGMidiNote'); }); it('should use provided ID when given', () => { // Clear previous calls vi.clearAllMocks(); new CreateNoteCommand('region-1', 0, 1, 60, 80, 'my-custom-id'); // Should not call generateUniqueId when ID is provided expect(generateUniqueId).not.toHaveBeenCalled(); }); }); describe('execute', () => { it('should create a note in the target region', () => { // Mock the addNote method const addNoteSpy = vi.spyOn(mockRegion, 'addNote'); // Execute the command command.execute(); // Verify note was added expect(addNoteSpy).toHaveBeenCalledTimes(1); // Verify the note has correct properties const addedNote = addNoteSpy.mock.calls[0][0] as KGMidiNote; expect(addedNote).toBeInstanceOf(KGMidiNote); expect(addedNote.getStartBeat()).toBe(0); expect(addedNote.getEndBeat()).toBe(1); expect(addedNote.getPitch()).toBe(60); expect(addedNote.getVelocity()).toBe(80); }); it('should throw error for non-existent region', () => { // Create command for non-existent region const badCommand = new CreateNoteCommand('non-existent-region', 0, 1, 60, 80); // Should throw error for non-existent region expect(() => badCommand.execute()).toThrow('MIDI region with ID non-existent-region not found'); }); it('should store created note for undo operation', () => { const addNoteSpy = vi.spyOn(mockRegion, 'addNote'); command.execute(); // Note should be created and stored internally for undo expect(addNoteSpy).toHaveBeenCalledTimes(1); }); }); describe('undo', () => { it('should remove the created note', () => { // Execute first to create the note command.execute(); // Mock removeNote method const removeNoteSpy = vi.spyOn(mockRegion, 'removeNote'); // Undo the command command.undo(); // Verify note was removed expect(removeNoteSpy).toHaveBeenCalledTimes(1); }); it('should throw error when undoing without execute', () => { // Try to undo without executing first expect(() => command.undo()).toThrow('Cannot undo: no note was created'); }); }); describe('re-execute (redo pattern)', () => { it('should re-add the note after undo using execute', () => { // Execute, undo, then execute again (redo pattern) command.execute(); command.undo(); const addNoteSpy = vi.spyOn(mockRegion, 'addNote'); command.execute(); // Commands are re-executed for redo // Note should be added again expect(addNoteSpy).toHaveBeenCalledTimes(1); }); }); describe('getDescription', () => { it('should return descriptive text', () => { const description = command.getDescription(); expect(description).toBeDefined(); expect(typeof description).toBe('string'); expect(description.length).toBeGreaterThan(0); }); }); describe('command lifecycle', () => { it('should support multiple execute/undo cycles', () => { const addNoteSpy = vi.spyOn(mockRegion, 'addNote'); const removeNoteSpy = vi.spyOn(mockRegion, 'removeNote'); // Execute -> Undo -> Execute -> Undo command.execute(); expect(addNoteSpy).toHaveBeenCalledTimes(1); command.undo(); expect(removeNoteSpy).toHaveBeenCalledTimes(1); command.execute(); // Re-execute for redo expect(addNoteSpy).toHaveBeenCalledTimes(2); command.undo(); expect(removeNoteSpy).toHaveBeenCalledTimes(2); }); }); });