/** * Task CAD-12a - Autosave-Service. * * Der Yjs-CRDT speichert bereits automatisch ueber WebSocket. * Dieser Service macht das SICHTBAR und ergaenzt: * - Save-Status-Text (Automatisch gespeichert / Offline / Speichert...) * - Periodischer Full-Save in SQLite (Redundanz) * - Reconnect-Handling (CRDT synced automatisch nach) */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { createAutosaveController, type AutosaveStatus, AUTOSAVE_INTERVAL_MS, } from '../src/services/autosaveService'; describe('CAD-12a: autosaveService', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it('AUTOSAVE_INTERVAL_MS ist 30 Sekunden', () => { expect(AUTOSAVE_INTERVAL_MS).toBe(30000); }); it('Controller: Status ist "idle" wenn kein CRDT verbunden', () => { const ctrl = createAutosaveController({ isConnected: () => false, hasUnsavedChanges: () => false, performFullSave: vi.fn(), onStatusChange: vi.fn(), }); const status: AutosaveStatus = ctrl.getStatus(); expect(status.state).toBe('offline'); ctrl.destroy(); }); it('Controller: Status ist "saved" wenn CRDT verbunden und keine ungespeicherten Aenderungen', () => { const ctrl = createAutosaveController({ isConnected: () => true, hasUnsavedChanges: () => false, performFullSave: vi.fn(), onStatusChange: vi.fn(), }); expect(ctrl.getStatus().state).toBe('saved'); ctrl.destroy(); }); it('Controller: Status wechselt zu "saving" wenn Aenderungen vorhanden und Timer feuert', () => { const fullSave = vi.fn().mockResolvedValue(undefined); const onStatus = vi.fn(); const ctrl = createAutosaveController({ isConnected: () => true, hasUnsavedChanges: () => true, performFullSave: fullSave, onStatusChange: onStatus, }); vi.advanceTimersByTime(31000); expect(fullSave).toHaveBeenCalledTimes(1); ctrl.destroy(); }); it('Controller: fuehrt KEINEN Full-Save aus wenn keine Aenderungen', () => { const fullSave = vi.fn(); const ctrl = createAutosaveController({ isConnected: () => true, hasUnsavedChanges: () => false, performFullSave: fullSave, onStatusChange: vi.fn(), }); vi.advanceTimersByTime(31000); expect(fullSave).not.toHaveBeenCalled(); ctrl.destroy(); }); it('Controller: fuehrt KEINEN Full-Save aus wenn CRDT offline', () => { const fullSave = vi.fn(); const ctrl = createAutosaveController({ isConnected: () => false, hasUnsavedChanges: () => true, performFullSave: fullSave, onStatusChange: vi.fn(), }); vi.advanceTimersByTime(31000); expect(fullSave).not.toHaveBeenCalled(); ctrl.destroy(); }); it('Controller: Status-Meldung an onStatusChange bei Aenderung', () => { const onStatus = vi.fn(); const ctrl = createAutosaveController({ isConnected: () => true, hasUnsavedChanges: () => false, performFullSave: vi.fn(), onStatusChange: onStatus, }); expect(onStatus).toHaveBeenCalledWith({ state: 'saved', lastSaved: null }); ctrl.destroy(); }); it('Controller: markChanges triggert saving beim naechsten Timer-Tick', () => { const fullSave = vi.fn().mockResolvedValue(undefined); const ctrl = createAutosaveController({ isConnected: () => true, hasUnsavedChanges: () => false, performFullSave: fullSave, onStatusChange: vi.fn(), }); ctrl.markChanges(); vi.advanceTimersByTime(31000); expect(fullSave).toHaveBeenCalled(); ctrl.destroy(); }); it('Controller: destroy stoppt den Timer', () => { const fullSave = vi.fn(); const ctrl = createAutosaveController({ isConnected: () => true, hasUnsavedChanges: () => true, performFullSave: fullSave, onStatusChange: vi.fn(), }); ctrl.destroy(); vi.advanceTimersByTime(60000); expect(fullSave).not.toHaveBeenCalled(); }); });