357 lines
17 KiB
JavaScript
357 lines
17 KiB
JavaScript
|
|
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|||
|
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
|||
|
|
|
|||
|
|
// Lecture des jetons, simulation des dichromaties, contraste et teinte
|
|||
|
|
// réservée (§ 12.7, § 13.3, § 14.5). Les fonctions s'éprouvent d'abord sur des
|
|||
|
|
// feuilles d'épreuve écrites ici, où chaque défaut est posé exprès, puis sur
|
|||
|
|
// le vrai fichier de jetons : ses familles, ses deux thèmes, les jetons que
|
|||
|
|
// lisent les composants, la teinte du conflit.
|
|||
|
|
import assert from 'node:assert/strict';
|
|||
|
|
import { readdirSync, readFileSync } from 'node:fs';
|
|||
|
|
import { join } from 'node:path';
|
|||
|
|
import { fileURLToPath } from 'node:url';
|
|||
|
|
import { describe, test } from '../../test/lanceur.js';
|
|||
|
|
import {
|
|||
|
|
SIMULATIONS,
|
|||
|
|
contraste,
|
|||
|
|
lireBlocs,
|
|||
|
|
lireJetons,
|
|||
|
|
simuler,
|
|||
|
|
teinte,
|
|||
|
|
verifierPalette,
|
|||
|
|
verifierTeinteReservee,
|
|||
|
|
} from './palette.js';
|
|||
|
|
|
|||
|
|
const DOSSIER = fileURLToPath(new URL('.', import.meta.url));
|
|||
|
|
const JETONS = readFileSync(new URL('./jetons.css', import.meta.url), 'utf8');
|
|||
|
|
const NOMS_SIMULATIONS = ['protanopie', 'deuteranopie', 'tritanopie'];
|
|||
|
|
|
|||
|
|
// Feuille d'épreuve : le même corps de couleurs dans les deux thèmes, le
|
|||
|
|
// clair valant aussi sans attribut.
|
|||
|
|
function feuille(couleurs, sombre = couleurs) {
|
|||
|
|
const corps = (table) =>
|
|||
|
|
Object.entries(table)
|
|||
|
|
.map(([nom, valeur]) => ` ${nom}: ${valeur};`)
|
|||
|
|
.join('\n');
|
|||
|
|
return [
|
|||
|
|
':root { --espace-1: 0.25rem; }',
|
|||
|
|
`:root,\n:root[data-theme='clair'] {\n color-scheme: light;\n${corps(couleurs)}\n}`,
|
|||
|
|
`:root[data-theme="sombre"] {\n color-scheme: dark;\n${corps(sombre)}\n}`,
|
|||
|
|
].join('\n\n');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Composantes sRGB de 0 à 255 d'une couleur #rrggbb.
|
|||
|
|
function hex(valeur) {
|
|||
|
|
return [1, 3, 5].map((i) => Number.parseInt(valeur.slice(i, i + 2), 16));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const enHex = (c) => `#${c.map((v) => v.toString(16).padStart(2, '0')).join('')}`;
|
|||
|
|
|
|||
|
|
describe('lireBlocs et lireJetons', () => {
|
|||
|
|
test('lit les blocs dans leur ordre, commentaires ôtés, et les couleurs de chaque thème', () => {
|
|||
|
|
const css = [
|
|||
|
|
"/* { un commentaire n'ouvre aucun bloc } */",
|
|||
|
|
':root { --espace-1: 0.25rem; --police-interface: system-ui, sans-serif; }',
|
|||
|
|
":root, :root[data-theme='clair'] {",
|
|||
|
|
' --couleur-a: #fff;',
|
|||
|
|
' --couleur-b: #102030;',
|
|||
|
|
' --couleur-c: rgb(1, 2, 3);',
|
|||
|
|
' --couleur-d: rgb(4 5 6);',
|
|||
|
|
' --couleur-e: var(--couleur-b);',
|
|||
|
|
' --couleur-f: rgba(7, 8, 9, 0.5);',
|
|||
|
|
' --couleur-g: #10203080;',
|
|||
|
|
' --couleur-h: transparent;',
|
|||
|
|
'}',
|
|||
|
|
':root[data-theme="sombre"] { --couleur-a: #000000; }',
|
|||
|
|
].join('\n');
|
|||
|
|
const blocs = lireBlocs(css);
|
|||
|
|
assert.equal(blocs.length, 3);
|
|||
|
|
assert.deepEqual(blocs[0].selecteurs, [':root']);
|
|||
|
|
assert.deepEqual(blocs[1].selecteurs, [':root', ":root[data-theme='clair']"]);
|
|||
|
|
assert.deepEqual(blocs[2].selecteurs, [":root[data-theme='sombre']"]);
|
|||
|
|
assert.equal(blocs[0].declarations.get('--police-interface'), 'system-ui, sans-serif');
|
|||
|
|
|
|||
|
|
const clair = lireJetons(css, 'clair');
|
|||
|
|
assert.deepEqual([...clair.keys()], [
|
|||
|
|
'--couleur-a', '--couleur-b', '--couleur-c', '--couleur-d',
|
|||
|
|
'--couleur-e', '--couleur-f', '--couleur-g', '--couleur-h',
|
|||
|
|
]);
|
|||
|
|
assert.deepEqual(clair.get('--couleur-a'), [255, 255, 255, 1]);
|
|||
|
|
assert.deepEqual(clair.get('--couleur-b'), [16, 32, 48, 1]);
|
|||
|
|
assert.deepEqual(clair.get('--couleur-c'), [1, 2, 3, 1]);
|
|||
|
|
assert.deepEqual(clair.get('--couleur-d'), [4, 5, 6, 1]);
|
|||
|
|
assert.deepEqual(clair.get('--couleur-e'), [16, 32, 48, 1]);
|
|||
|
|
assert.deepEqual(clair.get('--couleur-f'), [7, 8, 9, 0.5]);
|
|||
|
|
assert.deepEqual(clair.get('--couleur-g'), [16, 32, 48, 128 / 255]);
|
|||
|
|
assert.deepEqual(clair.get('--couleur-h'), [0, 0, 0, 0]);
|
|||
|
|
assert.deepEqual([...lireJetons(css, 'sombre')], [['--couleur-a', [0, 0, 0, 1]]]);
|
|||
|
|
// Sans attribut : les seuls blocs :root, ceux que le clair partage.
|
|||
|
|
assert.deepEqual(lireJetons(css, null), clair);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('lève sur un thème absent, une couleur illisible, un renvoi sans cible et un bloc imbriqué', () => {
|
|||
|
|
const css = feuille({ '--couleur-a': '#ffffff' });
|
|||
|
|
assert.throws(() => lireJetons(css, 'contraste'), /contraste/);
|
|||
|
|
assert.throws(() => lireJetons(feuille({ '--couleur-a': 'hsl(0 0% 50%)' }), 'clair'), /--couleur-a/);
|
|||
|
|
assert.throws(() => lireJetons(feuille({ '--couleur-a': 'var(--couleur-z)' }), 'clair'), /--couleur-z/);
|
|||
|
|
assert.throws(
|
|||
|
|
() => lireJetons(feuille({ '--couleur-a': 'var(--couleur-b)', '--couleur-b': 'var(--couleur-a)' }), 'clair'),
|
|||
|
|
/--couleur-a/,
|
|||
|
|
);
|
|||
|
|
assert.throws(
|
|||
|
|
() => lireBlocs('@media (prefers-color-scheme: dark) { :root { --couleur-a: #000; } }'),
|
|||
|
|
/imbriqué/,
|
|||
|
|
);
|
|||
|
|
assert.throws(() => lireBlocs(':root { --couleur-a: #000;'), /fermé/);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('lève sur une couleur déclarée sous un autre sélecteur que :root et les deux thèmes : elle masquerait le jeton lu', () => {
|
|||
|
|
const base = feuille({ '--couleur-sol': '#eeeeee', '--couleur-focus': '#5a3fc0' });
|
|||
|
|
for (const intrus of [
|
|||
|
|
":root[data-theme='sombre'] svg { --couleur-sol: #ff8a80; --couleur-focus: #e8e8e8; }",
|
|||
|
|
'html { --couleur-sol: #ff8a80; }',
|
|||
|
|
":root[data-theme='contraste'] { --couleur-sol: #ff8a80; }",
|
|||
|
|
":root, .plan { --couleur-sol: #ff8a80; }",
|
|||
|
|
]) {
|
|||
|
|
const css = `${base}\n${intrus}`;
|
|||
|
|
for (const theme of ['clair', 'sombre', null]) {
|
|||
|
|
assert.throws(() => lireJetons(css, theme), /sélecteur/, `${theme} : ${intrus}`);
|
|||
|
|
}
|
|||
|
|
assert.throws(() => lireJetons(`${JETONS}\n${intrus}`, 'sombre'), /sélecteur/, intrus);
|
|||
|
|
}
|
|||
|
|
// Un autre sélecteur sans couleur n'est pas lu, et ne lève pas.
|
|||
|
|
assert.deepEqual(lireJetons(`${base}\n.plan { --espace-1: 1rem; }`, 'clair'), lireJetons(base, 'clair'));
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('contraste et simulations', () => {
|
|||
|
|
test('noir sur blanc vaut 21, une couleur sur elle-même 1, dans les deux sens', () => {
|
|||
|
|
const noir = [0, 0, 0, 1];
|
|||
|
|
const blanc = [255, 255, 255, 1];
|
|||
|
|
assert.ok(Math.abs(contraste(noir, blanc) - 21) < 1e-9);
|
|||
|
|
assert.equal(contraste(blanc, noir), contraste(noir, blanc));
|
|||
|
|
assert.equal(contraste([90, 120, 30, 1], [90, 120, 30, 1]), 1);
|
|||
|
|
// Valeur connue : #767676 sur blanc, 4,54:1 au sens des WCAG.
|
|||
|
|
assert.ok(Math.abs(contraste(hex('#767676'), hex('#ffffff')) - 4.54) < 0.01);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("lève sur une couleur à canal alpha partiel : le ratio d'une couleur composée mentirait", () => {
|
|||
|
|
assert.throws(() => contraste([0, 0, 0, 0.5], [255, 255, 255, 1]), /alpha/);
|
|||
|
|
assert.throws(() => contraste([0, 0, 0, 1], [255, 255, 255, 0]), /alpha/);
|
|||
|
|
assert.ok(contraste([0, 0, 0], [255, 255, 255]) > 20);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('les trois matrices sont celles de Machado, Oliveira et Fernandes, sévérité 1', () => {
|
|||
|
|
assert.deepEqual(Object.keys(SIMULATIONS).sort(), [...NOMS_SIMULATIONS].sort());
|
|||
|
|
assert.deepEqual(SIMULATIONS.protanopie[0], [0.152286, 1.052583, -0.204868]);
|
|||
|
|
assert.deepEqual(SIMULATIONS.deuteranopie[1], [0.280085, 0.672501, 0.047413]);
|
|||
|
|
assert.deepEqual(SIMULATIONS.tritanopie[2], [0.004733, 0.691367, 0.3039]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('le blanc et le noir sont des points fixes des trois matrices, à 1/255 près ; alpha intact', () => {
|
|||
|
|
assert.equal(NOMS_SIMULATIONS.length, 3);
|
|||
|
|
for (const nom of NOMS_SIMULATIONS) {
|
|||
|
|
for (const couleur of [[255, 255, 255, 1], [0, 0, 0, 1]]) {
|
|||
|
|
const simulee = simuler(couleur, nom);
|
|||
|
|
for (let i = 0; i < 3; i += 1) {
|
|||
|
|
assert.ok(Math.abs(simulee[i] - couleur[i]) <= 1, `${nom} ${couleur} → ${simulee}`);
|
|||
|
|
}
|
|||
|
|
assert.equal(simulee[3], 1);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
assert.throws(() => simuler([0, 0, 0, 1], 'achromatopsie'), /achromatopsie/);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('un gris reste gris, un rouge pur perd sa clarté sous la protanopie', () => {
|
|||
|
|
const gris = simuler([118, 118, 118, 1], 'deuteranopie');
|
|||
|
|
assert.ok(Math.abs(gris[0] - 118) <= 1 && Math.abs(gris[2] - 118) <= 1, `${gris}`);
|
|||
|
|
const rouge = [255, 0, 0, 1];
|
|||
|
|
assert.ok(contraste(simuler(rouge, 'protanopie'), [0, 0, 0, 1]) < contraste(rouge, [0, 0, 0, 1]));
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('verifierPalette', () => {
|
|||
|
|
// Cherche un rouge et un vert qui tiennent 3:1 en couleurs d'origine et
|
|||
|
|
// tombent sous 3:1 sous au moins une simulation.
|
|||
|
|
function coupleRougeVert() {
|
|||
|
|
for (let a = 0; a <= 200; a += 8) {
|
|||
|
|
for (let v = 40; v <= 200; v += 8) {
|
|||
|
|
const rouge = [255, a, a, 1];
|
|||
|
|
const vert = [0, v, 0, 1];
|
|||
|
|
if (contraste(rouge, vert) < 3) continue;
|
|||
|
|
const simule = NOMS_SIMULATIONS.some(
|
|||
|
|
(nom) => contraste(simuler(rouge, nom), simuler(vert, nom)) < 3,
|
|||
|
|
);
|
|||
|
|
if (simule) return { rouge, vert };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
test('un couple rouge-vert qui tient 3:1 en origine et tombe une fois simulé est refusé, sous simulation seulement', () => {
|
|||
|
|
const couple = coupleRougeVert();
|
|||
|
|
assert.notEqual(couple, null);
|
|||
|
|
const css = feuille({ '--couleur-rouge': enHex(couple.rouge.slice(0, 3)), '--couleur-vert': enHex(couple.vert.slice(0, 3)) });
|
|||
|
|
const fautes = verifierPalette(css, [{ jeton: '--couleur-rouge', fonds: ['--couleur-vert'], seuil: 3 }]);
|
|||
|
|
assert.ok(fautes.length > 0);
|
|||
|
|
assert.ok(fautes.every((f) => f.simulation !== 'origine'), JSON.stringify(fautes));
|
|||
|
|
assert.ok(fautes.every((f) => f.ratio < 3 && f.seuil === 3));
|
|||
|
|
assert.ok(fautes.every((f) => f.jeton === '--couleur-rouge' && f.fond === '--couleur-vert'));
|
|||
|
|
assert.deepEqual([...new Set(fautes.map((f) => f.theme))], ['clair', 'sombre']);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('une marque qui tient sur son premier fond et pas sur le second est refusée sur le second', () => {
|
|||
|
|
const css = feuille({
|
|||
|
|
'--couleur-marque': '#202020',
|
|||
|
|
'--couleur-clair': '#ffffff',
|
|||
|
|
'--couleur-sombre': '#000000',
|
|||
|
|
});
|
|||
|
|
const fautes = verifierPalette(css, [
|
|||
|
|
{ jeton: '--couleur-marque', fonds: ['--couleur-clair', '--couleur-sombre'], seuil: 3 },
|
|||
|
|
]);
|
|||
|
|
// Deux thèmes, l'origine et trois simulations : huit fautes, toutes sur le second fond.
|
|||
|
|
assert.equal(fautes.length, 8);
|
|||
|
|
assert.ok(fautes.every((f) => f.fond === '--couleur-sombre'));
|
|||
|
|
assert.deepEqual(
|
|||
|
|
fautes.map((f) => `${f.theme}:${f.simulation}`),
|
|||
|
|
['clair', 'sombre'].flatMap((t) => ['origine', ...NOMS_SIMULATIONS].map((s) => `${t}:${s}`)),
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('rend [] quand chaque entrée tient sur chaque fond ; lève sur un jeton inconnu', () => {
|
|||
|
|
const css = feuille({ '--couleur-texte': '#000000', '--couleur-fond': '#ffffff' }, { '--couleur-texte': '#ffffff', '--couleur-fond': '#000000' });
|
|||
|
|
assert.deepEqual(verifierPalette(css, [{ jeton: '--couleur-texte', fonds: ['--couleur-fond'], seuil: 4.5 }]), []);
|
|||
|
|
assert.throws(
|
|||
|
|
() => verifierPalette(css, [{ jeton: '--couleur-texte', fonds: ['--couleur-absente'], seuil: 3 }]),
|
|||
|
|
/--couleur-absente/,
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('teinte et verifierTeinteReservee (§ 12.7)', () => {
|
|||
|
|
test('la teinte HSL ne vaut que pour une couleur dont les composantes s’écartent de 26 au moins', () => {
|
|||
|
|
assert.equal(teinte([255, 0, 0, 1]), 0);
|
|||
|
|
assert.equal(teinte([0, 255, 0, 1]), 120);
|
|||
|
|
assert.equal(teinte([0, 0, 255, 1]), 240);
|
|||
|
|
assert.equal(teinte([255, 0, 128, 1]) > 329 && teinte([255, 0, 128, 1]) < 331, true);
|
|||
|
|
assert.equal(teinte([138, 125, 125, 1]), null); // écart 13 : gris
|
|||
|
|
assert.equal(teinte([125, 100, 100, 1]), null); // écart 25 : gris
|
|||
|
|
assert.notEqual(teinte([126, 100, 100, 1]), null); // écart 26 : chromatique
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('un refus de la teinte du conflit et d’une autre clarté est refusé, valeurs pourtant distinctes', () => {
|
|||
|
|
const conflit = '#c62828';
|
|||
|
|
const refus = '#5a1212';
|
|||
|
|
assert.notEqual(conflit, refus);
|
|||
|
|
assert.ok(Math.abs(teinte(hex(conflit)) - teinte(hex(refus))) < 1);
|
|||
|
|
const css = feuille({
|
|||
|
|
'--couleur-conflit': conflit,
|
|||
|
|
'--couleur-conflit-trame': '#e57373',
|
|||
|
|
'--couleur-refus': refus,
|
|||
|
|
'--couleur-gris-rose': '#8a7d7d',
|
|||
|
|
'--couleur-bleu': '#1f3a5f',
|
|||
|
|
});
|
|||
|
|
const fautes = verifierTeinteReservee(css, ['--couleur-conflit', '--couleur-conflit-trame']);
|
|||
|
|
assert.deepEqual(fautes.map((f) => `${f.theme}:${f.jeton}`), ['clair:--couleur-refus', 'sombre:--couleur-refus']);
|
|||
|
|
assert.ok(fautes.every((f) => f.ecart < 30));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('le jeton de conflit lui-même doit être chromatique ; une teinte à 30° passe, à 29° non ; le cercle se referme', () => {
|
|||
|
|
const gris = feuille({ '--couleur-conflit': '#808080', '--couleur-bleu': '#1f3a5f' });
|
|||
|
|
assert.deepEqual(verifierTeinteReservee(gris, ['--couleur-conflit']), [
|
|||
|
|
{ theme: 'clair', jeton: '--couleur-conflit', ecart: null },
|
|||
|
|
{ theme: 'sombre', jeton: '--couleur-conflit', ecart: null },
|
|||
|
|
]);
|
|||
|
|
// Conflit à 0° ; 30° et 330° passent, 29° et 331° non.
|
|||
|
|
const css = feuille({
|
|||
|
|
'--couleur-conflit': '#ff0000',
|
|||
|
|
'--couleur-a': '#ff8000', // 30,1°
|
|||
|
|
'--couleur-b': '#ff7b00', // 28,9°
|
|||
|
|
'--couleur-c': '#ff0080', // 329,9°
|
|||
|
|
'--couleur-d': '#ff0079', // 331,5°
|
|||
|
|
});
|
|||
|
|
const fautes = verifierTeinteReservee(css, ['--couleur-conflit']);
|
|||
|
|
assert.deepEqual(fautes.map((f) => `${f.theme}:${f.jeton}`), [
|
|||
|
|
'clair:--couleur-b', 'clair:--couleur-d', 'sombre:--couleur-b', 'sombre:--couleur-d',
|
|||
|
|
]);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Fichiers .svelte et .css de src/interface, sous-dossiers compris, triés.
|
|||
|
|
function sourcesDeStyle() {
|
|||
|
|
return readdirSync(DOSSIER, { recursive: true, withFileTypes: true })
|
|||
|
|
.filter((e) => e.isFile() && /\.(svelte|css)$/.test(e.name))
|
|||
|
|
.map((e) => join(e.parentPath, e.name))
|
|||
|
|
.sort();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
describe('jetons.css', () => {
|
|||
|
|
// Lue dans chaque épreuve : une feuille illisible fait échouer chacune, sans
|
|||
|
|
// empêcher les épreuves des fonctions de se lancer.
|
|||
|
|
const blocs = () => lireBlocs(JETONS);
|
|||
|
|
const declares = () => new Set(blocs().flatMap((b) => [...b.declarations.keys()]));
|
|||
|
|
const nomsDe = (prefixe) => [...declares()].filter((n) => n.startsWith(prefixe));
|
|||
|
|
|
|||
|
|
test('un fichier, toutes les familles : couleurs, polices, échelle typographique, espacements, rayons, ombres (§ 13.3)', () => {
|
|||
|
|
for (const prefixe of ['--couleur-', '--police-', '--taille-', '--espace-', '--rayon-', '--ombre-']) {
|
|||
|
|
assert.ok(nomsDe(prefixe).length > 0, `aucun jeton ${prefixe}…`);
|
|||
|
|
}
|
|||
|
|
// Une échelle a trois pas au moins, de valeurs distinctes.
|
|||
|
|
const tailles = new Set(
|
|||
|
|
blocs().flatMap((b) => [...b.declarations].filter(([n]) => n.startsWith('--taille-')).map(([, v]) => v)),
|
|||
|
|
);
|
|||
|
|
assert.ok(tailles.size >= 3, `${tailles.size} tailles distinctes`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('deux thèmes posés par data-theme, de mêmes jetons de couleur ; sans attribut, le clair ; aucune préférence du système lue', () => {
|
|||
|
|
const clair = lireJetons(JETONS, 'clair');
|
|||
|
|
const sombre = lireJetons(JETONS, 'sombre');
|
|||
|
|
assert.ok(clair.size >= 20, `${clair.size} couleurs`);
|
|||
|
|
assert.deepEqual([...sombre.keys()].sort(), [...clair.keys()].sort());
|
|||
|
|
assert.deepEqual(lireJetons(JETONS, null), clair);
|
|||
|
|
// Le thème sombre redéfinit chaque couleur à une autre valeur que le clair,
|
|||
|
|
// sauf celles que les deux thèmes partagent exprès.
|
|||
|
|
const differentes = [...clair.keys()].filter((n) => String(clair.get(n)) !== String(sombre.get(n)));
|
|||
|
|
assert.ok(differentes.length > clair.size / 2, `${differentes.length} couleurs changent`);
|
|||
|
|
assert.equal(/prefers-color-scheme/.test(JETONS.replace(/\/\*[\s\S]*?\*\//g, '')), false);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('chaque var(--…) lu par un .svelte ou un .css de src/interface est déclaré dans jetons.css', () => {
|
|||
|
|
const sources = sourcesDeStyle();
|
|||
|
|
assert.ok(sources.length >= 2, `${sources.length} sources de style`);
|
|||
|
|
const lus = [];
|
|||
|
|
for (const chemin of sources) {
|
|||
|
|
for (const [, nom] of readFileSync(chemin, 'utf8').matchAll(/var\(\s*(--[\w-]+)/g)) {
|
|||
|
|
lus.push({ chemin, nom });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
assert.ok(lus.length >= 11, `${lus.length} lectures`);
|
|||
|
|
const noms = declares();
|
|||
|
|
const absents = lus.filter((l) => !noms.has(l.nom)).map((l) => `${l.chemin} ${l.nom}`);
|
|||
|
|
assert.deepEqual(absents, []);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("les onze jetons d'App.svelte gardent leur nom", () => {
|
|||
|
|
const noms = declares();
|
|||
|
|
for (const nom of [
|
|||
|
|
'--couleur-page-fond', '--couleur-page-texte', '--couleur-bandeau-fond',
|
|||
|
|
'--couleur-bandeau-texte', '--couleur-bandeau-filet', '--police-interface',
|
|||
|
|
'--taille-texte-bandeau', '--espace-2', '--espace-3', '--espace-4', '--rayon-1',
|
|||
|
|
]) {
|
|||
|
|
assert.ok(noms.has(nom), nom);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('la teinte du conflit est réservée (§ 12.7) : chromatique, sa valeur à lui seul, à 30° de tout autre jeton chromatique', () => {
|
|||
|
|
for (const theme of ['clair', 'sombre']) {
|
|||
|
|
const jetons = lireJetons(JETONS, theme);
|
|||
|
|
const conflit = jetons.get('--couleur-conflit');
|
|||
|
|
assert.notEqual(conflit, undefined, theme);
|
|||
|
|
assert.notEqual(teinte(conflit), null, `${theme} : conflit achromatique`);
|
|||
|
|
const memeValeur = [...jetons].filter(([n, c]) => n !== '--couleur-conflit' && String(c) === String(conflit));
|
|||
|
|
assert.deepEqual(memeValeur.map(([n]) => n), [], theme);
|
|||
|
|
}
|
|||
|
|
assert.deepEqual(verifierTeinteReservee(JETONS, ['--couleur-conflit']), []);
|
|||
|
|
});
|
|||
|
|
});
|