feat: add Vitest unit testing infrastructure with initial test suite.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user