gestion_table_tournante_libre/test/chromium.js
Mathieu Benoit 43dbfad8a5 [ADD] docs: photographed walkthrough publishes doc/captures
The screenshots must come from the tested walkthrough, not from a separate
script that can drift. The browser walkthrough runs on the simulated file
system with a driven clock and the direct executor, light theme, 1366 x
700 at scale 1, transitions off, and photographs each step once its
condition holds. make captures writes the images and the manifest; the
plain test run compares them instead. Eighteen captures are published, the
later ones named as postponed. The forged-names scan skips the manifest,
which records the demo names shown on screen.
Checked: red first; 3620 node, 681 browser and 101 long tests green from
the index alone.

--- FR ---

[ADD] docs : parcours photographié, publication de doc/captures

Les captures doivent venir du parcours éprouvé, non d'un script à part qui
dérive. Le parcours navigateur joue sur le système de fichiers simulé,
l'horloge pilotée et l'exécuteur direct, thème clair, 1366 × 700 au facteur
1, transitions coupées, et photographie chaque étape quand sa condition
tient. make captures écrit les images et le manifeste ; la série ordinaire
les compare au lieu de les écrire. Dix-huit captures sont publiées, les
suivantes nommées reportées. Le balayage des noms forgés saute le
manifeste, qui relève les noms de démonstration affichés.
Vérifié : rouge d'abord ; 3620 node, 681 navigateur et 101 longues vertes
depuis l'index seul.

Assisted-by: Claude Opus 5.5
2026-10-09 15:33:59 -04:00

