The plan's colours carry meaning — a conflict, a reservation, a mode — so they must stay readable for every reader and in both themes. jetons.css defines the tokens for the light and dark themes, set by data-theme on the root; palette.js reads them from the stylesheet text and checks, by computation, contrast to WCAG and separation under the three dichromacies, keeping the conflict hue reserved. The theme follows the system at each start and the operator's choice lasts the session. Checked: 1709 node and 43 browser tests. --- FR --- [ADD] interface : jetons de couleur, deux thèmes, palette, marques Les couleurs du plan portent un sens — un conflit, une réservation, un mode — et doivent rester lisibles pour tout lecteur et dans les deux thèmes. jetons.css définit les jetons des thèmes clair et sombre, posés par data-theme sur la racine ; palette.js les lit dans le texte de la feuille et contrôle, par calcul, le contraste au sens des WCAG et la séparation sous les trois dichromaties, la teinte du conflit restant réservée. Le thème suit le système à chaque démarrage ; le choix de l'opérateur vaut pour la séance. Vérifié : 1709 node et 43 navigateur. Assisted-by: Claude Opus 5.5
132 lines
6 KiB
JavaScript
132 lines
6 KiB
JavaScript
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||
|
||
// Le registre des marques porteuses d'information et des textes (§ 14.5,
|
||
// § 7.5, § 7.6, § 7.7), et la palette de jetons.css validée par calcul sur
|
||
// lui (§ 12.7) : chaque entrée tient son seuil sur chacun de ses fonds, dans
|
||
// les deux thèmes, en couleurs d'origine puis sous les trois dichromaties.
|
||
import assert from 'node:assert/strict';
|
||
import { readFileSync } from 'node:fs';
|
||
import { describe, test } from '../../test/lanceur.js';
|
||
import { MARQUES, TEXTES } from './marques.js';
|
||
import { lireBlocs, lireJetons, verifierPalette } from './palette.js';
|
||
|
||
const JETONS = readFileSync(new URL('./jetons.css', import.meta.url), 'utf8');
|
||
const THEMES = ['clair', 'sombre'];
|
||
|
||
const NOMS_MARQUES = [
|
||
'cadenasFerme', 'cadenasOuvert', 'anneau', 'hachure', 'remplissagePlein',
|
||
'contourConflit', 'trameConflit', 'pastille', 'poignee', 'gel', 'miseEnEvidence',
|
||
'selection', 'cadreLecture', 'cadreEcriture', 'cadreBloque', 'focus',
|
||
'contourTable', 'contourChaise', 'echelle', 'fantome',
|
||
];
|
||
|
||
// Les remplissages d'une chaise : fond, pleine, et les deux couleurs de la
|
||
// hachure — ses traits sur le fond de la chaise.
|
||
const REMPLISSAGES_CHAISE = ['--couleur-chaise-fond', '--couleur-chaise-pleine', '--couleur-hachure'];
|
||
|
||
// Fonds minimaux des marques de désignation et du fantôme. « chaise » vaut
|
||
// chacun des remplissages d'une chaise : la marque se pose sur une chaise
|
||
// vide, pourvue ou à pourvoir.
|
||
const c = (nom) => `--couleur-${nom}`;
|
||
const FONDS_REQUIS = {
|
||
focus: [...['page-fond', 'bandeau-fond', 'sol', 'table-fond'].map(c), ...REMPLISSAGES_CHAISE],
|
||
selection: [...['page-fond', 'sol', 'table-fond'].map(c), ...REMPLISSAGES_CHAISE],
|
||
miseEnEvidence: [...['page-fond', 'sol', 'table-fond'].map(c), ...REMPLISSAGES_CHAISE],
|
||
fantome: [...['sol', 'table-fond'].map(c), ...REMPLISSAGES_CHAISE],
|
||
};
|
||
|
||
const parNom = (registre, nom) => registre.find((e) => e.nom === nom);
|
||
|
||
describe('registre des marques', () => {
|
||
test('les vingt marques du contrat, noms uniques, seuil 3, au moins un fond chacune', () => {
|
||
assert.equal(MARQUES.length, NOMS_MARQUES.length);
|
||
assert.deepEqual(MARQUES.map((m) => m.nom), NOMS_MARQUES);
|
||
assert.equal(new Set(MARQUES.map((m) => m.nom)).size, MARQUES.length);
|
||
for (const marque of MARQUES) {
|
||
assert.deepEqual(Object.keys(marque).sort(), ['fonds', 'jeton', 'nom', 'seuil'], marque.nom);
|
||
assert.equal(marque.seuil, 3, marque.nom);
|
||
assert.ok(marque.fonds.length > 0, marque.nom);
|
||
assert.equal(new Set(marque.fonds).size, marque.fonds.length, `${marque.nom} : fond en double`);
|
||
}
|
||
});
|
||
|
||
test('le registre est figé', () => {
|
||
assert.ok(Object.isFrozen(MARQUES) && Object.isFrozen(TEXTES));
|
||
assert.ok(MARQUES.every((m) => Object.isFrozen(m) && Object.isFrozen(m.fonds)));
|
||
assert.ok(TEXTES.every((m) => Object.isFrozen(m) && Object.isFrozen(m.fonds)));
|
||
});
|
||
|
||
test('le focus, la sélection, la mise en évidence et le fantôme se mesurent sur chaque fond où ils se peignent, chaque remplissage de chaise compris', () => {
|
||
for (const nom of ['focus', 'selection', 'miseEnEvidence', 'fantome']) {
|
||
const marque = parNom(MARQUES, nom);
|
||
assert.notEqual(marque, undefined, nom);
|
||
for (const fond of FONDS_REQUIS[nom]) {
|
||
assert.ok(marque.fonds.includes(fond), `${nom} : ${fond}`);
|
||
}
|
||
}
|
||
});
|
||
|
||
test('une marque posée sur une chaise se mesure sur chacun de ses remplissages', () => {
|
||
const surChaise = ['cadenasFerme', 'cadenasOuvert', 'anneau'];
|
||
for (const nom of surChaise) {
|
||
for (const fond of REMPLISSAGES_CHAISE) {
|
||
assert.ok(parNom(MARQUES, nom).fonds.includes(fond), `${nom} : ${fond}`);
|
||
}
|
||
}
|
||
assert.deepEqual(parNom(MARQUES, 'hachure').fonds, ['--couleur-chaise-fond']);
|
||
});
|
||
|
||
test('chaque jeton et chaque fond du registre existe dans les deux thèmes', () => {
|
||
const entrees = [...MARQUES, ...TEXTES];
|
||
assert.ok(entrees.length > 20);
|
||
for (const theme of THEMES) {
|
||
const jetons = lireJetons(JETONS, theme);
|
||
for (const entree of entrees) {
|
||
for (const nom of [entree.jeton, ...entree.fonds]) {
|
||
assert.ok(jetons.has(nom), `${theme} : ${entree.nom} → ${nom}`);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('registre des textes', () => {
|
||
test('page, secondaire, bandeau, refus, avis, listes du plan, pastille ; seuil 4,5', () => {
|
||
assert.deepEqual(TEXTES.map((t) => t.nom), ['page', 'secondaire', 'bandeau', 'refus', 'avis', 'listes', 'pastille']);
|
||
assert.ok(TEXTES.every((t) => t.seuil === 4.5 && t.fonds.length > 0));
|
||
assert.deepEqual(parNom(TEXTES, 'listes').fonds, ['--couleur-sol']);
|
||
assert.deepEqual(parNom(TEXTES, 'pastille').fonds, ['--couleur-pastille-fond']);
|
||
});
|
||
});
|
||
|
||
describe('palette de jetons.css validée par calcul (§ 12.7, § 14.5)', () => {
|
||
test('aucun jeton de texte ou de marque, ni aucun de leurs fonds, n’a de canal alpha', () => {
|
||
const noms = new Set([...MARQUES, ...TEXTES].flatMap((e) => [e.jeton, ...e.fonds]));
|
||
assert.ok(noms.size > 20);
|
||
const blocs = lireBlocs(JETONS);
|
||
for (const theme of THEMES) {
|
||
const selecteur = `:root[data-theme='${theme}']`;
|
||
const brutes = new Map(
|
||
blocs.filter((b) => b.selecteurs.includes(selecteur)).flatMap((b) => [...b.declarations]),
|
||
);
|
||
const jetons = lireJetons(JETONS, theme);
|
||
for (const nom of noms) {
|
||
assert.equal(jetons.get(nom)[3], 1, `${theme} : ${nom}`);
|
||
assert.doesNotMatch(
|
||
brutes.get(nom),
|
||
/rgba|hsla|transparent|#[0-9a-f]{8}\b|#[0-9a-f]{4}\b|\/\s*[\d.]+%?\s*\)/i,
|
||
`${theme} : ${nom}`,
|
||
);
|
||
}
|
||
}
|
||
});
|
||
|
||
test('chaque marque tient 3:1 sur chacun de ses fonds, dans les deux thèmes, sous les trois simulations', () => {
|
||
assert.deepEqual(verifierPalette(JETONS, MARQUES), []);
|
||
});
|
||
|
||
test('chaque texte tient 4,5:1 sur chacun de ses fonds, dans les deux thèmes, sous les trois simulations', () => {
|
||
assert.deepEqual(verifierPalette(JETONS, TEXTES), []);
|
||
});
|
||
});
|