[ADD] home: best ranked proposition on every event line
Each event line of the home lists now names its rank-1 proposition and its cumulated collisions, whether or not a plan is retained, with the same label and tooltip as the generation block. One application function picks the rank-1 proposition for both places, so the list, the generation block and the propositions table show the same proposition and number. The list reads the examined charge, as opening the event does, so a file with a repeated proposition identifier no longer hides the whole folder. Checked: 2923 node, 609 browser, 62 long tests, from the index alone. --- FR --- [ADD] accueil : meilleure proposition classée sur chaque événement Chaque ligne des listes de l'accueil nomme sa proposition de rang 1 et ses collisions cumulées, retenu ou non, avec le même libellé et la même infobulle que le bloc de la génération. Une seule fonction d'application désigne le rang 1 aux deux endroits : la liste, le bloc et le tableau des propositions montrent la même proposition et le même nombre. La liste lit la charge examinée, comme l'ouverture : un fichier dont une proposition répète un identifiant ne fait plus disparaître tout le dossier. Vérifié : 2923 node, 609 navigateur, 62 longues, depuis l'index seul. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
5fc5ac9205
commit
9eb1275f53
12 changed files with 659 additions and 111 deletions
9
spec.md
9
spec.md
|
|
@ -1211,7 +1211,14 @@ tableau des propositions — et la cible celle de l'événement (§ 8.9), 1000 p
|
|||
défaut, réglable dans la section des propositions. Les deux se lisent dans la
|
||||
même mesure que le tableau, au moment de lister, sans rien écrire ; un événement
|
||||
sans retenu, ou dont le retenu est fautif ou en dérive (§ 8.9, § 9), y porte
|
||||
« — ». Chaque intitulé s'explique dans une infobulle.
|
||||
« — ». Chaque ligne porte aussi la **« Meilleure proposition du classement »**,
|
||||
sous les libellés du bloc que la section de la génération montre en tête :
|
||||
l'identifiant de la proposition au rang 1 du classement (§ 5.7) et ses
|
||||
collisions cumulées — la même proposition et le même nombre que ce bloc et que
|
||||
la première ligne du tableau des propositions, retenue ou non ; une proposition
|
||||
fautive, que l'ouverture écarte (§ 8.9, point 3), n'y entre pas. Sans proposition
|
||||
classée — aucune, ou aucune qui décrive encore la configuration —, l'un et
|
||||
l'autre valent « — ». Chaque intitulé s'explique dans une infobulle.
|
||||
**Un répertoire vide n'affiche pas une liste vide** : il présente la commande de
|
||||
création et les démonstrations — une liste vide sans issue est le premier écran
|
||||
que voit un opérateur qui lance l'exécutable.
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@
|
|||
// resumeDuRetenu donne à la liste des événements deux nombres du retenu,
|
||||
// lus dans la même mesure que sa ligne du tableau, sans le diagnostic ni
|
||||
// les plafonds : la liste en résume chaque événement du dossier.
|
||||
// premiereDuClassement est la seule définition de la proposition au rang 1
|
||||
// : la section de la génération la lit dans sa table, resumeDuMeilleur la
|
||||
// donne à la liste, sur la même table d'évaluation, diagnostic compris — le
|
||||
// premier critère du classement lit le plafond a priori.
|
||||
import { classer } from '../moteur/classement.js';
|
||||
import { indexerPlan, normaliser } from '../moteur/configuration.js';
|
||||
import { diagnostiquer } from '../moteur/diagnostic.js';
|
||||
|
|
@ -178,3 +182,42 @@ export function resumeDuRetenu(charge) {
|
|||
const cible = cibleRedondance(charge);
|
||||
return { collisionsCumulees, totalRedondance, cibleRedondance: cible, diffRedondance: Math.abs(cible - totalRedondance) };
|
||||
}
|
||||
|
||||
/**
|
||||
* La proposition au rang 1 du classement d'une table d'évaluation (§ 5.7) :
|
||||
* la première de ses propositions quand elle se compare — les comparables
|
||||
* ouvrent la liste, dans l'ordre du classement —, null quand aucune ne se
|
||||
* classe.
|
||||
*
|
||||
* @param {TableEvaluation} table
|
||||
* @returns {LigneEvaluation|null}
|
||||
*/
|
||||
export function premiereDuClassement(table) {
|
||||
const [premiere] = table.propositions;
|
||||
return premiere !== undefined && premiere.statut === 'comparable' ? premiere : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* La meilleure proposition du classement que montre la liste des
|
||||
* événements (§ 5.4, § 5.7, § 8.1) : l'identifiant de la proposition au
|
||||
* rang 1 (premiereDuClassement) et ses collisions cumulées, celles de sa
|
||||
* ligne d'evaluer. null sans proposition, quand aucune ne se compare —
|
||||
* toutes fautives ou en dérive —, et quand la configuration ne se normalise
|
||||
* pas : la liste écrit alors « — ». Le retenu n'y entre pas. Ne modifie pas
|
||||
* la charge.
|
||||
*
|
||||
* @param {import('../stockage/types.js').Charge} charge
|
||||
* @returns {{id: number, collisionsCumulees: number}|null}
|
||||
*/
|
||||
export function resumeDuMeilleur(charge) {
|
||||
if (charge.propositions.length === 0) return null;
|
||||
let table;
|
||||
try {
|
||||
table = evaluer(charge);
|
||||
} catch (erreur) {
|
||||
if (erreur instanceof ErreurConfiguration) return null;
|
||||
throw erreur;
|
||||
}
|
||||
const premiere = premiereDuClassement(table);
|
||||
return premiere === null ? null : { id: premiere.id, collisionsCumulees: premiere.mesures.collisionsCumulees };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,14 @@ import { analyser, capacite, configurationDepuisCharge } from '../stockage/docum
|
|||
import { planDepuisFichier } from '../stockage/placements.js';
|
||||
import { VERSION } from '../version.genere.js';
|
||||
import { appliquerCommande } from './commandes.js';
|
||||
import { CIBLE_REDONDANCE_PAR_DEFAUT, cibleRedondance, evaluer, resumeDuRetenu } from './evaluation.js';
|
||||
import {
|
||||
CIBLE_REDONDANCE_PAR_DEFAUT,
|
||||
cibleRedondance,
|
||||
evaluer,
|
||||
premiereDuClassement,
|
||||
resumeDuMeilleur,
|
||||
resumeDuRetenu,
|
||||
} from './evaluation.js';
|
||||
|
||||
const V1 = VERSION.affichee;
|
||||
|
||||
|
|
@ -424,3 +431,68 @@ describe('resumeDuRetenu : les mesures du retenu que lit la liste des événemen
|
|||
assert.equal(resumeDuRetenu(charge), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resumeDuMeilleur : la proposition au rang 1 que lit la liste des événements (§ 5.4, § 5.7, § 8.1)', () => {
|
||||
test("le rang 1 du classement, ni le plus petit identifiant ni le moins de collisions : son identifiant et ses collisions cumulées", () => {
|
||||
// Le classement range les plans écrits à la main 3, 2, 1, 4 : la 3 vient
|
||||
// en tête avec 6 collisions cumulées, quand la 1 n'en porte que 4.
|
||||
const charge = structuredClone(chargeDemo('petite-conflit'));
|
||||
charge.propositions = PLANS_CONFLIT.map(({ tours }, rang) => propositionDe(charge, rang + 1, tours));
|
||||
charge.prochainsIds.proposition = 5;
|
||||
figer(charge);
|
||||
const table = evaluer(charge);
|
||||
assert.deepEqual(table.classement.ordre, [3, 2, 1, 4]);
|
||||
assert.deepEqual(resumeDuMeilleur(charge), { id: 3, collisionsCumulees: 6 });
|
||||
assert.equal(premiereDuClassement(table), table.propositions[0]);
|
||||
assert.equal(premiereDuClassement(table).mesures.collisionsCumulees, 6);
|
||||
});
|
||||
|
||||
test("après des générations : la première ligne d'evaluer, comparable, et le retenu n'y change rien", () => {
|
||||
let charge = generee(chargeDemo('petite-conflit'), { graine: 4, nombre: 3, arret: 300 });
|
||||
charge = generee(charge, { graine: 9, nombre: 2, arret: 300 });
|
||||
const [premiere] = evaluer(charge).propositions;
|
||||
assert.equal(premiere.statut, 'comparable');
|
||||
const attendu = { id: premiere.id, collisionsCumulees: premiere.mesures.collisionsCumulees };
|
||||
assert.deepEqual(resumeDuMeilleur(figer(charge)), attendu);
|
||||
const derniere = charge.propositions.at(-1).id;
|
||||
const retenue = executer(charge, 'retenirProposition', { id: derniere });
|
||||
assert.deepEqual(resumeDuMeilleur(retenue), attendu);
|
||||
});
|
||||
|
||||
test('sans proposition, null, retenu ou non ; premiereDuClassement d’une table sans comparable, null', () => {
|
||||
assert.equal(resumeDuMeilleur(chargeDemo('petite')), null);
|
||||
const generees = generee(chargeDemo('petite-conflit'), { graine: 3, nombre: 2, arret: 300 });
|
||||
const retenue = executer(generees, 'retenirProposition', { id: generees.propositions[0].id });
|
||||
// Un retenu seul : la liste vidée à la main, la commande gardant la
|
||||
// proposition d'origine du retenu.
|
||||
const retenuSeul = structuredClone(retenue);
|
||||
retenuSeul.propositions = [];
|
||||
assert.equal(evaluer(retenuSeul).retenu.statut, 'comparable');
|
||||
assert.equal(resumeDuMeilleur(retenuSeul), null);
|
||||
assert.equal(premiereDuClassement(evaluer(retenuSeul)), null);
|
||||
});
|
||||
|
||||
test('des propositions toutes hors du classement — population changée, ou fautives — : null, sans lever', () => {
|
||||
const generees = generee(chargeDemo('petite'), { graine: 3, nombre: 2, arret: 300 });
|
||||
assert.notEqual(resumeDuMeilleur(generees), null);
|
||||
const exclue = executer(generees, 'exclureParticipant', { id: generees.participants[0].id });
|
||||
const table = evaluer(exclue);
|
||||
assert.equal(table.propositions.length, 2);
|
||||
assert.ok(table.propositions.every(({ statut }) => statut === 'derive'));
|
||||
assert.equal(premiereDuClassement(table), null);
|
||||
assert.equal(resumeDuMeilleur(exclue), null);
|
||||
|
||||
const fautives = structuredClone(generees);
|
||||
for (const proposition of fautives.propositions) proposition.placement[0].reserve.push(proposition.participants[0]);
|
||||
assert.ok(evaluer(fautives).propositions.every(({ statut }) => statut === 'fautif'));
|
||||
assert.equal(resumeDuMeilleur(fautives), null);
|
||||
});
|
||||
|
||||
test('une configuration qui ne se normalise pas rend null là où evaluer lève', () => {
|
||||
const charge = structuredClone(generee(chargeDemo('petite-conflit'), { graine: 3, nombre: 2, arret: 300 }));
|
||||
const table = charge.tables[0].id;
|
||||
charge.reservations = [1, 2, 3, 4].map((participant) => ({ participant, table, siege: null, portee: 'tous', tour: null }));
|
||||
assert.throws(() => evaluer(charge), (erreur) => erreur instanceof ErreurConfiguration && erreur.code === 'SURRESERVATION');
|
||||
assert.equal(resumeDuMeilleur(charge), null);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,10 +15,12 @@
|
|||
// épreuve la remplace après le montage.
|
||||
//
|
||||
// Lister ne fait que lire. Chaque ligne lisible porte le résumé de son
|
||||
// retenu (resumeDuRetenu, evaluation.js), lu dans l'état de l'événement au
|
||||
// moment de lister. Une opération qui écrit dérive ses refus lisibles
|
||||
// sans écrire — nom vide, plan bloqué, format plus récent — avant toute
|
||||
// écriture, sonde de verrou comprise. Renommer et supprimer ferment d'abord
|
||||
// retenu (resumeDuRetenu, evaluation.js) et la meilleure proposition de son
|
||||
// classement (resumeDuMeilleur), lus dans une seule lecture de l'état de
|
||||
// l'événement au moment de lister, sur sa charge examinée comme l'ouverture
|
||||
// l'examine. Une opération qui écrit dérive ses refus lisibles sans écrire
|
||||
// — nom vide, plan bloqué, format plus récent — avant toute écriture, sonde
|
||||
// de verrou comprise. Renommer et supprimer ferment d'abord
|
||||
// l'événement visé quand il est ouvert : la séance rend son verrou, et la
|
||||
// façade sonde alors celui du disque sous son propre identifiant de séance,
|
||||
// tiré une fois à sa création ; une séance vivante, ou dont la vie ne se
|
||||
|
|
@ -45,9 +47,10 @@ import {
|
|||
renommer as renommerSurDisque,
|
||||
supprimer as supprimerSurDisque,
|
||||
} from '../stockage/operations.js';
|
||||
import { examiner } from '../stockage/placements.js';
|
||||
import { lireReglages } from '../stockage/reglages_locaux.js';
|
||||
import { ErreurCommande, refus } from './erreurs.js';
|
||||
import { resumeDuRetenu } from './evaluation.js';
|
||||
import { resumeDuMeilleur, resumeDuRetenu } from './evaluation.js';
|
||||
import { libelle } from './libelles.js';
|
||||
|
||||
/** Sièges par défaut et tours d'un événement neuf (§ 2.1, étapes 4 et 5). */
|
||||
|
|
@ -130,20 +133,30 @@ async function derniereModification(fs, racine, base) {
|
|||
return lireJournal(texte).entrees.at(-1)?.horodatage ?? null;
|
||||
}
|
||||
|
||||
// Le résumé du retenu de l'événement base (resumeDuRetenu), lu dans son
|
||||
// état, sans rien écrire ; null pour un état illisible au moment de la
|
||||
// liste — un résumé illisible ne se relit pas —, et pour un état qui a
|
||||
// cessé de se lire depuis : la ligne en porte déjà la cause, ou la portera
|
||||
// à la liste suivante.
|
||||
async function mesuresDuRetenuDe(fs, racine, { base, illisible }) {
|
||||
if (illisible !== null) return null;
|
||||
// Les deux résumés de l'événement base, lus dans une seule lecture de son
|
||||
// état, sans rien écrire : mesuresDuRetenu (resumeDuRetenu) et
|
||||
// meilleureDuClassement (resumeDuMeilleur). Ils lisent la charge examinée
|
||||
// (examiner, § 8.9, point 3), celle que l'ouverture de l'état garde : une
|
||||
// proposition fautive, dont l'identifiant se répète ou atteint le compteur,
|
||||
// n'entre pas au classement, qui lève sur un identifiant répété, et la
|
||||
// liste nomme la proposition au rang 1 que le bloc de génération de
|
||||
// l'événement ouvert nomme. Chacun vaut null pour un état
|
||||
// illisible au moment de la liste — un résumé illisible ne se relit pas —,
|
||||
// et pour un état qui a cessé de se lire depuis : la ligne en porte déjà la
|
||||
// cause, ou la portera à la liste suivante.
|
||||
const AUCUN_RESUME = Object.freeze({ mesuresDuRetenu: null, meilleureDuClassement: null });
|
||||
async function resumesDe(fs, racine, { base, illisible }) {
|
||||
if (illisible !== null) return AUCUN_RESUME;
|
||||
let charge;
|
||||
try {
|
||||
const texte = await fs.lireTexte(racine, base + SUFFIXES.etat);
|
||||
return texte === null ? null : resumeDuRetenu(analyser(texte).charge);
|
||||
if (texte === null) return AUCUN_RESUME;
|
||||
charge = examiner(analyser(texte).charge).charge;
|
||||
} catch (erreur) {
|
||||
if (erreur instanceof ErreurStockage) return null;
|
||||
if (erreur instanceof ErreurStockage) return AUCUN_RESUME;
|
||||
throw erreur;
|
||||
}
|
||||
return { mesuresDuRetenu: resumeDuRetenu(charge), meilleureDuClassement: resumeDuMeilleur(charge) };
|
||||
}
|
||||
|
||||
// Pourquoi l'état d'un résumé illisible ne s'ouvre pas : les détails de
|
||||
|
|
@ -303,7 +316,7 @@ export function creerFacade({
|
|||
? resumes.filter((autre) => autre !== resume && autre.doublon && autre.id === id).map((autre) => autre.base)
|
||||
: [],
|
||||
verrou: await verrouDe(fs, courante, base),
|
||||
mesuresDuRetenu: await mesuresDuRetenuDe(fs, courante, resume),
|
||||
...(await resumesDe(fs, courante, resume)),
|
||||
});
|
||||
}
|
||||
return lignes;
|
||||
|
|
@ -485,7 +498,9 @@ export function creerFacade({
|
|||
/**
|
||||
* Les lignes de la liste (§ 8.1), dans l'ordre du dépôt ; n'écrit rien.
|
||||
* mesuresDuRetenu porte resumeDuRetenu de l'état, null sans retenu
|
||||
* mesurable ou pour un état illisible.
|
||||
* mesurable ou pour un état illisible ; meilleureDuClassement,
|
||||
* resumeDuMeilleur de l'état, null sans proposition classée ou pour un
|
||||
* état illisible.
|
||||
*/
|
||||
lister,
|
||||
/** Crée et ouvre en lecture l'événement { nom, date } (§ 2.1, étape 1). */
|
||||
|
|
|
|||
|
|
@ -20,9 +20,10 @@ import { analyser, configurationDepuisCharge, creerCharge } from '../stockage/do
|
|||
import { ErreurStockage } from '../stockage/erreurs.js';
|
||||
import { lireJournal } from '../stockage/journal.js';
|
||||
import { SUFFIXES } from '../stockage/noms.js';
|
||||
import { examiner } from '../stockage/placements.js';
|
||||
import { VERSION } from '../version.genere.js';
|
||||
import { appliquerCommande } from './commandes.js';
|
||||
import { evaluer, resumeDuRetenu } from './evaluation.js';
|
||||
import { evaluer, resumeDuMeilleur, resumeDuRetenu } from './evaluation.js';
|
||||
import { creerFacade } from './evenements.js';
|
||||
import { creerHorlogePilotee } from './horloge.js';
|
||||
import { libelle } from './libelles.js';
|
||||
|
|
@ -432,6 +433,7 @@ describe('lister (§ 8.1, § 19.4)', () => {
|
|||
doublonAvec: [],
|
||||
verrou: null,
|
||||
mesuresDuRetenu: null,
|
||||
meilleureDuClassement: null,
|
||||
},
|
||||
{
|
||||
base: 'petite',
|
||||
|
|
@ -447,6 +449,7 @@ describe('lister (§ 8.1, § 19.4)', () => {
|
|||
doublonAvec: [],
|
||||
verrou: null,
|
||||
mesuresDuRetenu: null,
|
||||
meilleureDuClassement: null,
|
||||
},
|
||||
]);
|
||||
assert.ok(petite.participants.length > 0);
|
||||
|
|
@ -601,6 +604,99 @@ describe('lister (§ 8.1, § 19.4)', () => {
|
|||
assert.notEqual(par.tronquee.illisible, null);
|
||||
assert.equal(par.tronquee.mesuresDuRetenu, null);
|
||||
});
|
||||
|
||||
test('la meilleure proposition du classement : celle de resumeDuMeilleur, retenu ou non ; null sans proposition ; rien d’écrit', async () => {
|
||||
const { fs, facade } = await deuxEvenements();
|
||||
// Les mêmes propositions, sans retenu, sous un autre identifiant : deux
|
||||
// événements distincts, aucun doublon.
|
||||
const sansRetenu = texteRetenu((charge) => ({
|
||||
...charge,
|
||||
retenu: null,
|
||||
evenement: { ...charge.evenement, id: 'evt-proposee', etat: 'propose' },
|
||||
}));
|
||||
fs.deposer('portable', `retenue${SUFFIXES.etat}`, texteRetenu());
|
||||
fs.deposer('portable', `proposee${SUFFIXES.etat}`, sansRetenu);
|
||||
const avant = ecritures(fs);
|
||||
const lignes = await facade.lister();
|
||||
assert.equal(ecritures(fs), avant);
|
||||
assert.equal(lignes.length, 4);
|
||||
const par = Object.fromEntries(lignes.map((ligne) => [ligne.base, ligne]));
|
||||
for (const [base, texte] of [
|
||||
['retenue', texteRetenu()],
|
||||
['proposee', sansRetenu],
|
||||
]) {
|
||||
const charge = analyser(texte).charge;
|
||||
const [premiere] = evaluer(charge).propositions;
|
||||
assert.equal(premiere.statut, 'comparable', base);
|
||||
assert.deepEqual(par[base].meilleureDuClassement, { id: premiere.id, collisionsCumulees: premiere.mesures.collisionsCumulees }, base);
|
||||
assert.deepEqual(par[base].meilleureDuClassement, resumeDuMeilleur(charge), base);
|
||||
}
|
||||
assert.equal(par.proposee.mesuresDuRetenu, null);
|
||||
assert.equal(par['soiree-a'].meilleureDuClassement, null);
|
||||
assert.equal(par.petite.meilleureDuClassement, null);
|
||||
});
|
||||
|
||||
test('le résumé du retenu et la meilleure proposition se lisent dans une seule lecture de l’état, après celle du dépôt', async () => {
|
||||
const { fs, facade } = await deuxEvenements();
|
||||
fs.deposer('portable', `retenue${SUFFIXES.etat}`, texteRetenu());
|
||||
const lireTexte = fs.lireTexte;
|
||||
const lus = [];
|
||||
fs.lireTexte = (racine, chemin) => {
|
||||
lus.push(chemin);
|
||||
return lireTexte.call(fs, racine, chemin);
|
||||
};
|
||||
const ligne = (await facade.lister()).find(({ base }) => base === 'retenue');
|
||||
assert.notEqual(ligne.mesuresDuRetenu, null);
|
||||
assert.notEqual(ligne.meilleureDuClassement, null);
|
||||
assert.equal(lus.filter((chemin) => chemin === `retenue${SUFFIXES.etat}`).length, 2, lus.join());
|
||||
});
|
||||
|
||||
// Un état que l'analyse admet et qu'examiner corrige (§ 8.9, point 3) :
|
||||
// la ligne lit la charge que l'ouverture lit, examinée, et non la charge
|
||||
// brute. Les propositions, sans retenu, sous modifier(charge).
|
||||
const texteExamine = (modifier) => texteRetenu((charge) => modifier({ ...charge, retenu: null }));
|
||||
const meilleureOuverte = (texte) => resumeDuMeilleur(examiner(analyser(texte).charge).charge);
|
||||
|
||||
test('un identifiant répété : la ligne rendue, avec la meilleure proposition de la charge examinée', async () => {
|
||||
const { fs, facade } = await deuxEvenements();
|
||||
const texte = texteExamine((charge) => {
|
||||
const [premiere, seconde, ...reste] = charge.propositions;
|
||||
return { ...charge, propositions: [premiere, { ...seconde, id: premiere.id }, ...reste] };
|
||||
});
|
||||
assert.equal(examiner(analyser(texte).charge).fautives[0].fautes[0].code, 'IDENTIFIANT_REPETE');
|
||||
fs.deposer('portable', `repetee${SUFFIXES.etat}`, texte);
|
||||
const ligne = (await facade.lister()).find(({ base }) => base === 'repetee');
|
||||
assert.notEqual(ligne, undefined);
|
||||
assert.notEqual(meilleureOuverte(texte), null);
|
||||
assert.deepEqual(ligne.meilleureDuClassement, meilleureOuverte(texte));
|
||||
});
|
||||
|
||||
test('une proposition hors du compteur : la ligne nomme la meilleure de la charge examinée, celle du bloc de génération', async () => {
|
||||
const { fs, facade } = await deuxEvenements();
|
||||
// Le compteur abaissé à l'identifiant de la proposition au rang 1 de la
|
||||
// charge brute : examiner l'écarte, et le rang 1 change.
|
||||
const rangUn = resumeDuMeilleur(analyser(texteExamine((charge) => charge)).charge);
|
||||
const texte = texteExamine((charge) => ({ ...charge, prochainsIds: { ...charge.prochainsIds, proposition: rangUn.id } }));
|
||||
assert.equal(examiner(analyser(texte).charge).fautives[0].fautes[0].code, 'IDENTIFIANT_HORS_COMPTEUR');
|
||||
assert.notDeepEqual(meilleureOuverte(texte), resumeDuMeilleur(analyser(texte).charge));
|
||||
fs.deposer('portable', `abaissee${SUFFIXES.etat}`, texte);
|
||||
const ligne = (await facade.lister()).find(({ base }) => base === 'abaissee');
|
||||
assert.deepEqual(ligne.meilleureDuClassement, meilleureOuverte(texte));
|
||||
});
|
||||
|
||||
test('des propositions toutes en dérive, et un état illisible : null, la liste entière rendue', async () => {
|
||||
const { fs, facade } = await deuxEvenements();
|
||||
const derivee = texteRetenu((charge) => appliquerCommande('changerTours', charge, { tours: '3' }).charge);
|
||||
assert.ok(evaluer(analyser(derivee).charge).propositions.every(({ statut }) => statut === 'derive'));
|
||||
fs.deposer('portable', `derivee${SUFFIXES.etat}`, derivee);
|
||||
fs.deposer('portable', `tronquee${SUFFIXES.etat}`, texteRetenu().slice(0, 40));
|
||||
const lignes = await facade.lister();
|
||||
assert.equal(lignes.length, 4);
|
||||
const par = Object.fromEntries(lignes.map((ligne) => [ligne.base, ligne]));
|
||||
assert.equal(par.derivee.meilleureDuClassement, null);
|
||||
assert.notEqual(par.tronquee.illisible, null);
|
||||
assert.equal(par.tronquee.meilleureDuClassement, null);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Créer (§ 2.1, § 8.3) ----------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { flushSync, tick } from 'svelte';
|
||||
import { appliquerCommande } from '../../application/commandes.js';
|
||||
import { evaluer } from '../../application/evaluation.js';
|
||||
import { creerFormat } from '../../application/format.js';
|
||||
import { creerHorlogePilotee } from '../../application/horloge.js';
|
||||
import { TABLES_APPLICATION, libelle } from '../../application/libelles.js';
|
||||
|
|
@ -41,7 +42,7 @@ import { planDepuisFichier } from '../../stockage/placements.js';
|
|||
import { VERSION } from '../../version.genere.js';
|
||||
import { creerFichiersSimules } from '../../../test/fichiers_simules.js';
|
||||
import { centre } from '../../../test/navigateur/ecrans.js';
|
||||
import { pointer, touche } from '../../../test/navigateur/entrees.js';
|
||||
import { fixerCadre, pointer, touche } from '../../../test/navigateur/entrees.js';
|
||||
import { monterApp } from '../../../test/navigateur/montage.js';
|
||||
import { visible as visibleDansLaFenetre } from '../../../test/navigateur/rendu.js';
|
||||
import { TABLES_INTERFACE } from '../libelles.js';
|
||||
|
|
@ -1087,45 +1088,68 @@ describe('la racine neutre et la liste qui défile (§ 18.6, § 19.4)', () => {
|
|||
|
||||
// --- Les mesures du retenu (§ 5.4, § 8.1, § 8.9) ------------------------------------
|
||||
|
||||
describe('les mesures du retenu dans la liste (§ 5.4, § 8.1, § 8.9)', () => {
|
||||
// La petite démonstration au conflit inévitable, deux propositions du
|
||||
// moteur, la seconde retenue par la commande.
|
||||
function chargeRetenue() {
|
||||
let charge = analyser(texteLivre('petite-conflit')).charge;
|
||||
const configuration = configurationDepuisCharge(charge);
|
||||
const propositions = rechercher(configuration, { graine: 6, nombre: 2, arret: 300 });
|
||||
charge = appliquerCommande('enregistrerGeneration', charge, { propositions, configuration, produitVersion: VERSION.affichee }).charge;
|
||||
return appliquerCommande('retenirProposition', charge, { id: charge.propositions[1].id }).charge;
|
||||
}
|
||||
// La petite démonstration au conflit inévitable, deux propositions du
|
||||
// moteur, la seconde retenue par la commande.
|
||||
function chargeRetenue() {
|
||||
let charge = analyser(texteLivre('petite-conflit')).charge;
|
||||
const configuration = configurationDepuisCharge(charge);
|
||||
const propositions = rechercher(configuration, { graine: 6, nombre: 2, arret: 300 });
|
||||
charge = appliquerCommande('enregistrerGeneration', charge, { propositions, configuration, produitVersion: VERSION.affichee }).charge;
|
||||
return appliquerCommande('retenirProposition', charge, { id: charge.propositions[1].id }).charge;
|
||||
}
|
||||
|
||||
const etat = (charge) => serialiser(charge, { revision: 1, produitVersion: VERSION.affichee });
|
||||
const TIRET = '\u{2014}';
|
||||
const terme = (ligne, nom) => ligne.querySelector(`[data-terme="${nom}"]`);
|
||||
const infobulles = () => [...document.querySelectorAll('[role="tooltip"]')];
|
||||
|
||||
// L'infobulle unique, ouverte sur le terme : visible, au texte de la clé,
|
||||
// désignée par l'aria-describedby du terme et de nul autre.
|
||||
function exigerInfobulle(ancre, cle) {
|
||||
const bulles = infobulles();
|
||||
expect(bulles.length).toBe(1);
|
||||
const [bulle] = bulles;
|
||||
expect(bulle.hidden, cle).toBe(false);
|
||||
expect(getComputedStyle(bulle).visibility, cle).toBe('visible');
|
||||
expect(bulle.textContent.trim(), cle).toBe(t(cle));
|
||||
expect(ancre.getAttribute('aria-describedby'), cle).toBe(bulle.id);
|
||||
expect(document.querySelectorAll(`[aria-describedby~="${bulle.id}"]`).length, cle).toBe(1);
|
||||
const b = bulle.getBoundingClientRect();
|
||||
const a = ancre.getBoundingClientRect();
|
||||
expect(b.width).toBeGreaterThan(0);
|
||||
expect(b.left < a.right && a.left < b.right && b.top < a.bottom && a.top < b.bottom, `${cle} sur son terme`).toBe(false);
|
||||
}
|
||||
|
||||
// Chaque terme de la ligne, survolé puis focalisé, ouvre l'infobulle sur sa
|
||||
// clé ; la sortie du pointeur et Échap la ferment, le focus reste au terme.
|
||||
async function eprouverTermes(ligne, cas) {
|
||||
expect(cas.length).toBeGreaterThan(0);
|
||||
await pointer('bouger', 0, 0);
|
||||
for (const [nom, cle] of cas) {
|
||||
const ancre = terme(ligne, nom);
|
||||
ancre.scrollIntoView({ block: 'center' });
|
||||
const { x, y } = await centre(ancre);
|
||||
await pointer('bouger', x, y);
|
||||
await vi.waitFor(() => exigerInfobulle(ancre, cle));
|
||||
await pointer('bouger', 0, 0);
|
||||
await vi.waitFor(() => expect(infobulles()[0].hidden).toBe(true));
|
||||
expect(ancre.tabIndex).toBe(0);
|
||||
ancre.focus();
|
||||
await vi.waitFor(() => exigerInfobulle(ancre, cle));
|
||||
await touche('Escape');
|
||||
await vi.waitFor(() => expect(infobulles()[0].hidden).toBe(true));
|
||||
expect(document.activeElement).toBe(ancre);
|
||||
ancre.blur();
|
||||
}
|
||||
}
|
||||
|
||||
describe('les mesures du retenu dans la liste (§ 5.4, § 8.1, § 8.9)', () => {
|
||||
// Les mesures du retenu, par le moteur sollicité une seconde fois (§ 19.1).
|
||||
function mesuresDe(charge) {
|
||||
const instance = normaliser(configurationDepuisCharge(charge));
|
||||
return mesurer(instance, indexerPlan(instance, planDepuisFichier(charge.retenu)));
|
||||
}
|
||||
|
||||
const etat = (charge) => serialiser(charge, { revision: 1, produitVersion: VERSION.affichee });
|
||||
const TIRET = '\u{2014}';
|
||||
const terme = (ligne, colonne) => ligne.querySelector(`[data-terme="${colonne}"]`);
|
||||
const infobulles = () => [...document.querySelectorAll('[role="tooltip"]')];
|
||||
|
||||
// L'infobulle unique, ouverte sur le terme : visible, au texte de la clé,
|
||||
// désignée par l'aria-describedby du terme et de nul autre.
|
||||
function exigerInfobulle(ancre, cle) {
|
||||
const bulles = infobulles();
|
||||
expect(bulles.length).toBe(1);
|
||||
const [bulle] = bulles;
|
||||
expect(bulle.hidden, cle).toBe(false);
|
||||
expect(getComputedStyle(bulle).visibility, cle).toBe('visible');
|
||||
expect(bulle.textContent.trim(), cle).toBe(t(cle));
|
||||
expect(ancre.getAttribute('aria-describedby'), cle).toBe(bulle.id);
|
||||
expect(document.querySelectorAll(`[aria-describedby~="${bulle.id}"]`).length, cle).toBe(1);
|
||||
const b = bulle.getBoundingClientRect();
|
||||
const a = ancre.getBoundingClientRect();
|
||||
expect(b.width).toBeGreaterThan(0);
|
||||
expect(b.left < a.right && a.left < b.right && b.top < a.bottom && a.top < b.bottom, `${cle} sur son terme`).toBe(false);
|
||||
}
|
||||
|
||||
test('un événement retenu : ses collisions cumulées et |1000 \u{2212} Σ r(p)| ; sans retenu : « — » ; rien d’écrit', async () => {
|
||||
const charge = chargeRetenue();
|
||||
const mesures = mesuresDe(charge);
|
||||
|
|
@ -1151,28 +1175,10 @@ describe('les mesures du retenu dans la liste (§ 5.4, § 8.1, § 8.9)', () => {
|
|||
const m = await monter({ fichiers: [fichier(`retenue${SUFFIXES.etat}`, etat(chargeRetenue()))] });
|
||||
await vi.waitFor(() => expect(lignes(m).length).toBe(1));
|
||||
const retenue = ligneDe(m, 'retenue');
|
||||
const cas = [
|
||||
await eprouverTermes(retenue, [
|
||||
['collisionsCumulees', 'accueil.explication.collisionsCumulees'],
|
||||
['diffRedondance', 'accueil.explication.diffRedondance'],
|
||||
];
|
||||
expect(cas.length).toBeGreaterThan(0);
|
||||
await pointer('bouger', 0, 0);
|
||||
for (const [nom, cle] of cas) {
|
||||
const ancre = terme(retenue, nom);
|
||||
ancre.scrollIntoView({ block: 'center' });
|
||||
const { x, y } = await centre(ancre);
|
||||
await pointer('bouger', x, y);
|
||||
await vi.waitFor(() => exigerInfobulle(ancre, cle));
|
||||
await pointer('bouger', 0, 0);
|
||||
await vi.waitFor(() => expect(infobulles()[0].hidden).toBe(true));
|
||||
expect(ancre.tabIndex).toBe(0);
|
||||
ancre.focus();
|
||||
await vi.waitFor(() => exigerInfobulle(ancre, cle));
|
||||
await touche('Escape');
|
||||
await vi.waitFor(() => expect(infobulles()[0].hidden).toBe(true));
|
||||
expect(document.activeElement).toBe(ancre);
|
||||
ancre.blur();
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
test('la cible changée dans les propositions, puis la liste : la différence se lit contre la nouvelle cible', async () => {
|
||||
|
|
@ -1197,3 +1203,126 @@ describe('les mesures du retenu dans la liste (§ 5.4, § 8.1, § 8.9)', () => {
|
|||
expect(colonne(ligneDe(m, 'retenue'), 'collisionsCumulees')).toBe(format.nombre(mesuresDe(charge).collisionsCumulees));
|
||||
});
|
||||
});
|
||||
|
||||
// --- La meilleure proposition du classement (§ 5.4, § 5.7, § 8.1) ------------------
|
||||
|
||||
describe('la meilleure proposition du classement dans la liste (§ 5.4, § 5.7, § 8.1)', () => {
|
||||
// Trois propositions du moteur sur la petite démonstration au conflit
|
||||
// inévitable, sous l'identifiant d'événement id ; retenir : une autre que
|
||||
// la première du classement est retenue.
|
||||
function chargeClassee({ id = 'evt-classee', retenir = true } = {}) {
|
||||
let charge = analyser(texteLivre('petite-conflit')).charge;
|
||||
charge.evenement.id = id;
|
||||
const configuration = configurationDepuisCharge(charge);
|
||||
const propositions = rechercher(configuration, { graine: 6, nombre: 3, arret: 300 });
|
||||
charge = appliquerCommande('enregistrerGeneration', charge, { propositions, configuration, produitVersion: VERSION.affichee }).charge;
|
||||
if (!retenir) return charge;
|
||||
const [premiere] = evaluer(charge).propositions;
|
||||
const autre = charge.propositions.find((proposition) => proposition.id !== premiere.id);
|
||||
return appliquerCommande('retenirProposition', charge, { id: autre.id }).charge;
|
||||
}
|
||||
|
||||
const blocDe = (ligne) => ligne.querySelector('[data-meilleure]');
|
||||
// Ce que le bloc d'une ligne, ou celui de la génération, écrit.
|
||||
const lu = (bloc) => ({
|
||||
id: bloc.querySelector('[data-meilleure-id]')?.textContent ?? null,
|
||||
collisions: bloc.querySelector('[data-meilleure-collisions]')?.textContent ?? null,
|
||||
});
|
||||
const blocGeneration = (m) => dans(m, '[data-section="generation"] [data-meilleure]');
|
||||
|
||||
test('un événement classé : le rang 1, son identifiant et ses collisions cumulées, à part du retenu ; sans proposition, « — » ; rien d’écrit', async () => {
|
||||
const charge = chargeClassee();
|
||||
const table = evaluer(charge);
|
||||
const [premiere] = table.propositions;
|
||||
expect(premiere.statut).toBe('comparable');
|
||||
expect(premiere.id).not.toBe(charge.retenu.proposition);
|
||||
const m = await monter({
|
||||
fichiers: [fichier(`classee${SUFFIXES.etat}`, etat(charge)), fichier(`club${SUFFIXES.etat}`, texteRenomme('Club des Merles'))],
|
||||
});
|
||||
await vi.waitFor(() => expect(lignes(m).length).toBe(2));
|
||||
const avant = ecritures(m);
|
||||
const classee = ligneDe(m, 'classee');
|
||||
const bloc = blocDe(classee);
|
||||
expect(lu(bloc)).toEqual({ id: format.nombre(premiere.id), collisions: format.nombre(premiere.mesures.collisionsCumulees) });
|
||||
expect(colonne(classee, 'collisionsCumulees')).toBe(format.nombre(table.retenu.mesures.collisionsCumulees));
|
||||
expect(terme(classee, 'meilleure').textContent).toBe(t('generation.meilleure.titre'));
|
||||
expect(bloc.querySelector('dt').textContent).toBe(t('generation.meilleure.proposition'));
|
||||
expect(terme(classee, 'meilleureCollisions').textContent).toBe(t('propositions.colonne.collisionsCumulees'));
|
||||
expect(bloc.getAttribute('role')).toBe('group');
|
||||
expect(document.getElementById(bloc.getAttribute('aria-labelledby'))).toBe(terme(classee, 'meilleure'));
|
||||
for (const valeur of ['[data-meilleure-id]', '[data-meilleure-collisions]']) {
|
||||
expect(visible(bloc.querySelector(valeur)), valeur).toBe(true);
|
||||
expect(atteint(bloc.querySelector(valeur)), valeur).toBe(true);
|
||||
}
|
||||
expect(lu(blocDe(ligneDe(m, 'club')))).toEqual({ id: TIRET, collisions: TIRET });
|
||||
expect(ecritures(m)).toBe(avant);
|
||||
});
|
||||
|
||||
test('le titre et l’intitulé des collisions s’expliquent : le survol puis le focus ouvrent l’infobulle, Échap et la sortie la ferment', async () => {
|
||||
const m = await monter({ fichiers: [fichier(`classee${SUFFIXES.etat}`, etat(chargeClassee()))] });
|
||||
await vi.waitFor(() => expect(lignes(m).length).toBe(1));
|
||||
await eprouverTermes(ligneDe(m, 'classee'), [
|
||||
['meilleure', 'accueil.explication.meilleure'],
|
||||
['meilleureCollisions', 'propositions.explication.collisionsCumulees'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('la ligne écrit ce que le bloc de la génération écrit du même événement, sous les mêmes libellés, et de nouveau au retour', async () => {
|
||||
const m = await monter({ fichiers: [fichier(`classee${SUFFIXES.etat}`, etat(chargeClassee()))] });
|
||||
await vi.waitFor(() => expect(lignes(m).length).toBe(1));
|
||||
const ligne = ligneDe(m, 'classee');
|
||||
const deLaListe = lu(blocDe(ligne));
|
||||
expect(deLaListe.id).toMatch(/\d/);
|
||||
const libelles = [terme(ligne, 'meilleure'), blocDe(ligne).querySelector('dt'), terme(ligne, 'meilleureCollisions')].map(
|
||||
(element) => element.textContent,
|
||||
);
|
||||
await ouvrirLigne(m, 'classee');
|
||||
await attendreEvenement(m);
|
||||
m.ui.allerA('generation');
|
||||
await vi.waitFor(() => expect(blocGeneration(m)?.querySelector('[data-meilleure-id]')).not.toBeNull());
|
||||
const bloc = blocGeneration(m);
|
||||
expect(lu(bloc)).toEqual(deLaListe);
|
||||
expect(
|
||||
[bloc.querySelector('h3'), bloc.querySelector('[data-meilleure-libelle-id]'), bloc.querySelector('[data-terme]')].map(
|
||||
(element) => element.textContent,
|
||||
),
|
||||
).toEqual(libelles);
|
||||
await revenirALaListe(m, 1);
|
||||
await vi.waitFor(() => expect(lu(blocDe(ligneDe(m, 'classee')))).toEqual(deLaListe));
|
||||
});
|
||||
|
||||
test('des propositions toutes hors du classement : « — » à la liste, là où le bloc de la génération le dit en mots', async () => {
|
||||
const classee = chargeClassee({ retenir: false });
|
||||
const charge = appliquerCommande('exclureParticipant', classee, { id: classee.participants[0].id }).charge;
|
||||
const statuts = evaluer(charge).propositions.map(({ statut }) => statut);
|
||||
expect(statuts.length).toBe(3);
|
||||
expect(statuts.every((statut) => statut !== 'comparable')).toBe(true);
|
||||
const m = await monter({ fichiers: [fichier(`horsclassement${SUFFIXES.etat}`, etat(charge))] });
|
||||
await vi.waitFor(() => expect(lignes(m).length).toBe(1));
|
||||
expect(lu(blocDe(ligneDe(m, 'horsclassement')))).toEqual({ id: TIRET, collisions: TIRET });
|
||||
await ouvrirLigne(m, 'horsclassement');
|
||||
await attendreEvenement(m);
|
||||
m.ui.allerA('generation');
|
||||
await vi.waitFor(() => expect(blocGeneration(m)?.querySelector('[data-meilleure-vide]')).not.toBeNull());
|
||||
});
|
||||
|
||||
test('à 1366 × 700, six événements classés : la page ne défile pas en largeur, chaque bloc tient dans la sienne', async () => {
|
||||
const avant = { largeur: innerWidth, hauteur: innerHeight };
|
||||
await fixerCadre(1366, 700);
|
||||
try {
|
||||
const bases = ['un', 'deux', 'trois', 'quatre', 'cinq', 'six'];
|
||||
const m = await monter({ fichiers: bases.map((base) => fichier(`${base}${SUFFIXES.etat}`, etat(chargeClassee({ id: `evt-${base}` })))) });
|
||||
await vi.waitFor(() => expect(lignes(m).length).toBe(bases.length));
|
||||
const blocs = lignes(m).map(blocDe);
|
||||
expect(blocs.length).toBe(bases.length);
|
||||
expect(document.documentElement.scrollWidth).toBeLessThanOrEqual(innerWidth);
|
||||
for (const bloc of blocs) {
|
||||
expect(lu(bloc).id).toMatch(/\d/);
|
||||
expect(bloc.scrollWidth).toBeLessThanOrEqual(bloc.clientWidth);
|
||||
expect(bloc.getBoundingClientRect().right).toBeLessThanOrEqual(innerWidth);
|
||||
}
|
||||
} finally {
|
||||
await fixerCadre(avant.largeur, avant.hauteur);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,8 +6,12 @@
|
|||
// travail, dans l'ordre de la façade, que désigne data-evenement. Chaque
|
||||
// ligne porte la commande qui l'ouvre en lecture — son nom —, ses cinq
|
||||
// autres informations et les deux mesures de son retenu (COLONNES_DU_RETENU)
|
||||
// sous leur intitulé, chacune dans son propre nœud (data-colonne), ses
|
||||
// signalements traduits — les bases qu'un doublon
|
||||
// sous leur intitulé, chacune dans son propre nœud (data-colonne), la
|
||||
// meilleure proposition de son classement (MEILLEURE) — un groupe
|
||||
// data-meilleure sous le titre et les libellés du bloc de la génération,
|
||||
// l'identifiant dans data-meilleure-id, les collisions cumulées dans
|
||||
// data-meilleure-collisions, « — » l'un et l'autre sans proposition
|
||||
// classée —, ses signalements traduits — les bases qu'un doublon
|
||||
// nomme suivent son texte, chacune dans son propre nœud (data-doublon) —,
|
||||
// le refus de son ouverture, et ses opérations. Un dossier sans événement n'affiche pas de liste vide :
|
||||
// la création et les démonstrations de l'accueil en tiennent lieu.
|
||||
|
|
@ -16,8 +20,10 @@
|
|||
// l'accueil ; refusOuverture : { base, refus } du dernier échec, ou null ;
|
||||
// apresOperation() : à appeler quand une opération a changé le dossier.
|
||||
//
|
||||
// L'intitulé de chaque mesure du retenu s'explique dans l'infobulle de la
|
||||
// liste, une seule (Infobulle, primitive) : son terme (data-terme) se
|
||||
// L'intitulé de chaque mesure du retenu, le titre de la meilleure
|
||||
// proposition et l'intitulé de ses collisions s'expliquent dans
|
||||
// l'infobulle de la liste, une seule (Infobulle, primitive) : chaque terme
|
||||
// (data-terme, que pose le fragment termeExplique) se
|
||||
// focalise au clavier, son survol ou son focus ouvre l'explication, le
|
||||
// quitter la referme quand elle est encore la sienne, Échap aussi. Le
|
||||
// terme expliqué porte aria-describedby vers elle, et lui seul.
|
||||
|
|
@ -25,7 +31,7 @@
|
|||
import Infobulle from '../primitives/Infobulle.svelte';
|
||||
import Refus from '../primitives/Refus.svelte';
|
||||
import Operations from './Operations.svelte';
|
||||
import { COLONNES_DU_RETENU, ligneAffichee } from './modele.js';
|
||||
import { COLONNES_DU_RETENU, MEILLEURE, ligneAffichee } from './modele.js';
|
||||
|
||||
let { lignes, ouvrir, refusOuverture, apresOperation } = $props();
|
||||
|
||||
|
|
@ -49,10 +55,32 @@
|
|||
const decrit = (terme) => (expliquee?.terme === terme ? idExplication : undefined);
|
||||
</script>
|
||||
|
||||
<!-- Un terme expliqué : nom est son data-terme, cle son intitulé,
|
||||
explication la clé que l'infobulle montre ; id, celui qu'un groupe
|
||||
nomme par aria-labelledby, ou undefined. -->
|
||||
{#snippet termeExplique(base, nom, cle, explication, id)}
|
||||
{@const terme = `${base}:${nom}`}
|
||||
<!-- L'intitulé est un terme, non une commande : le focus ne sert qu'à
|
||||
montrer au clavier l'explication que le pointeur montre au survol. -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_static_element_interactions -->
|
||||
<span
|
||||
{id}
|
||||
class="terme"
|
||||
data-terme={nom}
|
||||
tabindex="0"
|
||||
aria-describedby={decrit(terme)}
|
||||
onmouseenter={(evenement) => expliquer(evenement, terme, explication)}
|
||||
onmouseleave={() => taire(terme)}
|
||||
onfocus={(evenement) => expliquer(evenement, terme, explication)}
|
||||
onblur={() => taire(terme)}>{t(cle)}</span
|
||||
>
|
||||
{/snippet}
|
||||
|
||||
{#if lignes.length > 0}
|
||||
<ul class="liste">
|
||||
{#each lignes as ligne (ligne.base)}
|
||||
{#each lignes as ligne, position (ligne.base)}
|
||||
{@const affichee = ligneAffichee(ligne, format)}
|
||||
{@const idMeilleure = `${uid}-meilleure-${position}`}
|
||||
<li class="ligne" data-evenement={ligne.base}>
|
||||
<button type="button" class="ouvrir" data-commande="ouvrir" onclick={() => ouvrir(ligne)}
|
||||
><span data-colonne="nom">{affichee.nom}</span></button
|
||||
|
|
@ -75,24 +103,8 @@
|
|||
<dd data-colonne="modification">{affichee.modification}</dd>
|
||||
</div>
|
||||
{#each COLONNES_DU_RETENU as { colonne, cle, explication } (colonne)}
|
||||
{@const terme = `${ligne.base}:${colonne}`}
|
||||
<div>
|
||||
<dt>
|
||||
<!-- L'intitulé est un terme, non une commande : le focus ne
|
||||
sert qu'à montrer au clavier l'explication que le
|
||||
pointeur montre au survol. -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_static_element_interactions -->
|
||||
<span
|
||||
class="terme"
|
||||
data-terme={colonne}
|
||||
tabindex="0"
|
||||
aria-describedby={decrit(terme)}
|
||||
onmouseenter={(evenement) => expliquer(evenement, terme, explication)}
|
||||
onmouseleave={() => taire(terme)}
|
||||
onfocus={(evenement) => expliquer(evenement, terme, explication)}
|
||||
onblur={() => taire(terme)}>{t(cle)}</span
|
||||
>
|
||||
</dt>
|
||||
<dt>{@render termeExplique(ligne.base, colonne, cle, explication, undefined)}</dt>
|
||||
<dd data-colonne={colonne}>{affichee[colonne]}</dd>
|
||||
</div>
|
||||
{/each}
|
||||
|
|
@ -101,6 +113,29 @@
|
|||
<dd data-colonne="chemin">{affichee.chemin}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="meilleure" role="group" data-meilleure aria-labelledby={idMeilleure}>
|
||||
<span class="titre-meilleure"
|
||||
>{@render termeExplique(ligne.base, 'meilleure', MEILLEURE.titre, MEILLEURE.explication, idMeilleure)}</span
|
||||
>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>{t(MEILLEURE.proposition)}</dt>
|
||||
<dd data-meilleure-id>{affichee.meilleure.id}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>
|
||||
{@render termeExplique(
|
||||
ligne.base,
|
||||
'meilleureCollisions',
|
||||
MEILLEURE.collisions,
|
||||
MEILLEURE.explicationCollisions,
|
||||
undefined,
|
||||
)}
|
||||
</dt>
|
||||
<dd data-meilleure-collisions>{affichee.meilleure.collisions}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
{#each affichee.signalements as { cle, details, noms }, rang (rang)}
|
||||
<div class="signalement" data-signalement={cle}>
|
||||
<p>{t(cle, details)}</p>
|
||||
|
|
@ -185,6 +220,41 @@
|
|||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* La meilleure proposition du classement : un groupe encadré d'un filet,
|
||||
son titre en gras, puis chaque libellé suivi de sa valeur, en gras,
|
||||
comme dans le bloc de la génération. */
|
||||
.meilleure {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: var(--espace-1) var(--espace-4);
|
||||
padding: var(--espace-1) var(--espace-2);
|
||||
border: 1px solid var(--couleur-page-filet);
|
||||
border-radius: var(--rayon-1);
|
||||
}
|
||||
|
||||
.titre-meilleure {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.meilleure dl {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--espace-1) var(--espace-4);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.meilleure dl > div {
|
||||
display: flex;
|
||||
gap: var(--espace-2);
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.meilleure dd {
|
||||
font-weight: bold;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Un intitulé qui s'explique se signale par un soulignement pointillé ;
|
||||
focalisé, par le contour du focus. */
|
||||
.terme {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,14 @@
|
|||
// Le modèle de l'accueil, pur (§ 8.1, § 8.6, § 8.8) : ce que les composants
|
||||
// de la liste des événements affichent, sans arithmétique ni texte. Une
|
||||
// ligne de la liste (LigneListe de la façade, A3) devient ses six
|
||||
// informations et les deux mesures de son retenu, mises en forme, et ses
|
||||
// signalements ; une question du dossier
|
||||
// informations, les deux mesures de son retenu et la meilleure proposition
|
||||
// de son classement, mises en forme, et ses signalements ; une question du
|
||||
// dossier
|
||||
// de travail, ses réponses ; un état illisible, ses secours ; un refus de la
|
||||
// façade, le refus affiché sur la liste. Chaque clé rendue est écrite ici en
|
||||
// entier : aucun composant ne compose une clé.
|
||||
import { COLONNE_ANNONCEE } from '../generation/meilleure.js';
|
||||
import { COLONNES } from '../propositions/modele.js';
|
||||
|
||||
// Les états du plan (§ 9) et la clé de leur nom, dans l'ordre du § 9.
|
||||
const CLES_ETATS = new Map([
|
||||
|
|
@ -33,6 +36,25 @@ export const COLONNES_DU_RETENU = Object.freeze(
|
|||
),
|
||||
);
|
||||
|
||||
// La colonne du tableau des propositions que le bloc de la génération
|
||||
// annonce : son intitulé et son explication.
|
||||
const COLONNE_MEILLEURE = COLONNES.find(({ nom }) => nom === COLONNE_ANNONCEE);
|
||||
|
||||
/**
|
||||
* Les clés du bloc de la meilleure proposition du classement qu'une ligne
|
||||
* montre (§ 5.4, § 5.7), celles du bloc de la génération : titre,
|
||||
* explication du titre — ce que désigne le rang 1, et quand le bloc écrit
|
||||
* « — » —, libellé de l'identifiant, intitulé des collisions cumulées et
|
||||
* leur explication, celles de la colonne du tableau des propositions.
|
||||
*/
|
||||
export const MEILLEURE = Object.freeze({
|
||||
titre: 'generation.meilleure.titre',
|
||||
explication: 'accueil.explication.meilleure',
|
||||
proposition: 'generation.meilleure.proposition',
|
||||
collisions: COLONNE_MEILLEURE.cle,
|
||||
explicationCollisions: COLONNE_MEILLEURE.explication,
|
||||
});
|
||||
|
||||
// La clé de la valeur absente, celle de la mise en forme.
|
||||
const CLE_VIDE = 'format.vide';
|
||||
|
||||
|
|
@ -108,8 +130,9 @@ export function signalementsDe(ligne) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Les six informations d'une ligne (§ 8.1), et les deux mesures de son
|
||||
* retenu, mises en forme, et ses signalements. nom : celui inscrit dans le fichier, ou la base quand l'état
|
||||
* Les six informations d'une ligne (§ 8.1), les deux mesures de son retenu
|
||||
* et la meilleure proposition de son classement, mises en forme, et ses
|
||||
* signalements. nom : celui inscrit dans le fichier, ou la base quand l'état
|
||||
* ne se lit pas ; date, participants et dernière modification par format,
|
||||
* la valeur absente quand ils manquent — la dernière modification est
|
||||
* l'horodatage de la dernière entrée du journal, sur l'horloge de
|
||||
|
|
@ -117,13 +140,17 @@ export function signalementsDe(ligne) {
|
|||
* clé de son nom, que le composant traduit ; chemin : tel que la façade
|
||||
* l'affiche, en entier ; collisionsCumulees et diffRedondance : celles que
|
||||
* la façade rend dans mesuresDuRetenu, par format, la valeur absente sans
|
||||
* retenu mesurable — la différence se lit telle quelle, jamais refaite.
|
||||
* retenu mesurable — la différence se lit telle quelle, jamais refaite ;
|
||||
* meilleure : l'identifiant et les collisions cumulées que la façade rend
|
||||
* dans meilleureDuClassement, par format, la valeur absente l'un et l'autre
|
||||
* sans proposition classée.
|
||||
*
|
||||
* @param {Object} ligne LigneListe (A3)
|
||||
* @param {ReturnType<import('../../application/format.js').creerFormat>} format
|
||||
* @returns {{nom: string, date: string, etat: string, participants: string,
|
||||
* modification: string, chemin: string, collisionsCumulees: string,
|
||||
* diffRedondance: string, signalements: Array<{cle: string, details: Object, noms: string[]}>}}
|
||||
* diffRedondance: string, meilleure: {id: string, collisions: string},
|
||||
* signalements: Array<{cle: string, details: Object, noms: string[]}>}}
|
||||
*/
|
||||
export function ligneAffichee(ligne, format) {
|
||||
return {
|
||||
|
|
@ -135,6 +162,10 @@ export function ligneAffichee(ligne, format) {
|
|||
chemin: ligne.chemin,
|
||||
collisionsCumulees: format.nombre(ligne.mesuresDuRetenu?.collisionsCumulees ?? null),
|
||||
diffRedondance: format.nombre(ligne.mesuresDuRetenu?.diffRedondance ?? null),
|
||||
meilleure: {
|
||||
id: format.nombre(ligne.meilleureDuClassement?.id ?? null),
|
||||
collisions: format.nombre(ligne.meilleureDuClassement?.collisionsCumulees ?? null),
|
||||
},
|
||||
signalements: signalementsDe(ligne),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,13 +9,22 @@
|
|||
// traduction. Les noms d'épreuve sont inventés.
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, test } from '../../../test/lanceur.js';
|
||||
import { appliquerCommande } from '../../application/commandes.js';
|
||||
import { resumeDuMeilleur } from '../../application/evaluation.js';
|
||||
import { creerFormat } from '../../application/format.js';
|
||||
import { TABLES_APPLICATION } from '../../application/libelles.js';
|
||||
import { creerTraduction } from '../../application/traduction.js';
|
||||
import { texteLivre } from '../../demo/livrees.js';
|
||||
import { rechercher } from '../../moteur/recherche.js';
|
||||
import { analyser, configurationDepuisCharge } from '../../stockage/document.js';
|
||||
import { VERSION } from '../../version.genere.js';
|
||||
import { COLONNE_ANNONCEE, meilleureProposition } from '../generation/meilleure.js';
|
||||
import { TABLES_INTERFACE } from '../libelles.js';
|
||||
import { COLONNES, texteCellule } from '../propositions/modele.js';
|
||||
import {
|
||||
COLONNES_DU_RETENU,
|
||||
ETATS,
|
||||
MEILLEURE,
|
||||
SAISIES,
|
||||
champDuRefus,
|
||||
cleDEtat,
|
||||
|
|
@ -51,6 +60,7 @@ function ligne(champs = {}) {
|
|||
doublonAvec: [],
|
||||
verrou: null,
|
||||
mesuresDuRetenu: null,
|
||||
meilleureDuClassement: null,
|
||||
...champs,
|
||||
};
|
||||
}
|
||||
|
|
@ -64,6 +74,7 @@ describe('ligneAffichee : les six informations (§ 8.1)', () => {
|
|||
'date',
|
||||
'diffRedondance',
|
||||
'etat',
|
||||
'meilleure',
|
||||
'modification',
|
||||
'nom',
|
||||
'participants',
|
||||
|
|
@ -342,3 +353,58 @@ describe('ligneAffichee : les mesures du retenu (§ 5.4, § 8.1, § 8.9)', () =>
|
|||
assert.equal(t('accueil.colonne.diffRedondance'), "Diff somme d'appartenance");
|
||||
});
|
||||
});
|
||||
|
||||
describe('ligneAffichee : la meilleure proposition du classement (§ 5.4, § 5.7, § 8.1)', () => {
|
||||
const executer = (charge, nom, args) => appliquerCommande(nom, charge, args).charge;
|
||||
// La charge, une génération du moteur de plus, enregistrée par la commande.
|
||||
function generee(charge, reglages) {
|
||||
const configuration = configurationDepuisCharge(charge);
|
||||
const propositions = rechercher(configuration, reglages);
|
||||
return executer(charge, 'enregistrerGeneration', { propositions, configuration, produitVersion: VERSION.affichee });
|
||||
}
|
||||
|
||||
test("l'identifiant et les collisions cumulées que rend la façade, mis en forme ; aucun ne se calcule ici", () => {
|
||||
const affichee = ligneAffichee(ligne({ meilleureDuClassement: { id: 1234, collisionsCumulees: 5678 } }), format);
|
||||
assert.deepEqual(affichee.meilleure, { id: format.nombre(1234), collisions: format.nombre(5678) });
|
||||
});
|
||||
|
||||
test('sans proposition classée, et pour une ligne illisible, les deux valent « — »', () => {
|
||||
for (const champs of [{}, { nom: null, etat: null, participants: null, illisible: { raison: 'VIDE', chemin: null } }]) {
|
||||
assert.deepEqual(ligneAffichee(ligne(champs), format).meilleure, { id: VIDE, collisions: VIDE });
|
||||
}
|
||||
});
|
||||
|
||||
test('MEILLEURE : les libellés du bloc de la génération, son explication des collisions, et l’explication du titre, qui existent', () => {
|
||||
const colonne = COLONNES.find(({ nom }) => nom === COLONNE_ANNONCEE);
|
||||
assert.ok(Object.isFrozen(MEILLEURE));
|
||||
assert.deepEqual(MEILLEURE, {
|
||||
titre: 'generation.meilleure.titre',
|
||||
explication: 'accueil.explication.meilleure',
|
||||
proposition: 'generation.meilleure.proposition',
|
||||
collisions: colonne.cle,
|
||||
explicationCollisions: colonne.explication,
|
||||
});
|
||||
assert.equal(t(MEILLEURE.titre), 'Meilleure proposition du classement');
|
||||
assert.equal(t(MEILLEURE.collisions), 'Collisions cumulées (paire et tour)');
|
||||
for (const cle of Object.values(MEILLEURE)) assert.ok(t(cle).length > 0, cle);
|
||||
});
|
||||
|
||||
test('le texte de la ligne est celui du bloc de la génération sur la même charge, classée ou non', () => {
|
||||
const generees = generee(analyser(texteLivre('petite-conflit')).charge, { graine: 4, nombre: 3, arret: 300, historique: 1000 });
|
||||
const retenue = executer(generees, 'retenirProposition', { id: generees.propositions.at(-1).id });
|
||||
const derivees = executer(generees, 'exclureParticipant', { id: generees.participants[0].id });
|
||||
const charges = [generees, retenue, derivees, analyser(texteLivre('petite')).charge];
|
||||
let classees = 0;
|
||||
for (const charge of charges) {
|
||||
const affichee = ligneAffichee(ligne({ meilleureDuClassement: resumeDuMeilleur(charge) }), format);
|
||||
const bloc = meilleureProposition(charge);
|
||||
if (bloc === null) {
|
||||
assert.deepEqual(affichee.meilleure, { id: VIDE, collisions: VIDE });
|
||||
continue;
|
||||
}
|
||||
classees += 1;
|
||||
assert.deepEqual(affichee.meilleure, { id: format.nombre(bloc.id), collisions: texteCellule(bloc.cellule, { t, format }) });
|
||||
}
|
||||
assert.equal(classees, 2);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,8 +7,11 @@
|
|||
// Elle se lit dans le tableau des propositions, sur la table d'évaluation
|
||||
// de la charge (evaluation et tableau, propositions/modele.js) : la même
|
||||
// mesure, le même classement et la même cellule que la section des
|
||||
// propositions affiche au rang 1 ; aucune valeur ne se recalcule ici.
|
||||
// propositions affiche au rang 1 ; aucune valeur ne se recalcule ici. Le
|
||||
// rang 1 est celui de premiereDuClassement (evaluation.js), la définition
|
||||
// que la liste des événements lit aussi par resumeDuMeilleur.
|
||||
|
||||
import { premiereDuClassement } from '../../application/evaluation.js';
|
||||
import { evaluation, tableau } from '../propositions/modele.js';
|
||||
|
||||
/** La colonne du tableau que l'annonce reprend. */
|
||||
|
|
@ -29,7 +32,8 @@ export const COLONNE_ANNONCEE = 'collisionsCumulees';
|
|||
export function meilleureProposition(charge) {
|
||||
const { table } = evaluation(charge);
|
||||
if (table === null) return null;
|
||||
const [premiere] = tableau(table).lignes;
|
||||
if (premiere === undefined) return null;
|
||||
return { id: premiere.id, cellule: premiere.cellules.find(({ colonne }) => colonne === COLONNE_ANNONCEE) };
|
||||
const premiere = premiereDuClassement(table);
|
||||
if (premiere === null) return null;
|
||||
const ligne = tableau(table).lignes.find(({ id }) => id === premiere.id);
|
||||
return { id: premiere.id, cellule: ligne.cellules.find(({ colonne }) => colonne === COLONNE_ANNONCEE) };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,19 @@ export const TABLE = new Map([
|
|||
"propositions. « \u{2014} » quand aucun placement n'est retenu, ou que le retenu ne décrit plus la configuration.",
|
||||
],
|
||||
|
||||
// La meilleure proposition du classement de chaque événement (§ 5.4,
|
||||
// § 5.7) : son titre, l'identifiant et les collisions s'écrivent sous les
|
||||
// libellés du bloc de la génération ; l'explication du titre est propre à
|
||||
// la liste, qui l'écrit « — » quand rien n'est classé.
|
||||
[
|
||||
'accueil.explication.meilleure',
|
||||
"La proposition au rang 1 du classement de l'événement, celle que la section de la génération annonce en tête : " +
|
||||
'son identifiant et ses collisions cumulées. Le classement range les propositions comparables, retenue comprise ; ' +
|
||||
'le placement retenu se lit à part, sous ses propres intitulés. ' +
|
||||
"« \u{2014} » quand aucune proposition n'est classée : aucune proposition, ou aucune qui décrive encore la " +
|
||||
'configuration.',
|
||||
],
|
||||
|
||||
// Le nom d'un état du plan (§ 9), que rend cleDEtat (modele.js).
|
||||
['accueil.etat.brouillon', 'Brouillon'],
|
||||
['accueil.etat.propose', 'Proposé'],
|
||||
|
|
|
|||
|
|
@ -302,16 +302,18 @@ export const ECRANS = Object.freeze([
|
|||
mode: null,
|
||||
async atteindre(contexte) {
|
||||
// La principale, sa proposition retenue : la ligne montre les deux
|
||||
// mesures du retenu, et le pointeur réel sur l'intitulé de la
|
||||
// différence ouvre son infobulle.
|
||||
// mesures du retenu et la meilleure proposition du classement, et le
|
||||
// pointeur réel sur l'intitulé de la différence ouvre son infobulle.
|
||||
const principale = chargePrincipale();
|
||||
const charge = executer(principale, 'retenirProposition', { id: principale.propositions[0].id });
|
||||
const m = await contexte.monter({ fichiers: [fichierEtat(charge)] });
|
||||
const ligne = `[data-evenement="${BASE}"]`;
|
||||
await attendre('les mesures du retenu', () =>
|
||||
['collisionsCumulees', 'diffRedondance'].every((colonne) =>
|
||||
/\d/.test(m.racine.querySelector(`${ligne} [data-colonne="${colonne}"]`)?.textContent ?? ''),
|
||||
),
|
||||
await attendre('les mesures du retenu et la meilleure proposition', () =>
|
||||
[
|
||||
...['collisionsCumulees', 'diffRedondance'].map((colonne) => `[data-colonne="${colonne}"]`),
|
||||
'[data-meilleure-id]',
|
||||
'[data-meilleure-collisions]',
|
||||
].every((valeur) => /\d/.test(m.racine.querySelector(`${ligne} ${valeur}`)?.textContent ?? '')),
|
||||
);
|
||||
const { x, y } = await centre(await element(m, `${ligne} [data-terme="diffRedondance"]`));
|
||||
await pointer('bouger', x, y);
|
||||
|
|
|
|||
Loading…
Reference in a new issue