diff --git a/src/App.test.tsx b/src/App.test.tsx
index 4bd3d52..d25e1ec 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -2,6 +2,8 @@ import { act, render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
let mockState = { isPreparingPlayback: false };
+let mockActiveLoadCount = 0;
+let loadingListener: ((evt: { type: 'start' | 'end'; instrument: string }) => void) | null = null;
vi.mock('./stores/projectStore', () => ({
useProjectStore: (selector?: (state: typeof mockState) => unknown) => (
@@ -21,12 +23,21 @@ vi.mock('./components/ChatBox', () => ({ default: () => null }));
vi.mock('./components/KGOnePanel', () => ({ default: () => null }));
vi.mock('./components/EventListPanel', () => ({ default: () => null }));
vi.mock('./components/settings', () => ({ SettingsPanel: () => null }));
+vi.mock('./util/dialogUtil', () => ({
+ showAlert: vi.fn().mockResolvedValue(undefined),
+}));
vi.mock('./core/audio-interface/KGToneBuffersPool', () => ({
KGToneBuffersPool: {
instance: () => ({
- getActiveLoadCount: () => 0,
- addLoadingListener: () => undefined,
- removeLoadingListener: () => undefined,
+ getActiveLoadCount: () => mockActiveLoadCount,
+ addLoadingListener: (listener: (evt: { type: 'start' | 'end'; instrument: string }) => void) => {
+ loadingListener = listener;
+ },
+ removeLoadingListener: (listener: (evt: { type: 'start' | 'end'; instrument: string }) => void) => {
+ if (loadingListener === listener) {
+ loadingListener = null;
+ }
+ },
}),
},
}));
@@ -47,12 +58,14 @@ vi.mock('./core/KGCore', () => ({
},
}));
-import { PlaybackPreparationOverlayContainer } from './App';
+import { GlobalLoadingOverlayContainer, PlaybackPreparationOverlayContainer } from './App';
describe('PlaybackPreparationOverlayContainer', () => {
beforeEach(() => {
vi.useFakeTimers();
mockState = { isPreparingPlayback: false };
+ mockActiveLoadCount = 0;
+ loadingListener = null;
});
afterEach(() => {
@@ -112,3 +125,29 @@ describe('PlaybackPreparationOverlayContainer', () => {
expect(screen.queryByText('Preparing playback...')).not.toBeInTheDocument();
});
});
+
+describe('GlobalLoadingOverlayContainer', () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ mockActiveLoadCount = 0;
+ loadingListener = null;
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it('syncs to the pool active count instead of relying on incremental listener math', () => {
+ mockActiveLoadCount = 2;
+ render();
+
+ expect(screen.getByText('Loading ... (2)')).toBeInTheDocument();
+
+ mockActiveLoadCount = 0;
+ act(() => {
+ loadingListener?.({ type: 'end', instrument: 'woodblock' });
+ });
+
+ expect(screen.queryByText(/Loading \.\.\./)).not.toBeInTheDocument();
+ });
+});
diff --git a/src/App.tsx b/src/App.tsx
index c31919b..a4e4c9e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -195,19 +195,20 @@ function App() {
export default App;
// Local component to subscribe to pool events and manage a counter
-const GlobalLoadingOverlayContainer: React.FC = () => {
+export const GlobalLoadingOverlayContainer: React.FC = () => {
const [loadingCount, setLoadingCount] = useState(() => KGToneBuffersPool.instance().getActiveLoadCount());
const [overdue, setOverdue] = useState(false);
const timeoutRef = useRef(null);
useEffectReact(() => {
const pool = KGToneBuffersPool.instance();
- const listener = (evt: { type: 'start' | 'end'; instrument: string }) => {
- setLoadingCount(prev => {
- if (evt.type === 'start') return prev + 1;
- return Math.max(0, prev - 1);
- });
+ const syncLoadingCount = () => {
+ setLoadingCount(pool.getActiveLoadCount());
};
+ const listener = (_evt: { type: 'start' | 'end'; instrument: string }) => {
+ syncLoadingCount();
+ };
+ syncLoadingCount();
pool.addLoadingListener(listener);
return () => {
pool.removeLoadingListener(listener);
@@ -219,8 +220,11 @@ const GlobalLoadingOverlayContainer: React.FC = () => {
// When loading starts, start a 30s timer if not already overdue/timed
if (loadingCount > 0 && !overdue && timeoutRef.current === null) {
timeoutRef.current = window.setTimeout(async () => {
- // Only trigger if still loading
- if (loadingCount > 0) {
+ const activeLoadCount = KGToneBuffersPool.instance().getActiveLoadCount();
+ setLoadingCount(activeLoadCount);
+
+ // Only trigger if still loading according to the source of truth
+ if (activeLoadCount > 0) {
setOverdue(true);
await showAlert(
'Loading resources is taking longer than expected and may have partially failed. If you notice any playback issues, please refresh the page to retry downloading the audio files.'