134 lines
4.8 KiB
TypeScript
134 lines
4.8 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { gardenAudioConfig } from './audio/garden-audio-config';
|
|
import { getInitialVibe, hexToRgb, VIBE_PRESETS } from './vibes';
|
|
|
|
const originalLocalStorage = globalThis.localStorage;
|
|
|
|
const setBrowserVibeState = ({
|
|
storedVibeId = null,
|
|
}: {
|
|
storedVibeId?: string | null;
|
|
}) => {
|
|
Object.defineProperty(globalThis, 'localStorage', {
|
|
configurable: true,
|
|
value: {
|
|
getItem: vi.fn((key: string) =>
|
|
key === 'fleeting-garden:vibe' ? storedVibeId : null
|
|
),
|
|
},
|
|
});
|
|
};
|
|
|
|
describe('vibe selection', () => {
|
|
afterEach(() => {
|
|
Object.defineProperty(globalThis, 'localStorage', {
|
|
configurable: true,
|
|
value: originalLocalStorage,
|
|
});
|
|
});
|
|
|
|
it('uses a valid stored vibe id', () => {
|
|
setBrowserVibeState({ storedVibeId: 'sunlit-moss' });
|
|
|
|
expect(getInitialVibe().id).toBe('sunlit-moss');
|
|
});
|
|
|
|
it('falls back to the default preset for an unknown stored vibe id', () => {
|
|
setBrowserVibeState({ storedVibeId: 'unknown' });
|
|
|
|
expect(getInitialVibe()).toBe(VIBE_PRESETS[0]);
|
|
});
|
|
});
|
|
|
|
describe('vibe and audio config contract', () => {
|
|
it('keeps preset ids unique and URL-safe', () => {
|
|
const vibeIds = VIBE_PRESETS.map((vibe) => vibe.id);
|
|
|
|
expect(new Set(vibeIds).size).toBe(vibeIds.length);
|
|
expect(vibeIds.every((id) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(id))).toBe(true);
|
|
});
|
|
|
|
it('keeps each vibe palette and audio profile complete', () => {
|
|
VIBE_PRESETS.forEach((vibe) => {
|
|
expect(vibe.colors).toHaveLength(3);
|
|
vibe.colors.forEach((color) => {
|
|
expect(color).toMatch(/^#[0-9a-f]{6}$/i);
|
|
hexToRgb(color).forEach((channel) => {
|
|
expect(channel).toBeGreaterThanOrEqual(0);
|
|
expect(channel).toBeLessThanOrEqual(1);
|
|
});
|
|
});
|
|
|
|
const profile = vibe.audio;
|
|
expect(Number.isFinite(profile.rootMidi)).toBe(true);
|
|
expect(profile.scale.length).toBeGreaterThan(0);
|
|
expect(profile.scale.every((degree) => Number.isFinite(degree))).toBe(true);
|
|
expect(profile.brightness).toBeGreaterThan(0);
|
|
expect(profile.delayTimeMultiplier).toBeGreaterThan(0);
|
|
expect(profile.progression.length).toBeGreaterThan(0);
|
|
profile.progression.forEach((chord) => {
|
|
expect(Number.isFinite(chord.rootOffset)).toBe(true);
|
|
expect(['major', 'minor']).toContain(chord.quality);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('keeps audio style voices aligned with the rotating style pools', () => {
|
|
expect(gardenAudioConfig.styleVoices).toHaveLength(
|
|
gardenAudioConfig.generativePiano.stylePools.length
|
|
);
|
|
gardenAudioConfig.styleVoices.forEach((voice) => {
|
|
expect(Number.isFinite(voice.scaleDegreeOffset)).toBe(true);
|
|
expect(voice.velocityMultiplier).toBeGreaterThan(0);
|
|
expect(Math.abs(voice.panOffset)).toBeLessThanOrEqual(1);
|
|
});
|
|
});
|
|
|
|
it('keeps audio timing, graph, and density settings bounded', () => {
|
|
const { delay, generativePiano, graph, piano, rhythm } = gardenAudioConfig;
|
|
|
|
expect(rhythm.bpm).toBeGreaterThan(0);
|
|
expect(rhythm.stepsPerBeat).toBeGreaterThan(0);
|
|
expect(rhythm.stepsPerBar).toBeGreaterThanOrEqual(rhythm.stepsPerBeat);
|
|
expect(rhythm.lookaheadSeconds).toBeGreaterThanOrEqual(piano.scheduleAheadSeconds);
|
|
|
|
expect(delay.feedbackMin).toBeLessThanOrEqual(delay.feedback);
|
|
expect(delay.feedback).toBeLessThanOrEqual(delay.feedbackMax);
|
|
expect(delay.feedbackHighPassHz).toBeLessThan(delay.feedbackLowPassHz);
|
|
expect(delay.returnLowPassHz).toBeGreaterThan(delay.feedbackHighPassHz);
|
|
|
|
generativePiano.stylePools.forEach((register) => {
|
|
expect(register.midiMin).toBeLessThan(register.preferredMidi);
|
|
expect(register.preferredMidi).toBeLessThan(register.midiMax);
|
|
});
|
|
generativePiano.padRegisters.forEach((register) => {
|
|
expect(register.midiMin).toBeLessThan(register.preferredMidi);
|
|
expect(register.preferredMidi).toBeLessThan(register.midiMax);
|
|
});
|
|
|
|
expect(generativePiano.brushStreamIdleIntervalBeats).toBeGreaterThanOrEqual(
|
|
generativePiano.brushStreamActiveIntervalBeats
|
|
);
|
|
expect(generativePiano.brushStreamActiveIntervalBeats).toBeGreaterThanOrEqual(
|
|
generativePiano.brushStreamIntenseIntervalBeats
|
|
);
|
|
expect(generativePiano.brushStreamIntenseIntervalBeats).toBeGreaterThanOrEqual(
|
|
generativePiano.brushStreamManicIntervalBeats
|
|
);
|
|
expect(generativePiano.maxBrushPhraseLayers).toBeLessThanOrEqual(3);
|
|
expect(generativePiano.maxBrushStreamNotesPerBar).toBeLessThanOrEqual(
|
|
rhythm.stepsPerBar
|
|
);
|
|
|
|
Object.values(graph.pianoBusGains).forEach((gain) => {
|
|
expect(gain).toBeGreaterThan(0);
|
|
expect(gain).toBeLessThanOrEqual(1.2);
|
|
});
|
|
});
|
|
|
|
it('falls back to finite RGB channels for malformed hex colors', () => {
|
|
expect(hexToRgb('not-a-color')).toEqual([0, 0, 0]);
|
|
expect(hexToRgb('#abcdzz')).toEqual([0, 0, 0]);
|
|
});
|
|
});
|