gestion_table_tournante_libre/src/application/evenements.test.js
Mathieu Benoit abae4fa9b8 [ADD] application: observable session, events facade, places and seats
The screens of iteration 3 need state they can subscribe to and commands
that keep the plan sound. The session follows the Svelte store contract,
unblocks a plan, cancels a gesture, keeps the framing, and orders the
warnings of a gesture. The facade lists events, settles the working folder
and runs copy, rename, import and delete. Places cover reservations, titles,
designation and the retained plan: each gesture reads seats as the display
shows them, reserved people keep their table, and a person removed frees
her chair while every other seat stays.
Checked: 1973 node, 127 browser, 60 node-long tests; coverage floors kept.

--- FR ---

[ADD] application : séance observable, façade des événements, places

Les écrans de l'itération 3 exigent un état auquel s'abonner et des
commandes qui gardent le plan sain. La séance suit le contrat des magasins
de Svelte, débloque un plan, annule un geste, garde le cadrage et ordonne
les avertissements d'un geste. La façade liste les événements, règle le
dossier de travail et mène copie, renommage, import et suppression. Les
places couvrent réservations, titres, désignation et plan retenu : chaque
geste lit les sièges comme l'écran les montre, une personne réservée garde
sa table, et une personne retirée libère sa chaise sans déplacer les autres.
Vérifié : 1973 node, 127 navigateur, 60 node-long ; seuils de couverture.

Assisted-by: Claude Opus 5.5
2026-10-07 04:14:24 -04:00

1115 lines
48 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)
// É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]);
});
});