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>
203 lines
5.7 KiB
JavaScript
203 lines
5.7 KiB
JavaScript
import { readFileSync } from 'fs';
|
|
import { fileURLToPath } from 'url';
|
|
import path from 'path';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
// Helper pour créer un environnement DOM simulé
|
|
export function createMockDOM() {
|
|
// Mock global objects pour les tests
|
|
global.window = {
|
|
location: { protocol: 'http:', hostname: 'localhost', port: '8080' },
|
|
addEventListener: () => {},
|
|
dispatchEvent: () => {},
|
|
ContentModules: {},
|
|
GameModules: {},
|
|
fetch: async (url) => {
|
|
// Mock fetch pour les tests
|
|
if (url.includes('sbs-level-7-8-new.json')) {
|
|
return {
|
|
ok: true,
|
|
json: async () => ({
|
|
name: "SBS Level 7-8 (New)",
|
|
description: "Test content",
|
|
vocabulary: { "test": "test translation" }
|
|
})
|
|
};
|
|
}
|
|
throw new Error(`Mock fetch: URL not handled: ${url}`);
|
|
}
|
|
};
|
|
|
|
global.document = {
|
|
createElement: (tag) => ({
|
|
src: '',
|
|
onload: null,
|
|
onerror: null,
|
|
addEventListener: () => {}
|
|
}),
|
|
querySelector: () => null,
|
|
head: { appendChild: () => {} }
|
|
};
|
|
|
|
global.console = {
|
|
log: () => {},
|
|
warn: () => {},
|
|
error: () => {},
|
|
info: () => {},
|
|
debug: () => {}
|
|
};
|
|
|
|
// Mock pour logSh
|
|
global.logSh = (message, level = 'INFO') => {
|
|
// Silent dans les tests sauf si on veut debug
|
|
if (process.env.TEST_VERBOSE) {
|
|
console.log(`[TEST ${level}] ${message}`);
|
|
}
|
|
};
|
|
}
|
|
|
|
// Helper pour nettoyer l'environnement après tests
|
|
export function cleanupMockDOM() {
|
|
delete global.window;
|
|
delete global.document;
|
|
delete global.console;
|
|
delete global.logSh;
|
|
}
|
|
|
|
// Helper pour charger un fichier JS en tant que module dans les tests
|
|
export async function loadModuleForTest(relativePath) {
|
|
const fullPath = path.resolve(__dirname, '../../', relativePath);
|
|
|
|
// Lire le fichier et l'évaluer dans le contexte global
|
|
const code = readFileSync(fullPath, 'utf8');
|
|
|
|
// Remplacer les exports/require pour le navigateur
|
|
const browserCode = code
|
|
.replace(/export\s+default\s+/g, 'window.TestModule = ')
|
|
.replace(/export\s+\{([^}]+)\}/g, '')
|
|
.replace(/import\s+.*?from\s+['"].*?['"];?\s*/g, '');
|
|
|
|
// Évaluer dans le contexte global
|
|
eval(browserCode);
|
|
|
|
return global.window.TestModule;
|
|
}
|
|
|
|
// Helper pour créer des données de test
|
|
export function createTestContent() {
|
|
return {
|
|
name: "Test Content",
|
|
description: "Content pour les tests",
|
|
difficulty: "medium",
|
|
vocabulary: {
|
|
"hello": "bonjour",
|
|
"world": "monde",
|
|
"test": "test"
|
|
},
|
|
sentences: [
|
|
{
|
|
english: "Hello world",
|
|
chinese: "你好世界",
|
|
prononciation: "nǐ hǎo shì jiè"
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
// Helper pour simuler des requêtes réseau
|
|
export function createMockFetch(responses = {}) {
|
|
return async (url, options = {}) => {
|
|
if (responses[url]) {
|
|
const response = responses[url];
|
|
return {
|
|
ok: response.ok !== false,
|
|
status: response.status || 200,
|
|
json: async () => response.data,
|
|
text: async () => JSON.stringify(response.data)
|
|
};
|
|
}
|
|
|
|
// Réponse par défaut
|
|
return {
|
|
ok: false,
|
|
status: 404,
|
|
json: async () => ({ error: 'Not found in mock' }),
|
|
text: async () => 'Not found in mock'
|
|
};
|
|
};
|
|
}
|
|
|
|
// Helper pour attendre un délai
|
|
export function delay(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
// Helper pour capturer les logs
|
|
export function createLogCapture() {
|
|
const logs = [];
|
|
const originalLogSh = global.logSh;
|
|
|
|
global.logSh = (message, level = 'INFO') => {
|
|
logs.push({ message, level, timestamp: Date.now() });
|
|
if (originalLogSh) originalLogSh(message, level);
|
|
};
|
|
|
|
return {
|
|
logs,
|
|
restore: () => {
|
|
global.logSh = originalLogSh;
|
|
},
|
|
getLogs: (level) => level ? logs.filter(l => l.level === level) : logs,
|
|
clear: () => logs.length = 0
|
|
};
|
|
}
|
|
|
|
// Helper pour les assertions personnalisées
|
|
export function assertContains(actual, expected, message) {
|
|
if (!actual.includes(expected)) {
|
|
throw new Error(message || `Expected "${actual}" to contain "${expected}"`);
|
|
}
|
|
}
|
|
|
|
export function assertInstanceOf(actual, expectedClass, message) {
|
|
if (!(actual instanceof expectedClass)) {
|
|
throw new Error(message || `Expected instance of ${expectedClass.name}, got ${actual.constructor.name}`);
|
|
}
|
|
}
|
|
|
|
// Helper pour mocquer les timers
|
|
export function createTimerMock() {
|
|
const timers = new Map();
|
|
let timerId = 1;
|
|
|
|
const originalSetTimeout = global.setTimeout;
|
|
const originalClearTimeout = global.clearTimeout;
|
|
|
|
global.setTimeout = (callback, delay) => {
|
|
const id = timerId++;
|
|
timers.set(id, { callback, delay, type: 'timeout' });
|
|
return id;
|
|
};
|
|
|
|
global.clearTimeout = (id) => {
|
|
timers.delete(id);
|
|
};
|
|
|
|
return {
|
|
tick: (ms) => {
|
|
for (const [id, timer] of timers.entries()) {
|
|
if (timer.delay <= ms) {
|
|
timer.callback();
|
|
timers.delete(id);
|
|
}
|
|
}
|
|
},
|
|
restore: () => {
|
|
global.setTimeout = originalSetTimeout;
|
|
global.clearTimeout = originalClearTimeout;
|
|
timers.clear();
|
|
}
|
|
};
|
|
} |