import { test, describe, beforeEach, afterEach } from 'node:test'; import { strict as assert } from 'node:assert'; import { createMockDOM, cleanupMockDOM, createLogCapture, createTimerMock } from '../utils/test-helpers.js'; import { readFileSync } from 'fs'; import path from 'path'; describe('Edge Cases - Tests des Cas Limites', () => { let ContentScanner, GameLoader, EnvConfig; let logCapture, timerMock; beforeEach(() => { createMockDOM(); logCapture = createLogCapture(); timerMock = createTimerMock(); // Charger les modules 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'); eval(testCode); }); EnvConfig = global.EnvConfig; ContentScanner = global.ContentScanner; GameLoader = global.GameLoader; global.envConfig = new EnvConfig(); }); afterEach(() => { timerMock.restore(); logCapture.restore(); cleanupMockDOM(); }); describe('Cas de Données Corrompues', () => { test('devrait gérer JSON malformé', async () => { const scanner = new ContentScanner(); global.fetch = async () => ({ ok: true, json: async () => { throw new SyntaxError('Unexpected token in JSON'); } }); try { await scanner.loadJsonContent('corrupted.json'); assert.fail('Should have thrown an error'); } catch (error) { assert.ok(error.message.includes('Impossible de charger JSON')); } }); test('devrait gérer contenu avec caractères spéciaux', async () => { const scanner = new ContentScanner(); global.fetch = async () => ({ ok: true, json: async () => ({ name: "Contenu avec émojis 🎉🚀💥", vocabulary: { "café": "☕ boisson chaude", "🌟": "étoile", "测试": "test en chinois" } }) }); await scanner.loadJsonContent('special-chars.json'); assert.ok(global.ContentModules.SpecialChars); assert.equal(global.ContentModules.SpecialChars.vocabulary["café"], "☕ boisson chaude"); }); test('devrait gérer contenu avec structure profonde', async () => { const scanner = new ContentScanner(); // Créer un objet très profond let deepObject = { vocabulary: {} }; let current = deepObject; for (let i = 0; i < 100; i++) { current.nested = { level: i }; current = current.nested; } global.fetch = async () => ({ ok: true, json: async () => deepObject }); await scanner.loadJsonContent('deep-structure.json'); assert.ok(global.ContentModules.DeepStructure); }); test('devrait gérer contenu avec références circulaires', async () => { const scanner = new ContentScanner(); global.fetch = async () => ({ ok: true, json: async () => { const obj = { name: "Circular" }; obj.self = obj; // Référence circulaire return obj; } }); // Les références circulaires peuvent causer des problèmes lors de la sérialisation try { await scanner.loadJsonContent('circular.json'); // Si ça passe, c'est bon, sinon on teste l'erreur } catch (error) { assert.ok(error.message.includes('circular') || error.message.includes('Converting')); } }); }); describe('Cas de Réseau Instable', () => { test('devrait gérer les déconnexions intermittentes', async () => { const scanner = new ContentScanner(); let attemptCount = 0; global.fetch = async () => { attemptCount++; if (attemptCount < 3) { throw new Error('Network temporarily unavailable'); } return { ok: true, json: async () => ({ name: "Finally loaded", vocabulary: { "test": "test" } }) }; }; // Le premier appel devrait échouer, mais le fallback local pourrait réussir try { await scanner.loadJsonContent('unstable-network.json'); } catch (error) { assert.ok(error.message.includes('Impossible de charger JSON')); } assert.ok(attemptCount >= 1); }); test('devrait gérer les réponses partielles', async () => { const scanner = new ContentScanner(); global.fetch = async () => ({ ok: true, json: async () => { // Simuler une réponse incomplète qui s'interrompt await new Promise(resolve => setTimeout(resolve, 10)); throw new Error('Connection reset by peer'); } }); try { await scanner.loadJsonContent('partial-response.json'); assert.fail('Should have thrown an error'); } catch (error) { assert.ok(error.message.includes('Impossible de charger JSON')); } }); test('devrait gérer les timeouts variables', async () => { const config = new EnvConfig(); config.set('REMOTE_TIMEOUT', 100); // Timeout très court global.fetch = async () => { await new Promise(resolve => setTimeout(resolve, 200)); // Plus long que timeout return { ok: true, json: async () => ({}) }; }; const result = await config.testRemoteConnection(); assert.equal(result.success, false); assert.equal(result.isTimeout, true); }); test('devrait gérer les codes de statut inattendus', async () => { const scanner = new ContentScanner(); const statusCodes = [429, 502, 503, 504, 520, 521, 522, 523, 524]; for (const status of statusCodes) { global.fetch = async () => ({ ok: false, status: status, json: async () => ({ error: `HTTP ${status}` }) }); try { await scanner.tryRemoteLoad('test.json'); } catch (error) { // Ces erreurs sont attendues } } }); }); describe('Cas de Mémoire et Performance', () => { test('devrait gérer des fichiers de contenu très volumineux', async () => { const scanner = new ContentScanner(); // Créer un gros objet (simuler 1MB+ de données) const largeVocabulary = {}; for (let i = 0; i < 10000; i++) { largeVocabulary[`word${i}`] = `translation${i}`.repeat(50); } global.fetch = async () => ({ ok: true, json: async () => ({ name: "Large Content", vocabulary: largeVocabulary }) }); const startTime = Date.now(); await scanner.loadJsonContent('large-content.json'); const endTime = Date.now(); assert.ok(global.ContentModules.LargeContent); assert.ok(Object.keys(global.ContentModules.LargeContent.vocabulary).length === 10000); // Vérifier que ça ne prend pas trop de temps assert.ok(endTime - startTime < 5000); // Moins de 5 secondes }); test('devrait gérer le chargement simultané de nombreux modules', async () => { const scanner = new ContentScanner(); global.fetch = async (url) => { const filename = url.split('/').pop(); return { ok: true, json: async () => ({ name: `Module for ${filename}`, vocabulary: { [`word_${filename}`]: 'translation' } }) }; }; // Charger 50 modules simultanément const promises = []; for (let i = 0; i < 50; i++) { promises.push(scanner.loadJsonContent(`module${i}.json`)); } const results = await Promise.allSettled(promises); const successful = results.filter(r => r.status === 'fulfilled'); assert.ok(successful.length >= 45); // Au moins 90% de succès }); test('devrait nettoyer la mémoire après destruction', () => { const loader = new GameLoader(); // Créer plusieurs jeux avec des références for (let i = 0; i < 10; i++) { const mockGame = { data: new Array(1000).fill(`data${i}`), // Simule des données destroy: function() { this.data = null; this.destroyed = true; } }; loader.currentGame = mockGame; loader.cleanup(); } // Vérifier que le dernier jeu est bien nettoyé assert.equal(loader.currentGame, null); }); }); describe('Cas de Concurrence et Race Conditions', () => { test('devrait gérer le chargement concurrent du même module', async () => { const scanner = new ContentScanner(); let loadCount = 0; global.fetch = async () => { loadCount++; await new Promise(resolve => setTimeout(resolve, Math.random() * 100)); return { ok: true, json: async () => ({ name: "Concurrent Module", vocabulary: { "test": "test" }, loadedAt: Date.now() }) }; }; // Lancer 5 chargements simultanés du même fichier const promises = Array(5).fill().map(() => scanner.loadJsonContent('concurrent.json') ); await Promise.all(promises); // Le module devrait être chargé une seule fois (idéalement) assert.ok(global.ContentModules.Concurrent); // Mais on peut avoir plusieurs appels fetch (c'est normal sans cache) }); test('devrait gérer les changements de configuration pendant le chargement', async () => { const config = new EnvConfig(); const scanner = new ContentScanner(); global.fetch = async () => { // Changer la config pendant le chargement config.set('USE_REMOTE_CONTENT', false); await new Promise(resolve => setTimeout(resolve, 50)); return { ok: true, json: async () => ({ name: "Racing", vocabulary: { "race": "course" } }) }; }; try { await scanner.loadJsonContent('racing.json'); } catch (error) { // L'erreur est acceptable dans ce cas de race condition } }); test('devrait gérer la navigation pendant le chargement de jeu', async () => { const loader = new GameLoader(); global.GameModules = { SlowGame: class { constructor(options) { // Simuler un jeu qui prend du temps à charger setTimeout(() => { this.loaded = true; }, 100); } start() {} destroy() { this.destroyed = true; } } }; global.ContentModules = { TestContent: { vocabulary: { "test": "test" } } }; // Démarrer le chargement const loadPromise = loader.loadGame('slow-game', 'test-content', {}); // Changer de jeu immédiatement (simuler navigation rapide) setTimeout(() => { loader.cleanup(); // L'utilisateur navigue ailleurs }, 10); try { await loadPromise; } catch (error) { // Acceptable si le jeu a été nettoyé pendant le chargement } }); }); describe('Cas de Navigateur Non Supporté', () => { test('devrait détecter l\'absence de fetch', async () => { const originalFetch = global.fetch; delete global.fetch; const scanner = new ContentScanner(); try { await scanner.loadJsonContent('no-fetch.json'); assert.fail('Should have thrown an error'); } catch (error) { assert.ok(error.message.includes('fetch is not defined') || error.message.includes('Impossible de charger JSON')); } global.fetch = originalFetch; }); test('devrait détecter l\'absence de crypto.subtle', async () => { const originalCrypto = global.crypto; delete global.crypto; const config = new EnvConfig(); try { await config.generateAWSSignature('GET', 'https://test.com'); assert.fail('Should have thrown an error'); } catch (error) { assert.ok(error.message.includes('crypto') || error.message.includes('subtle')); } global.crypto = originalCrypto; }); test('devrait gérer l\'absence de URLSearchParams', () => { const OriginalURLSearchParams = global.URLSearchParams; delete global.URLSearchParams; try { // Test qui utilise URLSearchParams const params = new URLSearchParams('test=value'); assert.fail('Should have thrown an error'); } catch (error) { assert.ok(error.message.includes('URLSearchParams is not defined')); } global.URLSearchParams = OriginalURLSearchParams; }); test('devrait gérer l\'absence de localStorage', () => { const originalLocalStorage = global.localStorage; delete global.localStorage; // Simuler un code qui utilise localStorage try { localStorage.setItem('test', 'value'); assert.fail('Should have thrown an error'); } catch (error) { assert.ok(error.message.includes('localStorage is not defined')); } global.localStorage = originalLocalStorage; }); }); describe('Cas de Données Invalides', () => { test('devrait gérer des noms de module avec caractères spéciaux', async () => { const scanner = new ContentScanner(); const weirdFilenames = [ 'module with spaces.json', 'module-with-🚀-emoji.json', 'module.with.dots.json', 'module_with_underscores.json', 'Module123WithNumbers.json' ]; for (const filename of weirdFilenames) { global.fetch = async () => ({ ok: true, json: async () => ({ name: `Module for ${filename}`, vocabulary: { "test": "test" } }) }); try { await scanner.loadJsonContent(filename); // Vérifier que le nom de module généré est valide const moduleName = scanner.jsonFilenameToModuleName(filename); assert.ok(typeof moduleName === 'string'); assert.ok(moduleName.length > 0); } catch (error) { // Certains cas peuvent légitimement échouer } } }); test('devrait gérer des types de données inattendus', async () => { const loader = new GameLoader(); const invalidContents = [ null, undefined, "string instead of object", 123, [], { vocabulary: null }, { vocabulary: "not an object" }, { vocabulary: [] }, { vocabulary: { /* empty */ } } ]; for (const content of invalidContents) { global.ContentModules.InvalidContent = content; try { loader.validateGameContent(content, 'test-game'); assert.fail(`Should have rejected: ${JSON.stringify(content)}`); } catch (error) { // Ces erreurs sont attendues assert.ok(error.message.includes('Contenu invalide') || error.message.includes('vocabulary')); } } }); test('devrait gérer des URLs malformées', async () => { const config = new EnvConfig(); const badUrls = [ '', 'not-a-url', 'http://', 'https://', 'ftp://invalid-protocol.com', 'http://[invalid-ipv6', 'http://toolong' + 'x'.repeat(2000) + '.com' ]; for (const badUrl of badUrls) { try { await config.generateAWSSignature('GET', badUrl); } catch (error) { // Ces erreurs sont attendues pour des URLs malformées } } }); }); describe('Cas de Limites Système', () => { test('devrait gérer un grand nombre de modules chargés', () => { // Simuler 1000 modules chargés global.ContentModules = {}; for (let i = 0; i < 1000; i++) { global.ContentModules[`Module${i}`] = { name: `Module ${i}`, vocabulary: { [`word${i}`]: `translation${i}` } }; } const loader = new GameLoader(); // Vérifier que l'accès reste rapide const startTime = Date.now(); const exists = loader.getContentModuleName('module500') in global.ContentModules; const endTime = Date.now(); assert.ok(endTime - startTime < 100); // Moins de 100ms }); test('devrait gérer l\'épuisement de la pile d\'appels', () => { const scanner = new ContentScanner(); // Créer une chaîne d'appels très profonde function deepRecursion(depth) { if (depth > 10000) { return scanner.toPascalCase('deep-recursion'); } return deepRecursion(depth + 1); } try { deepRecursion(0); } catch (error) { // Stack overflow est acceptable assert.ok(error.message.includes('Maximum call stack') || error.message.includes('stack')); } }); }); });