import { test, describe, beforeEach, afterEach } from 'node:test'; import { strict as assert } from 'node:assert'; import { createMockDOM, cleanupMockDOM, createLogCapture } from '../utils/test-helpers.js'; import { readFileSync } from 'fs'; import path from 'path'; // Test d'intégration pour le flux complet de chargement de contenu describe('Flux de Chargement de Contenu - Tests d\'Intégration', () => { let ContentScanner, GameLoader, EnvConfig; let logCapture; // Helper pour simuler fetch vers le proxy function createProxyMockFetch() { return async (url) => { if (url.includes('localhost:8083/do-proxy/sbs-level-7-8-new.json')) { return { ok: true, status: 200, json: async () => ({ name: "SBS Level 7-8 (New)", description: "Test content from remote", difficulty: "intermediate", vocabulary: { "central": "中心的;中央的", "avenue": "大街;林荫道", "refrigerator": "冰箱" } }) }; } if (url.includes('localhost:8083/do-proxy/english-class-demo.json')) { return { ok: true, status: 200, json: async () => ({ name: "English Class Demo", description: "Demo content", vocabulary: { "hello": "bonjour", "world": "monde" } }) }; } // Local fallback if (url.includes('js/content/')) { return { ok: true, status: 200, json: async () => ({ name: "Local Content", vocabulary: { "local": "content" } }) }; } return { ok: false, status: 404, json: async () => ({ error: 'Not found' }) }; }; } beforeEach(() => { createMockDOM(); logCapture = createLogCapture(); // Charger tous les modules nécessaires const modules = [ { name: 'EnvConfig', path: 'js/core/env-config.js' }, { name: 'ContentScanner', path: 'js/core/content-scanner.js' }, { name: 'GameLoader', path: 'js/core/game-loader.js' } ]; modules.forEach(({ name, path: modulePath }) => { const fullPath = path.resolve(process.cwd(), modulePath); const code = readFileSync(fullPath, 'utf8'); const testCode = code .replace(/window\./g, 'global.') .replace(/typeof window !== 'undefined'/g, 'true') .replace(/typeof module !== 'undefined' && module\.exports/g, 'false'); eval(testCode); }); EnvConfig = global.EnvConfig; ContentScanner = global.ContentScanner; GameLoader = global.GameLoader; // Initialiser envConfig global global.envConfig = new EnvConfig(); // Mock fetch global.fetch = createProxyMockFetch(); // Mock GameModules global.GameModules = { WhackAMole: class { constructor(options) { this.container = options.container; this.content = options.content; this.started = false; } start() { this.started = true; } destroy() { this.destroyed = true; } } }; }); afterEach(() => { logCapture.restore(); cleanupMockDOM(); }); describe('Flux complet: Scan → Load → Game', () => { test('devrait scanner et charger le contenu distant puis créer un jeu', async () => { // Étape 1: Scanner le contenu const scanner = new ContentScanner(); const scanResults = await scanner.scanAllContent(); assert.ok(scanResults); assert.ok(scanResults.found.length > 0, 'Should find some content'); // Vérifier que les modules sont chargés assert.ok(global.ContentModules); assert.ok(global.ContentModules.SBSLevel78New || global.ContentModules.EnglishClassDemo, 'Should load at least one remote module'); // Étape 2: Charger un jeu avec le contenu trouvé const loader = new GameLoader(); const container = { innerHTML: '' }; const contentId = scanResults.found[0].id; const game = await loader.loadGame('whack-a-mole', contentId, container); assert.ok(game); assert.ok(game instanceof global.GameModules.WhackAMole); assert.ok(game.content); assert.ok(game.content.vocabulary); // Étape 3: Démarrer le jeu game.start(); assert.equal(game.started, true); // Vérifier les logs const logs = logCapture.getLogs(); assert.ok(logs.some(log => log.message.includes('Scan automatique'))); assert.ok(logs.some(log => log.message.includes('chargé avec succès'))); }); test('devrait gérer le fallback local si le distant échoue', async () => { // Mock fetch qui échoue pour le distant global.fetch = async (url) => { if (url.includes('localhost:8083')) { throw new Error('Network error'); } if (url.includes('js/content/')) { return { ok: true, json: async () => ({ name: "Local Fallback", vocabulary: { "fallback": "secours" } }) }; } throw new Error('Unknown URL'); }; const scanner = new ContentScanner(); // Forcer le chargement d'un fichier JSON await scanner.loadJsonContent('test-content.json'); assert.ok(global.ContentModules.TestContent); assert.equal(global.ContentModules.TestContent.name, "Local Fallback"); const logs = logCapture.getLogs('WARN'); assert.ok(logs.some(log => log.message.includes('Distant échoué'))); }); }); describe('Configuration et priorités', () => { test('devrait respecter la configuration TRY_REMOTE_FIRST', async () => { const config = new EnvConfig(); config.set('TRY_REMOTE_FIRST', true); const scanner = new ContentScanner(); // Compter les appels fetch let remoteAttempts = 0; let localAttempts = 0; global.fetch = async (url) => { if (url.includes('localhost:8083')) { remoteAttempts++; return { ok: true, json: async () => ({ name: "Remote", vocabulary: { "remote": "distant" } }) }; } if (url.includes('js/content/')) { localAttempts++; return { ok: true, json: async () => ({ name: "Local", vocabulary: { "local": "local" } }) }; } throw new Error('Unknown URL'); }; await scanner.loadJsonContent('test.json'); assert.ok(remoteAttempts > 0, 'Should try remote first'); // Local ne devrait pas être appelé si remote réussit assert.equal(localAttempts, 0, 'Should not fallback to local if remote succeeds'); }); test('devrait désactiver le distant si configuré', async () => { const config = new EnvConfig(); config.set('USE_REMOTE_CONTENT', false); const scanner = new ContentScanner(); let remoteAttempts = 0; let localAttempts = 0; global.fetch = async (url) => { if (url.includes('localhost:8083')) { remoteAttempts++; throw new Error('Should not be called'); } if (url.includes('js/content/')) { localAttempts++; return { ok: true, json: async () => ({ name: "Local Only", vocabulary: { "local": "only" } }) }; } throw new Error('Unknown URL'); }; await scanner.loadJsonContent('test.json'); assert.equal(remoteAttempts, 0, 'Should not try remote when disabled'); assert.ok(localAttempts > 0, 'Should use local'); }); }); describe('Gestion des erreurs en cascade', () => { test('devrait propager les erreurs si tout échoue', async () => { global.fetch = async () => { throw new Error('All methods failed'); }; const scanner = new ContentScanner(); try { await scanner.loadJsonContent('failing-content.json'); assert.fail('Should have thrown an error'); } catch (error) { assert.ok(error.message.includes('Impossible de charger JSON')); } const loader = new GameLoader(); try { await loader.loadGame('whack-a-mole', 'nonexistent-content', {}); assert.fail('Should have thrown an error'); } catch (error) { assert.ok(error.message.includes('Contenu non trouvé')); } }); test('devrait logger toutes les erreurs intermédiaires', async () => { global.fetch = async (url) => { if (url.includes('localhost:8083')) { throw new Error('Remote connection failed'); } if (url.includes('js/content/')) { throw new Error('Local file not found'); } throw new Error('Unknown error'); }; const scanner = new ContentScanner(); try { await scanner.loadJsonContent('test.json'); } catch (error) { // Expected } const logs = logCapture.getLogs(); const errorLogs = logs.filter(log => log.level === 'WARN' || log.level === 'ERROR'); assert.ok(errorLogs.length > 0, 'Should log intermediate errors'); assert.ok(errorLogs.some(log => log.message.includes('Remote connection failed'))); assert.ok(errorLogs.some(log => log.message.includes('Local file not found'))); }); }); describe('Performance et cache', () => { test('devrait éviter de recharger les modules déjà chargés', async () => { const scanner = new ContentScanner(); // Précharger un module global.ContentModules = { TestContent: { name: "Already Loaded", vocabulary: { "cached": "mis en cache" } } }; let fetchCalls = 0; global.fetch = async () => { fetchCalls++; throw new Error('Should not be called'); }; // Essayer de scanner un fichier pour un module déjà chargé const result = await scanner.scanContentFile('test-content.json'); assert.ok(result); assert.equal(result.name, "Already Loaded"); assert.equal(fetchCalls, 0, 'Should not fetch if module already loaded'); }); test('le chargement simultané de contenu devrait fonctionner', async () => { const scanner = new ContentScanner(); const promises = [ scanner.loadJsonContent('content1.json'), scanner.loadJsonContent('content2.json'), scanner.loadJsonContent('content3.json') ]; let fetchCount = 0; global.fetch = async (url) => { fetchCount++; await new Promise(resolve => setTimeout(resolve, 10)); // Simule latence return { ok: true, json: async () => ({ name: `Content ${fetchCount}`, vocabulary: { [`word${fetchCount}`]: `translation${fetchCount}` } }) }; }; const results = await Promise.allSettled(promises); assert.equal(results.length, 3); results.forEach((result, index) => { assert.equal(result.status, 'fulfilled', `Promise ${index} should succeed`); }); // Vérifier que les modules sont chargés assert.ok(global.ContentModules.Content1); assert.ok(global.ContentModules.Content2); assert.ok(global.ContentModules.Content3); }); }); describe('Validation de l\'intégrité des données', () => { test('devrait valider la structure du contenu chargé', async () => { global.fetch = async () => ({ ok: true, json: async () => ({ name: "Valid Content", vocabulary: { "valid": "valide", "test": "test" }, difficulty: "medium" }) }); const scanner = new ContentScanner(); await scanner.loadJsonContent('valid-content.json'); const loader = new GameLoader(); // Devrait réussir avec un contenu valide assert.doesNotThrow(() => { loader.validateGameContent(global.ContentModules.ValidContent, 'whack-a-mole'); }); }); test('devrait rejeter le contenu invalide', async () => { global.fetch = async () => ({ ok: true, json: async () => ({ name: "Invalid Content", // Pas de vocabulary }) }); const scanner = new ContentScanner(); await scanner.loadJsonContent('invalid-content.json'); const loader = new GameLoader(); assert.throws(() => { loader.validateGameContent(global.ContentModules.InvalidContent, 'whack-a-mole'); }); }); }); });