gestion_table_tournante_libre/src/application/evenements.test.js

1116 lines
48 KiB
JavaScript
Raw Normal View History

2026-10-07 04:14:24 -04:00
// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Épreuves de la façade des événements (§ 8.1, § 8.6, § 8.7, § 8.8, § 15,
// § 19.4), toutes sur le système de fichiers d'épreuve, qui compte ses
// lectures et ses écritures, avec l'horloge pilotée. La façade se branche sur
// une séance réelle, que le montage crée sur la racine que reçoit
// changerRacine, comme App la branche ; chaque appel que la façade fait à la
// séance s'inscrit au relevé du montage. Les noms d'épreuve sont inventés ; la
// version vient du module engendré.
import assert from 'node:assert/strict';
import { creerFichiersSimules } from '../../test/fichiers_simules.js';
import { describe, test } from '../../test/lanceur.js';
import { CATALOGUE } from '../demo/catalogue.js';
import { LIVREES, texteLivre } from '../demo/livrees.js';
import { serialiser } from '../stockage/canonique.js';
import { creer as creerDansLeDepot } from '../stockage/depot.js';
import { analyser, creerCharge } from '../stockage/document.js';
import { ErreurStockage } from '../stockage/erreurs.js';
import { lireJournal } from '../stockage/journal.js';
import { SUFFIXES } from '../stockage/noms.js';
import { VERSION } from '../version.genere.js';
import { creerFacade } from './evenements.js';
import { creerHorlogePilotee } from './horloge.js';
import { libelle } from './libelles.js';
import { creerSeance } from './seance.js';
const V1 = VERSION.affichee;
const DOCUMENTS = 'C:\\Users\\Exemple\\Documents\\Gestion table tournante Libre';
const DOC = { id: 'documents', chemin: DOCUMENTS };
const PUBLIE = 'E:\\soirees';
const PORTABLE = { id: 'portable', chemin: 'E:\\soirees\\data' };
const SOUS_APPDATA = 'C:\\Users\\Exemple\\AppData\\Local\\Programmes\\Tablee';
const PORTABLE_APPDATA = { id: 'portable', chemin: `${SOUS_APPDATA}\\data` };
// L'instant de départ de l'horloge pilotée, et le dossier de corbeille qu'il
// date.
const DEPART = '2026-10-06T19:00:00-04:00';
const CORBEILLE_DU_DEPART = 'corbeille/2026-10-06_19-00-00';
const LUCIOLES = 'Soirée des Lucioles';
const CERFS = 'Soirée des Cerfs';
const DEMONSTRATIONS = LIVREES.map(({ cle, texte }) => ({
cle,
nom: CATALOGUE.find((entree) => entree.cle === cle).nom,
texte,
}));
const UTF8 = new TextEncoder();
// --- Le montage -------------------------------------------------------------
// Identifiants d'épreuve, numérotés dans l'ordre des tirages.
function creerIdentifiants(prefixe) {
let n = 0;
return () => {
n += 1;
return `${prefixe}-${n}`;
};
}
// La façade sur le système fs, branchée comme App la branche : changerRacine
// crée la séance sur la racine reçue. appels relève, dans l'ordre, ce que la
// façade demande à la séance, et ce que l'épreuve y ajoute.
function monter({ fs = creerFichiersSimules(), horloge = creerHorlogePilotee({ depart: DEPART }), demonstrations = DEMONSTRATIONS } = {}) {
const identifiant = creerIdentifiants('id');
const appels = [];
let seance = null;
let racine = null;
const facade = creerFacade({
fs,
horloge,
identifiant,
produitVersion: V1,
demonstrations,
ouvert: () => seance?.ouvert?.base ?? null,
ouvrir: async (base) => {
appels.push(['ouvrir', base]);
return seance.ouvrir(base);
},
fermer: async () => {
appels.push(['fermer']);
return seance.fermer();
},
creer: async (champs) => {
appels.push(['creer', champs]);
return seance.creer(champs);
},
racine: () => racine,
changerRacine: async (nouvelle) => {
appels.push(['changerRacine', nouvelle]);
racine = nouvelle;
seance = creerSeance({
fs,
racine: nouvelle,
horloge,
identifiant,
produitVersion: V1,
planifier: horloge.planifier,
annuler: horloge.annuler,
});
},
});
return {
fs,
horloge,
facade,
appels,
get seance() {
return seance;
},
};
}
// Un événement posé par le dépôt, hors de la façade : sa paire, journal
// d'une entrée et son jalon, puis l'état.
async function deposerEvenement(fs, racine, { id, nom, etat = null }) {
const charge = creerCharge({ id, nom, siegesParDefaut: 8, tours: 2 });
if (etat !== null) charge.evenement.etat = etat;
const ouvert = await creerDansLeDepot(fs, racine, charge, {
libelle: libelle('ENTREE_CREATION', { nom }),
nomJalon: libelle('JALON_CREATION'),
horodatage: '2031-05-17T08:00:00-04:00',
produitVersion: V1,
});
return ouvert.base;
}
// Le texte d'état d'un événement neuf, que l'épreuve dépose tel quel.
const texteEtat = ({ id, nom }) =>
serialiser(creerCharge({ id, nom, siegesParDefaut: 8, tours: 2 }), { revision: 1, produitVersion: V1 });
// Le refus qu'une promesse rejette.
async function refusDe(promesse) {
try {
await promesse;
} catch (erreur) {
return erreur;
}
assert.fail('la promesse devait rejeter');
}
// Les textes de chaque fichier d'une racine, par nom, sous le dossier donné.
async function photographier(fs, racine, dossier = '') {
const entrees = await fs.lister(racine, dossier);
const photo = {};
for (const { nom, type } of entrees) {
if (type === 'fichier') photo[nom] = fs.contenu(racine.id, dossier === '' ? nom : `${dossier}/${nom}`);
}
return photo;
}
const ecritures = (fs) => fs.compteurs.ecritures;
// Le nom en NFD que copier et renommer reçoivent, et sa forme NFC, distinctes.
const NFD = 'Soire\u{0065}\u{0301}e des Hiboux'.normalize('NFD');
const NFC = NFD.normalize('NFC');
// Des Documents sous un chemin de 250 caractères : aucun nom long n'y tient
// sous la borne d'un chemin, que lève deriverBase.
const PROFOND = `C:\\${'p'.repeat(247)}`;
// --- La détermination du dossier de travail (§ 8.6) --------------------------
describe('demarrer : une détermination par séance (§ 8.6)', () => {
test('le second demarrer ne lit ni n’écrit rien, et rend le même résultat', async () => {
const { fs, facade, appels } = monter();
const premier = await facade.demarrer();
const { ecritures: e, lectures: l } = fs.compteurs;
const second = await facade.demarrer();
assert.equal(second, premier);
assert.equal(fs.compteurs.ecritures, e);
assert.equal(fs.compteurs.lectures, l);
assert.deepEqual(premier.dossier.racine, DOC);
assert.deepEqual(appels, [['changerRacine', DOC]]);
});
test('les cinq issues de S8 rendent leurs avis, dans l’ordre d’Avis', async () => {
const cas = [
[{}, [{ code: 'NON_PUBLIE', details: {} }]],
[{ executable: SOUS_APPDATA }, [{ code: 'DONNEES_APPLICATIVES', details: {} }]],
[{ executable: PUBLIE, sonde: { portable: 'EACCES' } }, [{ code: 'SONDE_ECHOUEE', details: { cause: 'EACCES' } }]],
[{ nature: 'web' }, [{ code: 'PLATEFORME_WEB', details: {} }]],
[{ executable: PUBLIE, support: { portable: 'amovible' } }, [{ code: 'SUPPORT_AMOVIBLE', details: {} }]],
[
{ nature: 'web', support: { documents: 'amovible' } },
[
{ code: 'PLATEFORME_WEB', details: {} },
{ code: 'SUPPORT_AMOVIBLE', details: {} },
],
],
[{ executable: PUBLIE }, []],
];
assert.equal(cas.length, 7);
for (const [systeme, attendus] of cas) {
const { facade } = monter({ fs: creerFichiersSimules(systeme) });
const { avis } = await facade.demarrer();
assert.deepEqual(avis, attendus, JSON.stringify(systeme));
}
});
test('changerRacine reçoit la racine retenue : data/ en mode portable', async () => {
const { facade, appels } = monter({ fs: creerFichiersSimules({ executable: PUBLIE }) });
const { dossier } = await facade.demarrer();
assert.equal(dossier.mode, 'portable');
assert.deepEqual(appels, [['changerRacine', PORTABLE]]);
assert.deepEqual(facade.explorateur(), { disponible: true, raison: null });
});
test('des réglages locaux corrompus ajoutent REGLAGES_ILLISIBLES après les avis de S8', async () => {
const fs = creerFichiersSimules();
fs.deposer('documents', 'reglages_locaux.json', '{ pas du JSON');
const { facade } = monter({ fs });
const { avis } = await facade.demarrer();
assert.deepEqual(avis, [
{ code: 'NON_PUBLIE', details: {} },
{ code: 'REGLAGES_ILLISIBLES', details: { chemin: 'reglages_locaux.json', raison: 'JSON' } },
]);
});
test('une lecture refusée rejette demarrer sans rien retenir, et le suivant refait la détermination', async () => {
const fs = creerFichiersSimules();
const lister = fs.lister;
let pannes = 1;
fs.lister = async (...parametres) => {
if (pannes > 0) {
pannes -= 1;
throw new ErreurStockage('LECTURE', { chemin: '', dossier: DOCUMENTS, cause: 'EIO' });
}
return lister(...parametres);
};
const { facade, appels } = monter({ fs });
const refus = await refusDe(facade.demarrer());
assert.equal(refus.code, 'LECTURE');
assert.deepEqual(appels, []);
const { dossier } = await facade.demarrer();
assert.deepEqual(dossier.racine, DOC);
assert.deepEqual(appels, [['changerRacine', DOC]]);
});
});
// --- Les questions (§ 8.6) ---------------------------------------------------
describe('les questions du dossier de travail (§ 8.6)', () => {
// data/ porte deux événements, posés par un premier processus ; le second,
// monté sur le même disque, voit data/ refuser l'écriture.
async function dataQuiRefuse() {
const disque = new Map();
const avant = creerFichiersSimules({ disque, executable: PUBLIE });
avant.deposerDossier('portable');
await deposerEvenement(avant, PORTABLE, { id: 'evt-lucioles', nom: LUCIOLES });
await deposerEvenement(avant, PORTABLE, { id: 'evt-cerfs', nom: CERFS });
const fs = creerFichiersSimules({ disque, executable: PUBLIE, sonde: { portable: 'EACCES' } });
const sondes = [];
const sonder = fs.sonder;
fs.sonder = async (racine) => {
sondes.push(racine.id);
return sonder(racine);
};
return { fs, sondes };
}
test('PORTABLE_NON_INSCRIPTIBLE : question au compte de 2, liste refusée tant qu’elle attend', async () => {
const { fs } = await dataQuiRefuse();
const { facade, appels } = monter({ fs });
const { dossier, avis } = await facade.demarrer();
assert.deepEqual(dossier.question, {
code: 'PORTABLE_NON_INSCRIPTIBLE',
racine: PORTABLE,
evenements: 2,
cause: 'EACCES',
});
assert.deepEqual(avis, [{ code: 'SONDE_ECHOUEE', details: { cause: 'EACCES' } }]);
assert.deepEqual(appels, []);
const refus = await refusDe(facade.lister());
assert.equal(refus.code, 'QUESTION_EN_ATTENTE');
assert.deepEqual(refus.details, { question: 'PORTABLE_NON_INSCRIPTIBLE', remede: null });
assert.equal(typeof libelle(refus.code, refus.details), 'string');
});
test('« reessayer » refait la détermination : deux sondes de plus, la même question', async () => {
const { fs, sondes } = await dataQuiRefuse();
const { facade, appels } = monter({ fs });
await facade.demarrer();
assert.deepEqual(sondes, ['portable', 'documents']);
const avant = ecritures(fs);
const { dossier } = await facade.repondre('reessayer');
assert.deepEqual(sondes, ['portable', 'documents', 'portable', 'documents']);
// Les deux sondes, et rien d'autre.
assert.equal(ecritures(fs), avant + 2);
assert.equal(dossier.question.code, 'PORTABLE_NON_INSCRIPTIBLE');
assert.deepEqual(appels, []);
assert.equal((await refusDe(facade.lister())).code, 'QUESTION_EN_ATTENTE');
});
test('« documents » copie les deux paires dans les Documents, originaux intacts', async () => {
const { fs } = await dataQuiRefuse();
const { facade, appels } = monter({ fs });
await facade.demarrer();
const originaux = await photographier(fs, PORTABLE);
assert.equal(Object.keys(originaux).length, 4);
const avant = ecritures(fs);
const { dossier } = await facade.repondre('documents');
// Chaque paire : son journal, puis son état.
assert.equal(ecritures(fs), avant + 4);
assert.equal(dossier.question, null);
assert.equal(dossier.autre, null);
assert.deepEqual(dossier.racine, DOC);
assert.deepEqual(appels, [['changerRacine', DOC]]);
assert.deepEqual(await photographier(fs, PORTABLE), originaux);
const lignes = await facade.lister();
assert.equal(lignes.length, 2);
assert.deepEqual(
lignes.map(({ id, nom }) => [id, nom]),
[
['evt-cerfs', CERFS],
['evt-lucioles', LUCIOLES],
],
);
for (const base of [LUCIOLES, CERFS]) {
assert.equal(fs.contenu('documents', base + SUFFIXES.etat), originaux[base + SUFFIXES.etat]);
assert.equal(fs.contenu('documents', base + SUFFIXES.journal), originaux[base + SUFFIXES.journal]);
}
assert.equal((await facade.demarrer()).dossier, dossier);
});
test('un data/ qui ne se lit pas, compte null : « documents » n’a rien à copier, ni à relire', async () => {
const { fs } = await dataQuiRefuse();
const lister = fs.lister;
const listes = [];
fs.lister = async (racine, dossier) => {
listes.push(racine.id);
if (racine.id === 'portable') throw new ErreurStockage('LECTURE', { chemin: dossier, dossier: PORTABLE.chemin, cause: 'EIO' });
return lister(racine, dossier);
};
const { facade, appels } = monter({ fs });
const { dossier } = await facade.demarrer();
assert.deepEqual(dossier.question, {
code: 'PORTABLE_NON_INSCRIPTIBLE',
racine: PORTABLE,
evenements: null,
cause: 'EACCES',
});
const lus = listes.length;
const avant = ecritures(fs);
const reponse = await facade.repondre('documents');
assert.deepEqual(reponse.importe, { importes: [], refuses: [] });
assert.deepEqual(reponse.dossier.racine, DOC);
assert.deepEqual(appels, [['changerRacine', DOC]]);
assert.deepEqual(listes.slice(lus), []);
assert.equal(ecritures(fs), avant);
});
test('« continuer » ne répond pas à PORTABLE_NON_INSCRIPTIBLE : RangeError', async () => {
const { fs } = await dataQuiRefuse();
const { facade, appels } = monter({ fs });
await facade.demarrer();
await assert.rejects(facade.repondre('continuer'), RangeError);
await assert.rejects(facade.repondre('ignorer'), RangeError);
assert.deepEqual(appels, []);
});
test('DOCUMENTS_NON_INSCRIPTIBLE nomme le dossier, la cause et le compte ; « continuer » ouvre la liste sans écrire', async () => {
const disque = new Map();
const avant = creerFichiersSimules({ disque });
avant.deposerDossier('documents');
await deposerEvenement(avant, DOC, { id: 'evt-lucioles', nom: LUCIOLES });
const fs = creerFichiersSimules({ disque, sonde: { documents: 'EACCES' } });
const { facade, appels } = monter({ fs });
const { dossier } = await facade.demarrer();
assert.deepEqual(dossier.question, {
code: 'DOCUMENTS_NON_INSCRIPTIBLE',
racine: DOC,
evenements: 1,
cause: 'EACCES',
});
assert.equal((await refusDe(facade.lister())).code, 'QUESTION_EN_ATTENTE');
await assert.rejects(facade.repondre('documents'), RangeError);
assert.deepEqual(appels, []);
const avant2 = ecritures(fs);
const reponse = await facade.repondre('continuer');
assert.equal(reponse.dossier.question, null);
assert.deepEqual(appels, [['changerRacine', DOC]]);
const lignes = await facade.lister();
assert.deepEqual(
lignes.map(({ base }) => base),
[LUCIOLES],
);
assert.equal(ecritures(fs), avant2);
});
});
// --- La liste (§ 8.1, § 19.4) -----------------------------------------------
describe('lister (§ 8.1, § 19.4)', () => {
// Publié en E:\soirees, data/ inscriptible : la petite démonstration
// déposée sans journal, et un événement renommé à la main.
async function deuxEvenements() {
const fs = creerFichiersSimules({ executable: PUBLIE });
const montage = monter({ fs });
await montage.facade.demarrer();
fs.deposer('portable', `petite${SUFFIXES.etat}`, texteLivre('petite'));
const base = await deposerEvenement(fs, PORTABLE, { id: 'evt-gala', nom: 'Gala du printemps' });
await fs.deplacer(PORTABLE, base + SUFFIXES.etat, `soiree-a${SUFFIXES.etat}`);
await fs.deplacer(PORTABLE, base + SUFFIXES.journal, `soiree-a${SUFFIXES.journal}`);
return montage;
}
test('six informations par ligne, le nom et le compte lus dans le fichier, rien d’écrit', async () => {
const { fs, facade } = await deuxEvenements();
const avant = ecritures(fs);
const lignes = await facade.lister();
assert.equal(ecritures(fs), avant);
const petite = analyser(texteLivre('petite')).charge;
assert.deepEqual(lignes, [
{
base: 'soiree-a',
id: 'evt-gala',
nom: 'Gala du printemps',
date: null,
etat: 'brouillon',
participants: 0,
derniereModification: '2031-05-17T08:00:00-04:00',
chemin: `E:\\soirees\\data\\soiree-a${SUFFIXES.etat}`,
illisible: null,
journalSeul: false,
doublonAvec: [],
verrou: null,
},
{
base: 'petite',
id: petite.evenement.id,
nom: petite.evenement.nom,
date: petite.evenement.date,
etat: petite.evenement.etat,
participants: petite.participants.length,
derniereModification: null,
chemin: `E:\\soirees\\data\\petite${SUFFIXES.etat}`,
illisible: null,
journalSeul: false,
doublonAvec: [],
verrou: null,
},
]);
assert.ok(petite.participants.length > 0);
});
test('deux exécutions rendent la même liste ; la date des fichiers n’y change rien', async () => {
const { fs, facade } = await deuxEvenements();
const avant = ecritures(fs);
const premiere = await facade.lister();
assert.equal(premiere.length, 2);
assert.deepEqual(await facade.lister(), premiere);
fs.deposer('portable', `petite${SUFFIXES.etat}`, texteLivre('petite'), 1);
fs.deposer('portable', `soiree-a${SUFFIXES.journal}`, fs.contenu('portable', `soiree-a${SUFFIXES.journal}`), 2);
assert.deepEqual(await facade.lister(), premiere);
assert.equal(ecritures(fs), avant);
});
test('la dernière modification est l’horodatage de la dernière entrée du journal', async () => {
const { fs, horloge, facade } = monter();
await facade.demarrer();
await facade.creer({ nom: LUCIOLES });
horloge.avancer(3_600_000);
await facade.renommer(LUCIOLES, { nom: CERFS });
const journal = lireJournal(fs.contenu('documents', CERFS + SUFFIXES.journal));
assert.deepEqual(
journal.entrees.map(({ horodatage }) => horodatage),
[DEPART, '2026-10-06T20:00:00-04:00'],
);
const avant = ecritures(fs);
const [ligne] = await facade.lister();
assert.equal(ligne.derniereModification, '2026-10-06T20:00:00-04:00');
assert.equal(ecritures(fs), avant);
});
test('un .gtt.verrou présent se lit sans écrire ; illisible, sa séance et son instant sont null', async () => {
const { fs, facade } = await deuxEvenements();
const contenu = { seance: 'seance-voisine', pid: 7, hote: 'poste-voisin', depuis: '2031-05-17T09:30:00-04:00' };
fs.deposer('portable', `soiree-a${SUFFIXES.verrou}`, `${JSON.stringify(contenu)}\n`);
fs.deposer('portable', `petite${SUFFIXES.verrou}`, 'pas un verrou');
const avant = ecritures(fs);
const lignes = await facade.lister();
assert.equal(ecritures(fs), avant);
assert.deepEqual(
lignes.map(({ base, verrou }) => [base, verrou]),
[
['soiree-a', { seance: 'seance-voisine', depuis: '2031-05-17T09:30:00-04:00' }],
['petite', { seance: null, depuis: null }],
],
);
});
test('signalements : état vide, journal seul, doublons ; rien pour les autres fichiers', async () => {
const fs = creerFichiersSimules();
const { facade } = monter({ fs });
await facade.demarrer();
fs.deposer('documents', `Soirée vide${SUFFIXES.etat}`, '');
const hiboux = await deposerEvenement(fs, DOC, { id: 'evt-hiboux', nom: 'Soirée des Hiboux' });
await fs.supprimer(DOC, hiboux + SUFFIXES.etat);
const merles = texteEtat({ id: 'evt-merles', nom: 'Club des Merles' });
fs.deposer('documents', `Merles A${SUFFIXES.etat}`, merles);
fs.deposer('documents', `Merles B${SUFFIXES.etat}`, merles);
fs.deposer('documents', 'reglages_locaux.json', '{}');
fs.deposer('documents', `corbeille/2031-01-01_00-00-00/Ancienne${SUFFIXES.etat}`, merles);
fs.deposer('documents', 'planche.pdf', '%PDF');
const avant = ecritures(fs);
const lignes = await facade.lister();
assert.equal(ecritures(fs), avant);
assert.equal(lignes.length, 4);
const signalees = lignes.filter((l) => l.illisible !== null || l.journalSeul || l.doublonAvec.length > 0);
assert.equal(signalees.length, 4);
const par = Object.fromEntries(lignes.map((ligne) => [ligne.base, ligne]));
assert.deepEqual(Object.keys(par).sort(), ['Merles A', 'Merles B', 'Soirée des Hiboux', 'Soirée vide']);
assert.equal(par['Soirée vide'].illisible.raison, 'VIDE');
assert.deepEqual(par['Soirée vide'].illisible.secours, { precedent: null, journal: null });
assert.equal(par['Soirée vide'].journalSeul, false);
assert.equal(typeof libelle('ETAT_ILLISIBLE', par['Soirée vide'].illisible), 'string');
assert.equal(par['Soirée des Hiboux'].journalSeul, true);
assert.equal(par['Soirée des Hiboux'].id, 'evt-hiboux');
assert.deepEqual(par['Soirée des Hiboux'].illisible.secours, { precedent: null, journal: 1 });
assert.equal(par['Soirée des Hiboux'].derniereModification, '2031-05-17T08:00:00-04:00');
assert.deepEqual(par['Merles A'].doublonAvec, ['Merles B']);
assert.deepEqual(par['Merles B'].doublonAvec, ['Merles A']);
assert.equal(par['Merles A'].illisible, null);
assert.equal(par['Soirée vide'].doublonAvec.length, 0);
});
});
// --- Créer (§ 2.1, § 8.3) ----------------------------------------------------
describe('creer (§ 2.1, § 8.3)', () => {
test('« Soirée d’essai » : creer de la séance, une entrée, le jalon « Création », ouvert en lecture', async () => {
const montage = monter();
const { fs, facade, appels } = montage;
await facade.demarrer();
const avant = ecritures(fs);
const { base } = await facade.creer({ nom: "Soirée d'essai", date: null });
assert.equal(ecritures(fs), avant + 2);
assert.deepEqual(appels.at(-1), ['creer', { nom: "Soirée d'essai", date: null, siegesParDefaut: 8, tours: 4 }]);
const journal = lireJournal(fs.contenu('documents', base + SUFFIXES.journal));
assert.equal(journal.entrees.length, 1);
assert.deepEqual(
journal.jalons.map(({ nom }) => nom),
['Création'],
);
assert.equal(montage.seance.ouvert.base, base);
assert.equal(montage.seance.mode, 'lecture');
const { charge } = analyser(fs.contenu('documents', base + SUFFIXES.etat));
assert.equal(charge.evenement.siegesParDefaut, 8);
assert.equal(charge.evenement.tours, 4);
});
test('une coupure après la première écriture laisse le journal sans l’état : le journal d’abord', async () => {
const disque = new Map();
const fs = creerFichiersSimules({ disque });
const { facade } = monter({ fs });
await facade.demarrer();
fs.pannes.couperApres(1);
await assert.rejects(facade.creer({ nom: "Soirée d'essai" }));
const relu = creerFichiersSimules({ disque });
assert.notEqual(relu.contenu('documents', `Soirée d'essai${SUFFIXES.journal}`), null);
assert.equal(relu.contenu('documents', `Soirée d'essai${SUFFIXES.etat}`), null);
});
test('un nom en NFD est inscrit en NFC', async () => {
const { fs, facade } = monter();
await facade.demarrer();
const nfd = 'Soire\u{0065}\u{0301}e des Lucioles'.normalize('NFD');
assert.notEqual(nfd, nfd.normalize('NFC'));
const { base } = await facade.creer({ nom: nfd });
assert.equal(analyser(fs.contenu('documents', base + SUFFIXES.etat)).charge.evenement.nom, nfd.normalize('NFC'));
});
test('nom blanc, date hors du calendrier ou mal écrite : refus, rien d’écrit', async () => {
const { fs, facade } = monter();
await facade.demarrer();
const avant = ecritures(fs);
assert.equal((await refusDe(facade.creer({ nom: ' ' }))).code, 'NOM_REQUIS');
for (const date of ['2026-02-30', '2026-2-3']) {
const refus = await refusDe(facade.creer({ nom: LUCIOLES, date }));
assert.equal(refus.code, 'DATE_INVALIDE');
assert.equal(refus.details.date, date);
}
assert.equal(ecritures(fs), avant);
});
test('des Documents sous un chemin de 250 caractères : CHEMIN_TROP_LONG, rien d’écrit', async () => {
assert.equal(PROFOND.length, 250);
const { fs, facade } = monter({ fs: creerFichiersSimules({ documents: PROFOND }) });
await facade.demarrer();
const avant = ecritures(fs);
assert.equal((await refusDe(facade.creer({ nom: LUCIOLES }))).code, 'CHEMIN_TROP_LONG');
assert.equal(ecritures(fs), avant);
});
});
// --- Charger une démonstration (§ 15) ---------------------------------------
describe('chargerDemonstration (§ 15)', () => {
test('deux chargements : « ‹nom› 1 » puis « ‹nom› 2 », identités distinctes, ouverts en lecture', async () => {
const montage = monter();
const { fs, facade } = montage;
await facade.demarrer();
const { base: courant } = await facade.creer({ nom: LUCIOLES });
const courantAvant = await photographier(fs, DOC);
const courants = [courant + SUFFIXES.etat, courant + SUFFIXES.journal];
assert.deepEqual(Object.keys(courantAvant).sort(), [...courants].sort());
const livre = texteLivre('petite');
const nom = CATALOGUE.find((entree) => entree.cle === 'petite').nom;
const avant = ecritures(fs);
const premier = await facade.chargerDemonstration('petite');
// Le journal, puis l'état.
assert.equal(ecritures(fs), avant + 2);
assert.equal(montage.seance.ouvert.base, premier.base);
assert.equal(montage.seance.mode, 'lecture');
const second = await facade.chargerDemonstration('petite');
assert.equal(ecritures(fs), avant + 4);
assert.equal(montage.seance.ouvert.base, second.base);
assert.equal(montage.seance.mode, 'lecture');
const lire = (base) => analyser(fs.contenu('documents', base + SUFFIXES.etat)).charge.evenement;
assert.equal(lire(premier.base).nom, `${nom} 1`);
assert.equal(lire(second.base).nom, `${nom} 2`);
assert.notEqual(lire(premier.base).id, lire(second.base).id);
const journal = lireJournal(fs.contenu('documents', premier.base + SUFFIXES.journal));
assert.deepEqual(
journal.entrees.map(({ libelle: texte }) => texte),
[`Chargement de la démonstration « ${nom} »`],
);
assert.equal(texteLivre('petite'), livre);
const photo = await photographier(fs, DOC);
for (const fichier of courants) {
assert.equal(typeof courantAvant[fichier], 'string');
assert.equal(photo[fichier], courantAvant[fichier]);
}
});
test('une clé inconnue lève RangeError, sans rien écrire', async () => {
const { fs, facade } = monter();
await facade.demarrer();
const avant = ecritures(fs);
await assert.rejects(facade.chargerDemonstration('inconnue'), RangeError);
assert.equal(ecritures(fs), avant);
});
});
// --- Copier (§ 8.7) ----------------------------------------------------------
describe('copier (§ 8.7)', () => {
test('depuis un plan bloqué : nouvel identifiant, une entrée, non bloquée, original intact', async () => {
const montage = monter();
const { fs, facade } = montage;
await facade.demarrer();
await deposerEvenement(fs, DOC, { id: 'evt-lucioles', nom: LUCIOLES, etat: 'bloque' });
const original = await photographier(fs, DOC);
assert.equal(Object.keys(original).length, 2);
const avant = ecritures(fs);
const { base } = await facade.copier(LUCIOLES, { nom: ' Variante des Lucioles ' });
// Le journal de la copie, puis son état.
assert.equal(ecritures(fs), avant + 2);
assert.equal(base, 'Variante des Lucioles');
assert.equal(montage.seance.ouvert.base, base);
assert.equal(montage.seance.mode, 'lecture');
const copie = analyser(fs.contenu('documents', base + SUFFIXES.etat)).charge.evenement;
assert.notEqual(copie.id, 'evt-lucioles');
assert.notEqual(copie.etat, 'bloque');
assert.equal(copie.nom, 'Variante des Lucioles');
const creation = libelle('ENTREE_CREATION', { nom: LUCIOLES });
const journal = lireJournal(fs.contenu('documents', base + SUFFIXES.journal));
assert.deepEqual(
journal.entrees.map(({ libelle: texte }) => texte),
[`Créée par copie de « ${LUCIOLES} », à l'instant « ${creation} »`],
);
// L'entrée nomme l'instant que la filiation inscrit.
assert.equal(copie.filiation.instant.libelle, creation);
const apres = await photographier(fs, DOC);
for (const [fichier, texte] of Object.entries(original)) assert.equal(apres[fichier], texte);
});
test('un nom vide est refusé, sans écrire, et aucun « (copie) » ne paraît', async () => {
const { fs, facade } = monter();
await facade.demarrer();
await deposerEvenement(fs, DOC, { id: 'evt-lucioles', nom: LUCIOLES });
const avant = ecritures(fs);
for (const nom of ['', ' \t ']) {
const refus = await refusDe(facade.copier(LUCIOLES, { nom }));
assert.equal(refus.code, 'NOM_REQUIS');
assert.equal(refus.details.remede, null);
}
assert.equal(ecritures(fs), avant);
const lignes = await facade.lister();
assert.equal(lignes.length, 1);
assert.ok(lignes.every(({ nom }) => !nom.includes('(copie)')));
});
test('un nom en NFD : la copie porte le nom et la base en NFC', async () => {
const { fs, facade } = monter();
await facade.demarrer();
await deposerEvenement(fs, DOC, { id: 'evt-lucioles', nom: LUCIOLES });
assert.notEqual(NFD, NFC);
const { base } = await facade.copier(LUCIOLES, { nom: NFD });
assert.equal(base, NFC);
assert.equal(analyser(fs.contenu('documents', NFC + SUFFIXES.etat)).charge.evenement.nom, NFC);
assert.equal(fs.contenu('documents', NFD + SUFFIXES.etat), null);
});
test('des Documents trop profonds pour le nom : CHEMIN_TROP_LONG, rien d’écrit', async () => {
const { fs, facade } = monter({ fs: creerFichiersSimules({ documents: PROFOND }) });
await facade.demarrer();
fs.deposer('documents', `Court${SUFFIXES.etat}`, texteEtat({ id: 'evt-court', nom: 'Court' }));
const avant = ecritures(fs);
const refus = await refusDe(facade.copier('Court', { nom: 'x'.repeat(120) }));
assert.equal(refus.code, 'CHEMIN_TROP_LONG');
assert.equal(ecritures(fs), avant);
assert.deepEqual(Object.keys(await photographier(fs, { id: 'documents', chemin: PROFOND })), [`Court${SUFFIXES.etat}`]);
});
});
// --- Renommer (§ 8.7, § 8.8) -------------------------------------------------
describe('renommer (§ 8.7, § 8.8)', () => {
test('nouveau nom, nouveau chemin, une entrée du renommage, aucun verrou restant', async () => {
const { fs, facade, appels } = monter();
await facade.demarrer();
await deposerEvenement(fs, DOC, { id: 'evt-lucioles', nom: LUCIOLES });
const avant = ecritures(fs);
const { base } = await facade.renommer(LUCIOLES, { nom: CERFS });
// La sonde qui prend le verrou ; la nouvelle paire — .precedent, journal,
// état — ; l'effacement de l'ancienne — état, .precedent, journal — ; le
// verrou rendu.
assert.equal(ecritures(fs), avant + 1 + 3 + 3 + 1);
assert.equal(base, CERFS);
const lignes = await facade.lister();
assert.deepEqual(
lignes.map(({ base: b, nom, chemin }) => [b, nom, chemin]),
[[CERFS, CERFS, `${DOCUMENTS}\\${CERFS}${SUFFIXES.etat}`]],
);
assert.equal(fs.contenu('documents', LUCIOLES + SUFFIXES.verrou), null);
assert.equal(fs.contenu('documents', CERFS + SUFFIXES.verrou), null);
const journal = lireJournal(fs.contenu('documents', CERFS + SUFFIXES.journal));
assert.equal(journal.entrees.at(-1).libelle, `Renommé de « ${LUCIOLES} » en « ${CERFS} »`);
assert.deepEqual(
appels.filter(([nom]) => nom !== 'changerRacine'),
[],
);
});
// Coupe le processus dès que l'écriture atomique a posé ce fichier.
function couperApresEcriture(fs, chemin) {
const ecrire = fs.ecrireAtomique;
fs.ecrireAtomique = async (racine, cible, texte) => {
await ecrire(racine, cible, texte);
if (cible === chemin) fs.pannes.couperApres(0);
};
}
test('une coupure après le nouvel état laisse deux paires, signalées doublons l’une de l’autre', async () => {
const disque = new Map();
const fs = creerFichiersSimules({ disque });
const { facade } = monter({ fs });
await facade.demarrer();
await deposerEvenement(fs, DOC, { id: 'evt-lucioles', nom: LUCIOLES });
couperApresEcriture(fs, CERFS + SUFFIXES.etat);
await assert.rejects(facade.renommer(LUCIOLES, { nom: CERFS }));
const relu = monter({ fs: creerFichiersSimules({ disque }) });
await relu.facade.demarrer();
const lignes = await relu.facade.lister();
assert.deepEqual(
lignes.map(({ base, doublonAvec }) => [base, doublonAvec]),
[
[CERFS, [LUCIOLES]],
[LUCIOLES, [CERFS]],
],
);
});
test('une coupure après le seul nouveau journal laisse une ligne, sous l’ancienne base', async () => {
const disque = new Map();
const fs = creerFichiersSimules({ disque });
const { facade } = monter({ fs });
await facade.demarrer();
await deposerEvenement(fs, DOC, { id: 'evt-lucioles', nom: LUCIOLES });
couperApresEcriture(fs, CERFS + SUFFIXES.journal);
await assert.rejects(facade.renommer(LUCIOLES, { nom: CERFS }));
const relu = monter({ fs: creerFichiersSimules({ disque }) });
await relu.facade.demarrer();
const lignes = await relu.facade.lister();
assert.deepEqual(
lignes.map(({ base, nom }) => [base, nom]),
[[LUCIOLES, LUCIOLES]],
);
});
test('plan bloqué : LECTURE_SEULE {raison: BLOQUE}, remède « débloquer », rien d’écrit', async () => {
const { fs, facade } = monter();
await facade.demarrer();
await deposerEvenement(fs, DOC, { id: 'evt-lucioles', nom: LUCIOLES, etat: 'bloque' });
const avant = ecritures(fs);
const refus = await refusDe(facade.renommer(LUCIOLES, { nom: CERFS }));
assert.equal(refus.code, 'LECTURE_SEULE');
assert.deepEqual(refus.details, { raison: 'BLOQUE', remede: { geste: 'debloquer' } });
assert.equal(ecritures(fs), avant);
});
test('un nom vide : NOM_REQUIS avant toute sonde, rien d’écrit', async () => {
const { fs, facade } = monter();
await facade.demarrer();
await deposerEvenement(fs, DOC, { id: 'evt-lucioles', nom: LUCIOLES });
const avant = ecritures(fs);
assert.equal((await refusDe(facade.renommer(LUCIOLES, { nom: ' ' }))).code, 'NOM_REQUIS');
assert.equal(ecritures(fs), avant);
});
test('un nom en NFD : le nom et la base renommés sont en NFC', async () => {
const { fs, facade } = monter();
await facade.demarrer();
await deposerEvenement(fs, DOC, { id: 'evt-lucioles', nom: LUCIOLES });
assert.notEqual(NFD, NFC);
const { base } = await facade.renommer(LUCIOLES, { nom: NFD });
assert.equal(base, NFC);
assert.equal(analyser(fs.contenu('documents', NFC + SUFFIXES.etat)).charge.evenement.nom, NFC);
assert.equal(fs.contenu('documents', NFD + SUFFIXES.etat), null);
});
test('CHEMIN_TROP_LONG, levé sous le verrou : le verrou se rend, le disque reste tel qu’il était', async () => {
const { fs, facade } = monter({ fs: creerFichiersSimules({ documents: PROFOND }) });
await facade.demarrer();
const profond = { id: 'documents', chemin: PROFOND };
fs.deposer('documents', `Court${SUFFIXES.etat}`, texteEtat({ id: 'evt-court', nom: 'Court' }));
const photo = await photographier(fs, profond);
assert.deepEqual(Object.keys(photo), [`Court${SUFFIXES.etat}`]);
const avant = ecritures(fs);
const refus = await refusDe(facade.renommer('Court', { nom: 'x'.repeat(120) }));
assert.equal(refus.code, 'CHEMIN_TROP_LONG');
// La sonde qui prend le verrou, puis le verrou rendu.
assert.equal(ecritures(fs), avant + 2);
assert.equal(fs.contenu('documents', `Court${SUFFIXES.verrou}`), null);
assert.deepEqual(await photographier(fs, profond), photo);
});
test('l’événement ouvert en écriture : fermé d’abord, aucun VERROU_PRIS, rouvert sous la nouvelle base', async () => {
const montage = monter();
const { facade, appels } = montage;
await facade.demarrer();
await facade.creer({ nom: LUCIOLES });
await montage.seance.passerEnEcriture();
assert.equal(montage.seance.mode, 'ecriture');
const debut = appels.length;
const { base } = await facade.renommer(LUCIOLES, { nom: CERFS });
assert.deepEqual(appels.slice(debut), [['fermer'], ['ouvrir', CERFS]]);
assert.equal(base, CERFS);
assert.equal(montage.seance.ouvert.base, CERFS);
assert.equal(montage.seance.mode, 'lecture');
assert.equal(montage.fs.contenu('documents', LUCIOLES + SUFFIXES.verrou), null);
});
test('le verrou d’une séance vivante : VERROU_PRIS, rien ne bouge ; celui d’une séance morte s’efface', async () => {
const disque = new Map();
const fs = creerFichiersSimules({ disque });
const { facade } = monter({ fs });
await facade.demarrer();
await deposerEvenement(fs, DOC, { id: 'evt-lucioles', nom: LUCIOLES });
const voisin = creerFichiersSimules({ disque });
await voisin.verrouiller(DOC, LUCIOLES + SUFFIXES.verrou, 'seance-voisine');
const avant = await photographier(fs, DOC);
assert.equal(Object.keys(avant).length, 3);
const ecrituresAvant = ecritures(fs);
const refus = await refusDe(facade.renommer(LUCIOLES, { nom: CERFS }));
assert.equal(refus.code, 'VERROU_PRIS');
assert.equal(refus.details.seance, 'seance-voisine');
assert.equal(refus.details.vivant, true);
// La seule sonde, refusée.
assert.equal(ecritures(fs), ecrituresAvant + 1);
assert.deepEqual(await photographier(fs, DOC), avant);
voisin.pannes.couperApres(0);
const avantReprise = ecritures(fs);
const { base } = await facade.renommer(LUCIOLES, { nom: CERFS });
// La sonde refusée, le verrou mort effacé, la sonde qui le prend ; puis
// le renommage : nouvelle paire, ancienne effacée, verrou rendu.
assert.equal(ecritures(fs), avantReprise + 3 + 3 + 3 + 1);
assert.equal(base, CERFS);
assert.equal(fs.contenu('documents', LUCIOLES + SUFFIXES.verrou), null);
});
});
// --- Supprimer (§ 8.7, § 8.8) ------------------------------------------------
describe('supprimer (§ 8.7, § 8.8)', () => {
test('les fichiers partent dans la corbeille datée par l’horloge, l’ouvert fermé d’abord', async () => {
const { fs, facade, appels } = monter();
await facade.demarrer();
await facade.creer({ nom: LUCIOLES });
const deplacer = fs.deplacer;
fs.deplacer = async (...parametres) => {
appels.push(['deplacer', parametres[1]]);
return deplacer(...parametres);
};
const avant = ecritures(fs);
const { dossier } = await facade.supprimer(LUCIOLES);
// La sonde qui prend le verrou, le dossier daté, puis trois déplacements :
// le verrou, le journal, l'état.
assert.equal(ecritures(fs), avant + 1 + 1 + 3);
assert.equal(dossier, CORBEILLE_DU_DEPART);
const fermer = appels.findIndex(([nom]) => nom === 'fermer');
const premierDeplacement = appels.findIndex(([nom]) => nom === 'deplacer');
assert.ok(fermer >= 0 && premierDeplacement > fermer);
assert.deepEqual(await facade.lister(), []);
assert.deepEqual(await facade.corbeille(), [
{
dossier: CORBEILLE_DU_DEPART,
chemin: `${DOCUMENTS}\\corbeille\\2026-10-06_19-00-00`,
fichiers: [LUCIOLES + SUFFIXES.journal, LUCIOLES + SUFFIXES.etat, LUCIOLES + SUFFIXES.verrou],
},
]);
});
test('le verrou d’une séance morte ne retient rien : il part avec l’événement', async () => {
const disque = new Map();
const fs = creerFichiersSimules({ disque });
const { facade } = monter({ fs });
await facade.demarrer();
await deposerEvenement(fs, DOC, { id: 'evt-lucioles', nom: LUCIOLES });
const mort = creerFichiersSimules({ disque });
await mort.verrouiller(DOC, LUCIOLES + SUFFIXES.verrou, 'seance-morte');
mort.pannes.couperApres(0);
const avant = ecritures(fs);
await facade.supprimer(LUCIOLES);
// La sonde, que le verrou mort n'arrête pas, le dossier daté, trois
// déplacements.
assert.equal(ecritures(fs), avant + 1 + 1 + 3);
const [{ fichiers }] = await facade.corbeille();
assert.ok(fichiers.includes(LUCIOLES + SUFFIXES.verrou));
assert.deepEqual(await facade.lister(), []);
});
test('un plan bloqué se supprime : rien ne se détruit, ses fichiers passent dans la corbeille', async () => {
const { fs, facade } = monter();
await facade.demarrer();
await deposerEvenement(fs, DOC, { id: 'evt-lucioles', nom: LUCIOLES, etat: 'bloque' });
const original = await photographier(fs, DOC);
assert.equal(Object.keys(original).length, 2);
const avant = ecritures(fs);
const { dossier } = await facade.supprimer(LUCIOLES);
assert.equal(ecritures(fs), avant + 1 + 1 + 3);
assert.equal(dossier, CORBEILLE_DU_DEPART);
assert.deepEqual(await facade.lister(), []);
const [{ fichiers }] = await facade.corbeille();
assert.deepEqual(fichiers, [LUCIOLES + SUFFIXES.journal, LUCIOLES + SUFFIXES.etat, LUCIOLES + SUFFIXES.verrou]);
for (const [fichier, texte] of Object.entries(original)) {
assert.equal(fs.contenu('documents', `${CORBEILLE_DU_DEPART}/${fichier}`), texte);
}
});
test('le verrou d’une séance vivante, puis d’un autre hôte : VERROU_PRIS, fichiers en place', async () => {
const cas = [
[{}, true],
[{ hote: 'poste-voisin' }, null],
];
assert.equal(cas.length, 2);
for (const [reglages, vivant] of cas) {
const disque = new Map();
const fs = creerFichiersSimules({ disque });
const { facade } = monter({ fs });
await facade.demarrer();
await deposerEvenement(fs, DOC, { id: 'evt-lucioles', nom: LUCIOLES });
const autre = creerFichiersSimules({ disque, ...reglages });
await autre.verrouiller(DOC, LUCIOLES + SUFFIXES.verrou, 'seance-autre');
const avant = await photographier(fs, DOC);
assert.equal(Object.keys(avant).length, 3);
const ecrituresAvant = ecritures(fs);
const refus = await refusDe(facade.supprimer(LUCIOLES));
assert.equal(refus.code, 'VERROU_PRIS');
assert.equal(refus.details.vivant, vivant);
// La seule sonde, refusée.
assert.equal(ecritures(fs), ecrituresAvant + 1);
assert.deepEqual(await photographier(fs, DOC), avant);
assert.deepEqual(await facade.corbeille(), []);
}
});
});
// --- Importer (§ 8.6, § 8.7) -------------------------------------------------
describe('importer (§ 8.6, § 8.7)', () => {
const MERLES = { id: 'evt-merles', nom: 'Club des Merles' };
test('un fichier choisi : ses octets sous une base dérivée de son nom ; une seconde fois, IDENTIFIANT_PRESENT', async () => {
const { fs, facade } = monter();
await facade.demarrer();
const octets = UTF8.encode(texteEtat(MERLES));
fs.choisirFichierAImporter = async () => ({ nom: 'recu-par-courriel.gtt.json', octets });
const avant = ecritures(fs);
const { base } = await facade.importerFichier();
// L'état seul, aucun verrou.
assert.equal(ecritures(fs), avant + 1);
assert.equal(base, MERLES.nom);
assert.deepEqual(fs.octets('documents', base + SUFFIXES.etat), octets);
const photo = await photographier(fs, DOC);
assert.equal(Object.keys(photo).length, 1);
const avantRefus = ecritures(fs);
const refus = await refusDe(facade.importerFichier());
assert.equal(refus.code, 'IDENTIFIANT_PRESENT');
assert.deepEqual(refus.details, { id: MERLES.id, base: MERLES.nom });
assert.equal(ecritures(fs), avantRefus);
assert.deepEqual(await photographier(fs, DOC), photo);
});
test('un fichier d’un format plus récent s’importe tel quel', async () => {
const { fs, facade } = monter();
await facade.demarrer();
const texte = texteEtat(MERLES).replace('"format":1', '"format":2');
assert.ok(texte.includes('"format":2'));
const octets = UTF8.encode(texte);
fs.choisirFichierAImporter = async () => ({ nom: 'futur.gtt.json', octets });
const avant = ecritures(fs);
const { base } = await facade.importerFichier();
assert.equal(ecritures(fs), avant + 1);
assert.deepEqual(fs.octets('documents', base + SUFFIXES.etat), octets);
});
test('le dialogue annulé rend null, sans rien écrire', async () => {
const { fs, facade } = monter();
await facade.demarrer();
const avant = ecritures(fs);
assert.equal(await facade.importerFichier(), null);
assert.equal(ecritures(fs), avant);
});
test('l’autre emplacement : Documents neufs, data/ en porte deux → copiés, originaux intacts', async () => {
const disque = new Map();
const avant = creerFichiersSimules({ disque, executable: SOUS_APPDATA });
avant.deposerDossier('portable');
await deposerEvenement(avant, PORTABLE_APPDATA, { id: 'evt-lucioles', nom: LUCIOLES });
await deposerEvenement(avant, PORTABLE_APPDATA, { id: 'evt-cerfs', nom: CERFS });
const fs = creerFichiersSimules({ disque, executable: SOUS_APPDATA });
const { facade } = monter({ fs });
const { dossier } = await facade.demarrer();
assert.deepEqual(dossier.autre, { racine: PORTABLE_APPDATA, evenements: 2 });
const originaux = await photographier(fs, PORTABLE_APPDATA);
assert.equal(Object.keys(originaux).length, 4);
const ecrituresAvant = ecritures(fs);
const { importes, refuses } = await facade.importerDepuisAutre();
// Chaque paire : son journal, puis son état.
assert.equal(ecritures(fs), ecrituresAvant + 4);
assert.equal(importes.length, 2);
assert.deepEqual(refuses, []);
assert.deepEqual(await photographier(fs, PORTABLE_APPDATA), originaux);
assert.deepEqual(
(await facade.lister()).map(({ id }) => id),
['evt-cerfs', 'evt-lucioles'],
);
assert.equal((await facade.demarrer()).dossier.autre, null);
});
});
// --- Les dialogues de fichiers, relayés (contrat de données) -----------------
describe('les dialogues de fichiers, lus sur fs à l’appel', () => {
test('choisirCsv rend tels quels {nom, octets} et null', async () => {
const { fs, facade } = monter();
assert.equal(await facade.choisirCsv(), null);
const choisi = { nom: 'liste.csv', octets: UTF8.encode('nom\nOmbrelle\n') };
fs.choisirFichierAImporter = async () => choisi;
assert.equal(await facade.choisirCsv(), choisi);
});
test('enregistrerSous remet nom et octets tels quels, et rend le chemin ou null', async () => {
const { fs, facade } = monter();
assert.equal(await facade.enregistrerSous('a.csv', new Uint8Array([1])), null);
const recus = [];
fs.enregistrerSous = async (nom, octets) => {
recus.push([nom, octets]);
return 'C:\\Users\\Exemple\\Documents\\liste-refus.csv';
};
const octets = UTF8.encode('motif\n');
assert.equal(await facade.enregistrerSous('liste-refus.csv', octets), 'C:\\Users\\Exemple\\Documents\\liste-refus.csv');
assert.equal(recus.length, 1);
assert.equal(recus[0][0], 'liste-refus.csv');
assert.equal(recus[0][1], octets);
});
});
// --- L'explorateur (§ 8.6) ---------------------------------------------------
describe('explorateur (§ 8.6)', () => {
test('sous web : indisponible, EXPLORATEUR_ABSENT, aucun appel', async () => {
const { fs, facade } = monter({ fs: creerFichiersSimules({ nature: 'web' }) });
await facade.demarrer();
const recus = [];
fs.ouvrirDansExplorateur = async (racine) => recus.push(racine);
assert.deepEqual(facade.explorateur(), { disponible: false, raison: 'EXPLORATEUR_ABSENT' });
assert.equal(typeof libelle('EXPLORATEUR_ABSENT'), 'string');
const refus = await refusDe(facade.ouvrirExplorateur());
assert.equal(refus.code, 'EXPLORATEUR_ABSENT');
assert.deepEqual(recus, []);
});
test('ailleurs : ouvrirDansExplorateur reçoit la racine', async () => {
const { fs, facade } = monter();
await facade.demarrer();
const recus = [];
fs.ouvrirDansExplorateur = async (racine) => recus.push(racine);
assert.deepEqual(facade.explorateur(), { disponible: true, raison: null });
await facade.ouvrirExplorateur();
assert.deepEqual(recus, [DOC]);
});
});