409 lines
21 KiB
JavaScript
409 lines
21 KiB
JavaScript
|
|
// © 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 });
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|