Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
387
tests/modules/moduleSystem.test.js
Normal file
387
tests/modules/moduleSystem.test.js
Normal file
@@ -0,0 +1,387 @@
|
||||
/**
|
||||
* @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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user