diff --git a/src/interface/diagnostic/Diagnostic.navigateur.test.js b/src/interface/diagnostic/Diagnostic.navigateur.test.js
index d3ad40e..9059c28 100644
--- a/src/interface/diagnostic/Diagnostic.navigateur.test.js
+++ b/src/interface/diagnostic/Diagnostic.navigateur.test.js
@@ -31,9 +31,10 @@ import { VERSION } from '../../version.genere.js';
import TEXTE_TOUTES_EN_CONFLIT from '../../../test/fixtures/plan/toutes_en_conflit.gtt.json?raw';
import { fixerCadre } from '../../../test/navigateur/entrees.js';
import { monterApp } from '../../../test/navigateur/montage.js';
-import { visible } from '../../../test/navigateur/rendu.js';
+import { composantes, visible } from '../../../test/navigateur/rendu.js';
+import { teintesAppartenances } from '../appartenances.js';
import { TABLES_INTERFACE } from '../libelles.js';
-import { enonces, numeroDeCharge } from './phrases.js';
+import { cartesDiagnostic, enonces, numeroDeCharge } from './phrases.js';
const { t } = creerTraduction([...TABLES_APPLICATION, ...TABLES_INTERFACE]);
const V1 = VERSION.affichee;
@@ -131,11 +132,16 @@ const texteDe = (element) => element.textContent.replace(/\s+/g, ' ').trim();
const charge = (m) => m.seance.etat().charge;
const revision = (m) => m.seance.etat().evenement.revision;
-// Chaque énoncé rendu sous la racine : sa clé, son appartenance, son texte,
-// et son remède, null quand il n'en porte pas.
+// L'intitulé de la liste qui porte un énoncé : la liste d'une carte est
+// nommée par son titre (aria-labelledby).
+const intituleDe = (li) => document.getElementById(li.closest('ul').getAttribute('aria-labelledby'));
+
+// Chaque énoncé rendu sous la racine : sa clé, son appartenance — la donnée
+// du titre de sa carte —, son texte, et son remède, null quand il n'en porte
+// pas.
function lus(racine) {
return [...racine.querySelectorAll('[data-enonce]')].map((li) => {
- const donnee = li.querySelector('[data-donnee]');
+ const donnee = intituleDe(li).querySelector('[data-donnee]');
const remede = li.querySelector('[data-remede]');
return {
cle: li.dataset.enonce,
@@ -159,6 +165,51 @@ function attendus(chargeLue, { avecRemede }) {
}));
}
+
+// La couleur qu'un jeton donne dans le thème courant, relue sur une sonde.
+function jeton(nom) {
+ const sonde = document.createElement('span');
+ sonde.style.color = `var(${nom})`;
+ document.body.append(sonde);
+ const couleur = composantes(getComputedStyle(sonde).color);
+ sonde.remove();
+ return couleur;
+}
+
+// La carte qui porte un élément : son plus proche ancêtre peint du fond de
+// la carte, bordé d'un filet et arrondi.
+function carteDe(element) {
+ const fond = jeton('--couleur-carte-fond');
+ for (let porteur = element.parentElement; porteur !== null; porteur = porteur.parentElement) {
+ const style = getComputedStyle(porteur);
+ const peinte = composantes(style.backgroundColor).join() === fond.join();
+ if (peinte && parseFloat(style.borderTopWidth) > 0 && parseFloat(style.borderTopLeftRadius) > 0) return porteur;
+ }
+ return null;
+}
+
+// Les cartes attendues : genre, groupe et clés de chaque carte, du moteur
+// sollicité une seconde fois.
+function cartesAttendues(chargeLue) {
+ const diagnostic = diagnostiquer(configurationDepuisCharge(chargeLue));
+ const liste = enonces(diagnostic, { numeroDe: numeroDeCharge(chargeLue), tours: chargeLue.evenement.tours });
+ return cartesDiagnostic(liste).map(({ genre, groupe, enonces: dedans }) => ({ genre, groupe, cles: dedans.map(({ cle }) => cle) }));
+}
+
+// Les cartes rendues sous la racine, lues comme cartesAttendues : chaque
+// liste d'énoncés et son titre ; groupe, la donnée du titre.
+function cartesLues(racine) {
+ return [...racine.querySelectorAll('ul')]
+ .filter((ul) => ul.querySelector('[data-enonce]') !== null)
+ .map((ul) => {
+ const donnee = document.getElementById(ul.getAttribute('aria-labelledby')).querySelector('[data-donnee]');
+ return {
+ groupe: donnee === null ? null : texteDe(donnee),
+ cles: [...ul.querySelectorAll('[data-enonce]')].map((li) => li.dataset.enonce),
+ };
+ });
+}
+
// --- Les épreuves ------------------------------------------------------------------
describe('le diagnostic se lit sans rien demander (§ 2.1 étape 8, § 5.6)', () => {
@@ -312,6 +363,86 @@ describe('la configuration refusée (§ 5.6, § 5.9)', () => {
});
});
+
+describe('le diagnostic en cartes (§ 5.6, § 12.7)', () => {
+ test('toutes en conflit : la section pose une carte par appartenance, titrée de sa pastille, de la teinte de son rang ; le panneau compact titre chaque groupe de même', async () => {
+ const m = await ouvrirTexte(TEXTE_TOUTES_EN_CONFLIT, 'toutes_en_conflit');
+ const rangs = teintesAppartenances(charge(m).participants);
+ const attendues = cartesAttendues(charge(m));
+ const appartenances = attendues.filter(({ genre }) => genre === 'appartenance');
+ expect(appartenances.length).toBe(2);
+ const sansGenre = attendues.map(({ groupe, cles }) => ({ groupe, cles }));
+
+ for (const [nom, racine] of [
+ ['panneau', () => panneau(m)],
+ ['section', () => (m.ui.allerA('diagnostic'), section(m))],
+ ]) {
+ await attendre(() => expect(cartesLues(racine())).toEqual(sansGenre));
+ const listes = [...racine().querySelectorAll('ul')].filter((ul) => ul.querySelector('[data-enonce]') !== null);
+ for (const { groupe } of appartenances) {
+ const ul = listes.find((liste) => document.getElementById(liste.getAttribute('aria-labelledby')).textContent.includes(groupe));
+ const titre = document.getElementById(ul.getAttribute('aria-labelledby'));
+ expect(/^H[2-6]$/.test(titre.tagName), `${nom} ${groupe} : un intitulé`).toBe(true);
+ const donnee = titre.querySelector('[data-donnee]');
+ expect(texteDe(donnee)).toBe(groupe);
+ // La pastille : un point de la marque du rang, le nom de l'encre de la page.
+ const point = donnee.querySelector('[aria-hidden="true"]');
+ expect(point, `${nom} ${groupe} : le point de la pastille`).not.toBeNull();
+ const rang = rangs.get(groupe);
+ expect(composantes(getComputedStyle(point).backgroundColor)).toEqual(jeton(`--couleur-appartenance-${rang}-marque`));
+ expect(visible(donnee)).toBe(true);
+ }
+ if (nom === 'section') {
+ // Chaque liste a sa propre carte.
+ const cartes = listes.map(carteDe);
+ expect(cartes.every((carte) => carte !== null)).toBe(true);
+ expect(new Set(cartes).size).toBe(listes.length);
+ }
+ }
+ });
+});
+
+describe('le rang de teinte d’une carte se compte sur tous les participants, exclus compris (§ 12.7)', () => {
+ test('une personne exclue dont l’appartenance se range avant les autres décale les deux cartes aux rangs 2 et 3', async () => {
+ // La charge « toutes en conflit » et une treizième personne, exclue,
+ // d'une appartenance qui se range avant les deux autres : le moteur ne
+ // la voit pas, les collisions restent celles des deux groupes.
+ const chargeAvecExclue = analyser(TEXTE_TOUTES_EN_CONFLIT).charge;
+ chargeAvecExclue.participants.push({
+ id: 13,
+ nom: 'Varech',
+ prenom: null,
+ appartenance: 'Atelier des Alisiers',
+ courriel: null,
+ titrePressenti: null,
+ notes: null,
+ exclu: true,
+ });
+ chargeAvecExclue.prochainsIds = { ...chargeAvecExclue.prochainsIds, participant: 14 };
+ const m = await ouvrirCharge(chargeAvecExclue);
+ const attendues = cartesAttendues(charge(m)).filter(({ genre }) => genre === 'appartenance');
+ expect(attendues.map(({ groupe }) => groupe).sort()).toEqual(['Cercle des Hérons', 'Club des Merles']);
+
+ for (const [nom, racine] of [
+ ['panneau', () => panneau(m)],
+ ['section', () => (m.ui.allerA('diagnostic'), section(m))],
+ ]) {
+ await attendre(() => expect(racine().querySelectorAll('[data-donnee]').length).toBe(2));
+ for (const [groupe, rang] of [
+ ['Cercle des Hérons', 2],
+ ['Club des Merles', 3],
+ ]) {
+ const donnee = [...racine().querySelectorAll('[data-donnee]')].find((d) => texteDe(d) === groupe);
+ expect(donnee, `${nom} ${groupe} : la pastille`).not.toBeUndefined();
+ const point = donnee.querySelector('[aria-hidden="true"]');
+ expect(composantes(getComputedStyle(point).backgroundColor), `${nom} ${groupe} : la marque du rang ${rang}`).toEqual(
+ jeton(`--couleur-appartenance-${rang}-marque`),
+ );
+ }
+ }
+ });
+});
+
describe('le bandeau du plan énonce le diagnostic (§ 7.5)', () => {
// Les entrées attendues du bandeau, du moteur sollicité une seconde fois :
// la phrase, collisionPartout, puis chaque plancher de groupe.
diff --git a/src/interface/diagnostic/Diagnostic.svelte b/src/interface/diagnostic/Diagnostic.svelte
index 0db3b4d..ba0d44a 100644
--- a/src/interface/diagnostic/Diagnostic.svelte
+++ b/src/interface/diagnostic/Diagnostic.svelte
@@ -9,29 +9,40 @@
// fait que traduire. Regarder n'écrit jamais (§ 8.4) : il se lit en
// lecture comme sur un plan bloqué.
//
+ // Les énoncés se regroupent par cartesDiagnostic, sans changer d'ordre :
+ // la séparation vidée de son sens, une carte par appartenance en
+ // collision, puis le reste de l'événement. Chaque groupe a son intitulé,
+ // qui nomme sa liste (aria-labelledby) : celui d'une appartenance est sa
+ // pastille, de la teinte que teintesAppartenances donne à son rang sur
+ // toute la liste des participants, exclus compris, et porte la donnée
+ // [data-donnee] ; les autres, un intertitre.
+ //
// compact : la forme du panneau latéral du plan, que SectionPlan monte
- // dans [data-panneau-diagnostic] — son titre et les énoncés seuls. Sinon, la section,
- // [data-section="diagnostic"] : chaque énoncé suivi du texte de son
- // remède et, quand une commande le réalise, de la Commande qui l'exécute
- // par la séance ; un refus de la séance — mode lecture, plan bloqué —
- // s'affiche à côté du bouton, avec son propre remède.
+ // dans [data-panneau-diagnostic] — une carte titrée, dont les groupes se
+ // suivent sous un filet, et les énoncés seuls. Sinon, la section,
+ // [data-section="diagnostic"] : une carte par groupe, chaque énoncé suivi
+ // du texte de son remède et, quand une commande le réalise, de la Commande
+ // qui l'exécute par la séance ; un refus de la séance — mode lecture, plan
+ // bloqué — s'affiche à côté du bouton, avec son propre remède.
//
// Une configuration que le moteur refuse n'a pas de diagnostic : son
// code, traduit, se pose à sa place, jamais le message de l'erreur.
- // L'appartenance d'un groupe est une donnée : elle occupe son propre
- // nœud, avant le texte.
import { getContext } from 'svelte';
import { diagnostiquerCharge } from '../../application/diagnostic.js';
+ import { teintesAppartenances } from '../appartenances.js';
+ import Carte from '../primitives/Carte.svelte';
import { actionRemede } from '../primitives/commande.js';
import Commande from '../primitives/Commande.svelte';
+ import Pastille from '../primitives/Pastille.svelte';
import Refus from '../primitives/Refus.svelte';
- import { enonces, numeroDeCharge } from './phrases.js';
+ import { cartesDiagnostic, enonces, numeroDeCharge } from './phrases.js';
let { compact = false } = $props();
const t = getContext('t');
const seance = getContext('seance');
const ui = getContext('ui') ?? null;
+ const uid = $props.id();
const charge = $derived($seance.charge);
const resultat = $derived(charge === null ? null : diagnostiquerCharge(charge));
@@ -40,53 +51,71 @@
? []
: enonces(resultat.diagnostic, { numeroDe: numeroDeCharge(charge), tours: charge.evenement.tours }),
);
+ const cartes = $derived(cartesDiagnostic(liste));
+ // Le rang de teinte de chaque appartenance, sur tous les participants.
+ const rangs = $derived(charge === null ? new Map() : teintesAppartenances(charge.participants));
+ // L'intitulé d'un groupe est un h3 dans la section, un h4 sous le titre
+ // du panneau.
+ const balise = $derived(compact ? 'h4' : 'h3');
// L'action d'un remède que réalise une commande ; null pour un remède de
// texte seul.
const actionDe = (remede) => (remede?.commande === undefined ? null : actionRemede(remede, { seance, ui }));
-{#snippet contenu()}
- {#if resultat !== null && resultat.erreur !== null}
-