[ADD] tests: spec coverage guards, delivery checks, spec corrections
New guards close the spec coverage gaps: only the configuration and the diagnostic read reservations in the engine, no second language in the sources, the engine and geometry never read the version, a test lives beside its module, fixtures never copy a shipped file, and the property library stays in a closed list. The delivery test pins the www output and the French language passed by the entry point. The spec names three test projects and the three shell files. Checked: 2778 node, 550 browser and 62 long tests from the index alone; the version check passes. --- FR --- [ADD] épreuves : gardes de couverture du spec, livrable, spec corrigé De nouvelles gardes ferment les manques de couverture : seules la configuration et le diagnostic lisent les réservations dans le moteur, aucun second langage dans les sources, le moteur et la géométrie ne lisent pas la version, une épreuve vit près de son module, aucune donnée d'épreuve ne copie un fichier livré, la bibliothèque de propriétés reste en liste fermée. Le livrable fige la sortie www et la langue française du point d'entrée. Le spec nomme trois projets et trois fichiers de coquille. Vérifié : 2778 node, 550 navigateur, 62 longues ; version conforme. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
ddd587da87
commit
fca74b6672
9 changed files with 931 additions and 153 deletions
|
|
@ -57,6 +57,7 @@ import { mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import path, { join, relative, resolve } from 'node:path';
|
import path, { join, relative, resolve } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { decrireSysteme } from '../electron/fichiers.js';
|
||||||
import { estSous } from '../src/stockage/dossier_travail.js';
|
import { estSous } from '../src/stockage/dossier_travail.js';
|
||||||
import { VERSION } from '../src/version.genere.js';
|
import { VERSION } from '../src/version.genere.js';
|
||||||
import { titreAvecVersion } from '../src/interface/libelles.js';
|
import { titreAvecVersion } from '../src/interface/libelles.js';
|
||||||
|
|
@ -114,21 +115,26 @@ export function environnementDEssai(env, { exe, home, temp }, chemins = path) {
|
||||||
/**
|
/**
|
||||||
* Les données applicatives que la coquille lancée sous env retiendrait et
|
* Les données applicatives que la coquille lancée sous env retiendrait et
|
||||||
* qui portent l'exécutable publié : vide quand le mode portable reste
|
* qui portent l'exécutable publié : vide quand le mode portable reste
|
||||||
* possible. La liste est celle du § 8.6 point 2, que decrireSysteme
|
* possible. La liste est celle que compose decrireSysteme
|
||||||
* (electron/fichiers.js) compose : AppData, le LOCALAPPDATA de env, le
|
* (electron/fichiers.js, § 8.6 point 2), appelée comme la coquille l'appelle,
|
||||||
* temporaire ; estSous compare comme le système. AppData prend chaque
|
* et estSous compare comme le système, selon la casse et le séparateur
|
||||||
* valeur que app.getPath peut rendre : sous Windows, l'APPDATA de env et
|
* qu'elle décrit. L'essai ne sait en propre que ce que rend app.getPath :
|
||||||
* celle de poste, dossier connu qu'aucune variable ne déplace ; sous macOS,
|
* appData prend chaque valeur qu'il peut rendre — sous Windows, l'APPDATA de
|
||||||
* Library/Application Support sous l'un et l'autre HOME ; ailleurs,
|
* env et celle du poste, dossier connu qu'aucune variable ne déplace ; sous
|
||||||
* XDG_CONFIG_HOME ou .config sous HOME. Le temporaire est TMP, puis TEMP,
|
* macOS, Library/Application Support sous l'un et l'autre HOME ; ailleurs,
|
||||||
* sous Windows, TMPDIR, puis /tmp, ailleurs. L'ordre est celui de cette
|
* XDG_CONFIG_HOME ou .config sous HOME —, decrireSysteme étant appelée une
|
||||||
* liste, sans doublon ; sans exécutable publié, rien ne le porte.
|
* fois pour chacune ; temp est TMP, puis TEMP, sous Windows, TMPDIR, puis
|
||||||
|
* /tmp, ailleurs. Un autre nom demandé à app.getPath lève : l'essai
|
||||||
|
* jugerait une liste à laquelle il manque un emplacement. Les dossiers se
|
||||||
|
* rendent dans l'ordre des appels, sans doublon ; sans exécutable publié,
|
||||||
|
* rien ne le porte.
|
||||||
* @param {Record<string, string|undefined>} env l'environnement de la coquille
|
* @param {Record<string, string|undefined>} env l'environnement de la coquille
|
||||||
* @param {Record<string, string|undefined>} poste l'environnement du poste
|
* @param {Record<string, string|undefined>} poste l'environnement du poste
|
||||||
* @param {string} plateforme process.platform
|
* @param {string} plateforme process.platform
|
||||||
|
* @param {typeof decrireSysteme} [decrire] la description du poste par la coquille
|
||||||
* @returns {string[]}
|
* @returns {string[]}
|
||||||
*/
|
*/
|
||||||
export function donneesApplicativesSurExe(env, poste, plateforme) {
|
export function donneesApplicativesSurExe(env, poste, plateforme, decrire = decrireSysteme) {
|
||||||
const executable = env.PORTABLE_EXECUTABLE_DIR || null;
|
const executable = env.PORTABLE_EXECUTABLE_DIR || null;
|
||||||
if (executable === null) return [];
|
if (executable === null) return [];
|
||||||
const chemins = plateforme === 'win32' ? path.win32 : path.posix;
|
const chemins = plateforme === 'win32' ? path.win32 : path.posix;
|
||||||
|
|
@ -140,10 +146,15 @@ export function donneesApplicativesSurExe(env, poste, plateforme) {
|
||||||
? [env.HOME, poste.HOME].map((maison) => sousMaison(maison, 'Library', 'Application Support'))
|
? [env.HOME, poste.HOME].map((maison) => sousMaison(maison, 'Library', 'Application Support'))
|
||||||
: [env.XDG_CONFIG_HOME || sousMaison(env.HOME, '.config')];
|
: [env.XDG_CONFIG_HOME || sousMaison(env.HOME, '.config')];
|
||||||
const temp = plateforme === 'win32' ? env.TMP || env.TEMP : env.TMPDIR || '/tmp';
|
const temp = plateforme === 'win32' ? env.TMP || env.TEMP : env.TMPDIR || '/tmp';
|
||||||
const systeme = { insensibleCasse: plateforme === 'win32' || plateforme === 'darwin', separateur: chemins.sep };
|
const portent = appData.flatMap((dossierAppData) => {
|
||||||
const portent = [...appData, env.LOCALAPPDATA, temp].filter(
|
const connus = { appData: dossierAppData, temp, documents: '' };
|
||||||
(dossier) => typeof dossier === 'string' && dossier !== '' && estSous(executable, dossier, systeme),
|
const cheminSysteme = (nom) => {
|
||||||
);
|
if (!Object.hasOwn(connus, nom)) throw new Error(`app.getPath('${nom}') : l'essai ne sait pas l'émuler`);
|
||||||
|
return connus[nom];
|
||||||
|
};
|
||||||
|
const { emplacements } = decrire({ env, plateforme, cheminSysteme, nomProduit: '' });
|
||||||
|
return emplacements.donneesApplicatives.filter((dossier) => estSous(executable, dossier, emplacements));
|
||||||
|
});
|
||||||
return [...new Set(portent)];
|
return [...new Set(portent)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { mkdtempSync, rmSync, symlinkSync } from 'node:fs';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import path, { dirname, join, relative, isAbsolute } from 'node:path';
|
import path, { dirname, join, relative, isAbsolute } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { decrireSysteme } from '../electron/fichiers.js';
|
||||||
import { estSous } from '../src/stockage/dossier_travail.js';
|
import { estSous } from '../src/stockage/dossier_travail.js';
|
||||||
import { describe, test } from '../test/lanceur.js';
|
import { describe, test } from '../test/lanceur.js';
|
||||||
import { donneesApplicativesSurExe, dossiersDEssai, environnementDEssai, estLanceDirectement } from './essai_demarrage.js';
|
import { donneesApplicativesSurExe, dossiersDEssai, environnementDEssai, estLanceDirectement } from './essai_demarrage.js';
|
||||||
|
|
@ -126,6 +127,29 @@ describe('essai de démarrage : dossiers et environnement (§ 8.6)', () => {
|
||||||
// Sans exécutable publié, rien ne le porte.
|
// Sans exécutable publié, rien ne le porte.
|
||||||
assert.deepEqual(donneesApplicativesSurExe({ TMPDIR: '/' }, {}, 'linux'), []);
|
assert.deepEqual(donneesApplicativesSurExe({ TMPDIR: '/' }, {}, 'linux'), []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('la liste des données applicatives est celle que decrireSysteme compose : un emplacement qu’elle ajoute est jugé, et un app.getPath que l’essai ne sait pas émuler lève', () => {
|
||||||
|
const P = path.posix;
|
||||||
|
const R = P.join('/', 'racine-essai', 'gtt-essai-demarrage-x');
|
||||||
|
const dossiers = dossiersDEssai(R, P);
|
||||||
|
const env = environnementDEssai({ HOME: P.join('/', 'maison-originale') }, dossiers, P);
|
||||||
|
const autre = P.join('/', 'racine-essai');
|
||||||
|
// Une coquille qui range aussi GTT_DONNEES parmi ses données applicatives.
|
||||||
|
const avecUnDossier = (poste) => {
|
||||||
|
const decrit = decrireSysteme(poste);
|
||||||
|
const donneesApplicatives = [...decrit.emplacements.donneesApplicatives, poste.env.GTT_DONNEES];
|
||||||
|
return { ...decrit, emplacements: { ...decrit.emplacements, donneesApplicatives } };
|
||||||
|
};
|
||||||
|
assert.deepEqual(donneesApplicativesSurExe({ ...env, GTT_DONNEES: autre }, {}, 'linux', avecUnDossier), [autre]);
|
||||||
|
assert.deepEqual(donneesApplicativesSurExe({ ...env, GTT_DONNEES: P.join('/', 'ailleurs') }, {}, 'linux', avecUnDossier), []);
|
||||||
|
// Une coquille qui lit un autre dossier du système : l'essai le refuse
|
||||||
|
// plutôt que de juger une liste incomplète.
|
||||||
|
const avecUserData = (poste) => {
|
||||||
|
poste.cheminSysteme('userData');
|
||||||
|
return decrireSysteme(poste);
|
||||||
|
};
|
||||||
|
assert.throws(() => donneesApplicativesSurExe(env, {}, 'linux', avecUserData), /userData/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('essai de démarrage : lancement direct', () => {
|
describe('essai de démarrage : lancement direct', () => {
|
||||||
|
|
|
||||||
191
spec.md
191
spec.md
|
|
@ -341,7 +341,9 @@ aperçoive**.
|
||||||
|
|
||||||
Moyennant cet ajout, le moteur n'a **qu'un seul chemin de code** : la
|
Moyennant cet ajout, le moteur n'a **qu'un seul chemin de code** : la
|
||||||
réservation est un prétraitement qui réduit les capacités, retire des
|
réservation est un prétraitement qui réduit les capacités, retire des
|
||||||
participants et annote les tables — jamais un second algorithme.
|
participants et annote les tables — jamais un second algorithme. Une garde de
|
||||||
|
l'arborescence le tient : dans le moteur, seuls la normalisation de la
|
||||||
|
configuration et le diagnostic lisent les réservations.
|
||||||
|
|
||||||
### 5.3 Les contraintes, activables une à une
|
### 5.3 Les contraintes, activables une à une
|
||||||
|
|
||||||
|
|
@ -393,9 +395,10 @@ une lecture et non un jugement :
|
||||||
| **manque** | plafond réalisé − rencontres (§ 5.5) — la grandeur qui compare deux propositions (§ 12.10) |
|
| **manque** | plafond réalisé − rencontres (§ 5.5) — la grandeur qui compare deux propositions (§ 12.10) |
|
||||||
| **écart d'itinéraire** | plafond a priori − plafond réalisé (§ 5.5, § 12.10) |
|
| **écart d'itinéraire** | plafond a priori − plafond réalisé (§ 5.5, § 12.10) |
|
||||||
|
|
||||||
**Les agrégats portent sur tous les participants placés.** Le minimum se
|
**Les agrégats portent sur tous les participants placés.** Le minimum se calcule
|
||||||
calcule sur l'ensemble, ancrés compris, et **deux lignes secondaires** donnent
|
sur l'ensemble, ancrés compris, et **deux lignes secondaires** donnent la même
|
||||||
la même mesure restreinte aux **mobiles** et aux **ancrés**. Un participant partiellement fixé compte parmi les mobiles.
|
mesure restreinte aux **mobiles** et aux **ancrés**. Un participant
|
||||||
|
partiellement fixé compte parmi les mobiles.
|
||||||
|
|
||||||
> Un ancré n'est pas structurellement désavantagé : à table de 8 avec un seul
|
> Un ancré n'est pas structurellement désavantagé : à table de 8 avec un seul
|
||||||
> ancré et R = 4, son plafond vaut 28, exactement celui d'un mobile visitant
|
> ancré et R = 4, son plafond vaut 28, exactement celui d'un mobile visitant
|
||||||
|
|
@ -680,8 +683,8 @@ dit, pour que l'opérateur sache lequel des deux il fait.
|
||||||
**Le placement se prépare, il ne suit pas la salle.** Retirer une personne du
|
**Le placement se prépare, il ne suit pas la salle.** Retirer une personne du
|
||||||
placement retenu libère sa chaise, et personne d'autre ne change de siège ni de
|
placement retenu libère sa chaise, et personne d'autre ne change de siège ni de
|
||||||
table : la chaise reste vide jusqu'à ce qu'un geste y assoie quelqu'un, et un
|
table : la chaise reste vide jusqu'à ce qu'un geste y assoie quelqu'un, et un
|
||||||
geste n'assied que les personnes qu'il nomme. Quand les sièges sont attribués, le
|
geste n'assied que les personnes qu'il nomme. Quand les sièges sont attribués,
|
||||||
fichier écrit cette chaise vide à sa place (§ 8.9).
|
le fichier écrit cette chaise vide à sa place (§ 8.9).
|
||||||
|
|
||||||
**Il n'existe aucune commande qui répartit les animateurs sur les tables.**
|
**Il n'existe aucune commande qui répartit les animateurs sur les tables.**
|
||||||
L'opérateur désigne lui-même quelle personne anime quelle table (§ 2.1,
|
L'opérateur désigne lui-même quelle personne anime quelle table (§ 2.1,
|
||||||
|
|
@ -738,8 +741,9 @@ proposition, une réservation, un titre, des réservations à convertir —,
|
||||||
qu'aucune commande ne fait revenir et sur laquelle l'interface n'offre pas le
|
qu'aucune commande ne fait revenir et sur laquelle l'interface n'offre pas le
|
||||||
geste ; d'un compteur au bout de sa règle, d'un plan qui n'est plus bloqué, d'un
|
geste ; d'un compteur au bout de sa règle, d'un plan qui n'est plus bloqué, d'un
|
||||||
geste sans effet, et d'un refus du CSV relayé par l'import, qui se lève dans le
|
geste sans effet, et d'un refus du CSV relayé par l'import, qui se lève dans le
|
||||||
fichier. Un remède posé pour la forme enverrait l'opérateur vers un geste qui ne
|
fichier ou dans l'association de ses colonnes — `NOM_NON_ASSOCIE` quand aucune
|
||||||
change rien.
|
colonne n'est associée au nom. Un remède posé pour la forme enverrait
|
||||||
|
l'opérateur vers un geste qui ne change rien.
|
||||||
|
|
||||||
### 5.10 Le coût d'évaluation
|
### 5.10 Le coût d'évaluation
|
||||||
|
|
||||||
|
|
@ -1088,9 +1092,10 @@ diamètre = max(70, places × 60 / π) en centimètres
|
||||||
Soit 115 cm pour 6 places, 153 pour 8, 191 pour 10. **Le plancher n'est pas
|
Soit 115 cm pour 6 places, 153 pour 8, 191 pour 10. **Le plancher n'est pas
|
||||||
cosmétique** : en deçà de quatre convives — la formule atteint 70 cm à 3,67
|
cosmétique** : en deçà de quatre convives — la formule atteint 70 cm à 3,67
|
||||||
places — ce n'est plus le périmètre qui borne la table mais le plateau, et la
|
places — ce n'est plus le périmètre qui borne la table mais le plateau, et la
|
||||||
formule seule dessinerait des guéridons. Les deux
|
formule seule dessinerait des guéridons. Les deux valeurs sont des **réglages
|
||||||
valeurs sont des **réglages par défaut, non des normes vérifiées** ; un diamètre
|
par défaut, non des normes vérifiées** ; un diamètre saisi à la main l'emporte,
|
||||||
saisi à la main l'emporte.
|
dès que le fichier d'état peut le porter : le format 1 n'a pas de champ pour
|
||||||
|
lui, et toute table s'y dessine selon la formule.
|
||||||
|
|
||||||
**Les étiquettes ne sont pas à l'échelle, le mobilier l'est.** Une liste de dix
|
**Les étiquettes ne sont pas à l'échelle, le mobilier l'est.** Une liste de dix
|
||||||
noms couvre plusieurs mètres carrés fictifs. **Le plan à l'échelle du bâtiment
|
noms couvre plusieurs mètres carrés fictifs. **Le plan à l'échelle du bâtiment
|
||||||
|
|
@ -1103,12 +1108,11 @@ table sans qu'aucune marque couvre la liste. Sa largeur est fixée dans le
|
||||||
dessin, et un nom trop long s'y abrège (§ 7.4). **Aucune liste ne passe sur les
|
dessin, et un nom trop long s'y abrège (§ 7.4). **Aucune liste ne passe sur les
|
||||||
chaises d'une voisine posée sur la grille** : une table créée prend la première
|
chaises d'une voisine posée sur la grille** : une table créée prend la première
|
||||||
case libre d'une grille dont les colonnes s'écartent d'une emprise, de deux
|
case libre d'une grille dont les colonnes s'écartent d'une emprise, de deux
|
||||||
écarts et d'un bloc de noms, à un pas entier au moins, sur l'un des deux axes, de
|
écarts et d'un bloc de noms, à un pas entier au moins, sur l'un des deux axes,
|
||||||
toute table déjà posée, et les démonstrations (§ 15) sont rangées sur la même
|
de toute table déjà posée, et les démonstrations (§ 15) sont rangées sur la même
|
||||||
grille. Le prix est la place : au
|
grille. Le prix est la place : au zoom ajusté, une grande salle descend sous le
|
||||||
zoom ajusté, une grande salle descend sous le seuil qui retire les listes
|
seuil qui retire les listes (§ 17, point 6), et un grand événement peut s'ouvrir
|
||||||
(§ 17, point 6), et un grand événement peut s'ouvrir sans listes de noms, qui se
|
sans listes de noms, qui se dessinent quand l'opérateur rapproche la vue.
|
||||||
dessinent quand l'opérateur rapproche la vue.
|
|
||||||
|
|
||||||
**Tant qu'aucune dimension de salle n'est saisie, le plan est exact dans ses
|
**Tant qu'aucune dimension de salle n'est saisie, le plan est exact dans ses
|
||||||
tables et arbitraire dans son cadre.** Le logiciel porte alors, sur le plan et
|
tables et arbitraire dans son cadre.** Le logiciel porte alors, sur le plan et
|
||||||
|
|
@ -2918,11 +2922,12 @@ service.
|
||||||
| `web` | la plateforme web de Capacitor, servie en local | le développement et les tests sous Linux |
|
| `web` | la plateforme web de Capacitor, servie en local | le développement et les tests sous Linux |
|
||||||
| `android` | la plateforme Android de Capacitor | ouverte, non requise pour la première livraison |
|
| `android` | la plateforme Android de Capacitor | ouverte, non requise pour la première livraison |
|
||||||
|
|
||||||
**Capacitor ne porte pas l'exécutable Windows, et c'est délibéré.** Il ne fournit
|
**Capacitor ne porte pas l'exécutable Windows, et c'est délibéré.** Il ne
|
||||||
de plateforme de bureau que par une extension communautaire, et une plateforme
|
fournit de plateforme de bureau que par une extension communautaire, et une
|
||||||
dont la mise à jour ne dépend pas du projet fige le moteur d'exécution embarqué :
|
plateforme dont la mise à jour ne dépend pas du projet fige le moteur
|
||||||
l'exécutable livré porterait un Chromium que plus personne ne corrige. La coquille
|
d'exécution embarqué : l'exécutable livré porterait un Chromium que plus
|
||||||
du projet tient en deux fichiers — le processus principal et le script de
|
personne ne corrige. La coquille du projet tient en trois fichiers — le
|
||||||
|
processus principal, ses primitives de fichiers (`fichiers.js`) et le script de
|
||||||
préchargement — et fixe elle-même sa version d'Electron, que la construction met
|
préchargement — et fixe elle-même sa version d'Electron, que la construction met
|
||||||
à jour comme n'importe quelle dépendance.
|
à jour comme n'importe quelle dépendance.
|
||||||
|
|
||||||
|
|
@ -2957,11 +2962,13 @@ navigateur.
|
||||||
Tout — le moteur, la géométrie, l'interface — est écrit en **JavaScript**.
|
Tout — le moteur, la géométrie, l'interface — est écrit en **JavaScript**.
|
||||||
Aucune partie du calcul n'est dupliquée dans un second langage.
|
Aucune partie du calcul n'est dupliquée dans un second langage.
|
||||||
|
|
||||||
Cette propriété doit être **préservée délibérément**. Dès que la même
|
Cette propriété doit être **préservée délibérément**, et une garde de
|
||||||
arithmétique existe en deux endroits, une suite de tests peut prouver qu'ils
|
l'arborescence la tient : sous `src/`, `electron/` et `scripts/`, rien d'autre
|
||||||
s'accordent **sans jamais prouver qu'ils ont raison** : les deux dérivent
|
que du JavaScript, des composants Svelte, des feuilles de style, des données
|
||||||
ensemble et les tests restent verts. Une seule implémentation rend ce mode de
|
JSON et les scripts d'installation nommés. Dès que la même arithmétique existe
|
||||||
défaillance impossible.
|
en deux endroits, une suite de tests peut prouver qu'ils s'accordent **sans
|
||||||
|
jamais prouver qu'ils ont raison** : les deux dérivent ensemble et les tests
|
||||||
|
restent verts. Une seule implémentation rend ce mode de défaillance impossible.
|
||||||
|
|
||||||
**Une exception, nommée, et une seule** : le calcul incrémental des indicateurs
|
**Une exception, nommée, et une seule** : le calcul incrémental des indicateurs
|
||||||
du § 5.10. Elle tient à une condition écrite — **le recalcul complet est la
|
du § 5.10. Elle tient à une condition écrite — **le recalcul complet est la
|
||||||
|
|
@ -3028,9 +3035,9 @@ quelques secondes — et c'est la condition pour qu'il soit éprouvé sérieusem
|
||||||
du rendu et placée plus bas que le moteur : elle n'importe ni le moteur, ni
|
du rendu et placée plus bas que le moteur : elle n'importe ni le moteur, ni
|
||||||
l'analyseur CSV, ni le générateur de démonstrations, ni l'application, ni
|
l'analyseur CSV, ni le générateur de démonstrations, ni l'application, ni
|
||||||
l'interface, ni le stockage, et ne touche pas le navigateur. Elle est
|
l'interface, ni le stockage, et ne touche pas le navigateur. Elle est
|
||||||
**requise** : sa garde échoue quand son arbre ne fournit aucun module. Le dessin,
|
**requise** : sa garde échoue quand son arbre ne fournit aucun module. Le
|
||||||
la désignation d'un siège et le dépôt partagent ses conversions et ses chaises
|
dessin, la désignation d'un siège et le dépôt partagent ses conversions et ses
|
||||||
(§ 7.2, § 7.3).
|
chaises (§ 7.2, § 7.3).
|
||||||
|
|
||||||
Les **deux frontières de plateforme** sont le système de fichiers et
|
Les **deux frontières de plateforme** sont le système de fichiers et
|
||||||
l'impression : une interface unique, deux implémentations — plus, pour le système
|
l'impression : une interface unique, deux implémentations — plus, pour le système
|
||||||
|
|
@ -3056,10 +3063,10 @@ ou d'une plateforme ; que `src/geometrie` importe en outre du moteur, de
|
||||||
de Svelte ; et que `src/interface` importe autre chose que Svelte, ses propres
|
de Svelte ; et que `src/interface` importe autre chose que Svelte, ses propres
|
||||||
modules, `src/application` et `src/geometrie` — elle atteint le CSV et le moteur
|
modules, `src/application` et `src/geometrie` — elle atteint le CSV et le moteur
|
||||||
par l'application, jamais en direct. Un type que la documentation d'un module
|
par l'application, jamais en direct. Un type que la documentation d'un module
|
||||||
importe compte comme un import. Le projet `node` ne chargeant pas le greffon Svelte (§ 14.8), un test de
|
importe compte comme un import. Le projet `node` ne chargeant pas le greffon
|
||||||
moteur qui importerait un composant échoue au chargement — mais cette garde-là
|
Svelte (§ 14.8), un test de moteur qui importerait un composant échoue au
|
||||||
ne couvre que les composants, et un module qui touche `document` ne tomberait
|
chargement — mais cette garde-là ne couvre que les composants, et un module qui
|
||||||
qu'à l'exécution de la branche fautive.
|
touche `document` ne tomberait qu'à l'exécution de la branche fautive.
|
||||||
|
|
||||||
### 13.5 Ce que le SVG donne et que le canvas ferait payer
|
### 13.5 Ce que le SVG donne et que le canvas ferait payer
|
||||||
|
|
||||||
|
|
@ -3145,8 +3152,9 @@ suivante ; une règle qui fait échouer la construction tient.
|
||||||
| **sa propre commande** | l'audit par mutation (§ 14.13) | avant une livraison, sous aucun budget |
|
| **sa propre commande** | l'audit par mutation (§ 14.13) | avant une livraison, sous aucun budget |
|
||||||
|
|
||||||
**La commande surveillée ne lance que le projet `node`.** Le lanceur en porte
|
**La commande surveillée ne lance que le projet `node`.** Le lanceur en porte
|
||||||
deux (§ 14.8) ; lancés ensemble, ils démarrent un navigateur à chaque
|
trois (§ 14.8) — `node`, `node-long` et `navigateur` ; lancés ensemble, ils
|
||||||
modification, et le niveau navigateur reste à « avant chaque livraison ».
|
démarrent un navigateur et la série longue à chaque modification, et le niveau
|
||||||
|
navigateur reste à « avant chaque livraison ».
|
||||||
|
|
||||||
**Le partage entre les deux séries `node` est écrit avant la première mesure**,
|
**Le partage entre les deux séries `node` est écrit avant la première mesure**,
|
||||||
et non après. Trois familles tombent naturellement dans la série surveillée sans
|
et non après. Trois familles tombent naturellement dans la série surveillée sans
|
||||||
|
|
@ -3212,10 +3220,10 @@ l'**analyseur CSV**, qui reçoivent l'horloge et l'aléa en paramètre. Un test
|
||||||
l'arborescence le refuse, **et échoue si son balayage ne trouve aucun fichier**.
|
l'arborescence le refuse, **et échoue si son balayage ne trouve aucun fichier**.
|
||||||
Son périmètre est nommé : `src/moteur`, `src/geometrie`, `src/demo`,
|
Son périmètre est nommé : `src/moteur`, `src/geometrie`, `src/demo`,
|
||||||
`src/stockage` et `src/csv`, à l'exclusion du code d'épreuve, où un tirage sert
|
`src/stockage` et `src/csv`, à l'exclusion du code d'épreuve, où un tirage sert
|
||||||
légitimement (§ 14.12). L'interdit ne porte pas sur
|
légitimement (§ 14.12). L'interdit ne porte pas sur le reste de l'application :
|
||||||
le reste de l'application : l'application lit l'horloge pour horodater les
|
l'application lit l'horloge pour horodater les entrées de l'historique, que le
|
||||||
entrées de l'historique, que le stockage écrit telles qu'il les reçoit, et le
|
stockage écrit telles qu'il les reçoit, et le banc de mesure du § 19.10 relève
|
||||||
banc de mesure du § 19.10 relève des temps par image.
|
des temps par image.
|
||||||
|
|
||||||
**Ce n'est pas une règle d'hygiène.** L'interdit de `performance.now` dans le
|
**Ce n'est pas une règle d'hygiène.** L'interdit de `performance.now` dans le
|
||||||
moteur est ce qui rend vraie la phrase « à graine et entrée égales, placement
|
moteur est ce qui rend vraie la phrase « à graine et entrée égales, placement
|
||||||
|
|
@ -3230,22 +3238,25 @@ d'horloge à l'exécution (§ 18.5) : le moteur et le générateur ne la lisent
|
||||||
|
|
||||||
### 14.8 L'outillage des épreuves
|
### 14.8 L'outillage des épreuves
|
||||||
|
|
||||||
**Un seul lanceur, deux projets : Vitest.** Le niveau `node` et le niveau
|
**Un seul lanceur, trois projets : Vitest.** Les deux séries `node` et le niveau
|
||||||
navigateur du § 14.4 sont deux configurations du même lanceur, pas deux outils.
|
navigateur du § 14.4 sont trois configurations du même lanceur, pas trois
|
||||||
|
outils.
|
||||||
|
|
||||||
| projet | environnement | ce qu'il inclut | greffon Svelte |
|
| projet | environnement | ce qu'il inclut | greffon Svelte |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `node` | `node`, aucun DOM | `src/moteur`, `src/geometrie`, `src/stockage`, `src/csv`, `src/demo`, `src/pdf`, `src/application`, et les modules purs de `src/interface` | **absent** |
|
| `node` | `node`, aucun DOM | `src/moteur`, `src/geometrie`, `src/stockage`, `src/csv`, `src/demo`, `src/pdf`, `src/application`, et les modules purs de `src/interface` | **absent** |
|
||||||
|
| `node-long` | `node`, aucun DOM | les épreuves lourdes du niveau `node:long` (§ 14.4), suffixe `.long.test.js`, hors de la série surveillée | **absent** |
|
||||||
| `navigateur` | un vrai moteur de rendu, piloté par le mode navigateur de Vitest | les composants de `src/interface`, les épreuves de coordonnées et de style calculé | présent |
|
| `navigateur` | un vrai moteur de rendu, piloté par le mode navigateur de Vitest | les composants de `src/interface`, les épreuves de coordonnées et de style calculé | présent |
|
||||||
|
|
||||||
**Pourquoi Vitest.** Il lit la **configuration Vite qui construit le livrable** : un module importé par un test se résout exactement comme un module
|
**Pourquoi Vitest.** Il lit la **configuration Vite qui construit le
|
||||||
|
livrable** : un module importé par un test se résout exactement comme un module
|
||||||
importé par l'application. Deux chaînes de résolution — l'une pour construire,
|
importé par l'application. Deux chaînes de résolution — l'une pour construire,
|
||||||
l'autre pour éprouver — sont le mode de défaillance du § 13.2 transposé à
|
l'autre pour éprouver — sont le mode de défaillance du § 13.2 transposé à
|
||||||
l'outillage : le test passe sur un fichier que l'application ne charge jamais, un
|
l'outillage : le test passe sur un fichier que l'application ne charge jamais,
|
||||||
alias diverge, et rien ne le signale. Le lanceur observe en outre le **graphe de
|
un alias diverge, et rien ne le signale. Le lanceur observe en outre le **graphe
|
||||||
dépendances**, donc une modification du moteur ne rejoue que les tests du moteur,
|
de dépendances**, donc une modification du moteur ne rejoue que les tests du
|
||||||
et son mode navigateur offre **la même API d'assertion** au niveau 2, de sorte
|
moteur, et son mode navigateur offre **la même API d'assertion** au niveau 2, de
|
||||||
que l'oracle de géométrie du § 7.2 s'écrit une fois.
|
sorte que l'oracle de géométrie du § 7.2 s'écrit une fois.
|
||||||
|
|
||||||
**Les trois durées sont des cibles, mesurées la première semaine.** Le démarrage
|
**Les trois durées sont des cibles, mesurées la première semaine.** Le démarrage
|
||||||
à froid, la relance sous surveillance et la série complète se mesurent sur un
|
à froid, la relance sous surveillance et la série complète se mesurent sur un
|
||||||
|
|
@ -3300,12 +3311,15 @@ une donnée d'épreuve qui emprunte une racine d'organisation plausible le fait
|
||||||
échouer. Un test vérifie cette disjonction, faute de quoi la garde du § 15.6
|
échouer. Un test vérifie cette disjonction, faute de quoi la garde du § 15.6
|
||||||
tombe au premier exemple écrit de bonne foi.
|
tombe au premier exemple écrit de bonne foi.
|
||||||
|
|
||||||
**La co-localisation se garde.** Un test de la construction affirme que le paquet
|
**La co-localisation se garde.** Un test de la construction affirme que le
|
||||||
livré ne contient **aucun fichier de test ni aucun import du lanceur**. Le § 14.15
|
paquet livré ne contient **aucun fichier de test ni aucun import du lanceur**.
|
||||||
pose que la suite n'est pas livrée ; avec des tests rangés dans `src/`, cette
|
Le § 14.15 pose que la suite n'est pas livrée ; avec des tests rangés dans
|
||||||
propriété tient par la seule analyse statique de Vite, c'est-à-dire par
|
`src/`, cette propriété tient par la seule analyse statique de Vite,
|
||||||
chance, jusqu'au jour où un module de production importe une
|
c'est-à-dire par chance, jusqu'au jour où un module de production importe une
|
||||||
donnée d'épreuve « pour un cas par défaut ».
|
donnée d'épreuve « pour un cas par défaut ». Une garde de l'arborescence exige
|
||||||
|
aussi que chaque test de `src/` importe un module de son propre répertoire, hors
|
||||||
|
d'exceptions nommées avec leur raison, et qu'aucune donnée de `test/fixtures/`
|
||||||
|
n'égale, octet pour octet, un fichier livré.
|
||||||
|
|
||||||
### 14.10 La carte de ce qui s'éprouve
|
### 14.10 La carte de ce qui s'éprouve
|
||||||
|
|
||||||
|
|
@ -3387,14 +3401,15 @@ sait pas fragmenter un SVG ; c'est du code qu'il faut éprouver, pas une règle
|
||||||
style.
|
style.
|
||||||
|
|
||||||
**Au niveau navigateur**, la fonction pure est confrontée à
|
**Au niveau navigateur**, la fonction pure est confrontée à
|
||||||
`getScreenCTM().inverse()` sur un rendu réel, à plusieurs `k` et après défilement
|
`getScreenCTM().inverse()` sur un rendu réel, à plusieurs `k` et après
|
||||||
de la page, à un millième de pixel d'écran près (§ 7.2). **Les trois invariants du § 7.2 ont chacun leur test** : que le `<svg>`
|
défilement de la page, à un millième de pixel d'écran près (§ 7.2). **Les trois
|
||||||
racine ne porte pas d'attribut `viewBox` ; que son style calculé donne une bordure
|
invariants du § 7.2 ont chacun leur test** : que le `<svg>` racine ne porte pas
|
||||||
et un remplissage intérieur nuls — `getBoundingClientRect` les inclut, donc
|
d'attribut `viewBox` ; que son style calculé donne une bordure et un remplissage
|
||||||
l'origine du contenu s'en décale ; qu'aucun ancêtre, ni lui-même, ne porte de
|
intérieur nuls — `getBoundingClientRect` les inclut, donc l'origine du contenu
|
||||||
transformation CSS. Il refuse la feuille de style qui ajoute un jour
|
s'en décale ; qu'aucun ancêtre, ni lui-même, ne porte de transformation CSS. Il
|
||||||
`transform: translateZ(0)` « pour la fluidité » : le plan répondrait faux partout,
|
refuse la feuille de style qui ajoute un jour `transform: translateZ(0)` « pour
|
||||||
et la cause serait cherchée dans le module de géométrie.
|
la fluidité » : le plan répondrait faux partout, et la cause serait cherchée
|
||||||
|
dans le module de géométrie.
|
||||||
|
|
||||||
#### Le stockage
|
#### Le stockage
|
||||||
|
|
||||||
|
|
@ -3632,16 +3647,25 @@ produire une configuration à partir d'une graine. Une boucle sur une grille de
|
||||||
graines et de tailles, appelant les deux vérificateurs, couvre l'essentiel sans
|
graines et de tailles, appelant les deux vérificateurs, couvre l'essentiel sans
|
||||||
dépendance nouvelle.
|
dépendance nouvelle.
|
||||||
|
|
||||||
**Une bibliothèque de propriétés se justifie à deux endroits, et seulement deux** :
|
**Une bibliothèque de propriétés se justifie à deux endroits du calcul, et
|
||||||
la formule du plafond et la détection du séparateur. Toutes deux prennent des
|
seulement deux** : la formule du plafond et la détection du séparateur. Toutes
|
||||||
entrées petites et combinatoires, toutes deux bénéficient du **rétrécissement** —
|
deux prennent des entrées petites et combinatoires, toutes deux bénéficient du
|
||||||
quand une configuration à 120 participants échoue, l'outil rend un contre-exemple à
|
**rétrécissement** — quand une configuration à 120 participants échoue, l'outil
|
||||||
trois tables, là où la réduction manuelle coûte une heure. Leurs oracles ne sont
|
rend un contre-exemple à trois tables, là où la réduction manuelle coûte une
|
||||||
pas de même nature et le texte ne les confond pas : le plafond s'arbitre par
|
heure. Leurs oracles ne sont pas de même nature et le texte ne les confond pas :
|
||||||
**énumération exhaustive** ; le séparateur s'arbitre par **construction** — un
|
le plafond s'arbitre par **énumération exhaustive** ; le séparateur s'arbitre
|
||||||
tableau de champs engendré, rendu avec un séparateur connu, et la détection doit le
|
par **construction** — un tableau de champs engendré, rendu avec un séparateur
|
||||||
retrouver. Ailleurs, non : une propriété sur la recherche rejouerait la partie
|
connu, et la détection doit le retrouver. Ailleurs, non : une propriété sur la
|
||||||
lente et épuiserait le budget du § 14.14.
|
recherche rejouerait la partie lente et épuiserait le budget du § 14.14.
|
||||||
|
|
||||||
|
**Le stockage l'emploie pour deux propriétés de plus, nommées.** L'aller-retour
|
||||||
|
des correctifs du journal — appliquer à une charge la différence qui mène à une
|
||||||
|
autre rend l'autre, sur des paires de charges tirées — et la restitution de
|
||||||
|
chaque instant d'une séance tirée, qui vit dans la série longue parce que ses
|
||||||
|
séances lancent des générations, à compte d'arrêt court. Leurs entrées sont des
|
||||||
|
suites d'opérations, où le **rétrécissement** sert comme au plafond : le
|
||||||
|
contre-exemple rendu tient en quelques opérations. La liste est fermée : une
|
||||||
|
garde de l'arborescence refuse la bibliothèque hors de ces quatre familles.
|
||||||
|
|
||||||
**La bibliothèque tourne sur une graine fixée.** Une bibliothèque de propriétés
|
**La bibliothèque tourne sur une graine fixée.** Une bibliothèque de propriétés
|
||||||
retire par défaut une graine neuve à chaque exécution : le même code échoue une
|
retire par défaut une graine neuve à chaque exécution : le même code échoue une
|
||||||
|
|
@ -4298,10 +4322,12 @@ Tout le reste en dérive, par un script de construction :
|
||||||
script puis compare — la même discipline qu'au § 15.5 pour les fichiers de
|
script puis compare — la même discipline qu'au § 15.5 pour les fichiers de
|
||||||
démonstration (§ 13.1).
|
démonstration (§ 13.1).
|
||||||
|
|
||||||
**Le module porte aussi la provenance de la construction** — livraison, séance de
|
**Le module engendré ne porte que la version.** La **provenance de la
|
||||||
développement, ou construction de documentation — posée par la commande qui
|
construction** — livraison, séance de développement, ou construction de
|
||||||
l'engendre, jamais lue dans l'horloge ni dans l'environnement. Sans elle, une
|
documentation — est le mode de construction que pose la commande qui construit,
|
||||||
capture d'écran prise pendant le développement annonce un numéro déjà livré, et le
|
celui de Vite, que l'application reçoit avec ses dépendances ; elle n'est jamais
|
||||||
|
lue dans l'horloge ni dans l'environnement du poste. Sans elle, une capture
|
||||||
|
d'écran prise pendant le développement annonce un numéro déjà livré, et le
|
||||||
diagnostic porte sur un binaire que personne ne détient.
|
diagnostic porte sur un binaire que personne ne détient.
|
||||||
|
|
||||||
### 18.4 Le `CHANGELOG.md`
|
### 18.4 Le `CHANGELOG.md`
|
||||||
|
|
@ -4474,12 +4500,13 @@ fois lors du premier geste suivant un remplacement d'exécutable, ce qui coûte
|
||||||
correctif et non du bruit à chaque geste. Il **n'est pas** la version de format,
|
correctif et non du bruit à chaque geste. Il **n'est pas** la version de format,
|
||||||
qui reste un nombre séparé, comparé selon ses propres règles.
|
qui reste un nombre séparé, comparé selon ses propres règles.
|
||||||
|
|
||||||
**Pendant une construction de documentation, le numéro est remplacé par une marque
|
**Pendant une construction de documentation, le numéro est remplacé par une
|
||||||
de provenance** — dans le bandeau, sur le premier écran, au pied des pages
|
marque de provenance** — dans le bandeau, sur le premier écran, au pied des
|
||||||
imprimées et sur la feuille de contrôle des badges. La raison est au § 19.4 : sans cette marque, le texte relevé
|
pages imprimées et sur la feuille de contrôle des badges. La raison est au
|
||||||
de chaque capture d'écran change à chaque incrément de version, et toutes les
|
§ 19.4 : sans cette marque, le texte relevé de chaque capture d'écran change à
|
||||||
images se réécrivent. Le manifeste, lui, enregistre la **version réelle**, et le
|
chaque incrément de version, et toutes les images se réécrivent. Le manifeste,
|
||||||
contrôle de version de la livraison la compare (§ 19.7).
|
lui, enregistre la **version réelle**, et le contrôle de version de la livraison
|
||||||
|
la compare (§ 19.7).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1094,14 +1094,23 @@ describe('chaque écran de l’itération (§ 14.5, § 14.6, § 19.11)', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('contrastes : chaque texte tient 4,5:1 sur son fond peint, dans les deux thèmes posés par le réglage ; les couleurs relues diffèrent d’un thème à l’autre', () => {
|
test('contrastes : chaque texte tient 4,5:1 sur son fond peint, dans les deux thèmes posés par le réglage ; les couleurs relues diffèrent d’un thème à l’autre', () => {
|
||||||
expect(atteints().reduce((n, r) => n + r.clair.textes.mesures + r.sombre.textes.mesures, 0)).toBeGreaterThan(0);
|
// Un compte par thème : un relevé vide dans l'un passerait sous la somme
|
||||||
|
// des deux, et la comparaison des couleurs n'aurait rien à comparer.
|
||||||
|
for (const theme of THEMES) {
|
||||||
|
expect(atteints().reduce((n, r) => n + r[theme].textes.mesures, 0), `textes mesurés [${theme}]`).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
const fautes = [];
|
const fautes = [];
|
||||||
|
let comparees = 0;
|
||||||
for (const r of atteints()) {
|
for (const r of atteints()) {
|
||||||
for (const theme of THEMES) fautes.push(...r[theme].textes.fautes.map((f) => `${r.nom} [${theme}] — ${f}`));
|
for (const theme of THEMES) fautes.push(...r[theme].textes.fautes.map((f) => `${r.nom} [${theme}] — ${f}`));
|
||||||
for (const [cle, couleurs] of r.clair.textes.couleurs) {
|
for (const [cle, couleurs] of r.clair.textes.couleurs) {
|
||||||
if (r.sombre.textes.couleurs.get(cle) === couleurs) fautes.push(`${r.nom} — ${cle} : ${couleurs} dans les deux thèmes`);
|
const sombres = r.sombre.textes.couleurs.get(cle);
|
||||||
|
if (sombres === undefined) continue;
|
||||||
|
comparees += 1;
|
||||||
|
if (sombres === couleurs) fautes.push(`${r.nom} — ${cle} : ${couleurs} dans les deux thèmes`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
expect(comparees, 'couleurs relevées dans les deux thèmes').toBeGreaterThan(0);
|
||||||
aucuneFaute([...new Set(fautes)], 'contrastes');
|
aucuneFaute([...new Set(fautes)], 'contrastes');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1124,8 +1133,9 @@ describe('chaque écran de l’itération (§ 14.5, § 14.6, § 19.11)', () => {
|
||||||
|
|
||||||
test('aucune atténuation : ni opacité partielle, ni filtre, ni fusion, ni masque sur un texte, une marque ou leur ascendance, dans les deux thèmes', () => {
|
test('aucune atténuation : ni opacité partielle, ni filtre, ni fusion, ni masque sur un texte, une marque ou leur ascendance, dans les deux thèmes', () => {
|
||||||
for (const theme of THEMES) {
|
for (const theme of THEMES) {
|
||||||
const mesures = atteints().reduce((n, r) => n + r[theme].textes.mesures + r[theme].marques.mesures, 0);
|
for (const sorte of ['textes', 'marques']) {
|
||||||
expect(mesures, `textes et marques mesurés [${theme}]`).toBeGreaterThan(0);
|
expect(atteints().reduce((n, r) => n + r[theme][sorte].mesures, 0), `${sorte} mesurés [${theme}]`).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const fautes = [];
|
const fautes = [];
|
||||||
for (const r of atteints()) {
|
for (const r of atteints()) {
|
||||||
|
|
|
||||||
|
|
@ -3,27 +3,32 @@
|
||||||
|
|
||||||
// Gardes de l'arborescence, lues dans le texte des sources : le moteur, la
|
// Gardes de l'arborescence, lues dans le texte des sources : le moteur, la
|
||||||
// géométrie du plan, le générateur de démonstrations, le stockage et
|
// géométrie du plan, le générateur de démonstrations, le stockage et
|
||||||
// l'analyseur CSV n'appellent aucune source non reproductible (§ 14.7), et ne parcourent les clés d'un
|
// l'analyseur CSV n'appellent aucune source non reproductible (§ 14.7), et ne
|
||||||
// objet que par exception nommée (§ 15.5, point 4) ; le graphe d'imports de
|
// parcourent les clés d'un objet que par exception nommée (§ 15.5, point 4) ;
|
||||||
// chaque couche, l'interface et ses composants compris, ne rejoint que ce que
|
// le graphe d'imports de chaque couche, l'interface et ses composants compris,
|
||||||
// le § 13.4 lui permet ; seuls deux lecteurs nommés touchent le pont de la
|
// ne rejoint que ce que le § 13.4 lui permet, et ni le moteur, ni la
|
||||||
// coquille (§ 13.1) ; hors de l'horloge, l'interface et l'application
|
// géométrie, ni le générateur ne lisent la version engendrée (§ 14.7) ; seuls
|
||||||
// n'arment aucune minuterie et ne lisent ni l'heure, ni l'aléa, ni la langue
|
// deux lecteurs nommés touchent le pont de la coquille (§ 13.1) ; hors de
|
||||||
// (§ 19.4) ; chaque paquet importé est déclaré dans devDependencies, et aucun
|
// l'horloge, l'interface et l'application n'arment aucune minuterie et ne
|
||||||
// paquet refusé ne l'est (§ 13.3, § 14.8) ; une valeur du contrat de données
|
// lisent ni l'heure, ni l'aléa, ni la langue (§ 19.4) ; chaque paquet importé
|
||||||
// n'a qu'une définition, celle de configuration.js ; la mesure de la
|
// est déclaré dans devDependencies, et aucun paquet refusé ne l'est (§ 13.3,
|
||||||
// couverture joue la série node, publie chaque module et tient ses seuils
|
// § 14.8) ; une valeur du contrat de données n'a qu'une définition, celle de
|
||||||
// durs sur des modules qui existent (§ 14.13) ; les épreuves s'écrivent
|
// configuration.js ; la mesure de la couverture joue la série node, publie
|
||||||
// selon le § 14.8, la série surveillée n'attend ni n'écrit hors d'un
|
// chaque module et tient ses seuils durs sur des modules qui existent
|
||||||
// temporaire, et ses motifs de surveillance ne retirent aucune relance
|
// (§ 14.13) ; les épreuves s'écrivent selon le § 14.8, la série surveillée
|
||||||
// (§ 14.14). Chaque relevé lit le texte entier, commentaires et chaînes
|
// n'attend ni n'écrit hors d'un temporaire, et ses motifs de surveillance ne
|
||||||
// compris, hors ceux qui disent le contraire — les accès au navigateur et au
|
// retirent aucune relance (§ 14.14) ; dans le moteur, deux modules nommés
|
||||||
// pont, et les relevés des épreuves, sautent les chaînes littérales : un
|
// lisent les réservations (§ 5.2) ; src/, electron/ et scripts/ n'ont qu'une
|
||||||
// commentaire qui nomme un appel refusé fait échouer la garde comme l'appel
|
// langue (§ 13.2) ; une épreuve de src/ vit à côté de son module, et aucune
|
||||||
// lui-même. Chaque garde déclare son périmètre, refuse de passer sur un
|
// donnée d'épreuve ne double un fichier livré (§ 14.9) ; fast-check ne se
|
||||||
// balayage vide (§ 14.2), et d'autres épreuves la font tourner sur des
|
// charge que dans les épreuves nommées (§ 14.12). Chaque relevé lit le texte
|
||||||
// arbres temporaires qui portent, à plus d'un niveau de profondeur, les
|
// entier, commentaires et chaînes compris, hors ceux qui disent le contraire —
|
||||||
// formes qu'elle refuse et des formes voisines qu'elle admet.
|
// les accès au navigateur et au pont, et les relevés des épreuves, sautent les
|
||||||
|
// chaînes littérales : un commentaire qui nomme un appel refusé fait échouer
|
||||||
|
// la garde comme l'appel lui-même. Chaque garde déclare son périmètre, refuse
|
||||||
|
// de passer sur un balayage vide (§ 14.2), et d'autres épreuves la font
|
||||||
|
// tourner sur des arbres temporaires qui portent, à plus d'un niveau de
|
||||||
|
// profondeur, les formes qu'elle refuse et des formes voisines qu'elle admet.
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
||||||
import { isBuiltin } from 'node:module';
|
import { isBuiltin } from 'node:module';
|
||||||
|
|
@ -498,7 +503,7 @@ const PAQUETS_DE_PLATEFORME = /^(?:svelte|electron|@capacitor\/.+)$/;
|
||||||
// chacun dans son seul texte. Aucun module du parcours n'importe un fichier
|
// chacun dans son seul texte. Aucun module du parcours n'importe un fichier
|
||||||
// admis : il ne passe qu'en paramètre, de la main de l'application.
|
// admis : il ne passe qu'en paramètre, de la main de l'application.
|
||||||
// composants : vrai quand ses composants Svelte sont des modules de la
|
// composants : vrai quand ses composants Svelte sont des modules de la
|
||||||
// couche, lus par leurs blocs de script.
|
// couche, lus par leurs blocs de script et les import() de leur gabarit.
|
||||||
//
|
//
|
||||||
// Le moteur et la géométrie ne connaissent ni l'écran, ni les fichiers, ni ce
|
// Le moteur et la géométrie ne connaissent ni l'écran, ni les fichiers, ni ce
|
||||||
// qui les commande. La géométrie est une couche à part, plus bas que le
|
// qui les commande. La géométrie est une couche à part, plus bas que le
|
||||||
|
|
@ -513,19 +518,32 @@ const PAQUETS_DE_PLATEFORME = /^(?:svelte|electron|@capacitor\/.+)$/;
|
||||||
// src/application/csv.js, et le moteur par la table d'évaluation, sans que
|
// src/application/csv.js, et le moteur par la table d'évaluation, sans que
|
||||||
// sa garde le lui reproche, l'application le faisant par construction ;
|
// sa garde le lui reproche, l'application le faisant par construction ;
|
||||||
// importés en direct, le stockage, le CSV, le moteur, les démonstrations et
|
// importés en direct, le stockage, le CSV, le moteur, les démonstrations et
|
||||||
// test/ sont refusés.
|
// test/ sont refusés. Le générateur de démonstrations lit le moteur et le
|
||||||
|
// stockage, qui sérialise ses fichiers, mais ni l'application, ni
|
||||||
|
// l'interface, ni le navigateur. Le moteur, la géométrie et le générateur ne
|
||||||
|
// lisent pas la version engendrée (§ 14.7, § 18.5), à aucune profondeur :
|
||||||
|
// une démonstration ou un placement qui en dépendrait changerait à chaque
|
||||||
|
// livraison.
|
||||||
|
const VERSION_ENGENDREE = 'src/version.genere.js';
|
||||||
const COUCHES = [
|
const COUCHES = [
|
||||||
{
|
{
|
||||||
arbres: ['src/moteur'],
|
arbres: ['src/moteur'],
|
||||||
requis: ['src/moteur'],
|
requis: ['src/moteur'],
|
||||||
exterieures: ['src/interface', 'src/application', 'src/stockage'],
|
exterieures: ['src/interface', 'src/application', 'src/stockage', VERSION_ENGENDREE],
|
||||||
paquets: PAQUETS_DE_PLATEFORME,
|
paquets: PAQUETS_DE_PLATEFORME,
|
||||||
navigateur: [],
|
navigateur: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
arbres: ['src/geometrie'],
|
arbres: ['src/geometrie'],
|
||||||
requis: ['src/geometrie'],
|
requis: ['src/geometrie'],
|
||||||
exterieures: ['src/interface', 'src/application', 'src/stockage', 'src/moteur', 'src/csv', 'src/demo'],
|
exterieures: ['src/interface', 'src/application', 'src/stockage', 'src/moteur', 'src/csv', 'src/demo', VERSION_ENGENDREE],
|
||||||
|
paquets: PAQUETS_DE_PLATEFORME,
|
||||||
|
navigateur: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arbres: ['src/demo'],
|
||||||
|
requis: ['src/demo'],
|
||||||
|
exterieures: ['src/interface', 'src/application', VERSION_ENGENDREE],
|
||||||
paquets: PAQUETS_DE_PLATEFORME,
|
paquets: PAQUETS_DE_PLATEFORME,
|
||||||
navigateur: [],
|
navigateur: [],
|
||||||
},
|
},
|
||||||
|
|
@ -700,7 +718,8 @@ function cibleDe(racine, fichier, specificateur) {
|
||||||
const composantsDe = (dossier) => fichiersDe(dossier).filter((fichier) => extname(fichier) === '.svelte').sort();
|
const composantsDe = (dossier) => fichiersDe(dossier).filter((fichier) => extname(fichier) === '.svelte').sort();
|
||||||
|
|
||||||
// Le texte qu'un relevé de chargements lit dans un fichier : celui de ses
|
// Le texte qu'un relevé de chargements lit dans un fichier : celui de ses
|
||||||
// blocs de script pour un composant Svelte, le texte entier sinon.
|
// blocs de script et des import() de son gabarit pour un composant Svelte
|
||||||
|
// (scriptsDe), le texte entier sinon.
|
||||||
const texteDesChargements = (fichier, texte) => (extname(fichier) === '.svelte' ? scriptsDe(texte) : texte);
|
const texteDesChargements = (fichier, texte) => (extname(fichier) === '.svelte' ? scriptsDe(texte) : texte);
|
||||||
|
|
||||||
// Relevé des refus de la frontière d'une couche dans son graphe d'imports.
|
// Relevé des refus de la frontière d'une couche dans son graphe d'imports.
|
||||||
|
|
@ -1191,11 +1210,67 @@ describe('arborescence : frontière des couches (§ 13.4)', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('src/application peut manquer ; un src/moteur, un src/geometrie, un src/stockage, un src/csv ou un src/interface sans module fait échouer la garde ; un composant seul suffit à l’interface', () => {
|
test('ni le moteur, ni la géométrie, ni le générateur de démonstrations ne lisent la version engendrée (§ 14.7, § 18.5) ; l’interface et l’application la lisent ; le générateur n’importe ni l’interface, ni l’application, ni une plateforme, ni le navigateur', () => {
|
||||||
|
const fichiers = {
|
||||||
|
...MODULES_VIDES,
|
||||||
|
'src/version.genere.js': "export const VERSION = Object.freeze({ affichee: '', technique: '' });\n",
|
||||||
|
'src/moteur/recherche.js': "import { VERSION } from '../version.genere.js';\n",
|
||||||
|
'src/geometrie/sous/vue.js': "import { VERSION } from '../../version.genere.js';\n",
|
||||||
|
'src/demo/catalogue.js': [
|
||||||
|
"import { VERSION } from '../version.genere.js';",
|
||||||
|
"import { ErreurConfiguration } from '../moteur/erreurs.js';",
|
||||||
|
"import { serialiser } from '../stockage/canonique.js';",
|
||||||
|
"import { t } from '../application/traduction.js';",
|
||||||
|
"import { x } from '../interface/x.js';",
|
||||||
|
"import { mount } from 'svelte';",
|
||||||
|
"import { readFileSync } from 'node:fs';",
|
||||||
|
'export const langue = navigator.language;',
|
||||||
|
].join('\n'),
|
||||||
|
'src/demo/livrees.js': "import { CATALOGUE } from './catalogue.js';\nimport petite from './livrees/petite.gtt.json' with { type: 'json' };\n",
|
||||||
|
'src/moteur/erreurs.js': 'export class ErreurConfiguration extends Error {}\n',
|
||||||
|
'src/stockage/canonique.js': "import { VERSION } from '../version.genere.js';\nexport const serialiser = () => VERSION;\n",
|
||||||
|
'src/application/a.js': "import { VERSION } from '../version.genere.js';\n",
|
||||||
|
'src/interface/App.svelte': "<script>\n import { VERSION } from '../version.genere.js';\n</script>\n",
|
||||||
|
};
|
||||||
|
assert.deepEqual(avecArbre(fichiers, releverFrontiere), [
|
||||||
|
'src/moteur/recherche.js:1 import ../version.genere.js',
|
||||||
|
'src/geometrie/sous/vue.js:1 import ../../version.genere.js',
|
||||||
|
'src/demo/catalogue.js:1 import ../version.genere.js',
|
||||||
|
'src/demo/catalogue.js:4 import ../application/traduction.js',
|
||||||
|
'src/demo/catalogue.js:5 import ../interface/x.js',
|
||||||
|
'src/demo/catalogue.js:6 import svelte',
|
||||||
|
'src/demo/catalogue.js:7 import node:fs',
|
||||||
|
'src/demo/catalogue.js:8 navigator.',
|
||||||
|
'src/demo/catalogue.js → src/stockage/canonique.js:1 import ../version.genere.js',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('un import() du gabarit d’un composant se relève comme celui d’un bloc de script ; le mot « import » d’un nœud de texte non', () => {
|
||||||
|
const fichiers = {
|
||||||
|
...MODULES_VIDES,
|
||||||
|
'src/interface/APropos.svelte': [
|
||||||
|
'<script>',
|
||||||
|
" import { apropos } from './apropos.js';",
|
||||||
|
'</script>',
|
||||||
|
"<p>import('../stockage/texte.js') reste du texte</p>",
|
||||||
|
"{#await import('../stockage/depot.js') then depot}{/await}",
|
||||||
|
'{#if apropos}',
|
||||||
|
" <button onclick={() => import('../moteur/recherche.js')}>{apropos}</button>",
|
||||||
|
'{/if}',
|
||||||
|
].join('\n'),
|
||||||
|
'src/interface/apropos.js': 'export const apropos = 1;\n',
|
||||||
|
};
|
||||||
|
assert.deepEqual(avecArbre(fichiers, releverFrontiere), [
|
||||||
|
'src/interface/APropos.svelte:5 import ../stockage/depot.js',
|
||||||
|
'src/interface/APropos.svelte:7 import ../moteur/recherche.js',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('src/application peut manquer ; un src/moteur, un src/geometrie, un src/demo, un src/stockage, un src/csv ou un src/interface sans module fait échouer la garde ; un composant seul suffit à l’interface', () => {
|
||||||
assert.deepEqual(avecArbre(MODULES_VIDES, releverFrontiere), []);
|
assert.deepEqual(avecArbre(MODULES_VIDES, releverFrontiere), []);
|
||||||
const composantSeul = { ...sansArbre(MODULES_VIDES, 'src/interface'), 'src/interface/App.svelte': '<p></p>\n' };
|
const composantSeul = { ...sansArbre(MODULES_VIDES, 'src/interface'), 'src/interface/App.svelte': '<p></p>\n' };
|
||||||
assert.deepEqual(avecArbre(composantSeul, releverFrontiere), []);
|
assert.deepEqual(avecArbre(composantSeul, releverFrontiere), []);
|
||||||
for (const arbre of ['src/moteur', 'src/geometrie', 'src/stockage', 'src/csv', 'src/interface']) {
|
for (const arbre of ['src/moteur', 'src/geometrie', 'src/demo', 'src/stockage', 'src/csv', 'src/interface']) {
|
||||||
const sansModule = { ...sansArbre(MODULES_VIDES, arbre), [`${arbre}/a.test.js`]: '' };
|
const sansModule = { ...sansArbre(MODULES_VIDES, arbre), [`${arbre}/a.test.js`]: '' };
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => avecArbre(sansModule, releverFrontiere),
|
() => avecArbre(sansModule, releverFrontiere),
|
||||||
|
|
@ -2206,17 +2281,44 @@ function delaisEt(texte, code, motifs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Relevé des épreuves du navigateur, chaque *.navigateur.test.js du
|
// Relevé des épreuves du navigateur, chaque *.navigateur.test.js du
|
||||||
// projet : une assertion de classe ou un délai fixe, « fichier:ligne
|
// projet, et des modules JavaScript de test/ qu'elles importent, à toute
|
||||||
// refus ». Lève quand le projet n'en porte aucune.
|
// profondeur, par un chemin relatif ou absolu depuis la racine : les aides
|
||||||
|
// partagées de test/navigateur/ attendent et agissent pour elles. Un module
|
||||||
|
// de src/ ne se parcourt pas : c'est le code éprouvé, que ses propres gardes
|
||||||
|
// tiennent. Chaque assertion de classe ou délai fixe se lit « accès:ligne
|
||||||
|
// refus », l'accès étant la suite des fichiers qui y mène depuis une
|
||||||
|
// épreuve, comme dans releverEcritureDesEpreuves. Lève quand le projet n'en
|
||||||
|
// porte aucune.
|
||||||
function releverEpreuvesNavigateur(racine) {
|
function releverEpreuvesNavigateur(racine) {
|
||||||
const epreuves = fichiersDuProjet(racine).filter((f) => EPREUVE_NAVIGATEUR.test(f));
|
const epreuves = fichiersDuProjet(racine).filter((f) => EPREUVE_NAVIGATEUR.test(f));
|
||||||
assert.ok(epreuves.length > 0, 'aucune épreuve du navigateur');
|
assert.ok(epreuves.length > 0, 'aucune épreuve du navigateur');
|
||||||
return epreuves.flatMap((fichier) => {
|
const sousTest = join(racine, 'test') + sep;
|
||||||
|
const aParcourir = [...epreuves];
|
||||||
|
const acces = new Map(epreuves.map((fichier) => [fichier, relative(racine, fichier)]));
|
||||||
|
const refus = [];
|
||||||
|
for (let i = 0; i < aParcourir.length; i += 1) {
|
||||||
|
const fichier = aParcourir[i];
|
||||||
const texte = readFileSync(fichier, 'utf8');
|
const texte = readFileSync(fichier, 'utf8');
|
||||||
return delaisEt(texte, sansChaines(texte), [...ASSERTIONS_DE_CLASSE, ...DELAIS]).map(
|
const code = sansChaines(texte);
|
||||||
({ ligne, motif }) => `${relative(racine, fichier)}:${ligne} ${motif}`,
|
for (const { indice, specificateur } of chargementsLitteraux(texte)) {
|
||||||
|
const cible = cibleDe(racine, fichier, specificateur);
|
||||||
|
if (
|
||||||
|
estDuCode(code, indice) &&
|
||||||
|
cible !== null &&
|
||||||
|
cible.startsWith(sousTest) &&
|
||||||
|
!acces.has(cible) &&
|
||||||
|
EXTENSIONS_MODULE.has(extname(cible)) &&
|
||||||
|
existsSync(cible)
|
||||||
|
) {
|
||||||
|
acces.set(cible, `${acces.get(fichier)} → ${relative(racine, cible)}`);
|
||||||
|
aParcourir.push(cible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refus.push(
|
||||||
|
...delaisEt(texte, code, [...ASSERTIONS_DE_CLASSE, ...DELAIS]).map(({ ligne, motif }) => `${acces.get(fichier)}:${ligne} ${motif}`),
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
return refus;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('arborescence : écriture des épreuves (§ 14.8, § 14.2)', () => {
|
describe('arborescence : écriture des épreuves (§ 14.8, § 14.2)', () => {
|
||||||
|
|
@ -2317,6 +2419,30 @@ describe('arborescence : écriture des épreuves (§ 14.8, § 14.2)', () => {
|
||||||
]);
|
]);
|
||||||
assert.throws(() => avecArbre({ 'src/a.test.js': '' }, releverEpreuvesNavigateur), /aucune épreuve du navigateur/);
|
assert.throws(() => avecArbre({ 'src/a.test.js': '' }, releverEpreuvesNavigateur), /aucune épreuve du navigateur/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('la garde des épreuves du navigateur suit leurs imports relatifs vers les modules de test/, à toute profondeur, et relève le refus avec son chemin d’accès ; un module de src/ ne se parcourt pas', () => {
|
||||||
|
const fichiers = {
|
||||||
|
'src/interface/plan/Plan.navigateur.test.js': [
|
||||||
|
"import { cliquer } from '../../../test/navigateur/aide.js';",
|
||||||
|
"import { horloge } from '../../application/horloge.js';",
|
||||||
|
"import { donnee } from '../../../test/fixtures/donnee.json' with { type: 'json' };",
|
||||||
|
].join('\n'),
|
||||||
|
'test/navigateur/aide.js': [
|
||||||
|
"import { attendre } from './profond/attente.js';",
|
||||||
|
'export const cliquer = async () => {',
|
||||||
|
' await new Promise((r) => setTimeout(r, 300));',
|
||||||
|
'};',
|
||||||
|
].join('\n'),
|
||||||
|
'test/navigateur/profond/attente.js': "export const attendre = (e) => e.classList.contains('vu');\n",
|
||||||
|
'src/application/horloge.js': 'export const horloge = () => setTimeout(() => {}, 10);\n',
|
||||||
|
'test/fixtures/donnee.json': '{}\n',
|
||||||
|
'test/navigateur/aide.navigateur.test.js': "import { cliquer } from './aide.js';\n",
|
||||||
|
};
|
||||||
|
assert.deepEqual(avecArbre(fichiers, releverEpreuvesNavigateur), [
|
||||||
|
'src/interface/plan/Plan.navigateur.test.js → test/navigateur/aide.js:3 setTimeout',
|
||||||
|
'src/interface/plan/Plan.navigateur.test.js → test/navigateur/aide.js → test/navigateur/profond/attente.js:1 classList.contains',
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Le projet de la série surveillée dans une configuration de Vitest.
|
// Le projet de la série surveillée dans une configuration de Vitest.
|
||||||
|
|
@ -2742,3 +2868,416 @@ describe('arborescence : les motifs de surveillance (§ 14.14)', () => {
|
||||||
assert.throws(() => releverSurveillance(RACINE, { watchTriggerPatterns: [], projects }), /aucun motif de surveillance/);
|
assert.throws(() => releverSurveillance(RACINE, { watchTriggerPatterns: [], projects }), /aucun motif de surveillance/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Un seul chemin, un seul langage, des épreuves à leur place -------------
|
||||||
|
|
||||||
|
// Le code d'un texte sans ses chaînes ni ses commentaires : sansChaines,
|
||||||
|
// puis chaque commentaire « // … » et « /* … */ » blanchi, fins de ligne
|
||||||
|
// gardées. Les chaînes déjà blanchies n'ouvrent plus aucun commentaire.
|
||||||
|
function codeSeul(texte) {
|
||||||
|
return sansChaines(texte).replace(/\/\/[^\n]*|\/\*[\s\S]*?(?:\*\/|$)/g, (commentaire) => commentaire.replace(/[^\n]/g, ' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Les seuls lecteurs des réservations dans le moteur (§ 5.2) : la
|
||||||
|
// réservation est un prétraitement qui réduit l'instance — capacités,
|
||||||
|
// mobiles, appartenances des ancrés —, jamais un second algorithme.
|
||||||
|
const LECTEURS_DES_RESERVATIONS = [
|
||||||
|
{
|
||||||
|
fichier: 'src/moteur/configuration.js',
|
||||||
|
raison: 'normaliser fixe les réservations et réduit l’instance que la recherche résout',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fichier: 'src/moteur/diagnostic.js',
|
||||||
|
raison: 'le diagnostic sonde la configuration sous une partie de ses réservations pour nommer la cause',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Une lecture des réservations : le nom reservations dans le code —
|
||||||
|
// propriété, déstructuration, paramètre —, ou une propriété nommée par une
|
||||||
|
// chaîne entre crochets.
|
||||||
|
const LECTURE_DES_RESERVATIONS = /\breservations\b/g;
|
||||||
|
const RESERVATIONS_ENTRE_CROCHETS = /\[\s*(['"`])reservations\1\s*\]/g;
|
||||||
|
|
||||||
|
// Relevé des lectures des réservations dans les modules de src/moteur, à
|
||||||
|
// toute profondeur, épreuves exclues, hors des lecteurs admis : chacune
|
||||||
|
// « fichier:ligne reservations », ou « fichier:ligne ['reservations'] » pour
|
||||||
|
// la forme à crochets, par fichier puis par ligne ; puis « exception sans
|
||||||
|
// objet : fichier » pour un lecteur admis qui n'en lit aucune. Le nom se lit
|
||||||
|
// dans le code sans chaînes ni commentaires : un typedef JSDoc qui le nomme
|
||||||
|
// ne lit rien. Lève quand src/moteur ne fournit aucun module.
|
||||||
|
function releverLecteursDesReservations(racine, admis = LECTEURS_DES_RESERVATIONS) {
|
||||||
|
const modules = modulesDe(join(racine, 'src', 'moteur'));
|
||||||
|
assert.ok(modules.length > 0, 'src/moteur ne fournit aucun module');
|
||||||
|
const lecteurs = new Set();
|
||||||
|
const refus = [];
|
||||||
|
for (const fichier of modules) {
|
||||||
|
const nom = enBarres(racine, fichier);
|
||||||
|
const texte = readFileSync(fichier, 'utf8');
|
||||||
|
const code = codeSeul(texte);
|
||||||
|
// Le crochet d'une lecture subsiste dans le code ; celui d'un
|
||||||
|
// commentaire est blanchi avec lui.
|
||||||
|
const trouves = [
|
||||||
|
...[...code.matchAll(LECTURE_DES_RESERVATIONS)].map(({ index }) => ({ indice: index, rang: 0, motif: 'reservations' })),
|
||||||
|
...[...texte.matchAll(RESERVATIONS_ENTRE_CROCHETS)]
|
||||||
|
.filter(({ index }) => code[index] === '[')
|
||||||
|
.map(({ index }) => ({ indice: index, rang: 1, motif: "['reservations']" })),
|
||||||
|
];
|
||||||
|
if (trouves.length > 0) lecteurs.add(nom);
|
||||||
|
if (admis.some((e) => e.fichier === nom)) continue;
|
||||||
|
trouves.sort((a, b) => a.indice - b.indice || a.rang - b.rang);
|
||||||
|
refus.push(...trouves.map(({ indice, motif }) => `${nom}:${ligneDe(texte, indice)} ${motif}`));
|
||||||
|
}
|
||||||
|
const sansObjet = admis.filter(({ fichier }) => !lecteurs.has(fichier)).map(({ fichier }) => `exception sans objet : ${fichier}`);
|
||||||
|
return [...refus, ...sansObjet];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Les arbres du code livré et de son outillage, où ne vit qu'une langue
|
||||||
|
// (§ 13.2) : JavaScript, composants Svelte, feuilles de style, et des
|
||||||
|
// données JSON, qui ne calculent rien.
|
||||||
|
const ARBRES_D_UNE_LANGUE = ['src', 'electron', 'scripts'];
|
||||||
|
const EXTENSIONS_D_UNE_LANGUE = new Set(['.js', '.cjs', '.mjs', '.svelte', '.css', '.json']);
|
||||||
|
|
||||||
|
// Les scripts d'installation et de construction, chacun à sa place : ils
|
||||||
|
// posent l'outillage ou le lancent, et ne refont aucun calcul du logiciel.
|
||||||
|
const SCRIPTS_NOMMES = [
|
||||||
|
{ fichier: 'scripts/construire_windows.sh', raison: 'construit l’exécutable Windows depuis Linux (§ 16)' },
|
||||||
|
{ fichier: 'scripts/verifier_systemes.sh', raison: 'éprouve l’installation sur chaque système, dans un conteneur' },
|
||||||
|
{ fichier: 'scripts/installation/avec_node.sh', raison: 'lance une commande avec le Node du projet' },
|
||||||
|
{ fichier: 'scripts/installation/commun.sh', raison: 'fonctions communes des scripts d’installation' },
|
||||||
|
{ fichier: 'scripts/installation/shell.nix', raison: 'environnement de développement sous NixOS' },
|
||||||
|
{
|
||||||
|
fichier: 'scripts/oracle/pcg32_reference.sh',
|
||||||
|
raison: 'compile et lance la référence de PCG32, oracle pris hors du code éprouvé (§ 14.11)',
|
||||||
|
},
|
||||||
|
{ fichier: 'scripts/windows/avec_node.ps1', raison: 'lance une commande avec le Node du projet, sous Windows' },
|
||||||
|
{ fichier: 'scripts/windows/commun.ps1', raison: 'fonctions communes des scripts Windows' },
|
||||||
|
{ fichier: 'scripts/windows/installer.ps1', raison: 'installe le projet sous Windows' },
|
||||||
|
{ fichier: 'scripts/windows/lancer.ps1', raison: 'lance le projet sous Windows' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Fichiers que le disque porte sans qu'ils soient du projet : un nom caché
|
||||||
|
// (.DS_Store, l'échange .x.js.swp de Vim), une sauvegarde d'éditeur (x.js~,
|
||||||
|
// #x.js#), un reste de fusion ou de correctif (x.js.orig, x.js.rej). Le relevé
|
||||||
|
// des langues est le seul à admettre une extension par sa liste plutôt qu'à
|
||||||
|
// choisir la sienne : sans ce filtre, il tombe sur un arbre conforme dès qu'un
|
||||||
|
// éditeur est ouvert, et la série surveillée le relance à chaque sauvegarde.
|
||||||
|
// La liste de git ne les écarte pas : ces noms ne sont pas ignorés par le
|
||||||
|
// dépôt, et --others les rend comme tout fichier nouveau.
|
||||||
|
const estArtefact = (nom) => nom.startsWith('.') || nom.endsWith('~') || /^#.*#$/.test(nom) || /\.(orig|rej)$/.test(nom);
|
||||||
|
|
||||||
|
// Relevé des fichiers d'une autre langue sous src/, electron/ et scripts/, à
|
||||||
|
// toute profondeur, artefacts d'éditeur et de fusion écartés : chacun
|
||||||
|
// « chemin : extension », ou « chemin : sans extension », dans l'ordre des
|
||||||
|
// chemins, hors des extensions admises et des scripts nommés ; puis
|
||||||
|
// « exception sans objet : fichier » pour un script nommé absent. Lève quand
|
||||||
|
// ces arbres ne portent aucun fichier.
|
||||||
|
function releverLangages(racine, nommes = SCRIPTS_NOMMES) {
|
||||||
|
const fichiers = ARBRES_D_UNE_LANGUE.flatMap((arbre) => fichiersDe(join(racine, arbre)))
|
||||||
|
.filter((fichier) => !fichier.split(sep).includes('node_modules') && !estArtefact(basename(fichier)))
|
||||||
|
.map((fichier) => enBarres(racine, fichier))
|
||||||
|
.sort();
|
||||||
|
assert.ok(fichiers.length > 0, 'aucun fichier sous src/, electron/ et scripts/');
|
||||||
|
const presents = new Set(fichiers);
|
||||||
|
const refus = fichiers
|
||||||
|
.filter((chemin) => !EXTENSIONS_D_UNE_LANGUE.has(extname(chemin)) && !nommes.some((e) => e.fichier === chemin))
|
||||||
|
.map((chemin) => `${chemin} : ${extname(chemin) || 'sans extension'}`);
|
||||||
|
const sansObjet = nommes.filter(({ fichier }) => !presents.has(fichier)).map(({ fichier }) => `exception sans objet : ${fichier}`);
|
||||||
|
return [...refus, ...sansObjet];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Les épreuves de src/ qui ne vivent pas à côté d'un module de leur
|
||||||
|
// répertoire, chacune avec sa raison.
|
||||||
|
const EPREUVES_SANS_VOISIN = [
|
||||||
|
{
|
||||||
|
fichier: 'src/interface/accueil/Accueil.navigateur.test.js',
|
||||||
|
raison: 'l’accueil n’existe qu’au sein de l’application : l’épreuve monte App par test/navigateur/montage.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fichier: 'src/interface/propositions/Propositions.navigateur.test.js',
|
||||||
|
raison: 'les propositions se lisent dans l’application montée, après génération : l’épreuve monte App',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fichier: 'src/interface/plan/ecouteurs.test.js',
|
||||||
|
raison: 'garde des sources : elle lit les composants de son répertoire par le disque, sans les importer',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Relevé des épreuves de src/ éloignées de leur module (§ 14.9) : chaque
|
||||||
|
// *.test.js de src/, à toute profondeur, dont aucun chargement de code ne
|
||||||
|
// désigne un fichier existant de son propre répertoire qui ne soit pas une
|
||||||
|
// épreuve — module ou composant ; le suffixe de Vite se retire, et
|
||||||
|
// « ./travailleur.js?worker&inline » désigne le module voisin —, hors des
|
||||||
|
// exceptions nommées : chacune « épreuve : aucun module de répertoire »,
|
||||||
|
// dans l'ordre des chemins ; puis « exception sans objet : fichier » pour
|
||||||
|
// une exception qui a désormais un voisin ou n'existe plus. Lève quand src/
|
||||||
|
// ne porte aucune épreuve.
|
||||||
|
function releverEpreuvesEloignees(racine, admis = EPREUVES_SANS_VOISIN) {
|
||||||
|
const epreuves = fichiersDe(join(racine, 'src'))
|
||||||
|
.filter((fichier) => EPREUVE.test(fichier))
|
||||||
|
.sort();
|
||||||
|
assert.ok(epreuves.length > 0, 'aucune épreuve sous src/');
|
||||||
|
const sansVoisin = epreuves.filter((fichier) => {
|
||||||
|
const texte = readFileSync(fichier, 'utf8');
|
||||||
|
const code = codeSeul(texte);
|
||||||
|
return !chargementsLitteraux(texte).some(({ indice, specificateur }) => {
|
||||||
|
const cible = cibleDe(racine, fichier, specificateur.replace(/\?.*$/, ''));
|
||||||
|
return (
|
||||||
|
estDuCode(code, indice) &&
|
||||||
|
cible !== null &&
|
||||||
|
dirname(cible) === dirname(fichier) &&
|
||||||
|
!EPREUVE.test(cible) &&
|
||||||
|
existsSync(cible)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const noms = sansVoisin.map((fichier) => enBarres(racine, fichier));
|
||||||
|
const refus = noms
|
||||||
|
.filter((nom) => !admis.some((e) => e.fichier === nom))
|
||||||
|
.map((nom) => `${nom} : aucun module de ${dirname(nom)}`);
|
||||||
|
const sansObjet = admis.filter(({ fichier }) => !noms.includes(fichier)).map(({ fichier }) => `exception sans objet : ${fichier}`);
|
||||||
|
return [...refus, ...sansObjet];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Les fichiers livrés qu'une donnée d'épreuve ne double pas (§ 14.9) : les
|
||||||
|
// exemples du § 10.3 et les démonstrations livrées.
|
||||||
|
const ARBRES_LIVRES = ['exemples', 'src/demo/livrees'];
|
||||||
|
|
||||||
|
// Relevé des données de test/fixtures/, à toute profondeur, égales octet
|
||||||
|
// pour octet à un fichier livré : chacune « donnée = fichier livré », dans
|
||||||
|
// l'ordre des chemins. Lève quand test/fixtures/ ou les arbres livrés ne
|
||||||
|
// portent aucun fichier.
|
||||||
|
function releverFixturesLivrees(racine) {
|
||||||
|
const fixtures = fichiersDe(join(racine, 'test', 'fixtures')).sort();
|
||||||
|
const livres = ARBRES_LIVRES.flatMap((arbre) => fichiersDe(join(racine, arbre))).sort();
|
||||||
|
assert.ok(fixtures.length > 0, 'test/fixtures/ ne porte aucune donnée');
|
||||||
|
assert.ok(livres.length > 0, 'aucun fichier livré sous exemples/ ni src/demo/livrees/');
|
||||||
|
const octets = new Map(livres.map((fichier) => [fichier, readFileSync(fichier)]));
|
||||||
|
return fixtures.flatMap((fixture) => {
|
||||||
|
const contenu = readFileSync(fixture);
|
||||||
|
return livres
|
||||||
|
.filter((livre) => octets.get(livre).equals(contenu))
|
||||||
|
.map((livre) => `${enBarres(racine, fixture)} = ${enBarres(racine, livre)}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Les seules épreuves qui chargent fast-check (§ 14.12) : la formule du
|
||||||
|
// plafond et la détection du séparateur, qui gagnent au rétrécissement ; et
|
||||||
|
// deux propriétés du stockage, sur des paires de charges et des séances
|
||||||
|
// tirées, dont le contre-exemple rétréci tient en quelques opérations.
|
||||||
|
const CHARGEURS_DE_FAST_CHECK = [
|
||||||
|
{ fichier: 'src/moteur/plafond.test.js', raison: 'la formule du plafond, arbitrée par énumération exhaustive' },
|
||||||
|
{ fichier: 'src/moteur/plafond.long.test.js', raison: 'la grande grille du plafond, sous la même graine écrite' },
|
||||||
|
{ fichier: 'src/csv/lecture.test.js', raison: 'la détection du séparateur, arbitrée par construction' },
|
||||||
|
{
|
||||||
|
fichier: 'src/stockage/correctifs.test.js',
|
||||||
|
raison: 'l’aller-retour appliquer(a, difference(a, b)) sur des paires de charges tirées',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fichier: 'src/stockage/journal.long.test.js',
|
||||||
|
raison: 'chaque instant d’une séance tirée se restitue tel qu’il s’est écrit',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Relevé des chargements de fast-check, sous-chemins compris, dans les
|
||||||
|
// fichiers JavaScript et les composants du projet, hors des épreuves
|
||||||
|
// nommées : chacun « fichier:ligne spécificateur », par fichier puis dans
|
||||||
|
// l'ordre du texte ; puis « exception sans objet : fichier » pour une
|
||||||
|
// épreuve nommée qui ne le charge pas. Un chargement écrit dans une chaîne,
|
||||||
|
// donnée d'épreuve, n'en est pas un. Lève quand le projet ne porte aucun
|
||||||
|
// module.
|
||||||
|
function releverFastCheck(racine, nommes = CHARGEURS_DE_FAST_CHECK) {
|
||||||
|
const fichiers = fichiersDuProjet(racine).filter(
|
||||||
|
(fichier) => EXTENSIONS_MODULE.has(extname(fichier)) || extname(fichier) === '.svelte',
|
||||||
|
);
|
||||||
|
assert.ok(fichiers.length > 0, 'aucun module dans le projet');
|
||||||
|
const chargeurs = new Set();
|
||||||
|
const refus = [];
|
||||||
|
for (const fichier of fichiers) {
|
||||||
|
const nom = enBarres(racine, fichier);
|
||||||
|
const texte = readFileSync(fichier, 'utf8');
|
||||||
|
const code = codeSansChaines(fichier, texte);
|
||||||
|
const trouves = chargementsLitteraux(texte).filter(
|
||||||
|
({ indice, specificateur }) => estDuCode(code, indice) && nomDePaquet(specificateur) === 'fast-check',
|
||||||
|
);
|
||||||
|
if (trouves.length > 0) chargeurs.add(nom);
|
||||||
|
if (nommes.some((e) => e.fichier === nom)) continue;
|
||||||
|
refus.push(...trouves.map(({ indice, specificateur }) => `${nom}:${ligneDe(texte, indice)} ${specificateur}`));
|
||||||
|
}
|
||||||
|
const sansObjet = nommes.filter(({ fichier }) => !chargeurs.has(fichier)).map(({ fichier }) => `exception sans objet : ${fichier}`);
|
||||||
|
return [...refus, ...sansObjet];
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('arborescence : un seul chemin de code pour les réservations (§ 5.2)', () => {
|
||||||
|
test('dans src/moteur, seuls configuration.js et diagnostic.js lisent les réservations', () => {
|
||||||
|
assert.deepEqual(releverLecteursDesReservations(RACINE), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('la garde relève chaque lecture par sa ligne — propriété, déstructuration, crochet —, hors commentaires, chaînes et épreuves ; un lecteur admis qui ne lit plus est sans objet', () => {
|
||||||
|
const fichiers = {
|
||||||
|
'src/moteur/configuration.js': 'export const normaliser = ({ reservations }) => reservations.length;\n',
|
||||||
|
'src/moteur/diagnostic.js': 'export const sonder = (c) => c.reservations;\n',
|
||||||
|
'src/moteur/recherche.js': [
|
||||||
|
'// les reservations sont fixées avant la recherche',
|
||||||
|
'export const rechercher = (configuration) => {',
|
||||||
|
' const { tables, reservations } = configuration;',
|
||||||
|
" const motif = 'reservations';",
|
||||||
|
' return configuration?.reservations ?? configuration["reservations"];',
|
||||||
|
'};',
|
||||||
|
].join('\n'),
|
||||||
|
'src/moteur/types.js': '/** @property {Reservation[]} reservations */\nexport const fixerReservations = 1;\n',
|
||||||
|
'src/moteur/sous/profond.js': 'export const lire = (c) => c.reservations;\n',
|
||||||
|
'src/moteur/recherche.test.js': 'const r = configuration.reservations;\n',
|
||||||
|
};
|
||||||
|
assert.deepEqual(avecArbre(fichiers, releverLecteursDesReservations), [
|
||||||
|
'src/moteur/recherche.js:3 reservations',
|
||||||
|
'src/moteur/recherche.js:5 reservations',
|
||||||
|
"src/moteur/recherche.js:5 ['reservations']",
|
||||||
|
'src/moteur/sous/profond.js:1 reservations',
|
||||||
|
]);
|
||||||
|
const sansLecture = { ...fichiers, 'src/moteur/diagnostic.js': 'export const sonder = (c) => c.tables;\n' };
|
||||||
|
assert.deepEqual(avecArbre(sansLecture, releverLecteursDesReservations).at(-1), 'exception sans objet : src/moteur/diagnostic.js');
|
||||||
|
assert.throws(() => avecArbre({ 'src/moteur/a.test.js': '' }, releverLecteursDesReservations), /src\/moteur ne fournit aucun module/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('arborescence : une seule langue (§ 13.2)', () => {
|
||||||
|
test('sous src/, electron/ et scripts/, rien que du JavaScript, des composants Svelte, des feuilles de style, des données JSON et les scripts d’installation nommés', () => {
|
||||||
|
assert.deepEqual(releverLangages(RACINE), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('la garde relève un second langage, à toute profondeur, et un fichier sans extension ; un script nommé hors de sa place l’est aussi ; un script nommé absent est sans objet', () => {
|
||||||
|
const fichiers = {
|
||||||
|
'src/moteur/a.js': '',
|
||||||
|
'src/moteur/b.mjs': '',
|
||||||
|
'electron/c.cjs': '',
|
||||||
|
'src/interface/D.svelte': '',
|
||||||
|
'src/interface/jetons.css': '',
|
||||||
|
'src/demo/livrees/e.gtt.json': '{}',
|
||||||
|
'src/moteur/calcul.py': '',
|
||||||
|
'src/geometrie/vue.ts': '',
|
||||||
|
'src/moteur/sous/recherche.wasm': '',
|
||||||
|
'electron/Makefile': '',
|
||||||
|
'scripts/outil.rb': '',
|
||||||
|
'scripts/construire_windows.sh': '',
|
||||||
|
'src/installation/avec_node.sh': '',
|
||||||
|
'test/aide.py': '',
|
||||||
|
'node_modules/p/x.py': '',
|
||||||
|
};
|
||||||
|
const releves = avecArbre(fichiers, releverLangages);
|
||||||
|
assert.deepEqual(
|
||||||
|
releves.filter((r) => !r.startsWith('exception sans objet')),
|
||||||
|
[
|
||||||
|
'electron/Makefile : sans extension',
|
||||||
|
'scripts/outil.rb : .rb',
|
||||||
|
'src/geometrie/vue.ts : .ts',
|
||||||
|
'src/installation/avec_node.sh : .sh',
|
||||||
|
'src/moteur/calcul.py : .py',
|
||||||
|
'src/moteur/sous/recherche.wasm : .wasm',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert.ok(releves.includes('exception sans objet : scripts/installation/avec_node.sh'));
|
||||||
|
assert.ok(!releves.includes('exception sans objet : scripts/construire_windows.sh'));
|
||||||
|
assert.throws(() => avecArbre({ 'test/a.js': '' }, releverLangages), /aucun fichier sous src\/, electron\/ et scripts\//);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('les fichiers cachés et les restes d\u{2019}éditeur ou de fusion ne sont pas du projet : la garde les passe, et relève encore ce qui les côtoie', () => {
|
||||||
|
const fichiers = {
|
||||||
|
'src/moteur/recherche.js': '',
|
||||||
|
'src/moteur/.recherche.js.swp': '',
|
||||||
|
'src/interface/.DS_Store': '',
|
||||||
|
'src/moteur/recherche.js~': '',
|
||||||
|
'src/moteur/recherche.js.orig': '',
|
||||||
|
'electron/principal.cjs.rej': '',
|
||||||
|
'scripts/#notes.txt#': '',
|
||||||
|
'src/moteur/calcul.py': '',
|
||||||
|
'src/moteur/orig': '',
|
||||||
|
'src/moteur/calcul.py.bak': '',
|
||||||
|
};
|
||||||
|
const releves = avecArbre(fichiers, releverLangages).filter((r) => !r.startsWith('exception sans objet'));
|
||||||
|
assert.deepEqual(releves, ['src/moteur/calcul.py : .py', 'src/moteur/calcul.py.bak : .bak', 'src/moteur/orig : sans extension']);
|
||||||
|
assert.throws(
|
||||||
|
() => avecArbre({ 'src/moteur/.a.js.swp': '', 'electron/a.cjs~': '' }, releverLangages),
|
||||||
|
/aucun fichier sous src\/, electron\/ et scripts\//,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('arborescence : où vivent les épreuves (§ 14.9, § 14.12)', () => {
|
||||||
|
test('chaque épreuve de src/ importe un module de son propre répertoire, hors des exceptions nommées', () => {
|
||||||
|
assert.deepEqual(releverEpreuvesEloignees(RACINE), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('la garde relève une épreuve qui n’importe que d’ailleurs — d’un sous-répertoire, d’un parent, une autre épreuve voisine —, à toute profondeur ; un composant voisin compte ; une exception sans objet se relève', () => {
|
||||||
|
const fichiers = {
|
||||||
|
'src/moteur/plafond.js': '',
|
||||||
|
'src/moteur/plafond.test.js': "import { plafond } from './plafond.js';\n",
|
||||||
|
'src/moteur/loin.test.js': "import { x } from '../csv/lecture.js';\nimport { y } from './sous/y.js';\n",
|
||||||
|
'src/moteur/sous/y.js': '',
|
||||||
|
'src/moteur/sous/y.long.test.js': "import { x } from '../plafond.js';\n",
|
||||||
|
'src/moteur/voisine.test.js': "import './plafond.test.js';\n// import { plafond } from './plafond.js';\n",
|
||||||
|
'src/csv/lecture.js': '',
|
||||||
|
'src/interface/Plan.svelte': '',
|
||||||
|
'src/interface/Plan.navigateur.test.js': "import Plan from './Plan.svelte';\n",
|
||||||
|
'src/interface/travailleur.js': '',
|
||||||
|
'src/interface/Travail.navigateur.test.js': "import Travailleur from './travailleur.js?worker&inline';\n",
|
||||||
|
'src/interface/Ecran.navigateur.test.js': "import { monter } from '../../test/montage.js';\n",
|
||||||
|
'test/a.test.js': "import { x } from '../src/csv/lecture.js';\n",
|
||||||
|
};
|
||||||
|
const exceptions = [
|
||||||
|
{ fichier: 'src/interface/Ecran.navigateur.test.js', raison: 'monte l’application entière' },
|
||||||
|
{ fichier: 'src/interface/Absente.navigateur.test.js', raison: 'disparue' },
|
||||||
|
];
|
||||||
|
assert.deepEqual(avecArbre(fichiers, (racine) => releverEpreuvesEloignees(racine, exceptions)), [
|
||||||
|
'src/moteur/loin.test.js : aucun module de src/moteur',
|
||||||
|
'src/moteur/sous/y.long.test.js : aucun module de src/moteur/sous',
|
||||||
|
'src/moteur/voisine.test.js : aucun module de src/moteur',
|
||||||
|
'exception sans objet : src/interface/Absente.navigateur.test.js',
|
||||||
|
]);
|
||||||
|
assert.throws(() => avecArbre({ 'src/a.js': '' }, releverEpreuvesEloignees), /aucune épreuve sous src\//);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('aucune donnée de test/fixtures/ n’est, octet pour octet, un fichier d’exemples/ ou de src/demo/livrees/', () => {
|
||||||
|
assert.deepEqual(releverFixturesLivrees(RACINE), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('la garde relève chaque donnée d’épreuve égale à un fichier livré, à toute profondeur, et rien pour un contenu voisin', () => {
|
||||||
|
const fichiers = {
|
||||||
|
'exemples/participants.csv': 'nom\nIris Ombrelle\n',
|
||||||
|
'src/demo/livrees/petite.gtt.json': '{"format":1}\n',
|
||||||
|
'test/fixtures/csv/copie.csv': 'nom\nIris Ombrelle\n',
|
||||||
|
'test/fixtures/plan/sous/petite.gtt.json': '{"format":1}\n',
|
||||||
|
'test/fixtures/csv/voisine.csv': 'nom\nIris Ombrelle\r\n',
|
||||||
|
};
|
||||||
|
assert.deepEqual(avecArbre(fichiers, releverFixturesLivrees), [
|
||||||
|
'test/fixtures/csv/copie.csv = exemples/participants.csv',
|
||||||
|
'test/fixtures/plan/sous/petite.gtt.json = src/demo/livrees/petite.gtt.json',
|
||||||
|
]);
|
||||||
|
assert.throws(() => avecArbre({ 'exemples/a.csv': 'x' }, releverFixturesLivrees), /test\/fixtures\/ ne porte aucune donnée/);
|
||||||
|
assert.throws(() => avecArbre({ 'test/fixtures/a.csv': 'x' }, releverFixturesLivrees), /aucun fichier livré/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fast-check ne se charge que dans les épreuves que le § 14.12 nomme', () => {
|
||||||
|
assert.deepEqual(releverFastCheck(RACINE), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('la garde relève chaque chargement de fast-check hors de la liste, à toute profondeur, et non dans une chaîne ; un fichier nommé qui ne le charge plus est sans objet', () => {
|
||||||
|
const fichiers = {
|
||||||
|
'src/moteur/plafond.test.js': "import fc from 'fast-check';\n",
|
||||||
|
'src/moteur/recherche.test.js': "import fc from 'fast-check';\nconst arb = require('fast-check/lib/arbitrary');\n",
|
||||||
|
'src/stockage/sous/depot.long.test.js': 'const fc = await import(`fast-check`);\n',
|
||||||
|
'test/aide.js': "const texte = \"import fc from 'fast-check';\";\n",
|
||||||
|
'node_modules/p/x.js': "import fc from 'fast-check';\n",
|
||||||
|
};
|
||||||
|
const nommes = [
|
||||||
|
{ fichier: 'src/moteur/plafond.test.js', raison: 'la formule du plafond' },
|
||||||
|
{ fichier: 'src/csv/lecture.test.js', raison: 'la détection du séparateur' },
|
||||||
|
];
|
||||||
|
assert.deepEqual(avecArbre(fichiers, (racine) => releverFastCheck(racine, nommes)), [
|
||||||
|
'src/moteur/recherche.test.js:1 fast-check',
|
||||||
|
'src/moteur/recherche.test.js:2 fast-check/lib/arbitrary',
|
||||||
|
'src/stockage/sous/depot.long.test.js:1 fast-check',
|
||||||
|
'exception sans objet : src/csv/lecture.test.js',
|
||||||
|
]);
|
||||||
|
assert.throws(() => avecArbre({ 'README.md': '' }, releverFastCheck), /aucun module dans le projet/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,9 @@
|
||||||
// l'arborescence (test/arborescence.test.js) et du livrable
|
// l'arborescence (test/arborescence.test.js) et du livrable
|
||||||
// (test/livrable.test.js) : import et export statiques, import seul, import()
|
// (test/livrable.test.js) : import et export statiques, import seul, import()
|
||||||
// et require(). Un composant Svelte se lit par le texte de ses blocs de
|
// et require(). Un composant Svelte se lit par le texte de ses blocs de
|
||||||
// script, que rend scriptsDe.
|
// script et des import() de son gabarit, que rend scriptsDe.
|
||||||
//
|
import { parse } from 'svelte/compiler';
|
||||||
|
|
||||||
// Le relevé lit le texte entier, commentaires et chaînes compris. Une
|
// Le relevé lit le texte entier, commentaires et chaînes compris. Une
|
||||||
// instruction à « from » se lit à partir de ce mot, que seuls import et
|
// instruction à « from » se lit à partir de ce mot, que seuls import et
|
||||||
// export font suivre d'une chaîne : un motif parti du mot-clé s'arrêterait à
|
// export font suivre d'une chaîne : un motif parti du mot-clé s'arrêterait à
|
||||||
|
|
@ -58,19 +59,51 @@ export function blocsScript(texte) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Le texte des blocs de script d'un composant Svelte, à la longueur du
|
// Les import() du gabarit d'un composant Svelte, en étendues { debut, fin }
|
||||||
// composant : chaque caractère hors de ces blocs devient une espace, fins de
|
// dans l'ordre du texte : chaque nœud ImportExpression de l'arbre que rend
|
||||||
// ligne gardées, si bien que les indices et les numéros de ligne restent
|
// l'analyseur de Svelte (svelte/compiler) hors des blocs de script et de la
|
||||||
// ceux du composant. Le balisage, le style et les expressions du gabarit ne
|
// feuille de style — un bloc {#await import(…)}, un attribut, une
|
||||||
// sont pas lus : un composant ne charge un module que dans un bloc de script,
|
// expression à toute profondeur. Le texte d'un nœud n'est pas une
|
||||||
// et le texte d'un nœud — « import » dans une phrase — n'en charge aucun.
|
// expression : « import » dans une phrase n'en donne aucun. null quand
|
||||||
|
// l'analyseur refuse le composant.
|
||||||
|
function importsDuGabarit(texte) {
|
||||||
|
let arbre;
|
||||||
|
try {
|
||||||
|
arbre = parse(texte, { modern: true });
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const etendues = [];
|
||||||
|
const vus = new WeakSet();
|
||||||
|
const parcourir = (noeud) => {
|
||||||
|
if (noeud === null || typeof noeud !== 'object' || vus.has(noeud)) return;
|
||||||
|
vus.add(noeud);
|
||||||
|
if (noeud.type === 'ImportExpression') etendues.push({ debut: noeud.start, fin: noeud.end });
|
||||||
|
for (const valeur of Object.values(noeud)) parcourir(valeur);
|
||||||
|
};
|
||||||
|
parcourir(arbre.fragment);
|
||||||
|
return etendues.sort((a, b) => a.debut - b.debut);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Le texte qu'un relevé de chargements lit dans un composant Svelte, à la
|
||||||
|
// longueur du composant : ses blocs de script et les import() de son
|
||||||
|
// gabarit gardés, tout autre caractère devenu une espace, fins de ligne
|
||||||
|
// gardées, si bien que les indices et les numéros de ligne restent ceux du
|
||||||
|
// composant. Le balisage, le style et les autres expressions du gabarit ne
|
||||||
|
// sont pas lus : le texte d'un nœud — « import » dans une phrase — ne charge
|
||||||
|
// aucun module. Un composant que l'analyseur de Svelte refuse se lit en
|
||||||
|
// entier, gabarit compris : le relevé y trouve plus, jamais moins, et la
|
||||||
|
// construction le refuse de toute façon.
|
||||||
export function scriptsDe(texte) {
|
export function scriptsDe(texte) {
|
||||||
|
const gabarit = importsDuGabarit(texte);
|
||||||
|
if (gabarit === null) return texte;
|
||||||
const blanc = (morceau) => morceau.replace(/[^\n]/g, ' ');
|
const blanc = (morceau) => morceau.replace(/[^\n]/g, ' ');
|
||||||
|
const gardes = [...blocsScript(texte), ...gabarit].sort((a, b) => a.debut - b.debut);
|
||||||
let sortie = '';
|
let sortie = '';
|
||||||
let fin = 0;
|
let fin = 0;
|
||||||
for (const bloc of blocsScript(texte)) {
|
for (const etendue of gardes) {
|
||||||
sortie += blanc(texte.slice(fin, bloc.debut)) + texte.slice(bloc.debut, bloc.fin);
|
sortie += blanc(texte.slice(fin, etendue.debut)) + texte.slice(etendue.debut, etendue.fin);
|
||||||
fin = bloc.fin;
|
fin = etendue.fin;
|
||||||
}
|
}
|
||||||
return sortie + blanc(texte.slice(fin));
|
return sortie + blanc(texte.slice(fin));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,8 +93,8 @@ describe('chargements', () => {
|
||||||
|
|
||||||
describe('chargements : les blocs de script d’un composant Svelte', () => {
|
describe('chargements : les blocs de script d’un composant Svelte', () => {
|
||||||
// Un composant à deux blocs, dont l'un sous attribut, du balisage qui écrit
|
// Un composant à deux blocs, dont l'un sous attribut, du balisage qui écrit
|
||||||
// « import » en texte et en expression, un style à @import, et une balise
|
// « import » en texte, un import() en expression du gabarit, un style à
|
||||||
// de script en commentaire HTML.
|
// @import, et une balise de script en commentaire HTML.
|
||||||
const COMPOSANT = [
|
const COMPOSANT = [
|
||||||
'<script module>',
|
'<script module>',
|
||||||
" import { tirer } from './module.js';",
|
" import { tirer } from './module.js';",
|
||||||
|
|
@ -113,13 +113,38 @@ describe('chargements : les blocs de script d’un composant Svelte', () => {
|
||||||
'</style>',
|
'</style>',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
test('le texte rendu garde la longueur et les lignes du composant ; seuls les blocs de script y restent', () => {
|
test('le texte rendu garde la longueur et les lignes du composant ; les blocs de script et les import() du gabarit y restent', () => {
|
||||||
const scripts = scriptsDe(COMPOSANT);
|
const scripts = scriptsDe(COMPOSANT);
|
||||||
assert.equal(scripts.length, COMPOSANT.length);
|
assert.equal(scripts.length, COMPOSANT.length);
|
||||||
assert.equal(scripts.split('\n').length, COMPOSANT.split('\n').length);
|
assert.equal(scripts.split('\n').length, COMPOSANT.split('\n').length);
|
||||||
assert.deepEqual(lus(scripts), ['2 ./module.js', '6 svelte', '7 ./Voisin.svelte']);
|
assert.deepEqual(lus(scripts), ['2 ./module.js', '6 svelte', '7 ./Voisin.svelte', '12 ./expression.js']);
|
||||||
assert.deepEqual(scripts.split('\n')[5], " import { onMount } from 'svelte';");
|
assert.deepEqual(scripts.split('\n')[5], " import { onMount } from 'svelte';");
|
||||||
assert.match(scripts.split('\n')[10], /^ *$/);
|
assert.match(scripts.split('\n')[10], /^ *$/);
|
||||||
|
assert.equal(scripts.split('\n')[11], " import('./expression.js') ");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('un import() du gabarit se lit à sa place, à toute profondeur — bloc {#await}, attribut, bloc imbriqué — ; le mot « import » d’un nœud de texte ou d’une chaîne ne se lit pas', () => {
|
||||||
|
const texte = [
|
||||||
|
'<script>',
|
||||||
|
" let ouvert = $state(false);",
|
||||||
|
'</script>',
|
||||||
|
"{#await import('../stockage/depot.js') then depot}{/await}",
|
||||||
|
'{#if ouvert}',
|
||||||
|
" <button onclick={() => import('./panneau.js')}>import(</button>",
|
||||||
|
' {#each [1] as n}',
|
||||||
|
" <p title={'import(\\'./chaine.js\\')'}>{n}</p>",
|
||||||
|
' {/each}',
|
||||||
|
'{/if}',
|
||||||
|
"<p>import('./texte.js')</p>",
|
||||||
|
].join('\n');
|
||||||
|
const scripts = scriptsDe(texte);
|
||||||
|
assert.equal(scripts.length, texte.length);
|
||||||
|
assert.deepEqual(lus(scripts), ['4 ../stockage/depot.js', '6 ./panneau.js']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('un composant que l’analyseur de Svelte refuse se lit en entier, gabarit compris : la garde relève plus, jamais moins', () => {
|
||||||
|
const texte = "<p>{import('./a.js')}</p>\n<div>\n<script>\nimport { b } from './b.js';";
|
||||||
|
assert.deepEqual(lus(scriptsDe(texte)), ['1 ./a.js', '4 ./b.js']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('les étendues désignent le contenu de chaque bloc, balises exclues, et un bloc non fermé court jusqu’à la fin', () => {
|
test('les étendues désignent le contenu de chaque bloc, balises exclues, et un bloc non fermé court jusqu’à la fin', () => {
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,19 @@
|
||||||
|
|
||||||
// Le livrable tel qu'electron-builder.config.cjs le décrit : son nom (§ 18.2),
|
// Le livrable tel qu'electron-builder.config.cjs le décrit : son nom (§ 18.2),
|
||||||
// sa forme (§ 16), ce qu'il emporte (§ 14.15) et l'identité inscrite dans sa
|
// sa forme (§ 16), ce qu'il emporte (§ 14.15) et l'identité inscrite dans sa
|
||||||
// ressource de version ; et le nom de l'application, un seul pour Electron,
|
// ressource de version ; le nom de l'application, un seul pour Electron,
|
||||||
// pour Capacitor et pour le titre de la fenêtre (§ 18.6). La configuration se
|
// pour Capacitor et pour le titre de la fenêtre (§ 18.6) ; le répertoire de
|
||||||
// charge ici comme electron-builder la charge, par require.
|
// la page construite, www/, que Vite, Capacitor, la coquille et
|
||||||
|
// electron-builder désignent ensemble (§ 13.1) ; la langue que la page passe
|
||||||
|
// à App, jamais la langue témoin (§ 14.6) ; et les démonstrations, composées
|
||||||
|
// dans src/main.js comme dans le montage des épreuves du navigateur. La
|
||||||
|
// configuration se charge ici comme electron-builder la charge, par require.
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { copyFileSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
import { copyFileSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
import { createRequire, isBuiltin } from 'node:module';
|
import { createRequire, isBuiltin } from 'node:module';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { dirname, join, relative, resolve, sep } from 'node:path';
|
import { dirname, join, relative, resolve, sep } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
import { deriver } from '../scripts/version.js';
|
import { deriver } from '../scripts/version.js';
|
||||||
import { titreAvecVersion } from '../src/interface/libelles.js';
|
import { titreAvecVersion } from '../src/interface/libelles.js';
|
||||||
import { VERSION } from '../src/version.genere.js';
|
import { VERSION } from '../src/version.genere.js';
|
||||||
|
|
@ -219,3 +223,103 @@ describe('livrable : identité', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Où vit la page construite, lu dans chaque fichier qui la désigne : la
|
||||||
|
// sortie de la construction de Vite (vite.config.js, chargé comme Vite le
|
||||||
|
// charge), le répertoire web de Capacitor, la page que la coquille charge
|
||||||
|
// par loadFile — la constante que l'appel nomme, une adresse relative à
|
||||||
|
// electron/main.js —, rendue relative à la racine, ou null quand l'appel ou
|
||||||
|
// sa constante manquent ; et si electron-builder emporte www/ en entier.
|
||||||
|
async function pageConstruite(racine) {
|
||||||
|
const { default: vite } = await import(pathToFileURL(join(racine, 'vite.config.js')).href);
|
||||||
|
const coquille = readFileSync(join(racine, 'electron', 'main.js'), 'utf8');
|
||||||
|
const appel = /\bloadFile\s*\(\s*([\p{L}_$][\p{L}\p{N}_$]*)\s*\)/u.exec(coquille);
|
||||||
|
const constante =
|
||||||
|
appel &&
|
||||||
|
new RegExp(
|
||||||
|
String.raw`\bconst\s+${appel[1]}\s*=\s*fileURLToPath\(\s*new\s+URL\(\s*(['"])([^'"]+)\1\s*,\s*import\.meta\.url\s*\)\s*\)`,
|
||||||
|
).exec(coquille);
|
||||||
|
return {
|
||||||
|
sortieDeVite: vite.build?.outDir,
|
||||||
|
repertoireWeb: lireJson(racine, 'capacitor.config.json').webDir,
|
||||||
|
pageDeLaCoquille: constante ? relative(racine, resolve(racine, 'electron', constante[2])).split(sep).join('/') : null,
|
||||||
|
emporte: charger(racine).files.includes('www/**'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// La langue que l'appel mount(App, …) d'un texte passe à App : le texte de
|
||||||
|
// la valeur de sa clé langue, tel qu'écrit — « 'fr' » pour la chaîne —, ou
|
||||||
|
// null sans appel ou sans clé écrite « langue: ». L'appel court jusqu'à la
|
||||||
|
// ligne qui s'ouvre sur « }); ». Lève quand l'appel en porte deux.
|
||||||
|
function langueDeLaPage(texte) {
|
||||||
|
const debut = texte.search(/\bmount\s*\(\s*App\s*,/);
|
||||||
|
if (debut === -1) return null;
|
||||||
|
const fin = texte.indexOf('\n});', debut);
|
||||||
|
const appel = texte.slice(debut, fin === -1 ? texte.length : fin);
|
||||||
|
const langues = [...appel.matchAll(/\blangue\s*:\s*([^,\n}]+?)\s*(?=[,\n}])/g)].map(([, valeur]) => valeur);
|
||||||
|
if (langues.length > 1) throw new Error(`deux langues passées à App : ${langues.join(', ')}`);
|
||||||
|
return langues[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// L'expression qui compose les démonstrations d'App dans un texte : la
|
||||||
|
// valeur de l'unique déclaration const dont l'initialiseur nomme LIVREES,
|
||||||
|
// blancs ramenés à un seul, retirés après une parenthèse, une accolade ou un
|
||||||
|
// crochet ouvrants et avant leurs fermants, virgule finale retirée. Lève
|
||||||
|
// quand le texte n'en porte aucune, ou plusieurs.
|
||||||
|
function compositionDesDemonstrations(texte) {
|
||||||
|
const compositions = [...texte.matchAll(/\bconst\s+[\p{L}_$][\p{L}\p{N}_$]*\s*=\s*([^;]*\bLIVREES\b[^;]*);/gu)].map(([, valeur]) =>
|
||||||
|
valeur
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.replace(/,\s*([)}\]])/g, '$1')
|
||||||
|
.replace(/([({[])\s+/g, '$1')
|
||||||
|
.replace(/\s+([)}\]])/g, '$1')
|
||||||
|
.trim(),
|
||||||
|
);
|
||||||
|
if (compositions.length === 0) throw new Error('aucune composition de LIVREES');
|
||||||
|
if (compositions.length > 1) throw new Error('deux compositions de LIVREES');
|
||||||
|
return compositions[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('livrable : la page construite et ce que la coquille lui passe', () => {
|
||||||
|
test('la page construite vit dans www/ : la sortie de Vite, le répertoire web de Capacitor, la page que charge la coquille et ce qu’emporte electron-builder (§ 13.1)', async () => {
|
||||||
|
const page = await pageConstruite(RACINE);
|
||||||
|
assert.deepEqual(page, {
|
||||||
|
sortieDeVite: 'www',
|
||||||
|
repertoireWeb: 'www',
|
||||||
|
pageDeLaCoquille: 'www/index.html',
|
||||||
|
emporte: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('src/main.js passe langue: \'fr\' à App : la construction livrée n’emploie jamais la langue témoin (§ 14.6)', () => {
|
||||||
|
assert.equal(langueDeLaPage(readFileSync(join(RACINE, 'src', 'main.js'), 'utf8')), "'fr'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('la langue lue est la seule que porte mount(App, …) : la langue témoin, une variable, une clé abrégée ou absente ne sont pas « fr »', () => {
|
||||||
|
const page = (ligne) => ["import App from './App.svelte';", 'mount(App, {', ' target: document.body,', ' props: {', ligne, ' demonstrations,', ' },', '});'].join('\n');
|
||||||
|
assert.equal(langueDeLaPage(page(" langue: 'fr',")), "'fr'");
|
||||||
|
assert.equal(langueDeLaPage(page(" langue: 'temoin',")), "'temoin'");
|
||||||
|
assert.equal(langueDeLaPage(page(' langue: LANGUE,')), 'LANGUE');
|
||||||
|
assert.equal(langueDeLaPage(page(' langue,')), null);
|
||||||
|
assert.equal(langueDeLaPage(page('')), null);
|
||||||
|
assert.equal(langueDeLaPage("const langue = 'fr';\nmount(Autre, { props: { langue: 'fr' } });\n"), null);
|
||||||
|
assert.throws(() => langueDeLaPage(page(" langue: 'fr',\n langue: 'temoin',")), /deux langues/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('les démonstrations que monterApp donne à App se composent comme celles de src/main.js', () => {
|
||||||
|
const deMain = compositionDesDemonstrations(readFileSync(join(RACINE, 'src', 'main.js'), 'utf8'));
|
||||||
|
const duMontage = compositionDesDemonstrations(readFileSync(join(RACINE, 'test', 'navigateur', 'montage.js'), 'utf8'));
|
||||||
|
assert.ok(deMain.startsWith('LIVREES.'), deMain);
|
||||||
|
assert.equal(duMontage, deMain);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('la composition se lit à blancs et virgule finale près ; un filtre, un autre ordre ou deux compositions se distinguent', () => {
|
||||||
|
const une = 'const d = LIVREES.map(({ cle, texte }) => ({\n cle,\n nom: CATALOGUE.find((e) => e.cle === cle).nom,\n texte,\n}));\n';
|
||||||
|
const resserree = 'const demonstrations = LIVREES.map(({ cle, texte }) => ({ cle, nom: CATALOGUE.find((e) => e.cle === cle).nom, texte }));\n';
|
||||||
|
assert.equal(compositionDesDemonstrations(une), compositionDesDemonstrations(resserree));
|
||||||
|
const filtree = une.replace('LIVREES.map', "LIVREES.filter(({ cle }) => cle !== 'grande').map");
|
||||||
|
assert.notEqual(compositionDesDemonstrations(filtree), compositionDesDemonstrations(une));
|
||||||
|
assert.throws(() => compositionDesDemonstrations('const d = [];\n'), /aucune composition de LIVREES/);
|
||||||
|
assert.throws(() => compositionDesDemonstrations(une + une), /deux compositions de LIVREES/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1176,6 +1176,11 @@ describe('sources de l’interface : les couleurs (§ 12.7, § 13.3, § 13.5)',
|
||||||
test('un arbre sans composant ni feuille fait échouer la garde', () => {
|
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é/);
|
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)', () => {
|
describe('sources de l’interface : balisage et fenêtres du navigateur (§ 13.1, § 2.1)', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue