- Complete test infrastructure with runners, helpers, and fixtures - Unit tests for core modules: EnvConfig, ContentScanner, GameLoader - Integration tests for proxy, content loading, and navigation - Edge case tests covering data corruption, network failures, security - Stress tests with 100+ concurrent requests and performance monitoring - Test fixtures with malicious content samples and edge case data - Comprehensive README with usage instructions and troubleshooting 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
313 lines
11 KiB
JavaScript
313 lines
11 KiB
JavaScript
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { createMockDOM, cleanupMockDOM, createMockFetch, createLogCapture } from '../utils/test-helpers.js';
|
|
import { sampleJSONContent, moduleNameMappingTests, networkTestResponses } from '../fixtures/content-samples.js';
|
|
import { readFileSync } from 'fs';
|
|
import path from 'path';
|
|
|
|
describe('ContentScanner - Tests Unitaires', () => {
|
|
let ContentScanner;
|
|
let logCapture;
|
|
|
|
beforeEach(() => {
|
|
createMockDOM();
|
|
logCapture = createLogCapture();
|
|
|
|
// Mock EnvConfig
|
|
global.envConfig = {
|
|
isRemoteContentEnabled: () => true,
|
|
get: (key) => {
|
|
const defaults = {
|
|
'REMOTE_TIMEOUT': 3000,
|
|
'TRY_REMOTE_FIRST': true,
|
|
'FALLBACK_TO_LOCAL': true
|
|
};
|
|
return defaults[key];
|
|
}
|
|
};
|
|
|
|
// Charger ContentScanner
|
|
const scannerPath = path.resolve(process.cwd(), 'js/core/content-scanner.js');
|
|
const code = readFileSync(scannerPath, 'utf8');
|
|
|
|
const testCode = code
|
|
.replace(/window\./g, 'global.')
|
|
.replace(/typeof window !== 'undefined'/g, 'true');
|
|
|
|
eval(testCode);
|
|
ContentScanner = global.ContentScanner;
|
|
});
|
|
|
|
afterEach(() => {
|
|
logCapture.restore();
|
|
cleanupMockDOM();
|
|
});
|
|
|
|
describe('Construction et initialisation', () => {
|
|
test('devrait créer une instance ContentScanner', () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
assert.ok(scanner instanceof ContentScanner);
|
|
assert.ok(scanner.discoveredContent instanceof Map);
|
|
assert.equal(scanner.contentDirectory, 'js/content/');
|
|
});
|
|
|
|
test('devrait initialiser avec envConfig', () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
assert.ok(scanner.envConfig);
|
|
assert.equal(scanner.envConfig.isRemoteContentEnabled(), true);
|
|
});
|
|
});
|
|
|
|
describe('Conversion de noms de modules', () => {
|
|
test('jsonFilenameToModuleName devrait convertir correctement', () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
moduleNameMappingTests.forEach(({ filename, expected }) => {
|
|
const result = scanner.jsonFilenameToModuleName(`http://example.com/${filename}`);
|
|
assert.equal(result, expected, `Failed for ${filename} -> ${expected}`);
|
|
});
|
|
});
|
|
|
|
test('toPascalCase devrait convertir les chaînes avec tirets', () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
assert.equal(scanner.toPascalCase('hello-world'), 'HelloWorld');
|
|
assert.equal(scanner.toPascalCase('test-file-name'), 'TestFileName');
|
|
assert.equal(scanner.toPascalCase('simple'), 'Simple');
|
|
});
|
|
|
|
test('extractContentId devrait extraire l\'ID du fichier', () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
assert.equal(scanner.extractContentId('test-content.js'), 'test-content');
|
|
assert.equal(scanner.extractContentId('test-content.json'), 'test-content');
|
|
assert.equal(scanner.extractContentId('simple.js'), 'simple');
|
|
});
|
|
});
|
|
|
|
describe('Chargement de contenu JSON', () => {
|
|
test('loadJsonContent devrait charger depuis le distant en priorité', async () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
global.fetch = createMockFetch(networkTestResponses);
|
|
|
|
await scanner.loadJsonContent('sbs-level-7-8-new.json');
|
|
|
|
// Vérifier que le module est chargé
|
|
assert.ok(global.ContentModules);
|
|
assert.ok(global.ContentModules.SBSLevel78New);
|
|
assert.equal(global.ContentModules.SBSLevel78New.name, sampleJSONContent.name);
|
|
|
|
// Vérifier les logs
|
|
const logs = logCapture.getLogs('INFO');
|
|
assert.ok(logs.some(log => log.message.includes('Chargement JSON: sbs-level-7-8-new.json')));
|
|
});
|
|
|
|
test('loadJsonContent devrait fallback vers local si distant échoue', async () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
// Mock fetch qui échoue pour le distant mais réussit pour le local
|
|
global.fetch = async (url) => {
|
|
if (url.includes('localhost:8083')) {
|
|
throw new Error('Network error');
|
|
}
|
|
if (url.includes('js/content/')) {
|
|
return {
|
|
ok: true,
|
|
json: async () => sampleJSONContent
|
|
};
|
|
}
|
|
throw new Error('Unknown URL');
|
|
};
|
|
|
|
await scanner.loadJsonContent('test-content.json');
|
|
|
|
// Vérifier que le module est chargé depuis local
|
|
assert.ok(global.ContentModules.TestContent);
|
|
|
|
const logs = logCapture.getLogs('WARN');
|
|
assert.ok(logs.some(log => log.message.includes('Distant échoué')));
|
|
});
|
|
|
|
test('loadJsonContent devrait échouer si toutes les méthodes échouent', async () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
global.fetch = async () => {
|
|
throw new Error('All methods failed');
|
|
};
|
|
|
|
try {
|
|
await scanner.loadJsonContent('nonexistent.json');
|
|
assert.fail('Should have thrown an error');
|
|
} catch (error) {
|
|
assert.ok(error.message.includes('Impossible de charger JSON'));
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Test de connectivité distante', () => {
|
|
test('shouldTryRemote devrait retourner true si configuré', () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
// Mock window.location pour protocol http
|
|
global.window.location.protocol = 'http:';
|
|
|
|
assert.equal(scanner.shouldTryRemote(), true);
|
|
});
|
|
|
|
test('shouldTryRemote devrait retourner false si pas configuré', () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
global.envConfig.isRemoteContentEnabled = () => false;
|
|
|
|
assert.equal(scanner.shouldTryRemote(), false);
|
|
});
|
|
|
|
test('tryRemoteLoad devrait utiliser le proxy local', async () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
global.fetch = createMockFetch(networkTestResponses);
|
|
|
|
const result = await scanner.tryRemoteLoad('sbs-level-7-8-new.json');
|
|
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.source, 'remote');
|
|
});
|
|
});
|
|
|
|
describe('Scan de fichiers de contenu', () => {
|
|
test('scanContentFile devrait traiter un fichier JSON', async () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
global.fetch = createMockFetch(networkTestResponses);
|
|
|
|
const result = await scanner.scanContentFile('sbs-level-7-8-new.json');
|
|
|
|
assert.ok(result);
|
|
assert.equal(result.id, 'sbs-level-7-8-new');
|
|
assert.equal(result.filename, 'sbs-level-7-8-new.json');
|
|
assert.ok(result.name);
|
|
});
|
|
|
|
test('scanContentFile devrait traiter un fichier JS', async () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
// Mock pour loadScript
|
|
scanner.loadScript = async () => {
|
|
global.ContentModules = global.ContentModules || {};
|
|
global.ContentModules.TestContent = {
|
|
name: 'Test Content',
|
|
vocabulary: { 'test': 'test' }
|
|
};
|
|
};
|
|
|
|
const result = await scanner.scanContentFile('test-content.js');
|
|
|
|
assert.ok(result);
|
|
assert.equal(result.id, 'test-content');
|
|
assert.equal(result.filename, 'test-content.js');
|
|
});
|
|
|
|
test('scanContentFile devrait échouer si module non trouvé', async () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
global.fetch = async () => ({ ok: true, json: async () => ({}) });
|
|
|
|
try {
|
|
await scanner.scanContentFile('nonexistent.json');
|
|
assert.fail('Should have thrown an error');
|
|
} catch (error) {
|
|
assert.ok(error.message.includes('Impossible de charger'));
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Extraction d\'informations de contenu', () => {
|
|
test('extractContentInfo devrait extraire les métadonnées', () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
const module = {
|
|
name: 'Test Module',
|
|
description: 'Test description',
|
|
difficulty: 'easy',
|
|
version: '2.0'
|
|
};
|
|
|
|
const info = scanner.extractContentInfo(module, 'test-id', 'test.js');
|
|
|
|
assert.equal(info.id, 'test-id');
|
|
assert.equal(info.filename, 'test.js');
|
|
assert.equal(info.name, 'Test Module');
|
|
assert.equal(info.description, 'Test description');
|
|
assert.equal(info.difficulty, 'easy');
|
|
assert.equal(info.enabled, true);
|
|
assert.equal(info.metadata.version, '2.0');
|
|
});
|
|
|
|
test('extractContentInfo devrait utiliser des valeurs par défaut', () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
const module = {};
|
|
const info = scanner.extractContentInfo(module, 'test-id', 'test.js');
|
|
|
|
assert.equal(info.difficulty, 'medium');
|
|
assert.equal(info.description, 'Contenu automatiquement détecté');
|
|
assert.equal(info.metadata.version, '1.0');
|
|
assert.equal(info.metadata.format, 'legacy');
|
|
});
|
|
});
|
|
|
|
describe('Gestion des erreurs et logs', () => {
|
|
test('updateConnectionStatus devrait émettre un événement', () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
let eventReceived = false;
|
|
global.window.dispatchEvent = (event) => {
|
|
if (event.type === 'contentConnectionStatus') {
|
|
eventReceived = true;
|
|
}
|
|
};
|
|
|
|
scanner.updateConnectionStatus('online', 'Test connection');
|
|
|
|
assert.equal(eventReceived, true);
|
|
});
|
|
|
|
test('devrait logger les erreurs appropriées', async () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
global.fetch = async () => {
|
|
throw new Error('Test error');
|
|
};
|
|
|
|
try {
|
|
await scanner.loadJsonContent('test.json');
|
|
} catch (error) {
|
|
// Expected
|
|
}
|
|
|
|
const errorLogs = logCapture.getLogs('WARN');
|
|
assert.ok(errorLogs.length > 0);
|
|
assert.ok(errorLogs.some(log => log.message.includes('Test error')));
|
|
});
|
|
});
|
|
|
|
describe('Discovery de fichiers', () => {
|
|
test('tryCommonFiles devrait essayer les fichiers connus', async () => {
|
|
const scanner = new ContentScanner();
|
|
|
|
global.fetch = createMockFetch({
|
|
'http://localhost:8083/do-proxy/sbs-level-7-8-new.json': { ok: true },
|
|
'http://localhost:8083/do-proxy/english-class-demo.json': { ok: true }
|
|
});
|
|
|
|
const files = await scanner.tryCommonFiles();
|
|
|
|
assert.ok(Array.isArray(files));
|
|
assert.ok(files.includes('sbs-level-7-8-new.json'));
|
|
assert.ok(files.includes('english-class-demo.json'));
|
|
});
|
|
});
|
|
}); |