- Next.js 14+ with App Router and TypeScript - Tailwind CSS and ShadCN UI styling - Zustand state management - Dexie.js for IndexedDB (local-first data) - Auth.js v5 for authentication - BMAD framework integration Co-Authored-By: Claude <noreply@anthropic.com>
112 lines
3.7 KiB
TypeScript
112 lines
3.7 KiB
TypeScript
|
|
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();
|
|
});
|
|
});
|
|
});
|