540 lines
28 KiB
JavaScript
540 lines
28 KiB
JavaScript
|
|
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|||
|
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
|||
|
|
|
|||
|
|
// Épreuves du manifeste des captures et de ses contrôles sous node (§ 19.3,
|
|||
|
|
// § 19.5, § 19.7) : la liste déclarée des captures, l'empreinte d'étape, la
|
|||
|
|
// forme canonique, l'appariement documentation, manifeste et disque, l'ordre
|
|||
|
|
// contre les étapes du § 2.1, l'en-tête PNG, l'écart de version, les chemins
|
|||
|
|
// hors de la racine neutre et la garde qui refuse un manifeste nommant sa
|
|||
|
|
// machine. Les données viennent de test/fixtures/captures/ : deux images
|
|||
|
|
// minimales de 1 366 × 700, une de 2 × 3, un guide et un manifeste inventés.
|
|||
|
|
import assert from 'node:assert/strict';
|
|||
|
|
import { createHash } from 'node:crypto';
|
|||
|
|
import { readFileSync } from 'node:fs';
|
|||
|
|
import { fileURLToPath } from 'node:url';
|
|||
|
|
import { describe, test } from '../../test/lanceur.js';
|
|||
|
|
import { versionVoisine } from '../../test/version_voisine.js';
|
|||
|
|
import { VERSION } from '../../src/version.genere.js';
|
|||
|
|
import {
|
|||
|
|
A_VENIR,
|
|||
|
|
CAPTURES,
|
|||
|
|
CHEMIN_MANIFESTE,
|
|||
|
|
ETAPES,
|
|||
|
|
REPORTEES,
|
|||
|
|
canoniser,
|
|||
|
|
cheminImage,
|
|||
|
|
cheminsHorsRacine,
|
|||
|
|
ecartDeVersion,
|
|||
|
|
empreinteEtape,
|
|||
|
|
fautesDImage,
|
|||
|
|
fautesDeMachine,
|
|||
|
|
imagesReferencees,
|
|||
|
|
lireEntetePng,
|
|||
|
|
verifierAppariement,
|
|||
|
|
verifierOrdre,
|
|||
|
|
} from './manifeste.js';
|
|||
|
|
|
|||
|
|
const FIXTURES = fileURLToPath(new URL('../../test/fixtures/captures/', import.meta.url));
|
|||
|
|
const lire = (chemin) => readFileSync(FIXTURES + chemin);
|
|||
|
|
|
|||
|
|
// Racine neutre inventée : elle n'existe nulle part ailleurs dans le dépôt.
|
|||
|
|
const RACINE = 'C:\\Racine\\Tablees';
|
|||
|
|
|
|||
|
|
const IMAGE_01 = 'doc/captures/01-premier-ecran.png';
|
|||
|
|
const IMAGE_02 = 'doc/captures/02-import-apercu.png';
|
|||
|
|
|
|||
|
|
// Le manifeste de fixture, lu à neuf à chaque appel. Il ne porte pas la
|
|||
|
|
// version réelle, qu'aucune donnée n'écrit en clair (§ 18.5, point 6) :
|
|||
|
|
// chaque capture reçoit ici celle de la construction.
|
|||
|
|
function manifesteFixture() {
|
|||
|
|
const manifeste = JSON.parse(lire('manifeste.json').toString('utf8'));
|
|||
|
|
for (const capture of manifeste.captures) capture.versionReelle = VERSION.affichee;
|
|||
|
|
return manifeste;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function captureFixture(rang = 0) {
|
|||
|
|
return manifesteFixture().captures[rang];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function referencesFixture() {
|
|||
|
|
return [{ document: 'guide.md', images: imagesReferencees(lire('guide.md').toString('utf8')) }];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Un manifeste dont seules les entrées comptent pour l'ordre : chaque capture
|
|||
|
|
// déclarée, sauf celles qu'on écarte, et chaque reportée.
|
|||
|
|
function manifesteDeclare({ sans = [], reportees = REPORTEES } = {}) {
|
|||
|
|
return {
|
|||
|
|
format: 1,
|
|||
|
|
captures: CAPTURES.filter((c) => !sans.includes(c.fichier)).map((c) => ({ fichier: c.fichier, etape: c.etape })),
|
|||
|
|
reportees: reportees.map((r) => ({ ...r })),
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const sha256 = (octets) => createHash('sha256').update(octets).digest('hex');
|
|||
|
|
const fautesDe = (fautes, motif) => fautes.filter((f) => motif.test(f.faute));
|
|||
|
|
|
|||
|
|
describe('manifeste : la liste déclarée (§ 19.3, Q8 a)', () => {
|
|||
|
|
test('les treize étapes du § 2.1, dans leur ordre', () => {
|
|||
|
|
assert.deepEqual(ETAPES, ['1', '1b', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12']);
|
|||
|
|
assert.ok(Object.isFrozen(ETAPES));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('CAPTURES égale la table du contrat : vingt-deux entrées, leur étape et leur source', () => {
|
|||
|
|
const table = CAPTURES.map((c) => [c.fichier, c.etape, c.source]);
|
|||
|
|
assert.deepEqual(table, [
|
|||
|
|
['01-premier-ecran', '1', 'navigateur'],
|
|||
|
|
['01b-passer-en-ecriture', '1b', 'navigateur'],
|
|||
|
|
['02-import-apercu', '2', 'navigateur'],
|
|||
|
|
['02b-import-cp1252', '2', 'navigateur'],
|
|||
|
|
['03-import-rapport', '2', 'navigateur'],
|
|||
|
|
['04-grille-participants', '3', 'navigateur'],
|
|||
|
|
['05-tables-etat', '4', 'navigateur'],
|
|||
|
|
['06-defaut-consequence', '4', 'navigateur'],
|
|||
|
|
['07-plan-tables', '4', 'navigateur'],
|
|||
|
|
['07a-cibles-sieges', '4', 'navigateur'],
|
|||
|
|
['07a-cibles-tables', '4', 'navigateur'],
|
|||
|
|
['07b-nombre-de-tours', '5', 'navigateur'],
|
|||
|
|
['08-animateurs-designes', '6', 'navigateur'],
|
|||
|
|
['09-refus-sur-place', '7', 'navigateur'],
|
|||
|
|
['10-diagnostic', '8', 'navigateur'],
|
|||
|
|
['11-propositions', '9', 'navigateur'],
|
|||
|
|
['19-ajustement', '11', 'navigateur'],
|
|||
|
|
['20-mode-lecture', '11', 'navigateur'],
|
|||
|
|
['22-plan-brouillon', '12', 'navigateur'],
|
|||
|
|
['23-plan-retenu', '12', 'navigateur'],
|
|||
|
|
['24-badges-controle', '12', 'pdf'],
|
|||
|
|
['25-badges-planche', '12', 'pdf'],
|
|||
|
|
]);
|
|||
|
|
for (const capture of CAPTURES) {
|
|||
|
|
assert.deepEqual(Object.keys(capture).sort(), ['configuration', 'etape', 'fichier', 'source'], capture.fichier);
|
|||
|
|
assert.equal(typeof capture.configuration, 'string', capture.fichier);
|
|||
|
|
assert.ok(capture.configuration.length > 0, capture.fichier);
|
|||
|
|
assert.ok(Object.isFrozen(capture), capture.fichier);
|
|||
|
|
}
|
|||
|
|
assert.ok(Object.isFrozen(CAPTURES));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('la configuration nomme le fichier d’import ou la démonstration que la capture exige', () => {
|
|||
|
|
const configuration = new Map(CAPTURES.map((c) => [c.fichier, c.configuration]));
|
|||
|
|
assert.equal(configuration.get('02-import-apercu'), 'participants_cas_limites.csv');
|
|||
|
|
assert.equal(configuration.get('02b-import-cp1252'), 'participants_cas_limites_cp1252.csv');
|
|||
|
|
assert.equal(configuration.get('03-import-rapport'), 'participants_cas_limites.csv');
|
|||
|
|
assert.equal(configuration.get('10-diagnostic'), 'grande');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('REPORTEES : 12 à 18 à l’étape 10, 21 à l’étape 11, raison « iteration5 », rien d’autre', () => {
|
|||
|
|
assert.deepEqual(
|
|||
|
|
REPORTEES.map((r) => [r.fichier, r.etape, r.raison]),
|
|||
|
|
[
|
|||
|
|
['12-qualite-profil', '10', 'iteration5'],
|
|||
|
|
['13-qualite-collisions', '10', 'iteration5'],
|
|||
|
|
['14-qualite-paires', '10', 'iteration5'],
|
|||
|
|
['15-pas-de-matrice', '10', 'iteration5'],
|
|||
|
|
['16-minimum-atteint', '10', 'iteration5'],
|
|||
|
|
['17-comparaison-manque', '10', 'iteration5'],
|
|||
|
|
['18-comparaison-escalier', '10', 'iteration5'],
|
|||
|
|
['21-historique', '11', 'iteration5'],
|
|||
|
|
],
|
|||
|
|
);
|
|||
|
|
const rangs = REPORTEES.map((r) => Number(r.fichier.slice(0, 2)));
|
|||
|
|
for (const rang of rangs) assert.ok((rang >= 12 && rang <= 18) || rang === 21, String(rang));
|
|||
|
|
assert.ok(Object.isFrozen(REPORTEES));
|
|||
|
|
for (const reportee of REPORTEES) assert.ok(Object.isFrozen(reportee), reportee.fichier);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('A_VENIR nomme les quatre captures de l’étape 12, toutes déclarées', () => {
|
|||
|
|
assert.deepEqual(A_VENIR, ['22-plan-brouillon', '23-plan-retenu', '24-badges-controle', '25-badges-planche']);
|
|||
|
|
const declarees = new Set(CAPTURES.map((c) => c.fichier));
|
|||
|
|
for (const fichier of A_VENIR) assert.ok(declarees.has(fichier), fichier);
|
|||
|
|
assert.ok(Object.isFrozen(A_VENIR));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('CAPTURES ∪ REPORTEES : des noms uniques, et l’ordre des noms suit celui des étapes', () => {
|
|||
|
|
const toutes = [...CAPTURES, ...REPORTEES];
|
|||
|
|
assert.equal(new Set(toutes.map((e) => e.fichier)).size, toutes.length);
|
|||
|
|
const parNom = [...toutes].sort((a, b) => (a.fichier < b.fichier ? -1 : a.fichier > b.fichier ? 1 : 0));
|
|||
|
|
const rangs = parNom.map((e) => ETAPES.indexOf(e.etape));
|
|||
|
|
assert.ok(rangs.every((rang) => rang >= 0));
|
|||
|
|
for (let i = 1; i < rangs.length; i += 1) assert.ok(rangs[i - 1] <= rangs[i], `${parNom[i - 1].fichier} › ${parNom[i].fichier}`);
|
|||
|
|
const capturesParNom = [...CAPTURES].sort((a, b) => (a.fichier < b.fichier ? -1 : 1)).map((c) => c.fichier);
|
|||
|
|
assert.deepEqual(CAPTURES.map((c) => c.fichier), capturesParNom);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('cheminImage et le chemin du manifeste sont sous doc/captures', () => {
|
|||
|
|
assert.equal(cheminImage('01-premier-ecran'), IMAGE_01);
|
|||
|
|
assert.equal(CHEMIN_MANIFESTE, 'doc/captures/manifeste.json');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('manifeste : l’empreinte d’étape (§ 19.5, amendé)', () => {
|
|||
|
|
test('SHA-256 en hexadécimal, égale à celle que porte le manifeste de fixture', () => {
|
|||
|
|
for (const capture of manifesteFixture().captures) {
|
|||
|
|
assert.match(empreinteEtape(capture), /^[0-9a-f]{64}$/);
|
|||
|
|
assert.equal(empreinteEtape(capture), capture.empreinte, capture.fichier);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('invariante à la version réelle, à la version du navigateur, à l’image, à l’empreinte et à l’ordre des clés', () => {
|
|||
|
|
const capture = captureFixture();
|
|||
|
|
const reference = empreinteEtape(capture);
|
|||
|
|
assert.equal(empreinteEtape({ ...capture, versionReelle: versionVoisine(VERSION.affichee) }), reference);
|
|||
|
|
const { versionReelle: _version, ...sansVersion } = capture;
|
|||
|
|
assert.equal(empreinteEtape(sansVersion), reference);
|
|||
|
|
assert.equal(empreinteEtape({ ...capture, versionNavigateur: '141.0.1.2' }), reference);
|
|||
|
|
assert.equal(empreinteEtape({ ...capture, pilote: { navigateur: 'chromium', versionNavigateur: '141.0.1.2' } }),
|
|||
|
|
empreinteEtape({ ...capture, pilote: { navigateur: 'chromium', versionNavigateur: '139.0.9.9' } }));
|
|||
|
|
assert.equal(empreinteEtape({ ...capture, image: { sha256: '0'.repeat(64), largeur: 1, hauteur: 1 } }), reference);
|
|||
|
|
assert.equal(empreinteEtape({ ...capture, empreinte: 'f'.repeat(64) }), reference);
|
|||
|
|
const inverse = Object.fromEntries(Object.entries(capture).reverse());
|
|||
|
|
assert.equal(empreinteEtape(inverse), reference);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('variable au texte, au thème, à l’état, aux styles, à la graine et au cadre', () => {
|
|||
|
|
const capture = captureFixture();
|
|||
|
|
const reference = empreinteEtape(capture);
|
|||
|
|
const variantes = [
|
|||
|
|
{ ...capture, texte: [...capture.texte, 'Théo Pervenche'] },
|
|||
|
|
{ ...capture, texte: capture.texte.slice().reverse() },
|
|||
|
|
{ ...capture, theme: 'sombre' },
|
|||
|
|
{ ...capture, etat: sha256('un autre état inventé') },
|
|||
|
|
{ ...capture, styles: { ...capture.styles, clair: [{ couleur: '#000000' }] } },
|
|||
|
|
{ ...capture, graine: 7 },
|
|||
|
|
{ ...capture, cadre: { largeur: 1366, hauteur: 701 } },
|
|||
|
|
];
|
|||
|
|
for (const variante of variantes) assert.notEqual(empreinteEtape(variante), reference);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('refuse ce qui n’est pas un objet, et une valeur que JSON ne porte pas', () => {
|
|||
|
|
assert.throws(() => empreinteEtape(null), TypeError);
|
|||
|
|
assert.throws(() => empreinteEtape([]), TypeError);
|
|||
|
|
assert.throws(() => empreinteEtape({ ...captureFixture(), graine: undefined }), TypeError);
|
|||
|
|
assert.throws(() => empreinteEtape({ ...captureFixture(), graine: Number.NaN }), TypeError);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('manifeste : la forme canonique', () => {
|
|||
|
|
test('clés triées à toute profondeur, une capture et une reportée par ligne, fin de ligne finale', () => {
|
|||
|
|
const manifeste = manifesteFixture();
|
|||
|
|
const texte = canoniser(manifeste);
|
|||
|
|
assert.ok(texte.endsWith(']}\n'), texte.slice(-10));
|
|||
|
|
const lignes = texte.split('\n');
|
|||
|
|
assert.equal(lignes[0], '{"captures":[');
|
|||
|
|
assert.ok(lignes[1].startsWith('{"cadre":{"hauteur":700,"largeur":1366},"empreinte":'), lignes[1]);
|
|||
|
|
assert.ok(lignes[1].endsWith('},'), lignes[1]);
|
|||
|
|
assert.ok(lignes[2].startsWith('{"cadre":'), lignes[2]);
|
|||
|
|
assert.ok(lignes[2].endsWith('}'), lignes[2]);
|
|||
|
|
assert.ok(lignes[3].startsWith('],"format":1,"pilote":{"cadre":{"hauteur":700,"largeur":1366},"facteurEchelle":1,'), lignes[3]);
|
|||
|
|
assert.ok(lignes[3].endsWith(',"reportees":['), lignes[3]);
|
|||
|
|
assert.equal(lignes[4], '{"etape":"10","fichier":"12-qualite-profil","raison":"iteration5"}');
|
|||
|
|
assert.equal(lignes[5], ']}');
|
|||
|
|
assert.equal(lignes[6], '');
|
|||
|
|
assert.deepEqual(JSON.parse(texte), manifeste);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('l’ordre des clés reçu ne change rien ; celui des tableaux, si', () => {
|
|||
|
|
const manifeste = manifesteFixture();
|
|||
|
|
const inverse = Object.fromEntries(Object.entries(manifeste).reverse());
|
|||
|
|
inverse.captures = manifeste.captures.map((c) => Object.fromEntries(Object.entries(c).reverse()));
|
|||
|
|
assert.equal(canoniser(inverse), canoniser(manifeste));
|
|||
|
|
assert.notEqual(canoniser({ ...manifeste, captures: manifeste.captures.slice().reverse() }), canoniser(manifeste));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('un tableau vide reste sur sa ligne', () => {
|
|||
|
|
assert.equal(canoniser({ format: 1, captures: [], reportees: [] }), '{"captures":[],"format":1,"reportees":[]}\n');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('refuse une valeur que JSON ne porte pas', () => {
|
|||
|
|
assert.throws(() => canoniser({ format: 1, captures: [{ fichier: undefined }] }), TypeError);
|
|||
|
|
assert.throws(() => canoniser({ format: Number.POSITIVE_INFINITY }), TypeError);
|
|||
|
|
assert.throws(() => canoniser([]), TypeError);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('manifeste : l’appariement documentation, manifeste, disque (§ 19.7)', () => {
|
|||
|
|
const images = [IMAGE_01, IMAGE_02];
|
|||
|
|
|
|||
|
|
test('imagesReferencees relève chaque image de doc/captures, une fois, dans l’ordre du texte', () => {
|
|||
|
|
assert.deepEqual(imagesReferencees(lire('guide.md').toString('utf8')), [IMAGE_01, IMAGE_02]);
|
|||
|
|
assert.deepEqual(imagesReferencees(' et doc/captures/x.jpg'), ['doc/captures/03-import-rapport.png']);
|
|||
|
|
assert.deepEqual(imagesReferencees('aucune image'), []);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('la fixture appariée ne porte aucune faute', () => {
|
|||
|
|
assert.deepEqual(verifierAppariement({ manifeste: manifesteFixture(), references: referencesFixture(), images }), []);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('balayage vide refusé : manifeste sans capture, document sans référence, aucune documentation, aucun fichier', () => {
|
|||
|
|
const vide = { ...manifesteFixture(), captures: [] };
|
|||
|
|
const sansCapture = verifierAppariement({ manifeste: vide, references: referencesFixture(), images });
|
|||
|
|
assert.equal(fautesDe(sansCapture, /aucune capture/).length, 1, JSON.stringify(sansCapture));
|
|||
|
|
assert.equal(fautesDe(sansCapture, /aucune capture/)[0].chemin, CHEMIN_MANIFESTE);
|
|||
|
|
|
|||
|
|
const documentNu = verifierAppariement({
|
|||
|
|
manifeste: manifesteFixture(),
|
|||
|
|
references: [...referencesFixture(), { document: 'GUIDE-WINDOWS.md', images: [] }],
|
|||
|
|
images,
|
|||
|
|
});
|
|||
|
|
assert.deepEqual(documentNu, [{ chemin: 'GUIDE-WINDOWS.md', faute: 'aucune image référencée' }]);
|
|||
|
|
|
|||
|
|
const sansDocument = verifierAppariement({ manifeste: manifesteFixture(), references: [], images });
|
|||
|
|
assert.equal(fautesDe(sansDocument, /aucun document/).length, 1, JSON.stringify(sansDocument));
|
|||
|
|
|
|||
|
|
const sansFichier = verifierAppariement({ manifeste: manifesteFixture(), references: referencesFixture(), images: [] });
|
|||
|
|
assert.equal(fautesDe(sansFichier, /aucune image sur le disque/).length, 1, JSON.stringify(sansFichier));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('orpheline côté document, côté manifeste, côté disque : trois fautes, chacune nommée', () => {
|
|||
|
|
const manifeste = manifesteFixture();
|
|||
|
|
const seule = (fichier) => ({ ...manifeste.captures[0], fichier });
|
|||
|
|
manifeste.captures.push(seule('04-grille-participants'));
|
|||
|
|
const references = referencesFixture();
|
|||
|
|
references[0].images.push('doc/captures/03-import-rapport.png');
|
|||
|
|
const fautes = verifierAppariement({ manifeste, references, images: [...images, 'doc/captures/05-tables-etat.png'] });
|
|||
|
|
assert.deepEqual(fautes, [
|
|||
|
|
{ chemin: 'doc/captures/03-import-rapport.png', faute: 'orpheline : référencée par guide.md ; absente du manifeste et du disque' },
|
|||
|
|
{ chemin: 'doc/captures/04-grille-participants.png', faute: 'orpheline : au manifeste ; absente de la documentation et du disque' },
|
|||
|
|
{ chemin: 'doc/captures/05-tables-etat.png', faute: 'orpheline : sur le disque ; absente de la documentation et du manifeste' },
|
|||
|
|
]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('une image présente de deux côtés sur trois est une orpheline du troisième', () => {
|
|||
|
|
const fautes = verifierAppariement({ manifeste: manifesteFixture(), references: referencesFixture(), images: [IMAGE_01] });
|
|||
|
|
assert.deepEqual(fautes, [{ chemin: IMAGE_02, faute: 'orpheline : référencée par guide.md, au manifeste ; absente du disque' }]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('une capture deux fois au manifeste est une faute', () => {
|
|||
|
|
const manifeste = manifesteFixture();
|
|||
|
|
manifeste.captures.push({ ...manifeste.captures[1] });
|
|||
|
|
const fautes = verifierAppariement({ manifeste, references: referencesFixture(), images });
|
|||
|
|
assert.deepEqual(fautes, [{ chemin: IMAGE_02, faute: 'deux fois au manifeste' }]);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('manifeste : l’ordre contre les étapes du § 2.1 (§ 19.3)', () => {
|
|||
|
|
test('CAPTURES ∪ REPORTEES réel : aucune faute', () => {
|
|||
|
|
assert.deepEqual(verifierOrdre(manifesteDeclare()), []);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('les captures d’A_VENIR, absentes du manifeste, sont admises', () => {
|
|||
|
|
assert.deepEqual(verifierOrdre(manifesteDeclare({ sans: A_VENIR })), []);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('A_VENIR vidé, l’étape 12 sans capture est une faute', () => {
|
|||
|
|
const fautes = verifierOrdre(manifesteDeclare({ sans: A_VENIR }), { aVenir: [] });
|
|||
|
|
assert.deepEqual(fautes, [{ chemin: CHEMIN_MANIFESTE, faute: 'étape 12 : aucune capture ni reportée' }]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('une étape sans capture ni reportée est une faute qui la nomme', () => {
|
|||
|
|
assert.deepEqual(verifierOrdre(manifesteDeclare({ sans: ['07b-nombre-de-tours'] })), [
|
|||
|
|
{ chemin: CHEMIN_MANIFESTE, faute: 'étape 5 : aucune capture ni reportée' },
|
|||
|
|
]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('les reportées couvrent l’étape 10 ; sans elles, elle manque', () => {
|
|||
|
|
assert.deepEqual(verifierOrdre(manifesteDeclare({ reportees: [] })), [
|
|||
|
|
{ chemin: CHEMIN_MANIFESTE, faute: 'étape 10 : aucune capture ni reportée' },
|
|||
|
|
]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('ordre contredit : 02 rattachée à l’étape 4 avant 03 rattachée à la 2', () => {
|
|||
|
|
const manifeste = manifesteDeclare();
|
|||
|
|
manifeste.captures.find((c) => c.fichier === '02-import-apercu').etape = '4';
|
|||
|
|
assert.deepEqual(verifierOrdre(manifeste), [
|
|||
|
|
{ chemin: 'doc/captures/02b-import-cp1252.png', faute: 'étape 2 après 02-import-apercu, de l’étape 4 : l’ordre contredit les étapes' },
|
|||
|
|
]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('l’ordre se lit sur les noms, pas sur la place dans le tableau', () => {
|
|||
|
|
const manifeste = manifesteDeclare();
|
|||
|
|
manifeste.captures.reverse();
|
|||
|
|
assert.deepEqual(verifierOrdre(manifeste), []);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('une étape inconnue, un nom déclaré deux fois : des fautes', () => {
|
|||
|
|
const inconnue = manifesteDeclare();
|
|||
|
|
inconnue.captures.find((c) => c.fichier === '05-tables-etat').etape = '4b';
|
|||
|
|
assert.deepEqual(verifierOrdre(inconnue), [{ chemin: 'doc/captures/05-tables-etat.png', faute: 'étape inconnue « 4b »' }]);
|
|||
|
|
|
|||
|
|
const double = manifesteDeclare();
|
|||
|
|
double.reportees.push({ fichier: '19-ajustement', etape: '11', raison: 'iteration5' });
|
|||
|
|
assert.deepEqual(verifierOrdre(double), [{ chemin: 'doc/captures/19-ajustement.png', faute: 'déclarée deux fois' }]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('balayage vide refusé : aucune entrée est une faute, et chaque étape manque', () => {
|
|||
|
|
const fautes = verifierOrdre({ format: 1, captures: [], reportees: [] }, { aVenir: [] });
|
|||
|
|
assert.equal(fautes[0].faute, 'aucune capture ni reportée');
|
|||
|
|
assert.equal(fautes.length, 1 + ETAPES.length);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('manifeste : l’image et son en-tête', () => {
|
|||
|
|
test('lireEntetePng lit 2 × 3 sur le PNG minimal', () => {
|
|||
|
|
assert.deepEqual(lireEntetePng(lire('deux_par_trois.png')), { largeur: 2, hauteur: 3 });
|
|||
|
|
assert.deepEqual(lireEntetePng(new Uint8Array(lire('doc/captures/01-premier-ecran.png'))), { largeur: 1366, hauteur: 700 });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('lireEntetePng refuse une signature fausse, un en-tête tronqué, un premier morceau qui n’est pas IHDR', () => {
|
|||
|
|
const octets = new Uint8Array(lire('deux_par_trois.png'));
|
|||
|
|
const signature = octets.slice();
|
|||
|
|
signature[1] = 0x51;
|
|||
|
|
assert.throws(() => lireEntetePng(signature), /signature PNG/);
|
|||
|
|
assert.throws(() => lireEntetePng(octets.slice(0, 20)), /tronqué/);
|
|||
|
|
const autre = octets.slice();
|
|||
|
|
autre[12] = 0x69;
|
|||
|
|
assert.throws(() => lireEntetePng(autre), /IHDR/);
|
|||
|
|
const nulle = octets.slice();
|
|||
|
|
nulle.fill(0, 16, 20);
|
|||
|
|
assert.throws(() => lireEntetePng(nulle), /largeur/);
|
|||
|
|
assert.throws(() => lireEntetePng('PNG'), TypeError);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('l’image de la fixture, octets et dimensions conformes : aucune faute', () => {
|
|||
|
|
const capture = captureFixture();
|
|||
|
|
assert.deepEqual(fautesDImage(capture, lire('doc/captures/01-premier-ecran.png')), []);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('image remplacée par les octets d’une autre : faute', () => {
|
|||
|
|
const fautes = fautesDImage(captureFixture(0), lire('doc/captures/02-import-apercu.png'));
|
|||
|
|
assert.deepEqual(fautes, [{ chemin: IMAGE_01, faute: 'image remplacée : son empreinte diffère de celle du manifeste' }]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('dimensions ≠ cadre × facteur : faute', () => {
|
|||
|
|
const capture = captureFixture();
|
|||
|
|
const double = { ...capture, facteurEchelle: 2 };
|
|||
|
|
assert.deepEqual(fautesDImage(double, lire('doc/captures/01-premier-ecran.png')), [
|
|||
|
|
{ chemin: IMAGE_01, faute: 'dimensions 1366 × 700, attendues 2732 × 1400 (cadre × facteur)' },
|
|||
|
|
]);
|
|||
|
|
const autreCadre = { ...capture, cadre: { largeur: 1280, hauteur: 700 } };
|
|||
|
|
assert.deepEqual(fautesDImage(autreCadre, lire('doc/captures/01-premier-ecran.png')), [
|
|||
|
|
{ chemin: IMAGE_01, faute: 'dimensions 1366 × 700, attendues 1280 × 700 (cadre × facteur)' },
|
|||
|
|
]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('dimensions du fichier ≠ celles que le manifeste inscrit : faute', () => {
|
|||
|
|
const capture = captureFixture();
|
|||
|
|
const inscrite = { ...capture, image: { ...capture.image, hauteur: 699 } };
|
|||
|
|
assert.deepEqual(fautesDImage(inscrite, lire('doc/captures/01-premier-ecran.png')), [
|
|||
|
|
{ chemin: IMAGE_01, faute: 'dimensions 1366 × 700, le manifeste inscrit 1366 × 699' },
|
|||
|
|
]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('un fichier qui n’est pas un PNG : faute, pas d’exception', () => {
|
|||
|
|
const capture = captureFixture();
|
|||
|
|
const octets = new TextEncoder().encode('ceci n’est pas une image');
|
|||
|
|
const fautes = fautesDImage({ ...capture, image: { ...capture.image, sha256: sha256(octets) } }, octets);
|
|||
|
|
assert.equal(fautes.length, 1, JSON.stringify(fautes));
|
|||
|
|
assert.match(fautes[0].faute, /^illisible : /);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('manifeste : les chemins hors de la racine neutre (§ 19.4)', () => {
|
|||
|
|
test('relève C:\\Users\\…, /home/…, /tmp/… ; admet la racine elle-même et ce qui est dessous', () => {
|
|||
|
|
const texte = [
|
|||
|
|
`Dossier de travail : ${RACINE}`,
|
|||
|
|
`Événement : ${RACINE}\\Bal des Grisailles.gtt.json`,
|
|||
|
|
'Copie : C:\\Users\\Exemple\\Bureau\\x',
|
|||
|
|
'Ailleurs : /home/exemple/x',
|
|||
|
|
'Temporaire (/tmp/x).',
|
|||
|
|
].join('\n');
|
|||
|
|
assert.deepEqual(cheminsHorsRacine(texte, RACINE), ['C:\\Users\\Exemple\\Bureau\\x', '/home/exemple/x', '/tmp/x']);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('la racine se reconnaît sous l’autre séparateur et l’autre casse de Windows ; un nom qui la prolonge ne l’est pas', () => {
|
|||
|
|
assert.deepEqual(cheminsHorsRacine('c:/racine/tablees/a et C:\\RACINE\\TABLEES\\b', RACINE), []);
|
|||
|
|
assert.deepEqual(cheminsHorsRacine(`${RACINE}X\\a`, RACINE), [`${RACINE}X\\a`]);
|
|||
|
|
assert.deepEqual(cheminsHorsRacine('D:\\Racine\\Tablees\\a', RACINE), ['D:\\Racine\\Tablees\\a']);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('une racine qui porte une espace se reconnaît entière', () => {
|
|||
|
|
const racine = 'C:\\Mes Tablées';
|
|||
|
|
assert.deepEqual(cheminsHorsRacine(`ouvert depuis ${racine}\\Soirée des Merles.gtt.json`, racine), []);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('chemin réseau, dossier personnel abrégé, racine POSIX', () => {
|
|||
|
|
assert.deepEqual(cheminsHorsRacine('\\\\serveur\\partage\\x et ~/x', RACINE), ['\\\\serveur\\partage\\x', '~/x']);
|
|||
|
|
assert.deepEqual(cheminsHorsRacine('/srv/neutre/a /srv/neutre /srv/neutreX/b', '/srv/neutre'), ['/srv/neutreX/b']);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('ni une adresse web, ni une fraction, ni un chemin relatif ne sont des chemins absolus', () => {
|
|||
|
|
assert.deepEqual(cheminsHorsRacine('http://www.example.org/a 3/4 tables, doc/captures/x.png, et/ou', RACINE), []);
|
|||
|
|
assert.deepEqual(cheminsHorsRacine('Lien:https://exemple.invalid/a à 12:30', RACINE), []);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('un chemin POSIX qui suit un deux-points est relevé', () => {
|
|||
|
|
assert.deepEqual(cheminsHorsRacine('Chemin:/home/exemple/x et poste-exemple:/var/y', RACINE), ['/home/exemple/x', '/var/y']);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('un chemin réseau aux barres obliques est relevé ; sous une racine réseau, il est admis', () => {
|
|||
|
|
assert.deepEqual(cheminsHorsRacine('//serveur/partage/x et (//serveur/autre)', RACINE), ['//serveur/partage/x', '//serveur/autre']);
|
|||
|
|
assert.deepEqual(cheminsHorsRacine('//serveur/partage/a', '\\\\serveur\\partage'), []);
|
|||
|
|
assert.deepEqual(cheminsHorsRacine('//serveur/partageX/a', '\\\\serveur\\partage'), ['//serveur/partageX/a']);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('une adresse file:// est relevée par le chemin qu’elle porte, encodage compris', () => {
|
|||
|
|
const texte = 'file:///home/exemple/x, FILE:///C:/Users/Exemple/x et file://serveur/partage/x';
|
|||
|
|
assert.deepEqual(cheminsHorsRacine(texte, RACINE), ['file:///home/exemple/x', 'FILE:///C:/Users/Exemple/x', 'file://serveur/partage/x']);
|
|||
|
|
assert.deepEqual(cheminsHorsRacine('file:///c:/racine/tablees/a.gtt.json', RACINE), []);
|
|||
|
|
assert.deepEqual(cheminsHorsRacine('file:///C:/Mes%20Tabl%C3%A9es/a.gtt.json', 'C:\\Mes Tablées'), []);
|
|||
|
|
assert.deepEqual(cheminsHorsRacine('file:///srv/neutre/a file:///srv/neutreX/b', '/srv/neutre'), ['file:///srv/neutreX/b']);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('chaque chemin une fois, dans l’ordre du texte ; une racine absente ou relative est refusée', () => {
|
|||
|
|
assert.deepEqual(cheminsHorsRacine('/tmp/x puis /tmp/x puis /var/y', RACINE), ['/tmp/x', '/var/y']);
|
|||
|
|
assert.throws(() => cheminsHorsRacine('x', ''), TypeError);
|
|||
|
|
assert.throws(() => cheminsHorsRacine('x', 'relatif\\dossier'), TypeError);
|
|||
|
|
assert.throws(() => cheminsHorsRacine(42, RACINE), TypeError);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('manifeste : l’écart de version (§ 19.7)', () => {
|
|||
|
|
test('null à version égale, par défaut celle de la construction', () => {
|
|||
|
|
assert.equal(ecartDeVersion(manifesteFixture(), VERSION.affichee), null);
|
|||
|
|
assert.equal(ecartDeVersion(manifesteFixture()), null);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('l’écart nommé sinon : la version du manifeste et celle de la construction', () => {
|
|||
|
|
const voisine = versionVoisine(VERSION.affichee);
|
|||
|
|
assert.deepEqual(ecartDeVersion(manifesteFixture(), voisine), { manifeste: VERSION.affichee, construction: voisine });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('deux versions au manifeste : chacune nommée, une fois, dans l’ordre des captures', () => {
|
|||
|
|
const voisine = versionVoisine(VERSION.affichee);
|
|||
|
|
const manifeste = manifesteFixture();
|
|||
|
|
manifeste.captures[1].versionReelle = voisine;
|
|||
|
|
manifeste.captures.push({ ...manifeste.captures[0] });
|
|||
|
|
assert.deepEqual(ecartDeVersion(manifeste, VERSION.affichee), { manifeste: `${VERSION.affichee}, ${voisine}`, construction: VERSION.affichee });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('un manifeste sans capture n’établit aucune version : écart', () => {
|
|||
|
|
assert.deepEqual(ecartDeVersion({ format: 1, captures: [] }, VERSION.affichee), { manifeste: null, construction: VERSION.affichee });
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('manifeste : un relevé ne nomme pas sa machine (contrainte 27)', () => {
|
|||
|
|
test('le manifeste de fixture passe', () => {
|
|||
|
|
assert.deepEqual(fautesDeMachine(manifesteFixture(), RACINE), []);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('un champ d’hôte au pilote, une clé inconnue d’une capture ou du manifeste : refusés', () => {
|
|||
|
|
const manifeste = manifesteFixture();
|
|||
|
|
manifeste.pilote.hote = 'poste-des-merles';
|
|||
|
|
manifeste.captures[1].utilisateur = 'ondine';
|
|||
|
|
manifeste.date = '2030-01-01';
|
|||
|
|
assert.deepEqual(fautesDeMachine(manifeste, RACINE), [
|
|||
|
|
{ chemin: CHEMIN_MANIFESTE, faute: 'clé hors du contrat : « date »' },
|
|||
|
|
{ chemin: CHEMIN_MANIFESTE, faute: 'clé hors du contrat : « pilote.hote »' },
|
|||
|
|
{ chemin: IMAGE_02, faute: 'clé hors du contrat : « utilisateur »' },
|
|||
|
|
]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('un chemin de compte dans le texte relevé ou dans le pilote : refusé', () => {
|
|||
|
|
const manifeste = manifesteFixture();
|
|||
|
|
manifeste.captures[0].texte.push('/home/exemple/x');
|
|||
|
|
manifeste.pilote.navigateur = 'C:\\Users\\Exemple\\chromium';
|
|||
|
|
assert.deepEqual(fautesDeMachine(manifeste, RACINE), [
|
|||
|
|
{ chemin: CHEMIN_MANIFESTE, faute: 'chemin hors de la racine neutre : « C:\\Users\\Exemple\\chromium »' },
|
|||
|
|
{ chemin: IMAGE_01, faute: 'chemin hors de la racine neutre : « /home/exemple/x »' },
|
|||
|
|
]);
|
|||
|
|
});
|
|||
|
|
});
|