/** * @jest-environment jsdom */ // Mock all dependencies jest.mock('../../resources/js/core/logger.js', () => ({ Logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn() } })); jest.mock('../../resources/js/core/ModuleErrorBoundary.js', () => ({ moduleErrorBoundary: { wrapModule: jest.fn((mod, name) => mod), reset: jest.fn() } })); jest.mock('../../resources/js/core/StateManager.js', () => ({ stateManager: { createScope: jest.fn(() => { const state = new Map(); const subscribers = new Map(); return { register: jest.fn((key, defaultValue) => { state.set(key, defaultValue); subscribers.set(key, []); }), get: jest.fn((key) => state.get(key)), set: jest.fn((key, value) => { const oldValue = state.get(key); state.set(key, value); const subs = subscribers.get(key) || []; subs.forEach(callback => callback(value, oldValue, key)); return true; }), subscribe: jest.fn((key, callback) => { if (!subscribers.has(key)) { subscribers.set(key, []); } subscribers.get(key).push(callback); return `sub_${key}_${Date.now()}`; }), unsubscribe: jest.fn(), cleanup: jest.fn(), reset: jest.fn(() => { state.clear(); subscribers.clear(); }) }; }), resetAll: jest.fn() } })); jest.mock('../../resources/js/core/DependencyManager.js', () => ({ dependencyManager: { register: jest.fn(), calculateInitializationOrder: jest.fn(() => ['test-module']), checkDependencies: jest.fn(() => ({ satisfied: true, missing: [] })), markInitializing: jest.fn(), markInitialized: jest.fn(), reset: jest.fn() } })); // Use manual mock for modules/index.js jest.mock('../../resources/js/modules/index.js'); // Mock import.meta.glob first const mockModules = { './test-module/index.js': { init: jest.fn(), destroy: jest.fn(), definition: { name: 'test-module', version: '1.0.0', dependencies: [], provides: [], priority: 0 } } }; // Create mock import.meta for Babel environment global.importMeta = { glob: jest.fn(() => mockModules) }; import { registerModules, destroyModules, getModuleHealth, activeModules } from '../../resources/js/modules/index.js'; import { moduleErrorBoundary } from '../../resources/js/core/ModuleErrorBoundary.js'; import { stateManager } from '../../resources/js/core/StateManager.js'; import { dependencyManager } from '../../resources/js/core/DependencyManager.js'; describe('Module System', () => { beforeEach(() => { // Clear active modules activeModules.clear(); // Reset all mocks jest.clearAllMocks(); // Reset DOM document.body.innerHTML = ''; }); describe('Module registration', () => { test('should register modules without DOM selectors in fallback mode', async () => { await registerModules(); expect(dependencyManager.register).toHaveBeenCalledWith( expect.objectContaining({ name: 'test-module', version: '1.0.0' }) ); expect(dependencyManager.calculateInitializationOrder).toHaveBeenCalled(); expect(mockModules['./test-module/index.js'].init).toHaveBeenCalled(); expect(activeModules.has('test-module')).toBe(true); }); test('should respect DOM-based module selection', async () => { // Add module selector to DOM const element = document.createElement('div'); element.setAttribute('data-module', 'test-module'); document.body.appendChild(element); await registerModules(); expect(mockModules['./test-module/index.js'].init).toHaveBeenCalled(); }); test('should skip modules not used in DOM when not in fallback mode', async () => { // Add different module selector to DOM const element = document.createElement('div'); element.setAttribute('data-module', 'other-module'); document.body.appendChild(element); await registerModules(); expect(mockModules['./test-module/index.js'].init).not.toHaveBeenCalled(); }); test('should create default definition for modules without explicit definition', async () => { // Module without definition const moduleWithoutDef = { init: jest.fn(), destroy: jest.fn() }; global.importMeta.glob.mockReturnValue({ './no-def-module/index.js': moduleWithoutDef }); dependencyManager.calculateInitializationOrder.mockReturnValue(['no-def-module']); await registerModules(); expect(dependencyManager.register).toHaveBeenCalledWith( expect.objectContaining({ name: 'no-def-module', version: '1.0.0', dependencies: [], provides: [], priority: 0 }) ); }); test('should handle module initialization errors', async () => { const errorModule = { init: jest.fn(() => { throw new Error('Init failed'); }), destroy: jest.fn() }; global.importMeta.glob.mockReturnValue({ './error-module/index.js': errorModule }); dependencyManager.calculateInitializationOrder.mockReturnValue(['error-module']); await registerModules(); const moduleData = activeModules.get('error-module'); expect(moduleData.mod).toBeNull(); expect(moduleData.error).toBeInstanceOf(Error); }); test('should handle unsatisfied dependencies', async () => { dependencyManager.checkDependencies.mockReturnValue({ satisfied: false, missing: ['missing-dependency'], reason: 'Missing: missing-dependency' }); await registerModules(); const moduleData = activeModules.get('test-module'); expect(moduleData.mod).toBeNull(); expect(moduleData.error.message).toBe('Missing: missing-dependency'); }); test('should pass config and state to module init', async () => { // Mock module config jest.doMock('../../resources/js/modules/config.js', () => ({ moduleConfig: { 'test-module': { setting: 'value' } } }), { virtual: true }); await registerModules(); expect(mockModules['./test-module/index.js'].init).toHaveBeenCalledWith( { setting: 'value' }, expect.any(Object) // scoped state manager ); }); test('should wrap modules with error boundary', async () => { await registerModules(); expect(moduleErrorBoundary.wrapModule).toHaveBeenCalledWith( mockModules['./test-module/index.js'], 'test-module' ); }); test('should create scoped state manager for each module', async () => { await registerModules(); expect(stateManager.createScope).toHaveBeenCalledWith('test-module'); }); test('should track initialization with dependency manager', async () => { await registerModules(); expect(dependencyManager.markInitializing).toHaveBeenCalledWith('test-module'); expect(dependencyManager.markInitialized).toHaveBeenCalledWith('test-module'); }); }); describe('Module destruction', () => { beforeEach(async () => { await registerModules(); }); test('should call destroy on all modules', () => { destroyModules(); expect(mockModules['./test-module/index.js'].destroy).toHaveBeenCalled(); }); test('should cleanup state subscriptions', () => { const mockState = { cleanup: jest.fn() }; // Manually add module with state activeModules.set('test-module', { mod: mockModules['./test-module/index.js'], config: {}, state: mockState }); destroyModules(); expect(mockState.cleanup).toHaveBeenCalled(); }); test('should handle destroy errors gracefully', () => { const errorModule = { destroy: jest.fn(() => { throw new Error('Destroy failed'); }) }; activeModules.set('error-module', { mod: errorModule, config: {}, state: null }); expect(() => destroyModules()).not.toThrow(); }); test('should reset all managers', () => { destroyModules(); expect(moduleErrorBoundary.reset).toHaveBeenCalled(); expect(stateManager.reset).toHaveBeenCalled(); expect(dependencyManager.reset).toHaveBeenCalled(); }); test('should clear active modules map', () => { destroyModules(); expect(activeModules.size).toBe(0); }); }); describe('Module health monitoring', () => { test('should report health status correctly', async () => { await registerModules(); const health = getModuleHealth(); expect(health.total).toBe(1); expect(health.active).toBe(1); expect(health.failed).toBe(0); expect(health.modules['test-module']).toEqual({ status: 'active' }); }); test('should report failed modules', () => { activeModules.set('failed-module', { mod: null, config: {}, error: new Error('Failed to init'), original: mockModules['./test-module/index.js'] }); const health = getModuleHealth(); expect(health.failed).toBe(1); expect(health.modules['failed-module']).toEqual({ status: 'failed', error: 'Failed to init' }); }); test('should include error boundary status', async () => { moduleErrorBoundary.getHealthStatus = jest.fn(() => ({ totalCrashedModules: 0, crashedModules: [], recoveryAttempts: {} })); await registerModules(); const health = getModuleHealth(); expect(health.errorBoundary).toBeDefined(); expect(health.errorBoundary.totalCrashedModules).toBe(0); }); }); describe('Integration tests', () => { test('should handle complete module lifecycle', async () => { // Register await registerModules(); expect(activeModules.size).toBe(1); expect(mockModules['./test-module/index.js'].init).toHaveBeenCalled(); // Destroy destroyModules(); expect(mockModules['./test-module/index.js'].destroy).toHaveBeenCalled(); expect(activeModules.size).toBe(0); }); test('should handle async module initialization', async () => { const asyncModule = { init: jest.fn(async () => { await new Promise(resolve => setTimeout(resolve, 10)); }), destroy: jest.fn() }; global.importMeta.glob.mockReturnValue({ './async-module/index.js': asyncModule }); dependencyManager.calculateInitializationOrder.mockReturnValue(['async-module']); await registerModules(); expect(asyncModule.init).toHaveBeenCalled(); expect(dependencyManager.markInitialized).toHaveBeenCalledWith('async-module'); }); }); });