/** * @jest-environment jsdom */ import { StateManager } from '../../resources/js/core/StateManager.js'; // Mock Logger jest.mock('../../resources/js/core/logger.js', () => ({ Logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn() } })); describe('StateManager', () => { let stateManager; beforeEach(() => { stateManager = new StateManager(); stateManager.setContext('test-module'); }); describe('State registration', () => { test('should register state with default value', () => { stateManager.register('testKey', 'defaultValue'); expect(stateManager.get('testKey')).toBe('defaultValue'); }); test('should not allow duplicate registration', () => { stateManager.register('testKey', 'value1'); stateManager.register('testKey', 'value2'); expect(stateManager.get('testKey')).toBe('value1'); }); test('should track state ownership', () => { stateManager.setContext('module-a'); stateManager.register('keyA', 'valueA'); stateManager.setContext('module-b'); stateManager.register('keyB', 'valueB'); expect(stateManager.stateOwners.get('keyA')).toBe('module-a'); expect(stateManager.stateOwners.get('keyB')).toBe('module-b'); }); }); describe('State access', () => { beforeEach(() => { stateManager.register('testKey', 42); }); test('should get current state value', () => { expect(stateManager.get('testKey')).toBe(42); }); test('should return undefined for unknown keys', () => { expect(stateManager.get('unknownKey')).toBeUndefined(); }); test('should allow owner to set value', () => { const success = stateManager.set('testKey', 100); expect(success).toBe(true); expect(stateManager.get('testKey')).toBe(100); }); test('should prevent non-owner from setting value', () => { stateManager.setContext('other-module'); const success = stateManager.set('testKey', 200); expect(success).toBe(false); expect(stateManager.get('testKey')).toBe(42); }); test('should allow forced set by non-owner', () => { stateManager.setContext('other-module'); const success = stateManager.set('testKey', 300, true); expect(success).toBe(true); expect(stateManager.get('testKey')).toBe(300); }); test('should not trigger subscribers for same value', () => { const callback = jest.fn(); stateManager.subscribe('testKey', callback); stateManager.set('testKey', 42); // Same value expect(callback).not.toHaveBeenCalled(); }); }); describe('State subscriptions', () => { beforeEach(() => { stateManager.register('testKey', 'initial'); }); test('should subscribe to state changes', () => { const callback = jest.fn(); const subscriptionId = stateManager.subscribe('testKey', callback); expect(subscriptionId).toBeTruthy(); expect(typeof subscriptionId).toBe('string'); }); test('should notify subscribers on state change', () => { const callback = jest.fn(); stateManager.subscribe('testKey', callback); stateManager.set('testKey', 'changed'); expect(callback).toHaveBeenCalledWith('changed', 'initial', 'testKey'); }); test('should handle multiple subscribers', () => { const callback1 = jest.fn(); const callback2 = jest.fn(); stateManager.subscribe('testKey', callback1); stateManager.subscribe('testKey', callback2); stateManager.set('testKey', 'updated'); expect(callback1).toHaveBeenCalledWith('updated', 'initial', 'testKey'); expect(callback2).toHaveBeenCalledWith('updated', 'initial', 'testKey'); }); test('should unsubscribe correctly', () => { const callback = jest.fn(); const subscriptionId = stateManager.subscribe('testKey', callback); stateManager.unsubscribe(subscriptionId); stateManager.set('testKey', 'after-unsubscribe'); expect(callback).not.toHaveBeenCalled(); }); test('should handle subscriber errors gracefully', () => { const errorCallback = jest.fn(() => { throw new Error('Subscriber error'); }); const goodCallback = jest.fn(); stateManager.subscribe('testKey', errorCallback); stateManager.subscribe('testKey', goodCallback); expect(() => { stateManager.set('testKey', 'trigger-error'); }).not.toThrow(); expect(goodCallback).toHaveBeenCalled(); }); test('should not subscribe to unknown keys', () => { const callback = jest.fn(); const subscriptionId = stateManager.subscribe('unknownKey', callback); expect(subscriptionId).toBeNull(); }); }); describe('State reset', () => { beforeEach(() => { stateManager.register('testKey', 'default'); stateManager.set('testKey', 'changed'); }); test('should reset to default value', () => { const success = stateManager.reset('testKey'); expect(success).toBe(true); expect(stateManager.get('testKey')).toBe('default'); }); test('should notify subscribers on reset', () => { const callback = jest.fn(); stateManager.subscribe('testKey', callback); stateManager.reset('testKey'); expect(callback).toHaveBeenCalledWith('default', 'changed', 'testKey'); }); test('should not reset unknown keys', () => { const success = stateManager.reset('unknownKey'); expect(success).toBe(false); }); }); describe('Module scoped interface', () => { test('should create scoped state manager', () => { const scoped = stateManager.createScope('scoped-module'); expect(scoped).toHaveProperty('register'); expect(scoped).toHaveProperty('get'); expect(scoped).toHaveProperty('set'); expect(scoped).toHaveProperty('subscribe'); expect(scoped).toHaveProperty('cleanup'); }); test('should use module context in scoped operations', () => { const scoped = stateManager.createScope('scoped-module'); scoped.register('scopedKey', 'scopedValue'); expect(stateManager.stateOwners.get('scopedKey')).toBe('scoped-module'); }); test('should cleanup module subscriptions', () => { const scoped = stateManager.createScope('cleanup-module'); const callback = jest.fn(); scoped.register('cleanupKey', 'value'); const subscriptionId = scoped.subscribe('cleanupKey', callback); scoped.cleanup(); // Subscription should be cleaned up stateManager.set('cleanupKey', 'after-cleanup', true); expect(callback).not.toHaveBeenCalled(); }); }); describe('Debugging and introspection', () => { test('should provide state snapshot', () => { stateManager.register('key1', 'value1'); stateManager.register('key2', 42); const snapshot = stateManager.getSnapshot(); expect(snapshot).toHaveProperty('state'); expect(snapshot).toHaveProperty('owners'); expect(snapshot).toHaveProperty('subscriptions'); expect(snapshot.state.key1).toBe('value1'); expect(snapshot.state.key2).toBe(42); }); test('should reset entire state manager', () => { stateManager.register('key1', 'value1'); stateManager.subscribe('key1', jest.fn()); stateManager.resetAll(); expect(stateManager.state.size).toBe(0); expect(stateManager.subscribers.size).toBe(0); expect(stateManager.stateOwners.size).toBe(0); }); }); describe('Performance', () => { test('should handle many state operations efficiently', () => { const start = performance.now(); // Register many state keys for (let i = 0; i < 100; i++) { stateManager.register(`key${i}`, i); } // Subscribe to all keys for (let i = 0; i < 100; i++) { stateManager.subscribe(`key${i}`, jest.fn()); } // Update all keys for (let i = 0; i < 100; i++) { stateManager.set(`key${i}`, i * 2); } const end = performance.now(); const duration = end - start; // Should complete in reasonable time expect(duration).toBeLessThan(100); }); }); });