ef7fe6e2bd
Corrections critiques implémentées: - Remplacement du cache global mutable par ModuleCache - Binding des méthodes dans TravellerNpcDialog - Suppression des ré-exports circulaires - Validation complète des options - Correction: Duplicate export de TravellerNpcDialog et openTravellerNpcDialog - Correction: distributeSkillLevels ne supprime plus les spécialisations (ex: Pilot-Spacecraft ET Pilot-Small Craft sont maintenant conservées) Améliorations majeures: - Optimisation de l'algorithme de distribution des compétences (single-pass) - Optimisation de la génération des caractéristiques (priorité-based) - Gestion d'erreur améliorée avec TravellerNpcError - Création de TravellerNpcUtils.js avec classes utilitaires Améliorations mineures: - CSS aligné avec les styles des dialogues /commerce et /pnj - Thème clair cohérent (#f5f0e8 background, #222 text) - Fieldset, onglets, formulaires alignés sur mgt2-npc-form - Boutons et résultats stylisés comme mgt2-npc-result - Suppression des styles inline redondants dans _applyThemeStyles - Design réactif, accessibilité, impression - Tests unitaires complets pour toutes les fonctions - Version bumpée à 1.3.0 Traductions en français: - Ajout de SKILL_LABELS_FR pour toutes les compétences Traveller - Ajout de CHARACTERISTIC_LABELS_FR pour STR, DEX, END, INT, EDU, SOC - Ajout de CITIZEN_CATEGORY_LABELS_FR, EXPERIENCE_LEVEL_LABELS_FR - Ajout de ROLE_LABELS_FR, GENDER_LABELS_FR - Mise à jour de generateTravellerNpc pour utiliser les libellés français - Mise à jour du template traveller-npc-result.hbs pour afficher labelFr - Mise à jour du template traveller-npc-dialog.hbs avec libellés français - Mise à jour de TravellerNpcDialog._prepareContext pour utiliser les libellés FR Fichiers ajoutés: - scripts/utils/travellerNpcUtils.js - scripts/tests/travellerNpcGenerator.test.js Fichiers modifiés: - scripts/data/travellerNpcGenerator.js (+ traductions FR) - scripts/travellerNpcGenerator.js (+ fonctions getSkillLabelFr, getCharacteristicLabelFr) - scripts/TravellerNpcDialog.js (libellés FR dans _prepareContext) - scripts/npc.js - styles/traveller-npc.css - templates/traveller-npc-dialog.hbs - templates/traveller-npc-result.hbs - module.json Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
1097 lines
36 KiB
JavaScript
1097 lines
36 KiB
JavaScript
/**
|
|
* 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 () => '<div>Test</div>'
|
|
}
|
|
}
|
|
};
|
|
|
|
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.');
|
|
}
|
|
};
|
|
}
|