import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; const mockSaveFn = vi.fn<(blockId: string, text: string) => Promise>(); const { createBlockAutoSave } = await import('../useBlockAutoSave.svelte'); describe('createBlockAutoSave', () => { beforeEach(() => { vi.useFakeTimers(); mockSaveFn.mockClear(); mockSaveFn.mockResolvedValue(undefined); }); afterEach(() => { vi.useRealTimers(); }); it('getSaveState returns idle initially', () => { const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' }); expect(as.getSaveState('block-1')).toBe('idle'); }); it('debounce coalesces multiple changes — saves once after 1500ms', async () => { const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' }); as.handleTextChange('block-1', 'text 1'); as.handleTextChange('block-1', 'text 2'); as.handleTextChange('block-1', 'text 3'); await vi.advanceTimersByTimeAsync(1500); expect(mockSaveFn).toHaveBeenCalledTimes(1); expect(mockSaveFn).toHaveBeenCalledWith('block-1', 'text 3'); }); it('handles concurrent blocks independently', async () => { const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' }); as.handleTextChange('block-1', 'hello'); as.handleTextChange('block-2', 'world'); await vi.advanceTimersByTimeAsync(1500); expect(mockSaveFn).toHaveBeenCalledTimes(2); }); it('sets save state to saving then saved on success', async () => { const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' }); as.handleTextChange('block-1', 'text'); vi.advanceTimersByTime(1500); expect(as.getSaveState('block-1')).toBe('saving'); await Promise.resolve(); expect(as.getSaveState('block-1')).toBe('saved'); }); it('sets save state to error on save failure', async () => { mockSaveFn.mockRejectedValue(new Error('save failed')); const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' }); as.handleTextChange('block-1', 'text'); await vi.advanceTimersByTimeAsync(1500); expect(as.getSaveState('block-1')).toBe('error'); }); it('handleRetry saves with provided current text', async () => { mockSaveFn.mockRejectedValueOnce(new Error('first fails')); mockSaveFn.mockResolvedValueOnce(undefined); const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' }); as.handleTextChange('block-1', 'original'); await vi.advanceTimersByTimeAsync(1500); expect(as.getSaveState('block-1')).toBe('error'); await as.handleRetry('block-1', 'original'); expect(mockSaveFn).toHaveBeenCalledTimes(2); expect(as.getSaveState('block-1')).toBe('saved'); }); it('clearBlock removes all state for a block', () => { const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' }); as.handleTextChange('block-1', 'text'); as.clearBlock('block-1'); expect(as.getSaveState('block-1')).toBe('idle'); }); it('destroy clears all pending timers so no save occurs', async () => { const as = createBlockAutoSave({ saveFn: mockSaveFn, documentId: 'doc-1' }); as.handleTextChange('block-1', 'text'); as.destroy(); await vi.advanceTimersByTimeAsync(2000); expect(mockSaveFn).not.toHaveBeenCalled(); }); });