gestion_table_tournante_libre/scripts/essai_demarrage.test.js
Mathieu Benoit 88d35f5979 [IMP] interface: Noto Sans everywhere, faces loaded before mounting
The screen must match the screenshots and the text the tests measure. The
interface now uses the embedded Noto Sans for every element, form controls
included, with a 1.3 line height. The entry point and the test mount wait
for both faces before mounting, so no text measurer caches the fallback
width; a face that fails to load is reported and the app mounts anyway.
The screens the wider font overflowed have their own CSS adjusted.
Checked: red first; 3216 node, 655 browser and 71 long tests green from the
index; the Electron start-up check loads the font from file://.

--- FR ---

[IMP] interface : Noto Sans partout, faces chargées avant le montage

L'écran doit ressembler aux captures et au texte que mesurent les épreuves.
L'interface emploie désormais Noto Sans, embarquée, pour tout élément,
contrôles de formulaire compris, avec un interligne de 1,3. Le point
d'entrée et le montage d'épreuve attendent les deux faces avant de monter :
aucun mesureur ne garde la largeur de repli ; une face qui ne se charge pas
est signalée et l'application se monte quand même. Les écrans que la police
plus large débordait ajustent leur propre CSS.
Vérifié : rouge d'abord ; 3216 node, 655 navigateur et 71 longues vertes
depuis l'index ; l'essai de démarrage charge la police par file://.

Assisted-by: Claude Opus 5.5
2026-10-09 13:01:49 -04:00

257 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// La police que l'essai relève dans la page chargée sous file:// (§ 19.4) :
// les deux faces de Noto Sans chargées, le corps du document en Noto Sans,
// et la chaîne témoin à la longueur des avances, dans la tolérance ; chaque
// écart se nomme.
//
// L'environnement où l'essai de démarrage lance la coquille (§ 8.6) : trois
// dossiers distincts sous la racine de l'essai, aucun sous un autre ; le
// lanceur portable publie exe, le poste a home pour dossier personnel et
// temp pour dossier temporaire, et la configuration XDG du poste réel n'y
// entre pas. Le module se charge sans rien lancer : l'essai ne part que
// lorsque node l'exécute.
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync, symlinkSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path, { dirname, join, relative, isAbsolute } from 'node:path';
import { fileURLToPath } from 'node:url';
import { decrireSysteme } from '../electron/fichiers.js';
import { estSous } from '../src/stockage/dossier_travail.js';
import { describe, test } from '../test/lanceur.js';
import {
donneesApplicativesSurExe,
dossiersDEssai,
environnementDEssai,
estLanceDirectement,
fautesDePolice,
} from './essai_demarrage.js';
const RACINE_ESSAI = join('/', 'racine-essai', 'gtt-essai-demarrage-x');
// Vrai quand b est a ou se trouve sous a.
const sous = (a, b) => {
const chemin = relative(a, b);
return chemin === '' || (!chemin.startsWith('..') && !isAbsolute(chemin));
};
describe('essai de démarrage : dossiers et environnement (§ 8.6)', () => {
test('trois dossiers distincts sous la racine, aucun sous un autre', () => {
const dossiers = dossiersDEssai(RACINE_ESSAI);
const noms = ['exe', 'home', 'temp'];
assert.deepEqual(Object.keys(dossiers).sort(), noms);
const chemins = noms.map((nom) => dossiers[nom]);
assert.equal(chemins.length, 3);
for (const chemin of chemins) assert.ok(sous(RACINE_ESSAI, chemin) && chemin !== RACINE_ESSAI, chemin);
for (const a of chemins) {
for (const b of chemins) if (a !== b) assert.ok(!sous(a, b), `${b} sous ${a}`);
}
assert.equal(new Set(chemins).size, 3);
});
test('exe publié, home et temp posés, XDG_CONFIG_HOME retiré, le reste gardé', () => {
const dossiers = dossiersDEssai(RACINE_ESSAI);
const poste = Object.freeze({
HOME: join('/', 'maison-originale'),
XDG_CONFIG_HOME: join('/', 'maison-originale', 'configuration'),
TMPDIR: join('/', 'temporaire-du-poste'),
PATH: join('/', 'outils'),
LANG: 'fr_CA.UTF-8',
});
const env = environnementDEssai(poste, dossiers);
assert.equal(env.PORTABLE_EXECUTABLE_DIR, dossiers.exe);
assert.equal(env.HOME, dossiers.home);
assert.equal(env.TMPDIR, dossiers.temp);
assert.equal(env.TEMP, dossiers.temp);
assert.equal(env.TMP, dossiers.temp);
assert.ok(!Object.hasOwn(env, 'XDG_CONFIG_HOME'));
assert.equal(env.PATH, poste.PATH);
assert.equal(env.LANG, poste.LANG);
// L'environnement du poste reste tel quel.
assert.equal(poste.HOME, join('/', 'maison-originale'));
});
test('sous X11 sans XAUTHORITY, le fichier d’autorisation reste celui du dossier personnel réel', () => {
const dossiers = dossiersDEssai(RACINE_ESSAI);
const maison = join('/', 'maison-originale');
const avecX = environnementDEssai({ HOME: maison, DISPLAY: ':9' }, dossiers);
assert.equal(avecX.XAUTHORITY, join(maison, '.Xauthority'));
const pose = join('/', 'autorisation-posee');
assert.equal(environnementDEssai({ HOME: maison, DISPLAY: ':9', XAUTHORITY: pose }, dossiers).XAUTHORITY, pose);
assert.ok(!Object.hasOwn(environnementDEssai({ HOME: maison }, dossiers), 'XAUTHORITY'));
});
test('sous Windows, un temporaire du poste rangé sous LOCALAPPDATA ne met pas exe sous les données applicatives', () => {
const W = path.win32;
const profil = 'C:\\Users\\profil-essai';
const local = W.join(profil, 'AppData', 'Local');
const poste = Object.freeze({
USERPROFILE: profil,
APPDATA: W.join(profil, 'AppData', 'Roaming'),
LOCALAPPDATA: local,
TEMP: W.join(local, 'Temp'),
TMP: W.join(local, 'Temp'),
});
const dossiers = dossiersDEssai(W.join(poste.TEMP, 'gtt-essai-demarrage-x'), W);
const env = environnementDEssai(poste, dossiers, W);
// Les données applicatives de la coquille (§ 8.6 point 2) : AppData,
// dossier connu que l'environnement ne déplace pas, le LOCALAPPDATA de
// son environnement, et le temporaire, que TMP désigne.
const emplacements = { insensibleCasse: true, separateur: '\\' };
assert.equal(env.PORTABLE_EXECUTABLE_DIR, W.join(poste.TEMP, 'gtt-essai-demarrage-x', 'exe'));
assert.deepEqual(
[poste.APPDATA, env.LOCALAPPDATA, env.TMP].filter((dossier) => estSous(dossiers.exe, dossier, emplacements)),
[],
);
// Le profil, AppData et LOCALAPPDATA de la coquille sont sous home.
assert.equal(env.USERPROFILE, dossiers.home);
assert.ok(estSous(env.APPDATA, dossiers.home, emplacements) && env.APPDATA !== dossiers.home, env.APPDATA);
assert.ok(estSous(env.LOCALAPPDATA, dossiers.home, emplacements) && env.LOCALAPPDATA !== dossiers.home, env.LOCALAPPDATA);
assert.deepEqual(donneesApplicativesSurExe(env, poste, 'win32'), []);
assert.equal(poste.LOCALAPPDATA, local);
});
test("la garde nomme les données applicatives qui portent exe, avant tout lancement", () => {
// Sous Linux : des chemins composés par path.posix de bout en bout, que
// le système hôte de l'épreuve ne change pas.
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);
assert.deepEqual(donneesApplicativesSurExe(env, {}, 'linux'), []);
// Le temporaire de la coquille au-dessus d'exe.
assert.deepEqual(donneesApplicativesSurExe({ ...env, TMPDIR: R }, {}, 'linux'), [R]);
// Le dossier de configuration XDG au-dessus d'exe.
assert.deepEqual(donneesApplicativesSurExe({ ...env, XDG_CONFIG_HOME: R }, {}, 'linux'), [R]);
// Sous Windows : l'AppData du poste, dossier connu, que l'environnement
// de la coquille ne déplace pas ; puis le LOCALAPPDATA de la coquille.
const W = path.win32;
const racine = W.join('D:\\', 'essais', 'gtt-essai-demarrage-x');
const dossiersW = dossiersDEssai(racine, W);
const envW = environnementDEssai({}, dossiersW, W);
assert.deepEqual(donneesApplicativesSurExe(envW, {}, 'win32'), []);
assert.deepEqual(donneesApplicativesSurExe(envW, { APPDATA: W.join('D:\\', 'essais') }, 'win32'), [
W.join('D:\\', 'essais'),
]);
assert.deepEqual(donneesApplicativesSurExe({ ...envW, LOCALAPPDATA: 'd:\\ESSAIS' }, {}, 'win32'), ['d:\\ESSAIS']);
// Sans exécutable publié, rien ne le porte.
assert.deepEqual(donneesApplicativesSurExe({ TMPDIR: '/' }, {}, 'linux'), []);
});
test('sous macOS, Application Support de la coquille et du poste ; sous Windows, TMP l’emporte sur TEMP', () => {
// macOS : un essai rangé sous le dossier Application Support du poste,
// que le HOME de la coquille ne déplace pas, puis sous celui de la
// coquille.
const P = path.posix;
const maison = P.join('/', 'maison-essai');
const support = P.join(maison, 'Library', 'Application Support');
const dossiers = dossiersDEssai(P.join(support, 'gtt-essai-demarrage-x'), P);
const env = environnementDEssai({ HOME: maison }, dossiers, P);
assert.deepEqual(donneesApplicativesSurExe(env, {}, 'darwin'), []);
assert.deepEqual(donneesApplicativesSurExe(env, { HOME: maison }, 'darwin'), [support]);
assert.deepEqual(donneesApplicativesSurExe({ ...env, HOME: maison }, {}, 'darwin'), [support]);
// Sous Linux, le même poste ne range rien sous Application Support.
assert.deepEqual(donneesApplicativesSurExe(env, { HOME: maison }, 'linux'), []);
// Windows : le temporaire est TMP, puis TEMP à défaut.
const W = path.win32;
const racine = W.join('D:\\', 'essais', 'gtt-essai-demarrage-x');
const envW = environnementDEssai({}, dossiersDEssai(racine, W), W);
const dessus = W.join('D:\\', 'essais');
const ailleurs = W.join('E:\\', 'temporaire');
assert.deepEqual(donneesApplicativesSurExe({ ...envW, TMP: dessus, TEMP: ailleurs }, {}, 'win32'), [dessus]);
assert.deepEqual(donneesApplicativesSurExe({ ...envW, TMP: ailleurs, TEMP: dessus }, {}, 'win32'), []);
assert.deepEqual(donneesApplicativesSurExe({ ...envW, TMP: '', TEMP: dessus }, {}, 'win32'), [dessus]);
});
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', () => {
const MODULE = fileURLToPath(new URL('./essai_demarrage.js', import.meta.url));
// Le lien désigne le dossier du module : une jonction sous Windows, qui se
// crée sans privilège, un lien symbolique ailleurs.
test('le script atteint par un lien symbolique est le module', () => {
const dossier = mkdtempSync(join(tmpdir(), 'gtt-essai-lien-'));
try {
const lien = join(dossier, 'scripts');
symlinkSync(dirname(MODULE), lien, 'junction');
assert.equal(estLanceDirectement(join(lien, 'essai_demarrage.js')), true);
assert.equal(estLanceDirectement(MODULE), true);
} finally {
rmSync(dossier, { recursive: true, force: true });
}
});
test('un autre script, un script absent ou aucun script ne lance pas l’essai', () => {
assert.equal(estLanceDirectement(fileURLToPath(import.meta.url)), false);
assert.equal(estLanceDirectement(join(tmpdir(), 'gtt-essai-absent', 'essai_demarrage.js')), false);
assert.equal(estLanceDirectement(undefined), false);
});
});
describe('essai de démarrage : la police de la page (§ 19.4)', () => {
// Un relevé conforme : les deux faces chargées, une face d'une autre
// famille, le corps en Noto Sans, la chaîne témoin à 1 000 px.
const RELEVE = Object.freeze({
faces: [
{ famille: 'Noto Sans', graisse: '400', style: 'normal', etat: 'loaded' },
{ famille: '"Noto Sans"', graisse: '700', style: 'normal', etat: 'loaded' },
{ famille: 'Famille Inventée', graisse: '400', style: 'normal', etat: 'unloaded' },
],
familleCorps: '"Noto Sans", system-ui, sans-serif',
largeurTemoin: 1000,
});
const TEMOIN = Object.freeze({ longueurAttendue: 1000, tolerance: 0.01 });
test('un relevé conforme ne porte aucune faute ; un écart égal à la tolérance est admis', () => {
assert.deepEqual(fautesDePolice(RELEVE, TEMOIN), []);
assert.deepEqual(fautesDePolice({ ...RELEVE, largeurTemoin: 1010 }, TEMOIN), []);
assert.deepEqual(fautesDePolice({ ...RELEVE, largeurTemoin: 990 }, TEMOIN), []);
});
test('une face absente ou non chargée se nomme par sa graisse ; une autre famille n’y entre pas', () => {
const sans700 = RELEVE.faces.filter(({ graisse, famille }) => !(graisse === '700' && famille.includes('Noto')));
assert.deepEqual(fautesDePolice({ ...RELEVE, faces: sans700 }, TEMOIN), ['face 700 de Noto Sans absente']);
const enErreur = RELEVE.faces.map((face) => (face.graisse === '400' && face.famille === 'Noto Sans' ? { ...face, etat: 'error' } : face));
assert.deepEqual(fautesDePolice({ ...RELEVE, faces: enErreur }, TEMOIN), ['face 400 de Noto Sans : error']);
assert.deepEqual(fautesDePolice({ ...RELEVE, faces: [] }, TEMOIN), [
'face 400 de Noto Sans absente',
'face 700 de Noto Sans absente',
]);
});
test('un corps du document dans une autre famille se nomme, telle que la page la calcule', () => {
assert.deepEqual(fautesDePolice({ ...RELEVE, familleCorps: 'system-ui, sans-serif' }, TEMOIN), [
'corps du document en system-ui, sans-serif',
]);
});
test('une chaîne témoin hors de la tolérance se nomme, mesurée et attendue', () => {
assert.deepEqual(fautesDePolice({ ...RELEVE, largeurTemoin: 1010.5 }, TEMOIN), ['chaîne témoin : 1010.5 px pour 1000 px']);
assert.deepEqual(fautesDePolice({ ...RELEVE, largeurTemoin: 960 }, TEMOIN), ['chaîne témoin : 960 px pour 1000 px']);
assert.deepEqual(fautesDePolice({ ...RELEVE, largeurTemoin: 0 }, TEMOIN), ['chaîne témoin : 0 px pour 1000 px']);
});
});