feat: add R as shortcut of recording button; fixed linter errors.
This commit is contained in:
+87
-87
@@ -673,212 +673,212 @@ export class KGDebugger {
|
||||
* await KGDebugger.opfs('cat project.json')
|
||||
*/
|
||||
public async opfs(command: string): Promise<void> {
|
||||
const parts = command.trim().match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? []
|
||||
const cmd = parts[0]
|
||||
const parts = command.trim().match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
|
||||
const cmd = parts[0];
|
||||
// Strip surrounding quotes from arguments
|
||||
const arg = parts.slice(1).map(p => p.replace(/^["']|["']$/g, '')).join(' ')
|
||||
const arg = parts.slice(1).map(p => p.replace(/^["']|["']$/g, '')).join(' ');
|
||||
|
||||
try {
|
||||
switch (cmd) {
|
||||
case 'pwd':
|
||||
console.log('/' + this.opfsCwd.join('/'))
|
||||
break
|
||||
console.log('/' + this.opfsCwd.join('/'));
|
||||
break;
|
||||
|
||||
case 'ls':
|
||||
await this.opfsLs()
|
||||
break
|
||||
await this.opfsLs();
|
||||
break;
|
||||
|
||||
case 'cd':
|
||||
await this.opfsCd(arg)
|
||||
break
|
||||
await this.opfsCd(arg);
|
||||
break;
|
||||
|
||||
case 'cat':
|
||||
await this.opfsCat(arg)
|
||||
break
|
||||
await this.opfsCat(arg);
|
||||
break;
|
||||
|
||||
case 'dl':
|
||||
await this.opfsDl(arg)
|
||||
break
|
||||
await this.opfsDl(arg);
|
||||
break;
|
||||
|
||||
case 'rm':
|
||||
await this.opfsRm(arg)
|
||||
break
|
||||
await this.opfsRm(arg);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(`opfs: command not found: ${cmd}`)
|
||||
console.log('Available commands: pwd, ls, cd <path>, cat <file>, dl <file>, rm <name>')
|
||||
console.log(`opfs: command not found: ${cmd}`);
|
||||
console.log('Available commands: pwd, ls, cd <path>, cat <file>, dl <file>, rm <name>');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`opfs: ${error}`)
|
||||
console.error(`opfs: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async opfsResolveCwd(): Promise<FileSystemDirectoryHandle> {
|
||||
let dir = await navigator.storage.getDirectory()
|
||||
let dir = await navigator.storage.getDirectory();
|
||||
for (const segment of this.opfsCwd) {
|
||||
dir = await dir.getDirectoryHandle(segment)
|
||||
dir = await dir.getDirectoryHandle(segment);
|
||||
}
|
||||
return dir
|
||||
return dir;
|
||||
}
|
||||
|
||||
private async opfsLs(): Promise<void> {
|
||||
const dir = await this.opfsResolveCwd()
|
||||
const entries: Array<{ kind: string; name: string; size: number; modified: string }> = []
|
||||
const dir = await this.opfsResolveCwd();
|
||||
const entries: Array<{ kind: string; name: string; size: number; modified: string }> = [];
|
||||
|
||||
for await (const entry of dir.values()) {
|
||||
if (entry.kind === 'file') {
|
||||
const fileHandle = entry as FileSystemFileHandle
|
||||
const file = await fileHandle.getFile()
|
||||
const fileHandle = entry as FileSystemFileHandle;
|
||||
const file = await fileHandle.getFile();
|
||||
entries.push({
|
||||
kind: 'file',
|
||||
name: entry.name,
|
||||
size: file.size,
|
||||
modified: new Date(file.lastModified).toISOString().replace('T', ' ').slice(0, 19),
|
||||
})
|
||||
});
|
||||
} else {
|
||||
entries.push({
|
||||
kind: 'dir',
|
||||
name: entry.name + '/',
|
||||
size: 0,
|
||||
modified: '-',
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort: directories first, then files, alphabetically within each group
|
||||
entries.sort((a, b) => {
|
||||
if (a.kind !== b.kind) return a.kind === 'dir' ? -1 : 1
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
if (a.kind !== b.kind) return a.kind === 'dir' ? -1 : 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
if (entries.length === 0) {
|
||||
console.log('(empty directory)')
|
||||
return
|
||||
console.log('(empty directory)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Print header
|
||||
const cwdPath = '/' + this.opfsCwd.join('/')
|
||||
console.log(`total ${entries.length} (${cwdPath})`)
|
||||
const cwdPath = '/' + this.opfsCwd.join('/');
|
||||
console.log(`total ${entries.length} (${cwdPath})`);
|
||||
|
||||
// Format like ls -lla
|
||||
const maxSizeLen = Math.max(...entries.map(e => String(e.size).length), 4)
|
||||
const maxSizeLen = Math.max(...entries.map(e => String(e.size).length), 4);
|
||||
for (const e of entries) {
|
||||
const typeChar = e.kind === 'dir' ? 'd' : '-'
|
||||
const perms = e.kind === 'dir' ? 'rwxr-xr-x' : 'rw-r--r--'
|
||||
const sizeStr = e.kind === 'dir' ? '-'.padStart(maxSizeLen) : String(e.size).padStart(maxSizeLen)
|
||||
console.log(`${typeChar}${perms} ${sizeStr} ${e.modified} ${e.name}`)
|
||||
const typeChar = e.kind === 'dir' ? 'd' : '-';
|
||||
const perms = e.kind === 'dir' ? 'rwxr-xr-x' : 'rw-r--r--';
|
||||
const sizeStr = e.kind === 'dir' ? '-'.padStart(maxSizeLen) : String(e.size).padStart(maxSizeLen);
|
||||
console.log(`${typeChar}${perms} ${sizeStr} ${e.modified} ${e.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async opfsCd(path: string): Promise<void> {
|
||||
if (!path || path === '') {
|
||||
// cd with no args goes to root
|
||||
this.opfsCwd = []
|
||||
return
|
||||
this.opfsCwd = [];
|
||||
return;
|
||||
}
|
||||
|
||||
let segments: string[]
|
||||
let segments: string[];
|
||||
|
||||
if (path === '/') {
|
||||
this.opfsCwd = []
|
||||
return
|
||||
this.opfsCwd = [];
|
||||
return;
|
||||
} else if (path.startsWith('/')) {
|
||||
// Absolute path
|
||||
segments = path.split('/').filter(Boolean)
|
||||
segments = path.split('/').filter(Boolean);
|
||||
} else {
|
||||
// Relative path
|
||||
segments = [...this.opfsCwd, ...path.split('/').filter(Boolean)]
|
||||
segments = [...this.opfsCwd, ...path.split('/').filter(Boolean)];
|
||||
}
|
||||
|
||||
// Resolve . and ..
|
||||
const resolved: string[] = []
|
||||
const resolved: string[] = [];
|
||||
for (const seg of segments) {
|
||||
if (seg === '.') continue
|
||||
if (seg === '.') continue;
|
||||
if (seg === '..') {
|
||||
resolved.pop()
|
||||
resolved.pop();
|
||||
} else {
|
||||
resolved.push(seg)
|
||||
resolved.push(seg);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the path exists
|
||||
let dir = await navigator.storage.getDirectory()
|
||||
let dir = await navigator.storage.getDirectory();
|
||||
for (const seg of resolved) {
|
||||
try {
|
||||
dir = await dir.getDirectoryHandle(seg)
|
||||
dir = await dir.getDirectoryHandle(seg);
|
||||
} catch {
|
||||
console.error(`opfs: cd: no such directory: ${path}`)
|
||||
return
|
||||
console.error(`opfs: cd: no such directory: ${path}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.opfsCwd = resolved
|
||||
this.opfsCwd = resolved;
|
||||
}
|
||||
|
||||
private async opfsCat(fileName: string): Promise<void> {
|
||||
if (!fileName) {
|
||||
console.error('opfs: cat: missing file name')
|
||||
return
|
||||
console.error('opfs: cat: missing file name');
|
||||
return;
|
||||
}
|
||||
|
||||
const dir = await this.opfsResolveCwd()
|
||||
const dir = await this.opfsResolveCwd();
|
||||
try {
|
||||
const fileHandle = await dir.getFileHandle(fileName)
|
||||
const file = await fileHandle.getFile()
|
||||
const text = await file.text()
|
||||
const fileHandle = await dir.getFileHandle(fileName);
|
||||
const file = await fileHandle.getFile();
|
||||
const text = await file.text();
|
||||
|
||||
// Pretty-print JSON files
|
||||
if (fileName.endsWith('.json')) {
|
||||
try {
|
||||
const parsed = JSON.parse(text)
|
||||
console.log(JSON.stringify(parsed, null, 2))
|
||||
const parsed = JSON.parse(text);
|
||||
console.log(JSON.stringify(parsed, null, 2));
|
||||
} catch {
|
||||
console.log(text)
|
||||
console.log(text);
|
||||
}
|
||||
} else {
|
||||
console.log(text)
|
||||
console.log(text);
|
||||
}
|
||||
} catch {
|
||||
console.error(`opfs: cat: ${fileName}: No such file`)
|
||||
console.error(`opfs: cat: ${fileName}: No such file`);
|
||||
}
|
||||
}
|
||||
|
||||
private async opfsDl(fileName: string): Promise<void> {
|
||||
if (!fileName) {
|
||||
console.error('opfs: dl: missing file name')
|
||||
return
|
||||
console.error('opfs: dl: missing file name');
|
||||
return;
|
||||
}
|
||||
|
||||
const dir = await this.opfsResolveCwd()
|
||||
const dir = await this.opfsResolveCwd();
|
||||
try {
|
||||
const fileHandle = await dir.getFileHandle(fileName)
|
||||
const file = await fileHandle.getFile()
|
||||
const url = URL.createObjectURL(file)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = fileName
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
console.log(`downloaded: ${fileName} (${file.size} bytes)`)
|
||||
const fileHandle = await dir.getFileHandle(fileName);
|
||||
const file = await fileHandle.getFile();
|
||||
const url = URL.createObjectURL(file);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
console.log(`downloaded: ${fileName} (${file.size} bytes)`);
|
||||
} catch {
|
||||
console.error(`opfs: dl: ${fileName}: No such file`)
|
||||
console.error(`opfs: dl: ${fileName}: No such file`);
|
||||
}
|
||||
}
|
||||
|
||||
private async opfsRm(name: string): Promise<void> {
|
||||
if (!name) {
|
||||
console.error('opfs: rm: missing file or directory name')
|
||||
return
|
||||
console.error('opfs: rm: missing file or directory name');
|
||||
return;
|
||||
}
|
||||
|
||||
const dir = await this.opfsResolveCwd()
|
||||
const dir = await this.opfsResolveCwd();
|
||||
try {
|
||||
await dir.removeEntry(name, { recursive: true })
|
||||
console.log(`removed: ${name}`)
|
||||
await dir.removeEntry(name, { recursive: true });
|
||||
console.log(`removed: ${name}`);
|
||||
} catch {
|
||||
console.error(`opfs: rm: ${name}: No such file or directory`)
|
||||
console.error(`opfs: rm: ${name}: No such file or directory`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,115 +1,115 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createMockProject } from '../../test/utils/mock-data'
|
||||
import { MockTransport } from '../../test/mocks/tone'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createMockProject } from '../../test/utils/mock-data';
|
||||
import { MockTransport } from '../../test/mocks/tone';
|
||||
|
||||
vi.mock('tone', async () => {
|
||||
const { ToneMock } = await import('../../test/mocks/tone')
|
||||
return ToneMock
|
||||
})
|
||||
const { ToneMock } = await import('../../test/mocks/tone');
|
||||
return ToneMock;
|
||||
});
|
||||
|
||||
vi.mock('../KGCore', () => ({
|
||||
KGCore: {
|
||||
instance: vi.fn()
|
||||
}
|
||||
}))
|
||||
}));
|
||||
|
||||
vi.mock('../config/ConfigManager', () => ({
|
||||
ConfigManager: {
|
||||
instance: vi.fn()
|
||||
}
|
||||
}))
|
||||
}));
|
||||
|
||||
import { KGCore } from '../KGCore'
|
||||
import { ConfigManager } from '../config/ConfigManager'
|
||||
import { KGAudioInterface } from './KGAudioInterface'
|
||||
import { KGCore } from '../KGCore';
|
||||
import { ConfigManager } from '../config/ConfigManager';
|
||||
import { KGAudioInterface } from './KGAudioInterface';
|
||||
|
||||
describe('KGAudioInterface preroll playback', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(0)
|
||||
vi.clearAllMocks()
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(0);
|
||||
vi.clearAllMocks();
|
||||
|
||||
MockTransport.position = 0
|
||||
MockTransport.start.mockClear()
|
||||
MockTransport.stop.mockClear()
|
||||
MockTransport.clear.mockClear()
|
||||
MockTransport.schedule.mockClear()
|
||||
MockTransport.bpm.value = 120
|
||||
MockTransport.position = 0;
|
||||
MockTransport.start.mockClear();
|
||||
MockTransport.stop.mockClear();
|
||||
MockTransport.clear.mockClear();
|
||||
MockTransport.schedule.mockClear();
|
||||
MockTransport.bpm.value = 120;
|
||||
|
||||
const project = createMockProject({
|
||||
bpm: 120,
|
||||
timeSignature: { numerator: 4, denominator: 4 },
|
||||
tracks: [],
|
||||
})
|
||||
});
|
||||
|
||||
vi.mocked(KGCore.instance).mockReturnValue({
|
||||
getCurrentProject: () => project,
|
||||
} as unknown as KGCore)
|
||||
} as unknown as KGCore);
|
||||
|
||||
vi.mocked(ConfigManager.instance).mockReturnValue({
|
||||
get: (key: string) => {
|
||||
if (key === 'audio.playback_delay') return 0.2
|
||||
if (key === 'audio.lookahead_time') return 0.05
|
||||
return null
|
||||
if (key === 'audio.playback_delay') return 0.2;
|
||||
if (key === 'audio.lookahead_time') return 0.05;
|
||||
return null;
|
||||
},
|
||||
} as unknown as ConfigManager)
|
||||
|
||||
;(KGAudioInterface as unknown as { _instance: KGAudioInterface | null })._instance = null
|
||||
})
|
||||
;(KGAudioInterface as unknown as { _instance: KGAudioInterface | null })._instance = null;
|
||||
});
|
||||
|
||||
it('uses virtual negative beats until the delayed transport start reaches beat 0', () => {
|
||||
const project = KGCore.instance().getCurrentProject()
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
const audio = KGAudioInterface.instance()
|
||||
;(audio as unknown as { isInitialized: boolean }).isInitialized = true
|
||||
;(audio as unknown as { isAudioContextStarted: boolean }).isAudioContextStarted = true
|
||||
;(audio as unknown as { isAudioContextStarted: boolean }).isAudioContextStarted = true;
|
||||
|
||||
const metronomeStart = vi.spyOn((audio as unknown as { metronome: { start: (...args: unknown[]) => void } }).metronome, 'start')
|
||||
audio.setMetronomeEnabled(true)
|
||||
const metronomeStart = vi.spyOn((audio as unknown as { metronome: { start: (...args: unknown[]) => void } }).metronome, 'start');
|
||||
audio.setMetronomeEnabled(true);
|
||||
|
||||
audio.preparePlayback(project, -2)
|
||||
audio.startPlayback()
|
||||
audio.preparePlayback(project, -2);
|
||||
audio.startPlayback();
|
||||
|
||||
expect(MockTransport.position).toBe(0)
|
||||
expect(MockTransport.start).not.toHaveBeenCalled()
|
||||
expect(metronomeStart).toHaveBeenCalledWith(-2, 4, 0.2)
|
||||
expect(audio.getTransportPosition()).toBeCloseTo(-2, 2)
|
||||
expect(MockTransport.position).toBe(0);
|
||||
expect(MockTransport.start).not.toHaveBeenCalled();
|
||||
expect(metronomeStart).toHaveBeenCalledWith(-2, 4, 0.2);
|
||||
expect(audio.getTransportPosition()).toBeCloseTo(-2, 2);
|
||||
|
||||
vi.advanceTimersByTime(500)
|
||||
expect(audio.getTransportPosition()).toBeCloseTo(-1, 1)
|
||||
vi.advanceTimersByTime(500);
|
||||
expect(audio.getTransportPosition()).toBeCloseTo(-1, 1);
|
||||
|
||||
vi.advanceTimersByTime(500)
|
||||
expect(MockTransport.start).toHaveBeenCalledTimes(1)
|
||||
expect(audio.getTransportPosition()).toBe(0)
|
||||
})
|
||||
vi.advanceTimersByTime(500);
|
||||
expect(MockTransport.start).toHaveBeenCalledTimes(1);
|
||||
expect(audio.getTransportPosition()).toBe(0);
|
||||
});
|
||||
|
||||
it('cancels the delayed transport start when playback stops during preroll', () => {
|
||||
const project = KGCore.instance().getCurrentProject()
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
const audio = KGAudioInterface.instance()
|
||||
;(audio as unknown as { isInitialized: boolean }).isInitialized = true
|
||||
;(audio as unknown as { isAudioContextStarted: boolean }).isAudioContextStarted = true
|
||||
;(audio as unknown as { isAudioContextStarted: boolean }).isAudioContextStarted = true;
|
||||
|
||||
audio.preparePlayback(project, -2)
|
||||
audio.startPlayback()
|
||||
audio.stopPlayback()
|
||||
vi.runAllTimers()
|
||||
audio.preparePlayback(project, -2);
|
||||
audio.startPlayback();
|
||||
audio.stopPlayback();
|
||||
vi.runAllTimers();
|
||||
|
||||
expect(MockTransport.start).not.toHaveBeenCalled()
|
||||
expect(MockTransport.stop).toHaveBeenCalledTimes(1)
|
||||
expect(audio.getTransportPosition()).toBe(0)
|
||||
})
|
||||
expect(MockTransport.start).not.toHaveBeenCalled();
|
||||
expect(MockTransport.stop).toHaveBeenCalledTimes(1);
|
||||
expect(audio.getTransportPosition()).toBe(0);
|
||||
});
|
||||
|
||||
it('allows a first-pass start before the loop start when explicitly requested', () => {
|
||||
const project = createMockProject({
|
||||
bpm: 120,
|
||||
timeSignature: { numerator: 4, denominator: 4 },
|
||||
tracks: [],
|
||||
})
|
||||
project.setIsLooping(true)
|
||||
project.setLoopingRange([4, 7])
|
||||
});
|
||||
project.setIsLooping(true);
|
||||
project.setLoopingRange([4, 7]);
|
||||
|
||||
const audio = KGAudioInterface.instance()
|
||||
audio.preparePlayback(project, 12, { allowStartBeforeLoopStart: true })
|
||||
const audio = KGAudioInterface.instance();
|
||||
audio.preparePlayback(project, 12, { allowStartBeforeLoopStart: true });
|
||||
|
||||
expect(MockTransport.position).toBe(6)
|
||||
})
|
||||
})
|
||||
expect(MockTransport.position).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,61 +1,61 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { MockLoop, MockTransport } from '../../test/mocks/tone'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { MockLoop, MockTransport } from '../../test/mocks/tone';
|
||||
|
||||
vi.mock('tone', async () => {
|
||||
const { ToneMock: toneMock } = await import('../../test/mocks/tone')
|
||||
return toneMock
|
||||
})
|
||||
const { ToneMock: toneMock } = await import('../../test/mocks/tone');
|
||||
return toneMock;
|
||||
});
|
||||
|
||||
import { KGMetronome } from './KGMetronome'
|
||||
import { KGMetronome } from './KGMetronome';
|
||||
|
||||
describe('KGMetronome', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(0)
|
||||
vi.clearAllMocks()
|
||||
MockTransport.bpm.value = 120
|
||||
MockTransport.getTicksAtTime.mockImplementation((time: number) => time * MockTransport.PPQ)
|
||||
})
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(0);
|
||||
vi.clearAllMocks();
|
||||
MockTransport.bpm.value = 120;
|
||||
MockTransport.getTicksAtTime.mockImplementation((time: number) => time * MockTransport.PPQ);
|
||||
});
|
||||
|
||||
it('schedules audible preroll clicks before beat 0 and keeps the beat 0 accent', () => {
|
||||
const metronome = new KGMetronome()
|
||||
const metronome = new KGMetronome();
|
||||
const triggerAttackRelease = vi.fn()
|
||||
;(metronome as unknown as { sampler: unknown }).sampler = {
|
||||
loaded: true,
|
||||
triggerAttackRelease,
|
||||
}
|
||||
};
|
||||
|
||||
metronome.start(-4, 4, 0.2)
|
||||
metronome.start(-4, 4, 0.2);
|
||||
|
||||
vi.advanceTimersByTime(200)
|
||||
expect(triggerAttackRelease.mock.calls[0]?.[0]).toBe('C5')
|
||||
expect(triggerAttackRelease.mock.calls[0]?.[1]).toBe('16n')
|
||||
expect(triggerAttackRelease.mock.calls[0]?.[2]).toBeCloseTo(0.2, 5)
|
||||
vi.advanceTimersByTime(200);
|
||||
expect(triggerAttackRelease.mock.calls[0]?.[0]).toBe('C5');
|
||||
expect(triggerAttackRelease.mock.calls[0]?.[1]).toBe('16n');
|
||||
expect(triggerAttackRelease.mock.calls[0]?.[2]).toBeCloseTo(0.2, 5);
|
||||
|
||||
vi.advanceTimersByTime(1000)
|
||||
expect(triggerAttackRelease.mock.calls[1]?.[0]).toBe('C4')
|
||||
expect(triggerAttackRelease.mock.calls[1]?.[1]).toBe('16n')
|
||||
expect(triggerAttackRelease.mock.calls[1]?.[2]).toBeCloseTo(0.7, 5)
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(triggerAttackRelease.mock.calls[1]?.[0]).toBe('C4');
|
||||
expect(triggerAttackRelease.mock.calls[1]?.[1]).toBe('16n');
|
||||
expect(triggerAttackRelease.mock.calls[1]?.[2]).toBeCloseTo(0.7, 5);
|
||||
|
||||
const transportLoop = MockLoop.mock.results[0]?.value
|
||||
expect(transportLoop).toBeDefined()
|
||||
const transportLoop = MockLoop.mock.results[0]?.value;
|
||||
expect(transportLoop).toBeDefined();
|
||||
|
||||
transportLoop.callback(0)
|
||||
expect(triggerAttackRelease).toHaveBeenLastCalledWith('C5', '16n', 0.2)
|
||||
})
|
||||
transportLoop.callback(0);
|
||||
expect(triggerAttackRelease).toHaveBeenLastCalledWith('C5', '16n', 0.2);
|
||||
});
|
||||
|
||||
it('cancels pending preroll clicks when stopped', () => {
|
||||
const metronome = new KGMetronome()
|
||||
const metronome = new KGMetronome();
|
||||
const triggerAttackRelease = vi.fn()
|
||||
;(metronome as unknown as { sampler: unknown }).sampler = {
|
||||
loaded: true,
|
||||
triggerAttackRelease,
|
||||
}
|
||||
};
|
||||
|
||||
metronome.start(-2, 4, 0.2)
|
||||
metronome.stop()
|
||||
vi.runAllTimers()
|
||||
metronome.start(-2, 4, 0.2);
|
||||
metronome.stop();
|
||||
vi.runAllTimers();
|
||||
|
||||
expect(triggerAttackRelease).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
expect(triggerAttackRelease).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
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'
|
||||
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')
|
||||
const { generateUniqueId } = await import('../../../util/miscUtil');
|
||||
|
||||
interface MockCore {
|
||||
getCurrentProject: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
describe('CreateNoteCommand', () => {
|
||||
let mockCore: MockCore
|
||||
let mockProject: ReturnType<typeof createMockProject>
|
||||
let mockTrack: ReturnType<typeof createMockMidiTrack>
|
||||
let mockRegion: ReturnType<typeof createMockMidiRegion>
|
||||
let command: CreateNoteCommand
|
||||
let mockCore: MockCore;
|
||||
let mockProject: ReturnType<typeof createMockProject>;
|
||||
let mockTrack: ReturnType<typeof createMockMidiTrack>;
|
||||
let mockRegion: ReturnType<typeof createMockMidiRegion>;
|
||||
let command: CreateNoteCommand;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create test data
|
||||
@@ -36,25 +36,25 @@ describe('CreateNoteCommand', () => {
|
||||
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)
|
||||
vi.mocked(KGCore.instance).mockReturnValue(mockCore as unknown as KGCore);
|
||||
|
||||
// Create command
|
||||
command = new CreateNoteCommand(
|
||||
@@ -63,135 +63,135 @@ describe('CreateNoteCommand', () => {
|
||||
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')
|
||||
const cmd = new CreateNoteCommand('region-1', 2, 4, 72, 100, 'custom-id');
|
||||
|
||||
expect(cmd).toBeInstanceOf(CreateNoteCommand)
|
||||
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)
|
||||
new CreateNoteCommand('region-1', 0, 1, 60, 80);
|
||||
|
||||
// The generateUniqueId mock should have been called
|
||||
expect(generateUniqueId).toHaveBeenCalledWith('KGMidiNote')
|
||||
})
|
||||
expect(generateUniqueId).toHaveBeenCalledWith('KGMidiNote');
|
||||
});
|
||||
|
||||
it('should use provided ID when given', () => {
|
||||
// Clear previous calls
|
||||
vi.clearAllMocks()
|
||||
vi.clearAllMocks();
|
||||
|
||||
new CreateNoteCommand('region-1', 0, 1, 60, 80, 'my-custom-id')
|
||||
new CreateNoteCommand('region-1', 0, 1, 60, 80, 'my-custom-id');
|
||||
|
||||
// Should not call generateUniqueId when ID is provided
|
||||
expect(generateUniqueId).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
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')
|
||||
const addNoteSpy = vi.spyOn(mockRegion, 'addNote');
|
||||
|
||||
// Execute the command
|
||||
command.execute()
|
||||
command.execute();
|
||||
|
||||
// Verify note was added
|
||||
expect(addNoteSpy).toHaveBeenCalledTimes(1)
|
||||
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)
|
||||
})
|
||||
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)
|
||||
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')
|
||||
})
|
||||
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')
|
||||
const addNoteSpy = vi.spyOn(mockRegion, 'addNote');
|
||||
|
||||
command.execute()
|
||||
command.execute();
|
||||
|
||||
// Note should be created and stored internally for undo
|
||||
expect(addNoteSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
expect(addNoteSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('undo', () => {
|
||||
it('should remove the created note', () => {
|
||||
// Execute first to create the note
|
||||
command.execute()
|
||||
command.execute();
|
||||
|
||||
// Mock removeNote method
|
||||
const removeNoteSpy = vi.spyOn(mockRegion, 'removeNote')
|
||||
const removeNoteSpy = vi.spyOn(mockRegion, 'removeNote');
|
||||
|
||||
// Undo the command
|
||||
command.undo()
|
||||
command.undo();
|
||||
|
||||
// Verify note was removed
|
||||
expect(removeNoteSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
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')
|
||||
})
|
||||
})
|
||||
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()
|
||||
command.execute();
|
||||
command.undo();
|
||||
|
||||
const addNoteSpy = vi.spyOn(mockRegion, 'addNote')
|
||||
command.execute() // Commands are re-executed for redo
|
||||
const addNoteSpy = vi.spyOn(mockRegion, 'addNote');
|
||||
command.execute(); // Commands are re-executed for redo
|
||||
|
||||
// Note should be added again
|
||||
expect(addNoteSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
expect(addNoteSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDescription', () => {
|
||||
it('should return descriptive text', () => {
|
||||
const description = command.getDescription()
|
||||
const description = command.getDescription();
|
||||
|
||||
expect(description).toBeDefined()
|
||||
expect(typeof description).toBe('string')
|
||||
expect(description.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
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')
|
||||
const addNoteSpy = vi.spyOn(mockRegion, 'addNote');
|
||||
const removeNoteSpy = vi.spyOn(mockRegion, 'removeNote');
|
||||
|
||||
// Execute -> Undo -> Execute -> Undo
|
||||
command.execute()
|
||||
expect(addNoteSpy).toHaveBeenCalledTimes(1)
|
||||
command.execute();
|
||||
expect(addNoteSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
command.undo()
|
||||
expect(removeNoteSpy).toHaveBeenCalledTimes(1)
|
||||
command.undo();
|
||||
expect(removeNoteSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
command.execute() // Re-execute for redo
|
||||
expect(addNoteSpy).toHaveBeenCalledTimes(2)
|
||||
command.execute(); // Re-execute for redo
|
||||
expect(addNoteSpy).toHaveBeenCalledTimes(2);
|
||||
|
||||
command.undo()
|
||||
expect(removeNoteSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
command.undo();
|
||||
expect(removeNoteSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,151 +1,151 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { KGMidiNote } from './KGMidiNote'
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { KGMidiNote } from './KGMidiNote';
|
||||
|
||||
describe('KGMidiNote', () => {
|
||||
let note: KGMidiNote
|
||||
let note: KGMidiNote;
|
||||
|
||||
beforeEach(() => {
|
||||
note = new KGMidiNote('test-note', 0, 1, 60, 80)
|
||||
})
|
||||
note = new KGMidiNote('test-note', 0, 1, 60, 80);
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create a note with correct properties', () => {
|
||||
const testNote = new KGMidiNote('note-1', 2, 4, 72, 100)
|
||||
const testNote = new KGMidiNote('note-1', 2, 4, 72, 100);
|
||||
|
||||
expect(testNote.getId()).toBe('note-1')
|
||||
expect(testNote.getStartBeat()).toBe(2)
|
||||
expect(testNote.getEndBeat()).toBe(4)
|
||||
expect(testNote.getPitch()).toBe(72)
|
||||
expect(testNote.getVelocity()).toBe(100)
|
||||
})
|
||||
expect(testNote.getId()).toBe('note-1');
|
||||
expect(testNote.getStartBeat()).toBe(2);
|
||||
expect(testNote.getEndBeat()).toBe(4);
|
||||
expect(testNote.getPitch()).toBe(72);
|
||||
expect(testNote.getVelocity()).toBe(100);
|
||||
});
|
||||
|
||||
it('should use default values when not provided', () => {
|
||||
const defaultNote = new KGMidiNote('default-note')
|
||||
const defaultNote = new KGMidiNote('default-note');
|
||||
|
||||
expect(defaultNote.getId()).toBe('default-note')
|
||||
expect(defaultNote.getStartBeat()).toBe(0)
|
||||
expect(defaultNote.getEndBeat()).toBe(0)
|
||||
expect(defaultNote.getPitch()).toBe(0)
|
||||
expect(defaultNote.getVelocity()).toBe(127)
|
||||
})
|
||||
})
|
||||
expect(defaultNote.getId()).toBe('default-note');
|
||||
expect(defaultNote.getStartBeat()).toBe(0);
|
||||
expect(defaultNote.getEndBeat()).toBe(0);
|
||||
expect(defaultNote.getPitch()).toBe(0);
|
||||
expect(defaultNote.getVelocity()).toBe(127);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getters and setters', () => {
|
||||
it('should get and set start beat', () => {
|
||||
expect(note.getStartBeat()).toBe(0)
|
||||
expect(note.getStartBeat()).toBe(0);
|
||||
|
||||
note.setStartBeat(1.5)
|
||||
expect(note.getStartBeat()).toBe(1.5)
|
||||
})
|
||||
note.setStartBeat(1.5);
|
||||
expect(note.getStartBeat()).toBe(1.5);
|
||||
});
|
||||
|
||||
it('should get and set end beat', () => {
|
||||
expect(note.getEndBeat()).toBe(1)
|
||||
expect(note.getEndBeat()).toBe(1);
|
||||
|
||||
note.setEndBeat(3.5)
|
||||
expect(note.getEndBeat()).toBe(3.5)
|
||||
})
|
||||
note.setEndBeat(3.5);
|
||||
expect(note.getEndBeat()).toBe(3.5);
|
||||
});
|
||||
|
||||
it('should get and set pitch', () => {
|
||||
expect(note.getPitch()).toBe(60)
|
||||
expect(note.getPitch()).toBe(60);
|
||||
|
||||
note.setPitch(72)
|
||||
expect(note.getPitch()).toBe(72)
|
||||
})
|
||||
note.setPitch(72);
|
||||
expect(note.getPitch()).toBe(72);
|
||||
});
|
||||
|
||||
it('should get and set velocity', () => {
|
||||
expect(note.getVelocity()).toBe(80)
|
||||
expect(note.getVelocity()).toBe(80);
|
||||
|
||||
note.setVelocity(100)
|
||||
expect(note.getVelocity()).toBe(100)
|
||||
})
|
||||
note.setVelocity(100);
|
||||
expect(note.getVelocity()).toBe(100);
|
||||
});
|
||||
|
||||
it('should get and set ID', () => {
|
||||
expect(note.getId()).toBe('test-note')
|
||||
expect(note.getId()).toBe('test-note');
|
||||
|
||||
note.setId('new-id')
|
||||
expect(note.getId()).toBe('new-id')
|
||||
})
|
||||
})
|
||||
note.setId('new-id');
|
||||
expect(note.getId()).toBe('new-id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('selection', () => {
|
||||
it('should start unselected', () => {
|
||||
expect(note.isSelected()).toBe(false)
|
||||
})
|
||||
expect(note.isSelected()).toBe(false);
|
||||
});
|
||||
|
||||
it('should select and deselect', () => {
|
||||
note.select()
|
||||
expect(note.isSelected()).toBe(true)
|
||||
note.select();
|
||||
expect(note.isSelected()).toBe(true);
|
||||
|
||||
note.deselect()
|
||||
expect(note.isSelected()).toBe(false)
|
||||
})
|
||||
})
|
||||
note.deselect();
|
||||
expect(note.isSelected()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('note duration', () => {
|
||||
it('should calculate duration correctly', () => {
|
||||
const durationNote = new KGMidiNote('duration-test', 1, 3, 60, 80)
|
||||
expect(durationNote.getEndBeat() - durationNote.getStartBeat()).toBe(2)
|
||||
})
|
||||
const durationNote = new KGMidiNote('duration-test', 1, 3, 60, 80);
|
||||
expect(durationNote.getEndBeat() - durationNote.getStartBeat()).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle zero duration', () => {
|
||||
const zeroDurationNote = new KGMidiNote('zero-duration', 2, 2, 60, 80)
|
||||
expect(zeroDurationNote.getEndBeat() - zeroDurationNote.getStartBeat()).toBe(0)
|
||||
})
|
||||
})
|
||||
const zeroDurationNote = new KGMidiNote('zero-duration', 2, 2, 60, 80);
|
||||
expect(zeroDurationNote.getEndBeat() - zeroDurationNote.getStartBeat()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pitch validation', () => {
|
||||
it('should accept valid MIDI pitch range', () => {
|
||||
// MIDI pitch range is typically 0-127
|
||||
note.setPitch(0)
|
||||
expect(note.getPitch()).toBe(0)
|
||||
note.setPitch(0);
|
||||
expect(note.getPitch()).toBe(0);
|
||||
|
||||
note.setPitch(127)
|
||||
expect(note.getPitch()).toBe(127)
|
||||
note.setPitch(127);
|
||||
expect(note.getPitch()).toBe(127);
|
||||
|
||||
note.setPitch(60) // Middle C
|
||||
expect(note.getPitch()).toBe(60)
|
||||
})
|
||||
})
|
||||
note.setPitch(60); // Middle C
|
||||
expect(note.getPitch()).toBe(60);
|
||||
});
|
||||
});
|
||||
|
||||
describe('velocity validation', () => {
|
||||
it('should accept valid MIDI velocity range', () => {
|
||||
// MIDI velocity range is typically 0-127
|
||||
note.setVelocity(0)
|
||||
expect(note.getVelocity()).toBe(0)
|
||||
note.setVelocity(0);
|
||||
expect(note.getVelocity()).toBe(0);
|
||||
|
||||
note.setVelocity(127)
|
||||
expect(note.getVelocity()).toBe(127)
|
||||
note.setVelocity(127);
|
||||
expect(note.getVelocity()).toBe(127);
|
||||
|
||||
note.setVelocity(64) // Mid velocity
|
||||
expect(note.getVelocity()).toBe(64)
|
||||
})
|
||||
})
|
||||
note.setVelocity(64); // Mid velocity
|
||||
expect(note.getVelocity()).toBe(64);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clone and comparison', () => {
|
||||
it('should create independent instances', () => {
|
||||
const note1 = new KGMidiNote('note-1', 0, 1, 60, 80)
|
||||
const note2 = new KGMidiNote('note-2', 0, 1, 60, 80)
|
||||
const note1 = new KGMidiNote('note-1', 0, 1, 60, 80);
|
||||
const note2 = new KGMidiNote('note-2', 0, 1, 60, 80);
|
||||
|
||||
expect(note1.getId()).not.toBe(note2.getId())
|
||||
expect(note1.getId()).not.toBe(note2.getId());
|
||||
|
||||
note1.setPitch(72)
|
||||
expect(note2.getPitch()).toBe(60) // Should remain unchanged
|
||||
})
|
||||
})
|
||||
note1.setPitch(72);
|
||||
expect(note2.getPitch()).toBe(60); // Should remain unchanged
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle negative start beat', () => {
|
||||
note.setStartBeat(-1)
|
||||
expect(note.getStartBeat()).toBe(-1)
|
||||
})
|
||||
note.setStartBeat(-1);
|
||||
expect(note.getStartBeat()).toBe(-1);
|
||||
});
|
||||
|
||||
it('should handle start beat after end beat', () => {
|
||||
note.setStartBeat(5)
|
||||
note.setEndBeat(2)
|
||||
note.setStartBeat(5);
|
||||
note.setEndBeat(2);
|
||||
|
||||
expect(note.getStartBeat()).toBe(5)
|
||||
expect(note.getEndBeat()).toBe(2)
|
||||
expect(note.getStartBeat()).toBe(5);
|
||||
expect(note.getEndBeat()).toBe(2);
|
||||
// Note: The class might need validation logic to prevent this
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
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'
|
||||
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
|
||||
let region: KGMidiRegion;
|
||||
|
||||
beforeEach(() => {
|
||||
region = new KGMidiRegion(
|
||||
@@ -15,8 +15,8 @@ describe('KGMidiRegion', () => {
|
||||
'Test Region',
|
||||
0,
|
||||
4
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create a MIDI region with correct properties', () => {
|
||||
@@ -27,327 +27,327 @@ describe('KGMidiRegion', () => {
|
||||
'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([])
|
||||
})
|
||||
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')
|
||||
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([])
|
||||
})
|
||||
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')
|
||||
})
|
||||
})
|
||||
expect(region.getCurrentType()).toBe('KGMidiRegion');
|
||||
});
|
||||
});
|
||||
|
||||
describe('note management', () => {
|
||||
let note1: KGMidiNote
|
||||
let note2: KGMidiNote
|
||||
let note3: KGMidiNote
|
||||
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 })
|
||||
})
|
||||
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)
|
||||
region.addNote(note1);
|
||||
|
||||
const notes = region.getNotes()
|
||||
expect(notes).toHaveLength(1)
|
||||
expect(notes[0]).toBe(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)
|
||||
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)
|
||||
})
|
||||
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)
|
||||
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)
|
||||
})
|
||||
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)
|
||||
region.addNote(note1);
|
||||
region.addNote(note1);
|
||||
|
||||
const notes = region.getNotes()
|
||||
expect(notes).toHaveLength(2)
|
||||
expect(notes[0]).toBe(note1)
|
||||
expect(notes[1]).toBe(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)
|
||||
})
|
||||
region.addNote(note1);
|
||||
region.addNote(note2);
|
||||
region.addNote(note3);
|
||||
});
|
||||
|
||||
it('should remove note by ID', () => {
|
||||
region.removeNote('note-2')
|
||||
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)
|
||||
})
|
||||
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
|
||||
const initialLength = region.getNotes().length;
|
||||
|
||||
region.removeNote('non-existent-note')
|
||||
region.removeNote('non-existent-note');
|
||||
|
||||
expect(region.getNotes()).toHaveLength(initialLength)
|
||||
})
|
||||
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)
|
||||
const duplicateNote = createMockMidiNote({ id: 'note-1', pitch: 72, startBeat: 3, endBeat: 4 });
|
||||
region.addNote(duplicateNote);
|
||||
|
||||
expect(region.getNotes()).toHaveLength(4)
|
||||
expect(region.getNotes()).toHaveLength(4);
|
||||
|
||||
region.removeNote('note-1')
|
||||
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)
|
||||
})
|
||||
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')
|
||||
const emptyRegion = new KGMidiRegion('empty', 'track', 0, 'Empty Region');
|
||||
|
||||
expect(() => emptyRegion.removeNote('note-1')).not.toThrow()
|
||||
expect(emptyRegion.getNotes()).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
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)
|
||||
})
|
||||
const notes = region.getNotes();
|
||||
expect(notes).toEqual([]);
|
||||
expect(notes).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should return all notes in region', () => {
|
||||
region.addNote(note1)
|
||||
region.addNote(note2)
|
||||
region.addNote(note1);
|
||||
region.addNote(note2);
|
||||
|
||||
const notes = region.getNotes()
|
||||
expect(notes).toHaveLength(2)
|
||||
expect(notes).toEqual([note1, 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()
|
||||
region.addNote(note1);
|
||||
const notes1 = region.getNotes();
|
||||
const notes2 = region.getNotes();
|
||||
|
||||
expect(notes1).toBe(notes2) // Same reference
|
||||
})
|
||||
})
|
||||
expect(notes1).toBe(notes2); // Same reference
|
||||
});
|
||||
});
|
||||
|
||||
describe('setNotes', () => {
|
||||
it('should replace all notes with new array', () => {
|
||||
region.addNote(note1)
|
||||
region.addNote(note2)
|
||||
region.addNote(note1);
|
||||
region.addNote(note2);
|
||||
|
||||
expect(region.getNotes()).toHaveLength(2)
|
||||
expect(region.getNotes()).toHaveLength(2);
|
||||
|
||||
region.setNotes([note3])
|
||||
region.setNotes([note3]);
|
||||
|
||||
const notes = region.getNotes()
|
||||
expect(notes).toHaveLength(1)
|
||||
expect(notes[0]).toBe(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.addNote(note1);
|
||||
region.addNote(note2);
|
||||
|
||||
region.setNotes([])
|
||||
region.setNotes([]);
|
||||
|
||||
expect(region.getNotes()).toHaveLength(0)
|
||||
})
|
||||
expect(region.getNotes()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should accept notes array with multiple notes', () => {
|
||||
const newNotes = [note1, note2, note3]
|
||||
region.setNotes(newNotes)
|
||||
const newNotes = [note1, note2, note3];
|
||||
region.setNotes(newNotes);
|
||||
|
||||
const retrievedNotes = region.getNotes()
|
||||
expect(retrievedNotes).toHaveLength(3)
|
||||
expect(retrievedNotes).toEqual(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)
|
||||
})
|
||||
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)
|
||||
expect(region.isSelected()).toBe(false);
|
||||
|
||||
region.select()
|
||||
expect(region.isSelected()).toBe(true)
|
||||
region.select();
|
||||
expect(region.isSelected()).toBe(true);
|
||||
|
||||
region.deselect()
|
||||
expect(region.isSelected()).toBe(false)
|
||||
})
|
||||
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.setName('Updated Region');
|
||||
expect(region.getName()).toBe('Updated Region');
|
||||
|
||||
region.setStartFromBeat(8)
|
||||
expect(region.getStartFromBeat()).toBe(8)
|
||||
region.setStartFromBeat(8);
|
||||
expect(region.getStartFromBeat()).toBe(8);
|
||||
|
||||
region.setLength(12)
|
||||
expect(region.getLength()).toBe(12)
|
||||
})
|
||||
})
|
||||
region.setLength(12);
|
||||
expect(region.getLength()).toBe(12);
|
||||
});
|
||||
});
|
||||
|
||||
describe('type identification', () => {
|
||||
it('should return correct current type', () => {
|
||||
expect(region.getCurrentType()).toBe('KGMidiRegion')
|
||||
})
|
||||
expect(region.getCurrentType()).toBe('KGMidiRegion');
|
||||
});
|
||||
|
||||
it('should return correct root type', () => {
|
||||
expect(region.getRootType()).toBe('KGRegion')
|
||||
})
|
||||
expect(region.getRootType()).toBe('KGRegion');
|
||||
});
|
||||
|
||||
it('should be instanceof both KGMidiRegion and KGRegion', () => {
|
||||
expect(region).toBeInstanceOf(KGMidiRegion)
|
||||
expect(region).toBeInstanceOf(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 })
|
||||
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)
|
||||
region.addNote(overlappingNote1);
|
||||
region.addNote(overlappingNote2);
|
||||
|
||||
const notes = region.getNotes()
|
||||
expect(notes).toHaveLength(2)
|
||||
expect(notes).toContain(overlappingNote1)
|
||||
expect(notes).toContain(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 })
|
||||
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)
|
||||
region.addNote(sameNote1);
|
||||
region.addNote(sameNote2);
|
||||
|
||||
expect(region.getNotes()).toHaveLength(2)
|
||||
})
|
||||
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 })
|
||||
const outsideNote = createMockMidiNote({ id: 'outside', pitch: 60, startBeat: 3, endBeat: 6 });
|
||||
|
||||
region.addNote(outsideNote)
|
||||
region.addNote(outsideNote);
|
||||
|
||||
const notes = region.getNotes()
|
||||
expect(notes).toHaveLength(1)
|
||||
expect(notes[0]).toBe(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 })
|
||||
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)
|
||||
zeroRegion.addNote(note);
|
||||
|
||||
expect(zeroRegion.getNotes()).toHaveLength(1)
|
||||
expect(zeroRegion.getLength()).toBe(0)
|
||||
})
|
||||
})
|
||||
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 })
|
||||
const originalNote = createMockMidiNote({ id: 'ref-test', pitch: 60, startBeat: 0, endBeat: 1 });
|
||||
|
||||
region.addNote(originalNote)
|
||||
const retrievedNote = region.getNotes()[0]
|
||||
region.addNote(originalNote);
|
||||
const retrievedNote = region.getNotes()[0];
|
||||
|
||||
expect(retrievedNote).toBe(originalNote) // Same reference
|
||||
expect(retrievedNote).toBe(originalNote); // Same reference
|
||||
|
||||
// Modify original note
|
||||
originalNote.setPitch(64)
|
||||
expect(retrievedNote.getPitch()).toBe(64) // Should reflect change
|
||||
})
|
||||
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)
|
||||
notes.forEach(note => region.addNote(note));
|
||||
expect(region.getNotes()).toHaveLength(3);
|
||||
|
||||
// Remove middle note
|
||||
region.removeNote('concurrent-2')
|
||||
expect(region.getNotes()).toHaveLength(2)
|
||||
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)
|
||||
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
|
||||
})
|
||||
})
|
||||
})
|
||||
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
|
||||
});
|
||||
});
|
||||
});
|
||||
+200
-200
@@ -1,70 +1,70 @@
|
||||
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'
|
||||
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
|
||||
let track: KGMidiTrack;
|
||||
|
||||
beforeEach(() => {
|
||||
track = new KGMidiTrack()
|
||||
})
|
||||
track = new KGMidiTrack();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create track with default values', () => {
|
||||
const defaultTrack = new KGMidiTrack()
|
||||
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) // DEFAULT_TRACK_VOLUME (0 dB)
|
||||
expect(defaultTrack.getRegions()).toEqual([])
|
||||
})
|
||||
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); // DEFAULT_TRACK_VOLUME (0 dB)
|
||||
expect(defaultTrack.getRegions()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should create track with custom parameters', () => {
|
||||
const customTrack = new KGMidiTrack('My Piano Track', 5, 'electric_piano_1', -4)
|
||||
const customTrack = new KGMidiTrack('My Piano Track', 5, 'electric_piano_1', -4);
|
||||
|
||||
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(-4)
|
||||
})
|
||||
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(-4);
|
||||
});
|
||||
|
||||
it('should set correct type identifier', () => {
|
||||
expect(track.getCurrentType()).toBe('KGMidiTrack')
|
||||
})
|
||||
expect(track.getCurrentType()).toBe('KGMidiTrack');
|
||||
});
|
||||
|
||||
it('should inherit from KGTrack', () => {
|
||||
expect(track).toBeInstanceOf(KGMidiTrack)
|
||||
expect(track).toBeInstanceOf(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')
|
||||
})
|
||||
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')
|
||||
})
|
||||
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')
|
||||
})
|
||||
})
|
||||
expect(track.getInstrument()).toBe('acoustic_grand_piano');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setInstrument', () => {
|
||||
it('should update instrument', () => {
|
||||
track.setInstrument('violin')
|
||||
expect(track.getInstrument()).toBe('violin')
|
||||
})
|
||||
track.setInstrument('violin');
|
||||
expect(track.getInstrument()).toBe('violin');
|
||||
});
|
||||
|
||||
it('should handle different instrument types', () => {
|
||||
const instruments: InstrumentType[] = [
|
||||
@@ -75,28 +75,28 @@ describe('KGMidiTrack', () => {
|
||||
'violin',
|
||||
'trumpet',
|
||||
'flute'
|
||||
]
|
||||
];
|
||||
|
||||
instruments.forEach(instrument => {
|
||||
track.setInstrument(instrument)
|
||||
expect(track.getInstrument()).toBe(instrument)
|
||||
})
|
||||
})
|
||||
track.setInstrument(instrument);
|
||||
expect(track.getInstrument()).toBe(instrument);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle rapid instrument changes', () => {
|
||||
track.setInstrument('piano')
|
||||
track.setInstrument('guitar')
|
||||
track.setInstrument('violin')
|
||||
track.setInstrument('piano');
|
||||
track.setInstrument('guitar');
|
||||
track.setInstrument('violin');
|
||||
|
||||
expect(track.getInstrument()).toBe('violin')
|
||||
})
|
||||
})
|
||||
})
|
||||
expect(track.getInstrument()).toBe('violin');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('region management', () => {
|
||||
let region1: KGMidiRegion
|
||||
let region2: KGMidiRegion
|
||||
let region3: KGMidiRegion
|
||||
let region1: KGMidiRegion;
|
||||
let region2: KGMidiRegion;
|
||||
let region3: KGMidiRegion;
|
||||
|
||||
beforeEach(() => {
|
||||
region1 = createMockMidiRegion({
|
||||
@@ -105,137 +105,137 @@ describe('KGMidiTrack', () => {
|
||||
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)
|
||||
const regions = [region1, region2];
|
||||
track.setRegions(regions);
|
||||
|
||||
expect(track.getRegions()).toHaveLength(2)
|
||||
expect(track.getRegions()).toEqual(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([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)
|
||||
})
|
||||
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([])
|
||||
track.setRegions([region1, region2]);
|
||||
track.setRegions([]);
|
||||
|
||||
expect(track.getRegions()).toHaveLength(0)
|
||||
})
|
||||
expect(track.getRegions()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should enforce KGMidiRegion type', () => {
|
||||
const regions: KGMidiRegion[] = [region1, region2]
|
||||
track.setRegions(regions)
|
||||
const regions: KGMidiRegion[] = [region1, region2];
|
||||
track.setRegions(regions);
|
||||
|
||||
const retrievedRegions = track.getRegions()
|
||||
const retrievedRegions = track.getRegions();
|
||||
retrievedRegions.forEach(region => {
|
||||
expect(region).toBeInstanceOf(KGMidiRegion)
|
||||
})
|
||||
})
|
||||
})
|
||||
expect(region).toBeInstanceOf(KGMidiRegion);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('inherited region methods', () => {
|
||||
beforeEach(() => {
|
||||
track.setRegions([region1, region2])
|
||||
})
|
||||
track.setRegions([region1, region2]);
|
||||
});
|
||||
|
||||
it('should inherit addRegion method', () => {
|
||||
track.addRegion(region3)
|
||||
track.addRegion(region3);
|
||||
|
||||
const regions = track.getRegions()
|
||||
expect(regions).toHaveLength(3)
|
||||
expect(regions).toContain(region3)
|
||||
})
|
||||
const regions = track.getRegions();
|
||||
expect(regions).toHaveLength(3);
|
||||
expect(regions).toContain(region3);
|
||||
});
|
||||
|
||||
it('should inherit removeRegion method', () => {
|
||||
track.removeRegion('region-1')
|
||||
track.removeRegion('region-1');
|
||||
|
||||
const regions = track.getRegions()
|
||||
expect(regions).toHaveLength(1)
|
||||
expect(regions).not.toContain(region1)
|
||||
expect(regions).toContain(region2)
|
||||
})
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
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', -1)
|
||||
const customTrack = new KGMidiTrack('Test Track', 42, 'violin', -1);
|
||||
|
||||
expect(customTrack.getName()).toBe('Test Track')
|
||||
expect(customTrack.getId()).toBe(42)
|
||||
expect(customTrack.getType()).toBe(TrackType.MIDI)
|
||||
expect(customTrack.getVolume()).toBe(-1)
|
||||
})
|
||||
expect(customTrack.getName()).toBe('Test Track');
|
||||
expect(customTrack.getId()).toBe(42);
|
||||
expect(customTrack.getType()).toBe(TrackType.MIDI);
|
||||
expect(customTrack.getVolume()).toBe(-1);
|
||||
});
|
||||
|
||||
it('should inherit base track setters', () => {
|
||||
track.setName('Updated Track')
|
||||
expect(track.getName()).toBe('Updated Track')
|
||||
track.setName('Updated Track');
|
||||
expect(track.getName()).toBe('Updated Track');
|
||||
|
||||
track.setVolume(-6)
|
||||
expect(track.getVolume()).toBe(-6)
|
||||
track.setVolume(-6);
|
||||
expect(track.getVolume()).toBe(-6);
|
||||
|
||||
track.setTrackIndex(3)
|
||||
expect(track.getTrackIndex()).toBe(3)
|
||||
})
|
||||
track.setTrackIndex(3);
|
||||
expect(track.getTrackIndex()).toBe(3);
|
||||
});
|
||||
|
||||
it('should inherit volume controls', () => {
|
||||
expect(track.getVolume()).toBe(0) // Default volume (0 dB)
|
||||
expect(track.getVolume()).toBe(0); // Default volume (0 dB)
|
||||
|
||||
track.setVolume(-6)
|
||||
expect(track.getVolume()).toBe(-6)
|
||||
track.setVolume(-6);
|
||||
expect(track.getVolume()).toBe(-6);
|
||||
|
||||
track.setVolume(0)
|
||||
expect(track.getVolume()).toBe(0)
|
||||
})
|
||||
})
|
||||
track.setVolume(0);
|
||||
expect(track.getVolume()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('type identification', () => {
|
||||
it('should return correct current type', () => {
|
||||
expect(track.getCurrentType()).toBe('KGMidiTrack')
|
||||
})
|
||||
expect(track.getCurrentType()).toBe('KGMidiTrack');
|
||||
});
|
||||
|
||||
it('should return correct root type', () => {
|
||||
expect(track.getRootType()).toBe('KGTrack')
|
||||
})
|
||||
expect(track.getRootType()).toBe('KGTrack');
|
||||
});
|
||||
|
||||
it('should have MIDI track type', () => {
|
||||
expect(track.getType()).toBe(TrackType.MIDI)
|
||||
})
|
||||
})
|
||||
expect(track.getType()).toBe(TrackType.MIDI);
|
||||
});
|
||||
});
|
||||
|
||||
describe('instrument type validation', () => {
|
||||
it('should handle all valid General MIDI instruments', () => {
|
||||
@@ -259,100 +259,100 @@ describe('KGMidiTrack', () => {
|
||||
'clarinet',
|
||||
'soprano_sax',
|
||||
'alto_sax'
|
||||
]
|
||||
];
|
||||
|
||||
validInstruments.forEach(instrument => {
|
||||
expect(() => {
|
||||
track.setInstrument(instrument)
|
||||
expect(track.getInstrument()).toBe(instrument)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
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(-3)
|
||||
track.setName('Piano Track');
|
||||
track.setInstrument('acoustic_grand_piano');
|
||||
track.setVolume(-3);
|
||||
|
||||
const regions = [
|
||||
createMockMidiRegion({ id: 'r1', trackId: '0', name: 'Intro' }),
|
||||
createMockMidiRegion({ id: 'r2', trackId: '0', name: 'Verse' })
|
||||
]
|
||||
track.setRegions(regions)
|
||||
];
|
||||
track.setRegions(regions);
|
||||
|
||||
// Verify initial state
|
||||
expect(track.getName()).toBe('Piano Track')
|
||||
expect(track.getInstrument()).toBe('acoustic_grand_piano')
|
||||
expect(track.getVolume()).toBe(-3)
|
||||
expect(track.getRegions()).toHaveLength(2)
|
||||
expect(track.getName()).toBe('Piano Track');
|
||||
expect(track.getInstrument()).toBe('acoustic_grand_piano');
|
||||
expect(track.getVolume()).toBe(-3);
|
||||
expect(track.getRegions()).toHaveLength(2);
|
||||
|
||||
// Modify state
|
||||
track.setInstrument('electric_piano_1')
|
||||
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(-3)
|
||||
expect(track.getRegions()).toHaveLength(3)
|
||||
})
|
||||
expect(track.getName()).toBe('Piano Track');
|
||||
expect(track.getInstrument()).toBe('electric_piano_1');
|
||||
expect(track.getVolume()).toBe(-3);
|
||||
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)
|
||||
track.addRegion(region);
|
||||
|
||||
// Verify region is in track
|
||||
expect(track.getRegions()).toContain(region)
|
||||
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)
|
||||
const foundRegion = track.getRegions().find(r => r.getId() === 'test-region');
|
||||
expect(foundRegion).toBe(region);
|
||||
|
||||
// Verify region properties
|
||||
expect(region.getTrackId()).toBe(track.getId().toString())
|
||||
})
|
||||
})
|
||||
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('')
|
||||
})
|
||||
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)
|
||||
})
|
||||
const negativeIdTrack = new KGMidiTrack('Test', -1, 'piano');
|
||||
expect(negativeIdTrack.getId()).toBe(-1);
|
||||
});
|
||||
|
||||
it('should handle volume boundaries', () => {
|
||||
track.setVolume(-60)
|
||||
expect(track.getVolume()).toBe(-60)
|
||||
track.setVolume(-60);
|
||||
expect(track.getVolume()).toBe(-60);
|
||||
|
||||
track.setVolume(0)
|
||||
expect(track.getVolume()).toBe(0)
|
||||
track.setVolume(0);
|
||||
expect(track.getVolume()).toBe(0);
|
||||
|
||||
track.setVolume(6)
|
||||
expect(track.getVolume()).toBe(6)
|
||||
track.setVolume(6);
|
||||
expect(track.getVolume()).toBe(6);
|
||||
|
||||
// Volume outside valid dB range is clamped
|
||||
track.setVolume(10)
|
||||
expect(track.getVolume()).toBe(6)
|
||||
track.setVolume(10);
|
||||
expect(track.getVolume()).toBe(6);
|
||||
|
||||
track.setVolume(-100)
|
||||
expect(track.getVolume()).toBe(-60)
|
||||
})
|
||||
track.setVolume(-100);
|
||||
expect(track.getVolume()).toBe(-60);
|
||||
});
|
||||
|
||||
it('should handle large number of regions', () => {
|
||||
const manyRegions = Array.from({ length: 100 }, (_, i) =>
|
||||
@@ -361,44 +361,44 @@ describe('KGMidiTrack', () => {
|
||||
trackId: track.getId().toString(),
|
||||
name: `Region ${i}`
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
track.setRegions(manyRegions)
|
||||
expect(track.getRegions()).toHaveLength(100)
|
||||
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')
|
||||
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()
|
||||
})
|
||||
})
|
||||
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')
|
||||
expect(track.getCurrentType()).toBe('KGMidiTrack');
|
||||
|
||||
// Test that default instrument fallback works
|
||||
const instrument = track.getInstrument()
|
||||
expect(instrument).toBe('acoustic_grand_piano')
|
||||
})
|
||||
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)
|
||||
track.setRegions(regions);
|
||||
|
||||
// Simulate what happens during serialization/deserialization
|
||||
const retrievedRegions = track.getRegions()
|
||||
expect(retrievedRegions).toHaveLength(2)
|
||||
const retrievedRegions = track.getRegions();
|
||||
expect(retrievedRegions).toHaveLength(2);
|
||||
retrievedRegions.forEach(region => {
|
||||
expect(region).toBeInstanceOf(KGMidiRegion)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
expect(region).toBeInstanceOf(KGMidiRegion);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user