// © 2026 TechnoLibre (http://www.technolibre.ca) // License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) // Épreuves des commandes des places (§ 4, § 4.1, § 5.8, § 5.9, § 7.1, § 8.2, // § 8.9, § 9, § 14.12) : réservations, titres, désignation, conversion, // retouches du retenu et leur vérificateur d'invariants, rangement par siège // d'une génération. Les noms d'épreuve sont inventés ; la version vient du // module engendré. import assert from 'node:assert/strict'; import { describe, test } from '../../test/lanceur.js'; import { FLUX, creerPcg32 } from '../demo/prng.js'; import { normaliser } from '../moteur/configuration.js'; import { rechercher } from '../moteur/recherche.js'; import { serialiser } from '../stockage/canonique.js'; import { analyser, configurationDepuisCharge, creerCharge } from '../stockage/document.js'; import { derive, fautes } from '../stockage/placements.js'; import { VERSION } from '../version.genere.js'; import { avertissementsApres } from './avertissements.js'; import { COMMANDES, appliquerCommande } from './commandes.js'; import { COMMANDES_PLACES } from './commandes_places.js'; import { ErreurCommande } from './erreurs.js'; import { libelle } from './libelles.js'; import { TABLE as TEXTES_DES_PLACES } from './libelles/places.js'; import { conversionsPossibles } from './listes_travail.js'; import { etatPlaces } from './places.js'; const executer = (nom, charge, args) => COMMANDES.get(nom)(charge, args); const NOMS = [ ['Ombrelle', 'Iris'], ['Pervenche', 'Théo'], ['Lacasse', 'Ondine'], ['Grisaille', null], ['Bruyère', 'Anouk'], ['Sarcelle', 'Capucine'], ['Ancolie', 'Amandine'], ['Mélèze', 'Basile'], ['Quenouille', 'Hortense'], ['Pivoine', 'Gaspard'], ['Aubépine', 'Colombe'], ['Lavande', 'Ysolde'], ['Zéphyrin', 'Lucioles'], ]; const personne = (id, autres = {}) => ({ id, nom: NOMS[(id - 1) % NOMS.length][0], prenom: NOMS[(id - 1) % NOMS.length][1], appartenance: null, courriel: null, titrePressenti: null, notes: null, exclu: false, ...autres, }); const resa = (participant, idTable, siege, tour = null) => ({ participant, table: idTable, siege, portee: tour === null ? 'tous' : 'tour', tour, }); // Le refus d'une ErreurCommande levée par appel, de ce code ; ses détails // contiennent ceux donnés. function refuse(appel, code, attendus = {}) { let leve = null; try { appel(); } catch (erreur) { leve = erreur; } assert.ok(leve instanceof ErreurCommande, `ErreurCommande ${code} attendue, reçu ${leve}`); assert.equal(leve.code, code, leve.message); for (const [cle, valeur] of Object.entries(attendus)) assert.deepEqual(leve.details[cle], valeur, cle); return leve; } // Gèle une valeur en profondeur : une commande qui la modifierait lèverait. function figer(valeur) { if (valeur !== null && typeof valeur === 'object' && !Object.isFrozen(valeur)) { Object.freeze(valeur); for (const element of Object.values(valeur)) figer(element); } return valeur; } // Un plan de fichier ; la réserve de chaque tour porte les déclarés qu'aucune // liste n'assied. function planFichier({ tables, capacites, participants, tours, siegesAttribues }) { return { siegesAttribues, tables, capacites, tours: tours.length, participants, placement: tours.map((listes) => { const assis = new Set(listes.flat()); return { sieges: listes, reserve: participants.filter((id) => !assis.has(id)) }; }), }; } const DOUZE = Array.from({ length: 12 }, (_, rang) => rang + 1); // Douze personnes, trois tours, les tables numérotées 4 et 7 d'identifiants // 5 et 8, de huit places ; la proposition 1, retenue, que tours donne. function chargeRetenue({ siegesAttribues = true, tours, capacite = 8, reservations = [] } = {}) { const charge = creerCharge({ id: 'evt-retenu', nom: 'Soirée des Lucioles', siegesParDefaut: capacite, tours: 3 }); charge.reglages.attribuerSieges = siegesAttribues; charge.participants = DOUZE.map((id) => personne(id)); charge.tables = [ { id: 5, numero: 4, sieges: null, forme: 'ronde', position: { x: 0, y: 0 } }, { id: 8, numero: 7, sieges: null, forme: 'carree', position: { x: 400, y: 0 } }, ]; charge.reservations = reservations; const plan = planFichier({ tables: [5, 8], capacites: [capacite, capacite], participants: DOUZE, siegesAttribues, tours: tours ?? [ [[1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 11, 12]], [[1, 8, 9, 10, 11, 12], [7, 2, 3, 4, 5, 6]], [[1, 2, 3, 7, 8, 9], [4, 5, 6, 10, 11, 12]], ], }); charge.propositions = [{ id: 1, graine: 48272, arret: 200000, historique: 1000, produitVersion: VERSION.affichee, ...plan }]; charge.retenu = { proposition: 1, ...structuredClone(plan) }; charge.evenement.etat = 'retenu'; charge.prochainsIds = { participant: 13, table: 9, proposition: 2 }; return charge; } const listes = (charge, tour) => charge.retenu.placement[tour - 1].sieges; const reserveDe = (charge, tour) => charge.retenu.placement[tour - 1].reserve; describe('commandes des places : le registre (§ 8.2)', () => { test('COMMANDES_PLACES porte les dix commandes des places, réunies dans COMMANDES', () => { assert.deepEqual([...COMMANDES_PLACES.keys()].sort(), [ 'convertirEnTous', 'deplacerDansRetenu', 'deplacerReservation', 'designer', 'echangerDansRetenu', 'poserTitre', 'reserver', 'retirerDuTour', 'retirerReservation', 'retirerTitre', ]); for (const [nom, commande] of COMMANDES_PLACES) assert.equal(COMMANDES.get(nom), commande); }); test('un plan bloqué refuse chaque commande des places, remède débloquer', () => { const charge = chargeRetenue(); charge.evenement.etat = 'bloque'; const essais = [ ['reserver', { participant: 1, table: 5, siege: 1, portee: 'tous', tour: null }], ['retirerReservation', { participant: 1, table: 5, portee: 'tous', tour: null }], ['deplacerReservation', { participant: 1, de: { table: 5, portee: 'tous', tour: null }, vers: { table: 8, siege: 1 } }], ['poserTitre', { table: 5, siege: 1, libelle: 'animation' }], ['retirerTitre', { table: 5, siege: 1 }], ['designer', { participant: 1, table: 5, siege: 1, libelle: 'animation' }], ['convertirEnTous', { participant: 1, table: 5 }], ['deplacerDansRetenu', { tour: 1, participant: 1, table: 8, siege: 7 }], ['echangerDansRetenu', { tour: 1, a: 1, b: 7 }], ['retirerDuTour', { tour: 1, participant: 1 }], ]; assert.equal(essais.length, COMMANDES_PLACES.size); for (const [nom, args] of essais) refuse(() => executer(nom, charge, args), 'PLAN_BLOQUE', { remede: { geste: 'debloquer' } }); }); }); describe('commandes des places : réservations et libellés figés (§ 5.9, § 8.2)', () => { test('reserver : la réservation, et le libellé qui nomme la personne et le numéro de la table', () => { const charge = chargeRetenue(); const { charge: apres, libelle, avertissements } = executer('reserver', charge, { participant: 2, table: 5, siege: 2, portee: 'tour', tour: 2, }); assert.equal(libelle, 'Réservé Théo Pervenche à la table 4, siège 2, tour 2'); assert.deepEqual(apres.reservations, [resa(2, 5, 2, 2)]); assert.deepEqual(avertissements, []); assert.equal( executer('reserver', charge, { participant: 4, table: 8, siege: null, portee: 'tous', tour: null }).libelle, 'Réservé Grisaille à la table 7, tous les tours', ); }); test('reserver une réservation qui existe déjà : SANS_EFFET ; une table gelée avertit', () => { const charge = chargeRetenue({ capacite: 2, tours: [[[1, 2], [3, 4]], [[1, 3], [2, 4]], [[1, 4], [2, 3]]] }); const une = executer('reserver', charge, { participant: 1, table: 5, siege: null, portee: 'tous', tour: null }).charge; refuse(() => executer('reserver', une, { participant: 1, table: 5, siege: null, portee: 'tous', tour: null }), 'SANS_EFFET', { commande: 'reserver', }); const { avertissements } = executer('reserver', une, { participant: 2, table: 5, siege: null, portee: 'tous', tour: null }); assert.deepEqual(avertissements, [{ code: 'TABLE_GELEE', details: { table: 5 } }]); }); test('retirer, déplacer : la réservation désignée par sa table et sa portée ; une réservation absente est refusée', () => { const charge = chargeRetenue({ reservations: [resa(1, 5, 1), resa(2, 8, 2, 2)] }); const retiree = executer('retirerReservation', charge, { participant: 2, table: 8, portee: 'tour', tour: 2 }); assert.deepEqual(retiree.charge.reservations, [resa(1, 5, 1)]); assert.equal(retiree.libelle, 'Réservation retirée : Théo Pervenche, table 7, tour 2'); refuse(() => executer('retirerReservation', charge, { participant: 2, table: 8, portee: 'tous', tour: null }), 'RESERVATION_ABSENTE', { participant: 2, remede: null, }); const deplacee = executer('deplacerReservation', charge, { participant: 1, de: { table: 5, portee: 'tous', tour: null }, vers: { table: 8, siege: 3 }, }); assert.deepEqual(deplacee.charge.reservations, [resa(1, 8, 3), resa(2, 8, 2, 2)]); assert.equal(deplacee.libelle, 'Réservation déplacée : Iris Ombrelle, de la table 4 à la table 7, siège 3, tous les tours'); refuse( () => executer('deplacerReservation', charge, { participant: 1, de: { table: 5, portee: 'tous', tour: null }, vers: { table: 8, siege: 2 } }), 'PLACE_RESERVEE', { titulaire: 2 }, ); }); }); describe('commandes des places : titres et désignation (§ 4, § 4.1, § 2.1 étape 6)', () => { // Deux tables, l'une de 2 places, trois tours, sans retenu. function chargeTitres(attribuerSieges = false) { const charge = creerCharge({ id: 'evt-titres', nom: 'Soirée des Lucioles', siegesParDefaut: 6, tours: 3 }); charge.reglages.attribuerSieges = attribuerSieges; charge.participants = [1, 2, 3, 4].map((id) => personne(id)); charge.tables = [ { id: 1, numero: 1, sieges: 2, forme: 'ronde', position: { x: 0, y: 0 } }, { id: 3, numero: 3, sieges: null, forme: 'ronde', position: { x: 400, y: 0 } }, ]; charge.prochainsIds = { participant: 5, table: 4, proposition: 1 }; return charge; } test('désigner Iris à la table 3 : une réservation « tous » et un titre au même siège, une entrée', () => { const { charge, libelle } = executer('designer', chargeTitres(true), { participant: 1, table: 3, siege: 2, libelle: 'animatrice' }); assert.deepEqual(charge.reservations, [resa(1, 3, 2)]); assert.deepEqual(charge.titres, [{ table: 3, siege: 2, libelle: 'animatrice' }]); assert.equal(libelle, 'Désigné Iris Ombrelle à la table 3, siège 2 : « animatrice »'); }); test('un titre non pourvu déjà posé à cette place est pourvu, aucun second', () => { const titree = executer('poserTitre', chargeTitres(), { table: 3, siege: 4, libelle: 'hôte' }).charge; const { charge } = executer('designer', titree, { participant: 2, table: 3, siege: null, libelle: 'animateur' }); assert.deepEqual(charge.titres, [{ table: 3, siege: 4, libelle: 'hôte' }]); assert.deepEqual(charge.reservations, [resa(2, 3, 4)]); const deuxieme = executer('designer', charge, { participant: 3, table: 3, siege: null, libelle: 'veille' }).charge; assert.deepEqual(deuxieme.titres, [{ table: 3, siege: 4, libelle: 'hôte' }, { table: 3, siege: 1, libelle: 'veille' }]); assert.deepEqual(deuxieme.reservations.at(-1), resa(3, 3, 1)); }); test('poserTitre : vide refusé LIBELLE_VIDE sans écriture ; NFC sans blancs de bord ; place titrée PLACE_TITREE', () => { const charge = figer(chargeTitres(true)); for (const libelle of ['', ' ']) { refuse(() => executer('poserTitre', charge, { table: 3, siege: 1, libelle }), 'LIBELLE_VIDE', { remede: null }); refuse(() => executer('designer', charge, { participant: 1, table: 3, siege: 1, libelle }), 'LIBELLE_VIDE', { remede: null }); } const nfd = ' Ho\u{0302}tesse\u{00A0}'; const { charge: titree, libelle } = executer('poserTitre', charge, { table: 3, siege: 1, libelle: nfd }); assert.deepEqual(titree.titres, [{ table: 3, siege: 1, libelle: 'H\u{00F4}tesse' }]); assert.equal(libelle, 'Posé le titre « H\u{00F4}tesse » à la table 3, siège 1'); refuse(() => executer('poserTitre', titree, { table: 3, siege: 1, libelle: 'hôte' }), 'PLACE_TITREE', { table: 3, siege: 1, libelle: 'H\u{00F4}tesse', remede: { commande: 'retirerTitre', arguments: { table: 3, siege: 1 } }, }); refuse(() => executer('poserTitre', titree, { table: 3, siege: 7, libelle: 'hôte' }), 'SIEGE_INEXISTANT', { siege: 7, capacite: 6 }); const retire = executer('retirerTitre', titree, { table: 3, siege: 1 }); assert.deepEqual(retire.charge.titres, []); assert.equal(retire.libelle, 'Retiré le titre « H\u{00F4}tesse » de la table 3, siège 1'); refuse(() => executer('retirerTitre', retire.charge, { table: 3, siege: 1 }), 'TITRE_ABSENT', { remede: null }); }); test('mode table : un titre sans siège prend le plus petit sans titre ; une table de 2 aux deux sièges titrés refuse AUCUN_SIEGE_SANS_TITRE', () => { let charge = chargeTitres(); charge = executer('poserTitre', charge, { table: 1, siege: 2, libelle: 'accueil' }).charge; charge = executer('poserTitre', charge, { table: 1, siege: null, libelle: 'animation' }).charge; assert.deepEqual(charge.titres.at(-1), { table: 1, siege: 1, libelle: 'animation' }); refuse(() => executer('poserTitre', charge, { table: 1, siege: null, libelle: 'veille' }), 'AUCUN_SIEGE_SANS_TITRE', { table: 1, capacite: 2, remede: { commande: 'changerPlacesTable', arguments: { id: 1, sieges: 3, origine: 'champ' } }, }); }); test('convertirEnTous : refusé sans les R réservations « tour » à une table ; libellé', () => { let charge = chargeTitres(); refuse(() => executer('convertirEnTous', charge, { participant: 1, table: 3 }), 'CONVERSION_IMPOSSIBLE', { remede: null }); for (const tour of [1, 2, 3]) charge = executer('reserver', charge, { participant: 1, table: 3, siege: 5, portee: 'tour', tour }).charge; const { charge: convertie, libelle } = executer('convertirEnTous', charge, { participant: 1, table: 3 }); assert.deepEqual(convertie.reservations, [resa(1, 3, 5)]); assert.equal(libelle, 'Converti en « tous les tours » : Iris Ombrelle, table 3'); }); }); describe('commandes des places : les invariants des réservations (§ 5.9, § 14.12)', () => { // Σ (c_t − k_t) − n de l'instance, et ses ancrés par table. const ecart = ({ capacite, ancresParTable, n }) => capacite.reduce((somme, c) => somme + c, 0) - ancresParTable.reduce((somme, k) => somme + k, 0) - n; // Une petite configuration tirée : deux à quatre tables de 2 à 5 places, // un à trois tours, trois à douze personnes dont parfois une exclue. function configurationTiree(rng) { const tours = 1 + rng.borne(3); const charge = creerCharge({ id: 'evt-grille', nom: 'Soirée des Lucioles', siegesParDefaut: 2 + rng.borne(4), tours }); charge.reglages.attribuerSieges = rng.borne(2) === 1; const nombre = 3 + rng.borne(10); charge.participants = Array.from({ length: nombre }, (_, rang) => personne(rang + 1, { exclu: rng.borne(6) === 0 })); const tables = 2 + rng.borne(3); charge.tables = Array.from({ length: tables }, (_, rang) => ({ id: rang + 1, numero: rang + 1, sieges: rng.borne(2) === 0 ? null : 2 + rng.borne(4), forme: 'ronde', position: { x: 300 * rang, y: 0 }, })); charge.prochainsIds = { participant: nombre + 1, table: tables + 1, proposition: 1 }; return charge; } // Une commande de réservation tirée sur la charge, et les tables qu'elle // touche : arguments valides en forme, que la commande accepte ou refuse. function commandeTiree(rng, charge) { const R = charge.evenement.tours; const participant = 1 + rng.borne(charge.participants.length); const idTable = 1 + rng.borne(charge.tables.length); const siege = rng.borne(3) === 0 ? null : 1 + rng.borne(5); const portee = rng.borne(2) === 0 ? 'tous' : 'tour'; const tour = portee === 'tous' ? null : 1 + rng.borne(R); const existante = charge.reservations.length > 0 ? charge.reservations[rng.borne(charge.reservations.length)] : null; const choix = rng.borne(6); if (choix <= 1 || existante === null) return ['reserver', { participant, table: idTable, siege, portee, tour }, [idTable]]; if (choix === 2) { const { participant: p, table: t, portee: po, tour: to } = existante; return ['retirerReservation', { participant: p, table: t, portee: po, tour: to }, [t]]; } if (choix === 3) { const { participant: p, table: t, portee: po, tour: to } = existante; return ['deplacerReservation', { participant: p, de: { table: t, portee: po, tour: to }, vers: { table: idTable, siege } }, [t, idTable]]; } if (choix === 4) { const possibles = conversionsPossibles(charge); const cible = possibles.length > 0 ? possibles[rng.borne(possibles.length)] : { participant, table: idTable }; return ['convertirEnTous', cible, [cible.table]]; } return ['designer', { participant, table: idTable, siege, libelle: 'animation' }, [idTable]]; } test('Σ (c_t − k_t) − n ne change pas ; la charge rendue se relit et se normalise ; les ancrés ne bougent qu’aux tables touchées', () => { assert.ok(!Object.values(FLUX).includes(7), 'le flux 7 est hors de FLUX'); const rng = creerPcg32(48271, 7); let acceptees = 0; const parCommande = new Map(); for (let essai = 0; essai < 300; essai += 1) { let charge = configurationTiree(rng); for (let geste = 0; geste < 6; geste += 1) { const avant = normaliser(configurationDepuisCharge(charge)); const [nom, args, touchees] = commandeTiree(rng, charge); let rendu; try { rendu = appliquerCommande(nom, charge, args); } catch (erreur) { if (erreur instanceof ErreurCommande) continue; throw erreur; } acceptees += 1; parCommande.set(nom, (parCommande.get(nom) ?? 0) + 1); const texte = serialiser(rendu.charge, { revision: 1, produitVersion: 'essai' }); assert.doesNotThrow(() => analyser(texte), `${nom} ${JSON.stringify(args)}`); const apres = normaliser(configurationDepuisCharge(rendu.charge)); assert.equal(ecart(apres), ecart(avant), `${nom} ${JSON.stringify(args)}`); avant.ancresParTable.forEach((k, t) => { const delta = apres.ancresParTable[t] - k; const idTable = avant.idsTables[t]; if (nom === 'convertirEnTous' || !touchees.includes(idTable)) assert.equal(delta, 0, `${nom} table ${idTable}`); else assert.ok(Math.abs(delta) <= 1, `${nom} table ${idTable} : ${delta}`); }); charge = rendu.charge; } } assert.ok(acceptees >= 200, `${acceptees} gestes acceptés`); for (const nom of ['reserver', 'retirerReservation', 'deplacerReservation', 'convertirEnTous', 'designer']) { assert.ok((parCommande.get(nom) ?? 0) > 0, `${nom} jamais accepté`); } }); }); describe('commandes des places : le retenu (§ 7.1, § 8.2, § 8.9)', () => { test('au tour 2, déplacer, échanger, retirer ne changent que le tour 2 du retenu, jamais la proposition', () => { const charge = figer(chargeRetenue()); const deplace = executer('deplacerDansRetenu', charge, { tour: 2, participant: 1, table: 8, siege: 7 }); assert.equal(deplace.libelle, 'Déplacé Iris Ombrelle de la table 4 à la table 7, tour 2'); assert.deepEqual(listes(deplace.charge, 2), [[null, 8, 9, 10, 11, 12], [7, 2, 3, 4, 5, 6, 1]]); const echange = executer('echangerDansRetenu', charge, { tour: 2, a: 1, b: 2 }); assert.deepEqual(listes(echange.charge, 2), [[2, 8, 9, 10, 11, 12], [7, 1, 3, 4, 5, 6]]); assert.equal(echange.libelle, 'Échangé Iris Ombrelle (table 4) et Théo Pervenche (table 7), tour 2'); const retire = executer('retirerDuTour', charge, { tour: 2, participant: 9 }); assert.equal(retire.libelle, 'Retiré Hortense Quenouille de la table 4, tour 2'); assert.deepEqual(reserveDe(retire.charge, 2), [9]); assert.deepEqual(etatPlaces(retire.charge, { tour: 2 }).reserve, [9]); for (const tour of [1, 3]) assert.deepEqual(etatPlaces(retire.charge, { tour }).reserve, []); for (const { charge: apres } of [deplace, echange, retire]) { assert.deepEqual(apres.propositions, charge.propositions); for (const tour of [1, 3]) assert.deepEqual(apres.retenu.placement[tour - 1], charge.retenu.placement[tour - 1]); assert.deepEqual(fautes(apres.retenu), []); } }); test('sièges attribués : un départ laisse sa chaise vide, et tout autre siège reste le même', () => { const charge = chargeRetenue(); const sans3 = executer('retirerDuTour', charge, { tour: 1, participant: 3 }).charge; assert.deepEqual(listes(sans3, 1)[0], [1, 2, null, 4, 5, 6]); const sans6 = executer('retirerDuTour', sans3, { tour: 1, participant: 6 }).charge; assert.deepEqual(listes(sans6, 1)[0], [1, 2, null, 4, 5]); assert.deepEqual(fautes(sans6.retenu), []); const retour = executer('deplacerDansRetenu', sans6, { tour: 1, participant: 12, table: 5, siege: 3 }).charge; assert.deepEqual(listes(retour, 1), [[1, 2, 12, 4, 5], [7, 8, 9, 10, 11]]); // La personne qui quitte sa table pour une autre libère aussi sa chaise. const quitte = executer('deplacerDansRetenu', charge, { tour: 1, participant: 9, table: 5, siege: 7 }).charge; assert.deepEqual(listes(quitte, 1), [[1, 2, 3, 4, 5, 6, 9], [7, 8, null, 10, 11, 12]]); }); test('sièges attribués : un dépôt sur la chaise 8 d’une table de 8 qui en tient cinq, les rangs sautés à null', () => { const charge = chargeRetenue({ tours: [ [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10, 11, 12]], [[1, 8, 9, 10, 11, 12], [7, 2, 3, 4, 5, 6]], [[1, 2, 3, 7, 8, 9], [4, 5, 6, 10, 11, 12]], ], }); const { charge: apres } = executer('deplacerDansRetenu', charge, { tour: 1, participant: 6, table: 5, siege: 8 }); assert.deepEqual(listes(apres, 1), [[1, 2, 3, 4, 5, null, null, 6], [null, 7, 8, 9, 10, 11, 12]]); assert.deepEqual(fautes(apres.retenu), []); refuse(() => executer('deplacerDansRetenu', charge, { tour: 1, participant: 6, table: 5, siege: 2 }), 'SIEGE_OCCUPE', { table: 5, siege: 2, tour: 1, occupant: 2, remede: { commande: 'echangerDansRetenu', arguments: { tour: 1, a: 6, b: 2 } }, }); }); test('sans attribution : la liste reste triée, une table pleine de présents refuse TABLE_PLEINE', () => { const charge = figer(chargeRetenue({ siegesAttribues: false, capacite: 6 })); // La table 4 tient six présents : refusée ; libérée d'un, elle reçoit. refuse(() => executer('deplacerDansRetenu', charge, { tour: 1, participant: 12, table: 5, siege: null }), 'TABLE_PLEINE', { table: 5, tour: 1, capacite: 6, remede: { commande: 'changerPlacesTable', arguments: { id: 5, sieges: 7, origine: 'champ' } }, }); const libre = executer('retirerDuTour', charge, { tour: 1, participant: 2 }).charge; assert.deepEqual(listes(libre, 1)[0], [1, 3, 4, 5, 6]); const recue = executer('deplacerDansRetenu', libre, { tour: 1, participant: 12, table: 5, siege: null }).charge; assert.deepEqual(listes(recue, 1), [[1, 3, 4, 5, 6, 12], [7, 8, 9, 10, 11]]); }); test('une retouche qui assiérait cinq personnes à une table de 4 au tour 2 est refusée, sans écriture', () => { const charge = figer( chargeRetenue({ siegesAttribues: false, capacite: 4, tours: [ [[1, 2, 3, 4], [5, 6, 7, 8]], [[1, 2, 3, 4], [5, 6, 7, 8]], [[1, 2, 3, 4], [5, 6, 7, 8]], ], }), ); refuse(() => appliquerCommande('deplacerDansRetenu', charge, { tour: 2, participant: 9, table: 5, siege: null }), 'TABLE_PLEINE'); }); test('la proposition montrée n’est jamais la cible : sans retenu, ou à un tour qu’il ne porte pas, une retouche est une faute du code', () => { const sans = chargeRetenue(); sans.retenu = null; assert.throws(() => executer('retirerDuTour', sans, { tour: 1, participant: 1 }), TypeError); assert.throws(() => executer('retirerDuTour', chargeRetenue(), { tour: 4, participant: 1 }), TypeError); }); test('un retenu fautif refuse ses retouches RETENU_FAUTIF, remède la version précédente', () => { const charge = chargeRetenue(); charge.retenu.placement[0].reserve = [1]; for (const [nom, args] of [ ['deplacerDansRetenu', { tour: 2, participant: 1, table: 8, siege: 7 }], ['echangerDansRetenu', { tour: 2, a: 1, b: 2 }], ['retirerDuTour', { tour: 2, participant: 1 }], ]) { refuse(() => executer(nom, charge, args), 'RETENU_FAUTIF', { remede: { geste: 'ouvrirSecours', source: 'precedent' } }); } }); test('une place réservée à B : refusée PLACE_RESERVEE par dépôt, puis par échange', () => { const charge = chargeRetenue({ reservations: [resa(2, 8, 8, 2), resa(7, 8, 1)] }); refuse(() => executer('deplacerDansRetenu', charge, { tour: 2, participant: 9, table: 8, siege: 8 }), 'PLACE_RESERVEE', { titulaire: 2, table: 8, siege: 8, }); refuse(() => executer('echangerDansRetenu', charge, { tour: 2, a: 9, b: 7 }), 'PLACE_RESERVEE', { titulaire: 7, siege: 1 }); // Contre sa propre réservation : RESERVEE_AILLEURS. refuse(() => executer('deplacerDansRetenu', charge, { tour: 2, participant: 7, table: 5, siege: 7 }), 'RESERVEE_AILLEURS', { participant: 7, tour: 2, }); }); }); describe('commandes des places : le retenu et la dérive (§ 9, § 4.4)', () => { test('une personne ajoutée après la retenue attend en réserve ; déplacée au tour 2, elle y est assise et attend aux autres', () => { const charge = chargeRetenue(); charge.participants.push(personne(13)); charge.prochainsIds.participant = 14; for (const tour of [1, 2, 3]) assert.ok(etatPlaces(charge, { tour }).reserve.includes(13)); const { charge: apres, libelle } = executer('deplacerDansRetenu', charge, { tour: 2, participant: 13, table: 8, siege: 8 }); assert.equal(libelle, 'Déplacé Lucioles Zéphyrin de la réserve à la table 7, tour 2'); assert.deepEqual(listes(apres, 2)[1], [7, 2, 3, 4, 5, 6, null, 13]); assert.deepEqual(reserveDe(apres, 1), [13]); assert.deepEqual(reserveDe(apres, 3), [13]); assert.ok(apres.retenu.participants.includes(13)); assert.deepEqual(fautes(apres.retenu), []); assert.ok(!derive(apres.retenu, apres).some(({ participant }) => participant === 13)); assert.equal(etatPlaces(apres, { tour: 2 }).tables[1].chaises[7].occupant, 13); }); test('Iris, exclue après la retenue, au siège 3 du tour 2 : y déposer Théo l’envoie en réserve, sans renuméroter personne', () => { const charge = chargeRetenue({ tours: [ [[1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 11, 12]], [[8, 9, 1, 10, 11, 12], [7, 2, 3, 4, 5, 6]], [[1, 2, 3, 7, 8, 9], [4, 5, 6, 10, 11, 12]], ], }); charge.participants[0].exclu = true; const { charge: apres } = executer('deplacerDansRetenu', charge, { tour: 2, participant: 2, table: 5, siege: 3 }); assert.deepEqual(listes(apres, 2), [[8, 9, 2, 10, 11, 12], [7, null, 3, 4, 5, 6]]); assert.deepEqual(reserveDe(apres, 2), [1]); refuse(() => executer('deplacerDansRetenu', charge, { tour: 2, participant: 1, table: 8, siege: 8 }), 'PERSONNE_EXCLUE', { participant: 1, }); }); test('sans attribution, une table pleine qui porte un exclu reçoit le dépôt ; l’exclu au plus petit identifiant va en réserve', () => { const charge = chargeRetenue({ siegesAttribues: false, capacite: 6 }); charge.participants[1].exclu = true; charge.participants[3].exclu = true; const { charge: apres } = executer('deplacerDansRetenu', charge, { tour: 1, participant: 12, table: 5, siege: null }); assert.deepEqual(listes(apres, 1), [[1, 3, 4, 5, 6, 12], [7, 8, 9, 10, 11]]); assert.deepEqual(reserveDe(apres, 1), [2]); }); test('une table créée après la retenue, écrite comme creerTable la laissera, reçoit un dépôt', () => { const charge = chargeRetenue(); charge.tables.push({ id: 9, numero: 9, sieges: null, forme: 'ronde', position: { x: 800, y: 0 } }); charge.prochainsIds.table = 10; charge.retenu.tables.push(9); charge.retenu.capacites.push(8); for (const tour of charge.retenu.placement) tour.sieges.push([]); const { charge: apres } = executer('deplacerDansRetenu', charge, { tour: 2, participant: 4, table: 9, siege: 1 }); assert.deepEqual(listes(apres, 2), [[1, 8, 9, 10, 11, 12], [7, 2, 3, null, 5, 6], [4]]); assert.deepEqual(fautes(apres.retenu), []); }); test('une table supprimée après la retenue : ses occupants attendent en réserve, et le remède de SIEGE_OCCUPE s’exécute tel quel', () => { const charge = chargeRetenue(); charge.tables = charge.tables.filter(({ id }) => id !== 8); assert.deepEqual(etatPlaces(charge, { tour: 1 }).reserve, [7, 8, 9, 10, 11, 12]); const occupe = refuse(() => executer('deplacerDansRetenu', charge, { tour: 1, participant: 7, table: 5, siege: 1 }), 'SIEGE_OCCUPE', { occupant: 1, }); const { commande, arguments: args } = occupe.details.remede; assert.deepEqual([commande, args], ['echangerDansRetenu', { tour: 1, a: 7, b: 1 }]); const { charge: apres } = executer(commande, charge, args); // 7 prend la chaise 1 ; 1 attend en réserve, et personne n'entre dans la // liste de la table que la charge n'a plus. assert.deepEqual(listes(apres, 1), [[7, 2, 3, 4, 5, 6], [null, 8, 9, 10, 11, 12]]); assert.deepEqual(reserveDe(apres, 1), [1]); assert.deepEqual(listes(apres, 2), listes(charge, 2)); const etat = etatPlaces(apres, { tour: 1 }); assert.equal(etat.tables.length, 1); assert.equal(etat.tables[0].chaises[0].occupant, 7); assert.deepEqual(etat.reserve, [1, 8, 9, 10, 11, 12]); }); test('une table réduite après la retenue : un rang au-delà de sa capacité se lit comme la réserve, dans l’échange aussi', () => { const charge = chargeRetenue(); charge.tables[0].sieges = 4; assert.deepEqual(etatPlaces(charge, { tour: 1 }).reserve, [5, 6]); const occupe = refuse(() => executer('deplacerDansRetenu', charge, { tour: 1, participant: 5, table: 5, siege: 1 }), 'SIEGE_OCCUPE', { occupant: 1, }); const { charge: apres } = executer(occupe.details.remede.commande, charge, occupe.details.remede.arguments); assert.deepEqual(listes(apres, 1)[0], [5, 2, 3, 4, null, 6]); assert.deepEqual(reserveDe(apres, 1), [1]); assert.deepEqual(etatPlaces(apres, { tour: 1 }).reserve, [1, 6]); }); test('sans attribution, une table réduite après la retenue : une personne que le plan montre en réserve s’y trouve pour déplacer et retirer', () => { const charge = chargeRetenue({ siegesAttribues: false }); charge.tables[0].sieges = 4; assert.deepEqual(etatPlaces(charge, { tour: 1 }).reserve, [5, 6]); // La table affichée assied quatre présents : pleine, et non SANS_EFFET. const pleine = refuse(() => executer('deplacerDansRetenu', charge, { tour: 1, participant: 6, table: 5, siege: null }), 'TABLE_PLEINE', { table: 5, tour: 1, capacite: 4, }); assert.deepEqual(pleine.details.remede, { commande: 'changerPlacesTable', arguments: { id: 5, sieges: 5, origine: 'champ' } }); // Déjà en réserve à l'affichage : retirer du tour n'a pas d'effet. refuse(() => executer('retirerDuTour', charge, { tour: 1, participant: 6 }), 'SANS_EFFET'); // Vers une autre table, elle part de la réserve. const { charge: apres, libelle } = executer('deplacerDansRetenu', charge, { tour: 1, participant: 6, table: 8, siege: null }); assert.equal(libelle, 'Déplacé Capucine Sarcelle de la réserve à la table 7, tour 1'); assert.deepEqual(listes(apres, 1), [[1, 2, 3, 4], [6, 7, 8, 9, 10, 11, 12]]); assert.deepEqual(reserveDe(apres, 1), [5]); assert.deepEqual(etatPlaces(apres, { tour: 1 }).reserve, [5]); }); test('sans attribution, une table réduite après la retenue : chaque geste verse d’abord en réserve ceux que le plan n’y montre pas, et n’assied que ceux qu’il nomme', () => { const charge = chargeRetenue({ siegesAttribues: false }); charge.tables[0].sieges = 4; figer(charge); const occupants = (apres) => etatPlaces(apres, { tour: 1 }).tables.map(({ chaises }) => chaises.map(({ occupant }) => occupant)); assert.deepEqual(occupants(charge), [ [1, 2, 3, 4], [7, 8, 9, 10, 11, 12, null, null], ]); assert.deepEqual(etatPlaces(charge, { tour: 1 }).reserve, [5, 6]); // 6, en réserve à l'affichage, prend la chaise de 2 ; 5 reste en réserve. const echange = executer('echangerDansRetenu', charge, { tour: 1, a: 6, b: 2 }).charge; assert.deepEqual(occupants(echange), [ [1, 3, 4, 6], [7, 8, 9, 10, 11, 12, null, null], ]); assert.deepEqual(etatPlaces(echange, { tour: 1 }).reserve, [2, 5]); assert.deepEqual(listes(echange, 1), [[1, 3, 4, 6], [7, 8, 9, 10, 11, 12]]); assert.deepEqual(reserveDe(echange, 1), [2, 5]); // 7, venu d'une autre table, prend la chaise de 2 et s'y voit assis. const croise = executer('echangerDansRetenu', charge, { tour: 1, a: 7, b: 2 }).charge; assert.deepEqual(occupants(croise), [ [1, 3, 4, 7], [2, 8, 9, 10, 11, 12, null, null], ]); assert.deepEqual(etatPlaces(croise, { tour: 1 }).reserve, [5, 6]); // 2, de la table réduite, prend la chaise de 7 : la table de a se verse // aussi, et 7 s'assied à la chaise de 2 sans que 5 la lui dispute. const inverse = executer('echangerDansRetenu', charge, { tour: 1, a: 2, b: 7 }).charge; assert.deepEqual(occupants(inverse), [ [1, 3, 4, 7], [2, 8, 9, 10, 11, 12, null, null], ]); assert.deepEqual(etatPlaces(inverse, { tour: 1 }).reserve, [5, 6]); assert.deepEqual(listes(inverse, 1), [[1, 3, 4, 7], [2, 8, 9, 10, 11, 12]]); // Le départ de 2 libère sa chaise : 5 ne s'y assied pas. const retire = executer('retirerDuTour', charge, { tour: 1, participant: 2 }).charge; assert.deepEqual(occupants(retire), [ [1, 3, 4, null], [7, 8, 9, 10, 11, 12, null, null], ]); assert.deepEqual(etatPlaces(retire, { tour: 1 }).reserve, [2, 5, 6]); assert.deepEqual(listes(retire, 1), [[1, 3, 4], [7, 8, 9, 10, 11, 12]]); const deplace = executer('deplacerDansRetenu', charge, { tour: 1, participant: 2, table: 8, siege: null }).charge; assert.deepEqual(occupants(deplace), [ [1, 3, 4, null], [2, 7, 8, 9, 10, 11, 12, null], ]); assert.deepEqual(etatPlaces(deplace, { tour: 1 }).reserve, [5, 6]); assert.deepEqual(listes(deplace, 1), [[1, 3, 4], [2, 7, 8, 9, 10, 11, 12]]); for (const apres of [echange, croise, inverse, retire, deplace]) { assert.deepEqual(fautes(apres.retenu), []); for (const tour of [2, 3]) assert.deepEqual(apres.retenu.placement[tour - 1], charge.retenu.placement[tour - 1]); } }); test('sans attribution, une table réduite : la personne réservée à cette table y reste assise, et un autre départ ne la verse pas en réserve', () => { // 6, réservé à la table 4 sans siège, est au-delà de ses quatre places // dans la liste : l'affichage l'assied avant 4 et 5, sans réservation. const charge = chargeRetenue({ siegesAttribues: false, reservations: [resa(6, 5, null)] }); charge.tables[0].sieges = 4; figer(charge); const occupantsTable4 = (etat) => etat.tables[0].chaises.map(({ occupant }) => occupant); assert.deepEqual(occupantsTable4(etatPlaces(charge, { tour: 1 })), [1, 2, 3, 6]); assert.deepEqual(etatPlaces(charge, { tour: 1 }).reserve, [4, 5]); const { charge: apres } = executer('retirerDuTour', charge, { tour: 1, participant: 2 }); // Seule la chaise que 2 libère reste vide. assert.deepEqual(occupantsTable4(etatPlaces(apres, { tour: 1 })), [1, 3, 6, null]); assert.deepEqual(listes(apres, 1), [[1, 3, 6], [7, 8, 9, 10, 11, 12]]); assert.deepEqual(reserveDe(apres, 1), [2, 4, 5]); assert.deepEqual(etatPlaces(apres, { tour: 1 }).reserve, [2, 4, 5]); assert.deepEqual( avertissementsApres(charge, apres).filter(({ code }) => code === 'RESERVATION_NON_HONOREE'), [], ); }); test('sièges attribués, une table réduite après la retenue : le rang au-delà de sa capacité part de la réserve vers une chaise libre', () => { const charge = chargeRetenue({ tours: [ [[1, null, 3, 4, 5, 6], [7, 8, 9, 10, 11, 12]], [[1, 8, 9, 10, 11, 12], [7, 2, 3, 4, 5, 6]], [[1, 2, 3, 7, 8, 9], [4, 5, 6, 10, 11, 12]], ], }); charge.tables[0].sieges = 4; assert.deepEqual(etatPlaces(charge, { tour: 1 }).reserve, [2, 5, 6]); refuse(() => executer('retirerDuTour', charge, { tour: 1, participant: 5 }), 'SANS_EFFET'); const { charge: apres, libelle } = executer('deplacerDansRetenu', charge, { tour: 1, participant: 5, table: 5, siege: 2 }); assert.equal(libelle, 'Déplacé Anouk Bruyère de la réserve à la table 4, tour 1'); assert.deepEqual(listes(apres, 1)[0], [1, 5, 3, 4, null, 6]); assert.deepEqual(reserveDe(apres, 1), [2]); assert.deepEqual(etatPlaces(apres, { tour: 1 }).reserve, [2, 6]); assert.deepEqual(fautes(apres.retenu), []); }); }); describe('commandes des places : le vérificateur d’invariants du retenu (§ 14.12, § 14.15)', () => { test('retirer du tour une personne réservée à sa table : INVARIANT_VIOLE, les seules violations nouvelles, sans écriture', () => { // Le retenu reçu porte déjà une violation : 7, réservé à la table 4 au // tour 2, y est assis à la table 7 ; elle n'entre pas dans le refus. const charge = figer(chargeRetenue({ reservations: [resa(4, 5, null, 1), resa(7, 5, null, 2)] })); refuse(() => executer('retirerDuTour', charge, { tour: 1, participant: 4 }), 'INVARIANT_VIOLE', { proposition: 1, violations: [{ code: 'RESERVATION', participant: 4, table: 5, tour: 1 }], remede: null, }); const { charge: apres } = executer('retirerDuTour', charge, { tour: 1, participant: 5 }); assert.deepEqual(reserveDe(apres, 1), [5]); }); test('une violation déjà présente, celle d’une dérive, ne bloque pas la retouche suivante', () => { const charge = chargeRetenue({ reservations: [resa(4, 8, null, 1)] }); charge.participants.push(personne(13)); charge.prochainsIds.participant = 14; const { charge: apres } = executer('retirerDuTour', charge, { tour: 2, participant: 9 }); assert.deepEqual(reserveDe(apres, 2), [9]); const { charge: ensuite } = executer('echangerDansRetenu', apres, { tour: 2, a: 9, b: 10 }); assert.deepEqual(reserveDe(ensuite, 2), [10]); }); }); describe('enregistrerGeneration : les sièges réservés dans une génération (§ 4, § 5.3, § 8.9)', () => { // Douze personnes, deux tables, Théo réservé « tous » au siège siege de la // table 2, sièges attribués. function chargeGeneration({ siege, capacite = 8, attribuerSieges = true }) { const charge = creerCharge({ id: 'evt-generation', nom: 'Soirée des Lucioles', siegesParDefaut: capacite, tours: 3 }); charge.reglages.attribuerSieges = attribuerSieges; charge.participants = DOUZE.map((id) => personne(id)); charge.tables = [ { id: 1, numero: 1, sieges: null, forme: 'ronde', position: { x: 0, y: 0 } }, { id: 2, numero: 2, sieges: null, forme: 'ronde', position: { x: 400, y: 0 } }, ]; charge.reservations = [resa(2, 2, siege)]; charge.prochainsIds = { participant: 13, table: 3, proposition: 1 }; return charge; } test('Théo, réservé au siège 3 de la table 2, y prend le rang 3 à chaque tour de chaque proposition', () => { const charge = chargeGeneration({ siege: 3 }); const configuration = configurationDepuisCharge(charge); const propositions = rechercher(configuration, { graine: 7, arret: 60, nombre: 2 }); const { charge: apres } = executer('enregistrerGeneration', charge, { propositions, configuration, produitVersion: VERSION.affichee }); assert.equal(apres.propositions.length, 2); for (const proposition of apres.propositions) { assert.deepEqual(fautes(proposition), []); const rang = proposition.tables.indexOf(2); assert.equal(proposition.placement.length, 3); for (const { sieges } of proposition.placement) { assert.equal(sieges[rang][2], 2); assert.notEqual(sieges[rang].at(-1), null); } } }); test('réservé au siège 8 d’une table de 8 qui en assied six : au rang 8, les rangs 6 et 7 à null', () => { const charge = chargeGeneration({ siege: 8 }); const configuration = configurationDepuisCharge(charge); const tour = { tables: [1, 2], tours: [[1, 3, 4, 5, 6, 7], [12, 2, 8, 11, 9, 10]] }; const proposition = { id: 1, graine: 48271, arret: 60, historique: 1000, plan: { tables: tour.tables, tours: [tour.tours, tour.tours, tour.tours], reserves: [[], [], []] }, }; const { charge: apres } = executer('enregistrerGeneration', charge, { propositions: [proposition], configuration, produitVersion: VERSION.affichee }); assert.equal(apres.propositions[0].placement.length, 3); for (const { sieges } of apres.propositions[0].placement) { assert.deepEqual(sieges, [[1, 3, 4, 5, 6, 7], [8, 9, 10, 11, 12, null, null, 2]]); } // Sans attribution, la liste reste triée par identifiant. const libre = chargeGeneration({ siege: 8, attribuerSieges: false }); const { charge: triee } = executer('enregistrerGeneration', libre, { propositions: [proposition], configuration: configurationDepuisCharge(libre), produitVersion: VERSION.affichee, }); assert.deepEqual(triee.propositions[0].placement[0].sieges, [[1, 3, 4, 5, 6, 7], [2, 8, 9, 10, 11, 12]]); }); test('une table supprimée et une capacité réduite pendant la génération : la proposition s’enregistre, rangée sur la configuration, en dérive', () => { const charge = chargeGeneration({ siege: 3 }); charge.tables.push({ id: 3, numero: 3, sieges: null, forme: 'ronde', position: { x: 800, y: 0 } }); charge.prochainsIds.table = 4; const configuration = configurationDepuisCharge(charge); const tours = [[1, 3, 4], [5, 6, 2, 7, 8, 9, 10, 11], [12]]; const proposition = { id: 1, graine: 48271, arret: 60, historique: 1000, plan: { tables: [1, 2, 3], tours: [tours, tours, tours], reserves: [[], [], []] }, }; // Entre la recherche et l'enregistrement : la table 3 disparaît, la // table 2 passe à six places alors que la proposition y en assied huit. charge.tables = charge.tables.filter(({ id }) => id !== 3); charge.tables[1].sieges = 6; const { charge: apres } = executer('enregistrerGeneration', charge, { propositions: [proposition], configuration, produitVersion: VERSION.affichee }); assert.equal(apres.propositions.length, 1); const [enregistree] = apres.propositions; assert.deepEqual(fautes(enregistree), []); assert.equal(enregistree.placement.length, 3); for (const { sieges } of enregistree.placement) assert.deepEqual(sieges, [[1, 3, 4], [5, 6, 2, 7, 8, 9, 10, 11], [12]]); assert.ok(derive(enregistree, apres).length > 0); }); test('une table agrandie pendant la génération, puis réservée à un siège nouveau : le rangement suit la capacité de la configuration', () => { const charge = chargeGeneration({ siege: null }); charge.reservations = []; const configuration = configurationDepuisCharge(charge); const tours = [[1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 11, 12]]; const proposition = { id: 1, graine: 48271, arret: 60, historique: 1000, plan: { tables: [1, 2], tours: [tours, tours, tours], reserves: [[], [], []] }, }; // Entre la recherche et l'enregistrement : la table 2 passe à dix places, // et 7 y est réservé au siège 9, au-delà des huit de la configuration. charge.tables[1].sieges = 10; charge.reservations = [resa(7, 2, 9)]; const { charge: apres } = executer('enregistrerGeneration', charge, { propositions: [proposition], configuration, produitVersion: VERSION.affichee }); assert.equal(apres.propositions.length, 1); const [enregistree] = apres.propositions; assert.deepEqual(fautes(enregistree), []); assert.equal(enregistree.placement.length, 3); // Le siège 9 n'existe pas dans la configuration : 7 se range par identifiant. for (const { sieges } of enregistree.placement) assert.deepEqual(sieges, tours); }); }); describe('commandes des places : les textes de leurs codes (§ 14.6)', () => { // Détails d'exemple de chaque code de libelles/places.js, hors libellés // figés, que les commandes composent ci-dessus. const EXEMPLES = new Map([ ['SIEGE_INEXISTANT', [{ table: 2, siege: 9, capacite: 8 }, '9']], ['DEUX_PLACES_MEME_TOUR', [{ participant: 1, tour: 2, autre: { table: 1, siege: null, portee: 'tous', tour: null } }, 'tour 2']], ['PERSONNE_EXCLUE', [{ participant: 3 }, 'exclue']], ['PLACE_RESERVEE', [{ table: 2, siege: 3, tour: 2, titulaire: 2, portee: 'tour' }, 'au tour 2']], ['RESERVEE_AILLEURS', [{ participant: 1, tour: 2, reservation: { table: 1, siege: null, portee: 'tous', tour: null } }, 'tour 2']], ['TABLE_GELEE', [{ table: 1 }, 'réservées']], ['LIBELLE_VIDE', [{}, 'vide']], ['PLACE_TITREE', [{ table: 3, siege: 1, libelle: 'hôte' }, '« hôte »']], ['AUCUN_SIEGE_SANS_TITRE', [{ table: 1, capacite: 2 }, '2 sièges']], ['TITRE_ABSENT', [{ table: 3, siege: 1 }, 'siège 1']], ['RESERVATION_ABSENTE', [{ participant: 1, table: 3, portee: 'tour', tour: 2 }, 'réservation']], ['CONVERSION_IMPOSSIBLE', [{ participant: 1, table: 3 }, 'convertir']], ['PROPOSITION_MONTREE', [{ proposition: 4 }, 'n° 4']], ['SIEGE_OCCUPE', [{ table: 5, siege: 2, tour: 1, occupant: 2 }, 'siège 2']], ['TABLE_PLEINE', [{ table: 5, tour: 1, capacite: 6 }, '6 places']], ]); test('chaque code de la table des places a son texte, composé de ses détails', () => { const codes = [...TEXTES_DES_PLACES.keys()].filter((cle) => /^[A-Z][A-Z0-9_]*$/.test(cle) && !cle.startsWith('ENTREE_')); assert.ok(codes.length > 0); assert.deepEqual(codes.sort(), [...EXEMPLES.keys()].sort()); for (const [code, [details, attendu]] of EXEMPLES) { const texte = libelle(code, { ...details, remede: null }); assert.ok(texte.includes(attendu), `${code} : ${texte}`); assert.ok(!texte.includes('undefined'), `${code} : ${texte}`); } assert.ok(libelle('PLACE_RESERVEE', { siege: 3, tour: null, portee: 'tous' }).includes('à tous les tours')); assert.equal(libelle('places.titreParDefaut'), 'animateur'); }); });