Class_generator/Legacy/tests/integration/content-loading-flow.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

412 lines
15 KiB
JavaScript

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