Major Changes: - Moved legacy system to Legacy/ folder for archival - Built new modular architecture with strict separation of concerns - Created core system: Module, EventBus, ModuleLoader, Router - Added Application bootstrap with auto-start functionality - Implemented development server with ES6 modules support - Created comprehensive documentation and project context - Converted SBS-7-8 content to JSON format - Copied all legacy games and content to new structure New Architecture Features: - Sealed modules with WeakMap private data - Strict dependency injection system - Event-driven communication only - Inviolable responsibility patterns - Auto-initialization without commands - Component-based UI foundation ready Technical Stack: - Vanilla JS/HTML/CSS only - ES6 modules with proper imports/exports - HTTP development server (no file:// protocol) - Modular CSS with component scoping - Comprehensive error handling and debugging Ready for Phase 2: Converting legacy modules to new architecture 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
313 lines
11 KiB
JavaScript
313 lines
11 KiB
JavaScript
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { createMockDOM, cleanupMockDOM, createMockFetch, createLogCapture } from '../utils/test-helpers.js';
|
|
import { sampleJSONContent, moduleNameMappingTests, networkTestResponses } from '../fixtures/content-samples.js';
|
|
import { readFileSync } from 'fs';
|
|
import path from 'path';
|
|
|
|
describe('ContentScanner - Tests Unitaires', () => {
|
|
let ContentScanner;
|
|
let logCapture;
|
|
|
|
beforeEach(() => {
|
|
createMockDOM();
|
|
logCapture = createLogCapture();
|
|
|
|
// Mock EnvConfig
|
|
global.envConfig = {
|
|
isRemoteContentEnabled: () => true,
|
|
get: (key) => {
|
|
const defaults = {
|
|
'REMOTE_TIMEOUT': 3000,
|
|
'TRY_REMOTE_FIRST': true,
|
|
'FALLBACK_TO_LOCAL': true
|
|
};
|
|
return defaults[key];
|
|
}
|
|
};
|
|
|
|
// Charger ContentScanner
|
|
const scannerPath = path.resolve(process.cwd(), 'js/core/content-scanner.js');
|
|
const code = readFileSync(scannerPath, 'utf8');
|
|
|
|
const testCode = code
|
|
.replace(/window\./g, 'global.')
|
|
.replace(/typeof window !== 'undefined'/g, 'true');
|
|
|
|
eval(testCode);
|
|
ContentScanner = global.ContentScanner;
|
|
});
|
|
|
|
afterEach(() => {
|
|
logCapture.restore();
|
|
cleanupMockDOM();
|
|
});
|
|
|
|
describe('Construction et initialisation', () => {
|
|
test('devrait créer une instance ContentScanner', () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
assert.ok(scanner instanceof ContentScanner);
|
|
assert.ok(scanner.discoveredContent instanceof Map);
|
|
assert.equal(scanner.contentDirectory, 'js/content/');
|
|
});
|
|
|
|
test('devrait initialiser avec envConfig', () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
assert.ok(scanner.envConfig);
|
|
assert.equal(scanner.envConfig.isRemoteContentEnabled(), true);
|
|
});
|
|
});
|
|
|
|
describe('Conversion de noms de modules', () => {
|
|
test('jsonFilenameToModuleName devrait convertir correctement', () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
moduleNameMappingTests.forEach(({ filename, expected }) => {
|
|
const result = scanner.jsonFilenameToModuleName(`http://example.com/${filename}`);
|
|
assert.equal(result, expected, `Failed for ${filename} -> ${expected}`);
|
|
});
|
|
});
|
|
|
|
test('toPascalCase devrait convertir les chaînes avec tirets', () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
assert.equal(scanner.toPascalCase('hello-world'), 'HelloWorld');
|
|
assert.equal(scanner.toPascalCase('test-file-name'), 'TestFileName');
|
|
assert.equal(scanner.toPascalCase('simple'), 'Simple');
|
|
});
|
|
|
|
test('extractContentId devrait extraire l\'ID du fichier', () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
assert.equal(scanner.extractContentId('test-content.js'), 'test-content');
|
|
assert.equal(scanner.extractContentId('test-content.json'), 'test-content');
|
|
assert.equal(scanner.extractContentId('simple.js'), 'simple');
|
|
});
|
|
});
|
|
|
|
describe('Chargement de contenu JSON', () => {
|
|
test('loadJsonContent devrait charger depuis le distant en priorité', async () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
global.fetch = createMockFetch(networkTestResponses);
|
|
|
|
await scanner.loadJsonContent('sbs-level-7-8-new.json');
|
|
|
|
// Vérifier que le module est chargé
|
|
assert.ok(global.ContentModules);
|
|
assert.ok(global.ContentModules.SBSLevel78New);
|
|
assert.equal(global.ContentModules.SBSLevel78New.name, sampleJSONContent.name);
|
|
|
|
// Vérifier les logs
|
|
const logs = logCapture.getLogs('INFO');
|
|
assert.ok(logs.some(log => log.message.includes('Chargement JSON: sbs-level-7-8-new.json')));
|
|
});
|
|
|
|
test('loadJsonContent devrait fallback vers local si distant échoue', async () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
// Mock fetch qui échoue pour le distant mais réussit pour le local
|
|
global.fetch = async (url) => {
|
|
if (url.includes('localhost:8083')) {
|
|
throw new Error('Network error');
|
|
}
|
|
if (url.includes('js/content/')) {
|
|
return {
|
|
ok: true,
|
|
json: async () => sampleJSONContent
|
|
};
|
|
}
|
|
throw new Error('Unknown URL');
|
|
};
|
|
|
|
await scanner.loadJsonContent('test-content.json');
|
|
|
|
// Vérifier que le module est chargé depuis local
|
|
assert.ok(global.ContentModules.TestContent);
|
|
|
|
const logs = logCapture.getLogs('WARN');
|
|
assert.ok(logs.some(log => log.message.includes('Distant échoué')));
|
|
});
|
|
|
|
test('loadJsonContent devrait échouer si toutes les méthodes échouent', async () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
global.fetch = async () => {
|
|
throw new Error('All methods failed');
|
|
};
|
|
|
|
try {
|
|
await scanner.loadJsonContent('nonexistent.json');
|
|
assert.fail('Should have thrown an error');
|
|
} catch (error) {
|
|
assert.ok(error.message.includes('Impossible de charger JSON'));
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Test de connectivité distante', () => {
|
|
test('shouldTryRemote devrait retourner true si configuré', () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
// Mock window.location pour protocol http
|
|
global.window.location.protocol = 'http:';
|
|
|
|
assert.equal(scanner.shouldTryRemote(), true);
|
|
});
|
|
|
|
test('shouldTryRemote devrait retourner false si pas configuré', () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
global.envConfig.isRemoteContentEnabled = () => false;
|
|
|
|
assert.equal(scanner.shouldTryRemote(), false);
|
|
});
|
|
|
|
test('tryRemoteLoad devrait utiliser le proxy local', async () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
global.fetch = createMockFetch(networkTestResponses);
|
|
|
|
const result = await scanner.tryRemoteLoad('sbs-level-7-8-new.json');
|
|
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.source, 'remote');
|
|
});
|
|
});
|
|
|
|
describe('Scan de fichiers de contenu', () => {
|
|
test('scanContentFile devrait traiter un fichier JSON', async () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
global.fetch = createMockFetch(networkTestResponses);
|
|
|
|
const result = await scanner.scanContentFile('sbs-level-7-8-new.json');
|
|
|
|
assert.ok(result);
|
|
assert.equal(result.id, 'sbs-level-7-8-new');
|
|
assert.equal(result.filename, 'sbs-level-7-8-new.json');
|
|
assert.ok(result.name);
|
|
});
|
|
|
|
test('scanContentFile devrait traiter un fichier JS', async () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
// Mock pour loadScript
|
|
scanner.loadScript = async () => {
|
|
global.ContentModules = global.ContentModules || {};
|
|
global.ContentModules.TestContent = {
|
|
name: 'Test Content',
|
|
vocabulary: { 'test': 'test' }
|
|
};
|
|
};
|
|
|
|
const result = await scanner.scanContentFile('test-content.js');
|
|
|
|
assert.ok(result);
|
|
assert.equal(result.id, 'test-content');
|
|
assert.equal(result.filename, 'test-content.js');
|
|
});
|
|
|
|
test('scanContentFile devrait échouer si module non trouvé', async () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
global.fetch = async () => ({ ok: true, json: async () => ({}) });
|
|
|
|
try {
|
|
await scanner.scanContentFile('nonexistent.json');
|
|
assert.fail('Should have thrown an error');
|
|
} catch (error) {
|
|
assert.ok(error.message.includes('Impossible de charger'));
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Extraction d\'informations de contenu', () => {
|
|
test('extractContentInfo devrait extraire les métadonnées', () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
const module = {
|
|
name: 'Test Module',
|
|
description: 'Test description',
|
|
difficulty: 'easy',
|
|
version: '2.0'
|
|
};
|
|
|
|
const info = scanner.extractContentInfo(module, 'test-id', 'test.js');
|
|
|
|
assert.equal(info.id, 'test-id');
|
|
assert.equal(info.filename, 'test.js');
|
|
assert.equal(info.name, 'Test Module');
|
|
assert.equal(info.description, 'Test description');
|
|
assert.equal(info.difficulty, 'easy');
|
|
assert.equal(info.enabled, true);
|
|
assert.equal(info.metadata.version, '2.0');
|
|
});
|
|
|
|
test('extractContentInfo devrait utiliser des valeurs par défaut', () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
const module = {};
|
|
const info = scanner.extractContentInfo(module, 'test-id', 'test.js');
|
|
|
|
assert.equal(info.difficulty, 'medium');
|
|
assert.equal(info.description, 'Contenu automatiquement détecté');
|
|
assert.equal(info.metadata.version, '1.0');
|
|
assert.equal(info.metadata.format, 'legacy');
|
|
});
|
|
});
|
|
|
|
describe('Gestion des erreurs et logs', () => {
|
|
test('updateConnectionStatus devrait émettre un événement', () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
let eventReceived = false;
|
|
global.window.dispatchEvent = (event) => {
|
|
if (event.type === 'contentConnectionStatus') {
|
|
eventReceived = true;
|
|
}
|
|
};
|
|
|
|
scanner.updateConnectionStatus('online', 'Test connection');
|
|
|
|
assert.equal(eventReceived, true);
|
|
});
|
|
|
|
test('devrait logger les erreurs appropriées', async () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
global.fetch = async () => {
|
|
throw new Error('Test error');
|
|
};
|
|
|
|
try {
|
|
await scanner.loadJsonContent('test.json');
|
|
} catch (error) {
|
|
// Expected
|
|
}
|
|
|
|
const errorLogs = logCapture.getLogs('WARN');
|
|
assert.ok(errorLogs.length > 0);
|
|
assert.ok(errorLogs.some(log => log.message.includes('Test error')));
|
|
});
|
|
});
|
|
|
|
describe('Discovery de fichiers', () => {
|
|
test('tryCommonFiles devrait essayer les fichiers connus', async () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
global.fetch = createMockFetch({
|
|
'http://localhost:8083/do-proxy/sbs-level-7-8-new.json': { ok: true },
|
|
'http://localhost:8083/do-proxy/english-class-demo.json': { ok: true }
|
|
});
|
|
|
|
const files = await scanner.tryCommonFiles();
|
|
|
|
assert.ok(Array.isArray(files));
|
|
assert.ok(files.includes('sbs-level-7-8-new.json'));
|
|
assert.ok(files.includes('english-class-demo.json'));
|
|
});
|
|
});
|
|
}); |