diff --git a/.gitignore b/.gitignore index 26a77e5..1b61721 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ ios/ .vite/ .vitest/ coverage/ +scripts/banc/sorties/ diff --git a/Makefile b/Makefile index 1f6920c..847f71c 100644 --- a/Makefile +++ b/Makefile @@ -68,6 +68,10 @@ test_tout: ## Joue les trois séries couverture: ## Mesure la couverture de la série node, module par module ; seuils durs du § 14.13 $(AVEC_NODE) npm run couverture +.PHONY: banc +banc: ## Relève les constantes du banc de mesure (§ 19.10), avant une livraison + $(AVEC_NODE) npm run banc + .PHONY: build build: ## Construit la page dans www/ $(AVEC_NODE) npm run build diff --git a/package.json b/package.json index e904e09..f2ef5b7 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "test:long": "vitest run --project node-long", "test:navigateur": "vitest run --project navigateur", "test:tout": "vitest run", - "couverture": "vitest run --project node --coverage --reporter=default" + "couverture": "vitest run --project node --coverage --reporter=default", + "banc": "node scripts/banc/lancer.js" }, "devDependencies": { "@capacitor/cli": "^8.5.2", diff --git a/scripts/banc/banc.test.js b/scripts/banc/banc.test.js new file mode 100644 index 0000000..8db0fa9 --- /dev/null +++ b/scripts/banc/banc.test.js @@ -0,0 +1,266 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Gardes du banc de mesure (§ 19.10, § 14.4, § 14.8). Le banc vit hors des +// trois séries de vitest.config.js : il se lance par sa propre commande, sur +// sa propre configuration, vitest.banc.config.js, qui ôte au Chromium sa +// cadence d'images. Une épreuve du banc que l'une des séries jouerait la +// mesurerait à cadence bridée, et alourdirait une série qui se lance à +// chaque modification ; une épreuve que le projet banc n'inclut pas ne se +// joue nulle part. Les gardes lisent par import vitest.config.js et le +// projet banc tel que scripts/banc/projet.js le décrit — vitest.banc.config.js +// charge Vitest, qu'une épreuve node ne charge pas (§ 14.8) —, le Makefile et +// package.json par le disque. +import assert from 'node:assert/strict'; +import { readdirSync, readFileSync } from 'node:fs'; +import { join, matchesGlob, posix, relative, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { chargements } from '../../test/chargements.js'; +import { describe, test } from '../../test/lanceur.js'; +import configuration from '../../vitest.config.js'; +import { optionsBanc, projetBanc } from './projet.js'; + +const RACINE = fileURLToPath(new URL('../../', import.meta.url)); + +// Ce fichier, relatif à la racine. Ses données d'épreuve écrivent en +// chaînes des chargements des outils du banc, que le relevé des chargements +// lit comme les autres : le parcours de l'arbre réel le laisse de côté. +const CE_FICHIER = relative(RACINE, fileURLToPath(import.meta.url)).split(sep).join('/'); + +// Les outils de page du banc : un module qui les charge mesure dans la page. +const OUTILS_DU_BANC = 'scripts/banc/navigateur.js'; + +// Les drapeaux qui ôtent à Chromium sa cadence d'images. +const DRAPEAUX_CADENCE = ['--disable-frame-rate-limit', '--disable-gpu-vsync']; + +// Ce que le parcours de l'arbre saute : les dépendances, à toute +// profondeur, et, à la racine, le dépôt et les sorties de construction et de +// mesure. +const HORS_DU_PARCOURS_A_LA_RACINE = new Set(['.git', 'www', 'dist-electron', 'coverage', '.vitest']); +const MODULE = /\.[cm]?js$/; + +// Les modules JavaScript du projet, { chemin, texte }, chemins relatifs à la +// racine en barres obliques, triés. +function modulesDuProjet(racine) { + const modules = []; + const parcourir = (dossier) => { + for (const entree of readdirSync(dossier, { withFileTypes: true })) { + const chemin = join(dossier, entree.name); + const exclu = entree.name === 'node_modules' || (dossier === racine && HORS_DU_PARCOURS_A_LA_RACINE.has(entree.name)); + if (entree.isDirectory() && !exclu) parcourir(chemin); + else if (entree.isFile() && MODULE.test(entree.name)) { + modules.push({ chemin: relative(racine, chemin).split(sep).join('/'), texte: readFileSync(chemin, 'utf8') }); + } + } + }; + parcourir(racine); + return modules.sort((a, b) => (a.chemin < b.chemin ? -1 : a.chemin > b.chemin ? 1 : 0)); +} + +// Le module que vise un spécificateur, relatif à la racine, en barres +// obliques, suffixe de Vite (« ?… ») retiré : relatif au fichier, ou absolu +// depuis la racine comme Vite les résout ; null pour un paquet. +function cible(chemin, specificateur) { + const sansSuffixe = specificateur.replace(/\?.*$/, ''); + if (sansSuffixe.startsWith('/')) return posix.normalize(sansSuffixe.slice(1)); + if (sansSuffixe.startsWith('.')) return posix.normalize(posix.join(posix.dirname(chemin), sansSuffixe)); + return null; +} + +// Une épreuve que Vitest peut jouer : un fichier *.banc.js, que le projet +// banc inclut, ou *.test.js, que les séries incluent. +const EPREUVE = /\.(?:banc|test)\.[cm]?js$/; + +// Les épreuves du banc parmi des modules { chemin, texte } : un fichier +// *.banc.js, ou une épreuve *.test.js qui charge les outils de page du banc, +// directement ou à travers des modules de la liste, à toute profondeur. Un +// module qui n'est pas une épreuve n'en est jamais une : une aide du banc +// qui charge les outils ne se joue que par une épreuve qui la charge. +function epreuvesDuBanc(modules) { + const textes = new Map(modules.map(({ chemin, texte }) => [chemin, texte])); + // Les modules que chaque module charge, lus une fois. + const lus = new Map(); + const voisins = (chemin) => { + if (!lus.has(chemin)) { + lus.set( + chemin, + chargements(textes.get(chemin)) + .filter(({ specificateur }) => specificateur !== undefined) + .map(({ specificateur }) => cible(chemin, specificateur)) + .filter((vise) => vise !== null), + ); + } + return lus.get(chemin); + }; + const atteintLesOutils = (depart) => { + const vus = new Set([depart]); + const aParcourir = [depart]; + for (let i = 0; i < aParcourir.length; i += 1) { + for (const vise of voisins(aParcourir[i])) { + if (vise === OUTILS_DU_BANC) return true; + if (vus.has(vise) || !textes.has(vise)) continue; + vus.add(vise); + aParcourir.push(vise); + } + } + return false; + }; + return modules.filter( + ({ chemin }) => chemin.endsWith('.banc.js') || (EPREUVE.test(chemin) && atteintLesOutils(chemin)), + ); +} + +const repondA = (chemin, motifs = []) => motifs.some((motif) => matchesGlob(chemin, motif)); +const inclus = (chemin, { include = [], exclude = [] }) => repondA(chemin, include) && !repondA(chemin, exclude); + +// Relevé des épreuves du banc mal rangées, dans l'ordre des modules reçus : +// « chemin : jouée par le projet nom » pour chaque projet des séries qui +// l'inclut, puis « chemin : hors du projet banc » quand le projet banc ne +// l'inclut pas. projets et banc : les objets test des projets de Vitest. +// Lève quand les modules ne portent aucune épreuve du banc (§ 14.2). +function releverBancs(modules, { projets, banc }) { + const bancs = epreuvesDuBanc(modules); + assert.ok(bancs.length > 0, 'aucune épreuve du banc'); + return bancs.flatMap(({ chemin }) => [ + ...projets.filter((projet) => inclus(chemin, projet)).map(({ name }) => `${chemin} : jouée par le projet ${name}`), + ...(inclus(chemin, banc) ? [] : [`${chemin} : hors du projet banc`]), + ]); +} + +// La cible nommée d'un Makefile : sa ligne de documentation (« ## … »), ses +// commandes (les lignes de recette qui la suivent, tabulation retirée) et sa +// déclaration .PHONY ; null quand le Makefile ne la porte pas. +function cibleDuMakefile(texte, nom) { + const lignes = texte.split('\n'); + const rang = lignes.findIndex((ligne) => ligne.startsWith(`${nom}:`)); + if (rang === -1) return null; + const documentation = /##\s*(.*)$/.exec(lignes[rang])?.[1] ?? null; + const commandes = []; + for (let i = rang + 1; i < lignes.length && lignes[i].startsWith('\t'); i += 1) commandes.push(lignes[i].slice(1)); + const factice = lignes.some((ligne) => /^\.PHONY:/.test(ligne) && ligne.slice('.PHONY:'.length).trim().split(/\s+/).includes(nom)); + return { documentation, commandes, factice }; +} + +const projetsDesSeries = () => configuration.test.projects.map(({ test: projet }) => projet); + +describe('banc : hors des trois séries (§ 14.4, § 14.8)', () => { + test('aucune épreuve du banc ne répond aux motifs des trois projets de vitest.config.js, et le projet banc les inclut toutes', () => { + const modules = modulesDuProjet(RACINE).filter(({ chemin }) => chemin !== CE_FICHIER); + assert.ok(epreuvesDuBanc(modules).some(({ chemin }) => chemin === 'scripts/banc/harnais.banc.js')); + assert.deepEqual(projetsDesSeries().map(({ name }) => name), ['node', 'node-long', 'navigateur']); + assert.deepEqual(releverBancs(modules, { projets: projetsDesSeries(), banc: projetBanc() }), []); + }); + + test('la garde relève une épreuve du banc renommée en épreuve du navigateur, une autre chargée par une épreuve de src/, directement ou par une aide, un *.banc.js hors de scripts/banc, et une série qui inclurait le banc ; une aide du banc qui n’est pas une épreuve passe', () => { + const outils = "import { mesurerImages } from './navigateur.js';\n"; + const modules = [ + { chemin: 'scripts/banc/navigateur.js', texte: "import { cdp } from 'vitest/browser';\n" }, + { chemin: 'scripts/banc/statistiques.test.js', texte: "import { statistiques } from './statistiques.js';\n" }, + { chemin: 'scripts/banc/defilement.banc.js', texte: outils }, + { chemin: 'scripts/banc/defilement.navigateur.test.js', texte: outils }, + { chemin: 'scripts/banc/gestes.js', texte: `${outils}export const mesurer = (geste) => mesurerImages(geste);\n` }, + { chemin: 'scripts/banc/pivots.js', texte: "export { mesurer } from './gestes.js?brut';\n" }, + { chemin: 'scripts/banc/zoom/crans.navigateur.test.js', texte: "import { mesurerImages } from '/scripts/banc/navigateur.js';\n" }, + { chemin: 'src/interface/plan/Pivot.navigateur.test.js', texte: "import { mesurer } from '../../../scripts/banc/pivots.js';\n" }, + { chemin: 'src/interface/plan/Zoom.navigateur.test.js', texte: "const o = await import('../../../scripts/banc/navigateur.js');\n" }, + { chemin: 'test/navigateur/listes.banc.js', texte: '' }, + ]; + assert.deepEqual( + epreuvesDuBanc(modules).map(({ chemin }) => chemin), + [ + 'scripts/banc/defilement.banc.js', + 'scripts/banc/defilement.navigateur.test.js', + 'scripts/banc/zoom/crans.navigateur.test.js', + 'src/interface/plan/Pivot.navigateur.test.js', + 'src/interface/plan/Zoom.navigateur.test.js', + 'test/navigateur/listes.banc.js', + ], + ); + assert.deepEqual(releverBancs(modules, { projets: projetsDesSeries(), banc: projetBanc() }), [ + 'scripts/banc/defilement.navigateur.test.js : hors du projet banc', + 'scripts/banc/zoom/crans.navigateur.test.js : hors du projet banc', + 'src/interface/plan/Pivot.navigateur.test.js : jouée par le projet navigateur', + 'src/interface/plan/Pivot.navigateur.test.js : hors du projet banc', + 'src/interface/plan/Zoom.navigateur.test.js : jouée par le projet navigateur', + 'src/interface/plan/Zoom.navigateur.test.js : hors du projet banc', + 'test/navigateur/listes.banc.js : hors du projet banc', + ]); + const serieTropLarge = { name: 'node', include: ['scripts/**/*.js'], exclude: ['**/*.navigateur.test.js'] }; + assert.deepEqual(releverBancs(modules.slice(0, 3), { projets: [serieTropLarge], banc: projetBanc() }), [ + 'scripts/banc/defilement.banc.js : jouée par le projet node', + ]); + }); + + test('la garde refuse de passer sans épreuve du banc', () => { + const modules = [{ chemin: 'scripts/banc/navigateur.js', texte: "import { cdp } from 'vitest/browser';\n" }]; + assert.throws(() => releverBancs(modules, { projets: projetsDesSeries(), banc: projetBanc() }), /aucune épreuve du banc/); + }); +}); + +describe('banc : sa configuration (§ 19.10)', () => { + test('le projet banc inclut scripts/banc/**/*.banc.js, et chaque appel rend un objet neuf', () => { + const projet = projetBanc(); + assert.equal(projet.name, 'banc'); + assert.deepEqual(projet.include, ['scripts/banc/**/*.banc.js']); + projet.include.push('ailleurs/**/*.js'); + projet.provide.cadreBanc.largeur = 1; + assert.deepEqual(projetBanc().include, ['scripts/banc/**/*.banc.js']); + assert.equal(projetBanc().provide.cadreBanc.largeur, 1366); + }); + + test('Chromium sans tête, cadence non bridée, cadre 1 366 × 700 au facteur 1, fichiers l’un après l’autre, délai large', () => { + const { browser, fileParallelism, testTimeout, hookTimeout, provide } = projetBanc(); + assert.equal(browser.enabled, true); + assert.equal(browser.headless, true); + assert.deepEqual(browser.instances, [{ browser: 'chromium' }]); + assert.deepEqual(browser.viewport, { width: 1366, height: 700 }); + const { launchOptions, contextOptions } = optionsBanc({ LANG: 'fr_CA.UTF-8' }); + assert.deepEqual(launchOptions.args.slice(-2), DRAPEAUX_CADENCE); + assert.deepEqual(contextOptions, { viewport: { width: 1366, height: 700 }, deviceScaleFactor: 1 }); + assert.deepEqual(provide, { cadreBanc: { largeur: 1366, hauteur: 700 } }); + assert.equal(fileParallelism, false); + assert.ok(testTimeout >= 600_000, `testTimeout ${testTimeout}`); + assert.ok(hookTimeout >= 600_000, `hookTimeout ${hookTimeout}`); + }); + + test('optionsBanc ajoute les drapeaux de cadence aux options de test/chromium.js, sans les perdre', () => { + assert.deepEqual(optionsBanc({ LANG: 'fr_CA.UTF-8' }), { + launchOptions: { args: DRAPEAUX_CADENCE }, + contextOptions: { viewport: { width: 1366, height: 700 }, deviceScaleFactor: 1 }, + }); + const env = { LANG: 'C', GTT_CHROMIUM: '/opt/chromium-epreuve/chrome' }; + const { launchOptions } = optionsBanc(env); + assert.equal(launchOptions.executablePath, '/opt/chromium-epreuve/chrome'); + assert.equal(launchOptions.env.LANG, 'C.UTF-8'); + assert.deepEqual(launchOptions.args, DRAPEAUX_CADENCE); + assert.deepEqual(env, { LANG: 'C', GTT_CHROMIUM: '/opt/chromium-epreuve/chrome' }); + }); +}); + +describe('banc : sa commande', () => { + const { scripts = {} } = JSON.parse(readFileSync(join(RACINE, 'package.json'), 'utf8')); + const makefile = readFileSync(join(RACINE, 'Makefile'), 'utf8'); + + test('npm run banc lance scripts/banc/lancer.js, et aucun autre script de package.json ne touche au banc', () => { + assert.equal(scripts.banc, 'node scripts/banc/lancer.js'); + const autres = Object.entries(scripts).filter(([nom, commande]) => nom !== 'banc' && /banc/.test(commande)); + assert.deepEqual(autres, []); + }); + + test('make banc lance npm run banc, cible factice documentée', () => { + assert.deepEqual(cibleDuMakefile(makefile, 'banc'), { + documentation: 'Relève les constantes du banc de mesure (§ 19.10), avant une livraison', + commandes: ['$(AVEC_NODE) npm run banc'], + factice: true, + }); + }); + + test('la lecture du Makefile relève une cible absente, une recette autre, une cible non factice', () => { + assert.equal(cibleDuMakefile('test:\n\t$(AVEC_NODE) npm test\n', 'banc'), null); + assert.deepEqual(cibleDuMakefile('.PHONY: test\nbanc: ## Mesure\n\tnode scripts/banc/lancer.js\n\nx:\n\techo\n', 'banc'), { + documentation: 'Mesure', + commandes: ['node scripts/banc/lancer.js'], + factice: false, + }); + }); +}); diff --git a/scripts/banc/harnais.banc.js b/scripts/banc/harnais.banc.js new file mode 100644 index 0000000..67093d7 --- /dev/null +++ b/scripts/banc/harnais.banc.js @@ -0,0 +1,134 @@ +// © 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); + }); +}); diff --git a/scripts/banc/navigateur.js b/scripts/banc/navigateur.js new file mode 100644 index 0000000..5bd5cf0 --- /dev/null +++ b/scripts/banc/navigateur.js @@ -0,0 +1,206 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Les outils de page du banc (§ 19.10), pour les épreuves *.banc.js que +// joue vitest.banc.config.js, sous Chromium sans cadence d'images : le +// contrôle du cadre, le ralentissement du processeur, la mesure des +// intervalles entre images pendant un geste, les deux contrôles qui disent +// si cette mesure voit ce qu'elle mesure, et l'écriture d'une sortie. +// +// Une horloge réelle se lit ici, et seulement pour mesurer : l'instant que +// requestAnimationFrame passe à son rappel, et performance.now pour tenir +// une image occupée. Ce qui s'écrit ne porte aucun instant, seulement des +// durées. +import { inject } from 'vitest'; +import { cdp, commands } from 'vitest/browser'; +import { fixerCadre } from '../../test/navigateur/entrees.js'; +import { statistiques } from './statistiques.js'; + +/** Le p95 sous lequel un plan vide prouve que la cadence n'est pas bridée : + * sous la marche de 16,7 ms d'une cadence calée à 60 images par seconde. */ +export const SEUIL_CADENCE_MS = 16; + +/** La durée d'une image chargée exprès, que la mesure doit voir. */ +export const CHARGE_MS = 50; + +// Le dossier des sorties, relatif à la racine du projet, où commands.writeFile +// résout un chemin ; ignoré par git. Un nom de sortie n'y désigne qu'un +// fichier de ce dossier. +const DOSSIER_SORTIES = 'scripts/banc/sorties'; +const NOM_SORTIE = /^[a-z0-9_]+$/; + +const uneImage = () => new Promise((resoudre) => requestAnimationFrame(resoudre)); + +/** + * Pose le cadre du banc, que vitest.banc.config.js fournit par + * inject('cadreBanc'), et vérifie qu'il est rendu tel quel : innerWidth et + * innerHeight (fixerCadre), un facteur d'échelle de 1, et aucune mise à + * l'échelle du cadre de l'épreuve par la page qui le porte, d'un cadre à son + * parent : le rapport entre la largeur rendue de chaque