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