fix: close Settings after successful saved project load

This commit is contained in:
Xiaohan-Tian
2026-07-02 14:18:33 -07:00
parent 86ec455dfe
commit 09dd717262
2 changed files with 100 additions and 4 deletions
+86 -1
View File
@@ -4,6 +4,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import Toolbar from './Toolbar'; import Toolbar from './Toolbar';
import { createDefaultGlobalTracks } from '../core/global-track'; import { createDefaultGlobalTracks } from '../core/global-track';
import { KGKeySignatureRegion } from '../core/region/KGKeySignatureRegion'; import { KGKeySignatureRegion } from '../core/region/KGKeySignatureRegion';
import { KGProject } from '../core/KGProject';
import { KGProjectStorage } from '../core/io/KGProjectStorage';
import { showAlert, showConfirm } from '../util/dialogUtil';
const executeCommandMock = vi.fn(); const executeCommandMock = vi.fn();
const storeState = { const storeState = {
@@ -41,6 +44,7 @@ const storeState = {
toggleKGOnePanel: vi.fn(), toggleKGOnePanel: vi.fn(),
toggleEventListPanel: vi.fn(), toggleEventListPanel: vi.fn(),
activateSidePanel: vi.fn(), activateSidePanel: vi.fn(),
setShowSettings: vi.fn(),
showKGOnePanel: true, showKGOnePanel: true,
showEventListPanel: false, showEventListPanel: false,
showChatBox: true, showChatBox: true,
@@ -61,6 +65,7 @@ const storeState = {
refreshProjectState: vi.fn(), refreshProjectState: vi.fn(),
requestMainContentScroll: vi.fn(), requestMainContentScroll: vi.fn(),
requestPianoRollScroll: vi.fn(), requestPianoRollScroll: vi.fn(),
loadProject: vi.fn(),
tracks: [] as unknown[], tracks: [] as unknown[],
globalTracks: createDefaultGlobalTracks(), globalTracks: createDefaultGlobalTracks(),
}; };
@@ -141,7 +146,24 @@ vi.mock('../util/midiUtil', () => ({
vi.mock('../core/audio-interface/KGOfflineRenderer', () => ({ KGOfflineRenderer: { instance: vi.fn(() => ({})) } })); vi.mock('../core/audio-interface/KGOfflineRenderer', () => ({ KGOfflineRenderer: { instance: vi.fn(() => ({})) } }));
vi.mock('./common/FileImportModal', () => ({ default: () => null })); vi.mock('./common/FileImportModal', () => ({ default: () => null }));
vi.mock('./common/LoadingOverlay', () => ({ default: () => null })); vi.mock('./common/LoadingOverlay', () => ({ default: () => null }));
vi.mock('./common/OpenProjectModal', () => ({ default: () => null })); vi.mock('./common/OpenProjectModal', () => ({
default: ({
onConfirmOpenProject,
onOpenProject,
}: {
onConfirmOpenProject: (projectName: string) => Promise<boolean>;
onOpenProject: (projectName: string) => Promise<void>;
}) => (
<div>
<button type="button" onClick={() => void onConfirmOpenProject('Loaded Project')}>
confirm-open-project
</button>
<button type="button" onClick={() => void onOpenProject('Loaded Project')}>
open-loaded-project
</button>
</div>
),
}));
vi.mock('../util/chatUtil', () => ({ clearChatHistoryAndUI: vi.fn() })); vi.mock('../util/chatUtil', () => ({ clearChatHistoryAndUI: vi.fn() }));
vi.mock('./common/icons/PianoIcon', () => ({ default: () => <span>piano</span> })); vi.mock('./common/icons/PianoIcon', () => ({ default: () => <span>piano</span> }));
vi.mock('./common/icons/MetronomeIcon', () => ({ default: () => <span>metro</span> })); vi.mock('./common/icons/MetronomeIcon', () => ({ default: () => <span>metro</span> }));
@@ -205,7 +227,11 @@ describe('Toolbar settings side-panel behavior', () => {
storeState.toggleKGOnePanel.mockClear(); storeState.toggleKGOnePanel.mockClear();
storeState.toggleEventListPanel.mockClear(); storeState.toggleEventListPanel.mockClear();
storeState.activateSidePanel.mockClear(); storeState.activateSidePanel.mockClear();
storeState.setShowSettings.mockClear();
storeState.setStatus.mockClear(); storeState.setStatus.mockClear();
storeState.cleanupProjectState.mockClear();
storeState.loadProject.mockReset();
storeState.loadProject.mockResolvedValue(undefined);
storeState.showSettings = true; storeState.showSettings = true;
storeState.showChatBox = true; storeState.showChatBox = true;
storeState.showKGOnePanel = true; storeState.showKGOnePanel = true;
@@ -216,6 +242,9 @@ describe('Toolbar settings side-panel behavior', () => {
storeState.setKeySignature.mockClear(); storeState.setKeySignature.mockClear();
storeState.refreshProjectState.mockClear(); storeState.refreshProjectState.mockClear();
executeCommandMock.mockClear(); executeCommandMock.mockClear();
vi.mocked(KGProjectStorage.getInstance).mockReset();
vi.mocked(showConfirm).mockReset();
vi.mocked(showAlert).mockReset();
}); });
it('suppresses active styling for side-panel buttons while Settings is visible', () => { it('suppresses active styling for side-panel buttons while Settings is visible', () => {
@@ -301,4 +330,60 @@ describe('Toolbar settings side-panel behavior', () => {
expect(storeState.refreshProjectState).toHaveBeenCalled(); expect(storeState.refreshProjectState).toHaveBeenCalled();
expect(storeState.setStatus).toHaveBeenCalledWith('Key signature changed to E minor'); expect(storeState.setStatus).toHaveBeenCalledWith('Key signature changed to E minor');
}); });
it('closes Settings after a saved project finishes loading successfully', async () => {
const storageMock = {
load: vi.fn().mockResolvedValue(new KGProject()),
cleanupOrphanMedia: vi.fn().mockResolvedValue(undefined),
};
vi.mocked(KGProjectStorage.getInstance).mockReturnValue(storageMock as unknown as ReturnType<typeof KGProjectStorage.getInstance>);
render(<Toolbar />);
fireEvent.click(screen.getByTitle('Load'));
fireEvent.click(screen.getByRole('button', { name: 'open-loaded-project' }));
await waitFor(() => {
expect(storageMock.load).toHaveBeenCalledWith('Loaded Project');
});
await waitFor(() => {
expect(storeState.loadProject).toHaveBeenCalled();
});
await waitFor(() => {
expect(storeState.setShowSettings).toHaveBeenCalledWith(false);
});
});
it('keeps Settings open when the user cancels project loading', async () => {
vi.mocked(showConfirm).mockResolvedValue(false);
render(<Toolbar />);
fireEvent.click(screen.getByTitle('Load'));
fireEvent.click(screen.getByRole('button', { name: 'confirm-open-project' }));
await waitFor(() => {
expect(showConfirm).toHaveBeenCalled();
});
expect(storeState.setShowSettings).not.toHaveBeenCalled();
});
it('keeps Settings open when project loading fails', async () => {
const storageMock = {
load: vi.fn().mockResolvedValue(new KGProject()),
cleanupOrphanMedia: vi.fn().mockResolvedValue(undefined),
};
vi.mocked(KGProjectStorage.getInstance).mockReturnValue(storageMock as unknown as ReturnType<typeof KGProjectStorage.getInstance>);
storeState.loadProject.mockRejectedValueOnce(new Error('load failed'));
render(<Toolbar />);
fireEvent.click(screen.getByTitle('Load'));
fireEvent.click(screen.getByRole('button', { name: 'open-loaded-project' }));
await waitFor(() => {
expect(showAlert).toHaveBeenCalled();
});
expect(storeState.setShowSettings).not.toHaveBeenCalled();
});
}); });
+14 -3
View File
@@ -55,7 +55,7 @@ const Toolbar: React.FC = () => {
isLooping, toggleLoop, isLooping, toggleLoop,
globalTracks, globalTracks,
canUndo, canRedo, undoDescription, redoDescription, undo, redo, canUndo, canRedo, undoDescription, redoDescription, undo, redo,
toggleChatBox, toggleSettings, toggleKGOnePanel, toggleEventListPanel, activateSidePanel, showKGOnePanel, showEventListPanel, showChatBox, showSettings, cleanupProjectState, toggleMetronome, isMetronomeEnabled, toggleChatBox, toggleSettings, toggleKGOnePanel, toggleEventListPanel, activateSidePanel, showKGOnePanel, showEventListPanel, showChatBox, showSettings, setShowSettings, cleanupProjectState, toggleMetronome, isMetronomeEnabled,
isRecording, startRecording, stopRecording, isRecording, startRecording, stopRecording,
// Piano roll state/actions // Piano roll state/actions
showPianoRoll, setShowPianoRoll, activeRegionId, setActiveRegionId, showPianoRoll, setShowPianoRoll, activeRegionId, setActiveRegionId,
@@ -220,7 +220,7 @@ const Toolbar: React.FC = () => {
}; };
// Common project loading logic extracted for reuse // Common project loading logic extracted for reuse
const loadProjectFromData = async (project: KGProject, sourceDescription: string, savedName?: string) => { const loadProjectFromData = async (project: KGProject, sourceDescription: string, savedName?: string): Promise<boolean> => {
try { try {
// Clean up UI state first // Clean up UI state first
cleanupProjectState(); cleanupProjectState();
@@ -245,10 +245,13 @@ const Toolbar: React.FC = () => {
console.log(`project loaded successfully from ${sourceDescription}`); console.log(`project loaded successfully from ${sourceDescription}`);
} }
return true;
} catch (error) { } catch (error) {
console.error(`Error loading project from ${sourceDescription}:`, error); console.error(`Error loading project from ${sourceDescription}:`, error);
setStatus(t('toolbar.status.loadFailed', { error: String(error) })); setStatus(t('toolbar.status.loadFailed', { error: String(error) }));
await showAlert(t('toolbar.load.error', { error: String(error) })); await showAlert(t('toolbar.load.error', { error: String(error) }));
return false;
} }
}; };
@@ -299,7 +302,15 @@ const Toolbar: React.FC = () => {
return; return;
} }
await loadProjectFromData(loadedProject, `Project "${projectNameToLoad}"`, projectNameToLoad); const didLoadProject = await loadProjectFromData(
loadedProject,
`Project "${projectNameToLoad}"`,
projectNameToLoad
);
if (didLoadProject && showSettings) {
setShowSettings(false);
}
} catch (error) { } catch (error) {
console.error("Error loading project:", error); console.error("Error loading project:", error);
await showAlert(t('toolbar.load.error', { error: String(error) })); await showAlert(t('toolbar.load.error', { error: String(error) }));