gestion_table_tournante_libre/src/interface/import/modele.test.js
Mathieu Benoit 134e7850b7 [ADD] interface: event list, open event, participants, import, plan
The working screens of an evening. The home screen lists the events of
the working folder, asks its question, loads demos, copies, renames,
imports and deletes, and shows what the trash holds. An open event frames
its mode, offers Modifier, Débloquer and a confirmed lock takeover, and
shows its warnings. Participants get a grid, a form and a file; an import
goes through a preview and a report; tables, the default and tours have
their screens; the plan draws tables, chairs, empty seats and names.
Checked: 2342 node, 356 browser, 60 node-long; the Electron start check.

--- FR ---

[ADD] interface : liste, événement, participants, import, tables, plan

Les écrans de travail d'une soirée. L'accueil liste les événements du
dossier de travail, pose sa question, charge les démonstrations, copie,
renomme, importe et supprime, et montre ce que tient la corbeille. Un
événement ouvert encadre son mode, offre Modifier, Débloquer et une
reprise de verrou confirmée, et montre ses avertissements. Les
participants ont une grille, un formulaire et une fiche ; un import passe
par un aperçu et un rapport ; tables, défaut et tours ont leurs écrans ;
le plan dessine tables, chaises, sièges vides et noms.
Vérifié : 2342 node, 356 navigateur, 60 node-long ; démarrage d'Electron.

Assisted-by: Claude Opus 5.5
2026-10-07 08:12:27 -04:00

