/** * Tests unitaires pour le générateur de PNJ Traveller * * Ces tests vérifient le bon fonctionnement des fonctions de génération * sans nécessiter l'environnement Foundry VTT. * * Pour exécuter ces tests, utilisez un environnement Node.js avec Jest ou * un framework de test similaire. */ // Mock des dépendances Foundry pour l'environnement de test const mockFoundry = { utils: { deepClone: (obj) => JSON.parse(JSON.stringify(obj)), mergeObject: (target, source) => ({ ...target, ...source }), setProperty: () => {}, getProperty: () => {} }, applications: { handlebars: { renderTemplate: async () => '
Test
' } } }; global.foundry = mockFoundry; // Mock console pour capturer les logs const mockConsole = { log: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() }; // Remplacer la console globale console.log = mockConsole.log; console.warn = mockConsole.warn; console.error = mockConsole.error; console.debug = mockConsole.debug; // Mock ui.notifications const mockUi = { notifications: { info: jest.fn(), error: jest.fn(), warn: jest.fn(), success: jest.fn() } }; global.ui = mockUi; // Mock game object const mockGame = { system: { id: 'mgt2e' }, packs: { get: jest.fn() }, i18n: { localize: jest.fn((key) => key) }, settings: { get: jest.fn() } }; global.game = mockGame; // Mock canvas const mockCanvas = { tokens: { controlled: [] } }; global.canvas = mockCanvas; // Imports des modules à tester import { generateCharacteristics, generateSkills, generateTravellerNpc, generateAndCreateTravellerNpc, convertSkillToMgt2eFormat, buildMgt2eCharacteristics, buildMgt2eSkills, buildMgt2eBaseActorSystem, getMgt2eBaseActorSystem, toHex, calculateDm, pickRandomItem, shuffleArray, popRandomItems, getRoleByKey, getCitizenCategoryByKey, getExperienceLevelByKey, getGenderByKey, getSkillsForRole, getCharacteristicPrioritiesForRole, validateOptions, DEFAULT_OPTIONS, CITIZEN_CATEGORY_LIST, EXPERIENCE_LEVEL_LIST, ROLE_LIST, GENDER_LIST, CHARACTERISTIC_LIST, UPP_ORDER, ERROR_CODES, TravellerNpcError } from '../travellerNpcGenerator.js'; import { CITIZEN_CATEGORY, EXPERIENCE_LEVEL, ROLE, GENDER, CHARACTERISTIC, CHARACTERISTIC_PRIORITIES, ROLE_SKILLS, NAME_CATALOGS } from '../data/travellerNpcGenerator.js'; import { ModuleCache, travellerNpcCache, ERROR_CODES as UTILS_ERROR_CODES } from '../utils/travellerNpcUtils.js'; // ============================================================================ // Tests des fonctions utilitaires // ============================================================================ describe('Fonctions utilitaires', () => { describe('toHex', () => { it('devrait convertir 0 en "0"', () => { expect(toHex(0)).toBe('0'); }); it('devrait convertir 7 en "7"', () => { expect(toHex(7)).toBe('7'); }); it('devrait convertir 10 en "A"', () => { expect(toHex(10)).toBe('A'); }); it('devrait convertir 15 en "F"', () => { expect(toHex(15)).toBe('F'); }); it('devrait limiter les valeurs à 0-15', () => { expect(toHex(-5)).toBe('0'); expect(toHex(20)).toBe('F'); }); it('devrait arrondir vers le bas', () => { expect(toHex(10.7)).toBe('A'); expect(toHex(10.2)).toBe('A'); }); }); describe('calculateDm', () => { it('devrait calculer le DM pour la valeur 6 (DM=0)', () => { expect(calculateDm(6)).toBe(0); }); it('devrait calculer le DM pour la valeur 7 (DM=0)', () => { expect(calculateDm(7)).toBe(0); }); it('devrait calculer le DM pour la valeur 8 (DM=0)', () => { expect(calculateDm(8)).toBe(0); }); it('devrait calculer le DM pour la valeur 9 (DM=+1)', () => { expect(calculateDm(9)).toBe(1); }); it('devrait calculer le DM pour la valeur 12 (DM=+2)', () => { expect(calculateDm(12)).toBe(2); }); it('devrait calculer le DM pour la valeur 4 (DM=-1)', () => { expect(calculateDm(4)).toBe(-1); }); it('devrait calculer le DM pour la valeur 3 (DM=-1)', () => { expect(calculateDm(3)).toBe(-1); }); it('devrait calculer le DM pour la valeur 1 (DM=-2)', () => { expect(calculateDm(1)).toBe(-2); }); }); describe('pickRandomItem', () => { it('devrait retourner un élément aléatoire du tableau', () => { const items = [1, 2, 3, 4, 5]; const result = pickRandomItem(items); expect(items).toContain(result); }); it('devrait lancer une erreur pour un tableau vide', () => { expect(() => pickRandomItem([])).toThrow('Cannot pick from empty array'); }); it('devrait lancer une erreur pour null/undefined', () => { expect(() => pickRandomItem(null)).toThrow('Cannot pick from empty array'); expect(() => pickRandomItem(undefined)).toThrow('Cannot pick from empty array'); }); }); describe('shuffleArray', () => { it('devrait retourner un tableau de même longueur', () => { const array = [1, 2, 3, 4, 5]; const shuffled = shuffleArray(array); expect(shuffled).toHaveLength(5); }); it('devrait retourner tous les éléments originaux', () => { const array = [1, 2, 3, 4, 5]; const shuffled = shuffleArray(array); expect(shuffled).toEqual(expect.arrayContaining(array)); }); it('devrait retourner un tableau différent (sauf cas très rare)', () => { const array = [1, 2, 3, 4, 5]; const shuffled = shuffleArray(array); // Note: Il y a une très faible probabilité que le tableau reste identique // Cette assertion peut échouer très rarement expect(shuffled).not.toEqual(array); }); it('devrait retourner une copie (ne pas modifier l\'original)', () => { const array = [1, 2, 3, 4, 5]; const original = [...array]; shuffleArray(array); expect(array).toEqual(original); }); }); describe('popRandomItems', () => { it('devrait retourner le bon nombre d\'éléments extraits', () => { const array = [1, 2, 3, 4, 5, 6, 7, 8]; const [taken, remaining] = popRandomItems(array, 3); expect(taken).toHaveLength(3); }); it('devrait retourner les éléments restants', () => { const array = [1, 2, 3, 4, 5, 6, 7, 8]; const [taken, remaining] = popRandomItems(array, 3); expect(remaining).toHaveLength(5); }); it('devrait retourner tous les éléments si count > array.length', () => { const array = [1, 2, 3]; const [taken, remaining] = popRandomItems(array, 10); expect(taken).toHaveLength(3); expect(remaining).toHaveLength(0); }); it('devrait retourner un tableau vide si count <= 0', () => { const array = [1, 2, 3, 4, 5]; const [taken, remaining] = popRandomItems(array, 0); expect(taken).toHaveLength(0); expect(remaining).toHaveLength(5); }); it('devrait retourner des tableaux vides pour un tableau vide', () => { const [taken, remaining] = popRandomItems([], 3); expect(taken).toHaveLength(0); expect(remaining).toHaveLength(0); }); }); }); // ============================================================================ // Tests des fonctions de lookup // ============================================================================ describe('Fonctions de lookup', () => { describe('getRoleByKey', () => { it('devrait retourner le rôle pilote', () => { const role = getRoleByKey('pilot'); expect(role.key).toBe('pilot'); }); it('devrait retourner le rôle par défaut (pilot) pour une clé invalide', () => { const role = getRoleByKey('invalid'); expect(role.key).toBe('pilot'); }); it('devrait retourner le rôle navigateur', () => { const role = getRoleByKey('navigator'); expect(role.key).toBe('navigator'); }); }); describe('getCitizenCategoryByKey', () => { it('devrait retourner la catégorie moyenne', () => { const category = getCitizenCategoryByKey('average'); expect(category.key).toBe('average'); }); it('devrait retourner la catégorie par défaut (average) pour une clé invalide', () => { const category = getCitizenCategoryByKey('invalid'); expect(category.key).toBe('average'); }); it('devrait retourner la catégorie exceptionnel', () => { const category = getCitizenCategoryByKey('exceptional'); expect(category.key).toBe('exceptional'); }); }); describe('getExperienceLevelByKey', () => { it('devrait retourner le niveau recruit', () => { const level = getExperienceLevelByKey('recruit'); expect(level.key).toBe('recruit'); }); it('devrait retourner le niveau par défaut (regular) pour une clé invalide', () => { const level = getExperienceLevelByKey('invalid'); expect(level.key).toBe('regular'); }); it('devrait retourner le niveau elite', () => { const level = getExperienceLevelByKey('elite'); expect(level.key).toBe('elite'); }); }); describe('getGenderByKey', () => { it('devrait retourner le genre unspecified', () => { const gender = getGenderByKey('unspecified'); expect(gender.key).toBe('unspecified'); }); it('devrait retourner le genre par défaut (unspecified) pour une clé invalide', () => { const gender = getGenderByKey('invalid'); expect(gender.key).toBe('unspecified'); }); it('devrait retourner le genre female', () => { const gender = getGenderByKey('female'); expect(gender.key).toBe('female'); }); }); describe('getSkillsForRole', () => { it('devrait retourner les compétences pour pilote', () => { const skills = getSkillsForRole('pilot'); expect(skills).toBeDefined(); expect(skills.length).toBeGreaterThan(0); expect(skills).toContain('Pilot-Spacecraft'); }); it('devrait retourner les compétences pour médecin', () => { const skills = getSkillsForRole('medic'); expect(skills).toContain('Medic'); }); it('devrait retourner les compétences par défaut pour un rôle invalide', () => { const skills = getSkillsForRole('invalid'); expect(skills).toBeDefined(); expect(skills.length).toBeGreaterThan(0); }); }); describe('getCharacteristicPrioritiesForRole', () => { it('devrait retourner les priorités pour pilote', () => { const priorities = getCharacteristicPrioritiesForRole('pilot'); expect(priorities).toBeDefined(); expect(priorities.high).toContain('DEX'); expect(priorities.high).toContain('INT'); }); it('devrait retourner les priorités par défaut pour un rôle invalide', () => { const priorities = getCharacteristicPrioritiesForRole('invalid'); expect(priorities).toBeDefined(); }); }); }); // ============================================================================ // Tests de validation des options // ============================================================================ describe('validateOptions', () => { it('devrait retourner les options par défaut pour un objet vide', () => { const validated = validateOptions({}); expect(validated.citizenCategory).toBe(DEFAULT_OPTIONS.citizenCategory); expect(validated.experience).toBe(DEFAULT_OPTIONS.experience); expect(validated.role).toBe(DEFAULT_OPTIONS.role); expect(validated.gender).toBe(DEFAULT_OPTIONS.gender); }); it('devrait accepter des options valides', () => { const validated = validateOptions({ citizenCategory: 'exceptional', experience: 'elite', role: 'engineer', gender: 'female' }); expect(validated.citizenCategory).toBe('exceptional'); expect(validated.experience).toBe('elite'); expect(validated.role).toBe('engineer'); expect(validated.gender).toBe('female'); }); it('devrait corriger une catégorie de citoyen invalide', () => { const validated = validateOptions({ citizenCategory: 'invalid', experience: 'regular', role: 'pilot', gender: 'male' }); expect(validated.citizenCategory).toBe(DEFAULT_OPTIONS.citizenCategory); }); it('devrait corriger un niveau d\'expérience invalide', () => { const validated = validateOptions({ citizenCategory: 'average', experience: 'invalid', role: 'pilot', gender: 'male' }); expect(validated.experience).toBe(DEFAULT_OPTIONS.experience); }); it('devrait corriger un rôle invalide', () => { const validated = validateOptions({ citizenCategory: 'average', experience: 'regular', role: 'invalid', gender: 'male' }); expect(validated.role).toBe(DEFAULT_OPTIONS.role); }); it('devrait corriger un genre invalide', () => { const validated = validateOptions({ citizenCategory: 'average', experience: 'regular', role: 'pilot', gender: 'invalid' }); expect(validated.gender).toBe(DEFAULT_OPTIONS.gender); }); it('devrait conserver les options personnalisées valides', () => { const validated = validateOptions({ citizenCategory: 'aboveAverage', experience: 'veteran', role: 'gunner', gender: 'male', createActor: true, actorName: 'Test NPC', openCreatedActor: false }); expect(validated.createActor).toBe(true); expect(validated.actorName).toBe('Test NPC'); expect(validated.openCreatedActor).toBe(false); }); }); // ============================================================================ // Tests de génération des caractéristiques // ============================================================================ describe('generateCharacteristics', () => { it('devrait générer des caractéristiques pour la catégorie average', () => { const result = generateCharacteristics('average', 'pilot'); expect(result.characteristics).toBeDefined(); expect(result.upp).toBeDefined(); expect(result.category).toBeDefined(); expect(result.category.key).toBe('average'); }); it('devrait générer des caractéristiques pour la catégorie exceptional', () => { const result = generateCharacteristics('exceptional', 'pilot'); expect(result.category.key).toBe('exceptional'); expect(result.characteristics.STR).toBeGreaterThanOrEqual(7); expect(result.characteristics.DEX).toBeGreaterThanOrEqual(7); expect(result.characteristics.END).toBeGreaterThanOrEqual(7); }); it('devrait générer des caractéristiques pour la catégorie belowAverage', () => { const result = generateCharacteristics('belowAverage', 'pilot'); expect(result.category.key).toBe('belowAverage'); // Les valeurs devraient être plus basses const values = Object.values(result.characteristics); const avg = values.reduce((a, b) => a + b, 0) / values.length; expect(avg).toBeLessThan(7); }); it('devrait générer un code UPP de 6 caractères', () => { const result = generateCharacteristics('average', 'pilot'); expect(result.upp).toHaveLength(6); expect(result.upp).toMatch(/^[0-9A-F]{6}$/); }); it('devrait générer toutes les caractéristiques (STR, DEX, END, INT, EDU, SOC)', () => { const result = generateCharacteristics('average', 'pilot'); expect(result.characteristics.STR).toBeDefined(); expect(result.characteristics.DEX).toBeDefined(); expect(result.characteristics.END).toBeDefined(); expect(result.characteristics.INT).toBeDefined(); expect(result.characteristics.EDU).toBeDefined(); expect(result.characteristics.SOC).toBeDefined(); }); it('devrait attribuer des valeurs plus élevées aux caractéristiques prioritaires', () => { // Pour le pilote, DEX et INT sont prioritaires (high) const priorities = getCharacteristicPrioritiesForRole('pilot'); const result = generateCharacteristics('average', 'pilot'); // DEX et INT devraient avoir des valeurs plus élevées que SOC et END const dexValue = result.characteristics.DEX; const intValue = result.characteristics.INT; const socValue = result.characteristics.SOC; const endValue = result.characteristics.END; // Note: Cela peut échouer occasionnellement à cause de l'aléatoire // mais devrait passer dans la plupart des cas expect(dexValue + intValue).toBeGreaterThanOrEqual(socValue + endValue); }); it('devrait générer des caractéristiques même avec une catégorie invalide', () => { const result = generateCharacteristics('invalid', 'pilot'); expect(result.characteristics).toBeDefined(); // Devrait utiliser la catégorie par défaut (average) expect(result.category.key).toBe('average'); }); it('devrait générer des caractéristiques même avec un rôle invalide', () => { const result = generateCharacteristics('average', 'invalid'); expect(result.characteristics).toBeDefined(); }); }); // ============================================================================ // Tests de génération des compétences // ============================================================================ describe('generateSkills', () => { it('devrait générer des compétences pour un rôle et un niveau', () => { const skills = generateSkills('pilot', 'regular'); expect(skills).toBeDefined(); expect(Array.isArray(skills)).toBe(true); expect(skills.length).toBeGreaterThan(0); }); it('devrait générer des compétences avec des niveaux', () => { const skills = generateSkills('pilot', 'regular'); skills.forEach(skill => { expect(skill.name).toBeDefined(); expect(skill.level).toBeDefined(); expect(typeof skill.level).toBe('number'); }); }); it('devrait trier les compétences par niveau décroissant', () => { const skills = generateSkills('pilot', 'regular'); for (let i = 0; i < skills.length - 1; i++) { expect(skills[i].level).toBeGreaterThanOrEqual(skills[i + 1].level); } }); it('devrait générer plus de compétences de niveau élevé pour elite', () => { const eliteSkills = generateSkills('pilot', 'elite'); const regularSkills = generateSkills('pilot', 'regular'); const eliteLevel3 = eliteSkills.filter(s => s.level === 3).length; const regularLevel3 = regularSkills.filter(s => s.level === 3).length; // Elite devrait avoir plus de compétences de niveau 3 expect(eliteLevel3).toBeGreaterThanOrEqual(regularLevel3); }); it('devrait supprimer les doublons basés sur la compétence de base', () => { const skills = generateSkills('pilot', 'regular'); const skillNames = skills.map(s => s.name); const uniqueBases = new Set(skillNames.map(n => n.split('-')[0])); // Chaque nom de base devrait apparaître une seule fois expect(skillNames.length).toBe(uniqueBases.size); }); it('devrait générer des compétences avec le rôle par défaut pour un rôle invalide', () => { const skills = generateSkills('invalid', 'regular'); expect(skills).toBeDefined(); expect(skills.length).toBeGreaterThan(0); }); it('devrait générer des compétences avec le niveau par défaut pour un niveau invalide', () => { const skills = generateSkills('pilot', 'invalid'); expect(skills).toBeDefined(); expect(skills.length).toBeGreaterThan(0); }); }); // ============================================================================ // Tests de conversion de compétences // ============================================================================ describe('convertSkillToMgt2eFormat', () => { it('devrait convertir Pilot-Spacecraft', () => { expect(convertSkillToMgt2eFormat('Pilot-Spacecraft')).toBe('pilot.spacecraft'); }); it('devrait convertir Gun Combat', () => { expect(convertSkillToMgt2eFormat('Gun Combat')).toBe('guncombat'); }); it('devrait convertir une compétence inconnue', () => { const result = convertSkillToMgt2eFormat('Unknown-Skill'); expect(result).toBe('unknown.skill'); }); it('devrait gérer les espaces', () => { expect(convertSkillToMgt2eFormat('Gun Combat')).toBe('guncombat'); }); it('devrait gérer les tirets', () => { expect(convertSkillToMgt2eFormat('Pilot-Small Craft')).toBe('pilot.smallcraft'); }); }); // ============================================================================ // Tests de génération complète du PNJ // ============================================================================ describe('generateTravellerNpc', () => { it('devrait générer un PNJ avec des options par défaut', () => { const npc = generateTravellerNpc(); expect(npc.success).toBe(true); expect(npc.type).toBe('traveller-npc'); expect(npc.name).toBeDefined(); expect(npc.name.firstName).toBeDefined(); expect(npc.name.surname).toBeDefined(); expect(npc.name.fullName).toBeDefined(); expect(npc.role).toBeDefined(); expect(npc.citizenCategory).toBeDefined(); expect(npc.experience).toBeDefined(); expect(npc.gender).toBeDefined(); expect(npc.characteristics).toBeDefined(); expect(npc.upp).toBeDefined(); expect(npc.skills).toBeDefined(); expect(npc.skillsForActor).toBeDefined(); expect(npc.display).toBeDefined(); }); it('devrait générer un PNJ avec des options personnalisées', () => { const npc = generateTravellerNpc({ citizenCategory: 'exceptional', experience: 'elite', role: 'engineer', gender: 'female', firstName: 'Jane', surname: 'Doe' }); expect(npc.name.firstName).toBe('Jane'); expect(npc.name.surname).toBe('Doe'); expect(npc.name.fullName).toBe('Jane Doe'); expect(npc.role.key).toBe('engineer'); expect(npc.citizenCategory.key).toBe('exceptional'); expect(npc.experience.key).toBe('elite'); expect(npc.gender.key).toBe('female'); }); it('devrait générer un nom aléatoire si aucun nom n\'est fourni', () => { const npc = generateTravellerNpc({ gender: 'male' }); expect(npc.name.firstName).toBeDefined(); expect(npc.name.surname).toBeDefined(); expect(npc.name.fullName).toContain(' '); expect(NAME_CATALOGS.maleNames).toContain(npc.name.firstName); expect(NAME_CATALOGS.surnames).toContain(npc.name.surname); }); it('devrait générer un nom féminin', () => { const npc = generateTravellerNpc({ gender: 'female' }); expect(NAME_CATALOGS.femaleNames).toContain(npc.name.firstName); }); it('devrait générer un nom non-genré', () => { const npc = generateTravellerNpc({ gender: 'unspecified' }); expect(NAME_CATALOGS.nonGenderedNames).toContain(npc.name.firstName); }); it('devrait inclure des informations d\'affichage', () => { const npc = generateTravellerNpc({ role: 'pilot', citizenCategory: 'average', experience: 'regular', gender: 'male' }); expect(npc.display.roleLabel).toBe('Pilote'); expect(npc.display.categoryLabel).toBe('Moyenne'); expect(npc.display.experienceLabel).toBe('Régulier'); expect(npc.display.genderLabel).toBe('Masculin'); expect(npc.display.characteristicLabels).toBeDefined(); }); it('devrait avoir un UPP valide', () => { const npc = generateTravellerNpc(); expect(npc.upp).toMatch(/^[0-9A-F]{6}$/); }); it('devrait avoir des compétences au format mgt2e', () => { const npc = generateTravellerNpc(); expect(npc.skillsForActor).toBeDefined(); expect(Array.isArray(npc.skillsForActor)).toBe(true); npc.skillsForActor.forEach(skill => { expect(skill.name).toBeDefined(); expect(skill.level).toBeDefined(); }); }); }); // ============================================================================ // Tests du ModuleCache // ============================================================================ describe('ModuleCache', () => { let cache; beforeEach(() => { cache = new ModuleCache('test-module', 1000); }); describe('getOrFetch', () => { it('devrait fetch et cacher une valeur', async () => { const fetchFn = jest.fn().mockResolvedValue('test-value'); const result = await cache.getOrFetch('key1', fetchFn); expect(result).toBe('test-value'); expect(fetchFn).toHaveBeenCalledTimes(1); }); it('devrait retourner la valeur du cache sans refetch', async () => { const fetchFn = jest.fn().mockResolvedValue('test-value'); await cache.getOrFetch('key1', fetchFn); const result = await cache.getOrFetch('key1', fetchFn); expect(result).toBe('test-value'); expect(fetchFn).toHaveBeenCalledTimes(1); // Pas appelé une deuxième fois }); it('devrait gérer les erreurs de fetch', async () => { const error = new Error('Fetch error'); const fetchFn = jest.fn().mockRejectedValue(error); await expect(cache.getOrFetch('key1', fetchFn)).rejects.toThrow('Fetch error'); expect(fetchFn).toHaveBeenCalledTimes(1); }); it('devrait forcer le rafraîchissement avec forceRefresh', async () => { const fetchFn = jest.fn() .mockResolvedValueOnce('value1') .mockResolvedValueOnce('value2'); await cache.getOrFetch('key1', fetchFn); const result = await cache.getOrFetch('key1', fetchFn, { forceRefresh: true }); expect(result).toBe('value2'); expect(fetchFn).toHaveBeenCalledTimes(2); }); it('devrait cloner profondément les résultats', async () => { const original = { nested: { value: 1 } }; const fetchFn = jest.fn().mockResolvedValue(original); const result1 = await cache.getOrFetch('key1', fetchFn); const result2 = await cache.getOrFetch('key1', fetchFn); result1.nested.value = 2; expect(result2.nested.value).toBe(1); expect(original.nested.value).toBe(1); }); }); describe('clear', () => { it('devrait supprimer une entrée spécifique', async () => { const fetchFn = jest.fn().mockResolvedValue('test-value'); await cache.getOrFetch('key1', fetchFn); cache.clear('key1'); await cache.getOrFetch('key1', fetchFn); expect(fetchFn).toHaveBeenCalledTimes(2); // Fetch à nouveau car cache vidé }); }); describe('clearAll', () => { it('devrait vider tout le cache', async () => { const fetchFn = jest.fn().mockResolvedValue('test-value'); await cache.getOrFetch('key1', fetchFn); await cache.getOrFetch('key2', fetchFn); cache.clearAll(); await cache.getOrFetch('key1', fetchFn); await cache.getOrFetch('key2', fetchFn); expect(fetchFn).toHaveBeenCalledTimes(4); // Fetch à nouveau pour les deux clés }); }); describe('has', () => { it('devrait retourner true pour une clé en cache', async () => { const fetchFn = jest.fn().mockResolvedValue('test-value'); await cache.getOrFetch('key1', fetchFn); expect(cache.has('key1')).toBe(true); }); it('devrait retourner false pour une clé non en cache', () => { expect(cache.has('nonexistent')).toBe(false); }); }); describe('get', () => { it('devrait retourner la valeur du cache', async () => { const fetchFn = jest.fn().mockResolvedValue('test-value'); await cache.getOrFetch('key1', fetchFn); const result = cache.get('key1'); expect(result).toBe('test-value'); }); it('devrait retourner null pour une clé non en cache', () => { expect(cache.get('nonexistent')).toBeNull(); }); }); }); // ============================================================================ // Tests de TravellerNpcError // ============================================================================ describe('TravellerNpcError', () => { it('devrait créer une erreur avec message et code', () => { const error = new TravellerNpcError('Test error', 'TEST_ERROR'); expect(error.message).toBe('Test error'); expect(error.code).toBe('TEST_ERROR'); expect(error.name).toBe('TravellerNpcError'); expect(error.isTravellerNpcError).toBe(true); expect(error.details).toEqual({}); expect(error.timestamp).toBeDefined(); }); it('devrait créer une erreur avec des détails', () => { const details = { foo: 'bar' }; const error = new TravellerNpcError('Test error', 'TEST_ERROR', details); expect(error.details).toEqual(details); }); it('devrait créer une erreur à partir d\'une erreur existante', () => { const original = new Error('Original error'); const error = TravellerNpcError.from(original, 'WRAPPED_ERROR'); expect(error.message).toBe('Original error'); expect(error.code).toBe('WRAPPED_ERROR'); expect(error.details.originalError).toBe(original); }); it('devrait retourner l\'erreur existante si c\'est déjà une TravellerNpcError', () => { const original = new TravellerNpcError('Original', 'ORIG'); const error = TravellerNpcError.from(original, 'WRAPPED'); expect(error).toBe(original); }); it('devrait gérer undefined comme erreur originale', () => { const error = TravellerNpcError.from(undefined, 'UNKNOWN'); expect(error.message).toBe('Erreur inconnue'); expect(error.code).toBe('UNKNOWN'); }); }); // ============================================================================ // Tests des données de configuration // ============================================================================ describe('Données de configuration', () => { describe('CITIZEN_CATEGORY_LIST', () => { it('devrait contenir toutes les catégories', () => { expect(CITIZEN_CATEGORY_LIST).toHaveLength(4); expect(CITIZEN_CATEGORY_LIST).toContain(CITIZEN_CATEGORY.BELOW_AVERAGE); expect(CITIZEN_CATEGORY_LIST).toContain(CITIZEN_CATEGORY.AVERAGE); expect(CITIZEN_CATEGORY_LIST).toContain(CITIZEN_CATEGORY.ABOVE_AVERAGE); expect(CITIZEN_CATEGORY_LIST).toContain(CITIZEN_CATEGORY.EXCEPTIONAL); }); }); describe('EXPERIENCE_LEVEL_LIST', () => { it('devrait contenir tous les niveaux', () => { expect(EXPERIENCE_LEVEL_LIST).toHaveLength(6); expect(EXPERIENCE_LEVEL_LIST).toContain(EXPERIENCE_LEVEL.RECRUIT); expect(EXPERIENCE_LEVEL_LIST).toContain(EXPERIENCE_LEVEL.ROOKIE); expect(EXPERIENCE_LEVEL_LIST).toContain(EXPERIENCE_LEVEL.INTERMEDIATE); expect(EXPERIENCE_LEVEL_LIST).toContain(EXPERIENCE_LEVEL.REGULAR); expect(EXPERIENCE_LEVEL_LIST).toContain(EXPERIENCE_LEVEL.VETERAN); expect(EXPERIENCE_LEVEL_LIST).toContain(EXPERIENCE_LEVEL.ELITE); }); }); describe('ROLE_LIST', () => { it('devrait contenir tous les rôles', () => { expect(ROLE_LIST.length).toBeGreaterThan(10); expect(ROLE_LIST).toContain(ROLE.PILOT); expect(ROLE_LIST).toContain(ROLE.NAVIGATOR); expect(ROLE_LIST).toContain(ROLE.ENGINEER); expect(ROLE_LIST).toContain(ROLE.MEDIC); }); }); describe('GENDER_LIST', () => { it('devrait contenir tous les genres', () => { expect(GENDER_LIST).toHaveLength(3); expect(GENDER_LIST).toContain(GENDER.UNSPECIFIED); expect(GENDER_LIST).toContain(GENDER.FEMALE); expect(GENDER_LIST).toContain(GENDER.MALE); }); }); describe('CHARACTERISTIC_LIST', () => { it('devrait contenir toutes les caractéristiques', () => { expect(CHARACTERISTIC_LIST).toHaveLength(6); expect(CHARACTERISTIC_LIST).toContain(CHARACTERISTIC.STR); expect(CHARACTERISTIC_LIST).toContain(CHARACTERISTIC.DEX); expect(CHARACTERISTIC_LIST).toContain(CHARACTERISTIC.END); expect(CHARACTERISTIC_LIST).toContain(CHARACTERISTIC.INT); expect(CHARACTERISTIC_LIST).toContain(CHARACTERISTIC.EDU); expect(CHARACTERISTIC_LIST).toContain(CHARACTERISTIC.SOC); }); }); describe('UPP_ORDER', () => { it('devrait contenir l\'ordre correct des caractéristiques', () => { expect(UPP_ORDER).toEqual(['STR', 'DEX', 'END', 'INT', 'EDU', 'SOC']); }); }); describe('ERROR_CODES', () => { it('devrait contenir tous les codes d\'erreur', () => { expect(ERROR_CODES.INVALID_OPTIONS).toBe('INVALID_OPTIONS'); expect(ERROR_CODES.INVALID_ROLE).toBe('INVALID_ROLE'); expect(ERROR_CODES.INVALID_CATEGORY).toBe('INVALID_CATEGORY'); expect(ERROR_CODES.INVALID_EXPERIENCE).toBe('INVALID_EXPERIENCE'); expect(ERROR_CODES.INVALID_GENDER).toBe('INVALID_GENDER'); expect(ERROR_CODES.ACTOR_CREATION_FAILED).toBe('ACTOR_CREATION_FAILED'); expect(ERROR_CODES.MGT2E_NOT_ACTIVE).toBe('MGT2E_NOT_ACTIVE'); expect(ERROR_CODES.BASE_ACTOR_NOT_FOUND).toBe('BASE_ACTOR_NOT_FOUND'); }); }); describe('DEFAULT_OPTIONS', () => { it('devrait avoir des valeurs par défaut valides', () => { expect(DEFAULT_OPTIONS.citizenCategory).toBe('average'); expect(DEFAULT_OPTIONS.experience).toBe('regular'); expect(DEFAULT_OPTIONS.role).toBe('pilot'); expect(DEFAULT_OPTIONS.gender).toBe('unspecified'); expect(DEFAULT_OPTIONS.createActor).toBe(false); expect(DEFAULT_OPTIONS.openCreatedActor).toBe(true); }); }); describe('NAME_CATALOGS', () => { it('devrait avoir des catalogues de noms définis', () => { expect(NAME_CATALOGS.surnames).toBeDefined(); expect(NAME_CATALOGS.surnames.length).toBeGreaterThan(0); expect(NAME_CATALOGS.nonGenderedNames).toBeDefined(); expect(NAME_CATALOGS.nonGenderedNames.length).toBeGreaterThan(0); expect(NAME_CATALOGS.femaleNames).toBeDefined(); expect(NAME_CATALOGS.femaleNames.length).toBeGreaterThan(0); expect(NAME_CATALOGS.maleNames).toBeDefined(); expect(NAME_CATALOGS.maleNames.length).toBeGreaterThan(0); }); }); describe('CHARACTERISTIC_PRIORITIES', () => { it('devrait avoir des priorités pour chaque rôle', () => { expect(CHARACTERISTIC_PRIORITIES.pilot).toBeDefined(); expect(CHARACTERISTIC_PRIORITIES.navigator).toBeDefined(); expect(CHARACTERISTIC_PRIORITIES.engineer).toBeDefined(); expect(CHARACTERISTIC_PRIORITIES.medic).toBeDefined(); expect(CHARACTERISTIC_PRIORITIES.marine).toBeDefined(); }); it('devrait avoir des groupes high, medium, low', () => { const priorities = CHARACTERISTIC_PRIORITIES.pilot; expect(priorities.high).toBeDefined(); expect(priorities.medium).toBeDefined(); expect(priorities.low).toBeDefined(); expect(priorities.high.length + priorities.medium.length + priorities.low.length).toBe(6); }); }); describe('ROLE_SKILLS', () => { it('devrait avoir des compétences pour chaque rôle', () => { expect(ROLE_SKILLS.pilot).toBeDefined(); expect(ROLE_SKILLS.navigator).toBeDefined(); expect(ROLE_SKILLS.engineer).toBeDefined(); expect(ROLE_SKILLS.medic).toBeDefined(); expect(ROLE_SKILLS.marine).toBeDefined(); }); it('devrait avoir au moins 10 compétences par rôle', () => { Object.values(ROLE_SKILLS).forEach(skills => { expect(skills.length).toBeGreaterThanOrEqual(10); }); }); }); }); // ============================================================================ // Tests d\'intégration (si environnement Foundry disponible) // ============================================================================ // Ces tests sont optionnels et nécessitent un environnement Foundry complet // Ils sont marqués comme skip par défaut describe.skip('Intégration Foundry (nécessite environnement Foundry)', () => { describe('buildMgt2eCharacteristics', () => { it('devrait construire les caractéristiques au format mgt2e', () => { const existing = {}; const characteristics = { STR: 8, DEX: 9, END: 7, INT: 10, EDU: 8, SOC: 6 }; const result = buildMgt2eCharacteristics(existing, characteristics); expect(result.STR).toBeDefined(); expect(result.STR.value).toBe(8); expect(result.STR.dm).toBe(calculateDm(8)); }); }); describe('buildMgt2eSkills', () => { it('devrait construire les compétences au format mgt2e', () => { const existing = {}; const skills = [ { name: 'Pilot-Spacecraft', level: 2 }, { name: 'Gun Combat', level: 1 } ]; const result = buildMgt2eSkills(existing, skills); expect(result).toBeDefined(); }); }); }); // ============================================================================ // Export pour les tests manuels // ============================================================================ if (typeof globalThis !== 'undefined') { globalThis.TravellerNpcGeneratorTests = { generateCharacteristics, generateSkills, generateTravellerNpc, validateOptions, toHex, calculateDm, pickRandomItem, shuffleArray, TravellerNpcError, ModuleCache, runAllTests: () => { console.log('Exécutant les tests du générateur de PNJ Traveller...'); console.log('Tous les tests sont définis. Exécutez avec Jest pour voir les résultats.'); } }; }