The generated documentation must list exactly what the screen shows. The generation settings, the keyboard shortcuts and the CSV columns each live in one table that both the interface and the documentation read; the target field of the proposals section reads its entry too. A guard reports any key named in the interface without its shortcut entry. The LISTE_SUR_VOISINE warning now shows its remedy, moving the covered neighbour. Checked: 3116 node and 643 browser tests green from the index alone. --- FR --- [ADD] interface : sources uniques des réglages, raccourcis, colonnes CSV La documentation engendrée doit lister exactement ce que l'écran montre. Les réglages de la génération, les raccourcis clavier et les colonnes du CSV vivent chacun dans une table que lisent l'interface et la documentation ; le champ de la cible des propositions lit aussi la sienne. Une garde relève toute touche nommée dans l'interface sans son entrée de raccourci. L'avertissement LISTE_SUR_VOISINE montre désormais son remède, le déplacement de la voisine couverte. Vérifié : 3116 node et 643 navigateur verts depuis l'index seul. Assisted-by: Claude Opus 5.5
182 lines
9 KiB
JavaScript
182 lines
9 KiB
JavaScript
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||
|
||
// Épreuves de la définition des réglages de la charge (§ 5.3, § 5.7,
|
||
// § 5.10, § 8.9, § 19.8) : sa forme, figée ; l'ordre des écrans ; les
|
||
// défauts, ceux d'une charge neuve, GENERATION_PAR_DEFAUT et
|
||
// CIBLE_REDONDANCE_PAR_DEFAUT compris ; chaque réglage du schéma du fichier,
|
||
// facultatif compris, et lui seul ; les clés que les commandes acceptent et
|
||
// les libellés qui existent ; la lecture d'une valeur dans une charge.
|
||
import assert from 'node:assert/strict';
|
||
import { describe, test } from '../../../test/lanceur.js';
|
||
import { TABLES_APPLICATION } from '../../application/libelles.js';
|
||
import { appliquerCommande } from '../../application/commandes.js';
|
||
import { creerTraduction } from '../../application/traduction.js';
|
||
import { CIBLE_REDONDANCE_PAR_DEFAUT, GENERATION_PAR_DEFAUT, SCHEMA, creerCharge } from '../../stockage/document.js';
|
||
import { TABLES_INTERFACE } from '../libelles.js';
|
||
import { REGLAGES_GENERATION, reglagesDeSection, reglagesDuGenre, valeurDuReglage } from './reglages.js';
|
||
|
||
const { t } = creerTraduction([...TABLES_APPLICATION, ...TABLES_INTERFACE]);
|
||
|
||
// Une charge neuve, telle que creerCharge la pose.
|
||
const chargeNeuveEpreuve = () => creerCharge({ id: 'epreuve', nom: 'Soirée des Merles', siegesParDefaut: 6, tours: 2 });
|
||
|
||
// La règle du champ cle d'une règle objet du schéma.
|
||
const champDe = (regle, cle) => regle.champs.find(([nom]) => nom === cle)[1];
|
||
|
||
// Les chemins des réglages que le schéma du fichier décrit, dans son ordre :
|
||
// <cle> pour une valeur à la racine des réglages, <objet>.<cle> pour une
|
||
// valeur d'un objet imbriqué ; un champ facultatif compte comme un autre.
|
||
function cheminsDuSchema() {
|
||
const chemins = [];
|
||
const parcourir = (regle, prefixe) => {
|
||
for (const [cle, sous] of regle.champs) {
|
||
if (sous.genre === 'objet') parcourir(sous, `${prefixe}${cle}.`);
|
||
else chemins.push(`${prefixe}${cle}`);
|
||
}
|
||
};
|
||
parcourir(champDe(champDe(SCHEMA, 'charge'), 'reglages'), '');
|
||
return chemins;
|
||
}
|
||
|
||
// La commande qui écrit un réglage de chaque section, et ses arguments.
|
||
const ECRITURES = new Map([
|
||
['generation', (entree, valeur) => ['modifierReglage', { cle: entree.cle, valeur }]],
|
||
['propositions', (entree, valeur) => ['changerCibleRedondance', { cible: String(valeur) }]],
|
||
]);
|
||
|
||
describe('REGLAGES_GENERATION : la définition que lisent les écrans et la documentation (§ 19.8)', () => {
|
||
test('chaque entrée porte nom, cle, etiquette, unite, defaut, genre et section ; la définition comme chaque entrée sont figées', () => {
|
||
assert.ok(REGLAGES_GENERATION.length > 0, 'définition vide');
|
||
assert.ok(Object.isFrozen(REGLAGES_GENERATION));
|
||
for (const entree of REGLAGES_GENERATION) {
|
||
assert.deepEqual(Object.keys(entree), ['nom', 'cle', 'etiquette', 'unite', 'defaut', 'genre', 'section']);
|
||
assert.ok(Object.isFrozen(entree), entree.nom);
|
||
}
|
||
assert.throws(() => REGLAGES_GENERATION.push({}), TypeError);
|
||
});
|
||
|
||
test('l’ordre des écrans : les trois champs de la recherche, les bascules du § 5.3, puis la cible de la section des propositions', () => {
|
||
assert.deepEqual(
|
||
REGLAGES_GENERATION.map(({ nom, genre, section }) => [section, genre, nom]),
|
||
[
|
||
['generation', 'champ', 'nombre'],
|
||
['generation', 'champ', 'arret'],
|
||
['generation', 'champ', 'historique'],
|
||
['generation', 'bascule', 'separerAppartenances'],
|
||
['generation', 'bascule', 'nouveauxVoisins'],
|
||
['generation', 'bascule', 'nouvelleTable'],
|
||
['generation', 'bascule', 'varierAppartenances'],
|
||
['generation', 'bascule', 'attribuerSieges'],
|
||
['propositions', 'champ', 'cibleRedondance'],
|
||
],
|
||
);
|
||
});
|
||
|
||
test('un champ de la génération : clé generation.<nom>, libellé generation.reglage.<nom>, une unité ; une bascule : clé <nom>, libellé generation.contrainte.<nom>, sans unité', () => {
|
||
const generation = reglagesDeSection(REGLAGES_GENERATION, 'generation');
|
||
const champs = reglagesDuGenre(generation, 'champ');
|
||
assert.equal(champs.length, 3);
|
||
for (const { nom, cle, etiquette } of champs) {
|
||
assert.equal(cle, `generation.${nom}`);
|
||
assert.equal(etiquette, `generation.reglage.${nom}`);
|
||
}
|
||
assert.deepEqual(champs.map(({ unite }) => unite), ['propositions', 'mouvements', 'mouvements']);
|
||
const bascules = reglagesDuGenre(generation, 'bascule');
|
||
assert.equal(bascules.length, 5);
|
||
for (const { nom, cle, etiquette, unite } of bascules) {
|
||
assert.equal(cle, nom);
|
||
assert.equal(etiquette, `generation.contrainte.${nom}`);
|
||
assert.equal(unite, null);
|
||
}
|
||
assert.equal(champs.length + bascules.length, generation.length);
|
||
});
|
||
|
||
test('la cible de la somme de redondance : un champ de la section des propositions, à la racine des réglages, sous le libellé de son champ, sans unité', () => {
|
||
assert.deepEqual(reglagesDeSection(REGLAGES_GENERATION, 'propositions'), [
|
||
{
|
||
nom: 'cibleRedondance',
|
||
cle: 'cibleRedondance',
|
||
etiquette: 'propositions.cible.etiquette',
|
||
unite: null,
|
||
defaut: CIBLE_REDONDANCE_PAR_DEFAUT,
|
||
genre: 'champ',
|
||
section: 'propositions',
|
||
},
|
||
]);
|
||
});
|
||
|
||
test('chaque défaut est celui d’une charge neuve : GENERATION_PAR_DEFAUT pour les champs, les bascules de creerCharge, CIBLE_REDONDANCE_PAR_DEFAUT pour la cible absente', () => {
|
||
const { reglages } = chargeNeuveEpreuve();
|
||
assert.equal(Object.hasOwn(reglages, 'cibleRedondance'), false);
|
||
for (const entree of REGLAGES_GENERATION) {
|
||
assert.equal(entree.defaut, valeurDuReglage(reglages, entree), entree.nom);
|
||
}
|
||
const generation = reglagesDeSection(REGLAGES_GENERATION, 'generation');
|
||
assert.deepEqual(
|
||
Object.fromEntries(reglagesDuGenre(generation, 'champ').map(({ nom, defaut }) => [nom, defaut])),
|
||
{ ...GENERATION_PAR_DEFAUT },
|
||
);
|
||
assert.deepEqual(
|
||
reglagesDuGenre(generation, 'bascule').map(({ defaut }) => defaut),
|
||
[true, true, true, true, false],
|
||
);
|
||
});
|
||
|
||
test('chaque réglage du schéma du fichier, facultatif compris, a son entrée, et chaque entrée en désigne un : aucun n’échappe aux écrans ni au tableau', () => {
|
||
const chemins = cheminsDuSchema();
|
||
assert.ok(chemins.includes('cibleRedondance') && chemins.includes('generation.nombre'), chemins.join(', '));
|
||
assert.deepEqual(
|
||
[...REGLAGES_GENERATION.map(({ cle }) => cle)].sort(),
|
||
[...chemins].sort(),
|
||
);
|
||
});
|
||
|
||
test('chaque entrée s’écrit par la commande de sa section, et chaque libellé existe', () => {
|
||
let charge = chargeNeuveEpreuve();
|
||
for (const entree of REGLAGES_GENERATION) {
|
||
const valeur = entree.genre === 'bascule' ? !entree.defaut : entree.defaut + 1;
|
||
const [commande, args] = ECRITURES.get(entree.section)(entree, valeur);
|
||
charge = appliquerCommande(commande, charge, args).charge;
|
||
assert.equal(valeurDuReglage(charge.reglages, entree), valeur, entree.cle);
|
||
assert.notEqual(t(entree.etiquette), entree.etiquette);
|
||
}
|
||
assert.deepEqual([...new Set(REGLAGES_GENERATION.map(({ section }) => section))], [...ECRITURES.keys()]);
|
||
});
|
||
});
|
||
|
||
describe('valeurDuReglage, reglagesDuGenre et reglagesDeSection', () => {
|
||
test('la valeur d’un réglage se lit au chemin de sa clé : sous generation pour un champ de la génération, à la racine sinon', () => {
|
||
const reglages = { nouvelleTable: false, cibleRedondance: 7, generation: { nombre: 9 } };
|
||
const [nombre] = REGLAGES_GENERATION;
|
||
const nouvelleTable = REGLAGES_GENERATION.find(({ nom }) => nom === 'nouvelleTable');
|
||
const cible = REGLAGES_GENERATION.find(({ nom }) => nom === 'cibleRedondance');
|
||
assert.equal(valeurDuReglage(reglages, nombre), 9);
|
||
assert.equal(valeurDuReglage(reglages, nouvelleTable), false);
|
||
assert.equal(valeurDuReglage(reglages, cible), 7);
|
||
});
|
||
|
||
test('un réglage facultatif absent vaut son défaut', () => {
|
||
const cible = REGLAGES_GENERATION.find(({ nom }) => nom === 'cibleRedondance');
|
||
assert.equal(valeurDuReglage({ generation: {} }, cible), CIBLE_REDONDANCE_PAR_DEFAUT);
|
||
assert.equal(valeurDuReglage({ generation: {} }, { ...cible, defaut: 3 }), 3);
|
||
});
|
||
|
||
test('un genre inconnu lève, et le nomme', () => {
|
||
assert.throws(() => valeurDuReglage({ generation: {} }, { nom: 'x', cle: 'x', genre: 'curseur' }), { name: 'RangeError', message: /"curseur"/ });
|
||
});
|
||
|
||
test('reglagesDuGenre et reglagesDeSection gardent l’ordre de la définition reçue, et une définition vide n’en rend aucun', () => {
|
||
const definition = [...REGLAGES_GENERATION].reverse();
|
||
assert.deepEqual(
|
||
reglagesDuGenre(definition, 'champ').map(({ nom }) => nom),
|
||
['cibleRedondance', 'historique', 'arret', 'nombre'],
|
||
);
|
||
assert.deepEqual(
|
||
reglagesDeSection(definition, 'generation').slice(0, 2).map(({ nom }) => nom),
|
||
['attribuerSieges', 'varierAppartenances'],
|
||
);
|
||
assert.deepEqual(reglagesDuGenre([], 'bascule'), []);
|
||
assert.deepEqual(reglagesDeSection([], 'generation'), []);
|
||
});
|
||
});
|