// © 2026 TechnoLibre (http://www.technolibre.ca) // License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) // Épreuves des commandes du mobilier (§ 4, § 4.3, § 5.9, § 6, § 7.1, § 8.2, // § 9, § 15.4) : tables, défaut, tours et réglages. Chaque commande acceptée // rend une charge que l'analyse relit et qui se normalise quand la charge // reçue se normalisait ; un refus ne change rien. Les gestes qui comptent // les entrées du journal, ou les avertissements que la séance ajoute, // passent par seance.executer sur le système de fichiers d'épreuve. Les noms // d'épreuve sont inventés ; la version vient du module engendré. import assert from 'node:assert/strict'; import { creerFichiersSimules } from '../../test/fichiers_simules.js'; import { describe, test } from '../../test/lanceur.js'; import { caseLibre } from '../geometrie/tables.js'; import { STATUT, nombrePlacesManquantes, normaliser } from '../moteur/configuration.js'; import { ErreurConfiguration } from '../moteur/erreurs.js'; import { serialiser } from '../stockage/canonique.js'; import { creer as creerDansLeDepot } from '../stockage/depot.js'; import { analyser, capacite, configurationDepuisCharge, creerCharge } from '../stockage/document.js'; import { lireJournal } from '../stockage/journal.js'; import { SUFFIXES } from '../stockage/noms.js'; import { derive, examiner, fautes } from '../stockage/placements.js'; import { VERSION } from '../version.genere.js'; import { invariantViole } from './avertissements.js'; import { COMMANDES, appliquerCommande } from './commandes.js'; import { COMMANDES_MOBILIER } from './commandes_mobilier.js'; import { ErreurCommande } from './erreurs.js'; import { libelle } from './libelles.js'; import { etatPlaces } from './places.js'; import { creerSeance } from './seance.js'; const V1 = VERSION.affichee; const NOM = 'Soirée des Merles'; const DOC = { id: 'documents', chemin: 'C:\\Users\\Exemple\\Documents\\Gestion table tournante Libre' }; 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'], ]; 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, table, siege, tour = null) => ({ participant, table, siege, portee: tour === null ? 'tous' : 'tour', tour }); const suite = (de, a) => Array.from({ length: a - de + 1 }, (_, rang) => de + rang); const afficherTour = (tour) => ({ geste: 'afficherTour', tour, proposition: null }); // Une charge : les tables données ({id, sieges}, numéro égal à l'identifiant // sauf indication), n personnes dont les exclus, R tours, un défaut. function chargeDe({ tables = [], n = 0, exclus = [], tours = 2, defaut = 8, attribuerSieges = false, reservations = [], titres = [] } = {}) { const charge = creerCharge({ id: 'evt-mobilier', nom: NOM, siegesParDefaut: defaut, tours }); charge.reglages.attribuerSieges = attribuerSieges; charge.participants = Array.from({ length: n }, (_, rang) => personne(rang + 1, { exclu: exclus.includes(rang + 1) })); charge.tables = tables.map(({ id, numero = id, sieges = null }) => ({ id, numero, sieges, forme: 'ronde', position: { x: 300 * id, y: 0 } })); charge.reservations = reservations; charge.titres = titres; charge.prochainsIds = { participant: n + 1, table: Math.max(0, ...tables.map(({ id }) => id)) + 1, proposition: 1 }; return charge; } // Ajoute la proposition id, ses listes dans l'ordre de charge.tables, et la // retient quand retenu est vrai. function placer(charge, listes, { id, retenu = false }) { const participants = charge.participants.filter(({ exclu }) => !exclu).map(({ id: p }) => p); const plan = { siegesAttribues: charge.reglages.attribuerSieges, tables: charge.tables.map(({ id: t }) => t), capacites: charge.tables.map((table) => capacite(charge, table)), tours: listes.length, participants, placement: listes.map((tour) => ({ sieges: tour, reserve: participants.filter((p) => !tour.flat().includes(p)) })), }; charge.propositions.push({ id, graine: id, arret: 40, historique: 1000, produitVersion: V1, ...plan }); charge.prochainsIds.proposition = id + 1; if (retenu) { charge.retenu = { proposition: id, ...structuredClone(plan) }; charge.evenement.etat = 'retenu'; } else if (charge.evenement.etat === 'brouillon') { charge.evenement.etat = 'propose'; } return charge; } // 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; } // Vrai quand la configuration de la charge se normalise. function seNormalise(charge) { try { normaliser(configurationDepuisCharge(charge)); return true; } catch (erreur) { if (erreur instanceof ErreurConfiguration) return false; throw erreur; } } // Une commande acceptée, sur une copie gelée de la charge : la charge rendue // se relit, et se normalise quand la charge reçue se normalisait. function accepter(nom, charge, args) { const recue = figer(structuredClone(charge)); const rendu = appliquerCommande(nom, recue, args); analyser(serialiser(rendu.charge, { revision: 1, produitVersion: 'essai' })); if (seNormalise(recue)) assert.ok(seNormalise(rendu.charge), `${nom} : la charge rendue ne se normalise plus`); return rendu; } // Le refus d'une ErreurCommande 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; } // Le même refus, rendu par une promesse. async function refuseAsync(promesse, code, attendus = {}) { let leve = null; try { await promesse; } catch (erreur) { leve = erreur; } return refuse(() => { if (leve !== null) throw leve; }, code, attendus); } // --- La séance d'épreuve ---------------------------------------------------- // Une séance sur le système d'épreuve, l'événement de la charge posé par le // dépôt, ouvert et passé en écriture. entrees() compte les entrées du // journal ; ecritures() les écritures du système. async function seanceSur(charge) { const fs = creerFichiersSimules(); fs.deposerDossier('documents'); await creerDansLeDepot(fs, DOC, charge, { libelle: libelle('ENTREE_CREATION', { nom: NOM }), nomJalon: libelle('JALON_CREATION'), horodatage: '2031-05-17T07:00:00-04:00', produitVersion: V1, }); let tirage = 0; const seance = creerSeance({ fs, racine: DOC, horloge: { horodatage: () => '2031-05-17T08:00:00-04:00' }, identifiant: () => { tirage += 1; return `seance-${tirage}`; }, produitVersion: V1, planifier: () => 1, annuler: () => {}, }); await seance.ouvrir(NOM); await seance.passerEnEcriture(); return { seance, entrees: () => lireJournal(fs.contenu('documents', `${NOM}${SUFFIXES.journal}`)).entrees.length, ecritures: () => fs.compteurs.ecritures, }; } // --- Les épreuves ----------------------------------------------------------- describe('mobilier : le registre (§ 8.2)', () => { test('COMMANDES_MOBILIER porte les huit commandes du mobilier, réunies dans COMMANDES', () => { const noms = ['creerTable', 'supprimerTable', 'deplacerTable', 'changerPlacesTable', 'realignerTable', 'changerDefaut', 'changerTours', 'modifierReglage']; assert.deepEqual([...COMMANDES_MOBILIER.keys()], noms); for (const nom of noms) assert.equal(COMMANDES.get(nom), COMMANDES_MOBILIER.get(nom), nom); }); test('un plan bloqué refuse chacune, PLAN_BLOQUE, remède debloquer', () => { const charge = chargeDe({ tables: [{ id: 1 }], n: 2 }); charge.evenement.etat = 'bloque'; const gestes = [ ['creerTable', { forme: 'ronde' }], ['supprimerTable', { id: 1 }], ['deplacerTable', { id: 1, position: { x: 5, y: 5 } }], ['changerPlacesTable', { id: 1, sieges: 6, origine: 'champ' }], ['realignerTable', { id: 1 }], ['changerDefaut', { sieges: 6, realigner: false }], ['changerTours', { tours: 3 }], ['modifierReglage', { cle: 'nouveauxVoisins', valeur: false }], ]; assert.equal(gestes.length, COMMANDES_MOBILIER.size); for (const [nom, args] of gestes) { refuse(() => COMMANDES.get(nom)(charge, args), 'PLAN_BLOQUE', { remede: { geste: 'debloquer' } }); } }); }); describe('mobilier : créer, déplacer et supprimer une table (§ 4, § 6.1, § 9)', () => { test("les identifiants ne se réutilisent jamais ; le numéro suit le plus grand, et le libellé nomme le numéro", () => { let charge = chargeDe(); for (let i = 0; i < 3; i += 1) charge = accepter('creerTable', charge, { forme: 'ronde' }).charge; assert.deepEqual(charge.tables.map(({ id, numero }) => [id, numero]), [[1, 1], [2, 2], [3, 3]]); charge = accepter('supprimerTable', charge, { id: 3 }).charge; const cree = accepter('creerTable', charge, { forme: 'carree' }); assert.equal(cree.libelle, 'Ajouté la table 3'); assert.deepEqual(cree.charge.tables.at(-1), { id: 4, numero: 3, sieges: null, forme: 'carree', position: caseLibre(charge.tables.map(({ position }) => position)), }); assert.equal(cree.charge.prochainsIds.table, 5); const deplace = accepter('deplacerTable', cree.charge, { id: 4, position: { x: 612.4, y: -0.4 } }); assert.equal(deplace.libelle, 'Déplacé la table 3'); assert.deepEqual(deplace.charge.tables.at(-1).position, { x: 612, y: 0 }); assert.ok(Object.is(deplace.charge.tables.at(-1).position.y, 0)); // Une position que l'arrondi ne change pas : le geste est sans effet. refuse(() => appliquerCommande('deplacerTable', deplace.charge, { id: 4, position: { x: 611.6, y: 0.2 } }), 'SANS_EFFET'); // Supprimer une table du milieu : le numéro suit encore le plus grand, // et non le nombre de tables. let milieu = chargeDe(); for (let i = 0; i < 3; i += 1) milieu = accepter('creerTable', milieu, { forme: 'ronde' }).charge; milieu = accepter('supprimerTable', milieu, { id: 2 }).charge; const apresMilieu = accepter('creerTable', milieu, { forme: 'ronde' }); assert.equal(apresMilieu.libelle, 'Ajouté la table 4'); assert.deepEqual(apresMilieu.charge.tables.map(({ id, numero }) => [id, numero]), [[1, 1], [3, 3], [4, 4]]); }); test('une forme inconnue, une table inconnue ou une position non finie sont des fautes du code', () => { const charge = chargeDe({ tables: [{ id: 1 }] }); assert.throws(() => COMMANDES.get('creerTable')(charge, { forme: 'ovale' }), TypeError); assert.throws(() => COMMANDES.get('supprimerTable')(charge, { id: 7 }), TypeError); assert.throws(() => COMMANDES.get('deplacerTable')(charge, { id: 1, position: { x: Number.NaN, y: 0 } }), TypeError); }); test('une table créée suit le défaut : sieges null, 8 places sous un défaut de 8, 10 sous un défaut de 10', () => { for (const defaut of [8, 10]) { const { charge } = accepter('creerTable', chargeDe({ defaut }), { forme: 'ronde' }); assert.equal(charge.tables[0].sieges, null); assert.equal(capacite(charge, charge.tables[0]), defaut); } const huit = accepter('creerTable', chargeDe(), { forme: 'ronde' }).charge; const dix = accepter('changerDefaut', huit, { sieges: 10, realigner: false }).charge; assert.equal(capacite(dix, dix.tables[0]), 10); }); test('une table créée après la retenue entre au retenu, vide à chaque tour ; les propositions dérivent TABLE_AJOUTEE', () => { const charge = chargeDe({ tables: [{ id: 1 }, { id: 2 }], n: 6 }); const listes = [[[1, 2, 3], [4, 5, 6]], [[1, 4, 5], [2, 3, 6]]]; placer(charge, listes, { id: 1 }); placer(charge, listes, { id: 2, retenu: true }); const { charge: apres, libelle: texte } = accepter('creerTable', charge, { forme: 'ronde' }); assert.equal(texte, 'Ajouté la table 3'); assert.deepEqual(apres.retenu.tables, [1, 2, 3]); assert.deepEqual(apres.retenu.capacites, [8, 8, 8]); for (const { sieges } of apres.retenu.placement) assert.deepEqual(sieges.at(-1), []); assert.deepEqual(fautes(apres.retenu), []); assert.deepEqual(derive(apres.retenu, apres), []); assert.deepEqual(apres.propositions, charge.propositions); assert.deepEqual(examiner(apres).derives, [ { id: 1, raisons: [{ code: 'TABLE_AJOUTEE', table: 3 }] }, { id: 2, raisons: [{ code: 'TABLE_AJOUTEE', table: 3 }] }, ]); // Un retenu fautif ne se réécrit pas. const fautif = structuredClone(charge); fautif.retenu.capacites = [8]; const sansRetouche = accepter('creerTable', fautif, { forme: 'ronde' }).charge; assert.deepEqual(sansRetouche.retenu, fautif.retenu); }); test("identifiant contre numéro : supprimer la table 2 laisse la réservation et les listes de la table 3 du retenu", () => { const charge = chargeDe({ tables: [{ id: 1 }, { id: 2 }, { id: 3 }], n: 6, reservations: [resa(1, 3, null)] }); placer(charge, [[[2, 3], [4, 5], [1, 6]], [[4, 6], [2, 3], [1, 5]]], { id: 1, retenu: true }); const { charge: apres, libelle: texte } = accepter('supprimerTable', charge, { id: 2 }); assert.equal(texte, 'Supprimé la table 2'); assert.deepEqual(apres.tables.map(({ id, numero }) => [id, numero]), [[1, 1], [3, 3]]); assert.deepEqual(apres.reservations, [resa(1, 3, null)]); assert.deepEqual(apres.retenu, charge.retenu); assert.ok(examiner(apres).retenu.raisons.some(({ code, table }) => code === 'TABLE_SUPPRIMEE' && table === 2)); }); test("supprimer une table garnie retire, dans la même entrée, ses réservations et ses titres", async () => { const charge = chargeDe({ tables: [{ id: 1 }, { id: 2 }], n: 4, reservations: [resa(2, 2, 2), resa(1, 1, null), resa(3, 2, null, 1)], titres: [{ table: 2, siege: 2, libelle: 'animateur' }, { table: 1, siege: 1, libelle: 'accueil' }], }); const { seance, entrees } = await seanceSur(charge); const avant = entrees(); const rendu = await seance.executer('supprimerTable', { id: 2 }); assert.equal(rendu.libelle, 'Supprimé la table 2 ; retirés : 2 réservations et 1 titre'); assert.equal(entrees() - avant, 1); assert.deepEqual(seance.charge.reservations, [resa(1, 1, null)]); assert.deepEqual(seance.charge.titres, [{ table: 1, siege: 1, libelle: 'accueil' }]); // Au singulier : une réservation, un titre ; une table nue ne retire rien. const seule = await seance.executer('supprimerTable', { id: 1 }); assert.equal(seule.libelle, 'Supprimé la table 1 ; retirés : 1 réservation et 1 titre'); const vide = accepter('supprimerTable', chargeDe({ tables: [{ id: 4 }] }), { id: 4 }); assert.equal(vide.libelle, 'Supprimé la table 4'); }); test('le manque : supprimer une table de 8 sous Σ c = N = 16 avertit PLACES_MANQUANTES {8} ; ramener une table à 6, {2}', async () => { const charge = chargeDe({ tables: [{ id: 1 }, { id: 2 }], n: 16 }); const un = await seanceSur(charge); const rendu = await un.seance.executer('supprimerTable', { id: 2 }); assert.equal(nombrePlacesManquantes(normaliser(configurationDepuisCharge(un.seance.charge))), 8); assert.deepEqual(rendu.avertissements, [{ code: 'PLACES_MANQUANTES', details: { placesManquantes: 8 } }]); const deux = await seanceSur(charge); const reduit = await deux.seance.executer('changerPlacesTable', { id: 1, sieges: 6, origine: 'champ' }); assert.deepEqual(reduit.avertissements, [{ code: 'PLACES_MANQUANTES', details: { placesManquantes: 2 } }]); }); }); describe("mobilier : les places d'une table, par le champ et par la poignée (§ 6.2, § 7.1)", () => { test('les saisies : « 1 » et « 0 » refusent MOINS_DE_DEUX ; un texte non entier, NON_ENTIER ; « 2 » passe', () => { const charge = chargeDe({ tables: [{ id: 1 }], n: 2 }); for (const sieges of ['1', '0']) { refuse(() => COMMANDES.get('changerPlacesTable')(charge, { id: 1, sieges, origine: 'champ' }), 'MOINS_DE_DEUX', { remede: null }); } const textes = ['-3', '2.5', '2,5', '8abc', 'huit', '']; assert.equal(textes.length, 6); for (const sieges of textes) { refuse(() => COMMANDES.get('changerPlacesTable')(charge, { id: 1, sieges, origine: 'champ' }), 'NON_ENTIER', { saisie: sieges, remede: null }); } const deux = accepter('changerPlacesTable', charge, { id: 1, sieges: '2', origine: 'champ' }); assert.equal(deux.charge.tables[0].sieges, 2); assert.equal(deux.libelle, 'Table 1 : 2 places'); }); test("une surcharge au défaut est une surcharge ; réaligner la retire, et le libellé nomme le défaut", () => { const charge = chargeDe({ tables: [{ id: 5, numero: 2 }], n: 2 }); const surchargee = accepter('changerPlacesTable', charge, { id: 5, sieges: 8, origine: 'champ' }); assert.equal(surchargee.charge.tables[0].sieges, 8); assert.equal(surchargee.libelle, 'Table 2 : 8 places'); const realignee = accepter('realignerTable', surchargee.charge, { id: 5 }); assert.equal(realignee.charge.tables[0].sieges, null); assert.equal(realignee.libelle, 'Table 2 : suit le défaut (8 places)'); refuse(() => appliquerCommande('realignerTable', realignee.charge, { id: 5 }), 'SANS_EFFET'); }); test("le champ dans le retenu, sièges attribués : les occupants des chaises 7 et 8 passent en réserve, la capacité du retenu suit", () => { const charge = chargeDe({ tables: [{ id: 1 }, { id: 2 }, { id: 3 }], n: 18, attribuerSieges: true }); placer( charge, [ [suite(1, 6), suite(7, 12), suite(13, 18)], [suite(1, 5), suite(6, 10), [18, 11, 12, 13, 14, 15, 16, 17]], ], { id: 1, retenu: true }, ); const chaises = etatPlaces(charge, { tour: 2 }).tables[2].chaises; const personnes = [chaises[6].occupant, chaises[7].occupant]; assert.deepEqual(personnes, [16, 17]); const { charge: apres, avertissements, libelle: texte } = accepter('changerPlacesTable', charge, { id: 3, sieges: 6, origine: 'champ' }); assert.equal(texte, 'Table 3 : 6 places'); assert.deepEqual(avertissements, [{ code: 'SURNUMERAIRES', details: { table: 3, tour: 2, personnes } }]); const etage = apres.retenu.placement[1]; assert.deepEqual(etage.sieges[2], [18, 11, 12, 13, 14, 15]); assert.deepEqual(etage.reserve, [16, 17]); assert.deepEqual([...etage.sieges.flat(), ...etage.reserve].sort((a, b) => a - b), suite(1, 18)); assert.deepEqual(apres.retenu.placement[0], charge.retenu.placement[0]); assert.deepEqual(apres.retenu.capacites, [8, 8, 6]); assert.deepEqual(fautes(apres.retenu), []); assert.deepEqual(derive(apres.retenu, apres), []); assert.deepEqual(apres.propositions, charge.propositions); assert.deepEqual(derive(apres.propositions[0], apres), [{ code: 'CAPACITE_CHANGEE', table: 3, avant: 8, maintenant: 6 }]); }); test("le champ dans le retenu, sièges non attribués : la personne réservée au siège 2 reste, les deux suivantes partent", () => { const charge = chargeDe({ tables: [{ id: 1 }, { id: 2 }, { id: 3 }], n: 18, reservations: [resa(18, 3, 2)] }); placer( charge, [ [suite(1, 6), suite(7, 12), suite(13, 18)], [suite(1, 5), suite(6, 10), suite(11, 18)], ], { id: 1, retenu: true }, ); const { charge: apres, avertissements } = accepter('changerPlacesTable', charge, { id: 3, sieges: 6, origine: 'champ' }); assert.deepEqual(avertissements, [{ code: 'SURNUMERAIRES', details: { table: 3, tour: 2, personnes: [16, 17] } }]); assert.deepEqual(apres.retenu.placement[1].sieges[2], [11, 12, 13, 14, 15, 18]); assert.deepEqual(apres.retenu.placement[1].reserve, [16, 17]); assert.deepEqual(derive(apres.retenu, apres), []); // La personne réservée garde sa place au plan, au siège 2. assert.equal(etatPlaces(apres, { tour: 2 }).tables[2].chaises[1].occupant, 18); }); test('sièges attribués : une liste réduite ne finit jamais par une chaise vide ; fautes(retenu) reste vide', () => { const charge = chargeDe({ tables: [{ id: 1 }, { id: 2 }], n: 12, attribuerSieges: true }); placer(charge, [[suite(1, 6), [7, 8, 9, 10, 11, null, null, 12]]], { id: 1, retenu: true }); for (const sieges of [6, 7]) { const { charge: apres, avertissements } = accepter('changerPlacesTable', charge, { id: 2, sieges, origine: 'champ' }); assert.deepEqual(apres.retenu.placement[0].sieges[1], [7, 8, 9, 10, 11]); assert.deepEqual(apres.retenu.placement[0].reserve, [12]); assert.deepEqual(avertissements, [{ code: 'SURNUMERAIRES', details: { table: 2, tour: 1, personnes: [12] } }]); assert.deepEqual(fautes(apres.retenu), []); } }); test('la poignée : 8 → 9 accepté, la capacité du retenu suit ; sous l\'occupation, SOUS_OCCUPES et rien ne change', () => { const cinq = suite(1, 5); const charge = chargeDe({ tables: [{ id: 1 }], n: 7, tours: 4 }); placer(charge, [cinq, cinq, suite(1, 7), cinq].map((liste) => [liste]), { id: 1, retenu: true }); const neuf = accepter('changerPlacesTable', charge, { id: 1, sieges: 9, origine: 'poignee' }); assert.deepEqual(neuf.charge.retenu.capacites, [9]); assert.deepEqual(neuf.charge.retenu.placement, charge.retenu.placement); assert.deepEqual(neuf.avertissements, []); refuse(() => COMMANDES.get('changerPlacesTable')(charge, { id: 1, sieges: 6, origine: 'poignee' }), 'SOUS_OCCUPES', { table: 1, n: 7, tour: 3, remede: afficherTour(3), }); }); test('réaligner une table surchargée à 10 qui en assied dix, sous un défaut de 8 : deux surnuméraires', () => { const charge = chargeDe({ tables: [{ id: 1, sieges: 10 }, { id: 2 }], n: 16, tours: 1 }); placer(charge, [[suite(1, 10), suite(11, 16)]], { id: 1, retenu: true }); const { charge: apres, avertissements, libelle: texte } = accepter('realignerTable', charge, { id: 1 }); assert.equal(texte, 'Table 1 : suit le défaut (8 places)'); assert.deepEqual(avertissements, [{ code: 'SURNUMERAIRES', details: { table: 1, tour: 1, personnes: [9, 10] } }]); assert.deepEqual(apres.retenu.placement[0].reserve, [9, 10]); assert.deepEqual(apres.retenu.capacites, [8, 8]); assert.deepEqual(derive(apres.retenu, apres), []); }); test("une hausse n'assied personne que le plan ne montrait pas : un retenu déclaré à 10 sur une table de 8", () => { // Avec la personne 9 réservée à la table 1 sans siège : sièges attribués, // son rang 9 ne se montre pas et la hausse ne l'assied pas ; sans // attribution, l'affichage l'élit déjà, et la personne 8 n'est pas montrée. // La dérive du retenu masquait la réservation non tenue de la personne 9 ; // la capacité rejointe, invariantViole la nomme. const nonTenue = { code: 'INVARIANT_VIOLE', details: { proposition: 1, violations: [{ code: 'RESERVATION', participant: 9, table: 1, tour: 1 }] } }; const cas = [ { attribuerSieges: true, reservations: [], liste: suite(1, 8), reserve: [9, 10], invariant: null }, { attribuerSieges: false, reservations: [], liste: suite(1, 8), reserve: [9, 10], invariant: null }, { attribuerSieges: true, reservations: [resa(9, 1, null)], liste: suite(1, 8), reserve: [9, 10], invariant: nonTenue }, { attribuerSieges: false, reservations: [resa(9, 1, null)], liste: [1, 2, 3, 4, 5, 6, 7, 9], reserve: [8, 10], invariant: null }, ]; for (const { attribuerSieges, reservations, liste, reserve, invariant } of cas) { for (const origine of ['champ', 'poignee']) { const charge = chargeDe({ tables: [{ id: 1, sieges: 10 }, { id: 2 }], n: 12, tours: 1, attribuerSieges, reservations }); placer(charge, [[suite(1, 10), [11, 12]]], { id: 1, retenu: true }); charge.tables[0].sieges = null; const montrees = etatPlaces(charge, { tour: 1 }).tables[0].chaises.map(({ occupant }) => occupant); const { charge: apres, avertissements } = accepter('changerPlacesTable', charge, { id: 1, sieges: 10, origine }); const nom = `attribuerSieges ${attribuerSieges}, ${reservations.length} réservation, ${origine}`; assert.deepEqual(montrees, liste, nom); assert.deepEqual(avertissements, [], nom); assert.deepEqual(apres.retenu.placement[0].sieges[0], liste, nom); assert.deepEqual(apres.retenu.placement[0].reserve, reserve, nom); assert.deepEqual(apres.retenu.capacites, [10, 8], nom); assert.deepEqual(derive(apres.retenu, apres), [], nom); assert.equal(invariantViole(charge), null, nom); assert.deepEqual(invariantViole(apres), invariant, nom); } } }); test('un retenu de plus de tours que la soirée se réduit à chacun de ses tours, sans faute', () => { const charge = chargeDe({ tables: [{ id: 1 }, { id: 2 }], n: 12, tours: 3, attribuerSieges: true }); placer(charge, [[suite(1, 6), suite(7, 12)], [suite(1, 6), suite(7, 12)], [suite(1, 4), suite(5, 12)]], { id: 1, retenu: true }); const deux = accepter('changerTours', charge, { tours: 2 }).charge; const { charge: apres, avertissements } = accepter('changerPlacesTable', deux, { id: 2, sieges: 6, origine: 'champ' }); // Le tour 3 n'est plus de la soirée, mais le retenu le porte encore : ses // surnuméraires se nomment comme ceux d'un tour montré. assert.deepEqual(avertissements, [{ code: 'SURNUMERAIRES', details: { table: 2, tour: 3, personnes: [11, 12] } }]); assert.deepEqual(apres.retenu.placement[2].sieges[1], suite(5, 10)); assert.deepEqual(apres.retenu.placement[2].reserve, [11, 12]); assert.deepEqual(fautes(apres.retenu), []); }); test('sans attribution, un tour du retenu que la soirée n\'a plus élit aussi la personne que la table réserve', () => { const charge = chargeDe({ tables: [{ id: 1 }, { id: 2 }], n: 12, tours: 3, reservations: [resa(12, 2, null)] }); placer(charge, [[suite(1, 6), suite(7, 12)], [suite(1, 6), suite(7, 12)], [suite(1, 4), suite(5, 12)]], { id: 1, retenu: true }); const deux = accepter('changerTours', charge, { tours: 2 }).charge; const { charge: apres, avertissements } = accepter('changerPlacesTable', deux, { id: 2, sieges: 6, origine: 'champ' }); assert.deepEqual(avertissements, [{ code: 'SURNUMERAIRES', details: { table: 2, tour: 3, personnes: [10, 11] } }]); assert.deepEqual(apres.retenu.placement[2].sieges[1], [5, 6, 7, 8, 9, 12]); assert.deepEqual(apres.retenu.placement[2].reserve, [10, 11]); assert.deepEqual(fautes(apres.retenu), []); }); test("une réduction acceptée garde chaque réservation que le retenu tenait : invariantViole reste null", () => { // La personne 8, réservée à la table 1 sans siège, est assise à la chaise 8. const gestes = [ ['changerPlacesTable', { id: 1, sieges: 6, origine: 'champ' }], ['changerDefaut', { sieges: 6, realigner: false }], ]; let acceptees = 0; for (const attribuerSieges of [true, false]) { for (const [nom, args] of gestes) { const charge = chargeDe({ tables: [{ id: 1 }, { id: 2 }], n: 12, tours: 1, attribuerSieges, reservations: [resa(8, 1, null)] }); placer(charge, [[suite(1, 8), suite(9, 12)]], { id: 1, retenu: true }); assert.equal(invariantViole(charge), null); const { charge: apres, avertissements } = accepter(nom, charge, args); const cas = `${nom}, attribuerSieges ${attribuerSieges}`; assert.equal(invariantViole(apres), null, cas); // Un non-réservé cède sa chaise : la chaise 6, la plus haute, sous des // sièges attribués ; sinon l'affichage élit d'abord la réservée. assert.deepEqual(apres.retenu.placement[0].sieges[0], [1, 2, 3, 4, 5, 8], cas); assert.deepEqual(apres.retenu.placement[0].reserve, [6, 7], cas); assert.deepEqual(avertissements, [{ code: 'SURNUMERAIRES', details: { table: 1, tour: 1, personnes: [6, 7] } }], cas); assert.deepEqual(derive(apres.retenu, apres), [], cas); acceptees += 1; } } assert.equal(acceptees, 4); // Sièges attribués, une chaise vide dans la capacité reçoit la réservée // avant qu'un non-réservé ne cède la sienne. const vide = chargeDe({ tables: [{ id: 1 }, { id: 2 }], n: 12, tours: 1, attribuerSieges: true, reservations: [resa(8, 1, null)] }); placer(vide, [[[1, 2, 3, null, 5, 6, 7, 8], suite(9, 12)]], { id: 1, retenu: true }); const { charge: apres, avertissements } = accepter('changerPlacesTable', vide, { id: 1, sieges: 6, origine: 'champ' }); assert.equal(invariantViole(apres), null); assert.deepEqual(apres.retenu.placement[0].sieges[0], [1, 2, 3, 8, 5, 6]); assert.deepEqual(apres.retenu.placement[0].reserve, [4, 7]); assert.deepEqual(avertissements, [{ code: 'SURNUMERAIRES', details: { table: 1, tour: 1, personnes: [7] } }]); // Réaligner une table surchargée à 10, la personne 10 réservée sans siège. const dix = chargeDe({ tables: [{ id: 1, sieges: 10 }, { id: 2 }], n: 14, tours: 1, attribuerSieges: true, reservations: [resa(10, 1, null)] }); placer(dix, [[suite(1, 10), suite(11, 14)]], { id: 1, retenu: true }); const realignee = accepter('realignerTable', dix, { id: 1 }); assert.equal(invariantViole(realignee.charge), null); assert.deepEqual(realignee.charge.retenu.placement[0].sieges[0], [1, 2, 3, 4, 5, 6, 7, 10]); assert.deepEqual(realignee.avertissements, [{ code: 'SURNUMERAIRES', details: { table: 1, tour: 1, personnes: [8, 9] } }]); }); test('sièges attribués, le retour des réservés : siège voulu, chaise libre ou d\'un absent, non-réservé du plus haut rang, ordre des rangs, tour couvert', () => { // La table 1, de 8, réduite à 6 par le champ ; les exclus le deviennent // après la retenue. Par tour : la liste qui reste, la réserve, et les // personnes de SURNUMERAIRES. const cas = [ { nom: "une chaise tenue par un absent est libre : la personne 8 la prend, l'absent 2 passe en réserve", reservations: [resa(8, 1, null)], exclus: [2], listes: [[suite(1, 8), suite(9, 12)]], attendus: [[[1, 8, 3, 4, 5, 6], [2, 7], [7]]], }, { nom: 'le siège réservé, libre, passe avant la première chaise libre', reservations: [resa(9, 1, 3), resa(9, 1, 4, 1)], listes: [[[1, null, null, null, 5, 6, 7, 9], [2, 3, 4, 8, 10, 11, 12]]], attendus: [[[1, null, 9, null, 5, 6], [7], [7]]], }, { nom: "le siège réservé est le premier siège non nul des réservations de la personne, dans l'ordre canonique", reservations: [resa(9, 1, null), resa(9, 1, 4, 1)], listes: [[[1, null, null, null, 5, 6, 7, 9], [2, 3, 4, 8, 10, 11, 12]]], attendus: [[[1, null, null, 9, 5, 6], [7], [7]]], }, { nom: "un siège réservé tenu par une personne présente n'est pas libre : le non-réservé du plus haut rang cède le sien", reservations: [resa(9, 1, 3)], listes: [[[1, 2, 3, 4, 5, 6, 7, 9], [8, 10, 11, 12]]], attendus: [[[1, 2, 3, 4, 5, 9], [6, 7], [6, 7]]], }, { nom: 'une personne réservée ne cède pas sa chaise : la personne 5 part, la 6 reste', reservations: [resa(6, 1, null), resa(8, 1, null)], listes: [[suite(1, 8), suite(9, 12)]], attendus: [[[1, 2, 3, 4, 8, 6], [5, 7], [5, 7]]], }, { nom: "dans l'ordre des rangs : la personne 7 prend la chaise vide, la 8 celle du plus haut rang", reservations: [resa(7, 1, null), resa(8, 1, null)], listes: [[[1, null, 3, 4, 5, 6, 7, 8], [2, 9, 10, 11, 12]]], attendus: [[[1, 7, 3, 4, 5, 8], [6], [6]]], }, { nom: 'une réservation « tour » ramène sa personne au tour 2 seulement', reservations: [resa(8, 1, null, 2)], listes: [ [suite(1, 8), suite(9, 12)], [suite(1, 8), suite(9, 12)], ], attendus: [ [suite(1, 6), [7, 8], [7, 8]], [[1, 2, 3, 4, 5, 8], [6, 7], [6, 7]], ], }, { nom: 'la réservation suspendue de la personne 8, exclue, ne la ramène pas', reservations: [resa(8, 1, null)], exclus: [8], listes: [[suite(1, 8), suite(9, 12)]], attendus: [[suite(1, 6), [7, 8], [7]]], }, ]; for (const { nom, reservations, exclus = [], listes, attendus } of cas) { const charge = chargeDe({ tables: [{ id: 1 }, { id: 2 }], n: 12, tours: listes.length, attribuerSieges: true, reservations }); placer(charge, listes, { id: 1, retenu: true }); for (const id of exclus) charge.participants[id - 1].exclu = true; const { charge: apres, avertissements } = accepter('changerPlacesTable', charge, { id: 1, sieges: 6, origine: 'champ' }); attendus.forEach(([liste, reserve], rang) => { assert.deepEqual(apres.retenu.placement[rang].sieges[0], liste, `${nom}, tour ${rang + 1}`); assert.deepEqual(apres.retenu.placement[rang].reserve, reserve, `${nom}, tour ${rang + 1}`); }); const surnumeraires = attendus.map(([, , personnes], rang) => ({ code: 'SURNUMERAIRES', details: { table: 1, tour: rang + 1, personnes } })); assert.deepEqual(avertissements, surnumeraires, nom); assert.equal(invariantViole(apres), null, nom); // La réduction n'ajoute aucune raison de dérive ; un exclu garde la sienne. assert.deepEqual(derive(apres.retenu, apres), derive(charge.retenu, charge), nom); assert.deepEqual(fautes(apres.retenu), [], nom); } }); test("sièges attribués, un tour du retenu que la soirée n'a plus ramène la personne qu'une réservation « tous » tient", () => { const charge = chargeDe({ tables: [{ id: 1 }, { id: 2 }], n: 12, tours: 3, attribuerSieges: true, reservations: [resa(12, 2, null)] }); placer(charge, [[suite(1, 6), suite(7, 12)], [suite(1, 6), suite(7, 12)], [suite(1, 4), suite(5, 12)]], { id: 1, retenu: true }); const deux = accepter('changerTours', charge, { tours: 2 }).charge; const { charge: apres, avertissements } = accepter('changerPlacesTable', deux, { id: 2, sieges: 6, origine: 'champ' }); assert.deepEqual(avertissements, [{ code: 'SURNUMERAIRES', details: { table: 2, tour: 3, personnes: [10, 11] } }]); assert.deepEqual(apres.retenu.placement[2].sieges[1], [5, 6, 7, 8, 9, 12]); assert.deepEqual(apres.retenu.placement[2].reserve, [10, 11]); assert.deepEqual(fautes(apres.retenu), []); assert.equal(invariantViole(apres), null); }); test('une baisse ne ramène pas la personne réservée d\'un rang au-delà de la capacité courante, que le plan ne montrait pas', () => { // Le retenu déclare la table 1 à 10, qui n'en a plus que 8 : sièges // attribués, la personne 9, réservée sans siège, est au rang 9. // Sièges attribués, la réservation que le plan montrait non tenue le reste, // et invariantViole la nomme une fois la dérive levée ; sans attribution, // l'affichage élisait la personne 9, qui reste. const nonTenue = { code: 'INVARIANT_VIOLE', details: { proposition: 1, violations: [{ code: 'RESERVATION', participant: 9, table: 1, tour: 1 }] } }; const cas = [ { attribuerSieges: true, liste: suite(1, 6), reserve: [7, 8, 9, 10], personnes: [7, 8], invariant: nonTenue }, { attribuerSieges: false, liste: [1, 2, 3, 4, 5, 9], reserve: [6, 7, 8, 10], personnes: [6, 7], invariant: null }, ]; for (const { attribuerSieges, liste, reserve, personnes, invariant } of cas) { const charge = chargeDe({ tables: [{ id: 1, sieges: 10 }, { id: 2 }], n: 12, tours: 1, attribuerSieges, reservations: [resa(9, 1, null)] }); placer(charge, [[suite(1, 10), [11, 12]]], { id: 1, retenu: true }); charge.tables[0].sieges = null; const { charge: apres, avertissements } = accepter('changerPlacesTable', charge, { id: 1, sieges: 6, origine: 'champ' }); const nom = `attribuerSieges ${attribuerSieges}`; assert.deepEqual(apres.retenu.placement[0].sieges[0], liste, nom); assert.deepEqual(apres.retenu.placement[0].reserve, reserve, nom); assert.deepEqual(avertissements, [{ code: 'SURNUMERAIRES', details: { table: 1, tour: 1, personnes } }], nom); assert.deepEqual(etatPlaces(charge, { tour: 1 }).reserve.includes(9), attribuerSieges, nom); assert.deepEqual(invariantViole(apres), invariant, nom); } }); test('sans attribution, une liste qui dépasse encore perd ses absents, le plus petit identifiant d\'abord', () => { const charge = chargeDe({ tables: [{ id: 1 }, { id: 2 }], n: 12, tours: 1 }); placer(charge, [[suite(1, 8), suite(9, 12)]], { id: 1, retenu: true }); for (const id of [2, 3]) charge.participants[id - 1].exclu = true; const { charge: apres, avertissements } = accepter('changerPlacesTable', charge, { id: 1, sieges: 6, origine: 'champ' }); // Six présents s'assoient ; les deux absents partent, le plus petit d'abord, jusqu'à six. assert.deepEqual(avertissements, []); assert.deepEqual(apres.retenu.placement[0].sieges[0], [1, 4, 5, 6, 7, 8]); assert.deepEqual(apres.retenu.placement[0].reserve, [2, 3]); const sept = accepter('changerPlacesTable', charge, { id: 1, sieges: 7, origine: 'champ' }).charge; assert.deepEqual(sept.retenu.placement[0].sieges[0], [1, 3, 4, 5, 6, 7, 8]); assert.deepEqual(sept.retenu.placement[0].reserve, [2]); }); test('un compteur de tables saturé refuse COMPTEUR_SATURE ; une origine ou un réalignement de forme fausse sont des fautes du code', () => { const charge = chargeDe({ tables: [{ id: 1 }], n: 2 }); charge.prochainsIds.table = 2 ** 31 - 1; refuse(() => COMMANDES.get('creerTable')(charge, { forme: 'ronde' }), 'COMPTEUR_SATURE', { compteur: 'table', remede: null }); assert.throws(() => COMMANDES.get('changerPlacesTable')(charge, { id: 1, sieges: 6, origine: 'clavier' }), TypeError); assert.throws(() => COMMANDES.get('changerDefaut')(charge, { sieges: 6 }), TypeError); }); }); describe('mobilier : changer le défaut (§ 6.1, § 6.2)', () => { function treizeTables(options = {}) { const surcharges = new Map([[3, 7], [6, 7], [9, 6], [12, 12]]); const tables = Array.from({ length: 13 }, (_, rang) => ({ id: rang + 1, sieges: surcharges.get(rang + 1) ?? null })); return chargeDe({ tables, n: 120, exclus: [40, 80], ...options }); } const capacites = (charge) => charge.tables.map((table) => capacite(charge, table)); test('appliquer : les quatre surchargées gardent 7, 7, 6 et 12, les neuf autres valent 10, une entrée ; réaligner : les treize suivent 10', async () => { const { seance, entrees } = await seanceSur(treizeTables()); const avant = entrees(); const rendu = await seance.executer('changerDefaut', { sieges: 10, realigner: false }); assert.equal(rendu.libelle, 'Sièges par défaut : 10'); assert.equal(entrees() - avant, 1); assert.deepEqual(capacites(seance.charge), [10, 10, 7, 10, 10, 7, 10, 10, 6, 10, 10, 12, 10]); assert.deepEqual(seance.charge.tables.filter(({ sieges }) => sieges !== null).map(({ id }) => id), [3, 6, 9, 12]); const realigne = await seance.executer('changerDefaut', { sieges: 10, realigner: true }); assert.equal(realigne.libelle, 'Sièges par défaut : 10 ; 4 tables réalignées'); assert.equal(entrees() - avant, 2); assert.ok(seance.charge.tables.every(({ sieges }) => sieges === null)); assert.deepEqual(capacites(seance.charge), Array(13).fill(10)); }); test('réaligner refusé en bloc : le titre du siège 12 refuse, table et remède compris, et rien ne s\'écrit ; sans réaligner, accepté', async () => { const charge = treizeTables({ attribuerSieges: true, titres: [{ table: 12, siege: 12, libelle: 'animateur' }] }); const { seance, ecritures } = await seanceSur(charge); const avant = ecritures(); const leve = await refuseAsync(seance.executer('changerDefaut', { sieges: 10, realigner: true }), 'SIEGES_RETIRES', { table: 12, sieges: [{ siege: 12, cause: 'SIEGE_TITRE', libelle: 'animateur' }], remede: { commande: 'retirerTitre', arguments: { table: 12, siege: 12 } }, }); assert.deepEqual(leve.details.autres, []); assert.equal(ecritures(), avant); assert.deepEqual(capacites(seance.charge), capacites(charge)); await seance.executer('changerDefaut', { sieges: 10, realigner: false }); assert.equal(seance.charge.tables[11].sieges, 12); }); test('baisse refusée en bloc : la table 2 porte 7 réservations ; le défaut 6 refuse, nommant la table 2, et les trois tables restent à 8', async () => { const reservations = suite(1, 7).map((id) => resa(id, 2, null)); const charge = chargeDe({ tables: [{ id: 1 }, { id: 2 }, { id: 3 }], n: 20, reservations }); const { seance, ecritures } = await seanceSur(charge); const avant = ecritures(); await refuseAsync(seance.executer('changerDefaut', { sieges: 6, realigner: false }), 'SOUS_RESERVATIONS', { table: 2, n: 7, tour: 1, remede: afficherTour(1), }); assert.equal(ecritures(), avant); assert.equal(seance.charge.evenement.siegesParDefaut, 8); assert.deepEqual(capacites(seance.charge), [8, 8, 8]); refuse(() => COMMANDES.get('changerDefaut')(charge, { sieges: '1', realigner: false }), 'MOINS_DE_DEUX', { remede: null }); refuse(() => COMMANDES.get('changerDefaut')(charge, { sieges: 'six', realigner: false }), 'NON_ENTIER'); }); test('une baisse sous un retenu occupé : accepté, SURNUMERAIRES, et ces deux personnes dans la réserve du tour 2', () => { const charge = chargeDe({ tables: [{ id: 1 }, { id: 2 }, { id: 3 }], n: 18 }); placer( charge, [ [suite(1, 6), suite(7, 12), suite(13, 18)], [suite(1, 5), suite(6, 10), suite(11, 18)], ], { id: 1, retenu: true }, ); const { charge: apres, avertissements } = accepter('changerDefaut', charge, { sieges: 6, realigner: false }); assert.deepEqual(avertissements, [{ code: 'SURNUMERAIRES', details: { table: 3, tour: 2, personnes: [17, 18] } }]); assert.deepEqual(apres.retenu.placement[1].reserve, [17, 18]); assert.deepEqual(apres.retenu.capacites, [6, 6, 6]); assert.deepEqual(derive(apres.retenu, apres), []); }); test('un défaut au-delà du seuil de lisibilité avertit une fois', () => { const charge = chargeDe({ tables: [{ id: 1 }, { id: 2 }], n: 4 }); const { avertissements } = accepter('changerDefaut', charge, { sieges: 40, realigner: false }); assert.equal(avertissements.length, 1); assert.equal(avertissements[0].code, 'AU_DELA_LISIBLE'); }); }); describe('mobilier : les tours (§ 2.1 étape 5, § 4.3)', () => { test('R = 4 → 3 → 4 : les réservations reviennent telles quelles, et la portée « tous » ancre encore au tour 4', () => { const debut = chargeDe({ tables: [{ id: 1 }, { id: 2 }], n: 4, tours: 4, reservations: [resa(1, 2, null)] }); const trois = accepter('changerTours', debut, { tours: 3 }); assert.equal(trois.libelle, 'Nombre de tours : 3'); const quatre = accepter('changerTours', trois.charge, { tours: '4' }).charge; assert.deepEqual(quatre.reservations, debut.reservations); const instance = normaliser(configurationDepuisCharge(quatre)); const p = instance.indexDe.get(1); assert.equal(instance.statut[p], STATUT.ANCRE); assert.equal(instance.fixe[p * instance.R + 3], instance.indexTableDe.get(2)); }); test("retirer le tour 4 détruit ses réservations « tour », garde les autres, et laisse propositions et retenu en dérive", () => { const reservations = [resa(1, 1, null, 4), resa(2, 2, null, 4), resa(3, 1, null, 2), resa(4, 2, null)]; const charge = chargeDe({ tables: [{ id: 1, sieges: 4 }, { id: 2, sieges: 4 }], n: 8, tours: 4, reservations }); const listes = [[[1, 2, 3, 5], [4, 6, 7, 8]], [[1, 3, 5, 6], [2, 4, 7, 8]], [[1, 5, 6, 7], [2, 3, 4, 8]], [[1, 3, 5, 7], [2, 4, 6, 8]]]; placer(charge, listes, { id: 1 }); placer(charge, listes, { id: 2, retenu: true }); const { charge: apres, libelle: texte } = accepter('changerTours', charge, { tours: 3 }); assert.equal(texte, 'Nombre de tours : 3 ; 2 réservations retirées'); assert.deepEqual(apres.reservations, [resa(3, 1, null, 2), resa(4, 2, null)]); analyser(serialiser(apres, { revision: 2, produitVersion: 'essai' })); assert.deepEqual(apres.propositions, charge.propositions); assert.deepEqual(apres.retenu, charge.retenu); assert.deepEqual(derive(apres.retenu, apres), [{ code: 'TOURS_CHANGES', avant: 4, maintenant: 3 }]); }); test('3 → 4 : trois réservations « tour » à une table rendent B partiellement fixé ; propositions et retenu gardés, TOURS_CHANGES', () => { const b = 5; const reservations = [1, 2, 3].map((tour) => resa(b, 1, null, tour)); const charge = chargeDe({ tables: [{ id: 1 }, { id: 2 }], n: 8, tours: 3, reservations }); const listes = [[[1, 2, 3, 5], [4, 6, 7, 8]], [[4, 5, 6, 7], [1, 2, 3, 8]], [[1, 5, 7, 8], [2, 3, 4, 6]]]; placer(charge, listes, { id: 1, retenu: true }); const avant = normaliser(configurationDepuisCharge(charge)); assert.equal(avant.statut[avant.indexDe.get(b)], STATUT.ANCRE); const { charge: apres } = accepter('changerTours', charge, { tours: 4 }); const instance = normaliser(configurationDepuisCharge(apres)); assert.equal(instance.statut[instance.indexDe.get(b)], STATUT.PARTIELLEMENT_FIXE); assert.deepEqual(apres.propositions, charge.propositions); assert.deepEqual(apres.retenu, charge.retenu); for (const placement of [apres.propositions[0], apres.retenu]) { assert.deepEqual(derive(placement, apres), [{ code: 'TOURS_CHANGES', avant: 3, maintenant: 4 }]); } }); test('« 0 » refuse MOINS_D_UN_TOUR {tours: 0}, « 2.5 » et « x » NON_ENTIER, sans écriture', async () => { const { seance, ecritures } = await seanceSur(chargeDe({ tables: [{ id: 1 }], n: 2, tours: 2 })); const avant = ecritures(); await refuseAsync(seance.executer('changerTours', { tours: '0' }), 'MOINS_D_UN_TOUR', { tours: 0, remede: null }); for (const tours of ['2.5', 'x']) { await refuseAsync(seance.executer('changerTours', { tours }), 'NON_ENTIER', { saisie: tours, remede: null }); } assert.equal(ecritures(), avant); assert.equal(seance.charge.evenement.tours, 2); }); }); describe('mobilier : les réglages (§ 5.3, § 15.4)', () => { test('« Séparer les appartenances : désactivé », une entrée', async () => { const { seance, entrees } = await seanceSur(chargeDe({ tables: [{ id: 1 }], n: 2 })); const avant = entrees(); const rendu = await seance.executer('modifierReglage', { cle: 'separerAppartenances', valeur: false }); assert.equal(rendu.libelle, 'Séparer les appartenances : désactivé'); assert.equal(entrees() - avant, 1); assert.equal(seance.charge.reglages.separerAppartenances, false); }); test('chaque réglage nommé se change ; une clé inconnue ou une valeur de forme fausse est une faute du code', () => { const charge = chargeDe({ tables: [{ id: 1 }], n: 2 }); const attendus = [ ['nouveauxVoisins', false, 'Nouveaux voisins à chaque tour : désactivé'], ['nouvelleTable', false, 'Nouvelle table à chaque tour : désactivé'], ['varierAppartenances', false, 'Varier les appartenances rencontrées : désactivé'], ['attribuerSieges', true, 'Attribuer les sièges : activé'], ['generation.nombre', 8, 'Propositions par génération : 8'], ['generation.arret', 300000, 'Mouvements par proposition : 300000'], ['generation.historique', 500, "Historique d'acceptation : 500"], ]; for (const [cle, valeur, texte] of attendus) { const rendu = accepter('modifierReglage', charge, { cle, valeur }); assert.equal(rendu.libelle, texte); const [tete, queue] = cle.split('.'); assert.equal(queue === undefined ? rendu.charge.reglages[tete] : rendu.charge.reglages[tete][queue], valeur); } assert.throws(() => COMMANDES.get('modifierReglage')(charge, { cle: 'couleur', valeur: true }), TypeError); assert.throws(() => COMMANDES.get('modifierReglage')(charge, { cle: 'nouveauxVoisins', valeur: 'non' }), TypeError); assert.throws(() => COMMANDES.get('modifierReglage')(charge, { cle: 'generation.nombre', valeur: 0 }), TypeError); refuse(() => appliquerCommande('modifierReglage', charge, { cle: 'nouveauxVoisins', valeur: true }), 'SANS_EFFET'); }); });