The operator now reaches tags, goals and priorities from the screens. A person's card adds and removes tags, the participant list carries a tag legend; each table takes an evening goal or one per round with its rule; the generation settings hold a keyboard-operable priority editor (move, join, split, restore, weights), the two optional criteria and the goal mode; the proposals table and its ranking sentence follow the order; the import screen reads a participants or a goals file. Removing a tag or a goal returns focus to a neighbouring control. Checked: node 4590, navigateur 942, node-long 124; captures republished. --- FR --- [ADD] écrans : étiquettes, objectifs des tables, priorités, import L'opérateur atteint étiquettes, objectifs et priorités depuis les écrans. La fiche ajoute et retire les étiquettes, la liste porte leur légende ; chaque table reçoit un objectif de soirée ou par tour avec sa règle ; les réglages de génération portent un éditeur des priorités utilisable au clavier (monter, joindre, séparer, rétablir, poids), les deux critères facultatifs et le mode des objectifs ; le tableau des propositions et sa phrase suivent l'ordre ; l'import lit un fichier de personnes ou d'objectifs. Retirer une étiquette ou un objectif rend le focus à côté. Vérifié : node 4590, navigateur 942, longues 124 ; captures republiées. Assisted-by: Claude Opus 5.5
133 lines
6.2 KiB
JavaScript
133 lines
6.2 KiB
JavaScript
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||
|
||
// L'évaluation que partagent Génération et Propositions, dans un vrai
|
||
// moteur de rendu (§ 5.7, § 13.3) : tant que la charge de la séance ne
|
||
// change pas, revenir à l'une ou l'autre section ne mesure rien de nouveau ;
|
||
// une charge écrite se mesure une fois, et la meilleure proposition annoncée
|
||
// la suit. Une charge sans proposition n'a pas de table partagée : l'éditeur
|
||
// des priorités y lit l'état des critères par une évaluation à lui, une fois
|
||
// par charge écrite.
|
||
//
|
||
// Le module de l'évaluation est remplacé par ses propres fonctions,
|
||
// espionnées (vi.mock, spy) : evaluer garde son effet, et ses appels se
|
||
// comptent. Chaque épreuve monte App par monterApp, sur le système de
|
||
// fichiers simulé, avec l'exécuteur direct ; le rendu se lit par les
|
||
// attributs data-… ; les noms d'épreuve sont inventés.
|
||
//
|
||
// Le projet navigateur emploie expect, vi, et test et describe de Vitest
|
||
// (§ 14.8).
|
||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||
import { flushSync } from 'svelte';
|
||
import { evaluer, premiereDuClassement } from '../../application/evaluation.js';
|
||
import { creerFormat } from '../../application/format.js';
|
||
import { creerExecuteurDirect } from '../../application/generation.js';
|
||
import { TABLES_APPLICATION } from '../../application/libelles.js';
|
||
import { creerTraduction } from '../../application/traduction.js';
|
||
import { rechercher } from '../../moteur/recherche.js';
|
||
import { configurationDepuisCharge } from '../../stockage/document.js';
|
||
import { VERSION } from '../../version.genere.js';
|
||
import { fixerCadre } from '../../../test/navigateur/entrees.js';
|
||
import { monterApp } from '../../../test/navigateur/montage.js';
|
||
import { TABLES_INTERFACE } from '../libelles.js';
|
||
import { meilleureProposition } from './meilleure.js';
|
||
|
||
vi.mock('../../application/evaluation.js', { spy: true });
|
||
|
||
const { t } = creerTraduction([...TABLES_APPLICATION, ...TABLES_INTERFACE]);
|
||
const format = creerFormat(t);
|
||
|
||
// Les montages de l'épreuve, démontés après elle ; le collecteur d'erreurs
|
||
// n'a rien reçu pendant l'épreuve.
|
||
let montes = [];
|
||
async function monter(options) {
|
||
const m = await monterApp(options);
|
||
montes.push(m);
|
||
return m;
|
||
}
|
||
beforeEach(async () => {
|
||
await fixerCadre(1366, 700);
|
||
});
|
||
afterEach(() => {
|
||
const erreurs = montes.flatMap((m) => m.erreurs());
|
||
for (const m of montes.reverse()) m.demonter();
|
||
montes = [];
|
||
window.scrollTo(0, 0);
|
||
expect(erreurs, 'erreurs reçues par le collecteur').toEqual([]);
|
||
});
|
||
|
||
const dans = (m, selecteur) => m.racine.querySelector(selecteur);
|
||
const charge = (m) => m.seance.etat().charge;
|
||
const identifiant = (m) => dans(m, '[data-section="generation"] [data-meilleure] [data-meilleure-id]');
|
||
const lignes = (m) => m.racine.querySelectorAll('[data-section="propositions"] tbody tr[data-proposition]');
|
||
const appels = () => vi.mocked(evaluer).mock.calls.length;
|
||
|
||
// Ouvre la section nommée, son rendu à jour.
|
||
function aller(m, section) {
|
||
m.ui.allerA(section);
|
||
flushSync();
|
||
}
|
||
|
||
// Une génération du moteur de plus, enregistrée par la séance.
|
||
async function enregistrer(m, reglages) {
|
||
const configuration = configurationDepuisCharge(charge(m));
|
||
const propositions = rechercher(configuration, { historique: 1000, ...reglages });
|
||
await m.seance.executer('enregistrerGeneration', { propositions, configuration, produitVersion: VERSION.affichee });
|
||
flushSync();
|
||
}
|
||
|
||
// L'identifiant au rang 1 d'une table neuve de la charge courante, tel que
|
||
// le bloc l'écrit.
|
||
const rangUnAttendu = (m) => format.nombre(premiereDuClassement(evaluer(charge(m))).id);
|
||
|
||
describe('Génération et Propositions relisent une même évaluation (§ 5.7, § 13.3)', () => {
|
||
test('revenir à Génération, puis à Propositions, ne mesure rien de nouveau ; une écriture mesure une fois, et l’annonce suit la charge écrite', async () => {
|
||
const m = await monter({ executeur: creerExecuteurDirect() });
|
||
await m.facade.chargerDemonstration('petite-conflit');
|
||
await m.seance.passerEnEcriture();
|
||
await enregistrer(m, { graine: 5, nombre: 3, arret: 300 });
|
||
aller(m, 'participants');
|
||
vi.mocked(evaluer).mockClear();
|
||
|
||
aller(m, 'generation');
|
||
expect(appels(), '1re visite de Génération').toBe(1);
|
||
const premiere = identifiant(m).textContent;
|
||
aller(m, 'participants');
|
||
aller(m, 'generation');
|
||
expect(appels(), '2e visite de Génération').toBe(1);
|
||
aller(m, 'propositions');
|
||
expect(lignes(m).length).toBe(3);
|
||
expect(appels(), 'Propositions après Génération').toBe(1);
|
||
aller(m, 'generation');
|
||
expect(appels(), 'Génération après Propositions').toBe(1);
|
||
expect(identifiant(m).textContent).toBe(premiere);
|
||
// Le modèle relit la même table que les deux sections.
|
||
expect(format.nombre(meilleureProposition(charge(m)).id)).toBe(premiere);
|
||
expect(appels(), 'le modèle après les deux sections').toBe(1);
|
||
|
||
// Une écriture qui remplace les propositions : la charge écrite se
|
||
// mesure une fois, et l'annonce lit son rang 1, que l'ancienne charge
|
||
// ne portait pas — un identifiant ne se réattribue jamais.
|
||
const anciens = charge(m).propositions.map(({ id }) => id);
|
||
const avantEcriture = appels();
|
||
await m.seance.executer('effacerPropositions', {});
|
||
flushSync();
|
||
expect(appels(), 'la charge sans proposition, évaluée une fois pour l’état des critères').toBe(avantEcriture + 1);
|
||
aller(m, 'participants');
|
||
aller(m, 'generation');
|
||
expect(appels(), 'la charge sans proposition, revisitée').toBe(avantEcriture + 1);
|
||
const avantGeneration = appels();
|
||
await enregistrer(m, { graine: 9, nombre: 2, arret: 300 });
|
||
expect(appels(), 'la charge écrite, mesurée une fois').toBe(avantGeneration + 1);
|
||
const attendu = rangUnAttendu(m);
|
||
const apres = appels();
|
||
expect(identifiant(m).textContent).toBe(attendu);
|
||
expect(anciens.map((id) => format.nombre(id))).not.toContain(attendu);
|
||
aller(m, 'participants');
|
||
aller(m, 'generation');
|
||
expect(identifiant(m).textContent).toBe(attendu);
|
||
aller(m, 'propositions');
|
||
expect(lignes(m).length).toBe(2);
|
||
expect(appels(), 'les deux sections sur la charge écrite').toBe(apres);
|
||
});
|
||
});
|