// © 2026 TechnoLibre (http://www.technolibre.ca) // License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) // Épreuves du modèle de dessin du plan (§ 7.3, § 7.4, § 7.5, § 7.6, § 7.7), // sans DOM. Le plan lit les conflits de l'état des places, jamais ne les // recalcule ; l'épreuve, elle, importe le moteur et le stockage pour // construire ses données et obtenir la valeur attendue (§ 19.1). Les noms // d'épreuve sont inventés ; la petite démonstration livrée se lit, et sa // variante se dérive, en mémoire depuis texteLivre. import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { describe, test } from '../../../test/lanceur.js'; import { TABLES_APPLICATION } from '../../application/libelles.js'; import { etatPlaces, instanceDe } from '../../application/places.js'; import { creerTraduction } from '../../application/traduction.js'; import { texteLivre } from '../../demo/livrees.js'; import { constante } from '../../geometrie/constantes.js'; import { regimeCibles } from '../../geometrie/designation.js'; import { geometrieTable } from '../../geometrie/tables.js'; import { indexerPlan } from '../../moteur/configuration.js'; import { conflitsParTable } from '../../moteur/conflits.js'; import { diagnostiquer } from '../../moteur/diagnostic.js'; import { rechercher } from '../../moteur/recherche.js'; import { serialiser } from '../../stockage/canonique.js'; import { analyser, configurationDepuisCharge, creerCharge } from '../../stockage/document.js'; import { planDepuisFichier, versFichier } from '../../stockage/placements.js'; import { TABLES_INTERFACE } from '../libelles.js'; import { LARGEUR_ECHELLE_PX, actionsPersonne, actionsPlace, ajusterLigne, detailConflits, executerSurPlace, formesTable, libelleLongueur, ligneListe, modeleDessin, ordonneesLignes, partiesLigne, texteLigne, textesLigne, } from './modele.js'; const { t } = creerTraduction([...TABLES_APPLICATION, ...TABLES_INTERFACE]); const K_LISTES = constante('K_LISTES'); // --- Les configurations d'épreuve ------------------------------------------- // Douze noms inventés : prénom (null pour un nom seul) et nom. const NOMS = [ ['Iris', 'Ombrelle'], ['Théo', 'Pervenche'], ['Ondine', 'Lacasse'], [null, 'Grisaille'], ['Basile', 'Tourmaline'], ['Capucine', 'Arrosoir'], ['Aurèle', 'Cerfeuil'], ['Mélisande', 'Bruyère'], ['Firmin', 'Galet'], ['Clélia', 'Mistral'], ['Anatole', 'Houblon'], ['Solveig', 'Sarcelle'], ]; const personne = (id, nom, prenom, appartenance) => ({ id, nom, prenom, appartenance, courriel: null, titrePressenti: null, notes: null, exclu: false, }); // Les propositions de rechercher(configuration, {graine, nombre, arret: // 4000}), passées par versFichier et posées dans la charge ; le compteur // passe au-delà du dernier identifiant, et le plan est proposé. function avecPropositions(charge, { graine = 7, nombre = 1 } = {}) { const configuration = configurationDepuisCharge(charge); const propositions = rechercher(configuration, { graine, nombre, arret: 4000 }); charge.propositions = propositions.map((p) => versFichier(p, configuration, { produitVersion: 'epreuve', attribuerSieges: false, decalage: 0 }), ); charge.prochainsIds.proposition = nombre + 1; charge.evenement.etat = 'propose'; return charge; } /** Douze personnes en deux appartenances de six — une seule avec uneSeule —, * quatre tables rondes de 3, quatre tours, séparation active, et une * proposition de rechercher(…, {graine: 7, nombre: 1, arret: 4000}). */ function toutesEnConflit({ uneSeule = false } = {}) { const charge = creerCharge({ id: 'epreuve:toutes-en-conflit', nom: 'Toutes en conflit', siegesParDefaut: 3, tours: 4 }); charge.participants = NOMS.map(([prenom, nom], i) => personne(i + 1, nom, prenom, uneSeule || i % 2 === 0 ? 'Club des Merles' : 'Cercle des Hérons'), ); charge.tables = [1, 2, 3, 4].map((id) => ({ id, numero: id, sieges: null, forme: 'ronde', position: { x: 300 * (id - 1), y: 0 } })); charge.prochainsIds = { participant: 13, table: 5, proposition: 1 }; return avecPropositions(charge); } const FIXTURE = new URL('../../../test/fixtures/plan/toutes_en_conflit.gtt.json', import.meta.url); // La petite démonstration livrée, relue en mémoire. const petite = (cle = 'petite') => analyser(texteLivre(cle)).charge; // nomDe sur les participants de la charge : prénom et nom, ou le nom seul. const nomsDe = (charge) => { const noms = new Map(charge.participants.map(({ id, nom, prenom }) => [id, prenom === null ? nom : `${prenom} ${nom}`])); return (id) => noms.get(id); }; const options = (charge, autres = {}) => ({ k: 1, listes: true, miseEnEvidence: null, nomDe: nomsDe(charge), ...autres }); // Une copie de l'état des places où la table d'identifiant donné est saine. function avecTableSaine(etat, id) { return { ...etat, tables: etat.tables.map((table) => (table.id === id ? { ...table, conflits: { nombre: 0, regles: [] } } : table)) }; } // --- Les conflits se lisent --------------------------------------------------- describe('modeleDessin : les conflits se lisent, jamais ne se recalculent (§ 7.5)', () => { test('petite démonstration, conflit inévitable : tables signalées et pastilles de chaque tour égalent conflitsParTable', () => { const charge = avecPropositions(petite('petite-conflit')); const instance = instanceDe(charge); const attendus = conflitsParTable(instance, indexerPlan(instance, planDepuisFichier(charge.propositions[0]))); let signalees = 0; for (let tour = 1; tour <= charge.evenement.tours; tour += 1) { const etat = etatPlaces(charge, { tour, proposition: 1 }); const modele = modeleDessin(etat, options(charge)); assert.equal(modele.tables.length, 4); const toutes = attendus[tour - 1].every(({ nombre }) => nombre > 0); assert.equal(modele.signalerConflits, !toutes, `tour ${tour}`); const nombreDe = new Map(attendus[tour - 1].map(({ table, nombre }) => [table, nombre])); for (const table of modele.tables) { const lu = etat.tables.find(({ id }) => id === table.id).conflits; assert.equal(table.pastille, lu.nombre); assert.equal(table.pastille, nombreDe.get(table.id), `tour ${tour}, table ${table.id}`); assert.equal(table.conflit, lu.nombre > 0); if (table.conflit && modele.signalerConflits) signalees += 1; } } assert.ok(signalees > 0, 'la variante conflit signale au moins une table, à un tour où une autre est saine'); }); test('conflits null — non évalués — : ni conflit, ni pastille', () => { const charge = petite(); const etat = etatPlaces(charge, { tour: 1 }); const sansEvaluation = { ...etat, evalue: false, tables: etat.tables.map((table) => ({ ...table, conflits: null })) }; const modele = modeleDessin(sansEvaluation, options(charge)); assert.equal(modele.tables.length, 4); for (const table of modele.tables) { assert.equal(table.conflit, false); assert.equal(table.pastille, 0); assert.deepEqual(table.nomAccessible.details.conflits, null); } assert.equal(modele.signalerConflits, true); assert.deepEqual(modele.bandeau, []); }); test('toutes en conflit : le fichier d\u{2019}épreuve se relit, et le plan cesse de signaler', () => { const texte = readFileSync(FIXTURE, 'utf8'); const attendu = toutesEnConflit(); assert.equal(texte, serialiser(attendu, { revision: 1, produitVersion: 'epreuve' }), 'le fichier est la configuration construite'); const { charge } = analyser(texte); for (let tour = 1; tour <= charge.evenement.tours; tour += 1) { const etat = etatPlaces(charge, { tour, proposition: 1 }); const modele = modeleDessin(etat, options(charge)); assert.equal(modele.tables.length, 4); assert.equal(modele.signalerConflits, false); for (const table of modele.tables) { assert.ok(table.conflit); assert.ok(table.pastille > 0); assert.equal(table.pastille, etat.tables.find(({ id }) => id === table.id).conflits.nombre); } assert.deepEqual(modele.bandeau, [{ cle: 'plan.bandeau.toutesEnConflit', details: {} }]); const sain = modeleDessin(avecTableSaine(etat, 2), options(charge)); assert.equal(sain.signalerConflits, true, 'une table saine rétablit le signalement'); assert.deepEqual(sain.bandeau, []); assert.deepEqual( sain.tables.map(({ id, conflit }) => [id, conflit]), [ [1, true], [2, false], [3, true], [4, true], ], ); } }); test('aucune table : aucun dessin, un bandeau vide', () => { const charge = creerCharge({ id: 'epreuve:vide', nom: 'Vide', siegesParDefaut: 6, tours: 1 }); const modele = modeleDessin(etatPlaces(charge, { tour: 1 }), options(charge)); assert.deepEqual(modele.tables, []); assert.deepEqual(modele.bandeau, []); assert.equal(modele.signalerConflits, true); }); }); // --- Le bandeau énonce le diagnostic ------------------------------------------- describe('modeleDessin : le bandeau énonce le diagnostic (§ 7.5)', () => { const lire = (charge) => etatPlaces(charge, { tour: 1, proposition: 1 }); test('deux appartenances de six : la phrase, collisionPartout, puis un plancher par groupe, chiffres du moteur', () => { const charge = toutesEnConflit(); const diagnostic = diagnostiquer(configurationDepuisCharge(charge)); assert.equal(diagnostic.collisionPartout, true); assert.equal(diagnostic.uneSeuleAppartenance, false); assert.equal(diagnostic.collisions.length, 2); const { bandeau } = modeleDessin(lire(charge), options(charge, { diagnostic })); assert.deepEqual(bandeau, [ { cle: 'plan.bandeau.toutesEnConflit', details: {} }, { cle: 'plan.bandeau.collisionPartout', details: {} }, ...diagnostic.collisions.map(({ groupe, effectif, plancherCumulees }) => ({ cle: 'plan.bandeau.plancherGroupe', details: { groupe, effectif, plancherCumulees }, })), ]); assert.deepEqual( bandeau.slice(2).map(({ details }) => details.effectif), [6, 6], ); for (const { cle, details } of bandeau) assert.equal(typeof t(cle, details), 'string'); }); test('une seule appartenance : l\u{2019}entrée uneSeuleAppartenance, sans plancher par groupe', () => { const charge = toutesEnConflit({ uneSeule: true }); const diagnostic = diagnostiquer(configurationDepuisCharge(charge)); assert.equal(diagnostic.uneSeuleAppartenance, true); const { bandeau, signalerConflits } = modeleDessin(lire(charge), options(charge, { diagnostic })); assert.equal(signalerConflits, false); assert.deepEqual( bandeau.map(({ cle }) => cle), [ 'plan.bandeau.toutesEnConflit', ...(diagnostic.collisionPartout ? ['plan.bandeau.collisionPartout'] : []), 'plan.bandeau.uneSeuleAppartenance', ], ); }); test('un diagnostic sans plancher : la phrase, puis l\u{2019}entrée qui le dit', () => { const charge = toutesEnConflit(); const diagnostic = { collisions: [], uneSeuleAppartenance: false, collisionPartout: false }; const { bandeau } = modeleDessin(lire(charge), options(charge, { diagnostic })); assert.deepEqual(bandeau, [ { cle: 'plan.bandeau.toutesEnConflit', details: {} }, { cle: 'plan.bandeau.sansPlancher', details: {} }, ]); }); test('diagnostic null : la phrase seule', () => { const charge = toutesEnConflit(); const { bandeau } = modeleDessin(lire(charge), options(charge, { diagnostic: null })); assert.deepEqual(bandeau, [{ cle: 'plan.bandeau.toutesEnConflit', details: {} }]); }); test('la petite démonstration : collisionPartout faux, son entrée absente', () => { const diagnostic = diagnostiquer(configurationDepuisCharge(petite())); assert.equal(diagnostic.collisionPartout, false); const charge = toutesEnConflit(); const { bandeau } = modeleDessin(lire(charge), options(charge, { diagnostic })); assert.equal(bandeau[0].cle, 'plan.bandeau.toutesEnConflit'); assert.ok(!bandeau.some(({ cle }) => cle === 'plan.bandeau.collisionPartout')); }); test('une table saine : le bandeau reste vide, diagnostic donné ou non', () => { const charge = toutesEnConflit(); const diagnostic = diagnostiquer(configurationDepuisCharge(charge)); const { bandeau } = modeleDessin(avecTableSaine(lire(charge), 3), options(charge, { diagnostic })); assert.deepEqual(bandeau, []); }); }); // --- La source -------------------------------------------------------------------- describe('modeleDessin : les lignes nomment les occupants de la source (contrat)', () => { test('proposition 2 demandée → ses occupants ; sans demande → ceux du retenu, tiré de la proposition 1', () => { const charge = avecPropositions(petite(), { graine: 11, nombre: 2 }); const [premiere] = charge.propositions; const { siegesAttribues, tables, capacites, tours, participants, placement } = structuredClone(premiere); charge.retenu = { proposition: 1, siegesAttribues, tables, capacites, tours, participants, placement }; charge.evenement.etat = 'retenu'; const nomDe = nomsDe(charge); // Les noms des lignes de chaque table, dans l'ordre des tables. const nomsDesLignes = (etat) => modeleDessin(etat, options(charge)).tables.map(({ lignes }) => lignes.map(({ nom }) => nom)); // Les noms qu'une proposition assied au tour 1, table par table. const nomsDeLaSource = (p) => charge.tables.map(({ id }) => p.placement[0].sieges[p.tables.indexOf(id)].map(nomDe).sort()); const montree = etatPlaces(charge, { tour: 1, proposition: 2 }); assert.equal(montree.source, 'proposition'); const retenu = etatPlaces(charge, { tour: 1 }); assert.equal(retenu.source, 'retenu'); const deLaDeux = nomsDesLignes(montree).map((noms) => [...noms].sort()); const duRetenu = nomsDesLignes(retenu).map((noms) => [...noms].sort()); assert.deepEqual(deLaDeux, nomsDeLaSource(charge.propositions[1])); assert.deepEqual(duRetenu, nomsDeLaSource(charge.retenu)); assert.notDeepEqual(deLaDeux, duRetenu, 'les deux sources assoient autrement au tour 1'); }); }); // --- Les lignes de la liste -------------------------------------------------------- // Une chaise de l'état des places, comme etatPlaces la rend. const chaise = (siege, { occupant = null, reservation = null, titre = null } = {}) => ({ siege, occupant, reservation, titre, remplissage: 'normal', marques: [], }); const NOMS_LIGNES = new Map([ [1, 'Iris Ombrelle'], [2, 'Théo Pervenche'], [3, 'Ondine Lacasse'], [4, 'Grisaille'], ]); const nomDeLigne = (id) => NOMS_LIGNES.get(id); const animateur = (pourvu) => ({ libelle: 'animateur', pourvu }); // Les six formes de la décision : chaise, ligne attendue, texte attendu. const SIX_FORMES = [ [ chaise(1, { occupant: 1, reservation: { participant: 1, portee: 'tous', tour: null }, titre: animateur(true) }), { numero: 1, etat: { cle: 'plan.ligne.reserveeTous', details: {} }, titulaire: null, nom: 'Iris Ombrelle', titre: animateur(true) }, '1. (réservée, tous les tours) Iris Ombrelle \u{2014} animateur', ], [ chaise(2, { occupant: 2, reservation: { participant: 2, portee: 'tour', tour: 2 } }), { numero: 2, etat: { cle: 'plan.ligne.reserveeTour', details: { tour: 2 } }, titulaire: null, nom: 'Théo Pervenche', titre: null }, '2. (réservée, tour 2) Théo Pervenche', ], [chaise(3, { occupant: 3 }), { numero: 3, etat: null, titulaire: null, nom: 'Ondine Lacasse', titre: null }, '3. Ondine Lacasse'], [ chaise(4, { occupant: 4, titre: animateur(false) }), { numero: 4, etat: null, titulaire: null, nom: 'Grisaille', titre: animateur(false) }, '4. Grisaille \u{2014} titre animateur non pourvu', ], [chaise(5), { numero: 5, etat: { cle: 'plan.ligne.libre', details: {} }, titulaire: null, nom: null, titre: null }, '5. (libre)'], [ chaise(6, { titre: animateur(false) }), { numero: 6, etat: { cle: 'plan.ligne.libre', details: {} }, titulaire: null, nom: null, titre: animateur(false) }, '6. (libre) \u{2014} titre animateur non pourvu', ], ]; // Le texte entier d'une ligne : ses parties dans l'ordre, les absentes omises. const joindre = ({ tete, titulaire, suite, nom, avantTitre, titre, apresTitre }) => [tete, titulaire, suite, nom, avantTitre, titre, apresTitre].filter((partie) => partie !== null).join(''); describe('ligneListe et textesLigne : les six formes (§ 7.6)', () => { test('chaque forme, exactement, titre valant {libelle, pourvu}', () => { assert.equal(SIX_FORMES.length, 6); for (const [entree, ligne, texte] of SIX_FORMES) { assert.deepEqual(ligneListe(entree, nomDeLigne), ligne); assert.equal(joindre(textesLigne(ligne, t)), texte); } }); test('le titre est sa propre partie, le libellé seul ; le nom aussi', () => { const textes = textesLigne(SIX_FORMES[0][1], t); assert.equal(textes.nom, 'Iris Ombrelle'); assert.equal(textes.titre, 'animateur'); assert.equal(textes.tete, '1. (réservée, tous les tours) '); assert.equal(textes.teteSansNom, '1. (réservée, tous les tours)'); assert.equal(textesLigne(SIX_FORMES[2][1], t).titre, null); }); }); // Mesures factices : une unité par unité de code, 1,5 en italique. const droit = (chaine) => chaine.length; const italique = (chaine) => 1.5 * chaine.length; const MARQUE = '\u{2026}'; const largeurDe = (ligne) => droit(`${ligne.tete}${ligne.titulaire ?? ''}${ligne.suite ?? ''}${ligne.nom ?? ''}${ligne.avantTitre ?? ''}`) + italique(ligne.titre ?? '') + droit(ligne.apresTitre ?? ''); // --- Une place réservée qu'occupe une autre personne ----------------------------------- describe('ligneListe : la réservation d\u{2019}une autre personne que l\u{2019}occupant (§ 7.6)', () => { test('petite démonstration, une réservation posée après la proposition : la ligne nomme sa titulaire, puis l\u{2019}occupant', () => { const charge = petite(); const configuration = configurationDepuisCharge(charge); const [proposition] = rechercher(configuration, { graine: 7, nombre: 1, arret: 1000 }); charge.propositions = [versFichier(proposition, configuration, { produitVersion: 'epreuve', attribuerSieges: false, decalage: 0 })]; charge.prochainsIds.proposition = 2; charge.evenement.etat = 'propose'; charge.reservations.push({ participant: 1, table: 1, siege: 2, portee: 'tous', tour: null }); const etat = etatPlaces(charge, { tour: 1, proposition: 1 }); const chaise2 = etat.tables[0].chaises[1]; assert.equal(chaise2.reservation.participant, 1); assert.equal(etat.tables[0].chaises.some(({ occupant }) => occupant === 1), false, 'la proposition n\u{2019}assied pas la titulaire à la table 1'); assert.notEqual(chaise2.occupant, null); const nomDe = nomsDe(charge); const titulaire = nomDe(1); const occupant = nomDe(chaise2.occupant); const ligne = ligneListe(chaise2, nomDe); assert.deepEqual(ligne, { numero: 2, etat: { cle: 'plan.ligne.reserveeA', details: { portee: 'tous', tour: null } }, titulaire, nom: occupant, titre: null, }); assert.deepEqual(modeleDessin(etat, options(charge)).tables[0].lignes[1], ligne); const textes = textesLigne(ligne, t); assert.equal(joindre(textes), `2. (réservée à ${titulaire}, tous les tours) ${occupant}`); // La titulaire et l'occupant occupent chacun leur partie, donc leur tspan. const parties = partiesLigne(ajusterLigne(textes, { mesurer: droit, mesurerItalique: italique, largeur: 1000, marque: MARQUE })); assert.deepEqual( parties.filter(({ partie }) => partie === 'titulaire' || partie === 'nom'), [ { partie: 'titulaire', texte: titulaire }, { partie: 'nom', texte: occupant }, ], ); }); test('réservée au seul tour 3 pour une autre personne, la place vide : « 2. (réservée à …, tour 3) »', () => { const ligne = ligneListe(chaise(2, { reservation: { participant: 1, portee: 'tour', tour: 3 } }), nomDeLigne); assert.deepEqual(ligne, { numero: 2, etat: { cle: 'plan.ligne.reserveeA', details: { portee: 'tour', tour: 3 } }, titulaire: 'Iris Ombrelle', nom: null, titre: null, }); assert.equal(joindre(textesLigne(ligne, t)), '2. (réservée à Iris Ombrelle, tour 3)'); }); test('trop étroite : le nom de l\u{2019}occupant s\u{2019}abrège, puis tombe ; la titulaire s\u{2019}abrège, dans sa partie', () => { const entree = textesLigne( ligneListe(chaise(2, { occupant: 2, reservation: { participant: 1, portee: 'tous', tour: null } }), nomDeLigne), t, ); const pleine = joindre(entree).length; const abregee = ajusterLigne(entree, { mesurer: droit, mesurerItalique: italique, largeur: pleine - 3, marque: MARQUE }); assert.equal(abregee.titulaire, 'Iris Ombrelle'); assert.ok(abregee.nom.endsWith(MARQUE)); assert.ok(largeurDe(abregee) <= pleine - 3); const sansNom = ajusterLigne(entree, { mesurer: droit, mesurerItalique: italique, largeur: 40, marque: MARQUE }); assert.equal(sansNom.nom, null); assert.ok(sansNom.titulaire.endsWith(MARQUE), sansNom.titulaire); assert.equal(sansNom.suite, entree.suiteSansNom); assert.ok(largeurDe(sansNom) <= 40, `${largeurDe(sansNom)}`); }); }); // --- La ligne entière tient dans la largeur du bloc ------------------------------------ describe('ajusterLigne : la ligne entière tient (§ 7.4)', () => { const textes = (nom, titre = null, pourvu = true) => textesLigne({ numero: 1, etat: null, nom, titre: titre === null ? null : { libelle: titre, pourvu } }, t); test('une ligne qui tient reste entière', () => { const entree = textes('Iris Ombrelle', 'animateur'); const ligne = ajusterLigne(entree, { mesurer: droit, mesurerItalique: italique, largeur: 100, marque: MARQUE }); assert.deepEqual(ligne, { tete: entree.tete, titulaire: null, suite: null, nom: entree.nom, avantTitre: entree.avantTitre, titre: entree.titre, apresTitre: null, }); }); test('le nom s\u{2019}abrège à la largeur que lui laissent les autres parties, mesurées chacune dans son style', () => { const entree = textes('W'.repeat(200), 'animateur'); // tête « 1. » + espace : 3 ; « — » entouré : 3 ; titre en italique : 13,5. const ligne = ajusterLigne(entree, { mesurer: droit, mesurerItalique: italique, largeur: 40, marque: MARQUE }); assert.equal(ligne.titre, 'animateur'); assert.ok(ligne.nom.endsWith(MARQUE)); assert.ok(largeurDe(ligne) <= 40); assert.ok(largeurDe(ligne) > 39, `le nom prend toute la place restante : ${largeurDe(ligne)}`); }); test('sans place pour la marque, le nom tombe et le titre s\u{2019}abrège', () => { const entree = textes('Iris Ombrelle', 'a'.repeat(30), false); const ligne = ajusterLigne(entree, { mesurer: droit, mesurerItalique: italique, largeur: 40, marque: MARQUE }); assert.equal(ligne.nom, null); assert.equal(ligne.tete, entree.teteSansNom); assert.ok(ligne.titre.endsWith(MARQUE)); assert.equal(ligne.apresTitre, entree.apresTitre); assert.ok(largeurDe(ligne) <= 40); }); test('sans place pour le titre, il tombe avec ce qui l\u{2019}entoure ; une tête trop large s\u{2019}abrège', () => { const entree = textesLigne( { numero: 1, etat: { cle: 'plan.ligne.reserveeTous', details: {} }, nom: 'Iris Ombrelle', titre: { libelle: 'animateur', pourvu: true } }, t, ); const etroite = ajusterLigne(entree, { mesurer: droit, mesurerItalique: italique, largeur: 20, marque: MARQUE }); assert.deepEqual([etroite.nom, etroite.avantTitre, etroite.titre, etroite.apresTitre], [null, null, null, null]); assert.ok(etroite.tete.startsWith('1. (r')); assert.ok(etroite.tete.endsWith(MARQUE)); assert.ok(largeurDe(etroite) <= 20); }); test('chaque ligne d\u{2019}un bloc ajusté tient, quelle que soit la largeur', () => { const lignes = SIX_FORMES.map(([, ligne]) => textesLigne(ligne, t)); assert.equal(lignes.length, 6); for (const largeur of [5, 12, 20, 40, 80, 200]) { for (const entree of lignes) { const ligne = ajusterLigne(entree, { mesurer: droit, mesurerItalique: italique, largeur, marque: MARQUE }); assert.ok(largeurDe(ligne) <= largeur, `${joindre(entree)} à ${largeur} : ${largeurDe(ligne)}`); } } }); }); describe('ordonneesLignes : une ligne sous la précédente', () => { test('la première ligne sous le haut du bloc, d\u{2019}une hauteur de ligne ; puis une hauteur de plus chacune', () => { assert.deepEqual(ordonneesLignes(-50, 3, 20), [-30, -10, 10]); assert.deepEqual(ordonneesLignes(0, 0, 20), []); }); }); // --- Régime, listes, marques, bords ------------------------------------------------------ // Une charge de tables rondes de capacités données, des personnes inventées. function chargeDeTables(capacites, { tours = 1 } = {}) { const charge = creerCharge({ id: 'epreuve:tables', nom: 'Tables', siegesParDefaut: 8, tours }); charge.tables = capacites.map((sieges, i) => ({ id: i + 1, numero: i + 1, sieges, forme: i % 2 === 0 ? 'ronde' : 'carree', position: { x: 400 * i, y: 0 }, })); charge.participants = NOMS.map(([prenom, nom], i) => personne(i + 1, nom, prenom, null)); charge.prochainsIds = { participant: 13, table: capacites.length + 1, proposition: 1 }; return charge; } describe('modeleDessin : le dessin d\u{2019}une table (§ 7.3, § 7.6, § 17 point 6)', () => { test('géométrie de geometrieTable ; chaises dessinées en régime sièges, aucune en régime table', () => { const charge = chargeDeTables([8, 12, 2]); const etat = etatPlaces(charge, { tour: 1 }); for (const k of [0.39, 0.41, 2]) { const modele = modeleDessin(etat, options(charge, { k })); assert.equal(modele.tables.length, 3); for (const table of modele.tables) { const enregistrement = charge.tables.find(({ id }) => id === table.id); const geometrie = geometrieTable(enregistrement, etat.tables.find(({ id }) => id === table.id).capacite); assert.deepEqual(table.geometrie, geometrie); assert.equal(table.regime, regimeCibles(k, geometrie)); if (table.regime === 'table') assert.deepEqual(table.chaises, []); else { assert.equal(table.chaises.length, geometrie.chaises.length); table.chaises.forEach((dessin, i) => { const { siege, x, y, rayon } = geometrie.chaises[i]; assert.deepEqual({ siege: dessin.siege, x: dessin.x, y: dessin.y, rayon: dessin.rayon }, { siege, x, y, rayon }); }); } } } const regimes = (k) => modeleDessin(etat, options(charge, { k })).tables.map(({ regime }) => regime); assert.deepEqual(regimes(0.39), ['table', 'table', 'sieges']); assert.deepEqual(regimes(0.41), ['sieges', 'sieges', 'sieges']); }); test('listes rendues : demandées et k ≥ K_LISTES', () => { const charge = chargeDeTables([8]); const etat = etatPlaces(charge, { tour: 1 }); assert.equal(modeleDessin(etat, options(charge, { k: K_LISTES - 0.01 })).listesRendues, false); assert.equal(modeleDessin(etat, options(charge, { k: K_LISTES })).listesRendues, true); assert.equal(modeleDessin(etat, options(charge, { k: K_LISTES + 0.01 })).listesRendues, true); assert.equal(modeleDessin(etat, options(charge, { k: 5, listes: false })).listesRendues, false); assert.equal('largeurListe' in modeleDessin(etat, options(charge)), false, 'la largeur du bloc est celle du registre'); }); test('une ligne par siège, de 1 à la capacité, quel que soit le régime', () => { const charge = chargeDeTables([8, 2]); const etat = etatPlaces(charge, { tour: 1 }); const modele = modeleDessin(etat, options(charge, { k: 0.1 })); assert.deepEqual( modele.tables.map(({ lignes }) => lignes.map(({ numero }) => numero)), [ [1, 2, 3, 4, 5, 6, 7, 8], [1, 2], ], ); }); test('remplissage, marques et mise en évidence reprennent l\u{2019}état des places, au tour montré', () => { const charge = chargeDeTables([8], { tours: 2 }); charge.reservations = [ { participant: 1, table: 1, siege: 1, portee: 'tous', tour: null }, { participant: 2, table: 1, siege: 3, portee: 'tour', tour: 1 }, ]; charge.titres = [ { table: 1, siege: 1, libelle: 'animateur' }, { table: 1, siege: 2, libelle: 'animateur' }, ]; for (const tour of [1, 2]) { const etat = etatPlaces(charge, { tour }); const modele = modeleDessin(etat, options(charge, { miseEnEvidence: 1 })); const [table] = modele.tables; assert.equal(table.chaises.length, 8); table.chaises.forEach((dessin, i) => { const lue = etat.tables[0].chaises[i]; assert.equal(dessin.remplissage, lue.remplissage); assert.deepEqual(dessin.marques, lue.marques); assert.equal(dessin.miseEnEvidence, lue.occupant === 1); }); assert.equal(table.chaises.filter(({ miseEnEvidence }) => miseEnEvidence).length, 1); } const [tour1] = modeleDessin(etatPlaces(charge, { tour: 1 }), options(charge)).tables; assert.deepEqual(tour1.chaises.slice(0, 3).map(({ marques }) => marques), [['cadenasFerme', 'anneau'], ['cadenasOuvert'], ['cadenasFerme']]); assert.deepEqual(tour1.chaises.slice(0, 3).map(({ remplissage }) => remplissage), ['plein', 'hachure', 'plein']); assert.ok(tour1.chaises.every(({ miseEnEvidence }) => miseEnEvidence === false), 'aucune mise en évidence demandée'); }); test('une table gelée le dit dans son nom accessible et en tête de sa liste ; une surchargée, dans son nom', () => { const charge = chargeDeTables([2, 3]); charge.reservations = [ { participant: 1, table: 1, siege: 1, portee: 'tous', tour: null }, { participant: 2, table: 1, siege: 2, portee: 'tous', tour: null }, ]; charge.evenement.siegesParDefaut = 3; const etat = etatPlaces(charge, { tour: 1 }); assert.equal(etat.tables[0].gelee, true); const [gelee, autre] = modeleDessin(etat, options(charge)).tables; assert.equal(gelee.gelee, true); assert.deepEqual(gelee.enTete, { cle: 'plan.liste.gelee', details: {} }); assert.equal(gelee.nomAccessible.cle, 'plan.table.nom'); assert.match(t(gelee.nomAccessible.cle, gelee.nomAccessible.details), /gelée/); assert.match(t(gelee.nomAccessible.cle, gelee.nomAccessible.details), /surchargée/); assert.equal(autre.gelee, false); assert.equal(autre.enTete, null); assert.doesNotMatch(t(autre.nomAccessible.cle, autre.nomAccessible.details), /gelée/); assert.match(t(autre.nomAccessible.cle, autre.nomAccessible.details), /^Table 2, 3 places, .*surchargée$/); }); test('sièges non attribués, deux titres et une réservation « tous » sans siège : la réservée au siège 1, pourvue ; « 2. (libre) — titre … non pourvu »', () => { const charge = chargeDeTables([4]); charge.participants = charge.participants.slice(0, 1); charge.prochainsIds.participant = 2; charge.reservations = [{ participant: 1, table: 1, siege: null, portee: 'tous', tour: null }]; charge.titres = [ { table: 1, siege: 1, libelle: 'animateur' }, { table: 1, siege: 2, libelle: 'animateur' }, ]; const etat = etatPlaces(charge, { tour: 1 }); const [table] = modeleDessin(etat, options(charge)).tables; const textes = table.lignes.map((ligne) => joindre(textesLigne(ligne, t))); assert.deepEqual(textes, [ '1. (réservée, tous les tours) Iris Ombrelle \u{2014} animateur', '2. (libre) \u{2014} titre animateur non pourvu', '3. (libre)', '4. (libre)', ]); }); }); describe('libelleLongueur : la longueur de la barre en mots (§ 7.7)', () => { test('cm sous le mètre, m sous le kilomètre, km au-delà ; décimales d\u{2019}une longueur sous l\u{2019}unité', () => { assert.deepEqual(libelleLongueur(50), { valeur: 50, decimales: 0, cle: 'plan.echelle.centimetres' }); assert.deepEqual(libelleLongueur(200), { valeur: 2, decimales: 0, cle: 'plan.echelle.metres' }); assert.deepEqual(libelleLongueur(500000), { valeur: 5, decimales: 0, cle: 'plan.echelle.kilometres' }); assert.deepEqual(libelleLongueur(0.5), { valeur: 0.5, decimales: 1, cle: 'plan.echelle.centimetres' }); assert.deepEqual(libelleLongueur(0.02), { valeur: 0.02, decimales: 2, cle: 'plan.echelle.centimetres' }); assert.deepEqual(libelleLongueur(0.1), { valeur: 0.1, decimales: 1, cle: 'plan.echelle.centimetres' }); assert.deepEqual(libelleLongueur(0.01), { valeur: 0.01, decimales: 2, cle: 'plan.echelle.centimetres' }); assert.deepEqual(libelleLongueur(1), { valeur: 1, decimales: 0, cle: 'plan.echelle.centimetres' }); assert.ok(LARGEUR_ECHELLE_PX > 0); }); }); describe('formesTable : les formes d\u{2019}une table, en attributs SVG (§ 7.1, § 7.7)', () => { test('ronde : disques du plateau et de l\u{2019}emprise ; carrée : carrés aux côtés parallèles aux axes', () => { const ronde = geometrieTable({ id: 1, numero: 1, forme: 'ronde', position: { x: 100, y: -40 } }, 8); const formes = formesTable(ronde); assert.equal(formes.ronde, true); assert.deepEqual(formes.plateau, { cx: 100, cy: -40, r: ronde.demiTaille }); assert.deepEqual(formes.emprise, { cx: 100, cy: -40, r: ronde.rayonEmprise }); const carree = geometrieTable({ id: 2, numero: 2, forme: 'carree', position: { x: 0, y: 0 } }, 12); const deCarree = formesTable(carree); assert.equal(deCarree.ronde, false); assert.deepEqual(deCarree.plateau, { x: -90, y: -90, width: 180, height: 180 }); assert.deepEqual(deCarree.emprise, { x: -carree.rayonEmprise, y: -carree.rayonEmprise, width: 2 * carree.rayonEmprise, height: 2 * carree.rayonEmprise, }); }); test('la pastille au centre du plateau ; la marque de gel dans le plateau, au-dessus de la pastille', () => { for (const places of [2, 8, 16]) { const geometrie = geometrieTable({ id: 1, numero: 1, forme: 'ronde', position: { x: 10, y: 20 } }, places); const { pastille, gel } = formesTable(geometrie); assert.deepEqual(pastille, { x: 10, y: 20 }); assert.equal(gel.x, 10); assert.ok(gel.y < 20 && gel.y > 20 - geometrie.demiTaille, `${places} places : ${gel.y}`); } }); }); describe('partiesLigne : les tspans d\u{2019}une ligne, dans l\u{2019}ordre', () => { test('chaque partie présente, nommée ; les absentes omises', () => { const ligne = { tete: '1. ', titulaire: null, suite: null, nom: 'Iris Ombrelle', avantTitre: ' \u{2014} ', titre: 'animateur', apresTitre: null }; assert.deepEqual(partiesLigne(ligne), [ { partie: 'tete', texte: '1. ' }, { partie: 'nom', texte: 'Iris Ombrelle' }, { partie: 'avantTitre', texte: ' \u{2014} ' }, { partie: 'titre', texte: 'animateur' }, ]); assert.deepEqual(partiesLigne({ tete: '5. (libre)', titulaire: null, suite: null, nom: null, avantTitre: null, titre: null, apresTitre: null }), [ { partie: 'tete', texte: '5. (libre)' }, ]); }); }); // --- L'option poignee (I10) ------------------------------------------------------ describe('modeleDessin : l\u{2019}option poignee dessine une table au nombre tiré, sans toucher l\u{2019}état (§ 7.1)', () => { test('sans option, ou poignee null : chaque table à sa capacité', () => { const charge = petite(); const etat = etatPlaces(charge, { tour: 1 }); const sans = modeleDessin(etat, options(charge)); assert.deepEqual(modeleDessin(etat, options(charge, { poignee: null })), sans); assert.equal(sans.tables.length, 4); for (const table of sans.tables) assert.equal(table.lignes.length, etat.tables.find(({ id }) => id === table.id).capacite); }); test('tirée vers plus : la table se dessine à places, chaises 1 à places, celles au-delà de sa capacité libres', () => { const charge = petite(); const etat = etatPlaces(charge, { tour: 1 }); const avant = JSON.stringify(etat); const cible = etat.tables[1]; const capacite = cible.capacite; const places = capacite + 3; const modele = modeleDessin(etat, options(charge, { poignee: { table: cible.id, places } })); assert.equal(JSON.stringify(etat), avant, 'l\u{2019}état des places n\u{2019}est pas touché'); const tiree = modele.tables[1]; assert.equal(tiree.id, cible.id); assert.deepEqual(tiree.geometrie, geometrieTable(cible, places)); assert.equal(tiree.chaises.length, places); assert.deepEqual(tiree.chaises.map(({ siege }) => siege), Array.from({ length: places }, (_, i) => i + 1)); for (const chaise of tiree.chaises.slice(capacite)) { assert.equal(chaise.remplissage, 'normal'); assert.deepEqual(chaise.marques, []); assert.equal(chaise.miseEnEvidence, false); } assert.equal(tiree.lignes.length, places); for (const ligne of tiree.lignes.slice(capacite)) { assert.deepEqual(ligne.etat, { cle: 'plan.ligne.libre', details: {} }); assert.equal(ligne.nom, null); } assert.deepEqual(tiree.lignes.slice(0, capacite), cible.chaises.map((chaise) => ligneListe(chaise, nomsDe(charge)))); assert.equal(tiree.nomAccessible.details.places, places); const autres = modeleDessin(etat, options(charge)); for (const rang of [0, 2, 3]) assert.deepEqual(modele.tables[rang], autres.tables[rang], `table de rang ${rang} inchangée`); }); test('tirée vers moins : chaises et lignes 1 à places ; le régime se lit sur la géométrie tirée', () => { const charge = petite(); const etat = etatPlaces(charge, { tour: 1 }); const cible = etat.tables[0]; const modele = modeleDessin(etat, options(charge, { poignee: { table: cible.id, places: 2 } })); const tiree = modele.tables[0]; assert.equal(tiree.chaises.length, 2); assert.equal(tiree.lignes.length, 2); assert.equal(tiree.geometrie.chaises.length, 2); for (const k of [0.2, 0.5, 1]) { const auZoom = modeleDessin(etat, options(charge, { k, poignee: { table: cible.id, places: 30 } })).tables[0]; assert.equal(auZoom.regime, regimeCibles(k, geometrieTable(cible, 30)), `k = ${k}`); } }); }); // --- La sélection et l'élément actif (I11) --------------------------------------- describe('modeleDessin : les options selection et actif marquent leur chaise, ou leur table, et elle seule (§ 7.1, § 7.3)', () => { // Les porteurs d'une marque : 'table:' ou 'siege::'. function porteurs(modele, marque) { const trouves = []; for (const table of modele.tables) { if (table[marque]) trouves.push(`table:${table.id}`); for (const chaise of table.chaises) if (chaise[marque]) trouves.push(`siege:${table.id}:${chaise.siege}`); } return trouves; } test('sans option, ni sélection ni focus ; chaque table et chaque chaise porte les deux drapeaux, faux', () => { const charge = petite(); const modele = modeleDessin(etatPlaces(charge, { tour: 1 }), options(charge)); assert.equal(modele.tables.length, 4); let chaises = 0; for (const table of modele.tables) { assert.equal(table.selection, false); assert.equal(table.focus, false); for (const chaise of table.chaises) { chaises += 1; assert.equal(chaise.selection, false); assert.equal(chaise.focus, false); } } assert.ok(chaises > 0); }); test('régime sièges : un siège marque sa chaise, une place sans siège marque sa table', () => { const charge = petite(); const etat = etatPlaces(charge, { tour: 1 }); const [, deuxieme, troisieme] = etat.tables; const modele = modeleDessin(etat, options(charge, { selection: { table: deuxieme.id, siege: 2 }, actif: { table: troisieme.id, siege: null } })); assert.equal(modele.tables[1].regime, 'sieges'); assert.deepEqual(porteurs(modele, 'selection'), [`siege:${deuxieme.id}:2`]); assert.deepEqual(porteurs(modele, 'focus'), [`table:${troisieme.id}`]); const memePlace = modeleDessin(etat, options(charge, { selection: { table: deuxieme.id, siege: 1 }, actif: { table: deuxieme.id, siege: 1 } })); assert.deepEqual(porteurs(memePlace, 'selection'), [`siege:${deuxieme.id}:1`]); assert.deepEqual(porteurs(memePlace, 'focus'), [`siege:${deuxieme.id}:1`]); }); test('régime table : un siège, que nulle chaise ne dessine, marque sa table', () => { const charge = petite(); const etat = etatPlaces(charge, { tour: 1 }); const cible = etat.tables[0]; const modele = modeleDessin(etat, options(charge, { k: 0.2, selection: { table: cible.id, siege: 2 }, actif: { table: cible.id, siege: 3 } })); assert.equal(modele.tables[0].regime, 'table'); assert.deepEqual(porteurs(modele, 'selection'), [`table:${cible.id}`]); assert.deepEqual(porteurs(modele, 'focus'), [`table:${cible.id}`]); }); test('une place d’une table absente ne marque rien', () => { const charge = petite(); const modele = modeleDessin(etatPlaces(charge, { tour: 1 }), options(charge, { selection: { table: 99, siege: 1 }, actif: { table: 99, siege: null } })); assert.deepEqual(porteurs(modele, 'selection'), []); assert.deepEqual(porteurs(modele, 'focus'), []); }); }); // --- Le détail d'une table en conflit (I11) -------------------------------------- describe('detailConflits : le détail que rend le moteur, nommé (§ 7.1, § 7.3)', () => { test('petite démonstration en conflit : chaque règle violée, ses paires ou ses personnes, dans l’ordre du moteur', () => { const charge = avecPropositions(petite('petite-conflit')); const nomDe = nomsDe(charge); let vus = 0; for (let tour = 1; tour <= charge.evenement.tours; tour += 1) { for (const table of etatPlaces(charge, { tour, proposition: 1 }).tables) { const detail = detailConflits(table, nomDe); if (table.conflits.nombre === 0) { assert.equal(detail, null); continue; } vus += 1; assert.deepEqual(detail, { numero: table.numero, nombre: table.conflits.nombre, regles: table.conflits.regles.map((regle) => ({ cle: `plan.infobulle.${regle.regle}`, groupes: (regle.paires ?? regle.personnes.map((id) => [id])).map((groupe) => groupe.map(nomDe)), })), }); for (const { cle } of detail.regles) assert.ok(typeof t(cle) === 'string' && t(cle).length > 0, cle); } } assert.ok(vus > 0, 'au moins une table en conflit'); }); test('conflits non évalués, ou nuls : null', () => { const charge = petite(); const [table] = etatPlaces(charge, { tour: 1 }).tables; assert.equal(detailConflits({ ...table, conflits: null }, nomsDe(charge)), null); assert.equal(detailConflits({ ...table, conflits: { nombre: 0, regles: [] } }, nomsDe(charge)), null); }); }); // --- Le texte entier d'une ligne, et les actions des panneaux (I11) -------------- describe('texteLigne : la ligne d’une chaise en un texte, nom accessible du siège', () => { const nomDe = (id) => ({ 1: 'Iris Ombrelle', 2: 'Théo Pervenche' })[id]; const chaise = (autres) => ({ siege: 3, occupant: null, reservation: null, titre: null, ...autres }); test('les parties de textesLigne, bout à bout', () => { const cas = [ [chaise({}), '3. (libre)'], [chaise({ occupant: 1 }), '3. Iris Ombrelle'], [ chaise({ occupant: 1, reservation: { participant: 1, portee: 'tous', tour: null }, titre: { libelle: 'animateur', pourvu: true } }), '3. (réservée, tous les tours) Iris Ombrelle \u{2014} animateur', ], [chaise({ titre: { libelle: 'hôte', pourvu: false } }), '3. (libre) \u{2014} titre hôte non pourvu'], [chaise({ occupant: 2, reservation: { participant: 1, portee: 'tour', tour: 2 } }), '3. (réservée à Iris Ombrelle, tour 2) Théo Pervenche'], ]; for (const [entree, attendu] of cas) assert.equal(texteLigne(ligneListe(entree, nomDe), t), attendu); }); }); describe('actionsPlace : ce qu’offre le panneau d’une place (§ 7.1, § 2.1 étape 6)', () => { const libre = { siege: 1, occupant: null, reservation: null, titre: null }; const tenue = { siege: 2, occupant: 1, reservation: { participant: 1, portee: 'tous', tour: null }, titre: { libelle: 'hôte', pourvu: true } }; const reservation = { genre: 'reservation', participant: 1, depuis: { table: 1, siege: 2 }, de: { table: 1, portee: 'tous', tour: null } }; test('une place libre : assigner, réserver, donner un titre, désigner — exactement', () => { assert.deepEqual(actionsPlace(libre, null), ['assigner', 'reserver', 'donnerTitre', 'designer']); assert.deepEqual(actionsPlace(libre, { genre: 'personne', participant: 2, depuis: null }), ['assigner', 'reserver', 'donnerTitre', 'designer']); }); test('une place réservée et titrée : retirer la réservation, retirer le titre ; une personne choisie y ajoute assigner, que la pose refusera', () => { assert.deepEqual(actionsPlace(tenue, null), ['retirerReservation', 'retirerTitre']); assert.deepEqual(actionsPlace(tenue, reservation), ['retirerReservation', 'retirerTitre', 'deplacerReservationIci']); assert.deepEqual(actionsPlace(tenue, { genre: 'personne', participant: 2, depuis: null }), ['assigner', 'retirerReservation', 'retirerTitre']); }); test('une place titrée, non tenue : assigner, réserver, désigner, retirer le titre', () => { assert.deepEqual(actionsPlace({ ...libre, titre: { libelle: 'hôte', pourvu: false } }, null), ['assigner', 'reserver', 'designer', 'retirerTitre']); }); test('une réservation choisie ajoute « Déplacer la réservation ici »', () => { assert.deepEqual(actionsPlace(libre, reservation), ['assigner', 'reserver', 'donnerTitre', 'designer', 'deplacerReservationIci']); }); }); describe('actionsPersonne : le menu d’une personne (§ 7.1)', () => { const assise = { siege: 2, occupant: 4, reservation: null, titre: null }; const retrait = { commande: 'retirerDuTour', arguments: { tour: 1, participant: 4 } }; test('assise, non réservée : déplacer, échanger, retirer du tour, réserver ici, exclure, ouvrir la fiche', () => { assert.deepEqual(actionsPersonne({ exclu: false, chaise: assise, retrait }), [ 'deplacer', 'echanger', 'retirerDuTour', 'reserverIci', 'exclure', 'ouvrirFiche', ]); }); test('assise sur sa réservation : déplacer sa réservation, plus de « réserver ici » ; sans retrait possible, pas de « retirer du tour »', () => { const tenue = { ...assise, reservation: { participant: 4, portee: 'tous', tour: null } }; assert.deepEqual(actionsPersonne({ exclu: false, chaise: tenue, retrait: null }), ['deplacer', 'echanger', 'deplacerReservation', 'exclure', 'ouvrirFiche']); }); test('hors du dessin — réserve, exclue — : déplacer, puis exclure ou réintégrer, et la fiche', () => { assert.deepEqual(actionsPersonne({ exclu: false, chaise: null, retrait: null }), ['deplacer', 'exclure', 'ouvrirFiche']); assert.deepEqual(actionsPersonne({ exclu: true, chaise: null, retrait: null }), ['deplacer', 'reintegrer', 'ouvrirFiche']); }); }); describe('executerSurPlace : la commande d’un panneau, son refus posé sur la place (§ 5.9)', () => { // Une séance et un état de navigation d'épreuve, qui relèvent leurs appels. function dependances(executer) { const appels = []; return { appels, seance: { executer: async (nom, args) => (appels.push(['executer', nom, args]), executer(nom, args)) }, ui: { choisir: (choix) => appels.push(['choisir', choix]), poserSurPlace: (pose) => appels.push(['poserSurPlace', pose]), }, }; } const refus = (code, details = {}) => Object.assign(new Error(code), { code, details }); const place = { table: 2, siege: 3 }; test('réussie : rend le rendu de la séance, oublie le choix en attente et retire la pose', async () => { const d = dependances(() => ({ libelle: 'ok', avertissements: [] })); assert.deepEqual(await executerSurPlace(d, place, 'reserver', { participant: 1 }), { libelle: 'ok', avertissements: [] }); assert.deepEqual(d.appels, [['executer', 'reserver', { participant: 1 }], ['choisir', null], ['poserSurPlace', null]]); }); test('refusée : le refus, code et détails, posé sur la place ; rien d’autre ; null', async () => { const d = dependances(() => { throw refus('PLACE_RESERVEE', { table: 2, siege: 3 }); }); assert.equal(await executerSurPlace(d, place, 'reserver', {}), null); assert.deepEqual(d.appels.slice(1), [ ['poserSurPlace', { place, refus: { code: 'PLACE_RESERVEE', details: { table: 2, siege: 3 } }, avertissements: [] }], ]); }); test('un code local, une place absente, ou une erreur sans code remontent à l’appelant', async () => { for (const [lieu, erreur, options] of [ [place, refus('LIBELLE_VIDE'), { locaux: ['LIBELLE_VIDE'] }], [null, refus('SANS_EFFET'), {}], [place, new TypeError('défaut'), {}], ]) { const d = dependances(() => { throw erreur; }); await assert.rejects(executerSurPlace(d, lieu, 'poserTitre', {}, options), (recue) => recue === erreur); assert.deepEqual(d.appels.map(([nom]) => nom), ['executer']); } }); }); describe('les clés que les panneaux composent existent (§ 14.6)', () => { test('plan.action., plan.menu., plan.infobulle. : chaque valeur possible a sa clé', () => { const tenue = { siege: 1, occupant: 1, reservation: { participant: 1, portee: 'tous', tour: null }, titre: { libelle: 'hôte', pourvu: true } }; const libre = { siege: 1, occupant: null, reservation: null, titre: null }; const reservation = { genre: 'reservation', participant: 1, depuis: null, de: { table: 1, portee: 'tous', tour: null } }; const actions = new Set([...actionsPlace(libre, reservation), ...actionsPlace(tenue, { genre: 'personne', participant: 2, depuis: null })]); const menu = new Set([ ...actionsPersonne({ exclu: false, chaise: { ...libre, occupant: 4 }, retrait: {} }), ...actionsPersonne({ exclu: true, chaise: tenue, retrait: {} }), ]); assert.equal(actions.size, 7); assert.equal(menu.size, 8); for (const action of actions) assert.equal(typeof t(`plan.action.${action}`), 'string', action); for (const action of menu) assert.equal(typeof t(`plan.menu.${action}`), 'string', action); for (const regle of ['separerAppartenances', 'nouveauxVoisins', 'nouvelleTable']) assert.equal(typeof t(`plan.infobulle.${regle}`), 'string'); }); });