gestion_table_tournante_libre/test/licence.test.js
Mathieu Benoit 5fbbab6477 [ADD] interface: application shell — banner, sections, theme, about
The window showed only a banner; it now holds the application. App mounts
the session, the facade and the generation, settles the working folder
and lists its events before it reports ready, re-reading the list once a
question is answered or a refused determination succeeds. The banner
carries version, mode and the event list command; an open event shows its
sections in their first form; the theme follows the system at each start
and the operator's choice lasts the session; À propos names the build.
Errors are collected for a report, and Electron removes the session's
locks at exit.
Checked: 2189 node, 193 browser, 60 node-long; the Electron start check.

--- FR ---

[ADD] interface : coquille — bandeau, sections, thème, À propos

La fenêtre ne montrait qu'un bandeau ; elle tient désormais l'application.
App monte la séance, la façade et la génération, règle le dossier de
travail et liste ses événements avant de se dire prête, et relit la liste
une fois une question répondue ou une détermination refusée réussie. Le
bandeau porte version, mode et la commande de la liste ; un événement
ouvert montre ses sections dans leur première forme ; le thème suit le
système à chaque démarrage et le choix de l'opérateur vaut pour la séance ;
À propos nomme la construction. Les erreurs se collectent pour un
signalement, et Electron efface à la sortie les verrous de la séance.
Vérifié : 2189 node, 193 navigateur, 60 node-long ; démarrage d'Electron.

Assisted-by: Claude Opus 5.5
2026-10-07 07:22:05 -04:00

