feat: persist global track visibility status and metronome status
This commit is contained in:
@@ -58,6 +58,10 @@ const storeState = {
|
||||
addAudioTrack: vi.fn(),
|
||||
projectName: 'Test Project',
|
||||
savedProjectName: 'Test Project',
|
||||
showGlobalTracks: false,
|
||||
setShowGlobalTracks: vi.fn((show: boolean) => {
|
||||
storeState.showGlobalTracks = show;
|
||||
}),
|
||||
requestPianoRollScroll: vi.fn(),
|
||||
mainContentScrollRequest: null,
|
||||
activeTrackAutomationTrackId: null,
|
||||
@@ -82,6 +86,11 @@ const finishTrackCreateDialogClose = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const toggleGlobalTracksAndRerender = (rerender: (ui: React.ReactNode) => void) => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||
rerender(<MainContent />);
|
||||
};
|
||||
|
||||
vi.mock('../stores/projectStore', () => ({
|
||||
useProjectStore: (selector?: StoreSelector) => (
|
||||
selector ? selector(storeState) : storeState
|
||||
@@ -160,6 +169,7 @@ describe('MainContent', () => {
|
||||
storeState.selectedRegionIds = [];
|
||||
storeState.activeRegionId = null;
|
||||
storeState.showPianoRoll = false;
|
||||
storeState.showGlobalTracks = false;
|
||||
storeState.playheadPosition = 0;
|
||||
storeState.timeSignature = { numerator: 4, denominator: 4 };
|
||||
storeState.clearAllSelections.mockClear();
|
||||
@@ -171,6 +181,7 @@ describe('MainContent', () => {
|
||||
storeState.openSpectrogramViewer.mockClear();
|
||||
storeState.addTrack.mockClear();
|
||||
storeState.addAudioTrack.mockClear();
|
||||
storeState.setShowGlobalTracks.mockClear();
|
||||
executeCommandMock.mockClear();
|
||||
});
|
||||
|
||||
@@ -385,29 +396,35 @@ describe('MainContent', () => {
|
||||
expect(screen.queryByRole('dialog', { name: 'Create New Track' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('toggles the mock global tracks button active state locally', () => {
|
||||
render(<MainContent />);
|
||||
it('toggles the persisted global tracks button active state through the store', () => {
|
||||
const { rerender } = render(<MainContent />);
|
||||
|
||||
const globalTracksButton = screen.getByRole('button', { name: 'Show global tracks' });
|
||||
let globalTracksButton = screen.getByRole('button', { name: 'Show global tracks' });
|
||||
|
||||
expect(globalTracksButton.className).not.toContain('active');
|
||||
|
||||
fireEvent.click(globalTracksButton);
|
||||
expect(storeState.setShowGlobalTracks).toHaveBeenCalledWith(true);
|
||||
rerender(<MainContent />);
|
||||
globalTracksButton = screen.getByRole('button', { name: 'Show global tracks' });
|
||||
expect(globalTracksButton.className).toContain('active');
|
||||
|
||||
fireEvent.click(globalTracksButton);
|
||||
expect(storeState.setShowGlobalTracks).toHaveBeenCalledWith(false);
|
||||
rerender(<MainContent />);
|
||||
globalTracksButton = screen.getByRole('button', { name: 'Show global tracks' });
|
||||
expect(globalTracksButton.className).not.toContain('active');
|
||||
});
|
||||
|
||||
it('renders the four mock global tracks only when toggled on', () => {
|
||||
render(<MainContent />);
|
||||
it('renders the four global tracks only when the persisted toggle is on', () => {
|
||||
const { rerender } = render(<MainContent />);
|
||||
|
||||
expect(screen.queryByText('Marker')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Tempo')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Key Signature')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Chord')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||
toggleGlobalTracksAndRerender(rerender);
|
||||
|
||||
expect(screen.getByText('Marker')).toBeInTheDocument();
|
||||
expect(screen.getByText('Tempo')).toBeInTheDocument();
|
||||
@@ -416,7 +433,7 @@ describe('MainContent', () => {
|
||||
|
||||
const globalTracksInfoShell = screen.getByRole('button', { name: 'Add Marker global track item' }).closest('.global-tracks-info-shell') as HTMLElement;
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||
toggleGlobalTracksAndRerender(rerender);
|
||||
fireEvent.transitionEnd(globalTracksInfoShell);
|
||||
|
||||
expect(screen.queryByText('Marker')).not.toBeInTheDocument();
|
||||
@@ -427,9 +444,9 @@ describe('MainContent', () => {
|
||||
|
||||
it('routes the tempo and chord global track add buttons through commands', () => {
|
||||
storeState.playheadPosition = 5;
|
||||
render(<MainContent />);
|
||||
const { rerender } = render(<MainContent />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||
toggleGlobalTracksAndRerender(rerender);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Add Tempo global track item' }));
|
||||
expect(executeCommandMock).toHaveBeenCalledTimes(1);
|
||||
@@ -450,10 +467,10 @@ describe('MainContent', () => {
|
||||
]);
|
||||
storeState.globalTracks = globalTracks;
|
||||
|
||||
render(<MainContent />);
|
||||
const { rerender } = render(<MainContent />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'select-midi-region' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||
toggleGlobalTracksAndRerender(rerender);
|
||||
fireEvent.click(screen.getByText('Am'));
|
||||
|
||||
expect(storeState.selectedRegionIds).toEqual(['chord-1']);
|
||||
@@ -471,9 +488,9 @@ describe('MainContent', () => {
|
||||
]);
|
||||
storeState.globalTracks = globalTracks;
|
||||
|
||||
render(<MainContent />);
|
||||
const { rerender } = render(<MainContent />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||
toggleGlobalTracksAndRerender(rerender);
|
||||
fireEvent.click(screen.getByText('Am'));
|
||||
fireEvent.click(screen.getByText('G'), { shiftKey: true });
|
||||
|
||||
@@ -492,9 +509,9 @@ describe('MainContent', () => {
|
||||
]);
|
||||
storeState.globalTracks = globalTracks;
|
||||
|
||||
render(<MainContent />);
|
||||
const { rerender } = render(<MainContent />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||
toggleGlobalTracksAndRerender(rerender);
|
||||
|
||||
const firstChord = screen.getByText('Am');
|
||||
const lastChord = screen.getByText('G');
|
||||
@@ -524,9 +541,9 @@ describe('MainContent', () => {
|
||||
]);
|
||||
storeState.globalTracks = globalTracks;
|
||||
|
||||
render(<MainContent />);
|
||||
const { rerender } = render(<MainContent />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||
toggleGlobalTracksAndRerender(rerender);
|
||||
fireEvent.click(screen.getByText('Am'));
|
||||
fireEvent.click(screen.getByText('128 BPM'), { metaKey: true });
|
||||
|
||||
@@ -548,9 +565,9 @@ describe('MainContent', () => {
|
||||
]);
|
||||
storeState.globalTracks = globalTracks;
|
||||
|
||||
render(<MainContent />);
|
||||
const { rerender } = render(<MainContent />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||
toggleGlobalTracksAndRerender(rerender);
|
||||
fireEvent.click(screen.getByText('Am'));
|
||||
fireEvent.click(screen.getByText('140 BPM'), { shiftKey: true });
|
||||
|
||||
@@ -566,9 +583,9 @@ describe('MainContent', () => {
|
||||
storeState.globalTracks = globalTracks;
|
||||
storeState.playheadPosition = 3;
|
||||
|
||||
render(<MainContent />);
|
||||
const { rerender } = render(<MainContent />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||
toggleGlobalTracksAndRerender(rerender);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Add Chord global track item' }));
|
||||
|
||||
expect(executeCommandMock).toHaveBeenCalledTimes(1);
|
||||
@@ -586,9 +603,9 @@ describe('MainContent', () => {
|
||||
]);
|
||||
storeState.globalTracks = globalTracks;
|
||||
|
||||
render(<MainContent />);
|
||||
const { rerender } = render(<MainContent />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||
toggleGlobalTracksAndRerender(rerender);
|
||||
fireEvent.doubleClick(screen.getByText('Am'));
|
||||
fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Tab' });
|
||||
|
||||
@@ -605,9 +622,9 @@ describe('MainContent', () => {
|
||||
]);
|
||||
storeState.globalTracks = globalTracks;
|
||||
|
||||
render(<MainContent />);
|
||||
const { rerender } = render(<MainContent />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||
toggleGlobalTracksAndRerender(rerender);
|
||||
fireEvent.doubleClick(screen.getByText('Am'));
|
||||
fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Tab' });
|
||||
|
||||
@@ -626,9 +643,9 @@ describe('MainContent', () => {
|
||||
]);
|
||||
storeState.globalTracks = globalTracks;
|
||||
|
||||
render(<MainContent />);
|
||||
const { rerender } = render(<MainContent />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||
toggleGlobalTracksAndRerender(rerender);
|
||||
fireEvent.doubleClick(screen.getByText('G'));
|
||||
fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Tab', shiftKey: true });
|
||||
|
||||
@@ -639,18 +656,18 @@ describe('MainContent', () => {
|
||||
});
|
||||
|
||||
it('routes the signature global track add button through a command', () => {
|
||||
render(<MainContent />);
|
||||
const { rerender } = render(<MainContent />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||
toggleGlobalTracksAndRerender(rerender);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Add Key Signature global track item' }));
|
||||
|
||||
expect(executeCommandMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('creates new marker regions with a one-bar default length', () => {
|
||||
render(<MainContent />);
|
||||
const { rerender } = render(<MainContent />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||
toggleGlobalTracksAndRerender(rerender);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Add Marker global track item' }));
|
||||
|
||||
expect(executeCommandMock).toHaveBeenCalledTimes(1);
|
||||
@@ -672,13 +689,13 @@ describe('MainContent', () => {
|
||||
return 1;
|
||||
});
|
||||
|
||||
const { container } = render(<MainContent />);
|
||||
const { container, rerender } = render(<MainContent />);
|
||||
const mainContent = container.querySelector('.main-content') as HTMLDivElement;
|
||||
Object.defineProperty(mainContent, 'scrollWidth', { configurable: true, value: 4000 });
|
||||
Object.defineProperty(mainContent, 'clientWidth', { configurable: true, value: 900 });
|
||||
mainContent.scrollLeft = 500;
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||
toggleGlobalTracksAndRerender(rerender);
|
||||
fireEvent.doubleClick(screen.getByText('Am'));
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -68,6 +68,8 @@ const MainContent: React.FC<MainContentProps> = ({
|
||||
addAudioTrack,
|
||||
projectName,
|
||||
savedProjectName,
|
||||
showGlobalTracks,
|
||||
setShowGlobalTracks,
|
||||
requestPianoRollScroll,
|
||||
mainContentScrollRequest,
|
||||
activeTrackAutomationTrackId,
|
||||
@@ -84,7 +86,6 @@ const MainContent: React.FC<MainContentProps> = ({
|
||||
const [draggedTrackIndex, setDraggedTrackIndex] = useState<number | null>(null);
|
||||
const [dragOverTrackIndex, setDragOverTrackIndex] = useState<number | null>(null);
|
||||
const [showCreateTrackModal, setShowCreateTrackModal] = useState(false);
|
||||
const [showGlobalTracksMock, setShowGlobalTracksMock] = useState(false);
|
||||
|
||||
const mainContentRegions = useMainContentRegions({
|
||||
tracks,
|
||||
@@ -252,10 +253,10 @@ const MainContent: React.FC<MainContentProps> = ({
|
||||
setShowCreateTrackModal(true);
|
||||
}, []);
|
||||
|
||||
const handleToggleGlobalTracksMock = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const handleToggleGlobalTracks = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
setShowGlobalTracksMock(previous => !previous);
|
||||
}, []);
|
||||
setShowGlobalTracks(!showGlobalTracks);
|
||||
}, [setShowGlobalTracks, showGlobalTracks]);
|
||||
|
||||
const handleGlobalChordDropToTrack = useCallback(async (draggedRegionId: string, trackIndex: number) => {
|
||||
const targetTrack = tracks[trackIndex];
|
||||
@@ -348,10 +349,10 @@ const MainContent: React.FC<MainContentProps> = ({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`track-header-button${showGlobalTracksMock ? ' active' : ''}`}
|
||||
className={`track-header-button${showGlobalTracks ? ' active' : ''}`}
|
||||
aria-label={t('mainContent.showGlobalTracks')}
|
||||
title={t('mainContent.showGlobalTracks')}
|
||||
onClick={handleToggleGlobalTracksMock}
|
||||
onClick={handleToggleGlobalTracks}
|
||||
>
|
||||
<FaSquareArrowUpRight />
|
||||
</button>
|
||||
@@ -385,7 +386,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
||||
</div>
|
||||
|
||||
<MainContentGlobalTracksSection
|
||||
visible={showGlobalTracksMock}
|
||||
visible={showGlobalTracks}
|
||||
{...mainContentGlobalTracks.sectionProps}
|
||||
chordLaneProps={{
|
||||
...mainContentGlobalTracks.sectionProps.chordLaneProps,
|
||||
|
||||
+28
-2
@@ -54,11 +54,19 @@ export class KGProject {
|
||||
@WithDefault(1)
|
||||
private pianoRollZoom: number = 1;
|
||||
|
||||
@Expose()
|
||||
@WithDefault(false)
|
||||
private showGlobalTracks: boolean = false;
|
||||
|
||||
@Expose()
|
||||
@WithDefault(false)
|
||||
private isMetronomeEnabled: boolean = false;
|
||||
|
||||
@Expose()
|
||||
@WithDefault(0)
|
||||
private projectStructureVersion: number = 0;
|
||||
|
||||
public static readonly CURRENT_PROJECT_STRUCTURE_VERSION: number = 15;
|
||||
public static readonly CURRENT_PROJECT_STRUCTURE_VERSION: number = 16;
|
||||
|
||||
@Expose()
|
||||
@Type(() => KGTrack, {
|
||||
@@ -89,7 +97,7 @@ export class KGProject {
|
||||
private globalTracks: KGGlobalTrack[] = createDefaultGlobalTracks();
|
||||
|
||||
// Constructor
|
||||
constructor(name: string = RESERVED_PROJECT_NAME, maxBars: number = 32, currentBars: number = 0, bpm: number = TIME_CONSTANTS.DEFAULT_BPM, timeSignature: TimeSignature = { numerator: 4, denominator: 4 }, keySignature: KeySignature = "C major", selectedMode: string = "ionian", isLooping: boolean = false, loopingRange: [number, number] = [0, 0], barWidthMultiplier: number = 2, tracks: KGTrack[] = [], projectStructureVersion: number = KGProject.CURRENT_PROJECT_STRUCTURE_VERSION, pianoRollZoom: number = 1, globalTracks: KGGlobalTrack[] = createDefaultGlobalTracks()) {
|
||||
constructor(name: string = RESERVED_PROJECT_NAME, maxBars: number = 32, currentBars: number = 0, bpm: number = TIME_CONSTANTS.DEFAULT_BPM, timeSignature: TimeSignature = { numerator: 4, denominator: 4 }, keySignature: KeySignature = "C major", selectedMode: string = "ionian", isLooping: boolean = false, loopingRange: [number, number] = [0, 0], barWidthMultiplier: number = 2, tracks: KGTrack[] = [], projectStructureVersion: number = KGProject.CURRENT_PROJECT_STRUCTURE_VERSION, pianoRollZoom: number = 1, globalTracks: KGGlobalTrack[] = createDefaultGlobalTracks(), showGlobalTracks: boolean = false, isMetronomeEnabled: boolean = false) {
|
||||
this.name = name;
|
||||
this.maxBars = maxBars;
|
||||
this.currentBars = currentBars;
|
||||
@@ -104,6 +112,8 @@ export class KGProject {
|
||||
this.projectStructureVersion = projectStructureVersion;
|
||||
this.pianoRollZoom = pianoRollZoom;
|
||||
this.globalTracks = globalTracks;
|
||||
this.showGlobalTracks = showGlobalTracks;
|
||||
this.isMetronomeEnabled = isMetronomeEnabled;
|
||||
}
|
||||
|
||||
// Getters
|
||||
@@ -188,6 +198,22 @@ export class KGProject {
|
||||
return this.projectStructureVersion;
|
||||
}
|
||||
|
||||
public getShowGlobalTracks(): boolean {
|
||||
return this.showGlobalTracks;
|
||||
}
|
||||
|
||||
public setShowGlobalTracks(showGlobalTracks: boolean): void {
|
||||
this.showGlobalTracks = showGlobalTracks;
|
||||
}
|
||||
|
||||
public getIsMetronomeEnabled(): boolean {
|
||||
return this.isMetronomeEnabled;
|
||||
}
|
||||
|
||||
public setIsMetronomeEnabled(isMetronomeEnabled: boolean): void {
|
||||
this.isMetronomeEnabled = isMetronomeEnabled;
|
||||
}
|
||||
|
||||
public getIsLooping(): boolean {
|
||||
return this.isLooping;
|
||||
}
|
||||
|
||||
@@ -163,6 +163,20 @@ describe('KGProjectStorage', () => {
|
||||
expect(loaded!.getPianoRollZoom()).toBe(5);
|
||||
});
|
||||
|
||||
it('preserves persisted global-track visibility and metronome state when saving and loading', async () => {
|
||||
const project = createTestProject('Toggle Song');
|
||||
project.setShowGlobalTracks(true);
|
||||
project.setIsMetronomeEnabled(true);
|
||||
|
||||
await storage.save('Toggle Song', project);
|
||||
|
||||
const loaded = await storage.load('Toggle Song');
|
||||
|
||||
expect(loaded).not.toBeNull();
|
||||
expect(loaded!.getShowGlobalTracks()).toBe(true);
|
||||
expect(loaded!.getIsMetronomeEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('preserves global tracks and marker regions when saving and loading', async () => {
|
||||
const project = createTestProject('Marker Song');
|
||||
const markerTrack = project.getGlobalTracks().find(track => track.getType() === GlobalTrackType.Marker);
|
||||
|
||||
@@ -14,6 +14,7 @@ import { upgradeToV12 } from './upgradeToV12';
|
||||
import { upgradeToV13 } from './upgradeToV13';
|
||||
import { upgradeToV14 } from './upgradeToV14';
|
||||
import { upgradeToV15 } from './upgradeToV15';
|
||||
import { upgradeToV16 } from './upgradeToV16';
|
||||
|
||||
/**
|
||||
* Upgrade the given project to the latest structure version, one version at a time.
|
||||
@@ -93,6 +94,10 @@ export function upgradeProjectToLatest(project: KGProject): KGProject {
|
||||
workingProject = upgradeToV15(workingProject);
|
||||
break;
|
||||
}
|
||||
case 16: {
|
||||
workingProject = upgradeToV16(workingProject);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// If an upgrader is missing, throw to prevent loading incompatible structures
|
||||
throw new Error(`No upgrader found for project structure version ${nextVersion}`);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { KGProject } from '../KGProject';
|
||||
import { upgradeProjectToLatest } from './KGProjectUpgrader';
|
||||
import { upgradeToV16 } from './upgradeToV16';
|
||||
|
||||
describe('upgradeToV16', () => {
|
||||
it('defaults persisted UI/audio toggles to false for legacy projects', () => {
|
||||
const project = new KGProject('Legacy', 32, 0, 125, undefined, undefined, undefined, undefined, [0, 0], 1, [], 15, 1, []);
|
||||
|
||||
upgradeToV16(project);
|
||||
|
||||
expect(project.getProjectStructureVersion()).toBe(16);
|
||||
expect(project.getShowGlobalTracks()).toBe(false);
|
||||
expect(project.getIsMetronomeEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('preserves existing persisted toggle values through the main upgrader path', () => {
|
||||
const project = new KGProject('Legacy', 32, 0, 125, undefined, undefined, undefined, undefined, [0, 0], 1, [], 15, 1, [], true, true);
|
||||
|
||||
const upgraded = upgradeProjectToLatest(project);
|
||||
|
||||
expect(upgraded.getProjectStructureVersion()).toBe(KGProject.CURRENT_PROJECT_STRUCTURE_VERSION);
|
||||
expect(upgraded.getShowGlobalTracks()).toBe(true);
|
||||
expect(upgraded.getIsMetronomeEnabled()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { KGProject } from '../KGProject';
|
||||
|
||||
export function upgradeToV16(project: KGProject): KGProject {
|
||||
try {
|
||||
if (typeof project.getShowGlobalTracks() !== 'boolean') {
|
||||
project.setShowGlobalTracks(false);
|
||||
}
|
||||
|
||||
if (typeof project.getIsMetronomeEnabled() !== 'boolean') {
|
||||
project.setIsMetronomeEnabled(false);
|
||||
}
|
||||
} finally {
|
||||
project.setProjectStructureVersion(16);
|
||||
}
|
||||
|
||||
return project;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { act } from '@testing-library/react';
|
||||
import { KGTrack } from '../core/track/KGTrack';
|
||||
import { KGMidiTrack } from '../core/track/KGMidiTrack';
|
||||
import { createDefaultGlobalTracks } from '../core/global-track';
|
||||
|
||||
const pianoRollStateMocks = vi.hoisted(() => ({
|
||||
setSheetMusicViewEnabled: vi.fn(),
|
||||
@@ -9,16 +10,27 @@ const pianoRollStateMocks = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
let mockTracks: KGTrack[] = [new KGMidiTrack('Track 1', 0, 'acoustic_grand_piano')];
|
||||
let mockIsMetronomeEnabled = false;
|
||||
let mockShowGlobalTracks = false;
|
||||
const mockProject = {
|
||||
getTimeSignature: () => ({ numerator: 4, denominator: 4 }),
|
||||
getMaxBars: () => 32,
|
||||
getBarWidthMultiplier: () => 1,
|
||||
getTracks: () => mockTracks,
|
||||
getGlobalTracks: () => createDefaultGlobalTracks(),
|
||||
getBpm: () => 120,
|
||||
getKeySignature: () => 'C major',
|
||||
getName: () => 'Test Project',
|
||||
getSelectedMode: () => 'major',
|
||||
getIsLooping: () => false,
|
||||
getIsMetronomeEnabled: () => mockIsMetronomeEnabled,
|
||||
setIsMetronomeEnabled: vi.fn((value: boolean) => {
|
||||
mockIsMetronomeEnabled = value;
|
||||
}),
|
||||
getShowGlobalTracks: () => mockShowGlobalTracks,
|
||||
setShowGlobalTracks: vi.fn((value: boolean) => {
|
||||
mockShowGlobalTracks = value;
|
||||
}),
|
||||
getLoopingRange: () => [0, 0] as [number, number],
|
||||
getPianoRollZoom: () => 1,
|
||||
};
|
||||
@@ -37,6 +49,7 @@ const mockAudioInterface = {
|
||||
setTrackVolume: vi.fn(),
|
||||
setTrackMute: vi.fn(),
|
||||
setTrackSolo: vi.fn(),
|
||||
setMetronomeEnabled: vi.fn(),
|
||||
};
|
||||
|
||||
const configValues = new Map<string, unknown>([
|
||||
@@ -126,6 +139,11 @@ describe('projectStore piano roll state', () => {
|
||||
mockAudioInterface.cancelAudioRecording.mockResolvedValue(undefined);
|
||||
mockAudioInterface.getTransportPosition.mockReset();
|
||||
mockAudioInterface.getTransportPosition.mockReturnValue(8);
|
||||
mockAudioInterface.setMetronomeEnabled.mockReset();
|
||||
mockIsMetronomeEnabled = false;
|
||||
mockShowGlobalTracks = false;
|
||||
mockProject.setIsMetronomeEnabled.mockClear();
|
||||
mockProject.setShowGlobalTracks.mockClear();
|
||||
configValues.set('audio.input_device_id', 'default');
|
||||
});
|
||||
|
||||
@@ -195,6 +213,42 @@ describe('projectStore piano roll state', () => {
|
||||
expect(state.requestedSheetMusicViewEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('initializes persisted metronome and global-track visibility state from the project', async () => {
|
||||
mockIsMetronomeEnabled = true;
|
||||
mockShowGlobalTracks = true;
|
||||
|
||||
const { useProjectStore } = await import('./projectStore');
|
||||
|
||||
const state = useProjectStore.getState();
|
||||
expect(state.isMetronomeEnabled).toBe(true);
|
||||
expect(state.showGlobalTracks).toBe(true);
|
||||
});
|
||||
|
||||
it('persists metronome toggles to the project and audio interface', async () => {
|
||||
const { useProjectStore } = await import('./projectStore');
|
||||
|
||||
act(() => {
|
||||
useProjectStore.getState().toggleMetronome();
|
||||
});
|
||||
|
||||
const state = useProjectStore.getState();
|
||||
expect(mockProject.setIsMetronomeEnabled).toHaveBeenCalledWith(true);
|
||||
expect(mockAudioInterface.setMetronomeEnabled).toHaveBeenCalledWith(true);
|
||||
expect(state.isMetronomeEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('persists global-track visibility changes to the project', async () => {
|
||||
const { useProjectStore } = await import('./projectStore');
|
||||
|
||||
act(() => {
|
||||
useProjectStore.getState().setShowGlobalTracks(true);
|
||||
});
|
||||
|
||||
const state = useProjectStore.getState();
|
||||
expect(mockProject.setShowGlobalTracks).toHaveBeenCalledWith(true);
|
||||
expect(state.showGlobalTracks).toBe(true);
|
||||
});
|
||||
|
||||
it('tracks playback preparation around startPlaying success', async () => {
|
||||
let resolveStart: (() => void) | null = null;
|
||||
mockCore.startPlaying.mockImplementationOnce(() => new Promise<void>((resolve) => {
|
||||
|
||||
@@ -92,6 +92,7 @@ interface ProjectState {
|
||||
selectedMode: string;
|
||||
isLooping: boolean;
|
||||
isMetronomeEnabled: boolean;
|
||||
showGlobalTracks: boolean;
|
||||
loopingRange: [number, number]; // [startBar, endBar] - bar indices (0-based)
|
||||
playheadPosition: number; // in beats
|
||||
isPlaying: boolean;
|
||||
@@ -194,6 +195,7 @@ interface ProjectState {
|
||||
stopTransport: () => Promise<void>;
|
||||
toggleLoop: () => void;
|
||||
toggleMetronome: () => void;
|
||||
setShowGlobalTracks: (show: boolean) => void;
|
||||
setBpm: (bpm: number) => void;
|
||||
setMaxBars: (maxBars: number) => void;
|
||||
setBarWidthMultiplier: (multiplier: number) => void;
|
||||
@@ -432,7 +434,8 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
||||
keySignature: currentProject.getKeySignature(),
|
||||
selectedMode: currentProject.getSelectedMode(),
|
||||
isLooping: currentProject.getIsLooping(),
|
||||
isMetronomeEnabled: false,
|
||||
isMetronomeEnabled: currentProject.getIsMetronomeEnabled(),
|
||||
showGlobalTracks: currentProject.getShowGlobalTracks(),
|
||||
loopingRange: currentProject.getLoopingRange(),
|
||||
playheadPosition: KGCore.instance().getPlayheadPosition(),
|
||||
isPlaying: KGCore.instance().getIsPlaying(),
|
||||
@@ -870,6 +873,7 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
||||
|
||||
// Setup audio synths for all tracks
|
||||
const audioInterface = KGAudioInterface.instance();
|
||||
audioInterface.setMetronomeEnabled(projectToLoad.getIsMetronomeEnabled());
|
||||
|
||||
// Clear any existing synths/buses first
|
||||
tracks.forEach(track => {
|
||||
@@ -949,6 +953,8 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
||||
keySignature,
|
||||
selectedMode: projectToLoad.getSelectedMode(),
|
||||
isLooping: projectToLoad.getIsLooping(),
|
||||
isMetronomeEnabled: projectToLoad.getIsMetronomeEnabled(),
|
||||
showGlobalTracks: projectToLoad.getShowGlobalTracks(),
|
||||
loopingRange: projectToLoad.getLoopingRange(),
|
||||
activeTrackAutomationTrackId: null,
|
||||
activeTrackAutomationType: null,
|
||||
@@ -1455,6 +1461,8 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
||||
toggleMetronome: () => {
|
||||
const { isMetronomeEnabled, isPlaying, timeSignature } = get();
|
||||
const newValue = !isMetronomeEnabled;
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
project.setIsMetronomeEnabled(newValue);
|
||||
set({ isMetronomeEnabled: newValue });
|
||||
|
||||
const audio = KGAudioInterface.instance();
|
||||
@@ -1470,6 +1478,12 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
||||
}
|
||||
},
|
||||
|
||||
setShowGlobalTracks: (show: boolean) => {
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
project.setShowGlobalTracks(show);
|
||||
set({ showGlobalTracks: show });
|
||||
},
|
||||
|
||||
setBpm: (bpm: number) => {
|
||||
try {
|
||||
// Create and execute the change project property command
|
||||
@@ -1886,6 +1900,8 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
||||
bpm: project.getBpm(),
|
||||
keySignature: project.getKeySignature(),
|
||||
selectedMode: project.getSelectedMode(),
|
||||
isMetronomeEnabled: project.getIsMetronomeEnabled(),
|
||||
showGlobalTracks: project.getShowGlobalTracks(),
|
||||
currentTime: formatCurrentTime(project, core.getPlayheadPosition()),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user