- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
387 lines
11 KiB
JavaScript
387 lines
11 KiB
JavaScript
/**
|
|
* @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');
|
|
});
|
|
});
|
|
}); |