// © 2026 TechnoLibre (http://www.technolibre.ca) // License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) // Épreuves des régions engendrées de la documentation (§ 19.8, § 19.7) : la // lecture des marqueurs et ses refus, le remplacement qui ne touche rien hors // des régions, la vérification qui nomme la région corrigée à la main, les // sept producteurs lus sur leur source unique, l'arbre des fichiers porteurs // et l'écart de version, non bloquant au cycle court et bloquant à la // livraison. L'arbre de test/fixtures/documentation/ porte un document aux // régions conformes et un manifeste inventé, sans version réelle : chaque // épreuve qui en a besoin l'écrit dans une copie temporaire. import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, test } from '../test/lanceur.js'; import { versionVoisine } from '../test/version_voisine.js'; import { creerFormat } from '../src/application/format.js'; import { COMMANDES } from '../src/application/commandes.js'; import { TABLES_APPLICATION, libelle } from '../src/application/libelles.js'; import { creerTraduction } from '../src/application/traduction.js'; import { ENTETES_DES_CHAMPS } from '../src/csv/colonnes.js'; import { LIVREES } from '../src/demo/livrees.js'; import { CATALOGUE } from '../src/demo/catalogue.js'; import { CONSTANTES } from '../src/geometrie/constantes.js'; import { REGLAGES_GENERATION } from '../src/interface/generation/reglages.js'; import { TABLES_INTERFACE } from '../src/interface/libelles.js'; import { RACCOURCIS } from '../src/interface/raccourcis.js'; import { diagnostiquer } from '../src/moteur/diagnostic.js'; import { analyser, configurationDepuisCharge } from '../src/stockage/document.js'; import { VERSION } from '../src/version.genere.js'; import { ErreurRegions, EXEMPLES_COMMANDES, PORTEURS, PRODUCTEURS, engendrerArbre, executer, lireRegions, producteursDe, remplacerRegions, tableau, tableauColonnesCsv, tableauCommandes, tableauContrastes, tableauDemonstrations, tableauRaccourcis, tableauReglages, tableauRegistre, verifierArbre, verifierRegions, } from './documentation.js'; const FIXTURE = fileURLToPath(new URL('../test/fixtures/documentation/', import.meta.url)); const SCRIPT = fileURLToPath(new URL('./documentation.js', import.meta.url)); const MANIFESTE = 'doc/captures/manifeste.json'; const { t } = creerTraduction([...TABLES_APPLICATION, ...TABLES_INTERFACE]); const { nombre } = creerFormat(t); // Producteurs inventés, comptés : chaque appel s'inscrit dans appels. function producteursComptes(textes) { const appels = []; const producteurs = new Map( Object.entries(textes).map(([nom, texte]) => [ nom, () => { appels.push(nom); return texte; }, ]), ); return { producteurs, appels }; } const debut = (nom) => ``; const fin = (nom) => ``; const region = (nom, contenu) => `${debut(nom)}${contenu}${fin(nom)}`; // Les lignes d'un tableau Markdown, sans en-tête ni séparateur : chacune la // liste de ses cellules, une barre précédée d'une barre oblique inverse // restant dans sa cellule. function lignesDuTableau(texte) { return texte .split('\n') .slice(2) .map((ligne) => ligne .slice(1, -1) .split(/(? cellule.trim()), ); } // Le texte privé du contenu de ses régions : ce qui reste hors des marqueurs, // marqueurs compris. function horsRegions(texte) { let reste = ''; let curseur = 0; for (const { debut: d, fin: f } of lireRegions(texte)) { reste += texte.slice(curseur, d); curseur = f; } return reste + texte.slice(curseur); } // Une copie temporaire de l'arbre de fixture, dont chaque capture porte la // version donnée (aucune version n'est écrite en clair, § 18.5, point 6) ; // rendue avec la fonction qui l'efface. function copieDeLArbre(version = VERSION.affichee) { const racine = mkdtempSync(join(tmpdir(), 'gtt-documentation-')); cpSync(FIXTURE, racine, { recursive: true }); const chemin = join(racine, MANIFESTE); const manifeste = JSON.parse(readFileSync(chemin, 'utf8')); for (const capture of manifeste.captures) capture.versionReelle = version; writeFileSync(chemin, `${JSON.stringify(manifeste, null, 2)}\n`); return { racine, effacer: () => rmSync(racine, { recursive: true, force: true }) }; } function manifesteFixture() { return JSON.parse(readFileSync(join(FIXTURE, MANIFESTE), 'utf8')); } // Une sortie qui retient ses lignes. function sortieRetenue() { const lignes = []; return { lignes, ecrire: (ligne) => lignes.push(ligne) }; } describe('documentation : lire les régions (§ 19.8)', () => { test('deux régions, dans leur ordre, chacune son nom, ses bornes et son contenu', () => { const texte = `Avant\n${region('reglages', '\nA\n')}\nEntre\n${region('raccourcis', '')}\nAprès\n`; const regions = lireRegions(texte); assert.deepEqual( regions.map(({ nom, contenu }) => ({ nom, contenu })), [ { nom: 'reglages', contenu: '\nA\n' }, { nom: 'raccourcis', contenu: '' }, ], ); for (const { debut: d, fin: f, contenu } of regions) assert.equal(texte.slice(d, f), contenu); assert.deepEqual(lireRegions('Aucune région ici.\n\n'), []); }); test('un marqueur sans fin : échec qui nomme la région', () => { assert.throws( () => lireRegions(`${debut('reglages')}\ncontenu\n`), (erreur) => { assert.ok(erreur instanceof ErreurRegions); assert.deepEqual(erreur.fautes.map((f) => f.region), ['reglages']); assert.match(erreur.fautes[0].faute, /sans fin/); return true; }, ); }); test('une fin sans début, une région ouverte dans une autre, une fin qui en ferme une autre : échecs nommés', () => { const cas = [ [`texte\n${fin('commandes')}\n`, 'commandes', /sans début/], [`${debut('reglages')}\n${region('commandes', '')}\n${fin('reglages')}`, 'commandes', /ouverte dans la région « reglages »/], [`${debut('reglages')}\n${fin('commandes')}\n`, 'commandes', /ferme la région « reglages »/], ]; for (const [texte, nom, motif] of cas) { assert.throws( () => lireRegions(texte), (erreur) => { assert.ok(erreur instanceof ErreurRegions, texte); const faute = erreur.fautes.find((f) => f.region === nom); assert.ok(faute !== undefined, `${texte} : ${JSON.stringify(erreur.fautes)}`); assert.match(faute.faute, motif); return true; }, ); } }); test('une région en double : échec qui la nomme', () => { assert.throws( () => lireRegions(`${region('raccourcis', '\n')}\n${region('raccourcis', '\n')}\n`), (erreur) => { assert.ok(erreur instanceof ErreurRegions); assert.deepEqual(erreur.fautes.map((f) => [f.region, /en double/.test(f.faute)]), [['raccourcis', true]]); return true; }, ); }); test('un marqueur illisible — sans nom, nom en capitales, mot inconnu, commentaire jamais fermé — est un échec, jamais un texte ordinaire', () => { for (const marqueur of [ '', '', '', '