feat: add Vitest unit testing infrastructure with initial test suite.

This commit is contained in:
Xiaohan-Tian
2025-09-03 22:15:05 -07:00
parent be39392aee
commit aa2b19966b
11 changed files with 2673 additions and 13 deletions
+115
View File
@@ -0,0 +1,115 @@
import { vi } from 'vitest'
/**
* Mock implementation of Tone.js for testing
* This provides consistent, deterministic behavior for audio-related tests
*/
// Mock Sampler class
export const MockSampler = vi.fn().mockImplementation(() => ({
triggerAttackRelease: vi.fn(),
triggerAttack: vi.fn(),
triggerRelease: vi.fn(),
dispose: vi.fn(),
loaded: true,
volume: {
value: -12
},
connect: vi.fn(),
disconnect: vi.fn(),
toDestination: vi.fn()
}))
// Mock Transport object
export const MockTransport = {
start: vi.fn(),
stop: vi.fn(),
pause: vi.fn(),
position: '0:0:0',
bpm: {
value: 120,
rampTo: vi.fn()
},
timeSignature: [4, 4],
state: 'stopped',
scheduleOnce: vi.fn(),
scheduleRepeat: vi.fn(),
cancel: vi.fn(),
clear: vi.fn()
}
// Mock Destination
export const MockDestination = {
volume: {
value: -12
},
mute: false,
connect: vi.fn(),
disconnect: vi.fn()
}
// Mock ToneAudioBuffer
export const MockToneAudioBuffer = vi.fn().mockImplementation(() => ({
loaded: true,
dispose: vi.fn(),
get: vi.fn(),
set: vi.fn(),
load: vi.fn().mockResolvedValue(undefined)
}))
// Mock Gain node
export const MockGain = vi.fn().mockImplementation(() => ({
gain: {
value: 1,
setValueAtTime: vi.fn(),
linearRampToValueAtTime: vi.fn(),
exponentialRampToValueAtTime: vi.fn()
},
connect: vi.fn(),
disconnect: vi.fn(),
dispose: vi.fn()
}))
// Mock Meter
export const MockMeter = vi.fn().mockImplementation(() => ({
getValue: vi.fn().mockReturnValue(-Infinity),
connect: vi.fn(),
disconnect: vi.fn(),
dispose: vi.fn()
}))
// Complete Tone.js mock
export const ToneMock = {
Sampler: MockSampler,
Transport: MockTransport,
Destination: MockDestination,
ToneAudioBuffer: MockToneAudioBuffer,
Gain: MockGain,
Meter: MockMeter,
// Context management
start: vi.fn().mockResolvedValue(undefined),
getContext: vi.fn().mockReturnValue({
state: 'running',
resume: vi.fn().mockResolvedValue(undefined),
suspend: vi.fn().mockResolvedValue(undefined),
close: vi.fn().mockResolvedValue(undefined)
}),
// Time utilities
Time: vi.fn().mockImplementation((time) => ({
toSeconds: vi.fn().mockReturnValue(parseFloat(time) || 0),
valueOf: vi.fn().mockReturnValue(parseFloat(time) || 0)
})),
// Frequency utilities
Frequency: vi.fn().mockImplementation((freq) => ({
toFrequency: vi.fn().mockReturnValue(parseFloat(freq) || 440),
valueOf: vi.fn().mockReturnValue(parseFloat(freq) || 440)
}))
}
// Setup the global mock
export const setupToneMocks = () => {
vi.doMock('tone', () => ToneMock)
}
+69
View File
@@ -0,0 +1,69 @@
import '@testing-library/jest-dom'
import 'reflect-metadata' // Required for class-transformer decorators
import { beforeAll, afterEach, vi } from 'vitest'
import { cleanup } from '@testing-library/react'
// Import our custom Tone.js mocks
import { setupToneMocks } from './mocks/tone'
// Setup global mocks
setupToneMocks()
// Mock KGCore globally to prevent store initialization issues
vi.mock('../core/KGCore', () => ({
KGCore: {
instance: vi.fn().mockReturnValue({
getCurrentProject: vi.fn().mockReturnValue({
getName: vi.fn().mockReturnValue('Test Project'),
getBpm: vi.fn().mockReturnValue(120),
getTimeSignature: vi.fn().mockReturnValue({ numerator: 4, denominator: 4 }),
getTracks: vi.fn().mockReturnValue([])
}),
getSelectedItems: vi.fn().mockReturnValue([]),
setSelectedItems: vi.fn(),
executeCommand: vi.fn()
})
}
}))
// Global test setup for all unit tests
// Clean up after each test
afterEach(() => {
cleanup()
vi.clearAllMocks()
})
// Setup before all tests
beforeAll(() => {
// Mock console methods to reduce noise in tests
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'warn').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
// Mock window.matchMedia (needed for some UI components)
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(), // deprecated
removeListener: vi.fn(), // deprecated
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
})
// Mock ResizeObserver (might be needed for some components)
global.ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}))
// Mock URL.createObjectURL (might be needed for file operations)
global.URL.createObjectURL = vi.fn(() => 'mocked-url')
global.URL.revokeObjectURL = vi.fn()
})
+155
View File
@@ -0,0 +1,155 @@
import { KGMidiNote } from '../../core/midi/KGMidiNote'
import { KGProject } from '../../core/KGProject'
import { KGMidiTrack } from '../../core/track/KGMidiTrack'
import { KGMidiRegion } from '../../core/region/KGMidiRegion'
/**
* Test data factories for creating mock objects
* These help create consistent test data across different test files
*/
export const createMockMidiNote = (overrides: Partial<{
pitch: number
velocity: number
startBeat: number
endBeat: number
id: string
}> = {}): KGMidiNote => {
const defaults = {
id: 'test-note-1',
startBeat: 0,
endBeat: 1,
pitch: 60, // Middle C
velocity: 80,
...overrides
}
return new KGMidiNote(
defaults.id,
defaults.startBeat,
defaults.endBeat,
defaults.pitch,
defaults.velocity
)
}
export const createMockMidiRegion = (overrides: Partial<{
id: string
trackId: string
trackIndex: number
name: string
startFromBeat: number
length: number
notes: KGMidiNote[]
}> = {}): KGMidiRegion => {
const defaults = {
id: 'test-region-1',
trackId: 'test-track-1',
trackIndex: 0,
name: 'Test Region',
startFromBeat: 0,
length: 4,
...overrides
}
const region = new KGMidiRegion(
defaults.id,
defaults.trackId,
defaults.trackIndex,
defaults.name,
defaults.startFromBeat,
defaults.length
)
// Add notes if provided
if (overrides.notes) {
overrides.notes.forEach(note => region.addNote(note))
}
return region
}
export const createMockMidiTrack = (overrides: Partial<{
name: string
id: number
instrument: string
volume: number
regions: KGMidiRegion[]
}> = {}): KGMidiTrack => {
const defaults = {
name: 'Test Track',
id: 0,
instrument: 'acoustic_grand_piano' as const,
volume: 0.8,
...overrides
}
const track = new KGMidiTrack(
defaults.name,
defaults.id,
defaults.instrument as keyof typeof import('../../constants/generalMidiConstants').FLUIDR3_INSTRUMENT_MAP,
defaults.volume
)
// Add regions if provided
if (overrides.regions) {
track.setRegions(overrides.regions)
}
return track
}
export const createMockProject = (overrides: Partial<{
name: string
bpm: number
timeSignature: { numerator: number; denominator: number }
tracks: KGMidiTrack[]
}> = {}): KGProject => {
const defaults = {
name: 'Test Project',
bpm: 120,
timeSignature: { numerator: 4, denominator: 4 },
tracks: [],
...overrides
}
const project = new KGProject(
defaults.name,
32, // maxBars
0, // currentBars
defaults.bpm,
defaults.timeSignature,
'C major', // keySignature
defaults.tracks, // tracks
1 // projectStructureVersion
)
return project
}
// Common test scenarios
export const createBasicProjectWithTrack = (): { project: KGProject; track: KGMidiTrack; region: KGMidiRegion } => {
const notes = [
createMockMidiNote({ pitch: 60, startBeat: 0, endBeat: 1 }),
createMockMidiNote({ pitch: 64, startBeat: 1, endBeat: 2 }),
]
const region = createMockMidiRegion({
id: 'region-1',
trackId: 'track-1',
notes
})
const track = createMockMidiTrack({
id: 1,
name: 'Track 1',
regions: [region]
})
const project = createMockProject({
name: 'Basic Test Project',
tracks: [track]
})
return { project, track, region }
}
+22
View File
@@ -0,0 +1,22 @@
import type { ReactElement } from 'react'
import { render, type RenderOptions } from '@testing-library/react'
// Custom render function that includes any providers your app needs
// This can be extended later with Zustand store providers, etc.
type CustomRenderOptions = Omit<RenderOptions, 'wrapper'>
const customRender = (
ui: ReactElement,
options?: CustomRenderOptions
) => {
// If you need to wrap components with providers (like Zustand store),
// you can create an AllTheProviders wrapper here
return render(ui, options)
}
// Re-export everything from testing-library/react
// eslint-disable-next-line react-refresh/only-export-components
export * from '@testing-library/react'
export { customRender as render }