149 lines
6.5 KiB
JavaScript

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Options du Chromium des épreuves du navigateur, que vitest.config.js passe
// au fournisseur Playwright. Playwright lance par défaut le Chromium qu'il a
// téléchargé ; GTT_CHROMIUM désigne un autre exécutable. Sous NixOS, où les
// binaires téléchargés ne démarrent pas, scripts/installation/shell.nix y
// met celui de nixpkgs.
//
// Sous Linux, Chromium convertit le nom d'une entrée de l'OPFS par les
// paramètres régionaux de son processus : hors UTF-8, un nom hors ASCII ne
// se convertit pas, et les épreuves du navigateur n'éprouvent plus le
// stockage. Les paramètres régionaux qui décident de cette conversion sont
// ceux de LC_CTYPE : LC_ALL, puis LC_CTYPE, puis LANG, la première variable
// non vide ; aucune, c'est « C ». Quand ils ne sont pas UTF-8, Chromium
// reçoit une copie de l'environnement sous LANG=C.UTF-8, LC_ALL et LC_CTYPE
// retirés puisqu'ils l'emporteraient sur LANG, comme
// scripts/verifier_systemes.sh le lance dans ses conteneurs. Seul le nom des
// paramètres régionaux se lit : un nom UTF-8 que l'hôte n'a pas générés
// laisse Chromium sous « C », et l'épreuve de l'OPFS du navigateur nomme
// alors cette cause. C.UTF-8 vient avec la glibc 2.35 et suivantes, et avec
// les distributions qui le livrent avant ; l'environnement reçu reste tel
// quel.
//
// Le projet navigateur reçoit optionsNavigateur : les mêmes options, plus
// trois arguments de lancement et un contexte au facteur d'échelle 1
// (§ 19.2, § 19.4). Le résolveur de Chromium ne résout que localhost ; un
// mandataire mort, 127.0.0.1:9, reçoit toute requête vers une adresse
// littérale, que le résolveur ne voit pas. Chromium contourne d'office le
// mandataire pour la boucle locale — localhost, 127.0.0.0/8, [::1] — : la
// page des épreuves se charge, et une requête qui sortirait de la machine
// échoue en connexion, observable, au lieu de partir. La règle de
// contournement <-loopback> retirerait ce contournement ; elle n'est pas
// posée. Le facteur d'échelle se fixe au lancement, que ni Playwright ni
// une commande du standard ne fixent pour la fenêtre de la machine.
// suivreRequetes relève, page par page, les requêtes hors de la boucle
// locale que la page émet, refusées ou non, et la cause de chaque échec.
// Un nom de paramètres régionaux dont le jeu de caractères est UTF-8 :
// « fr_CA.UTF-8 », « C.utf8 », « de_DE.UTF-8@euro ».
const NOM_UTF8 = /\.utf-?8(@[^.]*)?$/i;
// Paramètres régionaux de LC_CTYPE que env donne, « C » sans aucun.
function caracteresDe(env) {
for (const variable of ['LC_ALL', 'LC_CTYPE', 'LANG']) {
if (env[variable]) return env[variable];
}
return 'C';
}
export function optionsChromium(env) {
const lancement = {};
if (env.GTT_CHROMIUM) lancement.executablePath = env.GTT_CHROMIUM;
if (!NOM_UTF8.test(caracteresDe(env))) {
lancement.env = { ...env, LANG: 'C.UTF-8' };
delete lancement.env.LC_ALL;
delete lancement.env.LC_CTYPE;
}
return Object.keys(lancement).length > 0 ? { launchOptions: lancement } : {};
}
/** Les arguments de lancement du projet navigateur, dans cet ordre. */
export const ARGUMENTS_NAVIGATEUR = Object.freeze([
'--host-resolver-rules=MAP * ~NOTFOUND, EXCLUDE localhost',
'--proxy-server=127.0.0.1:9',
'--force-device-scale-factor=1',
]);
/** Le mode d'affichage du navigateur des captures, nommé au manifeste
* (§ 19.4) : une valeur de la configuration, jamais lue de la machine. */
export const MODE_AFFICHAGE = 'chromium-headless-shell';
/**
* Les options du fournisseur Playwright du projet navigateur : celles de
* optionsChromium(env), ARGUMENTS_NAVIGATEUR ajoutés à la fin des arguments
* de lancement, et un contexte de navigation au facteur d'échelle 1. env
* n'est pas modifié.
*
* @param {Record<string, string|undefined>} env
*/
export function optionsNavigateur(env) {
const { launchOptions = {}, ...autres } = optionsChromium(env);
return {
...autres,
launchOptions: { ...launchOptions, args: [...(launchOptions.args ?? []), ...ARGUMENTS_NAVIGATEUR] },
contextOptions: { deviceScaleFactor: 1 },
};
}
// Les schémas d'une adresse qui ne passe pas par le réseau.
const SANS_RESEAU = new Set(['data:', 'blob:', 'about:']);
// Un nom d'hôte de la boucle locale, tel que l'URL le rend : localhost,
// une adresse IPv4 de 127.0.0.0/8, ou [::1].
const BOUCLE = /^(localhost|127(\.\d{1,3}){3}|\[::1\])$/;
/**
* Vrai quand une requête vers cette adresse sortirait de la machine : toute
* adresse dont l'hôte n'est pas de la boucle locale, hors des schémas sans
* réseau (data:, blob:, about:). Une adresse illisible compte comme sortante.
*
* @param {string} adresse
* @returns {boolean}
*/
export function horsBoucle(adresse) {
let url;
try {
url = new URL(adresse);
} catch {
return true;
}
if (SANS_RESEAU.has(url.protocol)) return false;
return !BOUCLE.test(url.hostname);
}
// Les requêtes hors boucle relevées par page, et leurs échecs, en attente
// d'être lus.
const RELEVES = new WeakMap();
/**
* Suit les requêtes de la page Playwright : la première fois, deux
* écouteurs se posent sur la page, « request » et « requestfailed », qui
* relèvent chaque adresse hors de la boucle locale, cadres compris, et
* chaque échec d'une telle requête avec sa cause — l'errorText de
* Chromium, net::ERR_PROXY_CONNECTION_FAILED quand le mandataire mort l'a
* reçue. Rend { relever(), releverEchecs() } : les adresses, puis les
* échecs { adresse, cause }, relevés depuis la lecture précédente, dans
* l'ordre, chacun vidé par sa lecture. Deux suivis de la même page
* partagent le même relevé.
*
* @param {{on: (evenement: string, ecouteur: Function) => unknown}} page
* @returns {{relever: () => string[], releverEchecs: () => Array<{adresse: string, cause: string|null}>}}
*/
export function suivreRequetes(page) {
if (!RELEVES.has(page)) {
const releve = { requetes: [], echecs: [] };
RELEVES.set(page, releve);
page.on('request', (requete) => {
const adresse = requete.url();
if (horsBoucle(adresse)) releve.requetes.push(adresse);
});
page.on('requestfailed', (requete) => {
const adresse = requete.url();
if (horsBoucle(adresse)) releve.echecs.push({ adresse, cause: requete.failure()?.errorText ?? null });
});
}
const { requetes, echecs } = RELEVES.get(page);
return { relever: () => requetes.splice(0, requetes.length), releverEchecs: () => echecs.splice(0, echecs.length) };
}