[ADD] captures: badge control page and first sheet, rasterised from PDF
The walkthrough produces the retained plan's badge sheet through the
Badges section and rasterises the PDF under node with pdfjs at 96 dpi:
page 1 becomes the control page, page 2 the first sheet of four A6.
Manifest entries carry the PDF's sha256, so an unchanged PDF leaves the
images unchanged; two productions under the driven clock must hash the
same, and a missing canvas fails the step by name with nothing written.
No capture remains pending.
Checked: node 3787, browser 758, node-long 101 from the index alone.
--- FR ---
[ADD] captures : feuille de contrôle et planche, tramées du PDF
Le parcours produit la planche du retenu par la section Badges et trame
le PDF sous node par pdfjs à 96 ppp : la page 1 donne la feuille de
contrôle, la page 2 la première planche de quatre A6. Les entrées du
manifeste portent le sha256 du PDF : un PDF inchangé laisse les images
inchangées ; deux productions sous l'horloge pilotée ont même empreinte,
et un canevas absent fait échouer l'étape en le nommant, rien d'écrit.
Plus aucune capture n'est à venir.
Vérifié : node 3787, navigateur 758, longues 101 depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-10 14:36:21 -04:00
// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Épreuves sous node des commandes des captures qui touchent au PDF de la
// planche (§ 19.5, § 19.6) : tramerPdf lit chaque page demandée — texte,
// traits, taille à 96 ppp —, la trame en PNG sous le dossier d'attente quand
// on le lui demande, et refuse en nommant le canevas quand il manque ; la
// publication admet, à côté des captures du navigateur, une page de PDF à la
// taille de sa page. Chaque épreuve travaille dans un dossier d'épreuve
// (dossierEpreuve), effacé à la fin.
//
// Les PDF des épreuves se construisent ici par pdf-lib, une fois par
// fichier : le premier, une page A4 qui porte un trait de 100 mm et une ligne
// de texte, une page A6 qui porte un nom inventé, dans la police embarquée ;
// le second, deux pages vides aux dimensions de deux cadres d'écran. Toute
// image s'écrit par tramerPdf : l'épreuve n'écrit rien elle-même.
import assert from 'node:assert/strict' ;
import { createHash } from 'node:crypto' ;
import { readFile } from 'node:fs/promises' ;
[ADD] captures: headless mode, preview bar and machine identity checked
Three capture rules held only by convention. The browser project must
run headless, MODE_AFFICHAGE names that mode, and a screenshot refuses
a page whose user agent is not headless. Captures 22 and 23 assert the
preview bar has scrolled out of the frame before they are taken.
Publishing refuses a text carrying the machine's host or user name, or
an instant outside the driven clock; the checks are shown red on
invented names, and the default identity is read from node:os.
Checked: 3883 node and 767 browser tests from the index alone.
--- FR ---
[ADD] captures : mode sans tête, barre de l'aperçu, identité de machine
Trois règles des captures ne tenaient que par convention. Le projet
navigateur tourne sans tête, MODE_AFFICHAGE nomme ce mode, et une
capture refuse une page dont l'agent n'est pas sans tête. Les captures
22 et 23 vérifient que la barre de l'aperçu est sortie du cadre. La
publication refuse un texte qui porte le nom d'hôte ou d'utilisateur de
la machine, ou un instant hors de l'horloge pilotée ; montré rouge sur
des noms inventés, l'identité par défaut lue de node:os.
Vérifié : 3883 node et 767 navigateur depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-10 18:54:15 -04:00
import { hostname , userInfo } from 'node:os' ;
[ADD] captures: badge control page and first sheet, rasterised from PDF
The walkthrough produces the retained plan's badge sheet through the
Badges section and rasterises the PDF under node with pdfjs at 96 dpi:
page 1 becomes the control page, page 2 the first sheet of four A6.
Manifest entries carry the PDF's sha256, so an unchanged PDF leaves the
images unchanged; two productions under the driven clock must hash the
same, and a missing canvas fails the step by name with nothing written.
No capture remains pending.
Checked: node 3787, browser 758, node-long 101 from the index alone.
--- FR ---
[ADD] captures : feuille de contrôle et planche, tramées du PDF
Le parcours produit la planche du retenu par la section Badges et trame
le PDF sous node par pdfjs à 96 ppp : la page 1 donne la feuille de
contrôle, la page 2 la première planche de quatre A6. Les entrées du
manifeste portent le sha256 du PDF : un PDF inchangé laisse les images
inchangées ; deux productions sous l'horloge pilotée ont même empreinte,
et un canevas absent fait échouer l'étape en le nommant, rien d'écrit.
Plus aucune capture n'est à venir.
Vérifié : node 3787, navigateur 758, longues 101 depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-10 14:36:21 -04:00
import { join } from 'node:path' ;
import fontkit from '@pdf-lib/fontkit' ;
import { PDFDocument , grayscale } from 'pdf-lib' ;
import { describe , test } from '../lanceur.js' ;
import { lirePolices } from '../polices.js' ;
[ADD] captures: headless mode, preview bar and machine identity checked
Three capture rules held only by convention. The browser project must
run headless, MODE_AFFICHAGE names that mode, and a screenshot refuses
a page whose user agent is not headless. Captures 22 and 23 assert the
preview bar has scrolled out of the frame before they are taken.
Publishing refuses a text carrying the machine's host or user name, or
an instant outside the driven clock; the checks are shown red on
invented names, and the default identity is read from node:os.
Checked: 3883 node and 767 browser tests from the index alone.
--- FR ---
[ADD] captures : mode sans tête, barre de l'aperçu, identité de machine
Trois règles des captures ne tenaient que par convention. Le projet
navigateur tourne sans tête, MODE_AFFICHAGE nomme ce mode, et une
capture refuse une page dont l'agent n'est pas sans tête. Les captures
22 et 23 vérifient que la barre de l'aperçu est sortie du cadre. La
publication refuse un texte qui porte le nom d'hôte ou d'utilisateur de
la machine, ou un instant hors de l'horloge pilotée ; montré rouge sur
des noms inventés, l'identité par défaut lue de node:os.
Vérifié : 3883 node et 767 navigateur depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-10 18:54:15 -04:00
import { creerFormat } from '../../src/application/format.js' ;
import { creerHorlogePilotee , creerHorlogeSysteme } from '../../src/application/horloge.js' ;
import { libelle } from '../../src/application/libelles.js' ;
import { dater } from '../../src/application/libelles/aides.js' ;
import { PRENOMS } from '../../src/demo/noms.js' ;
[ADD] captures: badge control page and first sheet, rasterised from PDF
The walkthrough produces the retained plan's badge sheet through the
Badges section and rasterises the PDF under node with pdfjs at 96 dpi:
page 1 becomes the control page, page 2 the first sheet of four A6.
Manifest entries carry the PDF's sha256, so an unchanged PDF leaves the
images unchanged; two productions under the driven clock must hash the
same, and a missing canvas fails the step by name with nothing written.
No capture remains pending.
Checked: node 3787, browser 758, node-long 101 from the index alone.
--- FR ---
[ADD] captures : feuille de contrôle et planche, tramées du PDF
Le parcours produit la planche du retenu par la section Badges et trame
le PDF sous node par pdfjs à 96 ppp : la page 1 donne la feuille de
contrôle, la page 2 la première planche de quatre A6. Les entrées du
manifeste portent le sha256 du PDF : un PDF inchangé laisse les images
inchangées ; deux productions sous l'horloge pilotée ont même empreinte,
et un canevas absent fait échouer l'étape en le nommant, rien d'écrit.
Plus aucune capture n'est à venir.
Vérifié : node 3787, navigateur 758, longues 101 depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-10 14:36:21 -04:00
import { VERSION } from '../../src/version.genere.js' ;
import { CHEMIN _MANIFESTE , DOSSIER _CAPTURES , lireEntetePng } from '../../scripts/documentation/manifeste.js' ;
import {
COMMANDES ,
MODE _TRAMAGE ,
PPP _TRAMAGE ,
canevasDisponible ,
chargerCanevas ,
deposerCapture ,
dossierEpreuve ,
effacerDossierEpreuve ,
empreintesDossier ,
[ADD] captures: headless mode, preview bar and machine identity checked
Three capture rules held only by convention. The browser project must
run headless, MODE_AFFICHAGE names that mode, and a screenshot refuses
a page whose user agent is not headless. Captures 22 and 23 assert the
preview bar has scrolled out of the frame before they are taken.
Publishing refuses a text carrying the machine's host or user name, or
an instant outside the driven clock; the checks are shown red on
invented names, and the default identity is read from node:os.
Checked: 3883 node and 767 browser tests from the index alone.
--- FR ---
[ADD] captures : mode sans tête, barre de l'aperçu, identité de machine
Trois règles des captures ne tenaient que par convention. Le projet
navigateur tourne sans tête, MODE_AFFICHAGE nomme ce mode, et une
capture refuse une page dont l'agent n'est pas sans tête. Les captures
22 et 23 vérifient que la barre de l'aperçu est sortie du cadre. La
publication refuse un texte qui porte le nom d'hôte ou d'utilisateur de
la machine, ou un instant hors de l'horloge pilotée ; montré rouge sur
des noms inventés, l'identité par défaut lue de node:os.
Vérifié : 3883 node et 767 navigateur depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-10 18:54:15 -04:00
fautesDIdentite ,
[ADD] captures: badge control page and first sheet, rasterised from PDF
The walkthrough produces the retained plan's badge sheet through the
Badges section and rasterises the PDF under node with pdfjs at 96 dpi:
page 1 becomes the control page, page 2 the first sheet of four A6.
Manifest entries carry the PDF's sha256, so an unchanged PDF leaves the
images unchanged; two productions under the driven clock must hash the
same, and a missing canvas fails the step by name with nothing written.
No capture remains pending.
Checked: node 3787, browser 758, node-long 101 from the index alone.
--- FR ---
[ADD] captures : feuille de contrôle et planche, tramées du PDF
Le parcours produit la planche du retenu par la section Badges et trame
le PDF sous node par pdfjs à 96 ppp : la page 1 donne la feuille de
contrôle, la page 2 la première planche de quatre A6. Les entrées du
manifeste portent le sha256 du PDF : un PDF inchangé laisse les images
inchangées ; deux productions sous l'horloge pilotée ont même empreinte,
et un canevas absent fait échouer l'étape en le nommant, rien d'écrit.
Plus aucune capture n'est à venir.
Vérifié : node 3787, navigateur 758, longues 101 depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-10 14:36:21 -04:00
lireManifeste ,
[ADD] captures: headless mode, preview bar and machine identity checked
Three capture rules held only by convention. The browser project must
run headless, MODE_AFFICHAGE names that mode, and a screenshot refuses
a page whose user agent is not headless. Captures 22 and 23 assert the
preview bar has scrolled out of the frame before they are taken.
Publishing refuses a text carrying the machine's host or user name, or
an instant outside the driven clock; the checks are shown red on
invented names, and the default identity is read from node:os.
Checked: 3883 node and 767 browser tests from the index alone.
--- FR ---
[ADD] captures : mode sans tête, barre de l'aperçu, identité de machine
Trois règles des captures ne tenaient que par convention. Le projet
navigateur tourne sans tête, MODE_AFFICHAGE nomme ce mode, et une
capture refuse une page dont l'agent n'est pas sans tête. Les captures
22 et 23 vérifient que la barre de l'aperçu est sortie du cadre. La
publication refuse un texte qui porte le nom d'hôte ou d'utilisateur de
la machine, ou un instant hors de l'horloge pilotée ; montré rouge sur
des noms inventés, l'identité par défaut lue de node:os.
Vérifié : 3883 node et 767 navigateur depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-10 18:54:15 -04:00
machineCourante ,
[ADD] captures: badge control page and first sheet, rasterised from PDF
The walkthrough produces the retained plan's badge sheet through the
Badges section and rasterises the PDF under node with pdfjs at 96 dpi:
page 1 becomes the control page, page 2 the first sheet of four A6.
Manifest entries carry the PDF's sha256, so an unchanged PDF leaves the
images unchanged; two productions under the driven clock must hash the
same, and a missing canvas fails the step by name with nothing written.
No capture remains pending.
Checked: node 3787, browser 758, node-long 101 from the index alone.
--- FR ---
[ADD] captures : feuille de contrôle et planche, tramées du PDF
Le parcours produit la planche du retenu par la section Badges et trame
le PDF sous node par pdfjs à 96 ppp : la page 1 donne la feuille de
contrôle, la page 2 la première planche de quatre A6. Les entrées du
manifeste portent le sha256 du PDF : un PDF inchangé laisse les images
inchangées ; deux productions sous l'horloge pilotée ont même empreinte,
et un canevas absent fait échouer l'étape en le nommant, rien d'écrit.
Plus aucune capture n'est à venir.
Vérifié : node 3787, navigateur 758, longues 101 depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-10 14:36:21 -04:00
publierCaptures ,
tramer ,
tramerPdf ,
} from './commandes_documentation.js' ;
const sha256 = ( octets ) => createHash ( 'sha256' ) . update ( octets ) . digest ( 'hex' ) ;
const enBase64 = ( octets ) => Buffer . from ( octets ) . toString ( 'base64' ) ;
// Le point typographique et le pixel CSS, en millimètres.
const PT _PAR _MM = 72 / 25.4 ;
const PX _PAR _MM = 96 / 25.4 ;
// Les deux pages du PDF des épreuves, en mm, et ce que chacune porte.
const A4 = Object . freeze ( { largeur : 210 , hauteur : 297 } ) ;
const A6 = Object . freeze ( { largeur : 105 , hauteur : 148 } ) ;
const LIGNE _A4 = 'Feuille témoin' ;
const NOM _A6 = 'Ondine Lacasse' ;
// Le trait de la page A4 : 100 mm, à 20 mm du bord gauche et 30 mm du haut.
const TRAIT = Object . freeze ( { x : 20 , y : 30 , longueur : 100 } ) ;
// La racine neutre des textes relevés : inventée, elle n'existe nulle part
// ailleurs dans le dépôt.
const RACINE _NEUTRE = 'C:\\Seance\\Tables' ;
// Le dossier d'attente d'une racine, et le chemin d'une image déposée.
const ATTENTE = ` ${ DOSSIER _CAPTURES } /.attente ` ;
const deposee = ( racine , fichier ) => join ( racine , ATTENTE , ` ${ fichier } .png ` ) ;
// La taille attendue d'une page à 96 ppp : ses millimètres en pixels CSS,
// arrondis à l'entier.
const enPixels = ( { largeur , hauteur } ) => ( { largeur : Math . round ( largeur * PX _PAR _MM ) , hauteur : Math . round ( hauteur * PX _PAR _MM ) } ) ;
async function construirePdf ( ) {
const document = await PDFDocument . create ( { updateMetadata : false } ) ;
document . registerFontkit ( fontkit ) ;
const police = await document . embedFont ( lirePolices ( ) . regulier , { subset : true } ) ;
const a4 = document . addPage ( [ A4 . largeur * PT _PAR _MM , A4 . hauteur * PT _PAR _MM ] ) ;
const yTrait = ( A4 . hauteur - TRAIT . y ) * PT _PAR _MM ;
a4 . drawLine ( {
start : { x : TRAIT . x * PT _PAR _MM , y : yTrait } ,
end : { x : ( TRAIT . x + TRAIT . longueur ) * PT _PAR _MM , y : yTrait } ,
thickness : 0.5 ,
color : grayscale ( 0 ) ,
} ) ;
a4 . drawText ( LIGNE _A4 , { x : TRAIT . x * PT _PAR _MM , y : yTrait - 40 , size : 14 , font : police , color : grayscale ( 0 ) } ) ;
const a6 = document . addPage ( [ A6 . largeur * PT _PAR _MM , A6 . hauteur * PT _PAR _MM ] ) ;
a6 . drawText ( NOM _A6 , { x : 30 , y : 200 , size : 18 , font : police , color : grayscale ( 0 ) } ) ;
return document . save ( ) ;
}
// Deux pages vides de la taille de deux cadres d'écran, en px CSS : le
// cadre des captures du navigateur, et un autre.
const ECRAN = Object . freeze ( { largeur : 1366 , hauteur : 700 } ) ;
const AUTRE _ECRAN = Object . freeze ( { largeur : 1280 , hauteur : 700 } ) ;
async function construireEcrans ( ) {
const document = await PDFDocument . create ( { updateMetadata : false } ) ;
for ( const { largeur , hauteur } of [ ECRAN , AUTRE _ECRAN ] ) document . addPage ( [ ( largeur * 72 ) / 96 , ( hauteur * 72 ) / 96 ] ) ;
return document . save ( ) ;
}
const OCTETS = await construirePdf ( ) ;
const B64 = enBase64 ( OCTETS ) ;
const PAGES = Object . freeze ( [
{ page : 1 , fichier : '24-badges-controle' } ,
{ page : 2 , fichier : '25-badges-planche' } ,
] ) ;
// Un chargeur de canevas qui échoue comme un paquet optionnel absent.
const canevasAbsent = ( ) => {
const erreur = new Error ( "Cannot find module '@napi-rs/canvas'" ) ;
erreur . code = 'MODULE_NOT_FOUND' ;
throw erreur ;
} ;
// Exécute faire(racine) dans un dossier d'épreuve neuf, effacé ensuite.
async function dansUnDossier ( faire ) {
const racine = await dossierEpreuve ( ) ;
try {
return await faire ( racine ) ;
} finally {
await effacerDossierEpreuve ( null , racine ) ;
}
}
// La raison d'un saut quand le canevas manque : le tramage ne s'éprouve que
// là où make captures peut tramer.
const SANS _CANEVAS = 'le canevas de pdfjs (@napi-rs/canvas) manque sur ce système : le tramage ne s’ y éprouve pas' ;
// Le nombre de pixels d'une image PNG plus sombres que le gris moyen.
async function pixelsSombres ( octets ) {
const { createCanvas , loadImage } = chargerCanevas ( ) ;
const image = await loadImage ( Buffer . from ( octets ) ) ;
const canevas = createCanvas ( image . width , image . height ) ;
const contexte = canevas . getContext ( '2d' ) ;
contexte . drawImage ( image , 0 , 0 ) ;
const { data } = contexte . getImageData ( 0 , 0 , image . width , image . height ) ;
let sombres = 0 ;
for ( let i = 0 ; i < data . length ; i += 4 ) if ( data [ i ] + data [ i + 1 ] + data [ i + 2 ] < 3 * 128 ) sombres += 1 ;
return sombres ;
}
describe ( 'tramerPdf : la lecture des pages d’ un PDF (§ 19.6)' , ( ) => {
test ( 'déclarée parmi les commandes, exportée sous son nom' , ( ) => {
assert . ok ( COMMANDES . includes ( 'tramerPdf' ) ) ;
assert . equal ( typeof tramerPdf , 'function' ) ;
} ) ;
test ( 'sans écrire : le texte, les traits et la taille à 96 ppp de chaque page demandée, l’ empreinte des octets ; aucun canevas chargé, rien d’ écrit' , async ( ) => {
await dansUnDossier ( async ( racine ) => {
const lu = await tramer ( { octetsB64 : B64 , pages : PAGES , racine , ecrire : false } , { chargerCanevas : canevasAbsent } ) ;
assert . equal ( lu . pdf , sha256 ( OCTETS ) ) ;
assert . equal ( lu . mode , MODE _TRAMAGE ) ;
assert . equal ( PPP _TRAMAGE , 96 ) ;
assert . deepEqual (
lu . pages . map ( ( { fichier , page , cadre , chemin } ) => ( { fichier , page , cadre , chemin } ) ) ,
[
{ fichier : '24-badges-controle' , page : 1 , cadre : enPixels ( A4 ) , chemin : null } ,
{ fichier : '25-badges-planche' , page : 2 , cadre : enPixels ( A6 ) , chemin : null } ,
] ,
) ;
assert . deepEqual ( enPixels ( A4 ) , { largeur : 794 , hauteur : 1123 } ) ;
assert . deepEqual ( lu . pages [ 0 ] . textes , [ LIGNE _A4 ] ) ;
assert . deepEqual ( lu . pages [ 1 ] . textes , [ NOM _A6 ] ) ;
// pdf-lib écrit ses coordonnées en points à six décimales au plus.
const pres = ( a , b ) => Math . abs ( a - b ) < 1e-3 ;
const horizontaux = lu . pages [ 0 ] . segments . filter ( ( { y1 , y2 } ) => pres ( y1 , y2 ) ) ;
assert . equal ( horizontaux . length , 1 ) ;
const [ trait ] = horizontaux ;
assert . ok ( pres ( Math . abs ( trait . x2 - trait . x1 ) , TRAIT . longueur ) , JSON . stringify ( trait ) ) ;
assert . ok ( pres ( trait . y1 , TRAIT . y ) , JSON . stringify ( trait ) ) ;
assert . deepEqual ( lu . pages [ 1 ] . segments , [ ] ) ;
assert . deepEqual ( await empreintesDossier ( null , { racine } ) , [ ] ) ;
} ) ;
} ) ;
test ( 'une seule page, la seconde : seule lue' , async ( ) => {
const lu = await tramer ( { octetsB64 : B64 , pages : [ PAGES [ 1 ] ] , ecrire : false } , { chargerCanevas : canevasAbsent } ) ;
assert . deepEqual ( lu . pages . map ( ( { fichier , textes } ) => [ fichier , textes ] ) , [ [ '25-badges-planche' , [ NOM _A6 ] ] ] ) ;
} ) ;
test ( 'refus nommés, rien d’ écrit : octets hors du base64, PDF illisible, aucune page, page hors du document, nom refusé, nom en double' , async ( ) => {
await dansUnDossier ( async ( racine ) => {
const essayer = ( args ) => tramer ( { racine , ecrire : true , ... args } , { chargerCanevas : canevasAbsent } ) ;
await assert . rejects ( essayer ( { octetsB64 : 42 , pages : PAGES } ) , /tramerPdf : octetsB64/ ) ;
await assert . rejects ( essayer ( { octetsB64 : 'pas du base64 !' , pages : PAGES } ) , /tramerPdf : octetsB64/ ) ;
await assert . rejects ( essayer ( { octetsB64 : enBase64 ( new TextEncoder ( ) . encode ( '%PDF-1.7 tronqué' ) ) , pages : PAGES } ) , /tramerPdf : PDF illisible/ ) ;
await assert . rejects ( essayer ( { octetsB64 : B64 , pages : [ ] } ) , /tramerPdf : aucune page demandée/ ) ;
await assert . rejects ( essayer ( { octetsB64 : B64 , pages : [ { page : 3 , fichier : '25-badges-planche' } ] } ) , /tramerPdf : page 3 hors du document de 2 pages/ ) ;
await assert . rejects ( essayer ( { octetsB64 : B64 , pages : [ { page : 0 , fichier : '25-badges-planche' } ] } ) , /tramerPdf : page 0 hors du document de 2 pages/ ) ;
await assert . rejects ( essayer ( { octetsB64 : B64 , pages : [ { page : 1 , fichier : '../hors' } ] } ) , /nom de capture refusé/ ) ;
await assert . rejects (
essayer ( { octetsB64 : B64 , pages : [ PAGES [ 0 ] , { page : 2 , fichier : PAGES [ 0 ] . fichier } ] } ) ,
/tramerPdf : « 24-badges-controle » demandée deux fois/ ,
) ;
assert . deepEqual ( await empreintesDossier ( null , { racine } ) , [ ] ) ;
} ) ;
} ) ;
} ) ;
describe ( 'tramerPdf : le tramage, sous make captures (§ 19.6, décision 2)' , ( ) => {
test ( 'canevas absent : le refus nomme le paquet, sa cause et make captures ; rien d’ écrit' , async ( ) => {
await dansUnDossier ( async ( racine ) => {
await assert . rejects (
tramer ( { octetsB64 : B64 , pages : PAGES , racine , ecrire : true } , { chargerCanevas : canevasAbsent } ) ,
( erreur ) => {
assert . match ( erreur . message , /^tramerPdf : le canevas « @napi-rs\/canvas » manque/ ) ;
assert . match ( erreur . message , /make captures/ ) ;
assert . match ( erreur . message , /Cannot find module/ ) ;
return true ;
} ,
) ;
assert . deepEqual ( await empreintesDossier ( null , { racine } ) , [ ] ) ;
} ) ;
} ) ;
test ( 'chaque page demandée tramée en PNG sous le dossier d’ attente, à la taille de sa page à 96 ppp, son encre rendue' , async ( contexte ) => {
if ( ! canevasDisponible ( ) ) contexte . skip ( SANS _CANEVAS ) ;
await dansUnDossier ( async ( racine ) => {
const lu = await tramerPdf ( null , { octetsB64 : B64 , pages : PAGES , racine } ) ;
assert . equal ( lu . pdf , sha256 ( OCTETS ) ) ;
for ( const [ rang , format ] of [ A4 , A6 ] . entries ( ) ) {
const { fichier , chemin , cadre } = lu . pages [ rang ] ;
assert . equal ( chemin , deposee ( racine , fichier ) ) ;
const octets = new Uint8Array ( await readFile ( chemin ) ) ;
assert . deepEqual ( lireEntetePng ( octets ) , enPixels ( format ) , fichier ) ;
assert . deepEqual ( cadre , enPixels ( format ) , fichier ) ;
assert . ok ( ( await pixelsSombres ( octets ) ) > 0 , ` ${ fichier } : aucune encre tramée ` ) ;
}
const ecrits = ( await empreintesDossier ( null , { racine } ) ) . map ( ( [ chemin ] ) => chemin ) ;
assert . deepEqual ( ecrits , [ ` ${ ATTENTE } /24-badges-controle.png ` , ` ${ ATTENTE } /25-badges-planche.png ` ] ) ;
} ) ;
} ) ;
test ( 'les mêmes octets de PDF donnent les mêmes octets de PNG' , async ( contexte ) => {
if ( ! canevasDisponible ( ) ) contexte . skip ( SANS _CANEVAS ) ;
const tramees = [ ] ;
for ( let fois = 0 ; fois < 2 ; fois += 1 ) {
tramees . push (
await dansUnDossier ( async ( racine ) => {
const { pages } = await tramerPdf ( null , { octetsB64 : B64 , pages : PAGES , racine } ) ;
return Promise . all ( pages . map ( async ( { chemin } ) => sha256 ( await readFile ( chemin ) ) ) ) ;
} ) ,
) ;
}
assert . deepEqual ( tramees [ 1 ] , tramees [ 0 ] ) ;
assert . notEqual ( tramees [ 0 ] [ 0 ] , tramees [ 0 ] [ 1 ] ) ;
} ) ;
} ) ;
// Le contexte de commande que lit la publication : la version du navigateur,
// inconnue ici.
const CONTEXTE = Object . freeze ( { page : { context : ( ) => ( { browser : ( ) => null } ) } } ) ;
const ECRANS = await construireEcrans ( ) ;
[ADD] captures: headless mode, preview bar and machine identity checked
Three capture rules held only by convention. The browser project must
run headless, MODE_AFFICHAGE names that mode, and a screenshot refuses
a page whose user agent is not headless. Captures 22 and 23 assert the
preview bar has scrolled out of the frame before they are taken.
Publishing refuses a text carrying the machine's host or user name, or
an instant outside the driven clock; the checks are shown red on
invented names, and the default identity is read from node:os.
Checked: 3883 node and 767 browser tests from the index alone.
--- FR ---
[ADD] captures : mode sans tête, barre de l'aperçu, identité de machine
Trois règles des captures ne tenaient que par convention. Le projet
navigateur tourne sans tête, MODE_AFFICHAGE nomme ce mode, et une
capture refuse une page dont l'agent n'est pas sans tête. Les captures
22 et 23 vérifient que la barre de l'aperçu est sortie du cadre. La
publication refuse un texte qui porte le nom d'hôte ou d'utilisateur de
la machine, ou un instant hors de l'horloge pilotée ; montré rouge sur
des noms inventés, l'identité par défaut lue de node:os.
Vérifié : 3883 node et 767 navigateur depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-10 18:54:15 -04:00
// L'instant d'une capture : le départ de l'horloge pilotée, celle du
// montage des épreuves du navigateur.
const INSTANT _PILOTE = creerHorlogePilotee ( ) . horodatage ( ) ;
[ADD] captures: badge control page and first sheet, rasterised from PDF
The walkthrough produces the retained plan's badge sheet through the
Badges section and rasterises the PDF under node with pdfjs at 96 dpi:
page 1 becomes the control page, page 2 the first sheet of four A6.
Manifest entries carry the PDF's sha256, so an unchanged PDF leaves the
images unchanged; two productions under the driven clock must hash the
same, and a missing canvas fails the step by name with nothing written.
No capture remains pending.
Checked: node 3787, browser 758, node-long 101 from the index alone.
--- FR ---
[ADD] captures : feuille de contrôle et planche, tramées du PDF
Le parcours produit la planche du retenu par la section Badges et trame
le PDF sous node par pdfjs à 96 ppp : la page 1 donne la feuille de
contrôle, la page 2 la première planche de quatre A6. Les entrées du
manifeste portent le sha256 du PDF : un PDF inchangé laisse les images
inchangées ; deux productions sous l'horloge pilotée ont même empreinte,
et un canevas absent fait échouer l'étape en le nommant, rien d'écrit.
Plus aucune capture n'est à venir.
Vérifié : node 3787, navigateur 758, longues 101 depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-10 14:36:21 -04:00
// Dépose sous le dossier d'attente une capture de la source donnée : la page
// donnée, tramée par tramerPdf, en est l'image, et sa taille le cadre. Une
// page de PDF se tire du PDF des épreuves, une capture du navigateur des
[ADD] captures: headless mode, preview bar and machine identity checked
Three capture rules held only by convention. The browser project must
run headless, MODE_AFFICHAGE names that mode, and a screenshot refuses
a page whose user agent is not headless. Captures 22 and 23 assert the
preview bar has scrolled out of the frame before they are taken.
Publishing refuses a text carrying the machine's host or user name, or
an instant outside the driven clock; the checks are shown red on
invented names, and the default identity is read from node:os.
Checked: 3883 node and 767 browser tests from the index alone.
--- FR ---
[ADD] captures : mode sans tête, barre de l'aperçu, identité de machine
Trois règles des captures ne tenaient que par convention. Le projet
navigateur tourne sans tête, MODE_AFFICHAGE nomme ce mode, et une
capture refuse une page dont l'agent n'est pas sans tête. Les captures
22 et 23 vérifient que la barre de l'aperçu est sortie du cadre. La
publication refuse un texte qui porte le nom d'hôte ou d'utilisateur de
la machine, ou un instant hors de l'horloge pilotée ; montré rouge sur
des noms inventés, l'identité par défaut lue de node:os.
Vérifié : 3883 node et 767 navigateur depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-10 18:54:15 -04:00
// pages d'écran. remplacer remplace des champs de l'entrée.
async function deposer ( racine , { fichier , etape , source , page } , remplacer = { } ) {
[ADD] captures: badge control page and first sheet, rasterised from PDF
The walkthrough produces the retained plan's badge sheet through the
Badges section and rasterises the PDF under node with pdfjs at 96 dpi:
page 1 becomes the control page, page 2 the first sheet of four A6.
Manifest entries carry the PDF's sha256, so an unchanged PDF leaves the
images unchanged; two productions under the driven clock must hash the
same, and a missing canvas fails the step by name with nothing written.
No capture remains pending.
Checked: node 3787, browser 758, node-long 101 from the index alone.
--- FR ---
[ADD] captures : feuille de contrôle et planche, tramées du PDF
Le parcours produit la planche du retenu par la section Badges et trame
le PDF sous node par pdfjs à 96 ppp : la page 1 donne la feuille de
contrôle, la page 2 la première planche de quatre A6. Les entrées du
manifeste portent le sha256 du PDF : un PDF inchangé laisse les images
inchangées ; deux productions sous l'horloge pilotée ont même empreinte,
et un canevas absent fait échouer l'étape en le nommant, rien d'écrit.
Plus aucune capture n'est à venir.
Vérifié : node 3787, navigateur 758, longues 101 depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-10 14:36:21 -04:00
const octets = source === 'pdf' ? OCTETS : ECRANS ;
const { pdf , mode , pages } = await tramerPdf ( null , { octetsB64 : enBase64 ( octets ) , pages : [ { page , fichier } ] , racine } ) ;
const [ { cadre , chemin } ] = pages ;
const entree = {
fichier ,
etape ,
source ,
versionReelle : VERSION . affichee ,
graine : null ,
cadre ,
facteurEchelle : 1 ,
mode : source === 'pdf' ? mode : 'chromium-headless-shell' ,
theme : 'clair' ,
[ADD] captures: headless mode, preview bar and machine identity checked
Three capture rules held only by convention. The browser project must
run headless, MODE_AFFICHAGE names that mode, and a screenshot refuses
a page whose user agent is not headless. Captures 22 and 23 assert the
preview bar has scrolled out of the frame before they are taken.
Publishing refuses a text carrying the machine's host or user name, or
an instant outside the driven clock; the checks are shown red on
invented names, and the default identity is read from node:os.
Checked: 3883 node and 767 browser tests from the index alone.
--- FR ---
[ADD] captures : mode sans tête, barre de l'aperçu, identité de machine
Trois règles des captures ne tenaient que par convention. Le projet
navigateur tourne sans tête, MODE_AFFICHAGE nomme ce mode, et une
capture refuse une page dont l'agent n'est pas sans tête. Les captures
22 et 23 vérifient que la barre de l'aperçu est sortie du cadre. La
publication refuse un texte qui porte le nom d'hôte ou d'utilisateur de
la machine, ou un instant hors de l'horloge pilotée ; montré rouge sur
des noms inventés, l'identité par défaut lue de node:os.
Vérifié : 3883 node et 767 navigateur depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-10 18:54:15 -04:00
instant : INSTANT _PILOTE ,
[ADD] captures: badge control page and first sheet, rasterised from PDF
The walkthrough produces the retained plan's badge sheet through the
Badges section and rasterises the PDF under node with pdfjs at 96 dpi:
page 1 becomes the control page, page 2 the first sheet of four A6.
Manifest entries carry the PDF's sha256, so an unchanged PDF leaves the
images unchanged; two productions under the driven clock must hash the
same, and a missing canvas fails the step by name with nothing written.
No capture remains pending.
Checked: node 3787, browser 758, node-long 101 from the index alone.
--- FR ---
[ADD] captures : feuille de contrôle et planche, tramées du PDF
Le parcours produit la planche du retenu par la section Badges et trame
le PDF sous node par pdfjs à 96 ppp : la page 1 donne la feuille de
contrôle, la page 2 la première planche de quatre A6. Les entrées du
manifeste portent le sha256 du PDF : un PDF inchangé laisse les images
inchangées ; deux productions sous l'horloge pilotée ont même empreinte,
et un canevas absent fait échouer l'étape en le nommant, rien d'écrit.
Plus aucune capture n'est à venir.
Vérifié : node 3787, navigateur 758, longues 101 depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-10 14:36:21 -04:00
texte : [ fichier ] ,
styles : { clair : [ ] , sombre : [ ] } ,
etat : null ,
pdf : source === 'pdf' ? pdf : null ,
[ADD] captures: headless mode, preview bar and machine identity checked
Three capture rules held only by convention. The browser project must
run headless, MODE_AFFICHAGE names that mode, and a screenshot refuses
a page whose user agent is not headless. Captures 22 and 23 assert the
preview bar has scrolled out of the frame before they are taken.
Publishing refuses a text carrying the machine's host or user name, or
an instant outside the driven clock; the checks are shown red on
invented names, and the default identity is read from node:os.
Checked: 3883 node and 767 browser tests from the index alone.
--- FR ---
[ADD] captures : mode sans tête, barre de l'aperçu, identité de machine
Trois règles des captures ne tenaient que par convention. Le projet
navigateur tourne sans tête, MODE_AFFICHAGE nomme ce mode, et une
capture refuse une page dont l'agent n'est pas sans tête. Les captures
22 et 23 vérifient que la barre de l'aperçu est sortie du cadre. La
publication refuse un texte qui porte le nom d'hôte ou d'utilisateur de
la machine, ou un instant hors de l'horloge pilotée ; montré rouge sur
des noms inventés, l'identité par défaut lue de node:os.
Vérifié : 3883 node et 767 navigateur depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-10 18:54:15 -04:00
... remplacer ,
[ADD] captures: badge control page and first sheet, rasterised from PDF
The walkthrough produces the retained plan's badge sheet through the
Badges section and rasterises the PDF under node with pdfjs at 96 dpi:
page 1 becomes the control page, page 2 the first sheet of four A6.
Manifest entries carry the PDF's sha256, so an unchanged PDF leaves the
images unchanged; two productions under the driven clock must hash the
same, and a missing canvas fails the step by name with nothing written.
No capture remains pending.
Checked: node 3787, browser 758, node-long 101 from the index alone.
--- FR ---
[ADD] captures : feuille de contrôle et planche, tramées du PDF
Le parcours produit la planche du retenu par la section Badges et trame
le PDF sous node par pdfjs à 96 ppp : la page 1 donne la feuille de
contrôle, la page 2 la première planche de quatre A6. Les entrées du
manifeste portent le sha256 du PDF : un PDF inchangé laisse les images
inchangées ; deux productions sous l'horloge pilotée ont même empreinte,
et un canevas absent fait échouer l'étape en le nommant, rien d'écrit.
Plus aucune capture n'est à venir.
Vérifié : node 3787, navigateur 758, longues 101 depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-10 14:36:21 -04:00
} ;
return deposerCapture ( null , { fichier , entree , chemin , racine } ) ;
}
// Les images s'y écrivent par tramerPdf : sans canevas, ces épreuves ne
// s'y jouent pas.
describe ( 'publierCaptures : une page de PDF à côté des captures du navigateur (§ 19.5, § 19.6)' , ( ) => {
test ( 'une capture du navigateur et une page de PDF de cadres différents se publient ensemble ; le pilote porte le cadre du navigateur' , async ( contexte ) => {
if ( ! canevasDisponible ( ) ) contexte . skip ( SANS _CANEVAS ) ;
await dansUnDossier ( async ( racine ) => {
await deposer ( racine , { fichier : '01-premier-ecran' , etape : '1' , source : 'navigateur' , page : 1 } ) ;
await deposer ( racine , { fichier : '24-badges-controle' , etape : '12' , source : 'pdf' , page : 1 } ) ;
const bilan = await publierCaptures ( CONTEXTE , {
declarees : [ '01-premier-ecran' , '24-badges-controle' ] ,
racine ,
racineNeutre : RACINE _NEUTRE ,
} ) ;
assert . deepEqual ( bilan . ecrites , [ '01-premier-ecran' , '24-badges-controle' ] ) ;
const manifeste = await lireManifeste ( null , { racine } ) ;
assert . deepEqual ( manifeste . pilote . cadre , ECRAN ) ;
assert . deepEqual (
manifeste . captures . map ( ( { fichier , source , cadre , pdf } ) => ( { fichier , source , cadre , pdf } ) ) ,
[
{ fichier : '01-premier-ecran' , source : 'navigateur' , cadre : ECRAN , pdf : null } ,
{ fichier : '24-badges-controle' , source : 'pdf' , cadre : enPixels ( A4 ) , pdf : sha256 ( OCTETS ) } ,
] ,
) ;
const ecrits = ( await empreintesDossier ( null , { racine } ) ) . map ( ( [ chemin ] ) => chemin ) ;
assert . deepEqual ( ecrits , [ ` ${ DOSSIER _CAPTURES } /01-premier-ecran.png ` , ` ${ DOSSIER _CAPTURES } /24-badges-controle.png ` , CHEMIN _MANIFESTE ] ) ;
} ) ;
} ) ;
test ( 'deux captures du navigateur de cadres différents refusent toujours la publication, rien de publié' , async ( contexte ) => {
if ( ! canevasDisponible ( ) ) contexte . skip ( SANS _CANEVAS ) ;
await dansUnDossier ( async ( racine ) => {
await deposer ( racine , { fichier : '01-premier-ecran' , etape : '1' , source : 'navigateur' , page : 1 } ) ;
await deposer ( racine , { fichier : '02-import-apercu' , etape : '2' , source : 'navigateur' , page : 2 } ) ;
await assert . rejects (
publierCaptures ( CONTEXTE , { declarees : [ '01-premier-ecran' , '02-import-apercu' ] , racine , racineNeutre : RACINE _NEUTRE } ) ,
/publication : 02-import-apercu n’ a pas le cadre ni le facteur d’ échelle de 01-premier-ecran/ ,
) ;
assert . deepEqual ( await empreintesDossier ( null , { racine } ) , [ ] ) ;
} ) ;
} ) ;
test ( 'une page de PDF seule, sans capture du navigateur, ne donne aucun pilote : refusée, rien de publié' , async ( contexte ) => {
if ( ! canevasDisponible ( ) ) contexte . skip ( SANS _CANEVAS ) ;
await dansUnDossier ( async ( racine ) => {
await deposer ( racine , { fichier : '24-badges-controle' , etape : '12' , source : 'pdf' , page : 1 } ) ;
await assert . rejects (
publierCaptures ( CONTEXTE , { declarees : [ '24-badges-controle' ] , racine , racineNeutre : RACINE _NEUTRE } ) ,
/publication : aucune capture du navigateur/ ,
) ;
assert . deepEqual ( await empreintesDossier ( null , { racine } ) , [ ] ) ;
} ) ;
} ) ;
} ) ;
[ADD] captures: headless mode, preview bar and machine identity checked
Three capture rules held only by convention. The browser project must
run headless, MODE_AFFICHAGE names that mode, and a screenshot refuses
a page whose user agent is not headless. Captures 22 and 23 assert the
preview bar has scrolled out of the frame before they are taken.
Publishing refuses a text carrying the machine's host or user name, or
an instant outside the driven clock; the checks are shown red on
invented names, and the default identity is read from node:os.
Checked: 3883 node and 767 browser tests from the index alone.
--- FR ---
[ADD] captures : mode sans tête, barre de l'aperçu, identité de machine
Trois règles des captures ne tenaient que par convention. Le projet
navigateur tourne sans tête, MODE_AFFICHAGE nomme ce mode, et une
capture refuse une page dont l'agent n'est pas sans tête. Les captures
22 et 23 vérifient que la barre de l'aperçu est sortie du cadre. La
publication refuse un texte qui porte le nom d'hôte ou d'utilisateur de
la machine, ou un instant hors de l'horloge pilotée ; montré rouge sur
des noms inventés, l'identité par défaut lue de node:os.
Vérifié : 3883 node et 767 navigateur depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-10 18:54:15 -04:00
// Une machine inventée : ni son hôte, ni son domaine, ni son utilisateur
// n'existent ailleurs dans le dépôt. Le nom d'hôte est pleinement qualifié ;
// un texte peut ne porter que son premier libellé.
const MACHINE = Object . freeze ( { hote : 'cormoran-ecluse9.tourbiere.invalid' , utilisateur : 'rubanier' } ) ;
describe ( 'publierCaptures : ni l’ hôte, ni l’ utilisateur, ni un instant étranger (§ 19.5, contrainte 27)' , ( ) => {
test ( 'machineCourante lit le nom d’ hôte et l’ utilisateur de node:os, deux chaînes' , ( ) => {
const { hote , utilisateur } = machineCourante ( ) ;
assert . equal ( typeof hote , 'string' ) ;
assert . ok ( utilisateur === null || typeof utilisateur === 'string' , String ( utilisateur ) ) ;
// Les valeurs sont celles du système, lues au moment de l'appel.
assert . equal ( hote , hostname ( ) ) ;
let attendu = null ;
try {
attendu = userInfo ( ) . username ;
} catch {
// Sans entrée dans la base des comptes, machineCourante rend null.
}
assert . equal ( utilisateur , attendu ) ;
} ) ;
test ( 'publierCaptures refuse un texte qui porte le nom d’ hôte ou l’ utilisateur de la machine, et un instant hors de l’ horloge pilotée' , async ( contexte ) => {
if ( ! canevasDisponible ( ) ) contexte . skip ( SANS _CANEVAS ) ;
const declarees = [ '01-premier-ecran' , '24-badges-controle' ] ;
// Dépose les deux captures, la première sous les champs remplacés, et
// rend la publication de la machine inventée.
const publier = ( racine , remplacer , pdf = { } ) =>
( async ( ) => {
await deposer ( racine , { fichier : '01-premier-ecran' , etape : '1' , source : 'navigateur' , page : 1 } , remplacer ) ;
await deposer ( racine , { fichier : '24-badges-controle' , etape : '12' , source : 'pdf' , page : 1 } , pdf ) ;
return publierCaptures ( CONTEXTE , { declarees , racine , racineNeutre : RACINE _NEUTRE } , { machine : MACHINE } ) ;
} ) ( ) ;
const refus = [
[ { texte : [ 'Poste CORMORAN-ECLUSE9.tourbiere.invalid' ] } , { } , /doc\/captures\/01-premier-ecran\.png : nom d’ hôte de la machine/ ] ,
[ { texte : [ 'Ouvert sur cormoran-ecluse9, salle 2' ] } , { } , /doc\/captures\/01-premier-ecran\.png : nom d’ hôte de la machine/ ] ,
[ { texte : [ 'Dossier de travail' , 'session rubanier' ] } , { } , /doc\/captures\/01-premier-ecran\.png : utilisateur de la machine/ ] ,
[ { styles : { clair : [ { jeton : 'rubanier' } ] , sombre : [ ] } } , { } , /doc\/captures\/01-premier-ecran\.png : utilisateur de la machine/ ] ,
[ { instant : creerHorlogeSysteme ( ) . horodatage ( ) } , { } , /doc\/captures\/01-premier-ecran\.png : instant « [^»]+ » hors de l’ horloge pilotée/ ] ,
[ { } , { instant : '2031-05-06T20:15:00-04:00' } , /doc\/captures\/24-badges-controle\.png : instant « 2031-05-06T20:15:00-04:00 » hors de l’ horloge pilotée/ ] ,
[ { instant : '2026-10-06T18:59:59-04:00' } , { } , /instant « 2026-10-06T18:59:59-04:00 » hors de l’ horloge pilotée/ ] ,
[ { instant : '2026-10-06T23:00:00+00:00' } , { } , /instant « 2026-10-06T23:00:00\+00:00 » hors de l’ horloge pilotée/ ] ,
[ { instant : null } , { } , /instant null hors de l’ horloge pilotée/ ] ,
] ;
for ( const [ remplacer , pdf , motif ] of refus ) {
await dansUnDossier ( async ( racine ) => {
await assert . rejects ( publier ( racine , remplacer , pdf ) , motif , JSON . stringify ( remplacer ) ) ;
assert . deepEqual ( await empreintesDossier ( null , { racine } ) , [ ] , JSON . stringify ( remplacer ) ) ;
} ) ;
}
// Un mot qui ne fait que contenir l'un des noms passe, comme un instant
// que l'horloge pilotée atteint ; la publication se fait.
await dansUnDossier ( async ( racine ) => {
const horloge = creerHorlogePilotee ( ) ;
horloge . avancer ( 3 * 60 * 1000 ) ;
const bilan = await publier ( racine , { texte : [ 'rubaniers' , 'cormoran-ecluse90' ] , instant : horloge . horodatage ( ) } ) ;
assert . deepEqual ( bilan . ecrites , declarees ) ;
} ) ;
} ) ;
test ( 'fautesDIdentite ne cherche pas un nom que les sources du scénario portent déjà comme mot entier' , ( ) => {
const manifeste = {
pilote : null ,
captures : [ { fichier : '01-premier-ecran' , instant : INSTANT _PILOTE , texte : [ 'Ombrelin Bruvelin, table 3' ] } ] ,
reportees : [ ] ,
} ;
const machine = { hote : 'ombrelin.local' , utilisateur : 'ombrelin' } ;
assert . deepEqual ( fautesDIdentite ( manifeste , machine , { sources : [ ] } ) , [
{ chemin : 'doc/captures/01-premier-ecran.png' , faute : 'nom d’ hôte de la machine' } ,
{ chemin : 'doc/captures/01-premier-ecran.png' , faute : 'utilisateur de la machine' } ,
] ) ;
assert . deepEqual ( fautesDIdentite ( manifeste , machine , { sources : [ 'Ombrelin;Bruvelin;3' ] } ) , [ ] ) ;
// Un nom que la source ne porte que dans un mot plus long reste cherché.
assert . deepEqual ( fautesDIdentite ( manifeste , machine , { sources : [ 'Ombrelines' ] } ) . length , 2 ) ;
// L'instant de la capture se contrôle à part : une seule faute le nomme.
const etranger = { ... manifeste , captures : [ { ... manifeste . captures [ 0 ] , instant : '2031-05-06T20:15:00-04:00' } ] } ;
assert . deepEqual (
fautesDIdentite ( etranger , machine , { sources : [ 'Ombrelin' ] } ) . map ( ( { faute } ) => faute . split ( ' hors ' ) [ 0 ] ) ,
[ 'instant « 2031-05-06T20:15:00-04:00 »' ] ,
) ;
} ) ;
test ( 'une machine nommée comme un prénom de la démonstration publie ; un nom absent des sources du scénario reste refusé' , async ( contexte ) => {
if ( ! canevasDisponible ( ) ) contexte . skip ( SANS _CANEVAS ) ;
const declarees = [ '01-premier-ecran' , '24-badges-controle' ] ;
// Le prénom vient de la liste de la démonstration, que le scénario
// affiche : l'épreuve n'en écrit aucun.
const prenom = PRENOMS [ 0 ] ;
const texte = [ ` ${ prenom } Bruvelin ` , 'session rubanier' ] ;
const publier = ( racine , machine ) =>
( async ( ) => {
await deposer ( racine , { fichier : '01-premier-ecran' , etape : '1' , source : 'navigateur' , page : 1 } , { texte } ) ;
await deposer ( racine , { fichier : '24-badges-controle' , etape : '12' , source : 'pdf' , page : 1 } ) ;
return publierCaptures ( CONTEXTE , { declarees , racine , racineNeutre : RACINE _NEUTRE } , { machine } ) ;
} ) ( ) ;
for ( const machine of [
{ hote : ` ${ prenom . toLowerCase ( ) } -poste.invalid ` , utilisateur : prenom . toLowerCase ( ) } ,
{ hote : ` ${ prenom . toLowerCase ( ) } .local ` , utilisateur : null } ,
] ) {
await dansUnDossier ( async ( racine ) => {
const bilan = await publier ( racine , machine ) ;
assert . deepEqual ( bilan . ecrites , declarees , JSON . stringify ( machine ) ) ;
} ) ;
}
await dansUnDossier ( async ( racine ) => {
await assert . rejects (
publier ( racine , { hote : ` ${ prenom . toLowerCase ( ) } .local ` , utilisateur : 'rubanier' } ) ,
/doc\/captures\/01-premier-ecran\.png : utilisateur de la machine/ ,
) ;
assert . deepEqual ( await empreintesDossier ( null , { racine } ) , [ ] ) ;
} ) ;
} ) ;
test ( 'publierCaptures refuse un instant écrit dans un texte hors de l’ horloge pilotée, sous les formes de l’ application ; un instant qu’ elle atteint passe' , async ( contexte ) => {
if ( ! canevasDisponible ( ) ) contexte . skip ( SANS _CANEVAS ) ;
const declarees = [ '01-premier-ecran' , '24-badges-controle' ] ;
const publier = ( racine , remplacer , pdf = { } ) =>
( async ( ) => {
await deposer ( racine , { fichier : '01-premier-ecran' , etape : '1' , source : 'navigateur' , page : 1 } , remplacer ) ;
await deposer ( racine , { fichier : '24-badges-controle' , etape : '12' , source : 'pdf' , page : 1 } , pdf ) ;
return publierCaptures ( CONTEXTE , { declarees , racine , racineNeutre : RACINE _NEUTRE } , { machine : MACHINE } ) ;
} ) ( ) ;
const format = creerFormat ( libelle ) ;
const systeme = creerHorlogeSysteme ( ) . horodatage ( ) ;
const dansUnTexte = ( fichier ) => new RegExp ( ` doc/captures/ ${ fichier } \\ .png : instant « [^»]+ » dans un texte, hors de l’ horloge pilotée ` ) ;
const refus = [
[ { texte : [ 'Aperçu' , format . horodatage ( systeme ) ] } , { } , dansUnTexte ( '01-premier-ecran' ) ] ,
[ { } , { texte : [ libelle ( 'planche.pied.horodatage' , { horodatage : systeme } ) ] } , dansUnTexte ( '24-badges-controle' ) ] ,
[ { texte : [ ` Modifié ${ dater ( systeme , libelle ) } ` ] } , { } , dansUnTexte ( '01-premier-ecran' ) ] ,
[ { texte : [ ` journal ${ systeme } ` ] } , { } , dansUnTexte ( '01-premier-ecran' ) ] ,
[ { styles : { clair : [ { releve : '2026-10-10T18:03:00.000Z' } ] , sombre : [ ] } } , { } , dansUnTexte ( '01-premier-ecran' ) ] ,
[ { texte : [ format . horodatage ( '2026-10-06T18:59:00-04:00' ) ] } , { } , /instant « 6 octobre 2026, 18 h 59 » dans un texte/ ] ,
[ { } , { texte : [ libelle ( 'planche.pied.horodatage' , { horodatage : '2026-10-07T19:00:01-04:00' } ) ] } , /instant « 7 octobre 2026 à 19:00:01 » dans un texte/ ] ,
] ;
for ( const [ remplacer , pdf , motif ] of refus ) {
await dansUnDossier ( async ( racine ) => {
await assert . rejects ( publier ( racine , remplacer , pdf ) , motif , JSON . stringify ( [ remplacer , pdf ] ) ) ;
assert . deepEqual ( await empreintesDossier ( null , { racine } ) , [ ] , JSON . stringify ( remplacer ) ) ;
} ) ;
}
// Les mêmes formes d'un instant que l'horloge pilotée atteint, et une
// date seule, sans heure, passent.
await dansUnDossier ( async ( racine ) => {
const horloge = creerHorlogePilotee ( ) ;
horloge . avancer ( 2 * 60 * 1000 ) ;
const instant = horloge . horodatage ( ) ;
const bilan = await publier (
racine ,
{ texte : [ format . horodatage ( instant ) , ` Modifié ${ dater ( instant , libelle ) } ` , instant , 'Soirée du 17 mai 2031' ] } ,
{ texte : [ libelle ( 'planche.pied.horodatage' , { horodatage : instant } ) ] } ,
) ;
assert . deepEqual ( bilan . ecrites , declarees ) ;
} ) ;
} ) ;
} ) ;