gestion_table_tournante_libre/scripts/banc/harnais.banc.js
Mathieu Benoit 079b158587 [ADD] bench: headless Chromium harness, statistics and make banc entry
Rendering thresholds must be measured in a real browser, apart from the
test series that guard behaviour. A separate Vitest project runs
scripts/banc/**/*.banc.js in headless Chromium at 1366 x 700, scale 1,
without frame-rate limit, CPU throttling set per run; the statistics module
gives medians and spreads over repeated samples. Outputs go to an ignored
folder. make banc and npm run banc name the entry; its orchestrator comes
with the bench measures.
Checked: red first; 3410 node tests green from the index; the bench project
runs its harness.

--- FR ---

[ADD] banc : harnais Chromium sans tête, statistiques, entrée make banc

Les seuils de rendu se mesurent dans un vrai navigateur, à part des séries
qui gardent le comportement. Un projet Vitest distinct joue
scripts/banc/**/*.banc.js dans Chromium sans tête à 1366 × 700, facteur 1,
sans limite d'images, le ralentissement du processeur réglé par lancement ;
le module de statistiques rend médianes et dispersions sur des mesures
répétées. Les sorties vont dans un dossier ignoré. make banc et npm run banc
nomment l'entrée ; son orchestrateur vient avec les mesures du banc.
Vérifié : rouge d'abord ; 3410 node verts depuis l'index ; le projet du
banc joue son harnais.

Assisted-by: Claude Opus 5.5
2026-10-09 13:03:37 -04:00

134 lines
5.8 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)
// Le harnais du banc (§ 19.10, § 14.2) : avant de croire un temps par image,
// la mesure doit pouvoir le voir. Sur un plan vide, que rien ne charge, un
// glissement réel de 60 pas donne un p95 sous 16 ms — sinon la cadence
// d'images est bridée, et chaque intervalle se lit en marches de 16,7 ms.
// Une image chargée exprès de 50 ms se voit à 50 ms ou plus. Les deux
// contrôles se jouent sans ralentissement et sous le ralentissement de
// référence, ×4 (Q13 b). Chacun est montré capable de tomber : une cadence
// bridée, simulée par des images chargées de 20 ms, fait tomber le premier ;
// une charge qui ne s'applique pas fait tomber le second.
//
// Le cadre se vérifie d'abord : 1 366 × 700, facteur 1, sans mise à
// l'échelle par l'orchestrateur. La dernière épreuve écrit les statistiques
// relevées dans scripts/banc/sorties/harnais.json et les relit. Le contrôle
// du cadre est montré capable de tomber : sur un facteur d'échelle de 2, lu
// par le lecteur qu'il accepte, et sur le cadre de l'épreuve réellement mis
// à l'échelle 0,5 par sa page.
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest';
import { commands } from 'vitest/browser';
import { creerCharge } from '../../src/stockage/document.js';
import { centre, creerContexte, ouvrirEvenement } from '../../test/navigateur/ecrans.js';
import { glisser } from '../../test/navigateur/entrees.js';
import {
CHARGE_MS,
SEUIL_CADENCE_MS,
chargerImages,
controlerCadence,
controlerCadre,
controlerCharge,
ecrireSortie,
ralentirCpu,
} from './navigateur.js';
// Le nombre de pas d'un glissement mesuré (§ 19.10).
const PAS = 60;
// Le ralentissement de référence du banc (Q13 b).
const RALENTISSEMENTS = [1, 4];
// Un événement sans table : le plan vide.
const PLAN_VIDE = creerCharge({ id: 'banc-plan-vide', nom: 'Club des Merles', siegesParDefaut: 8, tours: 2 });
let contexte = null;
let montage = null;
const releves = [];
// La transformation de la vue du plan, que chaque pas d'un glissement change.
const transformation = () => montage.racine.querySelector('[data-vue]').getAttribute('transform');
// Un glissement de PAS pas sur le fond du plan, de part et d'autre de son
// centre ; lève si la vue n'a pas bougé : le geste n'aurait rien dessiné.
async function glissement() {
const avant = transformation();
const { x, y } = await centre(montage.racine.querySelector('[data-plan]'));
await glisser({ x: x - 150, y }, { x: x + 150, y }, { pas: PAS });
if (transformation() === avant) throw new Error('le glissement n’a pas déplacé la vue');
}
beforeAll(async () => {
contexte = creerContexte();
releves.push({ controle: 'cadre', ...(await controlerCadre()) });
montage = await ouvrirEvenement(contexte, PLAN_VIDE, { section: 'plan' });
});
afterEach(async () => {
await ralentirCpu(1);
});
afterAll(async () => {
await contexte?.defaire();
});
describe('harnais du banc', () => {
for (const ralentissement of RALENTISSEMENTS) {
test(`ralenti ×${ralentissement} : un plan vide glissé de ${PAS} pas donne un p95 sous ${SEUIL_CADENCE_MS} ms`, async () => {
await ralentirCpu(ralentissement);
const stats = await controlerCadence(glissement);
releves.push({ controle: 'cadence', ralentissement, ...stats });
expect(stats.p95Ms).toBeLessThan(SEUIL_CADENCE_MS);
expect(stats.images).toBeGreaterThanOrEqual(PAS);
});
test(`ralenti ×${ralentissement} : une image chargée de ${CHARGE_MS} ms se voit à ${CHARGE_MS} ms ou plus`, async () => {
await ralentirCpu(ralentissement);
const stats = await controlerCharge(glissement);
releves.push({ controle: 'charge', ralentissement, ...stats });
expect(stats.p95Ms).toBeGreaterThanOrEqual(CHARGE_MS);
});
}
test('le contrôle de cadence tombe sur une cadence bridée, simulée par des images chargées de 20 ms', async () => {
const arreter = chargerImages(20);
try {
await expect(controlerCadence(glissement)).rejects.toThrow(/^cadence bridée : p95 \d/);
} finally {
arreter();
}
});
test('le contrôle de charge tombe quand la charge ne s’applique pas', async () => {
await expect(controlerCharge(glissement, { charger: () => () => {} })).rejects.toThrow(/^image chargée de 50 ms vue à \d/);
});
test('le contrôle du cadre tombe sur un facteur d’échelle autre que 1', async () => {
await expect(controlerCadre({ lireFacteur: () => 2 })).rejects.toThrow(/^facteur d’échelle 2, attendu 1$/);
});
test('le contrôle du cadre tombe sur un cadre que la page qui le porte met à l’échelle', async () => {
const element = window.frameElement;
const avant = element.style.transform;
element.style.transform = 'scale(0.5)';
try {
await expect(controlerCadre()).rejects.toThrow(/^cadre mis à l’échelle 0\.5 par la page qui le porte$/);
} finally {
element.style.transform = avant;
}
await expect(controlerCadre()).resolves.toEqual({ largeur: 1366, hauteur: 700, facteurEchelle: 1 });
});
test('ecrireSortie refuse un nom hors de [a-z0-9_]', async () => {
for (const nom of ['', '../harnais', 'harnais.json', 'Harnais', 'temps image']) {
await expect(ecrireSortie(nom, {})).rejects.toThrow(RangeError);
}
});
test('les relevés du harnais s’écrivent dans scripts/banc/sorties/harnais.json, relus tels quels', async () => {
expect(releves.map(({ controle }) => controle)).toEqual(['cadre', 'cadence', 'charge', 'cadence', 'charge']);
const objet = { format: 1, releves };
await ecrireSortie('harnais', objet);
expect(JSON.parse(await commands.readFile('scripts/banc/sorties/harnais.json'))).toEqual(objet);
});
});