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>
415 lines
15 KiB
JavaScript
415 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 système de navigation
|
|
describe('Système de Navigation - Tests d\'Intégration', () => {
|
|
let AppNavigation;
|
|
let logCapture;
|
|
let navigationEvents = [];
|
|
|
|
beforeEach(() => {
|
|
createMockDOM();
|
|
logCapture = createLogCapture();
|
|
navigationEvents = [];
|
|
|
|
// Mock plus complet pour DOM
|
|
global.document = {
|
|
...global.document,
|
|
getElementById: (id) => {
|
|
const elements = {
|
|
'app': { style: {}, innerHTML: '', classList: { add: () => {}, remove: () => {} } },
|
|
'home-page': { style: {}, innerHTML: '', classList: { add: () => {}, remove: () => {} } },
|
|
'games-page': { style: {}, innerHTML: '', classList: { add: () => {}, remove: () => {} } },
|
|
'levels-page': { style: {}, innerHTML: '', classList: { add: () => {}, remove: () => {} } },
|
|
'game-page': { style: {}, innerHTML: '', classList: { add: () => {}, remove: () => {} } },
|
|
'breadcrumb': { innerHTML: '', style: {} },
|
|
'network-status': { textContent: '', className: '', style: {} }
|
|
};
|
|
return elements[id] || null;
|
|
},
|
|
querySelectorAll: () => [],
|
|
addEventListener: (event, handler) => {
|
|
global.document[`_${event}_handler`] = handler;
|
|
}
|
|
};
|
|
|
|
// Mock window avec navigation
|
|
global.window = {
|
|
...global.window,
|
|
location: {
|
|
protocol: 'http:',
|
|
hostname: 'localhost',
|
|
port: '8080',
|
|
search: '',
|
|
href: 'http://localhost:8080/',
|
|
assign: (url) => {
|
|
global.window.location.href = url;
|
|
global.window.location.search = url.includes('?') ? url.split('?')[1] : '';
|
|
navigationEvents.push({ type: 'assign', url });
|
|
}
|
|
},
|
|
history: {
|
|
pushState: (state, title, url) => {
|
|
global.window.location.href = url;
|
|
global.window.location.search = url.includes('?') ? url.split('?')[1] : '';
|
|
navigationEvents.push({ type: 'pushState', state, title, url });
|
|
},
|
|
replaceState: (state, title, url) => {
|
|
global.window.location.href = url;
|
|
global.window.location.search = url.includes('?') ? url.split('?')[1] : '';
|
|
navigationEvents.push({ type: 'replaceState', state, title, url });
|
|
}
|
|
},
|
|
addEventListener: (event, handler) => {
|
|
global.window[`_${event}_handler`] = handler;
|
|
},
|
|
dispatchEvent: (event) => {
|
|
if (event.type === 'popstate' && global.window._popstate_handler) {
|
|
global.window._popstate_handler(event);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Mock URLSearchParams
|
|
global.URLSearchParams = class {
|
|
constructor(search) {
|
|
this.params = new Map();
|
|
if (search) {
|
|
search.split('&').forEach(param => {
|
|
const [key, value] = param.split('=');
|
|
if (key && value) {
|
|
this.params.set(decodeURIComponent(key), decodeURIComponent(value));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
get(key) {
|
|
return this.params.get(key);
|
|
}
|
|
|
|
set(key, value) {
|
|
this.params.set(key, value);
|
|
}
|
|
|
|
toString() {
|
|
const pairs = [];
|
|
for (const [key, value] of this.params) {
|
|
pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
|
|
}
|
|
return pairs.join('&');
|
|
}
|
|
};
|
|
|
|
// Charger AppNavigation
|
|
const navPath = path.resolve(process.cwd(), 'js/core/navigation.js');
|
|
const code = readFileSync(navPath, 'utf8');
|
|
|
|
const testCode = code
|
|
.replace(/window\./g, 'global.')
|
|
.replace(/typeof window !== 'undefined'/g, 'true');
|
|
|
|
eval(testCode);
|
|
AppNavigation = global.AppNavigation;
|
|
});
|
|
|
|
afterEach(() => {
|
|
logCapture.restore();
|
|
cleanupMockDOM();
|
|
});
|
|
|
|
describe('Initialisation et configuration', () => {
|
|
test('devrait créer une instance AppNavigation', () => {
|
|
const nav = new AppNavigation();
|
|
|
|
assert.ok(nav instanceof AppNavigation);
|
|
assert.ok(Array.isArray(nav.navigationHistory));
|
|
assert.equal(nav.navigationHistory.length, 0);
|
|
});
|
|
|
|
test('devrait charger la configuration par défaut', () => {
|
|
const nav = new AppNavigation();
|
|
const config = nav.getDefaultConfig();
|
|
|
|
assert.ok(config);
|
|
assert.ok(config.games);
|
|
assert.ok(config.content);
|
|
assert.ok(config.ui);
|
|
});
|
|
});
|
|
|
|
describe('Analyse d\'URL et paramètres', () => {
|
|
test('parseURLParams devrait extraire les paramètres correctement', () => {
|
|
const nav = new AppNavigation();
|
|
|
|
global.window.location.search = '?page=games&game=whack&content=sbs8';
|
|
const params = nav.parseURLParams();
|
|
|
|
assert.equal(params.page, 'games');
|
|
assert.equal(params.game, 'whack');
|
|
assert.equal(params.content, 'sbs8');
|
|
});
|
|
|
|
test('parseURLParams devrait retourner objet vide si pas de paramètres', () => {
|
|
const nav = new AppNavigation();
|
|
|
|
global.window.location.search = '';
|
|
const params = nav.parseURLParams();
|
|
|
|
assert.equal(Object.keys(params).length, 0);
|
|
});
|
|
|
|
test('getCurrentRoute devrait identifier la route actuelle', () => {
|
|
const nav = new AppNavigation();
|
|
|
|
// Test route home
|
|
global.window.location.search = '';
|
|
assert.equal(nav.getCurrentRoute(), 'home');
|
|
|
|
// Test route games
|
|
global.window.location.search = '?page=games';
|
|
assert.equal(nav.getCurrentRoute(), 'games');
|
|
|
|
// Test route play
|
|
global.window.location.search = '?page=play&game=whack&content=sbs8';
|
|
assert.equal(nav.getCurrentRoute(), 'play');
|
|
});
|
|
});
|
|
|
|
describe('Navigation et routage', () => {
|
|
test('navigateTo devrait mettre à jour l\'URL et l\'historique', () => {
|
|
const nav = new AppNavigation();
|
|
|
|
nav.navigateTo('games', { category: 'vocabulary' });
|
|
|
|
assert.equal(navigationEvents.length, 1);
|
|
assert.equal(navigationEvents[0].type, 'pushState');
|
|
assert.ok(navigationEvents[0].url.includes('page=games'));
|
|
assert.ok(navigationEvents[0].url.includes('category=vocabulary'));
|
|
|
|
assert.equal(nav.navigationHistory.length, 1);
|
|
assert.equal(nav.navigationHistory[0].page, 'games');
|
|
});
|
|
|
|
test('navigateToGame devrait créer l\'URL correcte pour un jeu', () => {
|
|
const nav = new AppNavigation();
|
|
|
|
nav.navigateToGame('whack-a-mole', 'sbs-content');
|
|
|
|
const lastEvent = navigationEvents[navigationEvents.length - 1];
|
|
assert.ok(lastEvent.url.includes('page=play'));
|
|
assert.ok(lastEvent.url.includes('game=whack-a-mole'));
|
|
assert.ok(lastEvent.url.includes('content=sbs-content'));
|
|
});
|
|
|
|
test('goBack devrait revenir à la page précédente', () => {
|
|
const nav = new AppNavigation();
|
|
|
|
// Naviguer vers quelques pages
|
|
nav.navigateTo('games');
|
|
nav.navigateTo('levels', { game: 'whack' });
|
|
nav.navigateTo('play', { game: 'whack', content: 'sbs8' });
|
|
|
|
assert.equal(nav.navigationHistory.length, 3);
|
|
|
|
// Revenir en arrière
|
|
nav.goBack();
|
|
|
|
assert.equal(nav.navigationHistory.length, 2);
|
|
const lastEvent = navigationEvents[navigationEvents.length - 1];
|
|
assert.ok(lastEvent.url.includes('page=levels'));
|
|
});
|
|
|
|
test('handlePopState devrait gérer les événements navigateur', () => {
|
|
const nav = new AppNavigation();
|
|
|
|
// Simuler un événement popstate
|
|
const mockEvent = {
|
|
state: { page: 'games', timestamp: Date.now() }
|
|
};
|
|
|
|
nav.handlePopState(mockEvent);
|
|
|
|
// Devrait déclencher une mise à jour de la page
|
|
const logs = logCapture.getLogs();
|
|
assert.ok(logs.some(log => log.message.includes('Navigation')));
|
|
});
|
|
});
|
|
|
|
describe('Gestion des pages', () => {
|
|
test('showPage devrait afficher la page correcte', () => {
|
|
const nav = new AppNavigation();
|
|
|
|
nav.showPage('games');
|
|
|
|
// Vérifier que les éléments DOM sont mis à jour
|
|
const gamesPage = global.document.getElementById('games-page');
|
|
const homePage = global.document.getElementById('home-page');
|
|
|
|
// Les styles devraient être mis à jour pour afficher/masquer les pages
|
|
assert.ok(gamesPage); // Should exist
|
|
assert.ok(homePage); // Should exist
|
|
});
|
|
|
|
test('updateBreadcrumb devrait mettre à jour le fil d\'Ariane', () => {
|
|
const nav = new AppNavigation();
|
|
|
|
nav.updateBreadcrumb(['Accueil', 'Jeux', 'Whack-a-Mole']);
|
|
|
|
const breadcrumb = global.document.getElementById('breadcrumb');
|
|
assert.ok(breadcrumb.innerHTML.length > 0);
|
|
});
|
|
});
|
|
|
|
describe('État de connectivité', () => {
|
|
test('updateNetworkStatus devrait mettre à jour l\'indicateur réseau', () => {
|
|
const nav = new AppNavigation();
|
|
|
|
nav.updateNetworkStatus('online');
|
|
|
|
const networkStatus = global.document.getElementById('network-status');
|
|
assert.ok(networkStatus.className.includes('online') || networkStatus.textContent.includes('online'));
|
|
});
|
|
|
|
test('devrait gérer différents états de réseau', () => {
|
|
const nav = new AppNavigation();
|
|
|
|
const states = ['online', 'offline', 'connecting'];
|
|
|
|
states.forEach(state => {
|
|
nav.updateNetworkStatus(state);
|
|
const networkStatus = global.document.getElementById('network-status');
|
|
assert.ok(networkStatus.className.includes(state) || networkStatus.textContent);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Validation de routes', () => {
|
|
test('isValidRoute devrait valider les routes connues', () => {
|
|
const nav = new AppNavigation();
|
|
|
|
assert.equal(nav.isValidRoute('home'), true);
|
|
assert.equal(nav.isValidRoute('games'), true);
|
|
assert.equal(nav.isValidRoute('levels'), true);
|
|
assert.equal(nav.isValidRoute('play'), true);
|
|
assert.equal(nav.isValidRoute('invalid-route'), false);
|
|
});
|
|
|
|
test('validateParams devrait valider les paramètres requis', () => {
|
|
const nav = new AppNavigation();
|
|
|
|
// Route play nécessite game et content
|
|
const validParams = { page: 'play', game: 'whack', content: 'sbs8' };
|
|
const invalidParams1 = { page: 'play', game: 'whack' }; // manque content
|
|
const invalidParams2 = { page: 'play', content: 'sbs8' }; // manque game
|
|
|
|
assert.equal(nav.validateParams(validParams), true);
|
|
assert.equal(nav.validateParams(invalidParams1), false);
|
|
assert.equal(nav.validateParams(invalidParams2), false);
|
|
});
|
|
});
|
|
|
|
describe('Intégration avec le système de jeu', () => {
|
|
test('devrait intégrer avec GameLoader pour charger les jeux', () => {
|
|
const nav = new AppNavigation();
|
|
|
|
// Mock GameLoader
|
|
global.GameLoader = class {
|
|
async loadGame(gameType, contentType, container) {
|
|
return {
|
|
gameType,
|
|
contentType,
|
|
started: false,
|
|
start: function() { this.started = true; }
|
|
};
|
|
}
|
|
};
|
|
|
|
// Simuler une navigation vers un jeu
|
|
global.window.location.search = '?page=play&game=whack&content=sbs8';
|
|
|
|
// L'initialisation devrait déclencher le chargement du jeu
|
|
nav.init();
|
|
|
|
const logs = logCapture.getLogs();
|
|
assert.ok(logs.some(log => log.message.includes('Navigation') || log.message.includes('Initialisation')));
|
|
});
|
|
});
|
|
|
|
describe('Gestion des erreurs de navigation', () => {
|
|
test('devrait gérer les URLs invalides gracieusement', () => {
|
|
const nav = new AppNavigation();
|
|
|
|
global.window.location.search = '?page=invalid¶m=malformed%';
|
|
|
|
try {
|
|
nav.init();
|
|
// Ne devrait pas lever d'erreur
|
|
assert.ok(true);
|
|
} catch (error) {
|
|
assert.fail(`Navigation should handle invalid URLs gracefully: ${error.message}`);
|
|
}
|
|
});
|
|
|
|
test('devrait rediriger vers home si route invalide', () => {
|
|
const nav = new AppNavigation();
|
|
|
|
nav.navigateTo('invalid-page');
|
|
|
|
// Devrait rediriger vers home
|
|
const lastEvent = navigationEvents[navigationEvents.length - 1];
|
|
assert.ok(lastEvent.url.includes('page=home') || !lastEvent.url.includes('page='));
|
|
});
|
|
});
|
|
|
|
describe('Historique de navigation', () => {
|
|
test('devrait maintenir un historique de navigation', () => {
|
|
const nav = new AppNavigation();
|
|
|
|
nav.navigateTo('games');
|
|
nav.navigateTo('levels', { game: 'whack' });
|
|
nav.navigateTo('play', { game: 'whack', content: 'sbs8' });
|
|
|
|
assert.equal(nav.navigationHistory.length, 3);
|
|
|
|
const history = nav.getNavigationHistory();
|
|
assert.equal(history.length, 3);
|
|
assert.equal(history[0].page, 'games');
|
|
assert.equal(history[1].page, 'levels');
|
|
assert.equal(history[2].page, 'play');
|
|
});
|
|
|
|
test('devrait limiter la taille de l\'historique', () => {
|
|
const nav = new AppNavigation();
|
|
|
|
// Naviguer vers de nombreuses pages
|
|
for (let i = 0; i < 25; i++) {
|
|
nav.navigateTo('games', { test: i });
|
|
}
|
|
|
|
// L'historique devrait être limité
|
|
assert.ok(nav.navigationHistory.length <= 20);
|
|
});
|
|
});
|
|
|
|
describe('Événements personnalisés', () => {
|
|
test('devrait émettre des événements de navigation', () => {
|
|
const nav = new AppNavigation();
|
|
|
|
let eventReceived = false;
|
|
global.window.addEventListener = (event, handler) => {
|
|
if (event === 'navigationChange') {
|
|
eventReceived = true;
|
|
}
|
|
};
|
|
|
|
nav.navigateTo('games');
|
|
|
|
// L'événement devrait être émis (ou préparé pour émission)
|
|
assert.ok(navigationEvents.length > 0);
|
|
});
|
|
});
|
|
}); |