gestion_table_tournante_libre/scripts/banc/harnais.banc.js

135 lines
5.8 KiB
JavaScript
Raw Normal View History

// © 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);
});
});