gestion_table_tournante_libre/test/chromium.test.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

211 lines
11 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 Chromium des épreuves du navigateur : celui de Playwright par défaut,
// celui que GTT_CHROMIUM désigne sinon ; sous des paramètres régionaux qui ne
// sont pas UTF-8, il reçoit l'environnement sous LANG=C.UTF-8. Le projet
// navigateur le lance sans réseau hors de la boucle locale, au facteur
// d'échelle 1 (§ 19.2, § 19.4), et lui donne les commandes et le mode des
// captures d'écran (§ 19.5). vitest.config.js applique la règle ; le Makefile
// lance la publication.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import configuration from '../vitest.config.js';
import { ARGUMENTS_NAVIGATEUR, horsBoucle, optionsChromium, optionsNavigateur, suivreRequetes } from './chromium.js';
import { describe, test } from './lanceur.js';
import * as commandesDocumentation from './navigateur/commandes_documentation.js';
const RACINE = fileURLToPath(new URL('..', import.meta.url));
// Un environnement d'épreuve, sous des paramètres régionaux UTF-8.
const UTF8 = { PATH: '/usr/bin', LANG: 'fr_CA.UTF-8' };
describe('chromium : l’exécutable des épreuves du navigateur', () => {
test('sans GTT_CHROMIUM, Playwright lance le Chromium qu’il a téléchargé', () => {
assert.deepEqual(optionsChromium(UTF8), {});
assert.deepEqual(optionsChromium({ ...UTF8, GTT_CHROMIUM: '' }), {});
});
test('GTT_CHROMIUM désigne l’exécutable que Playwright lance', () => {
assert.deepEqual(optionsChromium({ ...UTF8, GTT_CHROMIUM: '/nix/store/abc-chromium/bin/chromium' }), {
launchOptions: { executablePath: '/nix/store/abc-chromium/bin/chromium' },
});
});
test('vitest.config.js passe au projet navigateur les options de cet environnement, réseau et échelle compris', () => {
const navigateur = configuration.test.projects.find(({ test: { name } }) => name === 'navigateur');
assert.deepEqual(navigateur.test.browser.provider.options, optionsNavigateur(process.env));
});
});
describe('chromium : les paramètres régionaux du navigateur des épreuves', () => {
test('des paramètres régionaux UTF-8 laissent Chromium hériter de l’environnement : LC_ALL, puis LC_CTYPE, puis LANG décident', () => {
const utf8 = [
{ LANG: 'C.UTF-8' },
{ LANG: 'C.utf8' },
{ LANG: 'de_DE.UTF-8@euro' },
{ LANG: 'C', LC_CTYPE: 'fr_CA.utf8' },
{ LANG: 'C', LC_CTYPE: 'C', LC_ALL: 'fr_CA.UTF-8' },
{ LANG: 'fr_CA.UTF-8', LC_ALL: '', LC_CTYPE: '' },
];
for (const env of utf8) assert.deepEqual(optionsChromium(env), {}, JSON.stringify(env));
});
test('hors UTF-8, Chromium reçoit une copie de l’environnement sous LANG=C.UTF-8, sans LC_ALL ni LC_CTYPE', () => {
const cas = [
[{}, {}],
[{ PATH: '/usr/bin', LC_ALL: 'C' }, { PATH: '/usr/bin' }],
[{ PATH: '/usr/bin', LANG: 'POSIX', LC_MESSAGES: 'fr_CA.UTF-8' }, { PATH: '/usr/bin', LC_MESSAGES: 'fr_CA.UTF-8' }],
[{ LANG: 'fr_CA.UTF-8', LC_CTYPE: 'C' }, {}],
[{ LANG: 'fr_CA.UTF-8', LC_ALL: 'fr_CA.ISO-8859-1' }, {}],
[{ LANG: 'C.UTF-8-ancien' }, {}],
];
for (const [env, reste] of cas) {
const avant = { ...env };
assert.deepEqual(
optionsChromium(env),
{ launchOptions: { env: { ...reste, LANG: 'C.UTF-8' } } },
JSON.stringify(env),
);
assert.deepEqual(env, avant, `${JSON.stringify(env)} : l'environnement reçu reste tel quel`);
}
});
test('hors UTF-8, GTT_CHROMIUM désigne toujours l’exécutable, que l’environnement accompagne', () => {
const env = { LC_ALL: 'C', GTT_CHROMIUM: '/opt/essai/chromium' };
assert.deepEqual(optionsChromium(env), {
launchOptions: { executablePath: '/opt/essai/chromium', env: { GTT_CHROMIUM: '/opt/essai/chromium', LANG: 'C.UTF-8' } },
});
});
});
describe('chromium : aucun réseau hors de la boucle locale, le facteur d’échelle 1 (§ 19.2, § 19.4)', () => {
test('les arguments du navigateur : le résolveur ne résout que la boucle locale, un mandataire mort reçoit le reste, le facteur d’échelle est fixé au lancement', () => {
assert.deepEqual(ARGUMENTS_NAVIGATEUR, [
'--host-resolver-rules=MAP * ~NOTFOUND, EXCLUDE localhost',
'--proxy-server=127.0.0.1:9',
'--force-device-scale-factor=1',
]);
// La boucle locale contourne le mandataire d'office ; <-loopback> la
// ferait passer par lui, et la page des épreuves ne se chargerait plus.
assert.equal(ARGUMENTS_NAVIGATEUR.some((argument) => argument.includes('<-loopback>')), false);
assert.equal(Object.isFrozen(ARGUMENTS_NAVIGATEUR), true);
});
test('optionsNavigateur : les options de optionsChromium, les arguments ajoutés, un contexte au facteur 1 ; l’environnement reçu reste tel quel', () => {
assert.deepEqual(optionsNavigateur({ LANG: 'fr_CA.UTF-8' }), {
launchOptions: { args: [...ARGUMENTS_NAVIGATEUR] },
contextOptions: { deviceScaleFactor: 1 },
});
const env = { LC_ALL: 'C', GTT_CHROMIUM: '/opt/essai/chromium' };
assert.deepEqual(optionsNavigateur(env), {
launchOptions: {
executablePath: '/opt/essai/chromium',
env: { GTT_CHROMIUM: '/opt/essai/chromium', LANG: 'C.UTF-8' },
args: [...ARGUMENTS_NAVIGATEUR],
},
contextOptions: { deviceScaleFactor: 1 },
});
assert.deepEqual(env, { LC_ALL: 'C', GTT_CHROMIUM: '/opt/essai/chromium' });
});
test('horsBoucle : une adresse hors de la boucle locale est relevée ; la boucle locale et les adresses sans réseau ne le sont pas', () => {
const dehors = [
'http://192.0.2.1/',
'https://exemple.invalid/police.woff2',
'ws://198.51.100.7:8080/',
'http://localhost.exemple.invalid/',
'http://127.0.0.1.exemple.invalid/',
'http://[2001:db8::1]/',
'ftp://exemple.invalid/x',
'gopher://exemple.invalid/',
];
const dedans = [
'http://localhost:63315/__vitest_test__/',
'ws://localhost:63315/__vitest_browser_api__',
'http://LOCALHOST/',
'http://127.0.0.1:5173/collecteur.js',
'http://127.8.9.10/',
'http://[::1]:63315/',
'data:text/plain,x',
'blob:http://localhost:63315/0f3c',
'about:blank',
];
for (const adresse of dehors) assert.equal(horsBoucle(adresse), true, adresse);
for (const adresse of dedans) assert.equal(horsBoucle(adresse), false, adresse);
assert.equal(horsBoucle('pas une adresse'), true);
});
test('suivreRequetes : chaque requête hors boucle de la page, relevée une fois et dans l’ordre ; un second suivi de la même page reprend le premier', () => {
const ecouteurs = [];
const page = { on: (evenement, fn) => ecouteurs.push([evenement, fn]) };
const requete = (adresse) => ({ url: () => adresse });
const suivi = suivreRequetes(page);
assert.deepEqual(ecouteurs.map(([evenement]) => evenement), ['request', 'requestfailed']);
const [[, ecouter]] = ecouteurs;
ecouter(requete('http://localhost:63315/a.js'));
ecouter(requete('http://192.0.2.1/'));
ecouter(requete('https://exemple.invalid/b'));
assert.deepEqual(suivreRequetes(page).relever(), ['http://192.0.2.1/', 'https://exemple.invalid/b']);
assert.equal(ecouteurs.length, 2);
assert.deepEqual(suivi.relever(), []);
ecouter(requete('http://192.0.2.1/c'));
assert.deepEqual(suivi.relever(), ['http://192.0.2.1/c']);
assert.deepEqual(suivreRequetes({ on: () => {} }).relever(), []);
});
test('suivreRequetes : chaque échec d’une requête hors boucle, avec sa cause, relevé une fois ; ceux de la boucle locale ne le sont pas', () => {
const ecouteurs = new Map();
const page = { on: (evenement, fn) => ecouteurs.set(evenement, fn) };
const suivi = suivreRequetes(page);
assert.deepEqual([...ecouteurs.keys()], ['request', 'requestfailed']);
const echec = (adresse, errorText) => ({ url: () => adresse, failure: () => (errorText === null ? null : { errorText }) });
ecouteurs.get('requestfailed')(echec('http://localhost:63315/a.js', 'net::ERR_ABORTED'));
ecouteurs.get('requestfailed')(echec('http://192.0.2.1/', 'net::ERR_PROXY_CONNECTION_FAILED'));
ecouteurs.get('requestfailed')(echec('http://192.0.2.1/b', null));
assert.deepEqual(suivi.releverEchecs(), [
{ adresse: 'http://192.0.2.1/', cause: 'net::ERR_PROXY_CONNECTION_FAILED' },
{ adresse: 'http://192.0.2.1/b', cause: null },
]);
assert.deepEqual(suivi.releverEchecs(), []);
assert.deepEqual(suivi.relever(), []);
});
});
describe('le projet navigateur : les commandes et le mode des captures d’écran (§ 19.5)', () => {
const navigateur = configuration.test.projects.find(({ test: { name } }) => name === 'navigateur');
test('modeCaptures : verifier par défaut, ecrire sous GTT_CAPTURES=ecrire ; une autre valeur lève en la nommant', () => {
assert.equal(commandesDocumentation.modeCaptures({}), 'verifier');
assert.equal(commandesDocumentation.modeCaptures({ GTT_CAPTURES: '' }), 'verifier');
assert.equal(commandesDocumentation.modeCaptures({ GTT_CAPTURES: 'verifier' }), 'verifier');
assert.equal(commandesDocumentation.modeCaptures({ GTT_CAPTURES: 'ecrire' }), 'ecrire');
assert.throws(() => commandesDocumentation.modeCaptures({ GTT_CAPTURES: 'ecrir' }), /GTT_CAPTURES.*« ecrir »/);
});
test('vitest.config.js fournit le mode à la page par provide, et déclare chaque commande de commandes_documentation.js', () => {
assert.deepEqual(navigateur.test.provide, { captures: { mode: commandesDocumentation.modeCaptures(process.env) } });
const declarees = navigateur.test.browser.commands;
assert.deepEqual(Object.keys(declarees).sort(), [...commandesDocumentation.COMMANDES].sort());
for (const nom of commandesDocumentation.COMMANDES) {
assert.equal(typeof commandesDocumentation[nom], 'function', nom);
assert.equal(declarees[nom], commandesDocumentation[nom], nom);
}
assert.ok(commandesDocumentation.COMMANDES.length > 0);
});
test('make captures lance le parcours du projet navigateur sous GTT_CAPTURES=ecrire, par avec_node ; .gitignore tait le dossier d’attente', () => {
const makefile = readFileSync(join(RACINE, 'Makefile'), 'utf8');
assert.match(makefile, /^\.PHONY: captures$/m);
assert.match(makefile, /^captures: export GTT_CAPTURES := ecrire$/m);
assert.match(
makefile,
/^captures: ## Photographie le parcours et publie doc\/captures \(§ 19\.5\)\n\t\$\(AVEC_NODE\) npx vitest run --project navigateur src\/interface\/parcours\.navigateur\.test\.js$/m,
);
const ignores = readFileSync(join(RACINE, '.gitignore'), 'utf8').split('\n');
assert.ok(ignores.includes('doc/captures/.attente/'), ignores.join(' | '));
assert.equal(ignores.some((ligne) => /^doc\/captures\/?$|^doc\/?$|\*\.png/.test(ligne)), false);
});
});