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
134 lines
5.8 KiB
JavaScript
134 lines
5.8 KiB
JavaScript
// © 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);
|
||
});
|
||
});
|