408 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Le modèle de l'aperçu d'import (§ 10.1, § 10.2) : la lecture d'un collage
// sans en-tête dans l'ordre de la grille, la clé de la cause et celle du
// remède de chaque refus global, l'encodage et le séparateur nommés, et les
// titres que « Remplacer » annonce, comptés par la règle unique des titres
// pourvus. Les noms d'épreuve sont inventés.
import assert from 'node:assert/strict';
import { describe, test } from '../../../test/lanceur.js';
import { TABLES_APPLICATION } from '../../application/libelles.js';
import { apercevoir, bilanRemplacement, CHAMPS, decoder } from '../../application/csv.js';
import { comptesTitres, titresNonPourvus } from '../../application/listes_travail.js';
import { creerTraduction } from '../../application/traduction.js';
import { creerCharge } from '../../stockage/document.js';
import { TABLES_INTERFACE } from '../libelles.js';
import {
COLONNES_DES_LIGNES,
ENCODAGES,
MODES_D_IMPORT,
SEPARATEURS,
avecEntetesDeLaGrille,
modeleApercu,
modePropose,
motifLigne,
nommer,
refuseesDe,
} from './modele.js';
const { t } = creerTraduction([...TABLES_APPLICATION, ...TABLES_INTERFACE]);
const TAB = '\t';
const lignes = (...enregistrements) => `${enregistrements.join('\n')}\n`;
// Les noms lus des lignes valides d'un aperçu, dans l'ordre du texte.
const nomsLus = (apercu) => apercu.lignes.map(({ champs }) => champs.nom);
// Le refus de decoder sur ces octets, sous la forme { code, details }.
function refusDeDecoder(octets) {
try {
decoder(octets);
} catch (erreur) {
return { code: erreur.code, details: erreur.details };
}
throw new Error('decoder a accepté les octets');
}
const octets = (...valeurs) => Uint8Array.from(valeurs);
const utf8 = (texte) => new TextEncoder().encode(texte);
const MARQUE_UTF8 = [0xef, 0xbb, 0xbf];
describe('avecEntetesDeLaGrille : un collage sans en-tête lu dans l’ordre de la grille (§ 10.2)', () => {
test('trois colonnes à tabulations, sans en-tête : AUCUN_ENTETE, puis nom, prénom et appartenance, les rangs comptant la ligne ajoutée', () => {
const texte = lignes(
['Ombrelle', 'Iris', 'Club des Merles'].join(TAB),
['Pervenche', 'Théo', 'Grisaille'].join(TAB),
);
const sans = apercevoir(texte);
assert.equal(sans.refusGlobal, 'AUCUN_ENTETE');
const avec = avecEntetesDeLaGrille(texte, sans);
assert.equal(avec, `${['nom', 'prenom', 'appartenance'].join(TAB)}\n${texte}`);
const lu = apercevoir(avec);
assert.equal(lu.refusGlobal, null);
assert.deepEqual(lu.association.colonnes, { nom: 0, prenom: 1, appartenance: 2 });
assert.equal(lu.lignes.length, 2);
assert.deepEqual(lu.lignes.map(({ ligne }) => ligne), [2, 3]);
assert.deepEqual(lu.lignes[0].champs.appartenance, 'Club des Merles');
});
test('deux colonnes : nom et prénom', () => {
const texte = lignes(['Ombrelle', 'Iris'].join(TAB), ['Pervenche', 'Théo'].join(TAB));
const sans = apercevoir(texte);
assert.equal(sans.refusGlobal, 'AUCUN_ENTETE');
const lu = apercevoir(avecEntetesDeLaGrille(texte, sans));
assert.equal(lu.refusGlobal, null);
assert.deepEqual(lu.association.colonnes, { nom: 0, prenom: 1 });
assert.deepEqual(nomsLus(lu), ['Ombrelle', 'Pervenche']);
});
test('neuf colonnes : les sept champs de la grille, puis deux en-têtes vides, listés non reconnus', () => {
const rangee = (nom) => [nom, 'Iris', 'Grisaille', 'iris@exemple.test', 'animation', 'non', 'note', 'x1', 'x2'].join(TAB);
const texte = lignes(rangee('Ombrelle'), rangee('Lacasse'));
const sans = apercevoir(texte);
assert.equal(sans.refusGlobal, 'AUCUN_ENTETE');
assert.equal(sans.entetes.length, 9);
const avec = avecEntetesDeLaGrille(texte, sans);
assert.equal(avec.split('\n')[0], [...CHAMPS, '', ''].join(TAB));
const lu = apercevoir(avec);
assert.equal(lu.refusGlobal, null);
assert.equal(CHAMPS.length, 7);
assert.deepEqual(lu.association.colonnes, Object.fromEntries(CHAMPS.map((champ, rang) => [champ, rang])));
assert.deepEqual(lu.association.nonReconnues, [7, 8]);
assert.deepEqual(nomsLus(lu), ['Ombrelle', 'Lacasse']);
});
test('une liste de noms d’une colonne, « Lacasse, Ondine » compris : SEPARATEUR_INTROUVABLE sans rang, puis une colonne, la ligne entière en nom', () => {
const texte = lignes('Ombrelle', 'Lacasse, Ondine', 'Pervenche');
const sans = apercevoir(texte);
assert.equal(sans.refusGlobal, 'SEPARATEUR_INTROUVABLE');
assert.equal(sans.refusGlobalLigne, null);
const avec = avecEntetesDeLaGrille(texte, sans);
assert.equal(avec, `nom\n${texte}`);
const lu = apercevoir(avec);
assert.equal(lu.refusGlobal, null);
assert.equal(lu.separateur, null);
assert.deepEqual(nomsLus(lu), ['Ombrelle', 'Lacasse, Ondine', 'Pervenche']);
});
test('un texte blanc, un guillemet ouvert, un séparateur écarté à un rang nommé, un aperçu sans refus : null', () => {
const blanc = ' \n\t\n';
assert.equal(apercevoir(blanc).refusGlobal, 'AUCUN_ENTETE');
assert.equal(avecEntetesDeLaGrille(blanc, apercevoir(blanc)), null);
const guillemet = lignes('nom;prenom', '"Ombrelle;Iris', 'Pervenche;Théo');
assert.equal(apercevoir(guillemet).refusGlobal, 'GUILLEMET_OUVERT');
assert.equal(avecEntetesDeLaGrille(guillemet, apercevoir(guillemet)), null);
const ecarte = lignes('Lacasse, Ondine', 'Ombrelle', 'Pervenche');
const refus = apercevoir(ecarte);
assert.equal(refus.refusGlobal, 'SEPARATEUR_INTROUVABLE');
assert.equal(refus.refusGlobalLigne, 2);
assert.equal(avecEntetesDeLaGrille(ecarte, refus), null);
const valide = lignes('nom;prenom', 'Ombrelle;Iris');
assert.equal(avecEntetesDeLaGrille(valide, apercevoir(valide)), null);
});
});
// Chaque refus global de la table des décisions d'I6, la façon de le
// produire, et la clé du remède attendue. Les quatre de decoder passent par
// refusDecodage ; les autres, par l'aperçu d'un fichier (encodage donné) ou
// d'un collage (encodage null).
const REFUS = [
['UTF16_SANS_MARQUE', { refusDecodage: refusDeDecoder(octets(0x6e, 0x00, 0x6f, 0x00)) }, 'ENREGISTRER_EN_UTF8'],
['UTF16_INVALIDE', { refusDecodage: refusDeDecoder(octets(0xff, 0xfe, 0x6e, 0x00, 0x6f)) }, 'ENREGISTRER_EN_UTF8'],
['UTF8_INVALIDE', { refusDecodage: refusDeDecoder(octets(...MARQUE_UTF8, 0x6e, 0xff, 0x6f)) }, 'ENREGISTRER_EN_UTF8'],
['CARACTERE_NUL', { refusDecodage: refusDeDecoder(octets(...MARQUE_UTF8, ...utf8('nom\nOmbrelle\u{0}\n'))) }, 'ENREGISTRER_EN_UTF8'],
['CARACTERE_NUL', { encodage: null, apercu: apercevoir('nom\nOmbrelle\u{0}\n') }, 'import.remede.caractereNulCollage'],
['AUCUN_ENTETE', { encodage: 'utf-8', apercu: apercevoir(lignes('a;b', 'Ombrelle;Iris')) }, 'import.remede.aucunEntete'],
['AUCUN_ENTETE', { encodage: null, apercu: apercevoir(lignes('Ombrelle;Iris', 'Pervenche;Théo')) }, 'import.remede.aucunEnteteCollage'],
['AUCUN_ENTETE', { encodage: null, apercu: apercevoir(' \n') }, 'import.remede.aucunEntete'],
['NOM_NON_ASSOCIE', { encodage: 'utf-8', apercu: apercevoir(lignes('prenom;notes', 'Iris;x')) }, 'import.remede.nomNonAssocie'],
['AUCUNE_LIGNE_VALIDE', { encodage: 'utf-8', apercu: apercevoir(lignes('nom;prenom', ';Iris')) }, 'import.remede.aucuneLigneValide'],
['GUILLEMET_OUVERT', { encodage: 'utf-8', apercu: apercevoir(lignes('nom;prenom', '"Ombrelle;Iris')) }, 'import.remede.guillemetOuvert'],
[
'SEPARATEUR_INTROUVABLE',
{ encodage: 'utf-8', apercu: apercevoir(lignes('nom;prenom', 'Ombrelle;Iris;x', 'Pervenche')) },
'import.remede.separateurIntrouvable',
],
[
'SEPARATEUR_INTROUVABLE',
{ encodage: null, apercu: apercevoir(lignes('Ombrelle', 'Lacasse, Ondine')) },
'import.remede.separateurIntrouvableCollage',
],
];
describe('modeleApercu : la cause et le remède de chaque refus global (§ 10.1)', () => {
test('chaque refus de la table des décisions rend la clé de sa cause, le code, et celle de son remède, toutes deux traduites', () => {
const codes = new Set(REFUS.map(([code]) => code));
assert.equal(codes.size, 9);
assert.equal(REFUS.length, 13);
for (const [code, entree, remede] of REFUS) {
if (entree.apercu !== undefined) assert.equal(entree.apercu.refusGlobal, code, code);
if (entree.refusDecodage !== undefined) assert.equal(entree.refusDecodage.code, code, code);
const { global } = modeleApercu(entree);
assert.notEqual(global, null, code);
assert.equal(global.cle, code);
assert.equal(global.remede.cle, remede, code);
assert.equal(typeof t(global.cle, global.details), 'string');
assert.ok(t(global.cle, global.details).length > 0, code);
assert.ok(t(global.remede.cle, global.remede.details).length > 0, code);
}
});
test('un refus de decoder : son texte compose déjà le remède, que le modèle dit et ne répète pas ; un refus de l’aperçu le porte à part', () => {
const [, entree] = REFUS[0];
const { global } = modeleApercu(entree);
assert.equal(global.remede.dansLaCause, true);
assert.ok(t(global.cle, global.details).includes(t(global.remede.cle, global.remede.details)));
for (const [code, apercuDonne] of REFUS.slice(4)) {
assert.equal(modeleApercu(apercuDonne).global.remede.dansLaCause, false, code);
}
});
test('le rang d’un guillemet ouvert et d’un séparateur écarté passe à la cause et au remède', () => {
const guillemet = modeleApercu(REFUS[10][1]).global;
assert.deepEqual(guillemet.details, { ligne: 2 });
assert.match(t(guillemet.cle, guillemet.details), /ligne 2/);
assert.match(t(guillemet.remede.cle, guillemet.remede.details), /2/);
const separateur = modeleApercu(REFUS[11][1]).global;
assert.deepEqual(separateur.details, { ligne: 2 });
assert.match(t(separateur.cle, separateur.details), /ligne 2/);
});
test('un code de refus que la table ne connaît pas lève, jamais un remède muet', () => {
const apercu = { ...apercevoir(lignes('nom', 'Ombrelle')), refusGlobal: 'REFUS_INVENTE' };
assert.throws(() => modeleApercu({ encodage: 'utf-8', apercu }), RangeError);
assert.throws(
() => modeleApercu({ refusDecodage: { code: 'REFUS_INVENTE', details: { remede: 'ENREGISTRER_EN_UTF8' } } }),
RangeError,
);
});
test('sous un refus de decoder : ni encodage, ni séparateur, ni colonne, ni ligne', () => {
const modele = modeleApercu(REFUS[0][1]);
assert.equal(modele.encodage, null);
assert.equal(modele.separateur, null);
for (const liste of ['colonnes', 'ambigus', 'lignes', 'refusees', 'fusions', 'doublons', 'homonymes', 'multilignes']) {
assert.deepEqual(modele[liste], [], liste);
}
assert.equal(modele.valides, 0);
});
});
describe('modeleApercu : ce que l’aperçu montre (§ 10.1)', () => {
test('chaque encodage de decoder et chaque séparateur ont leur clé, qui se traduit, et un collage n’a pas d’encodage', () => {
const encodages = ['utf-8-bom', 'utf-16le', 'utf-16be', 'utf-8', 'windows-1252'];
assert.deepEqual([...ENCODAGES.keys()], encodages);
for (const encodage of encodages) assert.ok(t(ENCODAGES.get(encodage)).length > 0, encodage);
assert.deepEqual([...SEPARATEURS.keys()], [';', ',', '\t', null]);
for (const cle of SEPARATEURS.values()) assert.ok(t(cle).length > 0, cle);
assert.equal(t(SEPARATEURS.get(null)), 'une seule colonne');
const apercu = apercevoir(lignes('nom,prenom', 'Ombrelle,Iris'));
const fichier = modeleApercu({ encodage: 'windows-1252', apercu });
assert.equal(fichier.encodage, ENCODAGES.get('windows-1252'));
assert.equal(fichier.separateur, SEPARATEURS.get(','));
const collage = modeleApercu({ apercu });
assert.equal(collage.encodage, null);
assert.equal(collage.separateur, SEPARATEURS.get(','));
const uneColonne = modeleApercu({ encodage: 'utf-8', apercu: apercevoir(lignes('nom', 'Lacasse, Ondine')) });
assert.equal(uneColonne.separateur, SEPARATEURS.get(null));
});
test('un séparateur que la lecture n’a pas établi n’est pas nommé « une seule colonne »', () => {
for (const [code, entree] of REFUS.slice(4)) {
const { separateur } = modeleApercu(entree);
const etabli = ['AUCUN_ENTETE', 'NOM_NON_ASSOCIE', 'AUCUNE_LIGNE_VALIDE', 'GUILLEMET_OUVERT'].includes(code) &&
entree.apercu.entetes.length > 0;
if (etabli) assert.equal(separateur, SEPARATEURS.get(entree.apercu.separateur), code);
else assert.equal(separateur, null, code);
}
});
test('colonnes associées, non reconnues, à trancher puis écartée ; ambiguïtés nommées par leurs en-têtes', () => {
const texte = lignes('Nom;Organisation;Entreprise;Allergies', 'Ombrelle;Club des Merles;Grisaille;aucune');
const ambigu = modeleApercu({ encodage: 'utf-8', apercu: apercevoir(texte) });
assert.deepEqual(
ambigu.colonnes.map(({ rang, entete, cle }) => [rang, entete, cle]),
[
[0, 'Nom', 'import.champ.nom'],
[1, 'Organisation', 'import.colonne.aTrancher'],
[2, 'Entreprise', 'import.colonne.aTrancher'],
[3, 'Allergies', 'import.colonne.nonReconnue'],
],
);
assert.deepEqual(ambigu.ambigus, [
{ champ: 'appartenance', cle: 'import.champ.appartenance', rangs: [{ rang: 1, entete: 'Organisation' }, { rang: 2, entete: 'Entreprise' }] },
]);
const tranche = modeleApercu({ encodage: 'utf-8', apercu: apercevoir(texte, { choix: { appartenance: 2 } }) });
assert.deepEqual(tranche.colonnes.map(({ cle }) => cle), [
'import.champ.nom', 'import.colonne.ecartee', 'import.champ.appartenance', 'import.colonne.nonReconnue',
]);
assert.deepEqual(tranche.ambigus, []);
for (const { cle } of [...ambigu.colonnes, ...tranche.colonnes]) assert.ok(t(cle).length > 0, cle);
});
test('chaque champ de CHAMPS a sa clé, qui se traduit', () => {
const texte = lignes(CHAMPS.join(';'), ['Ombrelle', 'Iris', 'Grisaille', 'i@exemple.test', 'x', 'non', 'n'].join(';'));
const { colonnes } = modeleApercu({ encodage: 'utf-8', apercu: apercevoir(texte) });
assert.equal(colonnes.length, 7);
for (const { cle } of colonnes) {
assert.match(cle, /^import\.champ\./);
assert.ok(t(cle).length > 0, cle);
}
});
test('lignes refusées, fusions et doublons, en données ; le motif affiché est celui de t', () => {
const existants = [{ id: 4, nom: 'Ombrelle', prenom: 'Iris', appartenance: 'Coopérative Bleue' }];
const texte = lignes(
'nom;prenom;appartenance;exclu',
'Ombrelle;Iris;cooperative bleue;',
';Théo;Grisaille;',
'Pervenche;Théo;COOPÉRATIVE BLEUE;peut-être',
'Pervenche;Théo;COOPÉRATIVE BLEUE;x',
'Lacasse;Ondine;;',
);
const modele = modeleApercu({ encodage: 'utf-8', apercu: apercevoir(texte, { participants: existants }) });
assert.equal(modele.valides, 3);
assert.deepEqual(modele.refusees, [
{ ligne: 3, code: 'NOM_ABSENT', details: { ligne: 3, valeur: null } },
{ ligne: 4, code: 'EXCLU_INCONNU', details: { ligne: 4, valeur: 'peut-être' } },
]);
assert.deepEqual(modele.fusions, [
{
affichee: 'Coopérative Bleue',
orthographes: [
{ texte: 'Coopérative Bleue', lignes: 0 },
{ texte: 'cooperative bleue', lignes: 1 },
{ texte: 'COOPÉRATIVE BLEUE', lignes: 1 },
],
},
]);
assert.deepEqual(modele.doublons, [{ lignes: [2], existants: [4] }]);
const motif = motifLigne(t);
const [, refusee] = modele.refusees;
assert.equal(motif(refusee.code, refusee.details), t('EXCLU_INCONNU', { ligne: 4, valeur: 'peut-être' }));
assert.match(motif(refusee.code, refusee.details), /peut-être/);
});
});
describe('les lignes, les refus de la mise à jour, les personnes désignées, le mode proposé (§ 10.1)', () => {
test('chaque ligne valide montre ses sept champs dans l’ordre de CHAMPS ; exclu par sa clé, vide sans valeur', () => {
const texte = lignes(
'notes;exclu;nom;prenom',
'arrive tard;Oui;Ombrelle;Iris',
';;Pervenche;',
';0;Lacasse;Ondine',
);
const { lignes: lues } = modeleApercu({ encodage: 'utf-8', apercu: apercevoir(texte) });
assert.equal(lues.length, 3);
assert.deepEqual(lues.map(({ ligne }) => ligne), [2, 3, 4]);
for (const { cellules } of lues) assert.deepEqual(cellules.map(({ champ }) => champ), [...CHAMPS]);
assert.deepEqual(COLONNES_DES_LIGNES.map(({ champ }) => champ), [...CHAMPS]);
for (const { cle } of COLONNES_DES_LIGNES) assert.ok(t(cle).length > 0, cle);
const [premiere, deuxieme, troisieme] = lues;
assert.deepEqual(premiere.cellules.map(({ texte: valeur }) => valeur), ['Ombrelle', 'Iris', null, null, null, null, 'arrive tard']);
assert.equal(premiere.cellules[5].cle, 'import.exclu.oui');
assert.equal(deuxieme.cellules[5].cle, null);
assert.equal(troisieme.cellules[5].cle, 'import.exclu.non');
for (const cle of ['import.exclu.oui', 'import.exclu.non']) assert.ok(t(cle).length > 0);
});
test('refuseesDe : les refus de la mise à jour gardent les personnes désignées et la première ligne, sans les champs découpés', () => {
const refusees = [
{ ligne: 3, code: 'PLUSIEURS_CORRESPONDENT', valeur: null, participants: [2, 5], brut: ['Lacasse', ''] },
{ ligne: 5, code: 'DEJA_DESIGNE', valeur: null, participants: [1], premiereLigne: 4, brut: ['Ombrelle', ''] },
];
assert.deepEqual(refuseesDe(refusees), [
{ ligne: 3, code: 'PLUSIEURS_CORRESPONDENT', details: { ligne: 3, valeur: null, participants: [2, 5] } },
{ ligne: 5, code: 'DEJA_DESIGNE', details: { ligne: 5, valeur: null, participants: [1], premiereLigne: 4 } },
]);
const motif = motifLigne(t);
assert.match(motif('DEJA_DESIGNE', refuseesDe(refusees)[1].details), /ligne 4/);
});
test('nommer : les personnes d’une liste d’identifiants, dans son ordre ; une personne que la liste n’a plus est omise', () => {
const participants = [
{ id: 2, nom: 'Lacasse', prenom: 'Ondine' },
{ id: 5, nom: 'Lacasse', prenom: null },
];
assert.deepEqual(nommer(participants, [5, 2, 9]), [
{ id: 5, nom: 'Lacasse', prenom: null },
{ id: 2, nom: 'Lacasse', prenom: 'Ondine' },
]);
});
test('les trois modes offerts, chacun nommé par sa clé, ceux de l’aperçu de src/csv', () => {
assert.deepEqual(MODES_D_IMPORT.map(({ mode }) => mode), ['ajouter', 'mettreAJour', 'remplacer']);
for (const { mode, cle } of MODES_D_IMPORT) {
assert.ok(t(cle).length > 0, cle);
assert.equal(apercevoir(lignes('nom', 'Ombrelle'), { mode }).mode, mode);
}
});
test('le mode proposé : Ajouter pour une liste vide, Mettre à jour sinon ; jamais Remplacer', () => {
assert.equal(modePropose([]), 'ajouter');
assert.equal(modePropose([{ id: 1, nom: 'Ombrelle' }]), 'mettreAJour');
});
});
// Une charge de deux tours, une table de six places, trois titres à cette
// table, et trois personnes : la première réservée « tous » au siège du
// premier titre, la deuxième réservée au tour 1 seul au siège du deuxième,
// la troisième sans réservation. Sans attribution, les réservations ne
// portent pas de siège.
function chargeAuxTitres({ attribuerSieges }) {
const charge = creerCharge({ id: 'epreuve', nom: 'Soirée des Merles', siegesParDefaut: 6, tours: 2 });
charge.reglages.attribuerSieges = attribuerSieges;
charge.participants = ['Ombrelle', 'Pervenche', 'Lacasse'].map((nom, rang) => ({
id: rang + 1, nom, prenom: null, appartenance: null, courriel: null, titrePressenti: null, notes: null, exclu: false,
}));
charge.tables = [{ id: 1, numero: 1, sieges: null, forme: 'ronde', position: { x: 0, y: 0 } }];
charge.titres = [1, 2, 3].map((siege) => ({ table: 1, siege, libelle: `Animation ${siege}` }));
charge.reservations = [
{ participant: 1, table: 1, siege: attribuerSieges ? 1 : null, portee: 'tous', tour: null },
{ participant: 2, table: 1, siege: attribuerSieges ? 2 : null, portee: 'tour', tour: 1 },
];
charge.prochainsIds = { participant: 4, table: 2, proposition: 1 };
return charge;
}
describe('les titres que « Remplacer » annonce (§ 10.1, § 4.1)', () => {
for (const attribuerSieges of [true, false]) {
test(`sièges ${attribuerSieges ? 'attribués' : 'non attribués, réservations sans siège'} : deux titres perdent leur réservation, un « tous » et un « tour »`, () => {
const charge = chargeAuxTitres({ attribuerSieges });
const bilan = bilanRemplacement(charge);
assert.equal(bilan.titresPourvus, 2);
assert.equal(charge.tables.length, 1);
const pourSoiree = charge.tables.reduce((somme, { id }) => somme + comptesTitres(charge, id).pourvus, 0);
assert.equal(pourSoiree, 1);
const aUnTour = titresNonPourvus(charge).filter(({ pourvuAuxTours }) => pourvuAuxTours.length > 0).length;
assert.equal(aUnTour, 1);
assert.equal(bilan.titresPourvus, pourSoiree + aUnTour);
assert.deepEqual(bilan, { participants: 3, reservations: 2, titresPourvus: 2, propositions: 0 });
});
}
});