176 lines
8.5 KiB
JavaScript

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Chaque fichier source du projet s'ouvre sur l'en-tête de licence : la ligne
// du titulaire, puis celle de la licence, après un éventuel shebang ou la
// déclaration <!doctype html>, et après la marque d'ordre d'octets que
// portent les scripts PowerShell. Les deux lignes sont des commentaires du
// langage du fichier ; le texte ne varie pas. package.json déclare la même
// licence et le même titulaire, que reprennent les outils d'emballage.
import assert from 'node:assert/strict';
import { readdirSync, readFileSync } from 'node:fs';
import { basename, extname, join, relative, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, test } from './lanceur.js';
const RACINE = fileURLToPath(new URL('..', import.meta.url));
const TITULAIRE = /^© \d{4} TechnoLibre \(http:\/\/www\.technolibre\.ca\)$/;
const LICENCE = 'License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)';
// Marqueurs de commentaire, par extension de fichier source, ou par nom pour
// un fichier sans extension. Un commentaire de ligne n'a pas de fermant. Un
// ouvrant est une chaîne qui ouvre la ligne, ou un motif : REM ouvre une
// ligne de commandes Windows, précédé ou non de @, quelle que soit sa casse,
// et suivi d'un blanc — @REMARQUE est une commande.
const COMMENTAIRE = new Map([
['.js', { ouvrant: '//', fermant: null }],
['.cjs', { ouvrant: '//', fermant: null }],
['.mjs', { ouvrant: '//', fermant: null }],
['.sh', { ouvrant: '#', fermant: null }],
['.svelte', { ouvrant: '<!--', fermant: '-->' }],
['.html', { ouvrant: '<!--', fermant: '-->' }],
['.css', { ouvrant: '/*', fermant: '*/' }],
['.ps1', { ouvrant: '#', fermant: null }],
['.nix', { ouvrant: '#', fermant: null }],
['.cmd', { ouvrant: /^@?rem(?=\s)/i, fermant: null }],
['Makefile', { ouvrant: '#', fermant: null }],
]);
// Clé d'un fichier dans COMMENTAIRE : son nom quand il y figure, son
// extension sinon.
const cle = (chemin) => (COMMENTAIRE.has(basename(chemin)) ? basename(chemin) : extname(chemin));
// Périmètre : les fichiers de la racine et les arbres des sources, des
// épreuves, des scripts, de la coquille et des fichiers que Vite copie tels
// quels dans la page (public/). Les dépendances et les sorties de
// construction n'y entrent pas, ni les données d'épreuve, qui sont des
// données et non des sources. Le séparateur final borne l'exclusion au
// répertoire test/fixtures/ : un module test/fixtures.js reste une source.
const ARBRES = ['src', 'test', 'scripts', 'electron', 'public'];
const DONNEES_EPREUVE = join(RACINE, 'test', 'fixtures') + sep;
function fichiersSources() {
const chemins = readdirSync(RACINE, { withFileTypes: true })
.filter((e) => e.isFile())
.map((e) => join(RACINE, e.name));
for (const arbre of ARBRES) {
const dossier = join(RACINE, arbre);
let entrees;
try {
entrees = readdirSync(dossier, { withFileTypes: true, recursive: true });
} catch (erreur) {
if (erreur.code === 'ENOENT') continue;
throw erreur;
}
for (const e of entrees) {
if (e.isFile()) chemins.push(join(e.parentPath, e.name));
}
}
return chemins
.filter((chemin) => COMMENTAIRE.has(cle(chemin)))
.filter((chemin) => !chemin.startsWith(DONNEES_EPREUVE))
.sort();
}
// Vrai quand les deux lignes sont des commentaires, et que le commentaire ne
// se prolonge pas au-delà de la seconde. Un commentaire de ligne ouvre
// chacune d'elles. Un commentaire de bloc court sur les deux — ouvert sur la
// première sans s'y fermer, fermé en fin de seconde — ou s'ouvre et se ferme
// sur chacune. Une seconde ligne hors commentaire se lit comme du code : un
// texte affiché avant le bandeau dans un composant, une règle de style
// invalidée dans une feuille, une commande dans un script.
function enCommentaire(premiere, seconde, { ouvrant, fermant }) {
const suite = seconde.trim();
const ouvre = (ligne) => (typeof ouvrant === 'string' ? ligne.startsWith(ouvrant) : ouvrant.test(ligne));
if (!ouvre(premiere)) return false;
if (fermant === null) return ouvre(suite);
const courant = !premiere.includes(fermant) && suite.endsWith(fermant);
const repete =
premiere.trimEnd().endsWith(fermant) &&
suite.startsWith(ouvrant) &&
suite.endsWith(fermant);
return courant || repete;
}
// Vrai quand le contenu s'ouvre sur l'en-tête, après une éventuelle marque
// d'ordre d'octets, un shebang ou la déclaration <!doctype html> : deux
// lignes de commentaire du langage de la clé, extension ou nom, dont le
// texte, débarrassé des marqueurs, est celui de l'en-tête. Les fins de ligne
// sont celles d'Unix ou de Windows.
function porteLEnTete(contenu, cleFichier) {
let lignes = contenu.replace(/^\uFEFF/, '').split(/\r?\n/);
if (lignes[0].startsWith('#!')) lignes = lignes.slice(1);
if (/^<!doctype html>$/i.test(lignes[0])) lignes = lignes.slice(1);
const [premiere = '', seconde = ''] = lignes;
const texte = (ligne) =>
ligne
.replace(/^\s*(?:\/\/|#|<!--|\/\*|\*|@?rem(?=\s))?\s*/i, '')
.replace(/\s*(?:-->|\*\/)?\s*$/, '');
return (
enCommentaire(premiere, seconde, COMMENTAIRE.get(cleFichier)) &&
TITULAIRE.test(texte(premiere)) &&
texte(seconde) === LICENCE
);
}
describe('licence', () => {
test("chaque fichier source s'ouvre sur l'en-tête de licence", () => {
const fichiers = fichiersSources();
assert.ok(fichiers.length > 0, 'aucun fichier source examiné');
const sansEnTete = fichiers
.filter((chemin) => !porteLEnTete(readFileSync(chemin, 'utf8'), cle(chemin)))
.map((chemin) => relative(RACINE, chemin));
assert.deepEqual(sansEnTete, []);
});
test('le périmètre couvre les fichiers que la page charge tels quels, sous public/', () => {
const relatifs = fichiersSources().map((chemin) => relative(RACINE, chemin).split(sep).join('/'));
assert.ok(relatifs.includes('public/collecteur.js'), 'public/collecteur.js hors du périmètre');
});
test("l'en-tête tient en deux lignes de commentaire du langage du fichier", () => {
const titulaire = '© 2026 TechnoLibre (http://www.technolibre.ca)';
// [extension, contenu, admis] : les formes admises ; puis des secondes
// lignes laissées hors commentaire, que le langage lirait comme du code,
// et un commentaire qui se prolonge au-delà de la seconde ligne.
const cas = [
['.js', `// ${titulaire}\n// ${LICENCE}\n`, true],
['.sh', `#!/bin/sh\n# ${titulaire}\n# ${LICENCE}\n`, true],
['.svelte', `<!-- ${titulaire}\n ${LICENCE} -->\n`, true],
['.svelte', `<!-- ${titulaire} -->\n<!-- ${LICENCE} -->\n`, true],
['.html', `<!doctype html>\n<!-- ${titulaire}\n ${LICENCE} -->\n`, true],
['.css', `/* ${titulaire}\n ${LICENCE} */\n`, true],
['.css', `/* ${titulaire} */\n/* ${LICENCE} */\n`, true],
['.js', `// ${titulaire}\n${LICENCE}\n`, false],
['.sh', `#!/bin/sh\n# ${titulaire}\n${LICENCE}\n`, false],
['.svelte', `<!-- ${titulaire} -->\n${LICENCE}\n`, false],
['.html', `<!doctype html>\n<!-- ${titulaire} -->\n${LICENCE}\n`, false],
['.html', `<!-- ${titulaire}\n ${LICENCE}\n-->\n`, false],
['.css', `/* ${titulaire} */\n${LICENCE}\n`, false],
['.css', `/* ${titulaire} */\n * ${LICENCE} */\n`, false],
['.ps1', `\uFEFF# ${titulaire}\r\n# ${LICENCE}\r\n`, true],
['.ps1', `# ${titulaire}\n# ${LICENCE}\n`, true],
['.nix', `# ${titulaire}\n# ${LICENCE}\n`, true],
['.cmd', `@REM ${titulaire}\r\n@REM ${LICENCE}\r\n@echo off\r\n`, true],
['.cmd', `REM ${titulaire}\r\nrem ${LICENCE}\r\n`, true],
['Makefile', `# ${titulaire}\n# ${LICENCE}\n`, true],
['.ps1', `\uFEFF# ${titulaire}\r\nWrite-Host '${LICENCE}'\r\n`, false],
['.cmd', `@REM ${titulaire}\r\n@echo ${LICENCE}\r\n`, false],
['.cmd', `@REMARQUE ${titulaire}\r\n@REM ${LICENCE}\r\n`, false],
['Makefile', `# ${titulaire}\n\t${LICENCE}\n`, false],
];
const ecarts = cas
.filter(([extension, contenu, admis]) => porteLEnTete(contenu, extension) !== admis)
.map(([extension, contenu, admis]) =>
`${extension} ${admis ? 'refusé' : 'admis'} : ${JSON.stringify(contenu)}`,
);
assert.deepEqual(ecarts, []);
});
test('package.json déclare la licence et le titulaire', () => {
const paquet = JSON.parse(readFileSync(join(RACINE, 'package.json'), 'utf8'));
assert.equal(paquet.license, 'AGPL-3.0-or-later');
assert.equal(paquet.author, 'TechnoLibre (http://www.technolibre.ca)');
});
});