[ADD] badges: tests for control page wrapping, margins and imposition

No test covered how the control page wraps its lines at spaces, its
margins on a continuation page, the second imposition value or the
default order a sheet takes when none is given. Twelve mutants of the
sheet producer and of its session layer survived; each is now caught
by one test. The margin cases need the full control sheet and run in
the long series. No source file changes.
Checked: 3855 node and 101 long tests, from the index alone.

--- FR ---

[ADD] badges : épreuves du repli, des marges et de l'imposition

Aucune épreuve ne couvrait le repli des lignes de la feuille de
contrôle aux espaces, ses marges sur une page de suite, l'autre valeur
d'imposition ni l'ordre par défaut d'une planche. Douze mutants de la
production et de sa couche de séance survivaient ; chacun tombe
désormais sous une épreuve. Les marges exigent la feuille de contrôle
pleine et vivent dans la série longue. Aucun fichier source ne change.
Vérifié : 3855 node et 101 longues, depuis l'index seul.

Assisted-by: Claude Opus 5.5
This commit is contained in:
Mathieu Benoit 2026-10-10 17:22:35 -04:00
parent 01b5582f62
commit 8f1edc83c7
3 changed files with 207 additions and 33 deletions

View file

@ -13,12 +13,15 @@
// productions de PDF au plus dans ce fichier, sur une soirée de quatre
// personnes — trois pour les trois modes, trois pour la stabilité des
// octets, une réédition, deux refus d'écriture et l'enregistrement sous,
// une sous web, une proposition. Celle sous web sort en Lettre, en pile,
// par table : sur deux badges par planche, l'imposition et l'ordre demandés
// se lisent dans la place de chaque nom. La réédition porte un caractère
// sans glyphe, que sa feuille de contrôle nomme. Le témoin de 257 ne
// s'analyse que pour son annonce. Les noms sont inventés ; la version vient
// du module engendré.
// une sous web, une proposition. L'imposition et l'ordre se lisent dans la
// place de chaque nom, et le papier, l'imposition et le nombre de planches
// dans la feuille de contrôle : la première production, sous la demande
// par défaut, en A4 — l'ordre alphabétique ; la proposition, sous la
// demande par défaut, en Lettre — feuille par feuille sur deux planches ;
// celle sous web en Lettre, en pile, par table. La réédition porte un
// caractère sans glyphe, que sa feuille de contrôle nomme. Le témoin de 257
// ne s'analyse que pour son annonce. Les noms sont inventés ; la version
// vient du module engendré.
import assert from 'node:assert/strict';
import { creerFichiersSimules } from '../../test/fichiers_simules.js';
import { H_PROPOSITION, H_RETENU, personne, soiree, table } from '../../test/fixtures/badges/cas.js';
@ -26,6 +29,8 @@ import { FORME_TEMOIN, chargeTemoin } from '../../test/fixtures/badges/temoin.js
import { describe, test } from '../../test/lanceur.js';
import { lirePdf } from '../../test/pdf.js';
import { lirePolices } from '../../test/polices.js';
import { PAPIERS } from '../geometrie/papiers.js';
import { cellules } from '../pdf/imposition.js';
import { serialiser } from '../stockage/canonique.js';
import { creer as creerDansLeDepot } from '../stockage/depot.js';
import { FORMAT } from '../stockage/document.js';
@ -161,17 +166,33 @@ async function refusee(promesse, code, details) {
// Les textes d'une page du PDF relu, et si l'un d'eux s'écrit au trait.
const textesDe = (page) => page.textes.map(({ texte }) => texte);
// Les noms d'une page de badges, parmi ceux donnés, du haut vers le bas puis
// de gauche à droite : l'ordre des cellules sur la feuille. Dans les boîtes
// que rend lirePdf, y croît vers le pied de la page.
const nomsDe = (page, noms) =>
page.textes
.filter(({ texte }) => noms.includes(texte))
.sort((a, b) => a.y - b.y || a.x - b.x)
.map(({ texte }) => texte);
const auTrait = (pdf) => pdf.pages.some((page) => page.modesTexte.includes(1));
// Les noms d'une page de badges, parmi ceux donnés, cellule par cellule
// dans l'ordre de cellules(papier) ; null pour une cellule où aucun ne se
// lit. Un nom appartient à la cellule qui contient le centre de sa boîte.
const NOMS = ['Iris Ombrelle', 'Théo Pervenche', 'Ondine Lacasse', 'Grisaille'];
function nomsParCellule(page, papier, noms = NOMS) {
return cellules(papier).map(({ x, y, largeur, hauteur }) => {
const lus = page.textes
.filter((t) => noms.includes(t.texte))
.filter((t) => t.x + t.l / 2 > x && t.x + t.l / 2 < x + largeur && t.y + t.h / 2 > y && t.y + t.h / 2 < y + hauteur)
.map(({ texte }) => texte);
assert.ok(lus.length <= 1, `plusieurs noms dans une cellule : ${lus.join(', ')}`);
return lus[0] ?? null;
});
}
// Les lignes de la feuille de contrôle qui disent le papier, l'imposition et
// le nombre de planches d'une production.
const lignesDeSortie = ({ papier, imposition, planches }) => [
libelle('planche.controle.papier', { papier, ...PAPIERS[papier] }),
libelle('planche.controle.imposition', { imposition }),
libelle('planche.controle.planches', { planches }),
];
// Celles que porte la page de contrôle relue, parmi les trois.
const sortieLue = (pdf, sortie) => lignesDeSortie(sortie).filter((ligne) => textesDe(pdf.pages[0]).includes(ligne));
describe('planche dans la séance : sortir n’est pas modifier (contrainte 24, § 8.4, § 9)', () => {
test('produire en lecture, sur un plan bloqué, sous un format plus récent : la trace ne porte que le .pdf, déposé à côté de l’événement', async () => {
const cas = [
@ -213,6 +234,12 @@ describe('planche dans la séance : sortir n’est pas modifier (contrainte 24,
const pdf = await lirePdf(retenu);
assert.ok(!pdf.pages.some((page) => textesDe(page).includes(FILIGRANE)));
assert.equal(auTrait(pdf), false);
// La demande par défaut : l'ordre alphabétique, par nom puis prénom, sur
// l'unique planche A4, feuille par feuille.
const sortie = { papier: 'A4', imposition: 'feuille', planches: 1 };
assert.deepEqual(sortieLue(pdf, sortie), lignesDeSortie(sortie), textesDe(pdf.pages[0]).join(' | '));
assert.equal(pdf.pages.length, 2);
assert.deepEqual(nomsParCellule(pdf.pages[1], 'A4'), ['Grisaille', 'Ondine Lacasse', 'Iris Ombrelle', 'Théo Pervenche']);
});
test('sans papier choisi : PAPIER_REQUIS avant toute écriture ; choisi, le nom et l’annonce le portent, et une autre séance de la racine ne le redemande pas', async () => {
@ -376,13 +403,25 @@ describe('planche dans la séance : les octets et le nom (§ 11.6, § 11.8)', ()
assert.equal(pdf.pages.length, 2);
});
test('une proposition produite porte le mot du filigrane au trait ; sa feuille de contrôle aussi', async () => {
const { fs, seance } = await monter();
test('une proposition produite porte le mot du filigrane au trait, sa feuille de contrôle aussi ; en Lettre sous la demande par défaut, feuille par feuille, dans l’ordre alphabétique', async () => {
const { fs, seance } = await monter(chargeLucioles(), { papier: 'Lettre' });
const rendu = await seance.produirePlanche({ source: 'proposition', proposition: 2 });
assert.equal(rendu.nom, nomDe({ horodatage: H_PROPOSITION }));
assert.equal(rendu.nom, nomDe({ papier: 'Lettre', horodatage: H_PROPOSITION }));
const pdf = await lirePdf(fs.octets('documents', rendu.nom));
assert.ok(pdf.pages.every((page) => textesDe(page).includes(FILIGRANE)));
assert.equal(auTrait(pdf), true);
// Deux badges par planche : feuille par feuille, la planche p porte les
// badges 2p − 1 et 2p de l'ordre alphabétique — en pile, ce seraient p
// et p + 2 ; par table, la proposition 2 assied Ombrelle avec Grisaille.
const sortie = { papier: 'Lettre', imposition: 'feuille', planches: 2 };
assert.deepEqual(sortieLue(pdf, sortie), lignesDeSortie(sortie), textesDe(pdf.pages[0]).join(' | '));
assert.deepEqual(
pdf.pages.slice(1).map((page) => nomsParCellule(page, 'Lettre')),
[
['Grisaille', 'Ondine Lacasse'],
['Iris Ombrelle', 'Théo Pervenche'],
],
);
});
});
@ -448,10 +487,11 @@ describe('planche dans la séance : l’écriture refusée, enregistrer sous, le
// Grisaille, Lacasse (table 2) ; en pile sur deux planches, la planche p
// porte les badges p et p + 2 de cet ordre.
const pdf = await lirePdf(recus[0][1]);
const noms = ['Iris Ombrelle', 'Théo Pervenche', 'Ondine Lacasse', 'Grisaille'];
assert.equal(pdf.pages.length, 3);
const sortie = { papier: 'Lettre', imposition: 'pile', planches: 2 };
assert.deepEqual(sortieLue(pdf, sortie), lignesDeSortie(sortie), textesDe(pdf.pages[0]).join(' | '));
assert.deepEqual(
pdf.pages.slice(1).map((page) => nomsDe(page, noms)),
pdf.pages.slice(1).map((page) => nomsParCellule(page, 'Lettre')),
[
['Iris Ombrelle', 'Grisaille'],
['Théo Pervenche', 'Ondine Lacasse'],

View file

@ -22,6 +22,7 @@ import { cadreUtile, cellules, filets, imposer, nombrePlanches } from './imposit
import { produirePlanche } from './planche.js';
const ZONE = constante('ZONE_SILENCE_MM');
const MARGE = constante('MARGE_PAGE_MM');
const TOLERANCE = 1e-6;
const FILIGRANE = libelle('planche.brouillon');
const polices = lirePolices();
@ -156,7 +157,7 @@ describe('produirePlanche : la géométrie du Lettre et le témoin (§ 11.2, §
});
describe('produirePlanche : une feuille de contrôle plus longue qu\u{2019}une page (§ 11.3)', () => {
test('les lignes qui débordent continuent sur une page de contrôle sans règle ni filet, avant les planches', async () => {
test('les lignes qui débordent continuent sur une page de contrôle sans règle ni filet, avant les planches, chaque page remplie dans ses marges', async () => {
const m = modele(150, { brouillon: true, reedition: true });
const controle = textesControle(m, { papier: 'A4', imposition: 'feuille', planches: nombrePlanches(150, 'A4') });
const lu = await produireEtLire(m, 'A4', 'feuille');
@ -173,6 +174,16 @@ describe('produirePlanche : une feuille de contrôle plus longue qu\u{2019}une p
assert.deepEqual(controleLu, ['0', '50', '100', ...controle]);
for (const page of lu.pages.slice(0, pagesControle)) assert.equal(page.textes.filter(estFiligrane).length, 1);
for (const page of lu.pages.slice(1, pagesControle)) assert.deepEqual(page.segments, []);
// Chaque texte des pages de contrôle dans les quatre marges : la page
// de suite commence sous la marge haute, et une page ne se remplit que
// jusqu'à la marge basse — la ligne qui la passerait part à la suivante.
const { largeur: W, hauteur: H } = PAPIERS.A4;
lu.pages.slice(0, pagesControle).forEach((page, rang) => {
for (const { texte, x, y, l, h } of page.textes) {
const dedans = x >= MARGE - TOLERANCE && y >= MARGE - TOLERANCE && x + l <= W - MARGE + TOLERANCE && y + h <= H - MARGE + TOLERANCE;
assert.ok(dedans, `page ${rang + 1} : « ${texte} » en ${JSON.stringify({ x, y, l, h })}`);
}
});
const attendu = imposer(150, 'A4', 'feuille').map((planche) => planche.map((i) => (i === null ? null : nomTemoin(i))));
assert.deepEqual(nomsLus(lu.pages.slice(pagesControle), 'A4'), attendu);
});

View file

@ -12,9 +12,13 @@
// petits modèles écrits à la main, que toutes les épreuves du fichier
// lisent — A4, cinq badges, feuille par feuille, brouillon, horodatage
// connu, produite deux fois ; Lettre, trois badges, en pile, retenu,
// horodatage inconnu. Les lignes de contrôle viennent de textesControle, le
// mot du filigrane de libelle, comme la séance les passera.
// horodatage inconnu, sous un nom d'événement plus large que la page, dont
// la ligne de contrôle se replie aux espaces, reporte entier un mot qui
// tient seul, puis coupe un mot qui ne tient pas seul. Les lignes de
// contrôle viennent de textesControle, le mot du filigrane de libelle,
// comme la séance les passera.
import assert from 'node:assert/strict';
import fontkit from '@pdf-lib/fontkit';
import { PDFDocument, PDFName } from 'pdf-lib';
import { lirePdf } from '../../test/pdf.js';
import { lirePolices } from '../../test/polices.js';
@ -25,6 +29,7 @@ import { constante } from '../geometrie/constantes.js';
import { PAPIERS } from '../geometrie/papiers.js';
import { VERSION } from '../version.genere.js';
import { cellules, filets, imposer, nombrePlanches } from './imposition.js';
import { metriquesDe } from './mise_en_page.js';
import { analyserPlanche, produirePlanche } from './planche.js';
const TOLERANCE = 1e-6;
@ -35,10 +40,18 @@ const TIRET = '\u{2014}';
const polices = lirePolices();
const EVENEMENT = { nom: 'Souper du Club des Merles', date: '2026-10-09' };
// Un nom d'événement plus large que la largeur utile d'une page : des mots,
// dont un mot long qui tient seul sur une ligne mais déborde en fin de
// ligne là où quelques-unes de ses grappes tiendraient encore, puis un mot
// composé sans espace, plus large à lui seul qu'une ligne.
const EVENEMENT_LONG = {
nom: `Grand souper annuel des amis du Club des Merles et de la Pervenche, au profit des Ombrellebleuetières, Merles${'-Lucioles'.repeat(14)}`,
date: '2026-10-09',
};
const HORODATAGE = '2026-10-09T19:00:00-04:00';
/** Un badge écrit à la main : ses textes tels que BA1 les compose. */
function badge(participant, nom, { appartenance = null, tours = 2, horodatage = HORODATAGE } = {}) {
function badge(participant, nom, { appartenance = null, tours = 2, horodatage = HORODATAGE, evenement = EVENEMENT } = {}) {
const lignes = Array.from({ length: tours }, (_, i) => ({ tour: i + 1, table: participant + i, siege: null, titre: null }));
const ligneHorodatage = horodatage === null ? libelle('planche.pied.horodatageInconnu') : libelle('planche.pied.horodatage', { horodatage });
return {
@ -51,17 +64,17 @@ function badge(participant, nom, { appartenance = null, tours = 2, horodatage =
nom,
lignes: lignes.map((ligne) => libelle('planche.tour', ligne)),
appartenance,
pied: [libelle('planche.pied.evenement', EVENEMENT), ligneHorodatage],
piedReduit: [libelle('planche.pied.date', EVENEMENT), ligneHorodatage],
pied: [libelle('planche.pied.evenement', evenement), ligneHorodatage],
piedReduit: [libelle('planche.pied.date', evenement), ligneHorodatage],
},
};
}
/** Un modèle de planche écrit à la main. */
function modele(badges, { horodatage = HORODATAGE, brouillon = false, exclus = 0 } = {}) {
function modele(badges, { horodatage = HORODATAGE, brouillon = false, exclus = 0, evenement = EVENEMENT } = {}) {
return {
badges,
evenement: EVENEMENT,
evenement,
horodatage,
brouillon,
comptes: { participants: badges.length + exclus, exclus, produits: badges.length },
@ -78,8 +91,8 @@ const MODELE_A4 = modele(
);
const NOMS_LETTRE = ['Aube Sarcelle', 'Côme Pervenche', 'Ysée Lacasse'];
const MODELE_LETTRE = modele(
NOMS_LETTRE.map((nom, i) => badge(i + 10, nom, { horodatage: null, tours: 3 })),
{ horodatage: null },
NOMS_LETTRE.map((nom, i) => badge(i + 10, nom, { horodatage: null, tours: 3, evenement: EVENEMENT_LONG })),
{ horodatage: null, evenement: EVENEMENT_LONG },
);
const entrees = (m, papier, imposition) => ({
@ -329,6 +342,17 @@ describe('produirePlanche : les badges (§ 11.3, § 11.4, § 11.6)', () => {
});
});
// La mesure de la police embarquée, celle que la production emploie :
// les deux graisses chargées par pdf-lib et fontkit dans un document
// neuf, qui ne s'enregistre pas.
const documentDeMesure = await PDFDocument.create({ updateMetadata: false });
documentDeMesure.registerFontkit(fontkit);
const metriques = metriquesDe({
regulier: await documentDeMesure.embedFont(polices.regulier, { subset: true }),
gras: await documentDeMesure.embedFont(polices.gras, { subset: true }),
});
const MM_PAR_PT = 25.4 / 72;
describe('produirePlanche : la feuille de contrôle, page 1 (§ 11.3)', () => {
// La graduation : le segment horizontal de 100 mm, et les repères qui le
// touchent, verticaux.
@ -343,6 +367,48 @@ describe('produirePlanche : la feuille de contrôle, page 1 (§ 11.3)', () => {
return { regle, reperes };
}
// Les lignes de contrôle d'une page : ses fragments dans l'ordre du flux,
// sans les chiffres de la règle, posés d'abord, ni le filigrane, que sa
// rotation rend plus haut qu'aucune ligne.
const CHIFFRES_REGLE = ['0', '50', '100'];
function fragmentsDeControle(page) {
const sansFiligrane = page.textes.filter(({ texte, h }) => !(texte === FILIGRANE && h > 20));
assert.deepEqual(sansFiligrane.slice(0, CHIFFRES_REGLE.length).map(({ texte }) => texte), CHIFFRES_REGLE);
return sansFiligrane.slice(CHIFFRES_REGLE.length);
}
// Les fragments rapportés aux lignes attendues, dans l'ordre : chaque ligne
// se lit d'un ou de plusieurs fragments consécutifs, accolés quand la coupe
// tombe dans un mot, séparés par une espace quand elle tombe sur une
// espace — aucun caractère perdu, ajouté ni déplacé, aucun fragment de
// reste. Rend, par ligne, ses morceaux, l'indice dans la ligne où chacun
// commence et la nature de chaque coupe.
function recomposer(fragments, lignes) {
let k = 0;
const recomposees = lignes.map((ligne) => {
const morceaux = [];
const debuts = [];
const coupes = [];
let position = 0;
while (position < ligne.length) {
const fragment = fragments[k];
assert.ok(fragment !== undefined && ligne.startsWith(fragment.texte, position), `« ${ligne} » à ${position} : « ${fragment?.texte} »`);
morceaux.push(fragment);
debuts.push(position);
k += 1;
position += fragment.texte.length;
if (position < ligne.length) {
const surEspace = ligne[position] === ' ';
coupes.push(surEspace ? 'espace' : 'mot');
if (surEspace) position += 1;
}
}
return { ligne, morceaux, debuts, coupes };
});
assert.deepEqual(fragments.slice(k).map(({ texte }) => texte), [], 'fragments de reste');
return recomposees;
}
test('graduation de 0 à 100 mm, repérée tous les 10 mm, mesurée sur les segments', () => {
for (const { papier, lu } of PRODUCTIONS) {
const { regle, reperes } = graduation(lu.pages[0]);
@ -356,11 +422,10 @@ describe('produirePlanche : la feuille de contrôle, page 1 (§ 11.3)', () => {
}
});
test("les lignes de contrôle se lisent dans la couche de texte : comptes, horodatage, version, papier", () => {
test("les lignes de contrôle se lisent dans la couche de texte, dans l'ordre et entières : comptes, horodatage, version, papier", () => {
for (const { papier, imposition, lu, modele: m } of PRODUCTIONS) {
const lus = new Set(lu.pages[0].textes.map(({ texte }) => texte));
const lignes = textesControle(m, { papier, imposition, planches: nombrePlanches(m.badges.length, papier) });
for (const ligne of lignes) assert.ok(lus.has(ligne), `${papier} : « ${ligne} » absente de ${[...lus].join(' | ')}`);
recomposer(fragmentsDeControle(lu.pages[0]), lignes);
}
const lusA4 = A4.pages[0].textes.map(({ texte }) => texte);
assert.ok(lusA4.includes(libelle('planche.controle.comptes', MODELE_A4.comptes)));
@ -368,6 +433,64 @@ describe('produirePlanche : la feuille de contrôle, page 1 (§ 11.3)', () => {
assert.ok(LETTRE.pages[0].textes.some(({ texte }) => texte === libelle('planche.pied.horodatageInconnu')));
});
test("une ligne plus large que la page se replie dans la largeur utile : aux espaces, puis dans le mot qui ne tient pas seul ; chaque morceau aussi plein qu'il se peut", () => {
const largeurUtile = PAPIERS.Lettre.largeur - 2 * MARGE;
const lignes = textesControle(MODELE_LETTRE, { papier: 'Lettre', imposition: 'pile', planches: nombrePlanches(3, 'Lettre') });
const recomposees = recomposer(fragmentsDeControle(LETTRE.pages[0]), lignes);
const evenement = recomposees.find(({ ligne }) => ligne === libelle('planche.controle.evenement', EVENEMENT_LONG));
assert.ok(evenement.coupes.includes('espace') && evenement.coupes.includes('mot'), JSON.stringify(evenement.coupes));
for (const { ligne, morceaux, debuts, coupes } of recomposees) {
if (ligne !== evenement.ligne) assert.equal(morceaux.length, 1, ligne);
const corps = morceaux[0].h / MM_PAR_PT;
const largeur = (texte) => metriques.largeur(texte, corps, false);
for (const morceau of morceaux) {
// La mesure de l'épreuve est celle du fichier : l'avance relue.
assert.ok(Math.abs(largeur(morceau.texte) - morceau.l) < 0.01, `« ${morceau.texte} » : ${largeur(morceau.texte)} contre ${morceau.l}`);
assert.ok(morceau.l <= largeurUtile + TOLERANCE, `« ${morceau.texte} » large de ${morceau.l} mm`);
}
// Une coupe ne tombe que là où la suite déborde : le premier mot (ou
// la première grappe) du morceau suivant, ajouté, passe la largeur.
coupes.forEach((coupe, i) => {
const suivant = morceaux[i + 1].texte;
const ajout = coupe === 'espace' ? ` ${suivant.split(' ')[0]}` : suivant.match(/\P{M}\p{M}*/u)[0];
assert.ok(largeur(morceaux[i].texte + ajout) > largeurUtile, `coupe ${coupe} trop tôt après « ${morceaux[i].texte} »`);
});
// Une coupe dans un mot ne tombe que dans un mot plus large à lui seul
// que la ligne, et ce mot ouvre le morceau qui porte son début : un mot
// qui tient seul passe entier à la ligne suivante.
coupes.forEach((coupe, i) => {
if (coupe !== 'mot') return;
const position = debuts[i] + morceaux[i].texte.length;
const debutMot = ligne.lastIndexOf(' ', position - 1) + 1;
const finMot = ligne.indexOf(' ', position);
const mot = ligne.slice(debutMot, finMot === -1 ? ligne.length : finMot);
assert.ok(largeur(mot) > largeurUtile, `« ${mot} », large de ${largeur(mot)} mm, coupé après « ${morceaux[i].texte} »`);
const porteur = debuts.findLastIndex((debut) => debut <= debutMot);
assert.equal(debuts[porteur], debutMot, `« ${mot} » commence dans « ${morceaux[porteur].texte} »`);
});
}
// Les données éprouvent le report d'un mot entier : le mot long, qui
// tient seul sur une ligne, ouvre un morceau alors que sa première
// grappe tenait encore au bout du morceau précédent.
const long = 'Ombrellebleuetières,';
const largeurLue = (texte) => metriques.largeur(texte, evenement.morceaux[0].h / MM_PAR_PT, false);
const avant = evenement.morceaux.findIndex(({ texte }) => texte.startsWith(long)) - 1;
assert.ok(avant >= 0, JSON.stringify(evenement.morceaux.map(({ texte }) => texte)));
assert.ok(largeurLue(long) <= largeurUtile && largeurLue(`${evenement.morceaux[avant].texte} ${long[0]}`) <= largeurUtile);
});
test('chaque texte de la feuille de contrôle, chiffres et filigrane compris, tient dans les marges ; chaque ligne part de la marge gauche', () => {
for (const { papier, lu } of PRODUCTIONS) {
const { largeur: W, hauteur: H } = PAPIERS[papier];
const [page] = lu.pages;
for (const { texte, x, y, l, h } of page.textes) {
const dedans = x >= MARGE - TOLERANCE && y >= MARGE - TOLERANCE && x + l <= W - MARGE + TOLERANCE && y + h <= H - MARGE + TOLERANCE;
assert.ok(dedans, `${papier} : « ${texte} » en ${JSON.stringify({ x, y, l, h })}`);
}
for (const { texte, x } of fragmentsDeControle(page)) assert.ok(Math.abs(x - MARGE) < TOLERANCE, `${papier} : « ${texte} » part de ${x} mm`);
}
});
test('BROUILLON sur la feuille de contrôle du brouillon : sa ligne et le filigrane, une fois, hors de la graduation', () => {
const page = A4.pages[0];
const marques = page.textes.filter(({ texte }) => texte === FILIGRANE);