fix: initial global spinner randomly hanging until 10s timeout
This commit is contained in:
+43
-4
@@ -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
@@ -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.'
|
||||
|
||||
Reference in New Issue
Block a user