gestion_table_tournante_libre/scripts/banc/temoin.banc.js
Mathieu Benoit 0b2b2dc303 [ADD] bench: frame time, readable seats and witness string thresholds
Spec 19.10 leaves three rendering thresholds to measurement: the frame time
of the plan while zooming, the smallest seat target that stays readable,
and the width of the witness string under the embedded font. Three bench
files measure them in Chromium, frame time throttled four times, and write
each measure with its own CPU factor into the record; seuils.js derives the
threshold from the samples. No value enters the registry from here.
Checked: red first; 3576 node tests green from the index; the bench
project runs the three measures.

--- FR ---

[ADD] banc : seuils du temps par image, sièges lisibles, chaîne témoin

Le § 19.10 laisse trois seuils de rendu à la mesure : le temps par image du
plan pendant un zoom, la plus petite cible de siège qui reste lisible, et
la largeur de la chaîne témoin sous la police embarquée. Trois fichiers du
banc les mesurent dans Chromium, le temps par image ralenti quatre fois, et
écrivent chaque mesure avec son propre facteur au relevé ; seuils.js tire
le seuil des échantillons. Aucune valeur n'entre au registre d'ici.
Vérifié : rouge d'abord ; 3576 node verts depuis l'index ; le projet du
banc joue les trois mesures.

Assisted-by: Claude Opus 5.5
2026-10-09 15:27:10 -04:00

