diff --git a/frontend/src/lib/components/TranscriptionEditView.svelte b/frontend/src/lib/components/TranscriptionEditView.svelte index 85becbc2..c156f6c0 100644 --- a/frontend/src/lib/components/TranscriptionEditView.svelte +++ b/frontend/src/lib/components/TranscriptionEditView.svelte @@ -73,7 +73,7 @@ $effect(() => { $effect(() => { function onBeforeUnload() { - autoSave.flushViaBeacon(); + autoSave.flushOnUnload(); } window.addEventListener('beforeunload', onBeforeUnload); return () => { diff --git a/frontend/src/lib/hooks/__tests__/useBlockAutoSave.svelte.test.ts b/frontend/src/lib/hooks/__tests__/useBlockAutoSave.svelte.test.ts index 148df5de..739fb432 100644 --- a/frontend/src/lib/hooks/__tests__/useBlockAutoSave.svelte.test.ts +++ b/frontend/src/lib/hooks/__tests__/useBlockAutoSave.svelte.test.ts @@ -83,7 +83,7 @@ describe('createBlockAutoSave', () => { }); }); -describe('flushViaBeacon', () => { +describe('flushOnUnload', () => { let mockFetch: Mock; beforeEach(() => { @@ -103,7 +103,7 @@ describe('flushViaBeacon', () => { const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' }); as.handleTextChange('block-1', 'hello'); as.handleTextChange('block-2', 'world'); - as.flushViaBeacon(); + as.flushOnUnload(); expect(mockFetch).toHaveBeenCalledTimes(2); expect(mockFetch).toHaveBeenCalledWith( @@ -128,14 +128,14 @@ describe('flushViaBeacon', () => { const sendBeaconSpy = vi.spyOn(navigator, 'sendBeacon').mockReturnValue(true); const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' }); as.handleTextChange('block-1', 'text'); - as.flushViaBeacon(); + as.flushOnUnload(); expect(sendBeaconSpy).not.toHaveBeenCalled(); }); it('does nothing when there are no pending edits', () => { const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' }); - as.flushViaBeacon(); + as.flushOnUnload(); expect(mockFetch).not.toHaveBeenCalled(); }); @@ -143,9 +143,21 @@ describe('flushViaBeacon', () => { it('cancels the debounce timer so saveFn is not also called', async () => { const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' }); as.handleTextChange('block-1', 'text'); - as.flushViaBeacon(); + as.flushOnUnload(); await vi.advanceTimersByTimeAsync(2000); expect(mockSaveFn).not.toHaveBeenCalled(); }); + + it('does not send fetch if debounce already fired and pendingTexts is empty', async () => { + const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' }); + as.handleTextChange('block-1', 'text'); + await vi.advanceTimersByTimeAsync(1500); + // debounce has fired; pendingTexts should be empty now + mockFetch.mockClear(); + + as.flushOnUnload(); + + expect(mockFetch).not.toHaveBeenCalled(); + }); }); diff --git a/frontend/src/lib/hooks/useBlockAutoSave.svelte.ts b/frontend/src/lib/hooks/useBlockAutoSave.svelte.ts index c90b669f..07dc5692 100644 --- a/frontend/src/lib/hooks/useBlockAutoSave.svelte.ts +++ b/frontend/src/lib/hooks/useBlockAutoSave.svelte.ts @@ -94,10 +94,10 @@ export function createBlockAutoSave({ saveFn, documentId }: Options) { saveStates.delete(blockId); } - function flushViaBeacon(): void { + function flushOnUnload(): void { for (const [blockId, text] of pendingTexts) { clearDebounce(blockId); - fetch(`/api/documents/${documentId}/transcription-blocks/${blockId}`, { + void fetch(`/api/documents/${documentId}/transcription-blocks/${blockId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }), @@ -124,7 +124,7 @@ export function createBlockAutoSave({ saveFn, documentId }: Options) { handleBlur, handleRetry, clearBlock, - flushViaBeacon, + flushOnUnload, destroy }; }