fix: initial global spinner randomly hanging until 10s timeout

This commit is contained in:
Xiaohan-Tian
2026-05-14 19:33:13 -07:00
parent c7a1630862
commit 6460d4ffa0
2 changed files with 55 additions and 12 deletions
+43 -4
View File
@@ -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(<GlobalLoadingOverlayContainer />);
expect(screen.getByText('Loading ... (2)')).toBeInTheDocument();
mockActiveLoadCount = 0;
act(() => {
loadingListener?.({ type: 'end', instrument: 'woodblock' });
});
expect(screen.queryByText(/Loading \.\.\./)).not.toBeInTheDocument();
});
});
+12 -8
View File
@@ -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<number>(() => KGToneBuffersPool.instance().getActiveLoadCount());
const [overdue, setOverdue] = useState<boolean>(false);
const timeoutRef = useRef<number | null>(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.'