gestion_table_tournante_libre/src/application/commandes_mobilier.test.js
Mathieu Benoit 964532fa19 [ADD] application: furniture — tables, default, tours, capacity checks
Changing a table, the default seat count or the number of tours reshapes
a plan the operator may already have retained. Eight commands create,
remove and resize tables and set the default and the tours; every change
of capacity first passes controlerCapacite, which refuses to cut a reserved
or titled seat, states who goes to the reserve, and only brings back a
reserved person the display showed seated (§ 6.1, § 6.2). bilans.js states
the figures of a change before it applies.
Checked: 2033 node and 127 browser tests.

--- FR ---

[ADD] application : mobilier — tables, défaut, tours, contrôles

Changer une table, le défaut de sièges ou le nombre de tours remodèle un
plan que l'opérateur a peut-être retenu. Huit commandes créent, retirent et
redimensionnent les tables, et règlent le défaut et les tours ; tout
changement de capacité passe d'abord par controlerCapacite, qui refuse de
couper un siège réservé ou titré, dit qui part en réserve, et ne ramène une
personne réservée que si l'écran la montrait assise (§ 6.1, § 6.2).
bilans.js énonce les chiffres d'un changement avant qu'il s'applique.
Vérifié : 2033 node et 127 navigateur.

Assisted-by: Claude Opus 5.5
2026-10-07 05:12:43 -04:00

870 lines
48 KiB
JavaScript

// © 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');
});
});