import { describe, it, expect, vi, beforeEach } from 'vitest'; import { SyncManager } from '../../../src/services/sync-manager'; // Define hoisted mocks to be accessible inside vi.mock const { mockAdd, mockWhere, mockUpdate, mockDelete, mockSortBy } = vi.hoisted(() => { const mockSortBy = vi.fn(); const mockEquals = vi.fn(() => ({ sortBy: mockSortBy })); const mockWhere = vi.fn(() => ({ equals: mockEquals })); return { mockAdd: vi.fn(), mockWhere, mockUpdate: vi.fn(), mockDelete: vi.fn(), mockSortBy }; }); // Mock Dexie vi.mock('../../../src/lib/db', () => ({ db: { syncQueue: { add: mockAdd, where: mockWhere, update: mockUpdate, delete: mockDelete, }, }, })); describe('SyncManager Service', () => { beforeEach(() => { vi.clearAllMocks(); // Default return for sortBy (mockPendingItems) mockSortBy.mockResolvedValue([]); }); describe('queueAction', () => { it('should add action to syncQueue with pending status', async () => { // GIVEN: A draft payload const payload = { draftData: { title: 'Test', content: 'Content' } as any }; mockAdd.mockResolvedValue(1); // WHEN: queueAction is called const id = await SyncManager.queueAction('saveDraft', payload); // THEN: It returns the new ID expect(id).toBe(1); // AND: It calls db.syncQueue.add with correct data expect(mockAdd).toHaveBeenCalledWith({ action: 'saveDraft', payload, status: 'pending', createdAt: expect.any(Number), retries: 0, }); }); }); describe('processQueue', () => { it('should process pending items in order', async () => { // GIVEN: Pending items in queue const items = [ { id: 1, action: 'saveDraft', payload: {}, status: 'pending', retries: 0 }, { id: 2, action: 'deleteDraft', payload: { draftId: 123 }, status: 'pending', retries: 0 }, ]; mockSortBy.mockResolvedValue(items); // WHEN: processQueue is called await SyncManager.processQueue(); // THEN: Items are processed // Verify update to 'processing' expect(mockUpdate).toHaveBeenCalledWith(1, { status: 'processing' }); expect(mockUpdate).toHaveBeenCalledWith(2, { status: 'processing' }); // Verify deletion after success (assuming success path) expect(mockDelete).toHaveBeenCalledWith(1); expect(mockDelete).toHaveBeenCalledWith(2); }); it('should handle execution errors and increment retries', async () => { // GIVEN: An item that will fail execution (invalid action) const item = { id: 1, action: 'invalidAction', // Will throw error in executeAction payload: {}, status: 'pending', retries: 0 }; mockSortBy.mockResolvedValue([item]); // WHEN: processQueue is called await SyncManager.processQueue(); // THEN: Item should be updated to processing expect(mockUpdate).toHaveBeenCalledWith(1, { status: 'processing' }); // AND: Should be updated with incremented retry count (not deleted) expect(mockUpdate).toHaveBeenCalledWith(1, { status: 'pending', retries: 1 }); expect(mockDelete).not.toHaveBeenCalled(); }); }); });