{t(\'k.titre\')} — {42}
', '', '', '{`${x} places`}{x ?? \'Aucun\'}{x && \'Plein\'}', '{x} / {x}{t('k.present')}
\n{t(`k.gabarit`)}
\n{t(`k.${a}`)}
\n{traduction.t(\"k.membre\")}
", }), 'src/interface/plan/modele.js': [ '// t(\'k.commentaire\') ne compte pas.', "export const cle = (t) => t('k.module', { n: 1 });", "export const autre = (texte) => texte('k.texte');", '', ].join('\n'), 'src/interface/plan/modele.test.js': "t('k.epreuve');\n", }; assert.deepEqual(avecArbre(fichiers, (racine) => releverClesAbsentes(racine, cles)), [ 'src/interface/A.svelte:4 clé absente « k.absente »', 'src/interface/A.svelte:7 clé absente « k.gabarit »', 'src/interface/A.svelte:9 clé absente « k.membre »', 'src/interface/plan/modele.js:2 clé absente « k.module »', ]); }); test('un arbre sans source d’interface fait échouer la garde', () => { assert.throws(() => avecArbre({ 'src/application/a.js': '' }, (r) => releverClesAbsentes(r, CLES)), /aucun fichier examiné/); }); test('des sources d’interface sans aucune clé littérale passée à t font échouer la garde', () => { const fichiers = { 'src/interface/A.svelte': "{tr('k.absente')}
\n", 'src/interface/B.svelte': composant({ script: " import { getContext } from 'svelte';\n const traduire = getContext('t');", gabarit: "{traduire('plan.x')}
\n{t(cle)}
", }), 'src/interface/c.js': "export const d = (t, n) => t(`k.${n}`);\n", }; assert.throws(() => avecArbre(fichiers, (r) => releverClesAbsentes(r, CLES)), /clés de t : aucune clé examinée/); }); }); describe('sources de l’interface : le contenu des tables de traduction (§ 5.5, § 7.6, § 7.7, § 12.8)', () => { test('aucune valeur des tables assemblées ne porte un mot refusé ni un pictogramme', () => { exigerVide(releverContenuTables([...TABLES_APPLICATION, ...TABLES_INTERFACE], localisateur(RACINE)), 'contenu des tables'); }); test('la garde relève chaque mot refusé, sans égard à la casse ni aux accents, dans une chaîne et dans une fonction', () => { const refusees = new Map([ ['a.conforme', 'Conforme'], ['a.nonConforme', 'Plan non conforme'], ['a.conformite', 'CONFORMITÉ'], ['a.ecart', ({ n }) => `Un écart de ${n}`], ['a.ecartSeul', (details, texte) => texte('a.ecart', details)], ['a.ecartType', 'Écart-type'], ['a.satisfaction', 'Taux de satisfaction'], ['a.variance', 'variance'], ['a.equite', 'Équité'], ['a.score', 'Score'], ['a.mieux', 'Peut mieux faire'], ['a.minimal', 'Manque minimal'], ['a.recuperable', 'récupérable'], ['a.evitable', 'Évitable'], ['a.emoji', ({ n }) => `${n} \u{1F512}`], ['a.copyright', '\u{00A9} Club des Merles'], ]); const admises = new Map([ ['b.itineraire', 'Écart d\u{2019}itinéraire'], ['b.itineraires', "écarts d'itinéraire maximal"], ['b.plafond', 'Écart au plafond a priori'], ['b.inevitable', 'Inévitable'], ['b.ecartee', 'Proposition écartée'], ['b.fleche', 'Tour 1 \u{2192} tour 2'], ['b.nom', 'Iris Ombrelle'], ['b.variable', ({ n }) => { const ecart = n + 1; return `Tour ${ecart}`; }], ]); assert.deepEqual(releverContenuTables([refusees, admises], () => null, []), [ "a.conforme : conforme « Conforme »", 'a.conformite : conforme « CONFORMITÉ »', 'a.copyright : pictogramme U+00A9 « © Club des Merles »', 'a.ecart : écart hors du glossaire « Un écart de »', 'a.ecartType : écart hors du glossaire « Écart-type »', 'a.ecartType : écart-type « Écart-type »', 'a.emoji : pictogramme U+1F512 « \u{1F512} »', 'a.equite : équité « Équité »', 'a.evitable : évitable « Évitable »', 'a.mieux : peut mieux faire « Peut mieux faire »', 'a.minimal : manque minimal « Manque minimal »', 'a.nonConforme : conforme « Plan non conforme »', 'a.recuperable : récupérable « récupérable »', 'a.satisfaction : satisfaction « Taux de satisfaction »', 'a.score : score « Score »', 'a.variance : variance « variance »', ]); }); test('la garde nomme le fichier et la ligne de la clé fautive, et lève sur des tables vides', () => { const fichiers = { 'src/interface/libelles/plan.js': "export const TABLE = new Map([\n ['plan.a', 'A'],\n [\n 'plan.b',\n 'Score',\n ],\n]);\n", }; const releves = avecArbre(fichiers, (racine) => releverContenuTables([new Map([['plan.a', 'A'], ['plan.b', 'Score']])], localisateur(racine), []), ); assert.deepEqual(releves, ['src/interface/libelles/plan.js:3 plan.b : score « Score »']); assert.throws(() => releverContenuTables([new Map()]), /aucune entrée examinée/); }); test('une exception nommée n’admet que sa forme, sous ses clés ; tout autre « évitable » se relève, et une exception que rien ne sert est sans objet', () => { const tables = new Map([ ['diagnostic.remede.un', '2 chaises de plus la rend évitable'], ['diagnostic.remede.deux', ({ n }) => (n === 1 ? 'une table la rend évitable' : `${n} tables la rendent évitable`)], ['diagnostic.remede.reste', 'la rend évitable, part évitable'], ['diagnostic.remede.autre', 'la place devient évitable'], ['diagnostic.titre', 'un tabouret la rend évitable'], ['plan.segment', 'la rend évitable'], ['diagnostic.remede.inevitable', 'inévitable'], ]); assert.deepEqual(releverContenuTables([tables]), [ 'diagnostic.remede.autre : évitable « la place devient évitable »', 'diagnostic.remede.reste : évitable « la rend évitable, part évitable »', 'diagnostic.titre : évitable « un tabouret la rend évitable »', 'plan.segment : évitable « la rend évitable »', ]); const sansRemede = new Map([['diagnostic.remede.un', 'ajouter une table']]); assert.deepEqual(releverContenuTables([sansRemede]), ['exception sans objet : diagnostic.remede.* « la rend(ent) évitable »']); }); }); describe('sources de l’interface : les couleurs (§ 12.7, § 13.3, § 13.5)', () => { test('aucun littéral de couleur, aucun jeton déclaré hors de jetons.css, le jeton de conflit lu par les seules règles de conflit du plan', () => { exigerVide(releverCouleurs(RACINE), 'couleurs'); }); test('chaque jeton de couleur est défini dans les deux thèmes', () => { exigerVide(releverThemes(RACINE), 'thèmes'); }); test('la garde relève stroke: #c00 et chaque littéral, et pas un jeton ni une adresse de motif', () => { const fichiers = { 'src/interface/jetons.css': JETONS_EPREUVE, 'src/interface/plan/A.svelte': composant({ gabarit: 'x
', style: ' .alerte { color: var(--couleur-conflit-voisins); }', }), 'src/interface/plan/Motif.svelte': composant({ gabarit: '', style: [ ' .trait.voisins-conflit { stroke: var(--couleur-conflit-voisins); }', ' .trait.table-conflit { stroke: var(--couleur-conflit-table); }', ' .trait { fill: var(--couleur-conflit-voisins); }', ' .conflit-voisins { fill: var(--couleur-conflit-table); }', ' .trait.table-conflit { fill: var(--couleur-conflit-voisins); }', ].join('\n'), }), }; assert.deepEqual(avecArbre(fichiers, releverCouleurs), [ "src/interface/accueil/Alerte.svelte:2 jeton de conflit hors d'une règle de conflit du plan, dans le script", "src/interface/accueil/Alerte.svelte:6 jeton de conflit hors d'une règle de conflit du plan, « .alerte »", "src/interface/plan/Motif.svelte:1 jeton de conflit hors d'une règle de conflit du plan, en ligne", "src/interface/plan/Motif.svelte:5 jeton de conflit hors d'une règle de conflit du plan, « .trait »", "src/interface/plan/Motif.svelte:6 jeton de conflit hors d'une règle de conflit du plan, « .conflit-voisins »", "src/interface/plan/Motif.svelte:7 jeton de conflit hors d'une règle de conflit du plan, « .trait.table-conflit »", ]); }); test('la garde des thèmes relève un jeton qu’un thème ne redéfinit pas', () => { const fichiers = { 'src/interface/jetons.css': [ ':root { --espace-1: 1px; }', ':root, :root[data-theme="clair"] { --couleur-sol: #fff; --couleur-gel: #00f; }', ":root[data-theme='sombre'] { --couleur-sol: #000; --couleur-fantome: #0f0; }", '', ].join('\n'), }; assert.deepEqual(avecArbre(fichiers, releverThemes), [ 'src/interface/jetons.css:2 --couleur-gel absent du thème sombre', 'src/interface/jetons.css:3 --couleur-fantome absent du thème clair', ]); }); test('un arbre sans composant ni feuille fait échouer la garde', () => { assert.throws(() => avecArbre({ 'src/interface/jetons.css': JETONS_EPREUVE }, releverCouleurs), /couleurs : aucun fichier examiné/); }); test('une feuille des jetons sans jeton de couleur fait échouer la garde des thèmes', () => { const fichiers = { 'src/interface/jetons.css': ':root, :root[data-theme="clair"] { --espace-1: 1px; }\n' }; assert.throws(() => avecArbre(fichiers, releverThemes), /thèmes : aucun jeton de couleur examiné/); }); }); describe('sources de l’interface : balisage et fenêtres du navigateur (§ 13.1, § 2.1)', () => { test('aucun balisage injecté, aucune fenêtre du navigateur, aucun beforeunload, aucun .message affiché', () => { exigerVide(releverBalisageEtFenetres(RACINE), 'balisage et fenêtres'); }); test('la garde relève {@html nom} et chaque forme refusée, et pas leurs voisines ni un commentaire', () => { const fichiers = { 'src/interface/A.svelte': composant({ script: " let nom = $state('');\n // aucun beforeunload, jamais alert(…) ni err.message", gabarit: '{@html nom}
\n{Math.round(liste.length)}
\n{liste.reduce((s, x) => s + x, 0)}
\n{liste.sort().join()}
\n{liste.filter(Boolean).length}
', }), 'src/interface/b.js': 'export const f = (l) => Math.max(...l.sort());\n', 'src/application/evaluation.js': "import { mesurer, manques } from '../moteur/indicateurs.js';\nexport { mesurer };\n", 'src/application/generation.js': [ "import { troisChiffres as chiffres } from '../moteur/indicateurs.js';", "import * as indicateurs from '../moteur/indicateurs.js';", "export { minimumAtteint } from '../moteur/indicateurs.js';", 'export const f = (x) => indicateurs.plafondsRealises(x) + indicateurs.autre(x);', "import { evaluer } from './evaluation.js';", '', ].join('\n'), 'src/application/generation.test.js': "import { mesurer } from '../moteur/indicateurs.js';\n", }; assert.deepEqual(avecArbre(fichiers, releverCablage), [ 'src/application/generation.js:1 import de troisChiffres', 'src/application/generation.js:3 import de minimumAtteint', 'src/application/generation.js:4 import de plafondsRealises', 'src/interface/plan/A.svelte:3 .toSorted(', 'src/interface/plan/A.svelte:6 Math.round', 'src/interface/plan/A.svelte:7 .reduce(', 'src/interface/plan/A.svelte:8 .sort(', ]); }); test('un arbre sans composant fait échouer la garde', () => { assert.throws(() => avecArbre({ 'src/application/a.js': '' }, releverCablage), /câblage des composants : aucun fichier examiné/); }); }); describe('sources de l’interface : les sélecteurs élagués (§ 13.3, § 14.2)', () => { test('aucun composant ne porte une règle que le compilateur élague', () => { exigerVide(releverSelecteursElagues(RACINE), 'sélecteurs élagués'); }); test('la garde relève une règle .conflit qui vise un élément sans classe dynamique, et admet class:conflit', () => { const fichiers = { 'src/interface/plan/sous/Table.svelte': composant({ gabarit: '{' '}
", }), 'src/interface/raccourcis.js': "export const R = ['Escape', 'Enter'];\nexport const lire = (e) => e.key === ' ';\n", 'src/interface/A.test.js': "if (e.key === 'Escape') f();\n", 'src/application/c.js': "export const k = (e) => e.key === 'Escape';\n", }; assert.deepEqual(avecArbre(fichiers, releverTouches), [ 'src/interface/A.svelte:2 touche "Enter"', 'src/interface/A.svelte:4 touche "Tab"', 'src/interface/plan/sous/b.js:2 touche "Escape"', 'src/interface/plan/sous/b.js:3 touche " "', 'src/interface/plan/sous/b.js:3 touche "a"', 'src/interface/plan/sous/b.js:4 touche "q"', 'src/interface/plan/sous/b.js:5 touche "ArrowLeft"', 'src/interface/plan/sous/b.js:5 touche "Maj+Tab"', 'src/interface/plan/sous/b.js:7 touche "F2"', ]); }); test('un arbre sans source d’interface, ou réduit à la table des raccourcis, fait échouer la garde', () => { assert.throws(() => avecArbre({ 'src/application/a.js': '' }, releverTouches), /touches nommées : aucun fichier examiné/); assert.throws( () => avecArbre({ 'src/interface/raccourcis.js': "export const R = ['Escape'];\n" }, releverTouches), /touches nommées : aucun fichier examiné/, ); }); });