/** * Optimistic UI Tests * * Tests for version-based optimistic concurrency control and conflict resolution. */ import { OptimisticStateManager } from '../../../../resources/js/modules/livecomponent/OptimisticStateManager.js'; describe('OptimisticStateManager', () => { let manager; beforeEach(() => { manager = new OptimisticStateManager(); }); afterEach(() => { manager.clear(); }); describe('applyOptimisticUpdate', () => { it('creates snapshot before applying update', () => { const componentId = 'test-component:1'; const currentState = { data: { count: 5 }, version: 1 }; const optimisticState = manager.applyOptimisticUpdate( componentId, currentState, (state) => ({ ...state, data: { ...state.data, count: state.data.count + 1 } }), { action: 'increment' } ); // Should create snapshot const snapshot = manager.getSnapshot(componentId); expect(snapshot).not.toBeNull(); expect(snapshot.version).toBe(1); expect(snapshot.data.count).toBe(5); }); it('increments version optimistically', () => { const componentId = 'test-component:1'; const currentState = { data: { count: 5 }, version: 1 }; const optimisticState = manager.applyOptimisticUpdate( componentId, currentState, (state) => ({ ...state, data: { ...state.data, count: state.data.count + 1 } }) ); expect(optimisticState.version).toBe(2); expect(optimisticState.data.count).toBe(6); }); it('tracks pending operation', () => { const componentId = 'test-component:1'; const currentState = { data: { count: 5 }, version: 1 }; manager.applyOptimisticUpdate( componentId, currentState, (state) => ({ ...state }), { action: 'testAction', params: { value: 1 } } ); const pendingOps = manager.getPendingOperations(componentId); expect(pendingOps).toHaveLength(1); expect(pendingOps[0].metadata.action).toBe('testAction'); expect(pendingOps[0].status).toBe('pending'); }); it('generates unique operation IDs', () => { const componentId = 'test-component:1'; const currentState = { data: {}, version: 1 }; manager.applyOptimisticUpdate(componentId, currentState, (s) => s); const op1 = manager.getPendingOperations(componentId)[0]; manager.clear(); manager.applyOptimisticUpdate(componentId, currentState, (s) => s); const op2 = manager.getPendingOperations(componentId)[0]; expect(op1.id).not.toBe(op2.id); }); }); describe('confirmOperation', () => { it('marks operation as confirmed', () => { const componentId = 'test-component:1'; const currentState = { data: {}, version: 1 }; manager.applyOptimisticUpdate(componentId, currentState, (s) => s); const operationId = manager.getPendingOperations(componentId)[0].id; const serverState = { data: {}, version: 2 }; manager.confirmOperation(componentId, operationId, serverState); // Operation should be removed from pending expect(manager.getPendingOperations(componentId)).toHaveLength(0); }); it('clears snapshot when no more pending operations', () => { const componentId = 'test-component:1'; const currentState = { data: {}, version: 1 }; manager.applyOptimisticUpdate(componentId, currentState, (s) => s); const operationId = manager.getPendingOperations(componentId)[0].id; const serverState = { data: {}, version: 2 }; manager.confirmOperation(componentId, operationId, serverState); // Snapshot should be cleared expect(manager.getSnapshot(componentId)).toBeNull(); }); it('keeps snapshot when pending operations remain', () => { const componentId = 'test-component:1'; const currentState = { data: {}, version: 1 }; // Create two pending operations manager.applyOptimisticUpdate(componentId, currentState, (s) => s); manager.applyOptimisticUpdate(componentId, { data: {}, version: 2 }, (s) => s); const operations = manager.getPendingOperations(componentId); const operationId = operations[0].id; // Confirm first operation manager.confirmOperation(componentId, operationId, { data: {}, version: 2 }); // Snapshot should still exist (second operation pending) expect(manager.getSnapshot(componentId)).not.toBeNull(); expect(manager.getPendingOperations(componentId)).toHaveLength(1); }); }); describe('handleConflict', () => { it('marks operation as failed', () => { const componentId = 'test-component:1'; const currentState = { data: {}, version: 1 }; manager.applyOptimisticUpdate(componentId, currentState, (s) => s); const operationId = manager.getPendingOperations(componentId)[0].id; const serverState = { data: {}, version: 3 }; // Conflict: expected 2, got 3 manager.handleConflict(componentId, operationId, serverState, { expectedVersion: 2, actualVersion: 3 }); // Operation should be removed expect(manager.getPendingOperations(componentId)).toHaveLength(0); }); it('returns server state and notification', () => { const componentId = 'test-component:1'; const currentState = { data: { count: 5 }, version: 1 }; manager.applyOptimisticUpdate( componentId, currentState, (s) => ({ ...s, data: { count: 6 } }), { action: 'increment' } ); const operationId = manager.getPendingOperations(componentId)[0].id; const serverState = { data: { count: 10 }, version: 3 }; const result = manager.handleConflict(componentId, operationId, serverState); expect(result.state).toEqual(serverState); expect(result.notification).toBeDefined(); expect(result.notification.type).toBe('conflict'); expect(result.notification.canRetry).toBe(true); }); it('clears all pending operations on conflict', () => { const componentId = 'test-component:1'; const currentState = { data: {}, version: 1 }; // Create multiple pending operations manager.applyOptimisticUpdate(componentId, currentState, (s) => s); manager.applyOptimisticUpdate(componentId, { data: {}, version: 2 }, (s) => s); const operationId = manager.getPendingOperations(componentId)[0].id; manager.handleConflict(componentId, operationId, { data: {}, version: 5 }); // All operations should be cleared expect(manager.getPendingOperations(componentId)).toHaveLength(0); }); it('clears snapshot after conflict', () => { const componentId = 'test-component:1'; const currentState = { data: {}, version: 1 }; manager.applyOptimisticUpdate(componentId, currentState, (s) => s); const operationId = manager.getPendingOperations(componentId)[0].id; manager.handleConflict(componentId, operationId, { data: {}, version: 3 }); expect(manager.getSnapshot(componentId)).toBeNull(); }); it('calls conflict handler if registered', () => { const componentId = 'test-component:1'; const currentState = { data: {}, version: 1 }; const conflictHandler = jest.fn(); manager.registerConflictHandler(componentId, conflictHandler); manager.applyOptimisticUpdate(componentId, currentState, (s) => s); const operationId = manager.getPendingOperations(componentId)[0].id; const serverState = { data: {}, version: 3 }; manager.handleConflict(componentId, operationId, serverState); expect(conflictHandler).toHaveBeenCalled(); const callArgs = conflictHandler.mock.calls[0][0]; expect(callArgs.serverState).toEqual(serverState); }); }); describe('retryOperation', () => { it('executes retry callback with operation metadata', async () => { const componentId = 'test-component:1'; const retryCallback = jest.fn().mockResolvedValue({ success: true }); const operation = { id: 'op-1', metadata: { action: 'testAction', params: { value: 1 } } }; const result = await manager.retryOperation(componentId, operation, retryCallback); expect(retryCallback).toHaveBeenCalledWith(operation.metadata); expect(result.success).toBe(true); }); it('propagates retry callback errors', async () => { const componentId = 'test-component:1'; const error = new Error('Retry failed'); const retryCallback = jest.fn().mockRejectedValue(error); const operation = { id: 'op-1', metadata: { action: 'testAction' } }; await expect( manager.retryOperation(componentId, operation, retryCallback) ).rejects.toThrow('Retry failed'); }); }); describe('state management', () => { it('tracks multiple components independently', () => { const comp1 = 'component:1'; const comp2 = 'component:2'; const state = { data: {}, version: 1 }; manager.applyOptimisticUpdate(comp1, state, (s) => s); manager.applyOptimisticUpdate(comp2, state, (s) => s); expect(manager.getPendingOperations(comp1)).toHaveLength(1); expect(manager.getPendingOperations(comp2)).toHaveLength(1); expect(manager.getSnapshot(comp1)).not.toBeNull(); expect(manager.getSnapshot(comp2)).not.toBeNull(); }); it('clears all state correctly', () => { const componentId = 'test-component:1'; const currentState = { data: {}, version: 1 }; manager.applyOptimisticUpdate(componentId, currentState, (s) => s); manager.registerConflictHandler(componentId, () => {}); manager.clear(); expect(manager.getPendingOperations(componentId)).toHaveLength(0); expect(manager.getSnapshot(componentId)).toBeNull(); expect(manager.getStats().total_components).toBe(0); }); }); describe('getStats', () => { it('returns accurate statistics', () => { const comp1 = 'component:1'; const comp2 = 'component:2'; const state = { data: {}, version: 1 }; manager.applyOptimisticUpdate(comp1, state, (s) => s); manager.applyOptimisticUpdate(comp1, { data: {}, version: 2 }, (s) => s); manager.applyOptimisticUpdate(comp2, state, (s) => s); const stats = manager.getStats(); expect(stats.total_components).toBe(2); expect(stats.total_pending_operations).toBe(3); expect(stats.total_snapshots).toBe(2); expect(stats.components[comp1].pending).toBe(2); expect(stats.components[comp2].pending).toBe(1); }); }); });