[FIX] badges: injective sheet file names, a reissue never overwrites
Two propositions with the same timestamp, or the by-table order, gave the same file name, so one badge sheet silently replaced another. The name now carries -p<id> for a proposition and _table for the by-table order; the unmarked retained alphabetical name is unchanged. A reissue, or a name cut to fit under a long folder, takes the first free rank and never replaces a file; a folder that cannot be listed refuses first. A faulty proposition is refused with its own message. The control sheet names its source. Checked: node 3896, browser 767, long 105; captures 24 and 25 republished. --- FR --- [FIX] badges : noms de planche injectifs, une réédition n'écrase rien Deux propositions au même horodatage, ou l'ordre par table, donnaient le même nom : une planche en remplaçait une autre sans un mot. Le nom porte -p<id> pour une proposition et _table pour l'ordre par table ; le nom du retenu alphabétique ne change pas. Une réédition, ou un nom raccourci sous un dossier long, prend le premier rang libre et n'écrase rien ; un dossier qui ne se liste pas refuse d'abord. Une proposition fautive a son refus. La feuille de contrôle nomme sa source. Vérifié : node 3896, navigateur 767, long 105 ; captures 24 et 25 republiées. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
3db0bb138c
commit
7c9b708cc9
14 changed files with 699 additions and 112 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 47 KiB |
File diff suppressed because one or more lines are too long
62
spec.md
62
spec.md
|
|
@ -2327,8 +2327,10 @@ sur une page ; **elle ne se découpe pas**, ses pages de suite non plus. Elle
|
||||||
porte une **échelle graphique de 100 mm, repérée tous les 10 mm**, sur sa
|
porte une **échelle graphique de 100 mm, repérée tous les 10 mm**, sur sa
|
||||||
première page, les dimensions annoncées du badge, le format de papier,
|
première page, les dimensions annoncées du badge, le format de papier,
|
||||||
l'imposition retenue, le nombre de planches, le compte de badges produits et le
|
l'imposition retenue, le nombre de planches, le compte de badges produits et le
|
||||||
compte attendu, le nom et la date de l'événement, l'horodatage du placement, la
|
compte attendu, le nom et la date de l'événement, la source — le placement
|
||||||
version (§ 18.6), la consigne d'impression — recto seul, à 100 % —, les
|
retenu, ou la proposition imprimée par son numéro, que l'horodatage ne
|
||||||
|
distingue pas des autres propositions de sa génération —, l'horodatage du
|
||||||
|
placement, la version (§ 18.6), la consigne d'impression — recto seul, à 100 % —, les
|
||||||
caractères que la police n'a pas, par leur point de code (§ 11.5), et le
|
caractères que la police n'a pas, par leur point de code (§ 11.5), et le
|
||||||
filigrane BROUILLON s'il y a lieu, sur chacune de ses pages. L'opérateur la
|
filigrane BROUILLON s'il y a lieu, sur chacune de ses pages. L'opérateur la
|
||||||
mesure sous une règle avant d'engager la
|
mesure sous une règle avant d'engager la
|
||||||
|
|
@ -2498,20 +2500,51 @@ la fin de la pile finale, ce qui est la propriété qui compte.
|
||||||
Les deux impositions produisent la même pile finale par deux gestes différents ;
|
Les deux impositions produisent la même pile finale par deux gestes différents ;
|
||||||
prendre l'une pour l'autre oblige à retrier la totalité. Le logiciel inscrit
|
prendre l'une pour l'autre oblige à retrier la totalité. Le logiciel inscrit
|
||||||
l'imposition retenue **dans le nom du fichier produit**, avec le nom de
|
l'imposition retenue **dans le nom du fichier produit**, avec le nom de
|
||||||
l'événement, le format de papier et l'horodatage du placement, parce que c'est la
|
l'événement, le format de papier, la source, l'ordre et l'horodatage du
|
||||||
seule chose qui survit à l'envoi de la planche à quelqu'un d'autre. Le nom suit
|
placement, parce que c'est la seule chose qui survit à l'envoi de la planche à
|
||||||
un gabarit fixe :
|
quelqu'un d'autre. Le nom suit un gabarit fixe :
|
||||||
|
|
||||||
```
|
```
|
||||||
<base>.badges[-<k>]_<A4|Lettre>_<feuille|pile>_<AAAA-MM-JJ_HH-MM-SS>.pdf
|
<base>.badges[-p<id>][-<k>]_<A4|Lettre>_<feuille|pile>[_table]_<AAAA-MM-JJ_HH-MM-SS>[_<n>].pdf
|
||||||
```
|
```
|
||||||
|
|
||||||
— la base des fichiers de l'événement (§ 8.6), le nombre k de badges d'une
|
— la base des fichiers de l'événement (§ 8.6), raccourcie et suivie d'un point
|
||||||
réédition, le papier, l'imposition, puis le jour et l'heure de l'horodatage du
|
quand le chemin dépasserait la borne (ci-dessous) ; l'identifiant de la proposition
|
||||||
placement, décalage retiré, ou `horodatage-inconnu` quand le placement n'en
|
imprimée, rien pour le retenu ; le nombre k de badges d'une réédition ; le
|
||||||
porte pas. Il ne contient aucun deux-points, que Windows refuse, et tient sous la
|
papier ; l'imposition ; `_table` pour l'ordre par table, rien pour l'ordre
|
||||||
borne de chemin du § 8.6 ; même événement, même papier, même imposition, même
|
alphabétique ; le jour et l'heure de l'horodatage du placement, décalage
|
||||||
placement : même nom.
|
retiré, ou `horodatage-inconnu` quand le placement n'en porte pas ; enfin le
|
||||||
|
rang n d'un nom déjà pris dans le dossier. Il ne contient aucun deux-points,
|
||||||
|
que Windows refuse.
|
||||||
|
|
||||||
|
**Le nom est injectif sur ce qui change la pile.** Une génération date toutes
|
||||||
|
les propositions qu'elle ajoute du même instant (§ 8.9) : sans leur
|
||||||
|
identifiant, deux propositions donneraient le même nom, et la seconde planche
|
||||||
|
remplacerait la première sans rien dire. L'ordre par table change la pile
|
||||||
|
autant que l'imposition, et se lit donc dans le nom de la même façon. Même
|
||||||
|
événement, même source, même papier, même imposition, même ordre, même
|
||||||
|
placement : même nom, et la planche entière remplace la sienne, qui imprime la
|
||||||
|
même pile. **Une réédition ne remplace jamais un fichier** : deux rééditions de
|
||||||
|
trois personnes différentes partagent tout le reste du nom, et la seconde prend
|
||||||
|
le premier rang libre, `_2`, `_3`…, comme le dossier daté de la corbeille
|
||||||
|
(§ 8.7). Une planche dont la base se raccourcit (ci-dessous) prend aussi le
|
||||||
|
premier rang libre : la base d'un événement et celle de sa copie (§ 8.7), qui
|
||||||
|
partagent propositions et horodatages, peuvent se raccourcir en une même tête.
|
||||||
|
Un dossier qui ne se liste pas refuse ces planches plutôt que de risquer un
|
||||||
|
remplacement, et offre « Enregistrer sous… ».
|
||||||
|
|
||||||
|
**Le nom tient sous la borne de chemin du § 8.6.** Sans marque, son plus long
|
||||||
|
suffixe tient dans la réserve que la borne fait à chaque base. Les marques et le
|
||||||
|
rang l'allongent : quand le chemin complet dépasserait la borne, la base se
|
||||||
|
raccourcit dans le nom de la planche, sans couper un caractère, espaces et points
|
||||||
|
finaux retirés, sans laisser un nom réservé (§ 8.6), suivie d'un point ; le reste
|
||||||
|
du nom demeure entier. Aucune base ne finit par un point, que Windows retire de
|
||||||
|
la fin d'un nom et que la dérivation retire donc aussi (§ 8.6) : un nom
|
||||||
|
raccourci ne se confond avec aucun nom entier, et la planche entière d'une base
|
||||||
|
qui tient ne remplace jamais une planche raccourcie. Le nom raccourci dépend de
|
||||||
|
la racine du dossier qui le reçoit. Quand même un caractère de la base ne tient
|
||||||
|
pas, le nom garde sa base, l'écriture refusée se dit, et « Enregistrer sous… »
|
||||||
|
dépose la planche ailleurs.
|
||||||
|
|
||||||
**Sous `electron`, le fichier se dépose à côté des fichiers d'événement** (§ 8.6),
|
**Sous `electron`, le fichier se dépose à côté des fichiers d'événement** (§ 8.6),
|
||||||
par une écriture atomique, et le logiciel en nomme le chemin ; une écriture
|
par une écriture atomique, et le logiciel en nomme le chemin ; une écriture
|
||||||
|
|
@ -2524,7 +2557,10 @@ nomme le fichier, sans chemin.
|
||||||
proposition choisie dans la section des propositions ; l'annonce nomme la source.
|
proposition choisie dans la section des propositions ; l'annonce nomme la source.
|
||||||
Une planche tirée d'une proposition sort **toujours** en BROUILLON (§ 11.7), à
|
Une planche tirée d'une proposition sort **toujours** en BROUILLON (§ 11.7), à
|
||||||
l'horodatage de cette proposition : elle n'est pas le plan retenu. Sans retenu ni
|
l'horodatage de cette proposition : elle n'est pas le plan retenu. Sans retenu ni
|
||||||
proposition désignée, la production se refuse et renvoie aux propositions.
|
proposition désignée, la production se refuse et renvoie aux propositions. Une
|
||||||
|
proposition qui se contredit se refuse aussi, et renvoie aux propositions : la
|
||||||
|
version précédente du fichier, le remède d'un retenu qui se contredit, ne se
|
||||||
|
reprend pas pour elle, puisque le retenu est sain.
|
||||||
|
|
||||||
**Produire la planche n'est pas une modification.** Elle n'écrit aucun fichier
|
**Produire la planche n'est pas une modification.** Elle n'écrit aucun fichier
|
||||||
d'événement et n'ajoute aucune entrée d'historique (§ 8.2), donc elle reste
|
d'événement et n'ajoute aucune entrée d'historique (§ 8.2), donc elle reste
|
||||||
|
|
|
||||||
|
|
@ -542,6 +542,7 @@ const EXEMPLES = new Map([
|
||||||
['ENTREE_CHANGER_CIBLE_REDONDANCE', { cible: 1200 }],
|
['ENTREE_CHANGER_CIBLE_REDONDANCE', { cible: 1200 }],
|
||||||
// Planche de badges
|
// Planche de badges
|
||||||
['PLANCHE_SANS_PLACEMENT', { remede: { geste: 'ouvrirSection', section: 'propositions' } }],
|
['PLANCHE_SANS_PLACEMENT', { remede: { geste: 'ouvrirSection', section: 'propositions' } }],
|
||||||
|
['PROPOSITION_FAUTIVE', { id: 2, remede: { geste: 'ouvrirSection', section: 'propositions' } }],
|
||||||
[
|
[
|
||||||
'PLANCHE_DERIVE',
|
'PLANCHE_DERIVE',
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,12 @@ export const TABLE = new Map([
|
||||||
'PLANCHE_SANS_PLACEMENT',
|
'PLANCHE_SANS_PLACEMENT',
|
||||||
() => "Aucun placement à imprimer : retenez une proposition, ou choisissez-en une parmi les propositions.",
|
() => "Aucun placement à imprimer : retenez une proposition, ou choisissez-en une parmi les propositions.",
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'PROPOSITION_FAUTIVE',
|
||||||
|
({ id }) =>
|
||||||
|
`La proposition n° ${id} se contredit, la planche ne se produit pas : ` +
|
||||||
|
'choisissez-en une autre parmi les propositions, ou relancez la génération.',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'PLANCHE_DERIVE',
|
'PLANCHE_DERIVE',
|
||||||
({ raisons }, texte) =>
|
({ raisons }, texte) =>
|
||||||
|
|
@ -126,6 +132,16 @@ export const TABLE = new Map([
|
||||||
(exclus === 0 ? '' : `, ${compte(exclus, 'exclu', 'exclus')}`),
|
(exclus === 0 ? '' : `, ${compte(exclus, 'exclu', 'exclus')}`),
|
||||||
],
|
],
|
||||||
['planche.controle.evenement', (evenement, texte) => `Événement : ${texte('planche.pied.evenement', evenement)}`],
|
['planche.controle.evenement', (evenement, texte) => `Événement : ${texte('planche.pied.evenement', evenement)}`],
|
||||||
|
// La source imprimée : le placement retenu, ou la proposition par son
|
||||||
|
// numéro, que rien d'autre sur la page ne distingue d'une autre proposition
|
||||||
|
// de la même génération.
|
||||||
|
[
|
||||||
|
'planche.controle.source',
|
||||||
|
({ source, proposition }) =>
|
||||||
|
source === 'proposition'
|
||||||
|
? `Source : la proposition n° ${proposition}, qui n\u{2019}est pas le placement retenu`
|
||||||
|
: 'Source : le placement retenu',
|
||||||
|
],
|
||||||
['planche.controle.version', ({ version }) => `Version : ${version}`],
|
['planche.controle.version', ({ version }) => `Version : ${version}`],
|
||||||
['planche.controle.consigne', 'Imprimer en recto seul, à 100 %'],
|
['planche.controle.consigne', 'Imprimer en recto seul, à 100 %'],
|
||||||
['planche.controle.reedition', ({ n }) => `Réédition : ${compte(n, 'badge', 'badges')}`],
|
['planche.controle.reedition', ({ n }) => `Réédition : ${compte(n, 'badge', 'badges')}`],
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,10 @@
|
||||||
// en page et le PDF (src/pdf) ne composent aucun mot.
|
// en page et le PDF (src/pdf) ne composent aucun mot.
|
||||||
//
|
//
|
||||||
// La planche imprime une source : le placement retenu, ou une proposition
|
// La planche imprime une source : le placement retenu, ou une proposition
|
||||||
// désignée par son identifiant (Q3), qui sort toujours en BROUILLON. Elle se
|
// désignée par son identifiant (Q3), qui sort toujours en BROUILLON, et que
|
||||||
// refuse sans placement, sur un placement qui se contredit ou qui ne décrit
|
// la feuille de contrôle nomme. Elle se refuse sans placement, sur un
|
||||||
// plus la soirée, avant de produire (§ 11.8).
|
// placement qui se contredit ou qui ne décrit plus la soirée, avant de
|
||||||
|
// produire (§ 11.8).
|
||||||
import { pourvoyeursDesTitres } from '../csv/import.js';
|
import { pourvoyeursDesTitres } from '../csv/import.js';
|
||||||
import { cleNormalisee } from '../csv/normalisation.js';
|
import { cleNormalisee } from '../csv/normalisation.js';
|
||||||
import { BADGE, PAPIERS } from '../geometrie/papiers.js';
|
import { BADGE, PAPIERS } from '../geometrie/papiers.js';
|
||||||
|
|
@ -46,6 +47,9 @@ import { versionVisible } from './version.js';
|
||||||
* pied: string[], piedReduit: string[]}} textes
|
* pied: string[], piedReduit: string[]}} textes
|
||||||
* @typedef {Object} ModelePlanche
|
* @typedef {Object} ModelePlanche
|
||||||
* @property {Badge[]} badges dans l'ordre de la pile
|
* @property {Badge[]} badges dans l'ordre de la pile
|
||||||
|
* @property {'retenu'|'proposition'} source le placement imprimé
|
||||||
|
* @property {number|null} proposition l'identifiant de la proposition
|
||||||
|
* imprimée ; null pour le retenu
|
||||||
* @property {{nom: string, date: string|null}} evenement
|
* @property {{nom: string, date: string|null}} evenement
|
||||||
* @property {string|null} horodatage
|
* @property {string|null} horodatage
|
||||||
* @property {boolean} brouillon
|
* @property {boolean} brouillon
|
||||||
|
|
@ -149,11 +153,14 @@ function placementDeSource(charge, source, proposition) {
|
||||||
/**
|
/**
|
||||||
* Le refus qui garde la planche avant de produire (§ 11.8, Q3), le premier
|
* Le refus qui garde la planche avant de produire (§ 11.8, Q3), le premier
|
||||||
* dans cet ordre : PLANCHE_SANS_PLACEMENT {} — la source retenu sans retenu,
|
* dans cet ordre : PLANCHE_SANS_PLACEMENT {} — la source retenu sans retenu,
|
||||||
* ou la source proposition sans proposition désignée ; RETENU_FAUTIF {},
|
* ou la source proposition sans proposition désignée ; quand le placement
|
||||||
* relayé tel que les places le lèvent, quand le placement de la source se
|
* de la source se contredit, RETENU_FAUTIF {}, relayé tel que les places le
|
||||||
* contredit ; PLANCHE_DERIVE {raisons}, les raisons de dérive de la source
|
* lèvent, remède la version précédente du fichier, pour le retenu, et
|
||||||
* nommées (raisonsNommees) quand elle ne décrit plus la soirée. null quand
|
* PROPOSITION_FAUTIVE {id}, remède les propositions, pour une proposition —
|
||||||
* la planche se produit. Les détails portent le remède.
|
* le retenu, sain, ne se reprend pas pour elle ; PLANCHE_DERIVE {raisons},
|
||||||
|
* les raisons de dérive de la source nommées (raisonsNommees) quand elle ne
|
||||||
|
* décrit plus la soirée. null quand la planche se produit. Les détails
|
||||||
|
* portent le remède.
|
||||||
*
|
*
|
||||||
* @param {import('../stockage/types.js').Charge} charge
|
* @param {import('../stockage/types.js').Charge} charge
|
||||||
* @param {{source?: 'retenu'|'proposition', proposition?: number|null}} [options]
|
* @param {{source?: 'retenu'|'proposition', proposition?: number|null}} [options]
|
||||||
|
|
@ -166,6 +173,7 @@ export function refusPlanche(charge, { source = 'retenu', proposition = null } =
|
||||||
const placement = placementDeSource(charge, source, proposition);
|
const placement = placementDeSource(charge, source, proposition);
|
||||||
if (placement === null) return { code: 'PLANCHE_SANS_PLACEMENT', details: { remede: versPropositions() } };
|
if (placement === null) return { code: 'PLANCHE_SANS_PLACEMENT', details: { remede: versPropositions() } };
|
||||||
if (fautes(placement).length > 0) {
|
if (fautes(placement).length > 0) {
|
||||||
|
if (source === 'proposition') return { code: 'PROPOSITION_FAUTIVE', details: { id: proposition, remede: versPropositions() } };
|
||||||
return { code: 'RETENU_FAUTIF', details: { remede: { geste: 'ouvrirSecours', source: 'precedent' } } };
|
return { code: 'RETENU_FAUTIF', details: { remede: { geste: 'ouvrirSecours', source: 'precedent' } } };
|
||||||
}
|
}
|
||||||
const raisons = derive(placement, charge);
|
const raisons = derive(placement, charge);
|
||||||
|
|
@ -308,7 +316,8 @@ function selectionLue(charge, selection) {
|
||||||
* Le modèle de la planche (§ 11.4 à § 11.8) : un badge par personne non
|
* Le modèle de la planche (§ 11.4 à § 11.8) : un badge par personne non
|
||||||
* exclue — ou par personne de la sélection, pour une réédition —, dans
|
* exclue — ou par personne de la sélection, pour une réédition —, dans
|
||||||
* l'ordre demandé, ses lignes de tour lues du placement de la source, ses
|
* l'ordre demandé, ses lignes de tour lues du placement de la source, ses
|
||||||
* textes composés. horodatage : horodatagePlacement de la source ;
|
* textes composés. source et proposition : le placement imprimé, que la
|
||||||
|
* feuille de contrôle nomme ; horodatage : horodatagePlacement de la source ;
|
||||||
* brouillon : estBrouillon sur toute la soirée, toujours vrai pour une
|
* brouillon : estBrouillon sur toute la soirée, toujours vrai pour une
|
||||||
* proposition ; version : versionVisible, que la feuille de contrôle
|
* proposition ; version : versionVisible, que la feuille de contrôle
|
||||||
* imprime et le badge jamais. Les comptes sont ceux de la soirée, produits
|
* imprime et le badge jamais. Les comptes sont ceux de la soirée, produits
|
||||||
|
|
@ -368,6 +377,8 @@ export function modelePlanche(
|
||||||
const exclus = charge.participants.filter(({ exclu }) => exclu).length;
|
const exclus = charge.participants.filter(({ exclu }) => exclu).length;
|
||||||
return {
|
return {
|
||||||
badges,
|
badges,
|
||||||
|
source,
|
||||||
|
proposition: source === 'proposition' ? proposition : null,
|
||||||
evenement,
|
evenement,
|
||||||
horodatage,
|
horodatage,
|
||||||
brouillon: estBrouillon(charge, { source, tour: 'tous' }),
|
brouillon: estBrouillon(charge, { source, tour: 'tous' }),
|
||||||
|
|
@ -389,7 +400,8 @@ const sansAbsents = (texte, absents) => [...texte].map((c) => (absents.has(c) ?
|
||||||
* Les lignes de la feuille de contrôle (§ 11.3, § 11.5, § 11.8), page 1 de
|
* Les lignes de la feuille de contrôle (§ 11.3, § 11.5, § 11.8), page 1 de
|
||||||
* la planche : les cotes du badge, le papier et ses cotes, l'imposition, le
|
* la planche : les cotes du badge, le papier et ses cotes, l'imposition, le
|
||||||
* nombre de planches, le compte produit et le compte attendu, l'événement
|
* nombre de planches, le compte produit et le compte attendu, l'événement
|
||||||
* et sa date, l'horodatage du placement, la version, la consigne
|
* et sa date, la source — le placement retenu, ou la proposition par son
|
||||||
|
* numéro —, l'horodatage du placement, la version, la consigne
|
||||||
* d'impression ; BROUILLON s'il y a lieu ; pour une réédition, son compte
|
* d'impression ; BROUILLON s'il y a lieu ; pour une réédition, son compte
|
||||||
* puis les noms réédités dans l'ordre de la pile ; les caractères absents
|
* puis les noms réédités dans l'ordre de la pile ; les caractères absents
|
||||||
* de la police, quand il y en a, par leur point de code. Dans toutes les
|
* de la police, quand il y en a, par leur point de code. Dans toutes les
|
||||||
|
|
@ -402,7 +414,8 @@ const sansAbsents = (texte, absents) => [...texte].map((c) => (absents.has(c) ?
|
||||||
* @param {number} options.planches entier ≥ 0
|
* @param {number} options.planches entier ≥ 0
|
||||||
* @param {string[]} [options.absents] un caractère chacun, doublons admis
|
* @param {string[]} [options.absents] un caractère chacun, doublons admis
|
||||||
* @returns {string[]}
|
* @returns {string[]}
|
||||||
* @throws {TypeError} une option hors de son domaine
|
* @throws {TypeError} une option hors de son domaine, une source du modèle
|
||||||
|
* hors de la sienne
|
||||||
*/
|
*/
|
||||||
export function textesControle(modele, { papier, imposition, planches, absents = [] }) {
|
export function textesControle(modele, { papier, imposition, planches, absents = [] }) {
|
||||||
if (typeof papier !== 'string' || !Object.hasOwn(PAPIERS, papier)) {
|
if (typeof papier !== 'string' || !Object.hasOwn(PAPIERS, papier)) {
|
||||||
|
|
@ -418,7 +431,12 @@ export function textesControle(modele, { papier, imposition, planches, absents =
|
||||||
if (!Array.isArray(absents) || !absents.every(unCaractere)) {
|
if (!Array.isArray(absents) || !absents.every(unCaractere)) {
|
||||||
throw new TypeError(`absents : caractères attendus, reçu ${JSON.stringify(absents)}`);
|
throw new TypeError(`absents : caractères attendus, reçu ${JSON.stringify(absents)}`);
|
||||||
}
|
}
|
||||||
const { badges, evenement, horodatage, brouillon, comptes, reedition, version } = modele;
|
const { badges, source, proposition, evenement, horodatage, brouillon, comptes, reedition, version } = modele;
|
||||||
|
const sourceBienFormee =
|
||||||
|
(source === 'retenu' && proposition === null) || (source === 'proposition' && Number.isSafeInteger(proposition));
|
||||||
|
if (!sourceBienFormee) {
|
||||||
|
throw new TypeError(`modele : source et proposition attendues, reçu ${JSON.stringify({ source, proposition })}`);
|
||||||
|
}
|
||||||
const lignes = [
|
const lignes = [
|
||||||
libelle('planche.controle.badge', BADGE),
|
libelle('planche.controle.badge', BADGE),
|
||||||
libelle('planche.controle.papier', { papier, ...PAPIERS[papier] }),
|
libelle('planche.controle.papier', { papier, ...PAPIERS[papier] }),
|
||||||
|
|
@ -426,6 +444,7 @@ export function textesControle(modele, { papier, imposition, planches, absents =
|
||||||
libelle('planche.controle.planches', { planches }),
|
libelle('planche.controle.planches', { planches }),
|
||||||
libelle('planche.controle.comptes', comptes),
|
libelle('planche.controle.comptes', comptes),
|
||||||
libelle('planche.controle.evenement', evenement),
|
libelle('planche.controle.evenement', evenement),
|
||||||
|
libelle('planche.controle.source', { source, proposition }),
|
||||||
horodatage === null ? libelle('planche.pied.horodatageInconnu') : libelle('planche.pied.horodatage', { horodatage }),
|
horodatage === null ? libelle('planche.pied.horodatageInconnu') : libelle('planche.pied.horodatage', { horodatage }),
|
||||||
libelle('planche.controle.version', { version }),
|
libelle('planche.controle.version', { version }),
|
||||||
libelle('planche.controle.consigne'),
|
libelle('planche.controle.consigne'),
|
||||||
|
|
|
||||||
|
|
@ -422,6 +422,8 @@ describe('planche : la source imprimée, le retenu ou une proposition (Q3)', ()
|
||||||
assert.deepEqual(badgeDe(tiree, 1).textes.lignes, ['Tour 1 — table 7', 'Tour 2 — table 3']);
|
assert.deepEqual(badgeDe(tiree, 1).textes.lignes, ['Tour 1 — table 7', 'Tour 2 — table 3']);
|
||||||
assert.deepEqual(badgeDe(retenu, 1).textes.lignes, ['Toute la soirée — table 7']);
|
assert.deepEqual(badgeDe(retenu, 1).textes.lignes, ['Toute la soirée — table 7']);
|
||||||
assert.notDeepEqual(tiree, retenu);
|
assert.notDeepEqual(tiree, retenu);
|
||||||
|
assert.deepEqual([retenu.source, retenu.proposition], ['retenu', null]);
|
||||||
|
assert.deepEqual([tiree.source, tiree.proposition], ['proposition', 2]);
|
||||||
const sansChamp = avecProposition();
|
const sansChamp = avecProposition();
|
||||||
delete sansChamp.propositions[0].horodatage;
|
delete sansChamp.propositions[0].horodatage;
|
||||||
assert.equal(modele(sansChamp, { source: 'proposition', proposition: 2 }).horodatage, null);
|
assert.equal(modele(sansChamp, { source: 'proposition', proposition: 2 }).horodatage, null);
|
||||||
|
|
@ -456,15 +458,31 @@ describe('planche : les refus avant de produire (§ 11.8, Q3)', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('une source qui se contredit : RETENU_FAUTIF, tel que le relaient les places', () => {
|
test('un retenu qui se contredit : RETENU_FAUTIF, tel que le relaient les places, remède la version précédente du fichier', () => {
|
||||||
const attendu = { code: 'RETENU_FAUTIF', details: { remede: { geste: 'ouvrirSecours', source: 'precedent' } } };
|
const attendu = { code: 'RETENU_FAUTIF', details: { remede: { geste: 'ouvrirSecours', source: 'precedent' } } };
|
||||||
const fautif = aReserve();
|
const fautif = aReserve();
|
||||||
fautif.retenu.placement[0].sieges[1] = [3, 3];
|
fautif.retenu.placement[0].sieges[1] = [3, 3];
|
||||||
assert.deepEqual(refusPlanche(fautif), attendu);
|
assert.deepEqual(refusPlanche(fautif), attendu);
|
||||||
|
assert.throws(() => modele(fautif), (erreur) => erreur instanceof ErreurCommande && erreur.code === 'RETENU_FAUTIF');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('une proposition qui se contredit : PROPOSITION_FAUTIVE la nomme, remède les propositions ; le retenu sain se produit, et ne la juge pas', () => {
|
||||||
const proposee = aReserve({ propositions: [[2, [{ sieges: [[1, 2], [3, 4]], reserve: [5] }, { sieges: [[1, 5], [3, 4]], reserve: [2] }]]] });
|
const proposee = aReserve({ propositions: [[2, [{ sieges: [[1, 2], [3, 4]], reserve: [5] }, { sieges: [[1, 5], [3, 4]], reserve: [2] }]]] });
|
||||||
proposee.propositions[0].placement[1].reserve = [];
|
proposee.propositions[0].placement[1].reserve = [];
|
||||||
|
const attendu = { code: 'PROPOSITION_FAUTIVE', details: { id: 2, remede: VERS_PROPOSITIONS } };
|
||||||
assert.deepEqual(refusPlanche(proposee, { source: 'proposition', proposition: 2 }), attendu);
|
assert.deepEqual(refusPlanche(proposee, { source: 'proposition', proposition: 2 }), attendu);
|
||||||
assert.throws(() => modele(fautif), (erreur) => erreur instanceof ErreurCommande && erreur.code === 'RETENU_FAUTIF');
|
assert.throws(
|
||||||
|
() => modele(proposee, { source: 'proposition', proposition: 2 }),
|
||||||
|
(erreur) => erreur instanceof ErreurCommande && erreur.code === 'PROPOSITION_FAUTIVE' && erreur.details.id === 2,
|
||||||
|
);
|
||||||
|
assert.equal(refusPlanche(proposee), null);
|
||||||
|
const texte = libelle(attendu.code, attendu.details);
|
||||||
|
assert.match(texte, /proposition n° 2\b/);
|
||||||
|
assert.doesNotMatch(texte, /retenu|version précédente/);
|
||||||
|
// Un retenu fautif n'empêche pas d'imprimer une proposition saine.
|
||||||
|
const retenuFautif = aReserve({ propositions: [[2, [{ sieges: [[1, 2], [3, 4]], reserve: [5] }, { sieges: [[1, 5], [3, 4]], reserve: [2] }]]] });
|
||||||
|
retenuFautif.retenu.placement[0].sieges[1] = [3, 3];
|
||||||
|
assert.equal(refusPlanche(retenuFautif, { source: 'proposition', proposition: 2 }), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('une source en dérive — une personne ajoutée depuis — : PLANCHE_DERIVE, qui la nomme avant de produire', () => {
|
test('une source en dérive — une personne ajoutée depuis — : PLANCHE_DERIVE, qui la nomme avant de produire', () => {
|
||||||
|
|
@ -493,6 +511,10 @@ describe('planche : les refus avant de produire (§ 11.8, Q3)', () => {
|
||||||
fautifEtDerive.retenu.placement[0].sieges[1] = [3, 3];
|
fautifEtDerive.retenu.placement[0].sieges[1] = [3, 3];
|
||||||
fautifEtDerive.participants.push(personne(6, 'Ondine', 'Grisaille'));
|
fautifEtDerive.participants.push(personne(6, 'Ondine', 'Grisaille'));
|
||||||
assert.equal(refusPlanche(fautifEtDerive).code, 'RETENU_FAUTIF');
|
assert.equal(refusPlanche(fautifEtDerive).code, 'RETENU_FAUTIF');
|
||||||
|
const fautiveEtDerive = aReserve({ propositions: [[2, [{ sieges: [[1, 2], [3, 4]], reserve: [5] }, { sieges: [[1, 5], [3, 4]], reserve: [2] }]]] });
|
||||||
|
fautiveEtDerive.propositions[0].placement[1].reserve = [];
|
||||||
|
fautiveEtDerive.participants.push(personne(6, 'Ondine', 'Grisaille'));
|
||||||
|
assert.equal(refusPlanche(fautiveEtDerive, { source: 'proposition', proposition: 2 }).code, 'PROPOSITION_FAUTIVE');
|
||||||
const sansPlacementEtDerive = aReserve({ plan: null });
|
const sansPlacementEtDerive = aReserve({ plan: null });
|
||||||
sansPlacementEtDerive.participants.push(personne(6, 'Ondine', 'Grisaille'));
|
sansPlacementEtDerive.participants.push(personne(6, 'Ondine', 'Grisaille'));
|
||||||
assert.equal(refusPlanche(sansPlacementEtDerive).code, 'PLANCHE_SANS_PLACEMENT');
|
assert.equal(refusPlanche(sansPlacementEtDerive).code, 'PLANCHE_SANS_PLACEMENT');
|
||||||
|
|
@ -632,6 +654,23 @@ describe('planche : la feuille de contrôle (§ 11.3, § 11.5, § 11.8)', () =>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('la source se nomme : le placement retenu, ou la proposition par son numéro, avant l’horodatage du placement', () => {
|
||||||
|
const charge = aReserve({ propositions: [[2, [{ sieges: [[1, 2], [3, 4]], reserve: [5] }, { sieges: [[3, 4], [1, 5]], reserve: [2] }]]] });
|
||||||
|
const options = { papier: 'A4', imposition: 'feuille', planches: 2 };
|
||||||
|
const retenu = textesControle(modele(charge), options);
|
||||||
|
const tiree = textesControle(modele(charge, { source: 'proposition', proposition: 2 }), options);
|
||||||
|
assert.ok(retenu.includes('Source : le placement retenu'), JSON.stringify(retenu));
|
||||||
|
const ligne = 'Source : la proposition n° 2, qui n\u{2019}est pas le placement retenu';
|
||||||
|
assert.ok(tiree.includes(ligne), JSON.stringify(tiree));
|
||||||
|
assert.equal(tiree.indexOf(ligne) + 1, tiree.indexOf(libelle('planche.pied.horodatage', { horodatage: H_PROPOSITION })));
|
||||||
|
assert.ok(!retenu.some((texte) => texte.includes('proposition')), JSON.stringify(retenu));
|
||||||
|
// Une source hors de son domaine lève.
|
||||||
|
const m = modele(charge);
|
||||||
|
for (const mauvaise of [{ source: undefined }, { source: 'plan' }, { source: 'proposition', proposition: null }, { source: 'retenu', proposition: 2 }]) {
|
||||||
|
assert.throws(() => textesControle({ ...m, ...mauvaise }, options), TypeError, JSON.stringify(mauvaise));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('une réédition s’annonce, « Réédition : 3 badges », puis nomme les trois dans l’ordre de la pile', () => {
|
test('une réédition s’annonce, « Réédition : 3 badges », puis nomme les trois dans l’ordre de la pile', () => {
|
||||||
const m = modele(aReserve(), { selection: [2, 5, 4] });
|
const m = modele(aReserve(), { selection: [2, 5, 4] });
|
||||||
const lignes = textesControle(m, { papier: 'A4', imposition: 'feuille', planches: 1 });
|
const lignes = textesControle(m, { papier: 'A4', imposition: 'feuille', planches: 1 });
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,20 @@
|
||||||
// et, pour une réédition, la sélection. Les refus s'ordonnent : ceux du
|
// et, pour une réédition, la sélection. Les refus s'ordonnent : ceux du
|
||||||
// placement (refusPlanche), puis ceux de la mise en page, nommant la
|
// placement (refusPlanche), puis ceux de la mise en page, nommant la
|
||||||
// personne du badge refusé, puis le papier à choisir.
|
// personne du badge refusé, puis le papier à choisir.
|
||||||
|
//
|
||||||
|
// Le nom du fichier (nomPlanche) porte tout ce qui change la pile : la
|
||||||
|
// source, l'imposition, l'ordre, le placement. Une réédition, ou une
|
||||||
|
// planche dont la borne du chemin raccourcit la base (nomPlancheExigeListe),
|
||||||
|
// ne remplace jamais un fichier du dossier : son nom prend le premier rang
|
||||||
|
// libre parmi les entrées que le dossier porte, listées à chaque
|
||||||
|
// préparation et à chaque production. Un dossier qui ne se liste pas laisse
|
||||||
|
// à l'annonce le nom sans dossier, et refuse la production
|
||||||
|
// PLANCHE_NON_ECRITE avant d'écrire, plutôt que de risquer de remplacer une
|
||||||
|
// planche précédente.
|
||||||
import { PAPIERS } from '../geometrie/papiers.js';
|
import { PAPIERS } from '../geometrie/papiers.js';
|
||||||
import { nombrePlanches } from '../pdf/imposition.js';
|
import { nombrePlanches } from '../pdf/imposition.js';
|
||||||
import { ErreurStockage } from '../stockage/erreurs.js';
|
import { ErreurStockage } from '../stockage/erreurs.js';
|
||||||
import { nomPlanche } from '../stockage/noms.js';
|
import { nomPlanche, nomPlancheExigeListe } from '../stockage/noms.js';
|
||||||
import { ErreurCommande, refus } from './erreurs.js';
|
import { ErreurCommande, refus } from './erreurs.js';
|
||||||
import { libelle } from './libelles.js';
|
import { libelle } from './libelles.js';
|
||||||
import { IMPOSITIONS, ORDRES, modelePlanche, refusPlanche, refusProduction, textesControle } from './planche.js';
|
import { IMPOSITIONS, ORDRES, modelePlanche, refusPlanche, refusProduction, textesControle } from './planche.js';
|
||||||
|
|
@ -118,7 +128,7 @@ export function creerPlanches({ polices, provenance, version }) {
|
||||||
async function etudier({ charge, papier }, demande) {
|
async function etudier({ charge, papier }, demande) {
|
||||||
const { source, proposition, ordre, selection, imposition } = demandeLue(demande);
|
const { source, proposition, ordre, selection, imposition } = demandeLue(demande);
|
||||||
const rejet = refusPlanche(charge, { source, proposition });
|
const rejet = refusPlanche(charge, { source, proposition });
|
||||||
if (rejet !== null) return { refus: rejet, modele: null, absents: [], papier, imposition, source };
|
if (rejet !== null) return { refus: rejet, modele: null, absents: [], papier, imposition, ordre, source };
|
||||||
if (typeof provenance !== 'string') throw new TypeError('provenance : le mode de construction attendu');
|
if (typeof provenance !== 'string') throw new TypeError('provenance : le mode de construction attendu');
|
||||||
const modele = modelePlanche(charge, { source, proposition, ordre, selection, provenance, version });
|
const modele = modelePlanche(charge, { source, proposition, ordre, selection, provenance, version });
|
||||||
const octets = await lesPolices();
|
const octets = await lesPolices();
|
||||||
|
|
@ -133,18 +143,63 @@ export function creerPlanches({ polices, provenance, version }) {
|
||||||
} else if (papier === null) {
|
} else if (papier === null) {
|
||||||
leRefus = papierRequis();
|
leRefus = papierRequis();
|
||||||
}
|
}
|
||||||
return { refus: leRefus, modele, absents: analyse.absents, papier, imposition, source };
|
return { refus: leRefus, modele, absents: analyse.absents, papier, imposition, ordre, source };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Le nom du fichier d'une étude, null sans papier.
|
// Les options de nomPlanche pour une étude, sous le dossier donné.
|
||||||
const nomDe = (base, { modele, papier, imposition }) =>
|
const optionsDe = ({ modele, papier, imposition, ordre }, dossier) => ({
|
||||||
papier === null ? null : nomPlanche(base, { papier, imposition, horodatage: modele.horodatage, reedition: modele.reedition });
|
papier,
|
||||||
|
imposition,
|
||||||
|
ordre,
|
||||||
|
horodatage: modele.horodatage,
|
||||||
|
reedition: modele.reedition,
|
||||||
|
proposition: modele.proposition,
|
||||||
|
dossier,
|
||||||
|
});
|
||||||
|
|
||||||
// Les octets de la planche d'une demande, et son nom ; le refus de
|
// Le nom du fichier d'une étude, sous le dossier donné (nomPlanche) ;
|
||||||
// l'étude lève, ErreurCommande, avant que rien ne se produise.
|
// null sans papier.
|
||||||
async function octetsDe(lecture, demande) {
|
const nomDe = (base, etude, dossier) => (etude.papier === null ? null : nomPlanche(base, optionsDe(etude, dossier)));
|
||||||
|
|
||||||
|
// Le dossier où la planche d'une étude se dépose, tel que nomPlanche le
|
||||||
|
// lit : sa racine, son séparateur et, quand le nom se choisit parmi elles
|
||||||
|
// (nomPlancheExigeListe), les noms qu'il porte ; null sous web, où la
|
||||||
|
// planche se télécharge. Un dossier qui ne se liste pas lève
|
||||||
|
// l'ErreurStockage de la liste.
|
||||||
|
async function dossierDe({ fs, racine, base }, etude) {
|
||||||
|
if (fs.nature === 'web') return null;
|
||||||
|
const { separateur } = await fs.emplacements();
|
||||||
|
const sansListe = { racine: racine.chemin, separateur, existants: [] };
|
||||||
|
if (!nomPlancheExigeListe(base, optionsDe(etude, sansListe))) return sansListe;
|
||||||
|
const existants = (await fs.lister(racine, '')).map(({ nom }) => nom);
|
||||||
|
return { ...sansListe, existants };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Le refus d'une planche qui ne se dépose pas : le nom, le dossier, la
|
||||||
|
// cause, remède « Enregistrer sous… ». Le support de l'événement n'en est
|
||||||
|
// pas rompu.
|
||||||
|
const nonEcrite = ({ chemin, dossier, cause }) =>
|
||||||
|
refus('PLANCHE_NON_ECRITE', { chemin, dossier, cause: cause ?? null }, { geste: 'enregistrerSous' });
|
||||||
|
|
||||||
|
// Les octets de la planche d'une demande, et son nom : sous le dossier de
|
||||||
|
// la lecture quand avecDossier est vrai, sans dossier sinon. Le refus de
|
||||||
|
// l'étude lève, ErreurCommande, avant que rien ne se produise. Le dossier
|
||||||
|
// se lit avant de produire : une liste refusée lève PLANCHE_NON_ECRITE
|
||||||
|
// sans rien produire ni écrire.
|
||||||
|
async function octetsDe(lecture, demande, { avecDossier }) {
|
||||||
const etude = await etudier(lecture, demande);
|
const etude = await etudier(lecture, demande);
|
||||||
if (etude.refus !== null) throw new ErreurCommande(etude.refus.code, etude.refus.details);
|
if (etude.refus !== null) throw new ErreurCommande(etude.refus.code, etude.refus.details);
|
||||||
|
let dossier = null;
|
||||||
|
if (avecDossier) {
|
||||||
|
try {
|
||||||
|
dossier = await dossierDe(lecture, etude);
|
||||||
|
} catch (erreur) {
|
||||||
|
if (!(erreur instanceof ErreurStockage)) throw erreur;
|
||||||
|
const cause = erreur.details?.cause ?? erreur.code;
|
||||||
|
throw nonEcrite({ chemin: nomDe(lecture.base, etude, null), dossier: lecture.racine.chemin, cause });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const nom = nomDe(lecture.base, etude, dossier);
|
||||||
const { modele, papier, imposition, absents } = etude;
|
const { modele, papier, imposition, absents } = etude;
|
||||||
const planches = nombrePlanches(modele.badges.length, papier);
|
const planches = nombrePlanches(modele.badges.length, papier);
|
||||||
const controle = textesControle(modele, { papier, imposition, planches, absents });
|
const controle = textesControle(modele, { papier, imposition, planches, absents });
|
||||||
|
|
@ -157,7 +212,7 @@ export function creerPlanches({ polices, provenance, version }) {
|
||||||
controle,
|
controle,
|
||||||
filigrane: libelle('planche.brouillon'),
|
filigrane: libelle('planche.brouillon'),
|
||||||
});
|
});
|
||||||
return { octets, nom: nomDe(lecture.base, etude) };
|
return { octets, nom };
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -166,7 +221,9 @@ export function creerPlanches({ polices, provenance, version }) {
|
||||||
* décision 12). Un refus du placement rend l'annonce sans modèle :
|
* décision 12). Un refus du placement rend l'annonce sans modèle :
|
||||||
* comptes, planches et nom null, ni homonymes ni absents, le brouillon
|
* comptes, planches et nom null, ni homonymes ni absents, le brouillon
|
||||||
* de la source. Sans papier, refus PAPIER_REQUIS et nom null, les
|
* de la source. Sans papier, refus PAPIER_REQUIS et nom null, les
|
||||||
* planches des deux papiers données. Rien ne s'écrit.
|
* planches des deux papiers données. Le nom est celui que la production
|
||||||
|
* donnerait, rang compris ; sans dossier, donc sans rang, quand le dossier
|
||||||
|
* ne se liste pas. Rien ne s'écrit.
|
||||||
*
|
*
|
||||||
* @throws {TypeError} une demande hors de son domaine, une séance sans
|
* @throws {TypeError} une demande hors de son domaine, une séance sans
|
||||||
* polices ou sans provenance
|
* polices ou sans provenance
|
||||||
|
|
@ -189,6 +246,14 @@ export function creerPlanches({ polices, provenance, version }) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { modele } = etude;
|
const { modele } = etude;
|
||||||
|
let dossier = null;
|
||||||
|
if (papier !== null) {
|
||||||
|
try {
|
||||||
|
dossier = await dossierDe(lecture, etude);
|
||||||
|
} catch (erreur) {
|
||||||
|
if (!(erreur instanceof ErreurStockage)) throw erreur;
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
refus: etude.refus,
|
refus: etude.refus,
|
||||||
papier,
|
papier,
|
||||||
|
|
@ -197,7 +262,7 @@ export function creerPlanches({ polices, provenance, version }) {
|
||||||
homonymes: modele.homonymes,
|
homonymes: modele.homonymes,
|
||||||
absents: etude.absents,
|
absents: etude.absents,
|
||||||
brouillon: modele.brouillon,
|
brouillon: modele.brouillon,
|
||||||
nom: nomDe(lecture.base, etude),
|
nom: nomDe(lecture.base, etude, dossier),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -206,14 +271,16 @@ export function creerPlanches({ polices, provenance, version }) {
|
||||||
* d'événement, par une écriture atomique, sous electron et sur le
|
* d'événement, par une écriture atomique, sous electron et sur le
|
||||||
* système d'épreuve, et rend son chemin en entier ; sous web, par
|
* système d'épreuve, et rend son chemin en entier ; sous web, par
|
||||||
* enregistrerSous, le téléchargement, et chemin null. Le refus de
|
* enregistrerSous, le téléchargement, et chemin null. Le refus de
|
||||||
* l'annonce lève, sans rien écrire. Une écriture refusée par le support
|
* l'annonce lève, sans rien écrire. Une écriture refusée par le support,
|
||||||
* lève PLANCHE_NON_ECRITE {chemin, dossier, cause}, remède
|
* ou un dossier qui ne se liste pas quand le nom se choisit parmi ses
|
||||||
|
* entrées, lève
|
||||||
|
* PLANCHE_NON_ECRITE {chemin, dossier, cause}, remède
|
||||||
* « Enregistrer sous… » : le support de l'événement n'en est pas rompu.
|
* « Enregistrer sous… » : le support de l'événement n'en est pas rompu.
|
||||||
*
|
*
|
||||||
* @throws {ErreurCommande} le refus de l'annonce ; PLANCHE_NON_ECRITE
|
* @throws {ErreurCommande} le refus de l'annonce ; PLANCHE_NON_ECRITE
|
||||||
*/
|
*/
|
||||||
async produire(lecture, demande) {
|
async produire(lecture, demande) {
|
||||||
const { octets, nom } = await octetsDe(lecture, demande);
|
const { octets, nom } = await octetsDe(lecture, demande, { avecDossier: true });
|
||||||
const { fs, racine } = lecture;
|
const { fs, racine } = lecture;
|
||||||
if (fs.nature === 'web') {
|
if (fs.nature === 'web') {
|
||||||
await fs.enregistrerSous(nom, octets);
|
await fs.enregistrerSous(nom, octets);
|
||||||
|
|
@ -223,8 +290,7 @@ export function creerPlanches({ polices, provenance, version }) {
|
||||||
await fs.ecrireOctetsAtomique(racine, nom, octets);
|
await fs.ecrireOctetsAtomique(racine, nom, octets);
|
||||||
} catch (erreur) {
|
} catch (erreur) {
|
||||||
if (!(erreur instanceof ErreurStockage && erreur.code === 'ECRITURE')) throw erreur;
|
if (!(erreur instanceof ErreurStockage && erreur.code === 'ECRITURE')) throw erreur;
|
||||||
const { chemin, dossier, cause } = erreur.details;
|
throw nonEcrite(erreur.details);
|
||||||
throw refus('PLANCHE_NON_ECRITE', { chemin, dossier, cause }, { geste: 'enregistrerSous' });
|
|
||||||
}
|
}
|
||||||
const { separateur } = await fs.emplacements();
|
const { separateur } = await fs.emplacements();
|
||||||
const tete = racine.chemin.endsWith(separateur) ? racine.chemin : racine.chemin + separateur;
|
const tete = racine.chemin.endsWith(separateur) ? racine.chemin : racine.chemin + separateur;
|
||||||
|
|
@ -233,13 +299,14 @@ export function creerPlanches({ polices, provenance, version }) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Produit la planche et la confie au dialogue d'enregistrement de la
|
* Produit la planche et la confie au dialogue d'enregistrement de la
|
||||||
* plateforme, sous le nom de la planche : le remède d'une écriture
|
* plateforme, sous le nom de la planche sans rang ni coupe, que
|
||||||
|
* l'opérateur garde ou change avec le dossier : le remède d'une écriture
|
||||||
* refusée. Rend le chemin choisi, null quand l'opérateur annule.
|
* refusée. Rend le chemin choisi, null quand l'opérateur annule.
|
||||||
*
|
*
|
||||||
* @throws {ErreurCommande} le refus de l'annonce
|
* @throws {ErreurCommande} le refus de l'annonce
|
||||||
*/
|
*/
|
||||||
async enregistrerSous(lecture, demande) {
|
async enregistrerSous(lecture, demande) {
|
||||||
const { octets, nom } = await octetsDe(lecture, demande);
|
const { octets, nom } = await octetsDe(lecture, demande, { avecDossier: false });
|
||||||
return lecture.fs.enregistrerSous(nom, octets);
|
return lecture.fs.enregistrerSous(nom, octets);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,17 +11,17 @@
|
||||||
//
|
//
|
||||||
// La série surveillée borne le coût (§ 14.4, contrainte 31) : douze
|
// La série surveillée borne le coût (§ 14.4, contrainte 31) : douze
|
||||||
// productions de PDF au plus dans ce fichier, sur une soirée de quatre
|
// 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
|
// personnes — trois pour les trois modes, deux pour la stabilité des
|
||||||
// octets, une réédition, deux refus d'écriture et l'enregistrement sous,
|
// octets, une réédition, deux refus d'écriture et l'enregistrement sous,
|
||||||
// une sous web, une proposition. L'imposition et l'ordre se lisent dans la
|
// une sous web, deux propositions de la même génération. L'imposition et
|
||||||
// place de chaque nom, et le papier, l'imposition et le nombre de planches
|
// l'ordre se lisent dans la place de chaque nom, et le papier, l'imposition
|
||||||
// dans la feuille de contrôle : la première production, sous la demande
|
// et le nombre de planches dans la feuille de contrôle : la première
|
||||||
// par défaut, en A4 — l'ordre alphabétique ; la proposition, sous la
|
// production, sous la demande par défaut, en A4 — l'ordre alphabétique ; la
|
||||||
// demande par défaut, en Lettre — feuille par feuille sur deux planches ;
|
// proposition, sous la demande par défaut, en Lettre — feuille par feuille
|
||||||
// celle sous web en Lettre, en pile, par table. La réédition porte un
|
// sur deux planches ; celle sous web en Lettre, en pile, par table. La
|
||||||
// caractère sans glyphe, que sa feuille de contrôle nomme. Le témoin de 257
|
// réédition porte un caractère sans glyphe, que sa feuille de contrôle
|
||||||
// ne s'analyse que pour son annonce. Les noms sont inventés ; la version
|
// nomme. Le témoin de 257 ne s'analyse que pour son annonce. Les noms sont
|
||||||
// vient du module engendré.
|
// inventés ; la version vient du module engendré.
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { creerFichiersSimules } from '../../test/fichiers_simules.js';
|
import { creerFichiersSimules } from '../../test/fichiers_simules.js';
|
||||||
import { H_PROPOSITION, H_RETENU, personne, soiree, table } from '../../test/fixtures/badges/cas.js';
|
import { H_PROPOSITION, H_RETENU, personne, soiree, table } from '../../test/fixtures/badges/cas.js';
|
||||||
|
|
@ -32,9 +32,10 @@ import { lirePolices } from '../../test/polices.js';
|
||||||
import { PAPIERS } from '../geometrie/papiers.js';
|
import { PAPIERS } from '../geometrie/papiers.js';
|
||||||
import { cellules } from '../pdf/imposition.js';
|
import { cellules } from '../pdf/imposition.js';
|
||||||
import { serialiser } from '../stockage/canonique.js';
|
import { serialiser } from '../stockage/canonique.js';
|
||||||
|
import { ErreurStockage } from '../stockage/erreurs.js';
|
||||||
import { creer as creerDansLeDepot } from '../stockage/depot.js';
|
import { creer as creerDansLeDepot } from '../stockage/depot.js';
|
||||||
import { FORMAT } from '../stockage/document.js';
|
import { FORMAT } from '../stockage/document.js';
|
||||||
import { SUFFIXES, nomPlanche } from '../stockage/noms.js';
|
import { SUFFIXES, deriverBase, nomPlanche } from '../stockage/noms.js';
|
||||||
import { VERSION } from '../version.genere.js';
|
import { VERSION } from '../version.genere.js';
|
||||||
import { libelle } from './libelles.js';
|
import { libelle } from './libelles.js';
|
||||||
import { creerSeance } from './seance.js';
|
import { creerSeance } from './seance.js';
|
||||||
|
|
@ -109,8 +110,8 @@ function chargeLucioles({ personnes = null, ...champs } = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Le nom de la planche de la soirée d'essai.
|
// Le nom de la planche de la soirée d'essai.
|
||||||
const nomDe = ({ papier = 'A4', imposition = 'feuille', horodatage = H_RETENU, reedition = null } = {}) =>
|
const nomDe = ({ papier = 'A4', imposition = 'feuille', horodatage = H_RETENU, reedition = null, proposition = null, ordre } = {}) =>
|
||||||
nomPlanche(BASE, { papier, imposition, horodatage, reedition });
|
nomPlanche(BASE, { papier, imposition, horodatage, reedition, proposition, ordre });
|
||||||
|
|
||||||
// Pose l'événement de la charge sur le disque, par le dépôt ; format :
|
// Pose l'événement de la charge sur le disque, par le dépôt ; format :
|
||||||
// celui qu'annonce l'en-tête de l'état, FORMAT par défaut.
|
// celui qu'annonce l'en-tête de l'état, FORMAT par défaut.
|
||||||
|
|
@ -131,7 +132,7 @@ async function deposer(fs, charge, { format = FORMAT } = {}) {
|
||||||
|
|
||||||
// Une séance sur l'événement de la charge, ouvert en lecture, le papier
|
// Une séance sur l'événement de la charge, ouvert en lecture, le papier
|
||||||
// choisi quand il est donné ; la trace part ensuite, vide.
|
// choisi quand il est donné ; la trace part ensuite, vide.
|
||||||
async function monter(charge = chargeLucioles(), { papier = 'A4', fs = creerFichiersSimules({ documents: DOCUMENTS }), format } = {}) {
|
async function monter(charge = chargeLucioles(), { papier = 'A4', fs = creerFichiersSimules({ documents: DOCUMENTS }), format, base = BASE } = {}) {
|
||||||
await deposer(fs, charge, { format });
|
await deposer(fs, charge, { format });
|
||||||
let tirage = 0;
|
let tirage = 0;
|
||||||
const seance = creerSeance({
|
const seance = creerSeance({
|
||||||
|
|
@ -149,7 +150,7 @@ async function monter(charge = chargeLucioles(), { papier = 'A4', fs = creerFich
|
||||||
provenance: 'production',
|
provenance: 'production',
|
||||||
});
|
});
|
||||||
if (papier !== null) await seance.choisirPapier(papier);
|
if (papier !== null) await seance.choisirPapier(papier);
|
||||||
await seance.ouvrir(BASE);
|
await seance.ouvrir(base);
|
||||||
const trace = tracer(fs);
|
const trace = tracer(fs);
|
||||||
return { fs, seance, trace };
|
return { fs, seance, trace };
|
||||||
}
|
}
|
||||||
|
|
@ -315,7 +316,9 @@ describe('planche dans la séance : l’annonce (§ 11.2, § 11.5, § 11.8, déc
|
||||||
test('une proposition s’annonce BROUILLON, sous son propre horodatage ; sans placement, PLANCHE_SANS_PLACEMENT et rien du modèle', async () => {
|
test('une proposition s’annonce BROUILLON, sous son propre horodatage ; sans placement, PLANCHE_SANS_PLACEMENT et rien du modèle', async () => {
|
||||||
const { seance } = await monter();
|
const { seance } = await monter();
|
||||||
const proposee = await seance.preparerPlanche({ source: 'proposition', proposition: 2 });
|
const proposee = await seance.preparerPlanche({ source: 'proposition', proposition: 2 });
|
||||||
assert.deepEqual([proposee.refus, proposee.brouillon, proposee.nom], [null, true, nomDe({ horodatage: H_PROPOSITION })]);
|
assert.deepEqual([proposee.refus, proposee.brouillon, proposee.nom], [null, true, nomDe({ horodatage: H_PROPOSITION, proposition: 2 })]);
|
||||||
|
assert.notEqual((await seance.preparerPlanche({ source: 'proposition', proposition: 1 })).nom, proposee.nom);
|
||||||
|
assert.equal((await seance.preparerPlanche({ ordre: 'table' })).nom, nomDe({ ordre: 'table' }));
|
||||||
|
|
||||||
const sans = await monter(chargeLucioles({ plan: null, etat: 'propose' }));
|
const sans = await monter(chargeLucioles({ plan: null, etat: 'propose' }));
|
||||||
const annonce = await sans.seance.preparerPlanche();
|
const annonce = await sans.seance.preparerPlanche();
|
||||||
|
|
@ -360,11 +363,12 @@ describe('planche dans la séance : l’annonce (§ 11.2, § 11.5, § 11.8, déc
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('planche dans la séance : les octets et le nom (§ 11.6, § 11.8)', () => {
|
describe('planche dans la séance : les octets et le nom (§ 11.6, § 11.8)', () => {
|
||||||
test('deux productions du même placement : mêmes octets, même nom ; un geste lancé pendant la seconde attend son tour ; la retouche change le nom ; défaire rend le premier', async () => {
|
test('deux productions du même placement : mêmes octets, même nom ; un geste lancé pendant la seconde attend son tour ; la retouche change le nom ; défaire rend la charge, donc le premier', async () => {
|
||||||
const { fs, seance, trace } = await monter();
|
const { fs, seance, trace } = await monter();
|
||||||
const premier = await seance.produirePlanche();
|
const premier = await seance.produirePlanche();
|
||||||
const octetsPremiers = fs.octets('documents', premier.nom);
|
const octetsPremiers = fs.octets('documents', premier.nom);
|
||||||
await seance.passerEnEcriture();
|
await seance.passerEnEcriture();
|
||||||
|
const chargePremiere = structuredClone(seance.charge);
|
||||||
trace.length = 0;
|
trace.length = 0;
|
||||||
|
|
||||||
const production = seance.produirePlanche();
|
const production = seance.produirePlanche();
|
||||||
|
|
@ -382,19 +386,30 @@ describe('planche dans la séance : les octets et le nom (§ 11.6, § 11.8)', ()
|
||||||
assert.notEqual(horodatage, H_RETENU);
|
assert.notEqual(horodatage, H_RETENU);
|
||||||
assert.equal(retouche.nom, nomDe({ horodatage }));
|
assert.equal(retouche.nom, nomDe({ horodatage }));
|
||||||
|
|
||||||
|
// Défaire rend la charge entière, horodatage compris : la production,
|
||||||
|
// qui ne lit qu'elle et la demande, rend les octets que les deux
|
||||||
|
// premières ont rendus à l'identique, sous le même nom.
|
||||||
await seance.defaire();
|
await seance.defaire();
|
||||||
const troisieme = await seance.produirePlanche();
|
assert.deepEqual(seance.charge, chargePremiere);
|
||||||
assert.deepEqual(troisieme, premier);
|
assert.equal((await seance.preparerPlanche()).nom, premier.nom);
|
||||||
assert.deepEqual(fs.octets('documents', troisieme.nom), octetsPremiers);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('une réédition de trois badges : un autre nom, « -3 », et la feuille de contrôle dit « Réédition : 3 badges » et nomme le caractère sans glyphe d’« Ondine 李 »', async () => {
|
test('une réédition de trois badges : un autre nom, « -3 », et la feuille de contrôle dit « Réédition : 3 badges » et nomme le caractère sans glyphe d’« Ondine 李 »', async () => {
|
||||||
const charge = chargeLucioles();
|
const charge = chargeLucioles();
|
||||||
charge.participants[2].nom = '李';
|
charge.participants[2].nom = '李';
|
||||||
const { fs, seance } = await monter(charge);
|
const { fs, seance, trace } = await monter(charge);
|
||||||
|
// Une réédition de trois autres personnes, déjà posée sous le même nom :
|
||||||
|
// la nouvelle prend le rang libre, et la première reste.
|
||||||
|
const pris = nomDe({ reedition: { ids: [3, 1, 4] } });
|
||||||
|
const anciens = new TextEncoder().encode('%PDF-réédition précédente');
|
||||||
|
fs.deposerOctets('documents', pris, anciens);
|
||||||
|
const annonce = await seance.preparerPlanche({ selection: [3, 1, 4] });
|
||||||
const rendu = await seance.produirePlanche({ selection: [3, 1, 4] });
|
const rendu = await seance.produirePlanche({ selection: [3, 1, 4] });
|
||||||
assert.equal(rendu.nom, nomDe({ reedition: { ids: [3, 1, 4] } }));
|
assert.equal(rendu.nom, pris.replace(/\.pdf$/, '_2.pdf'));
|
||||||
|
assert.equal(annonce.nom, rendu.nom);
|
||||||
assert.match(rendu.nom, /\.badges-3_A4_feuille_/);
|
assert.match(rendu.nom, /\.badges-3_A4_feuille_/);
|
||||||
|
assert.deepEqual(fs.octets('documents', pris), anciens);
|
||||||
|
assert.deepEqual(trace, [['ecrireOctetsAtomique', rendu.nom]]);
|
||||||
const pdf = await lirePdf(fs.octets('documents', rendu.nom));
|
const pdf = await lirePdf(fs.octets('documents', rendu.nom));
|
||||||
assert.ok(textesDe(pdf.pages[0]).includes(libelle('planche.controle.reedition', { n: 3 })), textesDe(pdf.pages[0]).join(' | '));
|
assert.ok(textesDe(pdf.pages[0]).includes(libelle('planche.controle.reedition', { n: 3 })), textesDe(pdf.pages[0]).join(' | '));
|
||||||
assert.ok(textesDe(pdf.pages[0]).includes('Réédition : 3 badges'));
|
assert.ok(textesDe(pdf.pages[0]).includes('Réédition : 3 badges'));
|
||||||
|
|
@ -403,11 +418,33 @@ describe('planche dans la séance : les octets et le nom (§ 11.6, § 11.8)', ()
|
||||||
assert.equal(pdf.pages.length, 2);
|
assert.equal(pdf.pages.length, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
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 () => {
|
test('deux propositions de la même génération, au même horodatage : deux fichiers ; la seconde porte le mot du filigrane au trait, sa feuille de contrôle aussi et la nomme ; 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 charge = chargeLucioles();
|
||||||
|
assert.equal(charge.propositions[0].horodatage, charge.propositions[1].horodatage);
|
||||||
|
const { fs, seance, trace } = await monter(charge, { papier: 'Lettre' });
|
||||||
|
const premiere = await seance.produirePlanche({ source: 'proposition', proposition: 1 });
|
||||||
|
const octetsPremiers = fs.octets('documents', premiere.nom);
|
||||||
const rendu = await seance.produirePlanche({ source: 'proposition', proposition: 2 });
|
const rendu = await seance.produirePlanche({ source: 'proposition', proposition: 2 });
|
||||||
assert.equal(rendu.nom, nomDe({ papier: 'Lettre', horodatage: H_PROPOSITION }));
|
assert.deepEqual(
|
||||||
|
[premiere.nom, rendu.nom],
|
||||||
|
[1, 2].map((proposition) => nomDe({ papier: 'Lettre', horodatage: H_PROPOSITION, proposition })),
|
||||||
|
);
|
||||||
|
assert.notEqual(premiere.nom, rendu.nom);
|
||||||
|
assert.deepEqual(fs.octets('documents', premiere.nom), octetsPremiers);
|
||||||
|
assert.notDeepEqual(fs.octets('documents', rendu.nom), octetsPremiers);
|
||||||
|
const pdfs = (await fs.lister(DOC, '')).map(({ nom }) => nom).filter((nom) => nom.endsWith('.pdf'));
|
||||||
|
assert.deepEqual(pdfs, [premiere.nom, rendu.nom]);
|
||||||
|
assert.deepEqual(trace, [
|
||||||
|
['ecrireOctetsAtomique', premiere.nom],
|
||||||
|
['ecrireOctetsAtomique', rendu.nom],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// La seconde : le mot du filigrane au trait, sur ses planches et sa
|
||||||
|
// feuille de contrôle, qui la nomme ; en Lettre sous la demande par
|
||||||
|
// défaut, feuille par feuille, dans l'ordre alphabétique.
|
||||||
const pdf = await lirePdf(fs.octets('documents', rendu.nom));
|
const pdf = await lirePdf(fs.octets('documents', rendu.nom));
|
||||||
|
const source = libelle('planche.controle.source', { source: 'proposition', proposition: 2 });
|
||||||
|
assert.ok(textesDe(pdf.pages[0]).includes(source), textesDe(pdf.pages[0]).join(' | '));
|
||||||
assert.ok(pdf.pages.every((page) => textesDe(page).includes(FILIGRANE)));
|
assert.ok(pdf.pages.every((page) => textesDe(page).includes(FILIGRANE)));
|
||||||
assert.equal(auTrait(pdf), true);
|
assert.equal(auTrait(pdf), true);
|
||||||
// Deux badges par planche : feuille par feuille, la planche p porte les
|
// Deux badges par planche : feuille par feuille, la planche p porte les
|
||||||
|
|
@ -425,6 +462,60 @@ describe('planche dans la séance : les octets et le nom (§ 11.6, § 11.8)', ()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('planche dans la séance : un nom par pile (§ 11.6)', () => {
|
||||||
|
test('une réédition dont le dossier ne se liste pas : l’annonce garde le nom sans rang, produire se refuse PLANCHE_NON_ECRITE avant d’écrire, plutôt que de risquer un remplacement', async () => {
|
||||||
|
const { fs, seance, trace } = await monter();
|
||||||
|
const lister = fs.lister;
|
||||||
|
fs.lister = async () => {
|
||||||
|
throw new ErreurStockage('LECTURE', { chemin: '', dossier: DOCUMENTS, cause: 'EACCES' });
|
||||||
|
};
|
||||||
|
const nom = nomDe({ reedition: { ids: [2] } });
|
||||||
|
assert.equal((await seance.preparerPlanche({ selection: [2] })).nom, nom);
|
||||||
|
await refusee(seance.produirePlanche({ selection: [2] }), 'PLANCHE_NON_ECRITE', {
|
||||||
|
chemin: nom,
|
||||||
|
dossier: DOCUMENTS,
|
||||||
|
cause: 'EACCES',
|
||||||
|
remede: { geste: 'enregistrerSous' },
|
||||||
|
});
|
||||||
|
assert.deepEqual(trace, []);
|
||||||
|
// Une planche entière ne liste pas le dossier : elle se nomme sans lui.
|
||||||
|
assert.equal((await seance.preparerPlanche()).nom, nomDe());
|
||||||
|
fs.lister = lister;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('une planche entière dont la base se raccourcit liste le dossier : elle prend le premier rang libre plutôt que de remplacer un fichier ; un dossier qui ne se liste pas la refuse avant d’écrire', async () => {
|
||||||
|
const nom = `Souper des Lucioles du printemps, \u{E9}dition sp\u{E9}ciale ${'et des Merles '.repeat(12)}`;
|
||||||
|
const fs = creerFichiersSimules({ documents: DOCUMENTS });
|
||||||
|
const { separateur } = await fs.emplacements();
|
||||||
|
const base = deriverBase(nom, { racine: DOCUMENTS, separateur, existantes: [] });
|
||||||
|
const { seance, trace } = await monter(chargeLucioles({ nom }), { papier: 'Lettre', fs, base });
|
||||||
|
const demande = { source: 'proposition', proposition: 2, ordre: 'table' };
|
||||||
|
const options = { papier: 'Lettre', imposition: 'feuille', horodatage: H_PROPOSITION, reedition: null, proposition: 2, ordre: 'table' };
|
||||||
|
const raccourci = nomPlanche(base, { ...options, dossier: { racine: DOCUMENTS, separateur, existants: [] } });
|
||||||
|
assert.notEqual(raccourci, nomPlanche(base, options), 'la base se raccourcit');
|
||||||
|
assert.equal((await seance.preparerPlanche(demande)).nom, raccourci);
|
||||||
|
// Le fichier d'un autre événement porte déjà ce nom : la planche prend le rang suivant.
|
||||||
|
fs.deposer('documents', raccourci, 'planche d’un autre événement');
|
||||||
|
const auRang2 = nomPlanche(base, { ...options, dossier: { racine: DOCUMENTS, separateur, existants: [raccourci] } });
|
||||||
|
assert.ok(auRang2.endsWith('_2.pdf'), auRang2);
|
||||||
|
assert.equal((await seance.preparerPlanche(demande)).nom, auRang2);
|
||||||
|
const lister = fs.lister;
|
||||||
|
fs.lister = async () => {
|
||||||
|
throw new ErreurStockage('LECTURE', { chemin: '', dossier: DOCUMENTS, cause: 'EACCES' });
|
||||||
|
};
|
||||||
|
assert.equal((await seance.preparerPlanche(demande)).nom, nomPlanche(base, options));
|
||||||
|
await refusee(seance.produirePlanche(demande), 'PLANCHE_NON_ECRITE', {
|
||||||
|
chemin: nomPlanche(base, options),
|
||||||
|
dossier: DOCUMENTS,
|
||||||
|
cause: 'EACCES',
|
||||||
|
remede: { geste: 'enregistrerSous' },
|
||||||
|
});
|
||||||
|
fs.lister = lister;
|
||||||
|
assert.deepEqual(trace, []);
|
||||||
|
assert.equal(fs.contenu('documents', raccourci), 'planche d’un autre événement');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('planche dans la séance : l’écriture refusée, enregistrer sous, le web (§ 8.6, décision 13)', () => {
|
describe('planche dans la séance : l’écriture refusée, enregistrer sous, le web (§ 8.6, décision 13)', () => {
|
||||||
test('un renommage refusé : PLANCHE_NON_ECRITE nomme le fichier, le dossier et la cause, offre « Enregistrer sous… », et le support de l’événement reste intact', async () => {
|
test('un renommage refusé : PLANCHE_NON_ECRITE nomme le fichier, le dossier et la cause, offre « Enregistrer sous… », et le support de l’événement reste intact', async () => {
|
||||||
const { fs, seance, trace } = await monter();
|
const { fs, seance, trace } = await monter();
|
||||||
|
|
@ -475,10 +566,10 @@ describe('planche dans la séance : l’écriture refusée, enregistrer sous, le
|
||||||
recus.push([nom, octets]);
|
recus.push([nom, octets]);
|
||||||
return nom;
|
return nom;
|
||||||
};
|
};
|
||||||
const attendu = nomDe({ papier: 'Lettre', imposition: 'pile' });
|
const attendu = nomDe({ papier: 'Lettre', imposition: 'pile', ordre: 'table' });
|
||||||
const rendu = await seance.produirePlanche({ imposition: 'pile', ordre: 'table' });
|
const rendu = await seance.produirePlanche({ imposition: 'pile', ordre: 'table' });
|
||||||
assert.deepEqual(rendu, { chemin: null, nom: attendu });
|
assert.deepEqual(rendu, { chemin: null, nom: attendu });
|
||||||
assert.match(attendu, /_Lettre_pile_/);
|
assert.match(attendu, /_Lettre_pile_table_/);
|
||||||
assert.deepEqual(trace, []);
|
assert.deepEqual(trace, []);
|
||||||
assert.equal(recus.length, 1);
|
assert.equal(recus.length, 1);
|
||||||
assert.equal(recus[0][0], attendu);
|
assert.equal(recus[0][0], attendu);
|
||||||
|
|
|
||||||
|
|
@ -73,12 +73,14 @@ const retenue = (champs = {}) =>
|
||||||
// Les opérations de la planche d'une séance, sur la police embarquée lue
|
// Les opérations de la planche d'une séance, sur la police embarquée lue
|
||||||
// du disque, la provenance d'épreuve et la version engendrée.
|
// du disque, la provenance d'épreuve et la version engendrée.
|
||||||
const planches = creerPlanches({ polices: async () => lirePolices(), provenance: 'test', version: VERSION.affichee });
|
const planches = creerPlanches({ polices: async () => lirePolices(), provenance: 'test', version: VERSION.affichee });
|
||||||
const fsInutile = Object.freeze({ nature: 'epreuve' });
|
// Un dossier de travail vide : le nom de la planche ne lit que son
|
||||||
|
// séparateur et, pour une réédition, ses entrées.
|
||||||
|
const fsVide = Object.freeze({ nature: 'epreuve', emplacements: async () => ({ separateur: '/' }), lister: async () => [] });
|
||||||
|
|
||||||
// L'annonce que la séance rendrait sur cette charge, ce papier et cette
|
// L'annonce que la séance rendrait sur cette charge, ce papier et cette
|
||||||
// demande : la lecture que la séance passe, sans système de fichiers lu.
|
// demande : la lecture que la séance passe, sur un dossier vide.
|
||||||
const annoncer = (charge, papier, demande = {}) =>
|
const annoncer = (charge, papier, demande = {}) =>
|
||||||
planches.preparer({ charge, base: 'lucioles', fs: fsInutile, racine: { id: 'documents', chemin: '/d' }, papier }, demande);
|
planches.preparer({ charge, base: 'lucioles', fs: fsVide, racine: { id: 'documents', chemin: '/d' }, papier }, demande);
|
||||||
|
|
||||||
describe('les sources offertes (Q3)', () => {
|
describe('les sources offertes (Q3)', () => {
|
||||||
test('le retenu d’abord, puis la proposition demandée au plan quand elle existe ; rien sans l’un ni l’autre', () => {
|
test('le retenu d’abord, puis la proposition demandée au plan quand elle existe ; rien sans l’un ni l’autre', () => {
|
||||||
|
|
@ -206,7 +208,7 @@ describe('les phrases de l’annonce (§ 11.2, § 11.5, § 11.8)', () => {
|
||||||
assert.equal(produireBloque(null), false);
|
assert.equal(produireBloque(null), false);
|
||||||
assert.equal(produireBloque({ ...vide, refus: null }), false);
|
assert.equal(produireBloque({ ...vide, refus: null }), false);
|
||||||
assert.equal(produireBloque({ ...vide, refus: { code: 'PAPIER_REQUIS', details: {} } }), false);
|
assert.equal(produireBloque({ ...vide, refus: { code: 'PAPIER_REQUIS', details: {} } }), false);
|
||||||
for (const code of ['PLANCHE_SANS_PLACEMENT', 'RETENU_FAUTIF', 'PLANCHE_DERIVE', 'PLANCHE_TROP_DE_TOURS', 'PLANCHE_NOM_TROP_LONG']) {
|
for (const code of ['PLANCHE_SANS_PLACEMENT', 'RETENU_FAUTIF', 'PROPOSITION_FAUTIVE', 'PLANCHE_DERIVE', 'PLANCHE_TROP_DE_TOURS', 'PLANCHE_NOM_TROP_LONG']) {
|
||||||
assert.equal(produireBloque({ ...vide, refus: { code, details: {} } }), true, code);
|
assert.equal(produireBloque({ ...vide, refus: { code, details: {} } }), true, code);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,8 @@ function modele(n, { brouillon = false, reedition = false } = {}) {
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
badges,
|
badges,
|
||||||
|
source: 'retenu',
|
||||||
|
proposition: null,
|
||||||
evenement: EVENEMENT,
|
evenement: EVENEMENT,
|
||||||
horodatage: HORODATAGE,
|
horodatage: HORODATAGE,
|
||||||
brouillon,
|
brouillon,
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,9 @@
|
||||||
//
|
//
|
||||||
// La série surveillée borne le coût (§ 14.4) : trois productions sur de
|
// La série surveillée borne le coût (§ 14.4) : trois productions sur de
|
||||||
// petits modèles écrits à la main, que toutes les épreuves du fichier
|
// petits modèles écrits à la main, que toutes les épreuves du fichier
|
||||||
// lisent — A4, cinq badges, feuille par feuille, brouillon, horodatage
|
// lisent — A4, cinq badges, feuille par feuille, brouillon tiré de la
|
||||||
// connu, produite deux fois ; Lettre, trois badges, en pile, retenu,
|
// proposition n° 7, horodatage connu, produite deux fois ; Lettre, trois
|
||||||
|
// badges, en pile, retenu,
|
||||||
// horodatage inconnu, sous un nom d'événement plus large que la page, dont
|
// 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
|
// 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
|
// tient seul, puis coupe un mot qui ne tient pas seul. Les lignes de
|
||||||
|
|
@ -72,9 +73,11 @@ function badge(participant, nom, { appartenance = null, tours = 2, horodatage =
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Un modèle de planche écrit à la main. */
|
/** Un modèle de planche écrit à la main. */
|
||||||
function modele(badges, { horodatage = HORODATAGE, brouillon = false, exclus = 0, evenement = EVENEMENT } = {}) {
|
function modele(badges, { horodatage = HORODATAGE, brouillon = false, exclus = 0, evenement = EVENEMENT, proposition = null } = {}) {
|
||||||
return {
|
return {
|
||||||
badges,
|
badges,
|
||||||
|
source: proposition === null ? 'retenu' : 'proposition',
|
||||||
|
proposition,
|
||||||
evenement,
|
evenement,
|
||||||
horodatage,
|
horodatage,
|
||||||
brouillon,
|
brouillon,
|
||||||
|
|
@ -88,7 +91,7 @@ function modele(badges, { horodatage = HORODATAGE, brouillon = false, exclus = 0
|
||||||
const NOMS_A4 = ['Iris Ombrelle', 'Ôjgq Æ Grisaille', 'Théo Pervenche', 'Ondine Lacasse', 'Merle Bleuet'];
|
const NOMS_A4 = ['Iris Ombrelle', 'Ôjgq Æ Grisaille', 'Théo Pervenche', 'Ondine Lacasse', 'Merle Bleuet'];
|
||||||
const MODELE_A4 = modele(
|
const MODELE_A4 = modele(
|
||||||
NOMS_A4.map((nom, i) => badge(i + 1, nom, { appartenance: i % 2 === 0 ? 'Club des Merles' : null })),
|
NOMS_A4.map((nom, i) => badge(i + 1, nom, { appartenance: i % 2 === 0 ? 'Club des Merles' : null })),
|
||||||
{ brouillon: true, exclus: 1 },
|
{ brouillon: true, exclus: 1, proposition: 7 },
|
||||||
);
|
);
|
||||||
const NOMS_LETTRE = ['Aube Sarcelle', 'Côme Pervenche', 'Ysée Lacasse'];
|
const NOMS_LETTRE = ['Aube Sarcelle', 'Côme Pervenche', 'Ysée Lacasse'];
|
||||||
const MODELE_LETTRE = modele(
|
const MODELE_LETTRE = modele(
|
||||||
|
|
@ -448,7 +451,7 @@ describe('produirePlanche : la feuille de contrôle, page 1 (§ 11.3)', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("les lignes de contrôle se lisent dans la couche de texte, dans l'ordre et entières : comptes, horodatage, version, papier", () => {
|
test("les lignes de contrôle se lisent dans la couche de texte, dans l'ordre et entières : comptes, source, horodatage, version, papier", () => {
|
||||||
for (const { papier, imposition, lu, modele: m } of PRODUCTIONS) {
|
for (const { papier, imposition, lu, modele: m } of PRODUCTIONS) {
|
||||||
const lignes = textesControle(m, { papier, imposition, planches: nombrePlanches(m.badges.length, papier) });
|
const lignes = textesControle(m, { papier, imposition, planches: nombrePlanches(m.badges.length, papier) });
|
||||||
recomposer(fragmentsDeControle(lu.pages[0]), lignes);
|
recomposer(fragmentsDeControle(lu.pages[0]), lignes);
|
||||||
|
|
@ -457,6 +460,9 @@ describe('produirePlanche : la feuille de contrôle, page 1 (§ 11.3)', () => {
|
||||||
assert.ok(lusA4.includes(libelle('planche.controle.comptes', MODELE_A4.comptes)));
|
assert.ok(lusA4.includes(libelle('planche.controle.comptes', MODELE_A4.comptes)));
|
||||||
assert.ok(lusA4.includes(libelle('planche.pied.horodatage', { horodatage: HORODATAGE })));
|
assert.ok(lusA4.includes(libelle('planche.pied.horodatage', { horodatage: HORODATAGE })));
|
||||||
assert.ok(LETTRE.pages[0].textes.some(({ texte }) => texte === libelle('planche.pied.horodatageInconnu')));
|
assert.ok(LETTRE.pages[0].textes.some(({ texte }) => texte === libelle('planche.pied.horodatageInconnu')));
|
||||||
|
// La source se nomme : la proposition imprimée par son numéro, ou le retenu.
|
||||||
|
assert.ok(lusA4.includes('Source : la proposition n° 7, qui n\u{2019}est pas le placement retenu'), lusA4.join(' | '));
|
||||||
|
assert.ok(LETTRE.pages[0].textes.some(({ texte }) => texte === 'Source : le placement retenu'));
|
||||||
});
|
});
|
||||||
|
|
||||||
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", () => {
|
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", () => {
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,10 @@
|
||||||
// tiendrait que pour l'état laisserait déborder le journal, le fichier
|
// tiendrait que pour l'état laisserait déborder le journal, le fichier
|
||||||
// précédent et leurs fichiers d'écriture. Le nom de la planche, posé à la
|
// précédent et leurs fichiers d'écriture. Le nom de la planche, posé à la
|
||||||
// racine du dossier de travail, tient sous la même borne : son plus long
|
// racine du dossier de travail, tient sous la même borne : son plus long
|
||||||
// suffixe, prolongé de SUFFIXE_ECRITURE, ne dépasse pas la partie fixe de
|
// suffixe sans marque, prolongé de SUFFIXE_ECRITURE, ne dépasse pas la partie
|
||||||
// longueurRelativeMax.
|
// fixe de longueurRelativeMax, et un nom marqué raccourcit sa base dans le
|
||||||
|
// dossier qui le reçoit, suivie d'un point qui le distingue de tout nom
|
||||||
|
// entier.
|
||||||
import { PAPIERS } from '../geometrie/papiers.js';
|
import { PAPIERS } from '../geometrie/papiers.js';
|
||||||
import { ErreurStockage } from './erreurs.js';
|
import { ErreurStockage } from './erreurs.js';
|
||||||
|
|
||||||
|
|
@ -305,25 +307,126 @@ export function dossierCorbeille(horodatage, existants) {
|
||||||
// écrit.
|
// écrit.
|
||||||
const IMPOSITIONS = Object.freeze(['feuille', 'pile']);
|
const IMPOSITIONS = Object.freeze(['feuille', 'pile']);
|
||||||
|
|
||||||
|
// Les ordres de la pile (§ 11.6) : l'ordre par table se marque dans le nom,
|
||||||
|
// l'ordre alphabétique, celui par défaut, ne s'y écrit pas.
|
||||||
|
const ORDRES = Object.freeze(['alphabetique', 'table']);
|
||||||
|
|
||||||
// Une réédition compte au plus REEDITION_MAX badges, quatre chiffres : le
|
// Une réédition compte au plus REEDITION_MAX badges, quatre chiffres : le
|
||||||
// plus long suffixe de la planche, « .badges-9999_<papier>_feuille_<date>.pdf »,
|
// plus long suffixe sans marque de la planche,
|
||||||
// en tient compte sous la borne du chemin.
|
// « .badges-9999_<papier>_feuille_<date>.pdf », en tient compte sous la borne
|
||||||
|
// du chemin.
|
||||||
const REEDITION_MAX = 9999;
|
const REEDITION_MAX = 9999;
|
||||||
|
|
||||||
// Ce qui tient lieu de date et d'heure quand le placement n'en porte pas.
|
// Ce qui tient lieu de date et d'heure quand le placement n'en porte pas.
|
||||||
const HORODATAGE_INCONNU = 'horodatage-inconnu';
|
const HORODATAGE_INCONNU = 'horodatage-inconnu';
|
||||||
|
|
||||||
|
// Ce qui suit une base raccourcie dans le nom de la planche : un point. Une
|
||||||
|
// base que rend deriverBase ne finit jamais par un point, si bien qu'un nom
|
||||||
|
// raccourci ne se confond avec aucun nom entier : la planche entière d'une
|
||||||
|
// base qui tient ne remplace jamais la planche raccourcie d'une autre.
|
||||||
|
const MARQUE_COUPE = '.';
|
||||||
|
|
||||||
|
// Le dossier de travail où la planche se dépose, contrôlé : null, ou sa
|
||||||
|
// racine, son séparateur et les noms qu'il porte déjà.
|
||||||
|
function dossierLu(dossier) {
|
||||||
|
if (dossier === null) return null;
|
||||||
|
const forme =
|
||||||
|
typeof dossier === 'object' &&
|
||||||
|
typeof dossier.racine === 'string' &&
|
||||||
|
(dossier.separateur === '\\' || dossier.separateur === '/') &&
|
||||||
|
typeof dossier.existants?.[Symbol.iterator] === 'function';
|
||||||
|
if (!forme) throw new TypeError('nomPlanche : dossier {racine, separateur, existants} ou null attendu');
|
||||||
|
return dossier;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Les options de nomPlanche contrôlées, et ce qu'il en tire : le dossier lu,
|
||||||
|
// le nom de rang donné, et si le nom se choisit parmi les entrées du dossier
|
||||||
|
// — une réédition, ou une base que la borne raccourcit dès le premier rang.
|
||||||
|
// Lève comme nomPlanche.
|
||||||
|
function composer(
|
||||||
|
base,
|
||||||
|
{ papier, imposition, horodatage, reedition = null, proposition = null, ordre = 'alphabetique', dossier = null } = {},
|
||||||
|
) {
|
||||||
|
if (typeof base !== 'string' || base === '') throw new TypeError(`nomPlanche : base attendue, reçu ${JSON.stringify(base)}`);
|
||||||
|
if (typeof papier !== 'string' || !Object.hasOwn(PAPIERS, papier)) {
|
||||||
|
throw new TypeError(`nomPlanche : papier inconnu ${JSON.stringify(papier)}`);
|
||||||
|
}
|
||||||
|
if (!IMPOSITIONS.includes(imposition)) throw new TypeError(`nomPlanche : imposition inconnue ${JSON.stringify(imposition)}`);
|
||||||
|
if (proposition !== null && !(Number.isSafeInteger(proposition) && proposition >= 1)) {
|
||||||
|
throw new TypeError(`nomPlanche : identifiant de proposition attendu, reçu ${JSON.stringify(proposition)}`);
|
||||||
|
}
|
||||||
|
if (!ORDRES.includes(ordre)) throw new TypeError(`nomPlanche : ordre inconnu ${JSON.stringify(ordre)}`);
|
||||||
|
const leDossier = dossierLu(dossier);
|
||||||
|
const date = horodatage === null ? HORODATAGE_INCONNU : jourEtHeure(horodatage);
|
||||||
|
let compte = '';
|
||||||
|
if (reedition !== null) {
|
||||||
|
if (!Array.isArray(reedition?.ids)) throw new TypeError('nomPlanche : réédition sans tableau ids');
|
||||||
|
const k = reedition.ids.length;
|
||||||
|
if (k < 1 || k > REEDITION_MAX) throw new RangeError(`nomPlanche : réédition de ${k} badges, de 1 à ${REEDITION_MAX} admis`);
|
||||||
|
compte = `-${k}`;
|
||||||
|
}
|
||||||
|
const source = proposition === null ? '' : `-p${proposition}`;
|
||||||
|
const marqueOrdre = ordre === 'table' ? '_table' : '';
|
||||||
|
const suffixe = (rang) =>
|
||||||
|
`.badges${source}${compte}_${papier}_${imposition}${marqueOrdre}_${date}${rang === 1 ? '' : `_${rang}`}.pdf`;
|
||||||
|
if (leDossier === null) return { leDossier, nomDeRang: (rang) => base + suffixe(rang), exigeListe: false };
|
||||||
|
|
||||||
|
// Le nom de rang donné : la base entière quand le chemin complet, en
|
||||||
|
// écriture atomique, tient sous la borne ; sinon la plus longue tête de la
|
||||||
|
// base que tenir rend, suivie de MARQUE_COUPE ; la base entière quand même
|
||||||
|
// un caractère n'y tient pas.
|
||||||
|
const { racine, separateur } = leDossier;
|
||||||
|
const unitesRacine = racine.endsWith(separateur) ? racine.length - 1 : racine.length;
|
||||||
|
const nomDeRang = (rang) => {
|
||||||
|
const fin = suffixe(rang);
|
||||||
|
const libres = LONGUEUR_CHEMIN_MAX - (unitesRacine + 1) - SUFFIXE_ECRITURE.length - fin.length;
|
||||||
|
if (base.length <= libres) return base + fin;
|
||||||
|
const tete = tenir(base, libres - MARQUE_COUPE.length);
|
||||||
|
return tete === null ? base + fin : tete + MARQUE_COUPE + fin;
|
||||||
|
};
|
||||||
|
return { leDossier, nomDeRang, exigeListe: reedition !== null || nomDeRang(1) !== base + suffixe(1) };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nom du fichier de la planche de badges (§ 11.6), posé à la racine du
|
* Nom du fichier de la planche de badges (§ 11.6), posé à la racine du
|
||||||
* dossier de travail : <base>.badges[-<k>]_<papier>_<imposition>_<date>.pdf,
|
* dossier de travail :
|
||||||
* où k est le nombre de badges d'une réédition, papier une clé de PAPIERS,
|
* <base>.badges[-p<id>][-<k>]_<papier>_<imposition>[_table]_<date>[_<n>].pdf,
|
||||||
* imposition feuille ou pile, et date le jour et l'heure de l'horodatage du
|
* où id est l'identifiant de la proposition imprimée — rien pour le
|
||||||
* placement, AAAA-MM-JJ_HH-MM-SS, décalage retiré — « horodatage-inconnu »
|
* retenu —, k le nombre de badges d'une réédition, papier une clé de
|
||||||
* quand il manque. Le nom ne porte aucun deux-points. Même base, même
|
* PAPIERS, imposition feuille ou pile, « _table » l'ordre par table — rien
|
||||||
* papier, même imposition, même placement : même nom. Le plus long suffixe,
|
* pour l'ordre alphabétique —, date le jour et l'heure de l'horodatage du
|
||||||
* 51 unités, prolongé de SUFFIXE_ECRITURE, tient sous la partie fixe de
|
* placement, AAAA-MM-JJ_HH-MM-SS, décalage retiré, « horodatage-inconnu »
|
||||||
* longueurRelativeMax : la base d'un événement, que deriverBase borne, laisse
|
* quand il manque, et n le rang d'un nom déjà pris que le dossier départage.
|
||||||
* donc place à sa planche.
|
* Le nom ne porte aucun deux-points.
|
||||||
|
*
|
||||||
|
* Le nom est injectif sur ce qui change la pile : deux propositions d'une
|
||||||
|
* même génération, qui partagent leur horodatage, ou les deux ordres d'un
|
||||||
|
* même placement donnent deux noms. Même base, même source, même papier,
|
||||||
|
* même imposition, même ordre, même placement : même nom, et la planche
|
||||||
|
* entière d'un placement remplace la sienne quand sa base tient entière.
|
||||||
|
* Un nom qui se choisit parmi les entrées du dossier — celui d'une
|
||||||
|
* réédition, ou celui dont la base se raccourcit (nomPlancheExigeListe) — ne
|
||||||
|
* remplace jamais un fichier : quand le dossier porte déjà ce nom, comparé
|
||||||
|
* par la majuscule de sa NFC comme Windows compare les noms, il prend le
|
||||||
|
* premier rang libre, « _2 », « _3 »…, comme le dossier daté de la
|
||||||
|
* corbeille. Deux bases qui ne diffèrent qu'au-delà de la coupe, celle d'un
|
||||||
|
* événement et celle de sa copie, se raccourcissent en une même tête : la
|
||||||
|
* seconde planche prend un rang plutôt que de remplacer la première.
|
||||||
|
*
|
||||||
|
* La borne du chemin (§ 8.6) : le plus long suffixe sans marque, 51 unités,
|
||||||
|
* prolongé de SUFFIXE_ECRITURE, tient sous la partie fixe de
|
||||||
|
* longueurRelativeMax, et la base d'un événement, que deriverBase borne,
|
||||||
|
* laisse place à sa planche. Les marques et le rang l'allongent au-delà :
|
||||||
|
* quand le dossier est donné, la base du nom se raccourcit, sans couper une
|
||||||
|
* paire de substitution, espaces et points finaux retirés, sans laisser un
|
||||||
|
* nom réservé, et prend un point à sa suite, jusqu'à ce que racine +
|
||||||
|
* séparateur + nom + SUFFIXE_ECRITURE tienne en LONGUEUR_CHEMIN_MAX ; le
|
||||||
|
* suffixe reste entier. Aucune base que rend deriverBase ne finit par un
|
||||||
|
* point : un nom raccourci ne se confond avec aucun nom entier.
|
||||||
|
* Quand même un caractère de base ne tient pas, le nom garde sa base
|
||||||
|
* entière : l'écriture refusée par le support se dit, et « Enregistrer
|
||||||
|
* sous… » la dépose ailleurs. Sans dossier — sous web, où la planche se
|
||||||
|
* télécharge, et pour le dialogue d'enregistrement —, ni rang ni coupe.
|
||||||
*
|
*
|
||||||
* @param {string} base celle de l'événement, que rend deriverBase
|
* @param {string} base celle de l'événement, que rend deriverBase
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
|
|
@ -332,25 +435,44 @@ const HORODATAGE_INCONNU = 'horodatage-inconnu';
|
||||||
* @param {string|null} options.horodatage AAAA-MM-JJTHH:MM:SS±HH:MM, ou null
|
* @param {string|null} options.horodatage AAAA-MM-JJTHH:MM:SS±HH:MM, ou null
|
||||||
* @param {null|{ids: number[]}} [options.reedition] les badges réédités ;
|
* @param {null|{ids: number[]}} [options.reedition] les badges réédités ;
|
||||||
* null ou absent pour une planche entière
|
* null ou absent pour une planche entière
|
||||||
|
* @param {number|null} [options.proposition] l'identifiant de la
|
||||||
|
* proposition imprimée ; null ou absent pour le retenu
|
||||||
|
* @param {'alphabetique'|'table'} [options.ordre] 'alphabetique' par défaut
|
||||||
|
* @param {null|{racine: string, separateur: '\\'|'/', existants: Iterable<string>}} [options.dossier]
|
||||||
|
* le dossier de travail : sa racine, son séparateur, et les noms des
|
||||||
|
* entrées qu'il porte déjà à sa racine
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
* @throws {TypeError} une base vide ou autre chose qu'une chaîne, un papier
|
* @throws {TypeError} une base vide ou autre chose qu'une chaîne, un papier
|
||||||
* hors de PAPIERS, une imposition hors de feuille et pile, un
|
* hors de PAPIERS, une imposition hors de feuille et pile, un
|
||||||
* horodatage d'une autre forme, une réédition sans tableau ids
|
* horodatage d'une autre forme, une réédition sans tableau ids, une
|
||||||
|
* proposition qui n'est pas un entier ≥ 1, un ordre inconnu, un
|
||||||
|
* dossier d'une autre forme
|
||||||
* @throws {RangeError} une réédition de 0 badge ou de plus de 9 999
|
* @throws {RangeError} une réédition de 0 badge ou de plus de 9 999
|
||||||
*/
|
*/
|
||||||
export function nomPlanche(base, { papier, imposition, horodatage, reedition = null } = {}) {
|
export function nomPlanche(base, options) {
|
||||||
if (typeof base !== 'string' || base === '') throw new TypeError(`nomPlanche : base attendue, reçu ${JSON.stringify(base)}`);
|
const { leDossier, nomDeRang, exigeListe } = composer(base, options);
|
||||||
if (typeof papier !== 'string' || !Object.hasOwn(PAPIERS, papier)) {
|
if (!exigeListe) return nomDeRang(1);
|
||||||
throw new TypeError(`nomPlanche : papier inconnu ${JSON.stringify(papier)}`);
|
const pris = new Set(Array.from(leDossier.existants, cle));
|
||||||
|
let rang = 1;
|
||||||
|
let candidat = nomDeRang(rang);
|
||||||
|
while (pris.has(cle(candidat))) {
|
||||||
|
rang += 1;
|
||||||
|
candidat = nomDeRang(rang);
|
||||||
}
|
}
|
||||||
if (!IMPOSITIONS.includes(imposition)) throw new TypeError(`nomPlanche : imposition inconnue ${JSON.stringify(imposition)}`);
|
return candidat;
|
||||||
const date = horodatage === null ? HORODATAGE_INCONNU : jourEtHeure(horodatage);
|
}
|
||||||
let rang = '';
|
|
||||||
if (reedition !== null) {
|
/**
|
||||||
if (!Array.isArray(reedition?.ids)) throw new TypeError('nomPlanche : réédition sans tableau ids');
|
* Vrai quand le nom de la planche (nomPlanche), sous le dossier donné, se
|
||||||
const k = reedition.ids.length;
|
* choisit parmi les entrées du dossier : pour une réédition, ou quand la
|
||||||
if (k < 1 || k > REEDITION_MAX) throw new RangeError(`nomPlanche : réédition de ${k} badges, de 1 à ${REEDITION_MAX} admis`);
|
* borne raccourcit la base. Faux sans dossier. Les entrées du dossier n'y
|
||||||
rang = `-${k}`;
|
* sont pas lues : l'appelant liste le dossier seulement quand il le faut.
|
||||||
}
|
*
|
||||||
return `${base}.badges${rang}_${papier}_${imposition}_${date}.pdf`;
|
* @param {string} base
|
||||||
|
* @param {Object} options celles de nomPlanche
|
||||||
|
* @returns {boolean}
|
||||||
|
* @throws {TypeError|RangeError} comme nomPlanche
|
||||||
|
*/
|
||||||
|
export function nomPlancheExigeListe(base, options) {
|
||||||
|
return composer(base, options).exigeListe;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import {
|
||||||
dossierCorbeille,
|
dossierCorbeille,
|
||||||
longueurRelativeMax,
|
longueurRelativeMax,
|
||||||
nomPlanche,
|
nomPlanche,
|
||||||
|
nomPlancheExigeListe,
|
||||||
} from './noms.js';
|
} from './noms.js';
|
||||||
|
|
||||||
// Les deux dossiers de travail possibles (§ 8.6) : le dossier du produit dans
|
// Les deux dossiers de travail possibles (§ 8.6) : le dossier du produit dans
|
||||||
|
|
@ -1020,6 +1021,15 @@ describe('déterminisme et pureté (§ 14.7)', () => {
|
||||||
dossierCorbeille('2026-10-06T19:02:11-04:00', ['2026-10-06_19-02-11']),
|
dossierCorbeille('2026-10-06T19:02:11-04:00', ['2026-10-06_19-02-11']),
|
||||||
nomPlanche('Gala', { papier: 'A4', imposition: 'pile', horodatage: '2026-10-06T19:02:11-04:00', reedition: { ids: [4, 9] } }),
|
nomPlanche('Gala', { papier: 'A4', imposition: 'pile', horodatage: '2026-10-06T19:02:11-04:00', reedition: { ids: [4, 9] } }),
|
||||||
nomPlanche('Gala', { papier: 'A4', imposition: 'feuille', horodatage: null, reedition: null }),
|
nomPlanche('Gala', { papier: 'A4', imposition: 'feuille', horodatage: null, reedition: null }),
|
||||||
|
nomPlanche('Gala', {
|
||||||
|
papier: 'A4',
|
||||||
|
imposition: 'pile',
|
||||||
|
horodatage: '2026-10-06T19:02:11-04:00',
|
||||||
|
reedition: { ids: [4] },
|
||||||
|
proposition: 2,
|
||||||
|
ordre: 'table',
|
||||||
|
dossier: { ...DOSSIERS[0], existants: ['gala.BADGES-p2-1_A4_pile_table_2026-10-06_19-02-11.pdf'] },
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
assert.deepEqual(sansSourcesVariables(rendre), rendre());
|
assert.deepEqual(sansSourcesVariables(rendre), rendre());
|
||||||
});
|
});
|
||||||
|
|
@ -1110,6 +1120,173 @@ describe('nomPlanche : le nom du fichier de la planche (§ 11.6, § 8.6)', () =>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('une proposition porte « -p<id> » après « .badges », avant le compte d’une réédition ; le retenu, rien : son nom ne change pas', () => {
|
||||||
|
const nom = (options) => nomPlanche(BASE, { papier: 'A4', imposition: 'feuille', horodatage: HORODATAGE, reedition: null, ...options });
|
||||||
|
assert.equal(nom({ proposition: 2 }), `${BASE}.badges-p2_A4_feuille_2031-05-17_20-15-07.pdf`);
|
||||||
|
assert.equal(nom({ proposition: 2 ** 31 - 1 }), `${BASE}.badges-p2147483647_A4_feuille_2031-05-17_20-15-07.pdf`);
|
||||||
|
assert.equal(nom({ proposition: 2, reedition: { ids: [3, 8, 12] } }), `${BASE}.badges-p2-3_A4_feuille_2031-05-17_20-15-07.pdf`);
|
||||||
|
assert.equal(nom({ proposition: null }), `${BASE}.badges_A4_feuille_2031-05-17_20-15-07.pdf`);
|
||||||
|
assert.equal(nom({}), `${BASE}.badges_A4_feuille_2031-05-17_20-15-07.pdf`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deux propositions de la même génération, donc du même horodatage : deux noms ; le retenu au même horodatage, un troisième', () => {
|
||||||
|
const nom = (proposition) => nomPlanche(BASE, { papier: 'Lettre', imposition: 'pile', horodatage: HORODATAGE, reedition: null, proposition });
|
||||||
|
const noms = [nom(1), nom(2), nom(null)];
|
||||||
|
assert.equal(new Set(noms).size, 3, noms.join(' | '));
|
||||||
|
// Sans horodatage, les trois se distinguent encore.
|
||||||
|
const inconnus = [1, 2, null].map((proposition) => nomPlanche(BASE, { papier: 'A4', imposition: 'feuille', horodatage: null, proposition }));
|
||||||
|
assert.equal(new Set(inconnus).size, 3, inconnus.join(' | '));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('l’ordre par table porte « _table » après l’imposition ; l’ordre alphabétique, rien', () => {
|
||||||
|
const nom = (ordre) => nomPlanche(BASE, { papier: 'A4', imposition: 'pile', horodatage: HORODATAGE, reedition: null, ordre });
|
||||||
|
assert.equal(nom('table'), `${BASE}.badges_A4_pile_table_2031-05-17_20-15-07.pdf`);
|
||||||
|
assert.equal(nom('alphabetique'), `${BASE}.badges_A4_pile_2031-05-17_20-15-07.pdf`);
|
||||||
|
assert.equal(nom(undefined), `${BASE}.badges_A4_pile_2031-05-17_20-15-07.pdf`);
|
||||||
|
assert.equal(
|
||||||
|
nomPlanche(BASE, { papier: 'Lettre', imposition: 'feuille', horodatage: null, reedition: reeditionDe(2), proposition: 4, ordre: 'table' }),
|
||||||
|
`${BASE}.badges-p4-2_Lettre_feuille_table_horodatage-inconnu.pdf`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('une réédition ne remplace jamais un fichier du dossier : le premier rang libre, « _2 », « _3 »…, comparé sans égard à la casse ; une planche entière garde son nom', () => {
|
||||||
|
const dossier = (existants) => ({ ...DOSSIERS[0], existants });
|
||||||
|
const options = { papier: 'A4', imposition: 'feuille', horodatage: HORODATAGE, reedition: reeditionDe(3) };
|
||||||
|
const libre = `${BASE}.badges-3_A4_feuille_2031-05-17_20-15-07.pdf`;
|
||||||
|
const rang = (n) => libre.replace(/\.pdf$/, `_${n}.pdf`);
|
||||||
|
assert.equal(nomPlanche(BASE, { ...options, dossier: dossier([]) }), libre);
|
||||||
|
assert.equal(nomPlanche(BASE, { ...options, dossier: dossier([`${BASE}.gtt.json`, 'corbeille']) }), libre);
|
||||||
|
assert.equal(nomPlanche(BASE, { ...options, dossier: dossier([libre]) }), rang(2));
|
||||||
|
assert.equal(nomPlanche(BASE, { ...options, dossier: dossier([rang(2), libre]) }), rang(3));
|
||||||
|
assert.equal(nomPlanche(BASE, { ...options, dossier: dossier([libre.toUpperCase()]) }), rang(2));
|
||||||
|
// Un rang pris ne compte pas tant que le nom sans rang est libre.
|
||||||
|
assert.equal(nomPlanche(BASE, { ...options, dossier: dossier([rang(2)]) }), libre);
|
||||||
|
// Une liste figée passe, et n'est pas modifiée.
|
||||||
|
const figee = Object.freeze([libre]);
|
||||||
|
assert.equal(nomPlanche(BASE, { ...options, dossier: { ...DOSSIERS[0], existants: figee } }), rang(2));
|
||||||
|
assert.deepEqual([...figee], [libre]);
|
||||||
|
// La planche entière du même placement reprend son nom : même placement, même planche.
|
||||||
|
const entiere = `${BASE}.badges_A4_feuille_2031-05-17_20-15-07.pdf`;
|
||||||
|
assert.equal(nomPlanche(BASE, { ...options, reedition: null, dossier: dossier([entiere]) }), entiere);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('le plus long nom marqué tient sous 259 unités à côté d’une base à la borne : la base s’y raccourcit, le suffixe reste entier ; le nom sans marque ne change pas', () => {
|
||||||
|
for (const dossier of DOSSIERS) {
|
||||||
|
const base = deriverBase(lettres(400), { ...dossier, existantes: [] });
|
||||||
|
const options = { papier: 'Lettre', imposition: 'feuille', horodatage: HORODATAGE, reedition: reeditionDe(9999), proposition: 2 ** 31 - 1, ordre: 'table' };
|
||||||
|
const sansDossier = nomPlanche(base, options);
|
||||||
|
const suffixe = sansDossier.slice(base.length);
|
||||||
|
assert.equal(suffixe, '.badges-p2147483647-9999_Lettre_feuille_table_2031-05-17_20-15-07.pdf');
|
||||||
|
// Dix rééditions de suite dans le même dossier : chacune prend le rang
|
||||||
|
// libre suivant, la base raccourcie d'autant.
|
||||||
|
const pris = [];
|
||||||
|
for (let rang = 1; rang <= 10; rang += 1) {
|
||||||
|
const nom = nomPlanche(base, { ...options, dossier: { ...dossier, existants: pris } });
|
||||||
|
const attendu = rang === 1 ? suffixe : suffixe.replace(/\.pdf$/, `_${rang}.pdf`);
|
||||||
|
assert.ok(nom.endsWith(attendu), nom);
|
||||||
|
// La tête raccourcie finit par le point de la coupe.
|
||||||
|
const tete = nom.slice(0, nom.length - attendu.length);
|
||||||
|
assert.ok(tete.length > 1 && tete.endsWith('.') && base.startsWith(tete.slice(0, -1)), tete);
|
||||||
|
const chemin = `${dossier.racine}${dossier.separateur}${nom}.ecriture`;
|
||||||
|
assert.equal(chemin.length, LONGUEUR_CHEMIN_MAX, `${dossier.racine} : ${chemin.length} unités`);
|
||||||
|
pris.push(nom);
|
||||||
|
}
|
||||||
|
assert.equal(new Set(pris).size, 10);
|
||||||
|
// Le nom du retenu, dans l'ordre alphabétique, ne bouge pas.
|
||||||
|
const retenu = { papier: 'Lettre', imposition: 'feuille', horodatage: HORODATAGE, reedition: null };
|
||||||
|
assert.equal(nomPlanche(base, { ...retenu, dossier: { ...dossier, existants: [] } }), nomPlanche(base, retenu));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deux événements dont les bases ne diffèrent qu’au-delà de la coupe : la planche entière du second prend un rang plutôt que de remplacer celle du premier ; un nom raccourci ne se confond pas avec un nom entier', () => {
|
||||||
|
const options = { papier: 'Lettre', imposition: 'feuille', horodatage: HORODATAGE, reedition: null, proposition: 12, ordre: 'table' };
|
||||||
|
const suffixe = nomPlanche('b', options).slice(1);
|
||||||
|
assert.equal(suffixe, '.badges-p12_Lettre_feuille_table_2031-05-17_20-15-07.pdf');
|
||||||
|
for (const dossier of DOSSIERS) {
|
||||||
|
// L'original et sa copie : la copie ne diffère que par « (2) », que la coupe retire.
|
||||||
|
const nom = `Souper des Lucioles du printemps, \u{E9}dition sp\u{E9}ciale ${lettres(400)}`;
|
||||||
|
const original = derive(nom, dossier);
|
||||||
|
const copie = derive(nom, dossier, [original]);
|
||||||
|
assert.ok(copie.endsWith(' (2)') && copie.length === original.length, copie);
|
||||||
|
const sous = (existants) => ({ ...options, dossier: { ...dossier, existants } });
|
||||||
|
const premier = nomPlanche(original, sous([]));
|
||||||
|
assert.notEqual(premier, original + suffixe, 'la base se raccourcit');
|
||||||
|
assert.ok(`${dossier.racine}${dossier.separateur}${premier}.ecriture`.length <= LONGUEUR_CHEMIN_MAX, premier);
|
||||||
|
// Sans le premier fichier dans le dossier, la copie prendrait le même nom :
|
||||||
|
// listé, il lui laisse le rang suivant.
|
||||||
|
assert.equal(nomPlanche(copie, sous([])), premier);
|
||||||
|
const second = nomPlanche(copie, sous([`${original}.gtt.json`, `${copie}.gtt.json`, premier]));
|
||||||
|
// Le rang allonge le suffixe : la tête se raccourcit d'autant.
|
||||||
|
const auRang2 = suffixe.replace(/\.pdf$/, '_2.pdf');
|
||||||
|
assert.ok(second.endsWith(`.${auRang2}`) && premier.startsWith(second.slice(0, -auRang2.length - 1)), second);
|
||||||
|
assert.ok(`${dossier.racine}${dossier.separateur}${second}.ecriture`.length <= LONGUEUR_CHEMIN_MAX, second);
|
||||||
|
assert.equal(nomPlanche(copie, sous([premier.toUpperCase()])), second);
|
||||||
|
// La planche entière raccourcie ne remplace pas davantage la sienne.
|
||||||
|
assert.equal(nomPlanche(original, sous([premier])), second);
|
||||||
|
// Une base égale à la tête raccourcie se nomme entière, autrement que le nom raccourci.
|
||||||
|
const tete = premier.slice(0, premier.length - suffixe.length);
|
||||||
|
assert.ok(tete.endsWith('.'), tete);
|
||||||
|
const courte = derive(tete.slice(0, -1), dossier);
|
||||||
|
assert.equal(courte, tete.slice(0, -1));
|
||||||
|
const entier = nomPlanche(courte, sous([premier]));
|
||||||
|
assert.equal(entier, courte + suffixe);
|
||||||
|
assert.notEqual(entier.toUpperCase(), premier.toUpperCase());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('le nom se choisit parmi les entrées du dossier pour une réédition, ou pour une base qui se raccourcit ; jamais sans dossier', () => {
|
||||||
|
const dossier = { ...DOSSIERS[0], existants: [] };
|
||||||
|
const options = { papier: 'Lettre', imposition: 'feuille', horodatage: HORODATAGE, reedition: null, proposition: 12, ordre: 'table' };
|
||||||
|
const longue = derive(lettres(400), DOSSIERS[0]);
|
||||||
|
assert.equal(nomPlancheExigeListe(BASE, { ...options, dossier }), false);
|
||||||
|
assert.equal(nomPlancheExigeListe(BASE, { ...options, reedition: reeditionDe(2), dossier }), true);
|
||||||
|
assert.equal(nomPlancheExigeListe(longue, { ...options, dossier }), true);
|
||||||
|
assert.equal(nomPlancheExigeListe(longue, { papier: 'A4', imposition: 'feuille', horodatage: HORODATAGE, dossier }), false);
|
||||||
|
assert.equal(nomPlancheExigeListe(longue, options), false);
|
||||||
|
assert.equal(nomPlancheExigeListe(longue, { ...options, reedition: reeditionDe(2) }), false);
|
||||||
|
assert.throws(() => nomPlancheExigeListe(longue, { ...options, papier: 'A5', dossier }), TypeError);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('la coupe rend un nom bien formé : ni demi-paire, ni espace ou point avant le point de la coupe, ni nom réservé, et le chemin tient sous 259 unités', () => {
|
||||||
|
const options = { papier: 'Lettre', imposition: 'feuille', horodatage: HORODATAGE, reedition: null, proposition: 12, ordre: 'table' };
|
||||||
|
const suffixe = '.badges-p12_Lettre_feuille_table_2031-05-17_20-15-07.pdf';
|
||||||
|
// Le dossier dont la racine laisse exactement libres unités à la tête du nom :
|
||||||
|
// racine, séparateur, tête, point de la coupe, suffixe, « .ecriture ».
|
||||||
|
const dossierPour = (libres) => dossierDeLongueur(259 - 1 - libres - 1 - suffixe.length - '.ecriture'.length);
|
||||||
|
const debut = (n) => `Gala ${lettres(n - 5)}`;
|
||||||
|
const L = 40;
|
||||||
|
const cas = [
|
||||||
|
['une paire coupée en son milieu', L, `${debut(L - 1)}\u{1F389}\u{1F389}x`, debut(L - 1)],
|
||||||
|
['une coupe sur une espace', L, `${debut(L - 1)} f\u{EA}te`, debut(L - 1)],
|
||||||
|
['une coupe sur un point', L, `${debut(L - 1)}.f\u{EA}te`, debut(L - 1)],
|
||||||
|
['une coupe sur une espace et un point', L, `${debut(L - 2)} .f\u{EA}t`, debut(L - 2)],
|
||||||
|
['une coupe qui laisserait CON', 3, 'CON soir', 'CO'],
|
||||||
|
['une coupe qui laisserait COM1', 4, 'COM1 soi', 'COM'],
|
||||||
|
];
|
||||||
|
for (const [quoi, libres, base, attendue] of cas) {
|
||||||
|
const dossier = dossierPour(libres);
|
||||||
|
assert.equal(derive(base, dossier), base, `${quoi} : la base se dérive telle quelle`);
|
||||||
|
const nom = nomPlanche(base, { ...options, dossier: { ...dossier, existants: [] } });
|
||||||
|
assert.equal(nom, `${attendue}.${suffixe}`, quoi);
|
||||||
|
const tete = nom.slice(0, nom.length - suffixe.length - 1);
|
||||||
|
assert.ok(!/[\ud800-\udfff]/u.test(tete), `${quoi} : demi-paire dans ${JSON.stringify(tete)}`);
|
||||||
|
assert.ok(!/[ .]$/.test(tete), `${quoi} : ${JSON.stringify(tete)} finit par une espace ou un point`);
|
||||||
|
assert.equal(estReserve(nom), false, `${quoi} : ${nom}`);
|
||||||
|
assert.ok(`${dossier.racine}${dossier.separateur}${nom}.ecriture`.length <= LONGUEUR_CHEMIN_MAX, quoi);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('une base qui ne laisse rien à couper sous une racine trop longue garde son nom entier : l’écriture refusée offrira « Enregistrer sous… »', () => {
|
||||||
|
// La racine la plus longue où « Gala » se dérive encore.
|
||||||
|
const dossier = dossierDeLongueur(LONGUEUR_CHEMIN_MAX - 1 - longueurRelativeMax('Gala'));
|
||||||
|
assert.equal(derive('Gala', dossier), 'Gala');
|
||||||
|
const options = { papier: 'Lettre', imposition: 'feuille', horodatage: HORODATAGE, reedition: reeditionDe(9999), proposition: 2 ** 31 - 1, ordre: 'table' };
|
||||||
|
assert.equal(nomPlanche('Gala', { ...options, dossier: { ...dossier, existants: [] } }), nomPlanche('Gala', options));
|
||||||
|
// Un nom qui tient ne se raccourcit pas.
|
||||||
|
const court = { papier: 'A4', imposition: 'feuille', horodatage: HORODATAGE, proposition: 3 };
|
||||||
|
assert.equal(nomPlanche('Gala', { ...court, dossier: { ...dossier, existants: [] } }), 'Gala.badges-p3_A4_feuille_2031-05-17_20-15-07.pdf');
|
||||||
|
});
|
||||||
|
|
||||||
test('un domaine hors contrat lève sans rien rendre : base, papier, imposition ou horodatage, TypeError ; réédition vide, de plus de 9 999 badges ou d’une autre forme, RangeError ou TypeError', () => {
|
test('un domaine hors contrat lève sans rien rendre : base, papier, imposition ou horodatage, TypeError ; réédition vide, de plus de 9 999 badges ou d’une autre forme, RangeError ou TypeError', () => {
|
||||||
const valides = { papier: 'A4', imposition: 'feuille', horodatage: HORODATAGE, reedition: null };
|
const valides = { papier: 'A4', imposition: 'feuille', horodatage: HORODATAGE, reedition: null };
|
||||||
const fautes = [
|
const fautes = [
|
||||||
|
|
@ -1127,6 +1304,15 @@ describe('nomPlanche : le nom du fichier de la planche (§ 11.6, § 8.6)', () =>
|
||||||
[TypeError, BASE, { ...valides, reedition: { ids: 'abc' } }],
|
[TypeError, BASE, { ...valides, reedition: { ids: 'abc' } }],
|
||||||
[RangeError, BASE, { ...valides, reedition: { ids: [] } }],
|
[RangeError, BASE, { ...valides, reedition: { ids: [] } }],
|
||||||
[RangeError, BASE, { ...valides, reedition: reeditionDe(10000) }],
|
[RangeError, BASE, { ...valides, reedition: reeditionDe(10000) }],
|
||||||
|
[TypeError, BASE, { ...valides, proposition: 0 }],
|
||||||
|
[TypeError, BASE, { ...valides, proposition: 1.5 }],
|
||||||
|
[TypeError, BASE, { ...valides, proposition: '2' }],
|
||||||
|
[TypeError, BASE, { ...valides, ordre: 'hasard' }],
|
||||||
|
[TypeError, BASE, { ...valides, ordre: null }],
|
||||||
|
[TypeError, BASE, { ...valides, dossier: { racine: 'E:\\data', separateur: '\\' } }],
|
||||||
|
[TypeError, BASE, { ...valides, dossier: { racine: 'E:\\data', separateur: ':', existants: [] } }],
|
||||||
|
[TypeError, BASE, { ...valides, dossier: { racine: 42, separateur: '/', existants: [] } }],
|
||||||
|
[TypeError, BASE, { ...valides, dossier: 'E:\\data' }],
|
||||||
];
|
];
|
||||||
for (const [type, base, options] of fautes) {
|
for (const [type, base, options] of fautes) {
|
||||||
assert.throws(() => nomPlanche(base, options), type, `${JSON.stringify(base)} ${JSON.stringify(options).slice(0, 120)}`);
|
assert.throws(() => nomPlanche(base, options), type, `${JSON.stringify(base)} ${JSON.stringify(options).slice(0, 120)}`);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue