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>
331 lines
12 KiB
JavaScript
331 lines
12 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 { sampleJSONContent, gameTestData } from '../fixtures/content-samples.js';
|
|
import { readFileSync } from 'fs';
|
|
import path from 'path';
|
|
|
|
describe('GameLoader - Tests Unitaires', () => {
|
|
let GameLoader;
|
|
let logCapture;
|
|
|
|
beforeEach(() => {
|
|
createMockDOM();
|
|
logCapture = createLogCapture();
|
|
|
|
// Mock GameModules avec des jeux de test
|
|
global.GameModules = {
|
|
WhackAMole: class {
|
|
constructor(options) {
|
|
this.container = options.container;
|
|
this.content = options.content;
|
|
this.onScoreUpdate = options.onScoreUpdate;
|
|
this.onGameEnd = options.onGameEnd;
|
|
}
|
|
start() { this.started = true; }
|
|
destroy() { this.destroyed = true; }
|
|
restart() { this.restarted = true; }
|
|
},
|
|
MemoryMatch: class {
|
|
constructor(options) {
|
|
this.container = options.container;
|
|
this.content = options.content;
|
|
}
|
|
start() { this.started = true; }
|
|
destroy() { this.destroyed = true; }
|
|
},
|
|
QuizGame: class {
|
|
constructor(options) {
|
|
this.container = options.container;
|
|
this.content = options.content;
|
|
}
|
|
start() { this.started = true; }
|
|
destroy() { this.destroyed = true; }
|
|
}
|
|
};
|
|
|
|
// Mock ContentModules
|
|
global.ContentModules = {
|
|
TestContent: sampleJSONContent
|
|
};
|
|
|
|
// Charger GameLoader
|
|
const loaderPath = path.resolve(process.cwd(), 'js/core/game-loader.js');
|
|
const code = readFileSync(loaderPath, 'utf8');
|
|
|
|
const testCode = code
|
|
.replace(/window\./g, 'global.')
|
|
.replace(/typeof window !== 'undefined'/g, 'true');
|
|
|
|
eval(testCode);
|
|
GameLoader = global.GameLoader;
|
|
});
|
|
|
|
afterEach(() => {
|
|
logCapture.restore();
|
|
cleanupMockDOM();
|
|
});
|
|
|
|
describe('Construction et initialisation', () => {
|
|
test('devrait créer une instance GameLoader', () => {
|
|
const loader = new GameLoader();
|
|
|
|
assert.ok(loader instanceof GameLoader);
|
|
assert.equal(loader.currentGame, null);
|
|
assert.equal(loader.currentGameType, null);
|
|
});
|
|
});
|
|
|
|
describe('Chargement de jeux', () => {
|
|
test('loadGame devrait charger un jeu avec succès', async () => {
|
|
const loader = new GameLoader();
|
|
const container = { innerHTML: '' };
|
|
|
|
const scoreCallback = (score) => {};
|
|
const endCallback = () => {};
|
|
|
|
const game = await loader.loadGame(
|
|
'whack-a-mole',
|
|
'test-content',
|
|
container,
|
|
scoreCallback,
|
|
endCallback
|
|
);
|
|
|
|
assert.ok(game);
|
|
assert.ok(game instanceof global.GameModules.WhackAMole);
|
|
assert.equal(game.container, container);
|
|
assert.equal(game.content, sampleJSONContent);
|
|
assert.equal(loader.currentGame, game);
|
|
assert.equal(loader.currentGameType, 'whack-a-mole');
|
|
});
|
|
|
|
test('loadGame devrait nettoyer le jeu précédent', async () => {
|
|
const loader = new GameLoader();
|
|
const container = { innerHTML: '' };
|
|
|
|
// Charger un premier jeu
|
|
const game1 = await loader.loadGame('whack-a-mole', 'test-content', container);
|
|
game1.start();
|
|
|
|
// Charger un second jeu
|
|
const game2 = await loader.loadGame('memory-match', 'test-content', container);
|
|
|
|
assert.equal(game1.destroyed, true);
|
|
assert.equal(loader.currentGame, game2);
|
|
assert.ok(game2 instanceof global.GameModules.MemoryMatch);
|
|
});
|
|
|
|
test('loadGame devrait échouer si le jeu n\'existe pas', async () => {
|
|
const loader = new GameLoader();
|
|
const container = { innerHTML: '' };
|
|
|
|
try {
|
|
await loader.loadGame('nonexistent-game', 'test-content', container);
|
|
assert.fail('Should have thrown an error');
|
|
} catch (error) {
|
|
assert.ok(error.message.includes('Jeu non trouvé'));
|
|
}
|
|
});
|
|
|
|
test('loadGame devrait échouer si le contenu n\'existe pas', async () => {
|
|
const loader = new GameLoader();
|
|
const container = { innerHTML: '' };
|
|
|
|
try {
|
|
await loader.loadGame('whack-a-mole', 'nonexistent-content', container);
|
|
assert.fail('Should have thrown an error');
|
|
} catch (error) {
|
|
assert.ok(error.message.includes('Contenu non trouvé'));
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Conversion de noms', () => {
|
|
test('getGameClassName devrait convertir les noms de jeu', () => {
|
|
const loader = new GameLoader();
|
|
|
|
assert.equal(loader.getGameClassName('whack-a-mole'), 'WhackAMole');
|
|
assert.equal(loader.getGameClassName('memory-match'), 'MemoryMatch');
|
|
assert.equal(loader.getGameClassName('quiz-game'), 'QuizGame');
|
|
assert.equal(loader.getGameClassName('fill-the-blank'), 'FillTheBlank');
|
|
});
|
|
|
|
test('getContentModuleName devrait convertir les noms de contenu', () => {
|
|
const loader = new GameLoader();
|
|
|
|
assert.equal(loader.getContentModuleName('test-content'), 'TestContent');
|
|
assert.equal(loader.getContentModuleName('sbs-level-7-8-new'), 'SBSLevel78New');
|
|
assert.equal(loader.getContentModuleName('english-class-demo'), 'EnglishClassDemo');
|
|
});
|
|
|
|
test('toPascalCase devrait convertir correctement', () => {
|
|
const loader = new GameLoader();
|
|
|
|
assert.equal(loader.toPascalCase('hello-world'), 'HelloWorld');
|
|
assert.equal(loader.toPascalCase('test-file-name'), 'TestFileName');
|
|
assert.equal(loader.toPascalCase('simple'), 'Simple');
|
|
assert.equal(loader.toPascalCase(''), '');
|
|
});
|
|
});
|
|
|
|
describe('Gestion du cycle de vie des jeux', () => {
|
|
test('cleanup devrait détruire le jeu actuel', () => {
|
|
const loader = new GameLoader();
|
|
const mockGame = {
|
|
destroy: () => { mockGame.destroyed = true; },
|
|
destroyed: false
|
|
};
|
|
|
|
loader.currentGame = mockGame;
|
|
loader.currentGameType = 'test-game';
|
|
|
|
loader.cleanup();
|
|
|
|
assert.equal(mockGame.destroyed, true);
|
|
assert.equal(loader.currentGame, null);
|
|
assert.equal(loader.currentGameType, null);
|
|
});
|
|
|
|
test('cleanup devrait être sûr si pas de jeu actuel', () => {
|
|
const loader = new GameLoader();
|
|
|
|
// Ne devrait pas lever d'erreur
|
|
loader.cleanup();
|
|
|
|
assert.equal(loader.currentGame, null);
|
|
assert.equal(loader.currentGameType, null);
|
|
});
|
|
|
|
test('restartCurrentGame devrait redémarrer le jeu actuel', () => {
|
|
const loader = new GameLoader();
|
|
const mockGame = {
|
|
restart: () => { mockGame.restarted = true; },
|
|
restarted: false
|
|
};
|
|
|
|
loader.currentGame = mockGame;
|
|
|
|
loader.restartCurrentGame();
|
|
|
|
assert.equal(mockGame.restarted, true);
|
|
});
|
|
|
|
test('restartCurrentGame devrait échouer si pas de jeu actuel', () => {
|
|
const loader = new GameLoader();
|
|
|
|
try {
|
|
loader.restartCurrentGame();
|
|
assert.fail('Should have thrown an error');
|
|
} catch (error) {
|
|
assert.ok(error.message.includes('Aucun jeu actuel'));
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Validation et contraintes', () => {
|
|
test('validateGameContent devrait valider le contenu minimal', () => {
|
|
const loader = new GameLoader();
|
|
|
|
const validContent = { vocabulary: { 'test': 'test' } };
|
|
const invalidContent1 = {};
|
|
const invalidContent2 = { vocabulary: {} };
|
|
const invalidContent3 = null;
|
|
|
|
assert.doesNotThrow(() => loader.validateGameContent(validContent, 'test-game'));
|
|
|
|
assert.throws(() => loader.validateGameContent(invalidContent1, 'test-game'));
|
|
assert.throws(() => loader.validateGameContent(invalidContent2, 'test-game'));
|
|
assert.throws(() => loader.validateGameContent(invalidContent3, 'test-game'));
|
|
});
|
|
|
|
test('checkGameRequirements devrait vérifier les exigences spécifiques', () => {
|
|
const loader = new GameLoader();
|
|
|
|
// Contenu avec vocabulaire suffisant
|
|
const goodContent = {
|
|
vocabulary: gameTestData.whackAMole.vocabulary
|
|
};
|
|
|
|
// Contenu avec vocabulaire insuffisant
|
|
const poorContent = {
|
|
vocabulary: { 'only': 'one' }
|
|
};
|
|
|
|
assert.doesNotThrow(() => loader.checkGameRequirements(goodContent, 'whack-a-mole'));
|
|
assert.throws(() => loader.checkGameRequirements(poorContent, 'whack-a-mole'));
|
|
});
|
|
});
|
|
|
|
describe('Gestion des erreurs', () => {
|
|
test('devrait logger les erreurs de chargement', async () => {
|
|
const loader = new GameLoader();
|
|
const container = { innerHTML: '' };
|
|
|
|
try {
|
|
await loader.loadGame('nonexistent-game', 'test-content', container);
|
|
} catch (error) {
|
|
// Expected
|
|
}
|
|
|
|
const errorLogs = logCapture.getLogs('ERROR');
|
|
assert.ok(errorLogs.length > 0);
|
|
assert.ok(errorLogs.some(log => log.message.includes('Erreur lors du chargement')));
|
|
});
|
|
|
|
test('devrait gérer les erreurs de construction de jeu', async () => {
|
|
const loader = new GameLoader();
|
|
const container = { innerHTML: '' };
|
|
|
|
// Mock un jeu qui lève une erreur à la construction
|
|
global.GameModules.BrokenGame = class {
|
|
constructor() {
|
|
throw new Error('Construction failed');
|
|
}
|
|
};
|
|
|
|
try {
|
|
await loader.loadGame('broken-game', 'test-content', container);
|
|
assert.fail('Should have thrown an error');
|
|
} catch (error) {
|
|
assert.ok(error.message.includes('Erreur lors de la création'));
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('État et informations', () => {
|
|
test('getCurrentGameInfo devrait retourner les informations du jeu actuel', () => {
|
|
const loader = new GameLoader();
|
|
const mockGame = {};
|
|
|
|
loader.currentGame = mockGame;
|
|
loader.currentGameType = 'test-game';
|
|
|
|
const info = loader.getCurrentGameInfo();
|
|
|
|
assert.equal(info.gameType, 'test-game');
|
|
assert.equal(info.game, mockGame);
|
|
assert.equal(typeof info.loadTime, 'number');
|
|
});
|
|
|
|
test('getCurrentGameInfo devrait retourner null si pas de jeu', () => {
|
|
const loader = new GameLoader();
|
|
|
|
const info = loader.getCurrentGameInfo();
|
|
|
|
assert.equal(info, null);
|
|
});
|
|
|
|
test('isGameLoaded devrait retourner l\'état correct', () => {
|
|
const loader = new GameLoader();
|
|
|
|
assert.equal(loader.isGameLoaded(), false);
|
|
|
|
loader.currentGame = {};
|
|
assert.equal(loader.isGameLoaded(), true);
|
|
|
|
loader.currentGame = null;
|
|
assert.equal(loader.isGameLoaded(), false);
|
|
});
|
|
});
|
|
}); |