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';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
let mockState = { isPreparingPlayback: false };
|
let mockState = { isPreparingPlayback: false };
|
||||||
|
let mockActiveLoadCount = 0;
|
||||||
|
let loadingListener: ((evt: { type: 'start' | 'end'; instrument: string }) => void) | null = null;
|
||||||
|
|
||||||
vi.mock('./stores/projectStore', () => ({
|
vi.mock('./stores/projectStore', () => ({
|
||||||
useProjectStore: (selector?: (state: typeof mockState) => unknown) => (
|
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/KGOnePanel', () => ({ default: () => null }));
|
||||||
vi.mock('./components/EventListPanel', () => ({ default: () => null }));
|
vi.mock('./components/EventListPanel', () => ({ default: () => null }));
|
||||||
vi.mock('./components/settings', () => ({ SettingsPanel: () => null }));
|
vi.mock('./components/settings', () => ({ SettingsPanel: () => null }));
|
||||||
|
vi.mock('./util/dialogUtil', () => ({
|
||||||
|
showAlert: vi.fn().mockResolvedValue(undefined),
|
||||||
|
}));
|
||||||
vi.mock('./core/audio-interface/KGToneBuffersPool', () => ({
|
vi.mock('./core/audio-interface/KGToneBuffersPool', () => ({
|
||||||
KGToneBuffersPool: {
|
KGToneBuffersPool: {
|
||||||
instance: () => ({
|
instance: () => ({
|
||||||
getActiveLoadCount: () => 0,
|
getActiveLoadCount: () => mockActiveLoadCount,
|
||||||
addLoadingListener: () => undefined,
|
addLoadingListener: (listener: (evt: { type: 'start' | 'end'; instrument: string }) => void) => {
|
||||||
removeLoadingListener: () => undefined,
|
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', () => {
|
describe('PlaybackPreparationOverlayContainer', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
mockState = { isPreparingPlayback: false };
|
mockState = { isPreparingPlayback: false };
|
||||||
|
mockActiveLoadCount = 0;
|
||||||
|
loadingListener = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -112,3 +125,29 @@ describe('PlaybackPreparationOverlayContainer', () => {
|
|||||||
expect(screen.queryByText('Preparing playback...')).not.toBeInTheDocument();
|
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;
|
export default App;
|
||||||
|
|
||||||
// Local component to subscribe to pool events and manage a counter
|
// 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 [loadingCount, setLoadingCount] = useState<number>(() => KGToneBuffersPool.instance().getActiveLoadCount());
|
||||||
const [overdue, setOverdue] = useState<boolean>(false);
|
const [overdue, setOverdue] = useState<boolean>(false);
|
||||||
const timeoutRef = useRef<number | null>(null);
|
const timeoutRef = useRef<number | null>(null);
|
||||||
|
|
||||||
useEffectReact(() => {
|
useEffectReact(() => {
|
||||||
const pool = KGToneBuffersPool.instance();
|
const pool = KGToneBuffersPool.instance();
|
||||||
const listener = (evt: { type: 'start' | 'end'; instrument: string }) => {
|
const syncLoadingCount = () => {
|
||||||
setLoadingCount(prev => {
|
setLoadingCount(pool.getActiveLoadCount());
|
||||||
if (evt.type === 'start') return prev + 1;
|
|
||||||
return Math.max(0, prev - 1);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
const listener = (_evt: { type: 'start' | 'end'; instrument: string }) => {
|
||||||
|
syncLoadingCount();
|
||||||
|
};
|
||||||
|
syncLoadingCount();
|
||||||
pool.addLoadingListener(listener);
|
pool.addLoadingListener(listener);
|
||||||
return () => {
|
return () => {
|
||||||
pool.removeLoadingListener(listener);
|
pool.removeLoadingListener(listener);
|
||||||
@@ -219,8 +220,11 @@ const GlobalLoadingOverlayContainer: React.FC = () => {
|
|||||||
// When loading starts, start a 30s timer if not already overdue/timed
|
// When loading starts, start a 30s timer if not already overdue/timed
|
||||||
if (loadingCount > 0 && !overdue && timeoutRef.current === null) {
|
if (loadingCount > 0 && !overdue && timeoutRef.current === null) {
|
||||||
timeoutRef.current = window.setTimeout(async () => {
|
timeoutRef.current = window.setTimeout(async () => {
|
||||||
// Only trigger if still loading
|
const activeLoadCount = KGToneBuffersPool.instance().getActiveLoadCount();
|
||||||
if (loadingCount > 0) {
|
setLoadingCount(activeLoadCount);
|
||||||
|
|
||||||
|
// Only trigger if still loading according to the source of truth
|
||||||
|
if (activeLoadCount > 0) {
|
||||||
setOverdue(true);
|
setOverdue(true);
|
||||||
await showAlert(
|
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.'
|
'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