166 lines
6.7 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 tolérance de la chaîne témoin (§ 19.4, § 14.1), relevée par le banc :
// la longueur rendue de CHAINE_TEMOIN, par getComputedTextLength, dix fois
// sur la police embarquée, puis dix fois sur la famille de repli forcée — la
// police de l'interface sans Noto Sans, ce que le moteur rend quand la face
// manque. La longueur attendue se calcule des avances de la police
// (longueurTemoin, fontkit), jamais saisie. toleranceTemoin (seuils.js) en
// tire la valeur proposée : le quart de l'écart du repli, au moins trois fois
// la gigue des dix mesures de la police embarquée.
//
// Chaque mesure se prend sur un <text> neuf, décalé d'un dixième de pixel
// de la précédente : la gigue couvre le placement du texte au sous-pixel,
// non la seule relecture d'une longueur gardée. Crénage et ligatures sont
// coupés (font-kerning: none, font-variant-ligatures: none) : la longueur
// attendue est la somme des avances, sans l'un ni l'autre. Le corps est
// celui du texte courant de l'interface, 16 px, où l'arrondi des avances
// pèse le plus.
//
// La police embarquée doit être chargée et la première famille de la
// police de l'interface : sinon la mesure se refuse (C8). Une tolérance
// refusée s'écrit tout de même, valeur proposée null et refus nommé, et
// l'épreuve échoue : le relevé le dit, et le passage tombe.
import { afterAll, beforeAll, describe, expect, test } from 'vitest';
import { commands } from 'vitest/browser';
import { CONSTANTES } from '../../src/geometrie/constantes.js';
import { creerContexte } from '../../test/navigateur/ecrans.js';
import { CHAINE_TEMOIN, longueurTemoin } from '../../test/temoin.js';
import { controlerCadre, ecrireSortie } from './navigateur.js';
import { verifierReleve } from './releve.js';
import { toleranceTemoin } from './seuils.js';
const SVG = 'http://www.w3.org/2000/svg';
const FAMILLE = 'Noto Sans';
const CORPS_PX = 16;
const MESURES = 10;
const NOM = 'TOLERANCE_CHAINE_TEMOIN';
// Le nom d'une famille sans ses guillemets.
const sansGuillemets = (texte) => texte.trim().replace(/^(["'])(.*)\1$/, '$2');
let contexte = null;
let octets = null;
let mesure = null;
/**
* Les familles de la police de l'interface, dans l'ordre de
* --police-interface, sans guillemets.
* @returns {string[]}
*/
function famillesInterface() {
return getComputedStyle(document.documentElement).getPropertyValue('--police-interface').split(',').map(sansGuillemets);
}
/**
* Dix longueurs rendues de la chaîne témoin, en px, sur la famille donnée
* (une valeur de font-family), en graisse 400, au corps CORPS_PX : chacune
* sur un <text> neuf, posé à x = rang / 10 px, retiré après sa lecture.
* @param {string} famille
* @returns {number[]}
*/
function longueursRendues(famille) {
const svg = document.createElementNS(SVG, 'svg');
svg.setAttribute('width', '800');
svg.setAttribute('height', '40');
document.body.append(svg);
try {
return Array.from({ length: MESURES }, (_, rang) => {
const texte = document.createElementNS(SVG, 'text');
texte.setAttribute('x', String(rang / 10));
texte.setAttribute('y', '30');
texte.setAttribute(
'style',
`font-family: ${famille}; font-size: ${CORPS_PX}px; font-weight: 400; font-kerning: none; font-variant-ligatures: none`,
);
texte.textContent = CHAINE_TEMOIN;
svg.append(texte);
const longueur = texte.getComputedTextLength();
texte.remove();
return longueur;
});
} finally {
svg.remove();
}
}
/** Vrai quand la face 400 de la police embarquée est chargée dans le document. */
const faceChargee = () =>
[...document.fonts].some((face) => sansGuillemets(face.family) === FAMILLE && face.weight === '400' && face.status === 'loaded');
beforeAll(async () => {
await controlerCadre();
contexte = creerContexte();
const m = await contexte.monter();
({ regulier: octets } = await m.polices());
});
afterAll(async () => {
await contexte?.defaire();
});
describe('la tolérance de la chaîne témoin (§ 19.4)', () => {
test('la police embarquée est chargée et nommée en tête de la police de l’interface ; le repli forcé est le reste de la liste', () => {
const familles = famillesInterface();
expect(familles[0]).toBe(FAMILLE);
expect(familles.length).toBeGreaterThan(1);
expect(familles.slice(1)).not.toContain(FAMILLE);
expect(faceChargee()).toBe(true);
});
test('dix mesures sur chaque police ; la tolérance proposée écarte le repli et admet la police embarquée', async () => {
const familles = famillesInterface();
const repli = familles.slice(1).join(', ');
const attenduePx = longueurTemoin(octets, CORPS_PX);
const embarqueePx = longueursRendues(`'${FAMILLE}', ${repli}`);
const repliPx = longueursRendues(repli);
const police = familles[0] === FAMILLE && faceChargee() ? FAMILLE : null;
const tolerance = toleranceTemoin({ attenduePx, embarqueePx, repliPx });
const refus = police === null ? `police ${familles[0]} : la mesure se prend sur ${FAMILLE}` : tolerance.refus;
mesure = {
nom: NOM,
unite: 'rapport',
valeurProposee: refus === null ? tolerance.valeur : null,
critere:
`le quart de l’écart relatif du repli (${repli}) à la longueur des avances de ${FAMILLE}, et au moins trois fois la ` +
`gigue de ${MESURES} mesures ; chaîne témoin à ${CORPS_PX} px, graisse 400, crénage et ligatures coupés`,
police,
ralentissementCpu: 1,
corpsPx: CORPS_PX,
attenduePx,
embarqueePx,
repliPx,
ecartRepli: tolerance.ecartRepli,
gigue: tolerance.gigue,
biais: tolerance.biais,
refus,
};
expect(refus).toBeNull();
expect(mesure.valeurProposee).toBeGreaterThan(0);
expect(mesure.valeurProposee).toBeLessThan(tolerance.ecartRepli);
expect(mesure.valeurProposee).toBeGreaterThanOrEqual(tolerance.biais);
});
test('la mesure s’écrit dans scripts/banc/sorties/temoin.json, relue telle quelle, et se range dans un relevé', async () => {
expect(mesure).not.toBeNull();
const objet = { format: 1, mesures: [mesure] };
const releve = {
format: 1,
configuration: {
cadre: { largeur: innerWidth, hauteur: innerHeight },
facteurEchelle: devicePixelRatio,
navigateur: null,
node: null,
processeursLogiques: null,
ralentissementCpu: 1,
police: mesure.police,
},
mesures: [mesure],
};
expect(verifierReleve(releve, CONSTANTES)).toEqual([]);
await ecrireSortie('temoin', objet);
expect(JSON.parse(await commands.readFile('scripts/banc/sorties/temoin.json'))).toEqual(objet);
expect(mesure.refus).toBeNull();
});
});