Class_generator/Legacy/tests/unit/content-scanner.test.js
StillHammer 38920cc858 Complete architectural rewrite with ultra-modular system
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>
2025-09-22 07:08:39 +08:00

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'));
});
});
});