167 lines
6.7 KiB
JavaScript
167 lines
6.7 KiB
JavaScript
|
|
// © 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();
|
|||
|
|
});
|
|||
|
|
});
|