[ADD] interface: event list, open event, participants, import, plan
The working screens of an evening. The home screen lists the events of
the working folder, asks its question, loads demos, copies, renames,
imports and deletes, and shows what the trash holds. An open event frames
its mode, offers Modifier, Débloquer and a confirmed lock takeover, and
shows its warnings. Participants get a grid, a form and a file; an import
goes through a preview and a report; tables, the default and tours have
their screens; the plan draws tables, chairs, empty seats and names.
Checked: 2342 node, 356 browser, 60 node-long; the Electron start check.
--- FR ---
[ADD] interface : liste, événement, participants, import, tables, plan
Les écrans de travail d'une soirée. L'accueil liste les événements du
dossier de travail, pose sa question, charge les démonstrations, copie,
renomme, importe et supprime, et montre ce que tient la corbeille. Un
événement ouvert encadre son mode, offre Modifier, Débloquer et une
reprise de verrou confirmée, et montre ses avertissements. Les
participants ont une grille, un formulaire et une fiche ; un import passe
par un aperçu et un rapport ; tables, défaut et tours ont leurs écrans ;
le plan dessine tables, chaises, sièges vides et noms.
Vérifié : 2342 node, 356 navigateur, 60 node-long ; démarrage d'Electron.
Assisted-by: Claude Opus 5.5
2026-10-07 08:12:27 -04:00
// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// L'événement ouvert dans un vrai moteur de rendu (§ 2.1, § 8.3 à § 8.9,
// § 9, § 14.5) : aucune section n'en exige une autre ; la place fixe de
// « Modifier », de « Débloquer » et de « Revenir en lecture », plein écran
// compris ; le cadre de mode et son libellé ; l'inactivité ; l'état bloqué
// et son déblocage confirmé en place ; les états du plan ; défaire et
// refaire ; les annonces d'ouverture, du journal et de la dérive ; le verrou
// d'une autre séance et sa reprise ; le support rompu ; le format plus
// récent. Chaque épreuve monte App par monterApp, sur le système de fichiers
// simulé, au cadre de 1 366 × 700, et lit le rendu : la boîte, l'élément
// qu'atteint un point, le style calculé, le texte — jamais une classe.
//
// Les éléments s'atteignent par leurs attributs data-… : data-place-fixe, la
// place fixe du cadre ; data-etat, un état du plan de l'en-tête ;
// data-verrou, le verrou d'une autre séance ; data-support-rompu, le message
// du support rompu. Le texte attendu vient de la même traduction que celle
// de l'application ; les noms d'épreuve sont inventés.
import { afterEach , beforeEach , describe , expect , test } from 'vitest' ;
import { flushSync } from 'svelte' ;
import { TABLES _APPLICATION , libelle } from '../../application/libelles.js' ;
import { INACTIVITE _MS } from '../../application/seance.js' ;
import { creerTraduction } from '../../application/traduction.js' ;
import { apercevoir } from '../../csv/apercu.js' ;
import { FLUX , creerPcg32 } from '../../demo/prng.js' ;
import { rechercher } from '../../moteur/recherche.js' ;
import { serialiser } from '../../stockage/canonique.js' ;
import { FORMAT , analyser , configurationDepuisCharge , etatDeduit } from '../../stockage/document.js' ;
import { lireJournal } from '../../stockage/journal.js' ;
import { SUFFIXES } from '../../stockage/noms.js' ;
import { VERSION } from '../../version.genere.js' ;
[FIX] event: page never scrolls, broken-support remedies always shown
At 1366 x 700 an open event that shows warnings no longer scrolls the page:
the warnings scroll within their zone and the plan keeps at least 75 % of
its height. A broken support never shrinks, so its three remedies stay in
view, the only way to write the event elsewhere (spec 8.6). Tests now
prove the working-folder path and duplicate nodes are visible, the guard
of « Remplacer » in import, and which header a refused re-alignment shows.
Checked: 2692 node, 499 browser tests, from the index alone.
--- FR ---
[FIX] événement : sans défilement, remèdes du support rompu visibles
À 1366 x 700, un événement ouvert qui montre des avertissements ne fait
plus défiler la page : les avertissements défilent dans leur zone et le
plan garde au moins 75 % de sa hauteur. Un support rompu ne rétrécit
jamais : ses trois remèdes, seul moyen d'écrire l'événement ailleurs
(§ 8.6), restent visibles. Les épreuves prouvent que le chemin et les
doublons du dossier sont visibles, la garde de « Remplacer » à l'import, et
l'en-tête que montre un réalignement refusé. Vérifié : 2692 node, 499.
Assisted-by: Claude Opus 5.5
2026-10-07 15:05:59 -04:00
import { fixerCadre , molette } from '../../../test/navigateur/entrees.js' ;
[ADD] interface: event list, open event, participants, import, plan
The working screens of an evening. The home screen lists the events of
the working folder, asks its question, loads demos, copies, renames,
imports and deletes, and shows what the trash holds. An open event frames
its mode, offers Modifier, Débloquer and a confirmed lock takeover, and
shows its warnings. Participants get a grid, a form and a file; an import
goes through a preview and a report; tables, the default and tours have
their screens; the plan draws tables, chairs, empty seats and names.
Checked: 2342 node, 356 browser, 60 node-long; the Electron start check.
--- FR ---
[ADD] interface : liste, événement, participants, import, tables, plan
Les écrans de travail d'une soirée. L'accueil liste les événements du
dossier de travail, pose sa question, charge les démonstrations, copie,
renomme, importe et supprime, et montre ce que tient la corbeille. Un
événement ouvert encadre son mode, offre Modifier, Débloquer et une
reprise de verrou confirmée, et montre ses avertissements. Les
participants ont une grille, un formulaire et une fiche ; un import passe
par un aperçu et un rapport ; tables, défaut et tours ont leurs écrans ;
le plan dessine tables, chaises, sièges vides et noms.
Vérifié : 2342 node, 356 navigateur, 60 node-long ; démarrage d'Electron.
Assisted-by: Claude Opus 5.5
2026-10-07 08:12:27 -04:00
import { monterApp } from '../../../test/navigateur/montage.js' ;
import { visible } from '../../../test/navigateur/rendu.js' ;
import { TABLES _INTERFACE } from '../libelles.js' ;
import { CONFIRMATION _DEBLOCAGE , DELAI _CONFIRMATION _MS } from '../primitives/commande.js' ;
import { SECTIONS } from './sections.js' ;
const { t } = creerTraduction ( [ ... TABLES _APPLICATION , ... TABLES _INTERFACE ] ) ;
const NOMS = [ 'Ombrelle' , 'Pervenche' , 'Lacasse' , 'Grisaille' , 'Merlette' , 'Fauvelle' ] ;
const EVENEMENT = { nom : 'Club des Merles' , date : null , siegesParDefaut : 3 , tours : 2 } ;
const DOCUMENTS = 'C:\\Users\\Exemple\\Documents\\Gestion table tournante Libre' ;
// Les montages de l'épreuve, démontés après elle ; le collecteur d'erreurs
// n'a rien reçu pendant l'épreuve.
let montes = [ ] ;
async function monter ( options ) {
const m = await monterApp ( options ) ;
montes . push ( m ) ;
return m ;
}
beforeEach ( async ( ) => {
await fixerCadre ( 1366 , 700 ) ;
} ) ;
afterEach ( ( ) => {
const erreurs = montes . flatMap ( ( m ) => m . erreurs ( ) ) ;
for ( const m of montes . reverse ( ) ) m . demonter ( ) ;
montes = [ ] ;
window . scrollTo ( 0 , 0 ) ;
expect ( erreurs , 'erreurs reçues par le collecteur' ) . toEqual ( [ ] ) ;
} ) ;
// --- Les lectures du rendu ----------------------------------------------------
const dans = ( m , selecteur ) => m . racine . querySelector ( selecteur ) ;
const tous = ( m , selecteur ) => [ ... m . racine . querySelectorAll ( selecteur ) ] ;
const cadre = ( m ) => dans ( m , '[data-mode]' ) ;
const fixe = ( m , nom ) => dans ( m , ` [data-place-fixe] [data-commande=" ${ nom } "] ` ) ;
const historique = ( m , nom ) => dans ( m , ` [data-mode] [data-historique] [data-commande=" ${ nom } "] ` ) ;
const boutonEtat = ( m , etat ) => dans ( m , ` [data-etat=" ${ etat } "] [data-commande="etat"] ` ) ;
const libelleCadre = ( m ) => dans ( m , '[data-mode-libelle="cadre"]' ) ;
const libelleBandeau = ( m ) => dans ( m , '[data-mode-libelle="bandeau"]' ) ;
const avertissement = ( m , code ) => dans ( m , ` [data-avertissement=" ${ code } "] ` ) ;
const ecran = ( m ) => dans ( m , '[data-ecran]' ) ;
// Le coin haut gauche de la boîte d'un élément.
const coin = ( element ) => {
const { left , top } = element . getBoundingClientRect ( ) ;
return [ left , top ] ;
} ;
// L'élément qu'atteint le centre de la boîte d'un élément.
const auCentre = ( element ) => {
const { left , top , width , height } = element . getBoundingClientRect ( ) ;
return document . elementFromPoint ( left + width / 2 , top + height / 2 ) ;
} ;
// Le refus posé à côté d'un bouton de Commande : dans son conteneur.
const refusPres = ( bouton , code ) => bouton . parentElement . querySelector ( ` [data-refus=" ${ code } "] ` ) ;
// Attend que l'assertion passe, le rendu de Svelte à jour.
async function attendre ( assertion ) {
for ( let essai = 0 ; ; essai += 1 ) {
flushSync ( ) ;
try {
return assertion ( ) ;
} catch ( erreur ) {
if ( essai >= 200 ) throw erreur ;
await new Promise ( ( resoudre ) => requestAnimationFrame ( resoudre ) ) ;
}
}
}
// --- Les événements d'épreuve -----------------------------------------------------
const baseDe = ( m ) => m . seance . etat ( ) . evenement . base ;
const fichier = ( m , suffixe , racine = 'documents' ) => m . fs . contenu ( racine , ` ${ baseDe ( m ) } ${ suffixe } ` ) ;
const journalDe = ( m , racine = 'documents' ) => lireJournal ( fichier ( m , SUFFIXES . journal , racine ) ) ;
// Un montage où l'événement neuf « Club des Merles », zéro participant et
// zéro table, est ouvert en lecture.
async function neuf ( options = { } ) {
const m = await monter ( options ) ;
await m . seance . creer ( EVENEMENT ) ;
flushSync ( ) ;
return m ;
}
// L'événement ouvert en écriture, garni par la séance des six participants
// et de trois tables de trois places.
async function garnir ( m ) {
await m . seance . passerEnEcriture ( ) ;
for ( const nom of NOMS ) await m . seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
for ( let n = 0 ; n < 3 ; n += 1 ) await m . seance . executer ( 'creerTable' , { forme : 'ronde' } ) ;
flushSync ( ) ;
}
// Enregistre deux propositions du moteur, puis, quand retenir le veut, retient
// la première.
async function generer ( m , { retenir = true } = { } ) {
const charge = m . seance . etat ( ) . charge ;
const configuration = configurationDepuisCharge ( charge ) ;
const propositions = rechercher ( configuration , { graine : 7 , arret : 40 , nombre : 2 } ) ;
await m . seance . executer ( 'enregistrerGeneration' , { propositions , configuration , produitVersion : VERSION . affichee } ) ;
if ( retenir ) await m . seance . executer ( 'retenirProposition' , { id : m . seance . etat ( ) . charge . propositions [ 0 ] . id } ) ;
flushSync ( ) ;
}
// Réécrit l'état d'un événement fermé : change(charge) le modifie en place,
// et l'état se sérialise à sa révision.
function reecrireEtat ( m , base , change ) {
const chemin = ` ${ base } ${ SUFFIXES . etat } ` ;
const { entete , charge } = analyser ( m . fs . contenu ( 'documents' , chemin ) ) ;
change ( charge ) ;
m . fs . deposer ( 'documents' , chemin , serialiser ( charge , { revision : entete . revision , produitVersion : entete . produitVersion } ) ) ;
}
// --- Les épreuves -----------------------------------------------------------------
describe ( 'aucun assistant (§ 2.1)' , ( ) => {
test ( 'un événement neuf : chaque section s’ ouvre depuis la navigation, dans un ordre tiré puis renversé, et rend son titre ; aucune n’ est grisée, aucun « Suivant »' , async ( ) => {
const m = await neuf ( ) ;
const { charge } = m . seance . etat ( ) ;
expect ( [ charge . participants . length , charge . tables . length ] ) . toEqual ( [ 0 , 0 ] ) ;
const ordre = creerPcg32 ( 20261006 , FLUX . DEMO ) . melanger ( [ ... SECTIONS ] ) ;
expect ( ordre . length ) . toBe ( 7 ) ;
for ( const nom of [ ... ordre , ... [ ... ordre ] . reverse ( ) ] ) {
const bouton = dans ( m , ` [data-commande="section"][data-cible=" ${ nom } "] ` ) ;
expect ( visible ( bouton ) , nom ) . toBe ( true ) ;
bouton . click ( ) ;
flushSync ( ) ;
expect ( m . ui . etat ( ) . section ) . toBe ( nom ) ;
const section = dans ( m , ` [data-section=" ${ nom } "] ` ) ;
expect ( section , nom ) . not . toBeNull ( ) ;
expect ( section . textContent ) . toContain ( t ( ` coquille.section. ${ nom } ` ) ) ;
}
const boutons = tous ( m , '[data-ecran="evenement"] button' ) ;
expect ( boutons . length ) . toBeGreaterThan ( 0 ) ;
for ( const bouton of boutons ) {
expect ( bouton . disabled ) . toBe ( false ) ;
expect ( bouton . getAttribute ( 'aria-disabled' ) ) . toBeNull ( ) ;
}
expect ( ecran ( m ) . textContent ) . not . toMatch ( /suivant/i ) ;
} ) ;
} ) ;
describe ( 'la place fixe (§ 8.4, § 8.5)' , ( ) => {
test ( '« Modifier » aux mêmes coordonnées sur les sept sections ; plein écran, il reste visible et atteint ; « Revenir en lecture » puis « Débloquer » au même endroit' , async ( ) => {
const m = await neuf ( ) ;
const coins = [ ] ;
for ( const nom of SECTIONS ) {
m . ui . allerA ( nom ) ;
flushSync ( ) ;
expect ( visible ( fixe ( m , 'modifier' ) ) , nom ) . toBe ( true ) ;
coins . push ( coin ( fixe ( m , 'modifier' ) ) ) ;
}
expect ( coins . length ) . toBe ( 7 ) ;
for ( const lu of coins ) expect ( lu ) . toEqual ( coins [ 0 ] ) ;
// Un refus posé à côté d'un état, au-dessus de la surface, ne déplace
// pas la place fixe, et ne la couvre pas.
boutonEtat ( m , 'bloque' ) . click ( ) ;
await attendre ( ( ) => expect ( dans ( m , '[data-etat="bloque"] [data-refus="MODE_LECTURE"]' ) ) . not . toBeNull ( ) ) ;
expect ( coin ( fixe ( m , 'modifier' ) ) ) . toEqual ( coins [ 0 ] ) ;
expect ( visible ( fixe ( m , 'modifier' ) ) ) . toBe ( true ) ;
m . ui . allerA ( 'plan' ) ;
m . ui . basculerPleinEcran ( ) ;
flushSync ( ) ;
expect ( visible ( fixe ( m , 'modifier' ) ) ) . toBe ( true ) ;
expect ( fixe ( m , 'modifier' ) . contains ( auCentre ( fixe ( m , 'modifier' ) ) ) ) . toBe ( true ) ;
expect ( coin ( fixe ( m , 'modifier' ) ) ) . toEqual ( coins [ 0 ] ) ;
expect ( dans ( m , '[data-commande="section"]' ) ) . toBeNull ( ) ;
m . ui . basculerPleinEcran ( ) ;
flushSync ( ) ;
await m . seance . passerEnEcriture ( ) ;
flushSync ( ) ;
expect ( fixe ( m , 'modifier' ) ) . toBeNull ( ) ;
expect ( fixe ( m , 'lecture' ) . textContent ) . toBe ( t ( 'communs.commande.lecture' ) ) ;
expect ( coin ( fixe ( m , 'lecture' ) ) ) . toEqual ( coins [ 0 ] ) ;
await m . seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
flushSync ( ) ;
expect ( fixe ( m , 'debloquer' ) . textContent ) . toBe ( t ( 'communs.commande.debloquer' ) ) ;
expect ( coin ( fixe ( m , 'debloquer' ) ) ) . toEqual ( coins [ 0 ] ) ;
expect ( visible ( fixe ( m , 'debloquer' ) ) ) . toBe ( true ) ;
} ) ;
} ) ;
describe ( 'un nom long ou de glyphes larges (point d’ attention 3)' , ( ) => {
test ( 'deux cents caractères sans espace : la page ne s’ élargit pas, et « Modifier » garde sa place' , async ( ) => {
const m = await neuf ( ) ;
const place = coin ( fixe ( m , 'modifier' ) ) ;
await m . seance . fermer ( ) ;
for ( const motif of [ 'W' , 'WW W' , '\u{C6}\u{C6}\u{C6}' , '\u{D4}jgq' ] ) {
const nom = motif . repeat ( Math . ceil ( 200 / motif . length ) ) . slice ( 0 , 200 ) ;
expect ( nom . length ) . toBe ( 200 ) ;
await m . seance . creer ( { ... EVENEMENT , nom } ) ;
flushSync ( ) ;
const titre = dans ( m , '[data-ecran="evenement"] h1' ) ;
expect ( titre . textContent ) . toBe ( nom ) ;
expect ( document . documentElement . scrollWidth , motif ) . toBeLessThanOrEqual ( window . innerWidth ) ;
expect ( titre . getBoundingClientRect ( ) . right , motif ) . toBeLessThanOrEqual ( window . innerWidth ) ;
[FIX] event: page never scrolls, broken-support remedies always shown
At 1366 x 700 an open event that shows warnings no longer scrolls the page:
the warnings scroll within their zone and the plan keeps at least 75 % of
its height. A broken support never shrinks, so its three remedies stay in
view, the only way to write the event elsewhere (spec 8.6). Tests now
prove the working-folder path and duplicate nodes are visible, the guard
of « Remplacer » in import, and which header a refused re-alignment shows.
Checked: 2692 node, 499 browser tests, from the index alone.
--- FR ---
[FIX] événement : sans défilement, remèdes du support rompu visibles
À 1366 x 700, un événement ouvert qui montre des avertissements ne fait
plus défiler la page : les avertissements défilent dans leur zone et le
plan garde au moins 75 % de sa hauteur. Un support rompu ne rétrécit
jamais : ses trois remèdes, seul moyen d'écrire l'événement ailleurs
(§ 8.6), restent visibles. Les épreuves prouvent que le chemin et les
doublons du dossier sont visibles, la garde de « Remplacer » à l'import, et
l'en-tête que montre un réalignement refusé. Vérifié : 2692 node, 499.
Assisted-by: Claude Opus 5.5
2026-10-07 15:05:59 -04:00
// Ni le titre ni la tête qui le porte ne débordent : la tête défile en
// hauteur, et un nom qui ne passerait pas à la ligne s'étendrait en
// largeur sous elle sans élargir la page.
const tete = titre . closest ( 'header' ) . parentElement ;
expect ( [ titre . scrollWidth , tete . scrollWidth ] , motif ) . toEqual ( [ titre . clientWidth , tete . clientWidth ] ) ;
[ADD] interface: event list, open event, participants, import, plan
The working screens of an evening. The home screen lists the events of
the working folder, asks its question, loads demos, copies, renames,
imports and deletes, and shows what the trash holds. An open event frames
its mode, offers Modifier, Débloquer and a confirmed lock takeover, and
shows its warnings. Participants get a grid, a form and a file; an import
goes through a preview and a report; tables, the default and tours have
their screens; the plan draws tables, chairs, empty seats and names.
Checked: 2342 node, 356 browser, 60 node-long; the Electron start check.
--- FR ---
[ADD] interface : liste, événement, participants, import, tables, plan
Les écrans de travail d'une soirée. L'accueil liste les événements du
dossier de travail, pose sa question, charge les démonstrations, copie,
renomme, importe et supprime, et montre ce que tient la corbeille. Un
événement ouvert encadre son mode, offre Modifier, Débloquer et une
reprise de verrou confirmée, et montre ses avertissements. Les
participants ont une grille, un formulaire et une fiche ; un import passe
par un aperçu et un rapport ; tables, défaut et tours ont leurs écrans ;
le plan dessine tables, chaises, sièges vides et noms.
Vérifié : 2342 node, 356 navigateur, 60 node-long ; démarrage d'Electron.
Assisted-by: Claude Opus 5.5
2026-10-07 08:12:27 -04:00
expect ( coin ( fixe ( m , 'modifier' ) ) , motif ) . toEqual ( place ) ;
expect ( visible ( fixe ( m , 'modifier' ) ) , motif ) . toBe ( true ) ;
await m . seance . fermer ( ) ;
}
} ) ;
} ) ;
describe ( 'passer en écriture (§ 8.4, § 19.3 01b)' , ( ) => {
test ( 'avant « Modifier », « Bloqué » est refusé à côté de l’ état avec « Modifier », sans rien écrire ; après, il écrit une entrée, un jalon, et la séance revient en lecture' , async ( ) => {
const m = await neuf ( ) ;
const avant = m . trace . length ;
boutonEtat ( m , 'bloque' ) . click ( ) ;
const refus = await attendre ( ( ) => {
const element = dans ( m , '[data-etat="bloque"] [data-refus="MODE_LECTURE"]' ) ;
expect ( element ) . not . toBeNull ( ) ;
return element ;
} ) ;
expect ( refus . textContent ) . toContain ( t ( 'MODE_LECTURE' , { } ) ) ;
expect ( refus . querySelector ( '[data-commande="modifier"]' ) . textContent ) . toBe ( t ( 'communs.remede.modifier' ) ) ;
expect ( visible ( refus ) ) . toBe ( true ) ;
expect ( m . trace . length ) . toBe ( avant ) ;
fixe ( m , 'modifier' ) . click ( ) ;
await attendre ( ( ) => expect ( m . seance . etat ( ) . mode ) . toBe ( 'ecriture' ) ) ;
expect ( fichier ( m , SUFFIXES . verrou ) ) . not . toBeNull ( ) ;
// Sur un brouillon neuf, seul « Bloqué » pose un état : les autres sont
// refusés à côté d'eux, sans rien écrire.
const ecrites = m . trace . length ;
for ( const [ etat , code ] of [
[ 'brouillon' , 'SANS_EFFET' ] ,
[ 'propose' , 'AUCUNE_PROPOSITION' ] ,
[ 'retenu' , 'AUCUN_RETENU' ] ,
] ) {
boutonEtat ( m , etat ) . click ( ) ;
await attendre ( ( ) => expect ( refusPres ( boutonEtat ( m , etat ) , code ) ) . not . toBeNull ( ) ) ;
}
expect ( m . trace . length ) . toBe ( ecrites ) ;
const { entrees , jalons } = journalDe ( m ) ;
boutonEtat ( m , 'bloque' ) . click ( ) ;
await attendre ( ( ) => expect ( m . seance . etat ( ) . evenement . etat ) . toBe ( 'bloque' ) ) ;
await attendre ( ( ) => expect ( m . seance . etat ( ) . mode ) . toBe ( 'lecture' ) ) ;
const apres = journalDe ( m ) ;
expect ( apres . entrees . length ) . toBe ( entrees . length + 1 ) ;
expect ( apres . jalons . length ) . toBe ( jalons . length + 1 ) ;
expect ( fichier ( m , SUFFIXES . verrou ) ) . toBeNull ( ) ;
expect ( fixe ( m , 'debloquer' ) ) . not . toBeNull ( ) ;
} ) ;
} ) ;
describe ( 'le cadre de mode (§ 8.4, § 14.5)' , ( ) => {
test ( 'il contient la surface, sa bordure n’ est pas nulle, son libellé se voit ; texte et trait diffèrent en lecture, en écriture et bloqué ; même boîte après défilement' , async ( ) => {
const m = await neuf ( ) ;
const releves = [ ] ;
const relever = ( ) => {
flushSync ( ) ;
const boite = cadre ( m ) . getBoundingClientRect ( ) ;
const surface = dans ( m , '[data-section]' ) . getBoundingClientRect ( ) ;
expect ( boite . left ) . toBeLessThanOrEqual ( surface . left ) ;
expect ( boite . top ) . toBeLessThanOrEqual ( surface . top ) ;
expect ( boite . right ) . toBeGreaterThanOrEqual ( surface . right ) ;
expect ( boite . bottom ) . toBeGreaterThanOrEqual ( surface . bottom ) ;
const style = getComputedStyle ( cadre ( m ) ) ;
expect ( Number . parseFloat ( style . borderTopWidth ) ) . toBeGreaterThan ( 0 ) ;
expect ( style . borderTopStyle ) . not . toBe ( 'none' ) ;
const libelle = libelleCadre ( m ) ;
expect ( libelle . checkVisibility ( ) ) . toBe ( true ) ;
expect ( visible ( libelle ) ) . toBe ( true ) ;
expect ( libelle . contains ( auCentre ( libelle ) ) ) . toBe ( true ) ;
expect ( libelle . textContent ) . toBe ( libelleBandeau ( m ) . textContent ) ;
releves . push ( { texte : libelle . textContent , trait : style . borderTopStyle } ) ;
} ;
relever ( ) ;
await m . seance . passerEnEcriture ( ) ;
relever ( ) ;
await m . seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
relever ( ) ;
expect ( releves . map ( ( { texte } ) => texte ) ) . toEqual ( [
t ( 'coquille.mode.lecture' ) ,
t ( 'coquille.mode.ecriture' ) ,
t ( 'coquille.mode.bloque' ) ,
] ) ;
expect ( new Set ( releves . map ( ( { trait } ) => trait ) ) . size ) . toBe ( 3 ) ;
// Une surface plus haute que la fenêtre : la page et la surface
// défilées jusqu'en bas, le libellé garde sa boîte.
const avant = libelleCadre ( m ) . getBoundingClientRect ( ) . toJSON ( ) ;
const haut = document . createElement ( 'div' ) ;
haut . style . height = '3000px' ;
dans ( m , '[data-section]' ) . append ( haut ) ;
window . scrollTo ( 0 , document . documentElement . scrollHeight ) ;
for ( let porteur = dans ( m , '[data-section]' ) ; porteur !== null ; porteur = porteur . parentElement ) {
porteur . scrollTop = porteur . scrollHeight ;
}
expect ( libelleCadre ( m ) . getBoundingClientRect ( ) . toJSON ( ) ) . toEqual ( avant ) ;
expect ( visible ( libelleCadre ( m ) ) ) . toBe ( true ) ;
expect ( visible ( fixe ( m , 'debloquer' ) ) ) . toBe ( true ) ;
haut . remove ( ) ;
} ) ;
} ) ;
describe ( 'l’ inactivité (§ 8.4, § 19.4)' , ( ) => {
for ( const [ ouvrir , fermer ] of [
[ 'debuterGeste' , 'finirGeste' ] ,
[ 'debuterCalcul' , 'finirCalcul' ] ,
] ) {
test ( ` ${ ouvrir } ouvert, onze minutes ne ramènent pas en lecture ; ${ fermer } , dix minutes y ramènent : verrou rendu, cadre, bandeau et avertissement le disent ` , async ( ) => {
const m = await neuf ( ) ;
fixe ( m , 'modifier' ) . click ( ) ;
await attendre ( ( ) => expect ( m . seance . etat ( ) . mode ) . toBe ( 'ecriture' ) ) ;
m . seance [ ouvrir ] ( ) ;
m . horloge . avancer ( 11 * 60 * 1000 ) ;
flushSync ( ) ;
expect ( m . seance . etat ( ) . mode ) . toBe ( 'ecriture' ) ;
m . seance [ fermer ] ( ) ;
m . horloge . avancer ( INACTIVITE _MS ) ;
await attendre ( ( ) => expect ( m . seance . etat ( ) . mode ) . toBe ( 'lecture' ) ) ;
expect ( fichier ( m , SUFFIXES . verrou ) ) . toBeNull ( ) ;
expect ( libelleCadre ( m ) . textContent ) . toBe ( t ( 'coquille.mode.lecture' ) ) ;
expect ( libelleBandeau ( m ) . textContent ) . toBe ( t ( 'coquille.mode.lecture' ) ) ;
const annonce = avertissement ( m , 'RETOUR_AUTOMATIQUE' ) ;
expect ( annonce . textContent ) . toContain ( t ( 'RETOUR_AUTOMATIQUE' , { } ) ) ;
expect ( visible ( annonce ) ) . toBe ( true ) ;
expect ( fixe ( m , 'modifier' ) ) . not . toBeNull ( ) ;
} ) ;
}
} ) ;
describe ( 'bloqué (§ 8.5, § 9)' , ( ) => {
test ( '« Bloquer » rend le verrou ; une autre séance le lit bloqué ; ni « Modifier », ni « Retenu », ni « Défaire » ne débloquent ; « Débloquer » se confirme en place, puis écrit' , async ( ) => {
const m = await neuf ( ) ;
await garnir ( m ) ;
boutonEtat ( m , 'bloque' ) . click ( ) ;
await attendre ( ( ) => expect ( m . seance . etat ( ) . mode ) . toBe ( 'lecture' ) ) ;
expect ( m . seance . etat ( ) . evenement . etat ) . toBe ( 'bloque' ) ;
expect ( fichier ( m , SUFFIXES . verrou ) ) . toBeNull ( ) ;
expect ( fixe ( m , 'debloquer' ) ) . not . toBeNull ( ) ;
const base = baseDe ( m ) ;
const autre = await monter ( { systeme : { disque : m . disque } } ) ;
await autre . seance . ouvrir ( base ) ;
flushSync ( ) ;
expect ( autre . seance . etat ( ) . evenement . etat ) . toBe ( 'bloque' ) ;
expect ( fixe ( autre , 'debloquer' ) . textContent ) . toBe ( t ( 'communs.commande.debloquer' ) ) ;
expect ( fixe ( autre , 'modifier' ) ) . toBeNull ( ) ;
const avant = m . trace . length ;
expect ( fixe ( m , 'modifier' ) ) . toBeNull ( ) ;
boutonEtat ( m , 'retenu' ) . click ( ) ;
await attendre ( ( ) => expect ( refusPres ( boutonEtat ( m , 'retenu' ) , 'LECTURE_SEULE' ) ) . not . toBeNull ( ) ) ;
historique ( m , 'defaire' ) . click ( ) ;
const refusDefaire = await attendre ( ( ) => {
const element = refusPres ( historique ( m , 'defaire' ) , 'LECTURE_SEULE' ) ;
expect ( element ) . not . toBeNull ( ) ;
return element ;
} ) ;
expect ( refusDefaire . querySelector ( '[data-commande="debloquer"]' ) . textContent ) . toBe ( t ( 'communs.remede.debloquer' ) ) ;
expect ( m . seance . etat ( ) . evenement . etat ) . toBe ( 'bloque' ) ;
expect ( m . trace . length ) . toBe ( avant ) ;
// Un clic arme, sans rien écrire ; l'horloge le désarme au terme du délai.
fixe ( m , 'debloquer' ) . click ( ) ;
flushSync ( ) ;
expect ( fixe ( m , 'debloquer' ) . textContent ) . toBe ( t ( CONFIRMATION _DEBLOCAGE ) ) ;
m . horloge . avancer ( DELAI _CONFIRMATION _MS ) ;
flushSync ( ) ;
expect ( fixe ( m , 'debloquer' ) . textContent ) . toBe ( t ( 'communs.commande.debloquer' ) ) ;
fixe ( m , 'debloquer' ) . click ( ) ;
flushSync ( ) ;
expect ( m . trace . length ) . toBe ( avant ) ;
expect ( m . seance . etat ( ) . evenement . etat ) . toBe ( 'bloque' ) ;
const { entrees , jalons } = journalDe ( m ) ;
fixe ( m , 'debloquer' ) . click ( ) ;
await attendre ( ( ) => expect ( m . seance . etat ( ) . mode ) . toBe ( 'ecriture' ) ) ;
const { charge } = m . seance . etat ( ) ;
expect ( charge . evenement . etat ) . toBe ( etatDeduit ( charge ) ) ;
expect ( journalDe ( m ) . entrees . length ) . toBe ( entrees . length + 1 ) ;
expect ( journalDe ( m ) . jalons . length ) . toBe ( jalons . length + 1 ) ;
expect ( fixe ( m , 'lecture' ) ) . not . toBeNull ( ) ;
} ) ;
} ) ;
describe ( 'les états du plan (§ 9)' , ( ) => {
test ( 'avec des propositions et un retenu, « Brouillon » puis « Retenu » : rien d’ autre ne change, deux entrées, deux jalons, libellés exacts' , async ( ) => {
const m = await neuf ( ) ;
await garnir ( m ) ;
await generer ( m ) ;
const avant = m . seance . etat ( ) . charge ;
expect ( avant . evenement . etat ) . toBe ( 'retenu' ) ;
const { entrees , jalons } = journalDe ( m ) ;
boutonEtat ( m , 'brouillon' ) . click ( ) ;
await attendre ( ( ) => expect ( m . seance . etat ( ) . evenement . etat ) . toBe ( 'brouillon' ) ) ;
boutonEtat ( m , 'retenu' ) . click ( ) ;
await attendre ( ( ) => expect ( m . seance . etat ( ) . evenement . etat ) . toBe ( 'retenu' ) ) ;
const apres = m . seance . etat ( ) . charge ;
expect ( apres ) . toEqual ( avant ) ;
const journal = journalDe ( m ) ;
expect ( journal . entrees . slice ( entrees . length ) . map ( ( entree ) => entree . libelle ) ) . toEqual ( [
libelle ( 'ENTREE_CHANGER_ETAT' , { etat : 'brouillon' } ) ,
libelle ( 'ENTREE_CHANGER_ETAT' , { etat : 'retenu' } ) ,
] ) ;
expect ( journal . jalons . length ) . toBe ( jalons . length + 2 ) ;
expect ( journal . jalons . slice ( - 2 ) . map ( ( { nom } ) => nom ) ) . toEqual ( [
libelle ( 'JALON_ETAT' , { etat : 'brouillon' } ) ,
libelle ( 'JALON_ETAT' , { etat : 'retenu' } ) ,
] ) ;
} ) ;
test ( '« Retenu » sans retenu : refusé à côté de l’ état, avec la section des propositions pour remède' , async ( ) => {
const m = await neuf ( ) ;
fixe ( m , 'modifier' ) . click ( ) ;
await attendre ( ( ) => expect ( m . seance . etat ( ) . mode ) . toBe ( 'ecriture' ) ) ;
const avant = m . trace . length ;
boutonEtat ( m , 'retenu' ) . click ( ) ;
const refus = await attendre ( ( ) => {
const element = refusPres ( boutonEtat ( m , 'retenu' ) , 'AUCUN_RETENU' ) ;
expect ( element ) . not . toBeNull ( ) ;
return element ;
} ) ;
expect ( visible ( refus ) ) . toBe ( true ) ;
const remede = refus . querySelector ( '[data-commande="ouvrirSection"]' ) ;
expect ( remede . textContent ) . toBe ( t ( 'communs.remede.ouvrirSection.propositions' ) ) ;
remede . click ( ) ;
await attendre ( ( ) => expect ( m . ui . etat ( ) . section ) . toBe ( 'propositions' ) ) ;
expect ( m . trace . length ) . toBe ( avant ) ;
} ) ;
} ) ;
describe ( 'défaire, refaire (§ 8.3)' , ( ) => {
const nombre = ( m ) => m . seance . etat ( ) . charge . participants . length ;
test ( 'trois gestes, « Défaire » rend le deuxième et offre « Refaire » ; « Refaire » rend le troisième ; un geste après « Défaire » retire « Refaire »' , async ( ) => {
const m = await neuf ( ) ;
await m . seance . passerEnEcriture ( ) ;
for ( const nom of NOMS . slice ( 0 , 3 ) ) await m . seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
flushSync ( ) ;
expect ( historique ( m , 'refaire' ) ) . toBeNull ( ) ;
historique ( m , 'defaire' ) . click ( ) ;
await attendre ( ( ) => expect ( nombre ( m ) ) . toBe ( 2 ) ) ;
expect ( historique ( m , 'refaire' ) . textContent ) . toBe ( t ( 'evenement.commande.refaire' ) ) ;
historique ( m , 'refaire' ) . click ( ) ;
await attendre ( ( ) => expect ( nombre ( m ) ) . toBe ( 3 ) ) ;
historique ( m , 'defaire' ) . click ( ) ;
await attendre ( ( ) => expect ( nombre ( m ) ) . toBe ( 2 ) ) ;
expect ( historique ( m , 'refaire' ) ) . not . toBeNull ( ) ;
await m . seance . executer ( 'ajouterParticipant' , { champs : { nom : NOMS [ 3 ] } } ) ;
await attendre ( ( ) => expect ( historique ( m , 'refaire' ) ) . toBeNull ( ) ) ;
} ) ;
test ( 'un retour de 5 à 2, puis « Défaire » : la cible est le prédécesseur de 2 sur le fil, non la ligne précédente' , async ( ) => {
const m = await neuf ( ) ;
await m . seance . passerEnEcriture ( ) ;
for ( const nom of NOMS . slice ( 0 , 4 ) ) await m . seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
expect ( m . seance . etat ( ) . evenement . revision ) . toBe ( 5 ) ;
await m . seance . revenir ( 2 ) ;
flushSync ( ) ;
expect ( nombre ( m ) ) . toBe ( 1 ) ;
historique ( m , 'defaire' ) . click ( ) ;
await attendre ( ( ) => expect ( nombre ( m ) ) . toBe ( 0 ) ) ;
} ) ;
test ( 'en lecture, « Défaire » est refusé à côté de lui avec « Modifier » ; bloqué, avec « Débloquer » ; rien ne s’ écrit' , async ( ) => {
const m = await neuf ( ) ;
await m . seance . passerEnEcriture ( ) ;
await m . seance . executer ( 'ajouterParticipant' , { champs : { nom : NOMS [ 0 ] } } ) ;
await m . seance . passerEnLecture ( ) ;
flushSync ( ) ;
const avant = m . trace . length ;
historique ( m , 'defaire' ) . click ( ) ;
const enLecture = await attendre ( ( ) => {
const element = refusPres ( historique ( m , 'defaire' ) , 'MODE_LECTURE' ) ;
expect ( element ) . not . toBeNull ( ) ;
return element ;
} ) ;
expect ( enLecture . querySelector ( '[data-commande="modifier"]' ) . textContent ) . toBe ( t ( 'communs.remede.modifier' ) ) ;
expect ( m . trace . length ) . toBe ( avant ) ;
expect ( nombre ( m ) ) . toBe ( 1 ) ;
await m . seance . passerEnEcriture ( ) ;
await m . seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
flushSync ( ) ;
const bloque = m . trace . length ;
historique ( m , 'defaire' ) . click ( ) ;
const refus = await attendre ( ( ) => {
const element = refusPres ( historique ( m , 'defaire' ) , 'LECTURE_SEULE' ) ;
expect ( element ) . not . toBeNull ( ) ;
return element ;
} ) ;
expect ( refus . querySelector ( '[data-commande="debloquer"]' ) ) . not . toBeNull ( ) ;
expect ( m . trace . length ) . toBe ( bloque ) ;
} ) ;
} ) ;
describe ( 'les annonces d’ ouverture (§ 8.9, § 9)' , ( ) => {
test ( 'une proposition de longueur fausse et une périmée : l’ écartée nomme son numéro et sa faute ; la dérive nomme la périmée et sa raison, et la charge la garde' , async ( ) => {
const m = await neuf ( ) ;
await garnir ( m ) ;
await generer ( m , { retenir : false } ) ;
const base = baseDe ( m ) ;
const [ premiere , seconde ] = m . seance . etat ( ) . charge . propositions . map ( ( { id } ) => id ) ;
await m . seance . fermer ( ) ;
reecrireEtat ( m , base , ( charge ) => {
charge . propositions [ 0 ] . placement . pop ( ) ;
charge . participants [ 0 ] . exclu = true ;
} ) ;
await m . seance . ouvrir ( base ) ;
flushSync ( ) ;
const etat = m . seance . etat ( ) ;
expect ( etat . fautives . length ) . toBe ( 1 ) ;
const [ fautive ] = etat . fautives ;
expect ( fautive . id ) . toBe ( premiere ) ;
const ecartee = avertissement ( m , 'PROPOSITIONS_FAUTIVES' ) ;
expect ( visible ( ecartee ) ) . toBe ( true ) ;
expect ( ecartee . textContent ) . toContain ( t ( 'PROPOSITIONS_FAUTIVES' , { ids : [ fautive . id ] } ) ) ;
expect ( fautive . fautes . length ) . toBeGreaterThan ( 0 ) ;
for ( const faute of fautive . fautes ) expect ( ecartee . textContent ) . toContain ( t ( faute . code , faute ) ) ;
expect ( ecartee . querySelector ( '[data-fautive]' ) . textContent ) . toContain ( String ( fautive . id ) ) ;
const derive = avertissement ( m , 'DERIVE' ) ;
expect ( derive . textContent ) . toContain ( t ( 'DERIVE' , { ids : [ seconde ] , retenu : false } ) ) ;
const [ { raisons } ] = etat . examen . derives ;
expect ( raisons . length ) . toBeGreaterThan ( 0 ) ;
for ( const raison of raisons ) expect ( derive . textContent ) . toContain ( t ( raison . code , raison ) ) ;
expect ( etat . charge . propositions . map ( ( { id } ) => id ) ) . toEqual ( [ seconde ] ) ;
derive . querySelector ( '[data-commande="ouvrirSection"]' ) . click ( ) ;
await attendre ( ( ) => expect ( m . ui . etat ( ) . section ) . toBe ( 'propositions' ) ) ;
} ) ;
test ( 'un retenu fautif : son message vient en premier et offre « Ouvrir la version précédente », qui l’ ouvre en lecture sans rien écrire' , async ( ) => {
const m = await neuf ( ) ;
await garnir ( m ) ;
await generer ( m ) ;
const base = baseDe ( m ) ;
await m . seance . fermer ( ) ;
reecrireEtat ( m , base , ( charge ) => {
charge . retenu . placement . pop ( ) ;
} ) ;
await m . seance . ouvrir ( base ) ;
flushSync ( ) ;
const premier = dans ( m , '[data-avertissement]' ) ;
expect ( premier . dataset . avertissement ) . toBe ( 'RETENU_FAUTIF' ) ;
expect ( premier . textContent ) . toContain ( t ( 'RETENU_FAUTIF' , { } ) ) ;
const remede = premier . querySelector ( '[data-commande="ouvrirSecours"]' ) ;
expect ( remede . textContent ) . toBe ( t ( 'communs.remede.ouvrirSecours.precedent' ) ) ;
const avant = m . trace . length ;
remede . click ( ) ;
await attendre ( ( ) => expect ( m . seance . etat ( ) . examen . retenu . fautes ) . toEqual ( [ ] ) ) ;
expect ( m . seance . etat ( ) . mode ) . toBe ( 'lecture' ) ;
expect ( m . seance . etat ( ) . charge . retenu ) . toBeNull ( ) ;
expect ( avertissement ( m , 'RETENU_FAUTIF' ) ) . toBeNull ( ) ;
expect ( m . trace . length ) . toBe ( avant ) ;
} ) ;
} ) ;
[FIX] event: page never scrolls, broken-support remedies always shown
At 1366 x 700 an open event that shows warnings no longer scrolls the page:
the warnings scroll within their zone and the plan keeps at least 75 % of
its height. A broken support never shrinks, so its three remedies stay in
view, the only way to write the event elsewhere (spec 8.6). Tests now
prove the working-folder path and duplicate nodes are visible, the guard
of « Remplacer » in import, and which header a refused re-alignment shows.
Checked: 2692 node, 499 browser tests, from the index alone.
--- FR ---
[FIX] événement : sans défilement, remèdes du support rompu visibles
À 1366 x 700, un événement ouvert qui montre des avertissements ne fait
plus défiler la page : les avertissements défilent dans leur zone et le
plan garde au moins 75 % de sa hauteur. Un support rompu ne rétrécit
jamais : ses trois remèdes, seul moyen d'écrire l'événement ailleurs
(§ 8.6), restent visibles. Les épreuves prouvent que le chemin et les
doublons du dossier sont visibles, la garde de « Remplacer » à l'import, et
l'en-tête que montre un réalignement refusé. Vérifié : 2692 node, 499.
Assisted-by: Claude Opus 5.5
2026-10-07 15:05:59 -04:00
describe ( 'l’ écran tient dans la fenêtre, avertissements compris (§ 8.4)' , ( ) => {
// Ferme l'événement garni et généré de base, l'abîme, puis le rouvre : une
// proposition de longueur fausse, une périmée, et une entrée du journal
// abîmée lui donnent trois avertissements à l'ouverture, au moins.
async function rouvrirAbime ( m , base ) {
await m . seance . fermer ( ) ;
reecrireEtat ( m , base , ( charge ) => {
charge . propositions [ 0 ] . placement . pop ( ) ;
charge . participants [ 0 ] . exclu = true ;
} ) ;
const chemin = ` ${ base } ${ SUFFIXES . journal } ` ;
const lignes = m . fs . contenu ( 'documents' , chemin ) . split ( '\n' ) ;
const rang = lignes . findIndex ( ( ligne ) => ligne . startsWith ( '{"type":"entree"' ) && JSON . parse ( ligne ) . revision === 4 ) ;
expect ( rang ) . toBeGreaterThan ( 0 ) ;
lignes [ rang ] = '{"type":"entree", abîmée' ;
m . fs . deposer ( 'documents' , chemin , lignes . join ( '\n' ) ) ;
await m . seance . ouvrir ( base ) ;
flushSync ( ) ;
await new Promise ( ( resoudre ) => requestAnimationFrame ( resoudre ) ) ;
}
test ( 'à 1 366 × 700, trois avertissements et plus : la page ne défile pas, le plan garde sa hauteur ; les avertissements défilent à la molette, sans rien déplacer' , async ( ) => {
const m = await neuf ( ) ;
await garnir ( m ) ;
await generer ( m , { retenir : false } ) ;
const base = baseDe ( m ) ;
// La hauteur du dessin du plan sans aucune annonce : la mesure de
// référence de ce que les avertissements peuvent lui prendre.
m . ui . allerA ( 'plan' ) ;
flushSync ( ) ;
expect ( dans ( m , '[data-annonces]' ) ) . toBeNull ( ) ;
const hauteurSeule = dans ( m , '[data-plan]' ) . getBoundingClientRect ( ) . height ;
expect ( hauteurSeule ) . toBeGreaterThan ( 0 ) ;
await rouvrirAbime ( m , base ) ;
expect ( m . ui . etat ( ) . section ) . toBe ( 'plan' ) ;
const montres = tous ( m , '[data-avertissement]' ) ;
expect ( montres . length ) . toBeGreaterThanOrEqual ( 3 ) ;
const boite = ( element ) => element . getBoundingClientRect ( ) . toJSON ( ) ;
// La page ne défile pas : le cadre tient dans la fenêtre, et la section
// du plan dans le cadre, sans que la surface qui la porte ait à défiler.
expect ( document . documentElement . scrollHeight ) . toBeLessThanOrEqual ( innerHeight ) ;
expect ( cadre ( m ) . getBoundingClientRect ( ) . bottom ) . toBeLessThanOrEqual ( innerHeight ) ;
const section = dans ( m , '[data-section]' ) ;
const porteur = section . parentElement ;
expect ( section . getBoundingClientRect ( ) . bottom ) . toBeLessThanOrEqual ( porteur . getBoundingClientRect ( ) . bottom ) ;
expect ( porteur . scrollHeight ) . toBeLessThanOrEqual ( porteur . clientHeight ) ;
expect ( visible ( dans ( m , '[data-plan]' ) ) ) . toBe ( true ) ;
// Le plan reste la surface de travail : les annonces ne prennent pas
// plus du quart de la hauteur de son dessin.
expect ( dans ( m , '[data-plan]' ) . getBoundingClientRect ( ) . height ) . toBeGreaterThanOrEqual ( hauteurSeule * 0.75 ) ;
// Les avertissements débordent leur zone, qui défile seule, à la
// molette, comme sous la main de l'opérateur : le dernier, amené dans la
// zone, se voit, et ni la page, ni la place fixe, ni la section n'ont
// bougé.
const zone = montres [ 0 ] . closest ( '[data-annonces]' ) ;
expect ( zone ) . not . toBeNull ( ) ;
for ( const montre of montres ) expect ( zone . contains ( montre ) ) . toBe ( true ) ;
expect ( zone . scrollHeight ) . toBeGreaterThan ( zone . clientHeight ) ;
expect ( [ 'auto' , 'scroll' ] ) . toContain ( getComputedStyle ( zone ) . overflowY ) ;
expect ( visible ( montres [ 0 ] ) ) . toBe ( true ) ;
const place = coin ( fixe ( m , 'modifier' ) ) ;
const avant = boite ( section ) ;
const surZone = zone . getBoundingClientRect ( ) ;
await molette ( surZone . left + surZone . width / 2 , surZone . top + surZone . height / 2 , zone . scrollHeight ) ;
await attendre ( ( ) => expect ( visible ( montres . at ( - 1 ) ) ) . toBe ( true ) ) ;
expect ( zone . scrollTop ) . toBeGreaterThan ( 0 ) ;
expect ( window . scrollY ) . toBe ( 0 ) ;
expect ( coin ( fixe ( m , 'modifier' ) ) ) . toEqual ( place ) ;
expect ( boite ( section ) ) . toEqual ( avant ) ;
// Sur chaque section, la page reste sans défilement, et la zone garde
// sa boîte.
const zoneAvant = boite ( zone ) ;
for ( const nom of SECTIONS ) {
m . ui . allerA ( nom ) ;
flushSync ( ) ;
expect ( document . documentElement . scrollHeight , nom ) . toBeLessThanOrEqual ( innerHeight ) ;
expect ( boite ( dans ( m , '[data-annonces]' ) ) , nom ) . toEqual ( zoneAvant ) ;
}
} ) ;
test ( 'à 1 366 × 700, un support rompu et trois avertissements et plus : chaque issue du support rompu se voit sans défiler, la page ne défile pas, et les avertissements défilent sous lui' , async ( ) => {
const m = await neuf ( ) ;
await garnir ( m ) ;
await generer ( m , { retenir : false } ) ;
await rouvrirAbime ( m , baseDe ( m ) ) ;
expect ( tous ( m , '[data-avertissement]' ) . length ) . toBeGreaterThanOrEqual ( 3 ) ;
fixe ( m , 'modifier' ) . click ( ) ;
await attendre ( ( ) => expect ( m . seance . etat ( ) . mode ) . toBe ( 'ecriture' ) ) ;
m . fs . pannes . ecriture ( 'documents' ) ;
await expect ( m . seance . executer ( 'ajouterParticipant' , { champs : { nom : NOMS [ 0 ] } } ) ) . rejects . toMatchObject ( { code : 'ECRITURE' } ) ;
flushSync ( ) ;
await new Promise ( ( resoudre ) => requestAnimationFrame ( resoudre ) ) ;
expect ( m . seance . etat ( ) . supportRompu ) . not . toBeNull ( ) ;
const montres = tous ( m , '[data-avertissement]' ) ;
expect ( montres . length ) . toBeGreaterThanOrEqual ( 3 ) ;
expect ( document . documentElement . scrollHeight ) . toBeLessThanOrEqual ( innerHeight ) ;
expect ( cadre ( m ) . getBoundingClientRect ( ) . bottom ) . toBeLessThanOrEqual ( innerHeight ) ;
// Les issues du support rompu, seule sortie de l'opérateur, se voient
// sans que rien ne défile.
const message = dans ( m , '[data-support-rompu]' ) ;
for ( const nom of [ 'choisirDossier' , 'abandonner' ] ) {
const issue = message . querySelector ( ` [data-commande=" ${ nom } "] ` ) ;
expect ( issue , nom ) . not . toBeNull ( ) ;
expect ( visible ( issue ) , nom ) . toBe ( true ) ;
}
expect ( visible ( fixe ( m , 'lecture' ) ) ) . toBe ( true ) ;
expect ( visible ( dans ( m , '[data-plan]' ) ) ) . toBe ( true ) ;
// Les avertissements restent dans la zone, qui défile seule : le
// dernier, amené à la molette, se voit, et la page n'a pas bougé.
const zone = message . closest ( '[data-annonces]' ) ;
expect ( zone ) . not . toBeNull ( ) ;
for ( const montre of montres ) expect ( zone . contains ( montre ) ) . toBe ( true ) ;
const surZone = zone . getBoundingClientRect ( ) ;
await molette ( surZone . left + surZone . width / 2 , surZone . top + surZone . height / 2 , zone . scrollHeight ) ;
await attendre ( ( ) => expect ( visible ( montres . at ( - 1 ) ) ) . toBe ( true ) ) ;
expect ( window . scrollY ) . toBe ( 0 ) ;
} ) ;
} ) ;
[ADD] interface: event list, open event, participants, import, plan
The working screens of an evening. The home screen lists the events of
the working folder, asks its question, loads demos, copies, renames,
imports and deletes, and shows what the trash holds. An open event frames
its mode, offers Modifier, Débloquer and a confirmed lock takeover, and
shows its warnings. Participants get a grid, a form and a file; an import
goes through a preview and a report; tables, the default and tours have
their screens; the plan draws tables, chairs, empty seats and names.
Checked: 2342 node, 356 browser, 60 node-long; the Electron start check.
--- FR ---
[ADD] interface : liste, événement, participants, import, tables, plan
Les écrans de travail d'une soirée. L'accueil liste les événements du
dossier de travail, pose sa question, charge les démonstrations, copie,
renomme, importe et supprime, et montre ce que tient la corbeille. Un
événement ouvert encadre son mode, offre Modifier, Débloquer et une
reprise de verrou confirmée, et montre ses avertissements. Les
participants ont une grille, un formulaire et une fiche ; un import passe
par un aperçu et un rapport ; tables, défaut et tours ont leurs écrans ;
le plan dessine tables, chaises, sièges vides et noms.
Vérifié : 2342 node, 356 navigateur, 60 node-long ; démarrage d'Electron.
Assisted-by: Claude Opus 5.5
2026-10-07 08:12:27 -04:00
describe ( 'le journal (§ 8.6, § 8.2)' , ( ) => {
test ( 'cent entrées, la 37ᵉ abîmée : JOURNAL_ECARTEES {n: 64} s’ affiche, son texte porte « 64 »' , async ( ) => {
const m = await neuf ( ) ;
await m . seance . passerEnEcriture ( ) ;
for ( let n = 2 ; n <= 100 ; n += 1 ) await m . seance . executer ( 'ajouterParticipant' , { champs : { nom : ` Ombrelle ${ n } ` } } ) ;
const base = baseDe ( m ) ;
const journal = journalDe ( m ) ;
expect ( journal . entrees . length ) . toBe ( 100 ) ;
expect ( journal . jalons . length ) . toBe ( 1 ) ;
await m . seance . fermer ( ) ;
const chemin = ` ${ base } ${ SUFFIXES . journal } ` ;
const lignes = m . fs . contenu ( 'documents' , chemin ) . split ( '\n' ) ;
const rang = lignes . findIndex ( ( ligne ) => ligne . startsWith ( '{"type":"entree"' ) && JSON . parse ( ligne ) . revision === 37 ) ;
expect ( rang ) . toBeGreaterThan ( 0 ) ;
lignes [ rang ] = '{"type":"entree", abîmée' ;
m . fs . deposer ( 'documents' , chemin , lignes . join ( '\n' ) ) ;
const avant = m . trace . length ;
await m . seance . ouvrir ( base ) ;
flushSync ( ) ;
const ecartees = avertissement ( m , 'JOURNAL_ECARTEES' ) ;
expect ( ecartees . textContent ) . toContain ( t ( 'JOURNAL_ECARTEES' , { n : 64 } ) ) ;
expect ( ecartees . textContent ) . toContain ( '64' ) ;
expect ( visible ( ecartees ) ) . toBe ( true ) ;
expect ( m . trace . length ) . toBe ( avant ) ;
} ) ;
test ( 'un journal en avance d’ une entrée : la charge du geste est celle qui s’ ouvre, annoncée, sans rien écrire' , async ( ) => {
const m = await neuf ( ) ;
await m . seance . passerEnEcriture ( ) ;
await m . seance . executer ( 'ajouterParticipant' , { champs : { nom : NOMS [ 0 ] } } ) ;
await m . seance . executer ( 'ajouterParticipant' , { champs : { nom : NOMS [ 1 ] } } ) ;
const base = baseDe ( m ) ;
await m . seance . fermer ( ) ;
m . fs . deposer ( 'documents' , ` ${ base } ${ SUFFIXES . etat } ` , m . fs . contenu ( 'documents' , ` ${ base } ${ SUFFIXES . precedent } ` ) ) ;
const avant = m . trace . length ;
await m . seance . ouvrir ( base ) ;
flushSync ( ) ;
expect ( m . seance . etat ( ) . charge . participants . map ( ( { nom } ) => nom ) ) . toEqual ( NOMS . slice ( 0 , 2 ) ) ;
expect ( m . seance . etat ( ) . evenement . revision ) . toBe ( 3 ) ;
expect ( avertissement ( m , 'JOURNAL_REPRIS' ) . textContent ) . toContain ( t ( 'JOURNAL_REPRIS' , { } ) ) ;
expect ( m . trace . length ) . toBe ( avant ) ;
} ) ;
} ) ;
describe ( 'la dérive (§ 9)' , ( ) => {
test ( 'retenu, puis exclure une personne : l’ avertissement nomme le retenu et la raison, et le geste suivant reste permis' , async ( ) => {
const m = await neuf ( ) ;
await garnir ( m ) ;
await generer ( m ) ;
const exclue = m . seance . etat ( ) . charge . participants [ 0 ] . id ;
await m . seance . executer ( 'exclureParticipant' , { id : exclue } ) ;
flushSync ( ) ;
const nouvelle = m . seance . etat ( ) . avertissements . find ( ( { code } ) => code === 'DERIVE_NOUVELLE' ) ;
expect ( nouvelle . details . touchees . map ( ( { id } ) => id ) ) . toContain ( 'retenu' ) ;
const element = avertissement ( m , 'DERIVE_NOUVELLE' ) ;
expect ( element . textContent ) . toContain ( t ( 'DERIVE_NOUVELLE' , nouvelle . details ) ) ;
expect ( element . textContent ) . toContain ( t ( 'PARTICIPANT_EXCLU' , { participant : exclue } ) ) ;
await m . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Rouvraie' } } ) ;
expect ( m . seance . etat ( ) . charge . participants . length ) . toBe ( NOMS . length + 1 ) ;
} ) ;
test ( 'retenu : supprimer une table, réduire une capacité, ajouter une personne, chacun accepté, l’ avertissement nommant les propositions touchées ; leur nombre inchangé ; un import, IMPORT_EN_RETENU' , async ( ) => {
const m = await neuf ( ) ;
await garnir ( m ) ;
await generer ( m ) ;
const propositions = m . seance . etat ( ) . charge . propositions . length ;
const [ premiere , seconde ] = m . seance . etat ( ) . charge . tables ;
const gestes = [
[ 'supprimerTable' , { id : premiere . id } ] ,
[ 'changerPlacesTable' , { id : seconde . id , sieges : 2 , origine : 'champ' } ] ,
[ 'ajouterParticipant' , { champs : { nom : 'Escarbille' } } ] ,
] ;
for ( const [ nom , args ] of gestes ) {
await m . seance . executer ( nom , args ) ;
flushSync ( ) ;
const nouvelle = m . seance . etat ( ) . avertissements . find ( ( { code } ) => code === 'DERIVE_NOUVELLE' ) ;
expect ( nouvelle , nom ) . toBeDefined ( ) ;
expect ( nouvelle . details . touchees . length , nom ) . toBeGreaterThan ( 0 ) ;
expect ( avertissement ( m , 'DERIVE_NOUVELLE' ) . textContent ) . toContain ( t ( 'DERIVE_NOUVELLE' , nouvelle . details ) ) ;
expect ( m . seance . etat ( ) . charge . propositions . length ) . toBe ( propositions ) ;
}
const apercu = apercevoir ( 'nom\nTilleraie\n' , { participants : m . seance . etat ( ) . charge . participants } ) ;
await m . seance . executer ( 'importerParticipants' , { apercu , mode : 'ajouter' } ) ;
flushSync ( ) ;
expect ( avertissement ( m , 'IMPORT_EN_RETENU' ) . textContent ) . toContain ( t ( 'IMPORT_EN_RETENU' , { } ) ) ;
} ) ;
} ) ;
describe ( 'le verrou d’ une autre séance (§ 8.8)' , ( ) => {
test ( 'le second montage ouvre en lecture ; son « Modifier » est refusé, la séance et l’ instant affichés ; le premier coupé, il reprend le verrou du mort' , async ( ) => {
const premier = await neuf ( ) ;
await premier . seance . passerEnEcriture ( ) ;
const base = baseDe ( premier ) ;
const tenu = JSON . parse ( fichier ( premier , SUFFIXES . verrou ) ) ;
const second = await monter ( { systeme : { disque : premier . disque } } ) ;
// Les deux montages s'empilent dans la page : le premier se cache, et
// le second occupe la fenêtre.
premier . racine . hidden = true ;
await second . seance . ouvrir ( base ) ;
flushSync ( ) ;
expect ( second . seance . etat ( ) . mode ) . toBe ( 'lecture' ) ;
expect ( dans ( second , '[data-refus]' ) ) . toBeNull ( ) ;
fixe ( second , 'modifier' ) . click ( ) ;
const refus = await attendre ( ( ) => {
const element = refusPres ( fixe ( second , 'modifier' ) , 'VERROU_PRIS' ) ;
expect ( element ) . not . toBeNull ( ) ;
return element ;
} ) ;
const pris = second . seance . etat ( ) . verrouAutre ;
expect ( pris ) . toEqual ( { seance : tenu . seance , depuis : tenu . depuis , vivant : true } ) ;
expect ( refus . textContent ) . toContain ( t ( 'VERROU_PRIS' , { ... pris , remede : null } ) ) ;
const verrou = dans ( second , '[data-verrou]' ) ;
expect ( visible ( verrou ) ) . toBe ( true ) ;
expect ( dans ( second , '[data-verrou-seance]' ) . textContent ) . toBe ( tenu . seance ) ;
expect ( dans ( second , '[data-verrou-depuis]' ) . textContent ) . toBe ( tenu . depuis ) ;
expect ( verrou . querySelector ( '[data-commande="reprendreVerrou"]' ) ) . toBeNull ( ) ;
premier . fs . pannes . couperApres ( 0 ) ;
fixe ( second , 'modifier' ) . click ( ) ;
await attendre ( ( ) => expect ( second . seance . etat ( ) . mode ) . toBe ( 'ecriture' ) ) ;
const repris = avertissement ( second , 'VERROU_REPRIS' ) ;
expect ( repris . textContent ) . toContain ( t ( 'VERROU_REPRIS' , { seance : tenu . seance , depuis : tenu . depuis } ) ) ;
expect ( JSON . parse ( fichier ( second , SUFFIXES . verrou ) ) . seance ) . toBe ( second . seance . id ) ;
expect ( dans ( second , '[data-verrou]' ) ) . toBeNull ( ) ;
} ) ;
test ( 'le verrou d’ un autre poste : la reprise s’ offre à côté du refus, se confirme en place en nommant la séance et l’ instant, puis le reprend' , async ( ) => {
const premier = await neuf ( ) ;
await premier . seance . passerEnEcriture ( ) ;
const base = baseDe ( premier ) ;
const tenu = JSON . parse ( fichier ( premier , SUFFIXES . verrou ) ) ;
const second = await monter ( { systeme : { disque : premier . disque , hote : 'poste-voisin' } } ) ;
premier . racine . hidden = true ;
await second . seance . ouvrir ( base ) ;
flushSync ( ) ;
fixe ( second , 'modifier' ) . click ( ) ;
await attendre ( ( ) => expect ( refusPres ( fixe ( second , 'modifier' ) , 'VERROU_PRIS' ) ) . not . toBeNull ( ) ) ;
expect ( second . seance . etat ( ) . verrouAutre . vivant ) . toBeNull ( ) ;
const verrou = dans ( second , '[data-verrou]' ) ;
expect ( dans ( second , '[data-verrou-seance]' ) . textContent ) . toBe ( tenu . seance ) ;
expect ( dans ( second , '[data-verrou-depuis]' ) . textContent ) . toBe ( tenu . depuis ) ;
const reprendre = ( ) => dans ( second , '[data-verrou] [data-commande="reprendreVerrou"]' ) ;
expect ( reprendre ( ) . textContent ) . toBe ( t ( 'communs.remede.reprendreVerrou' ) ) ;
expect ( visible ( reprendre ( ) ) ) . toBe ( true ) ;
const avant = second . trace . length ;
reprendre ( ) . click ( ) ;
flushSync ( ) ;
expect ( reprendre ( ) . textContent ) . toBe ( t ( 'evenement.verrou.confirmerReprise' ) ) ;
expect ( verrou . contains ( reprendre ( ) ) ) . toBe ( true ) ;
expect ( second . trace . length ) . toBe ( avant ) ;
expect ( second . seance . etat ( ) . mode ) . toBe ( 'lecture' ) ;
reprendre ( ) . click ( ) ;
await attendre ( ( ) => expect ( second . seance . etat ( ) . mode ) . toBe ( 'ecriture' ) ) ;
expect ( JSON . parse ( fichier ( second , SUFFIXES . verrou ) ) . seance ) . toBe ( second . seance . id ) ;
expect ( avertissement ( second , 'VERROU_REPRIS' ) . textContent ) . toContain (
t ( 'VERROU_REPRIS' , { seance : tenu . seance , depuis : tenu . depuis } ) ,
) ;
expect ( dans ( second , '[data-verrou]' ) ) . toBeNull ( ) ;
} ) ;
} ) ;
describe ( 'le support rompu (§ 8.6, § 8.2)' , ( ) => {
const message = ( m ) => dans ( m , '[data-support-rompu]' ) ;
const commandeListe = ( m ) => dans ( m , '[data-bandeau] [data-commande="liste"]' ) ;
[FIX] event: page never scrolls, broken-support remedies always shown
At 1366 x 700 an open event that shows warnings no longer scrolls the page:
the warnings scroll within their zone and the plan keeps at least 75 % of
its height. A broken support never shrinks, so its three remedies stay in
view, the only way to write the event elsewhere (spec 8.6). Tests now
prove the working-folder path and duplicate nodes are visible, the guard
of « Remplacer » in import, and which header a refused re-alignment shows.
Checked: 2692 node, 499 browser tests, from the index alone.
--- FR ---
[FIX] événement : sans défilement, remèdes du support rompu visibles
À 1366 x 700, un événement ouvert qui montre des avertissements ne fait
plus défiler la page : les avertissements défilent dans leur zone et le
plan garde au moins 75 % de sa hauteur. Un support rompu ne rétrécit
jamais : ses trois remèdes, seul moyen d'écrire l'événement ailleurs
(§ 8.6), restent visibles. Les épreuves prouvent que le chemin et les
doublons du dossier sont visibles, la garde de « Remplacer » à l'import, et
l'en-tête que montre un réalignement refusé. Vérifié : 2692 node, 499.
Assisted-by: Claude Opus 5.5
2026-10-07 15:05:59 -04:00
// Le message offre exactement les issues noms, et chacune se voit au cadre
// de l'épreuve : c'est la seule sortie de l'opérateur d'un support rompu.
function issuesVisibles ( m , noms ) {
const offertes = [ ... message ( m ) . querySelectorAll ( '[data-commande]' ) ] . map ( ( issue ) => issue . dataset . commande ) ;
expect ( offertes ) . toEqual ( noms ) ;
for ( const nom of noms ) expect ( visible ( message ( m ) . querySelector ( ` [data-commande=" ${ nom } "] ` ) ) , nom ) . toBe ( true ) ;
}
[ADD] interface: event list, open event, participants, import, plan
The working screens of an evening. The home screen lists the events of
the working folder, asks its question, loads demos, copies, renames,
imports and deletes, and shows what the trash holds. An open event frames
its mode, offers Modifier, Débloquer and a confirmed lock takeover, and
shows its warnings. Participants get a grid, a form and a file; an import
goes through a preview and a report; tables, the default and tours have
their screens; the plan draws tables, chairs, empty seats and names.
Checked: 2342 node, 356 browser, 60 node-long; the Electron start check.
--- FR ---
[ADD] interface : liste, événement, participants, import, tables, plan
Les écrans de travail d'une soirée. L'accueil liste les événements du
dossier de travail, pose sa question, charge les démonstrations, copie,
renomme, importe et supprime, et montre ce que tient la corbeille. Un
événement ouvert encadre son mode, offre Modifier, Débloquer et une
reprise de verrou confirmée, et montre ses avertissements. Les
participants ont une grille, un formulaire et une fiche ; un import passe
par un aperçu et un rapport ; tables, défaut et tours ont leurs écrans ;
le plan dessine tables, chaises, sièges vides et noms.
Vérifié : 2342 node, 356 navigateur, 60 node-long ; démarrage d'Electron.
Assisted-by: Claude Opus 5.5
2026-10-07 08:12:27 -04:00
// L'événement ouvert en écriture, puis le support de racine refuse
// l'écriture d'un geste.
async function rompre ( m , racine ) {
fixe ( m , 'modifier' ) . click ( ) ;
await attendre ( ( ) => expect ( m . seance . etat ( ) . mode ) . toBe ( 'ecriture' ) ) ;
m . fs . pannes . ecriture ( racine ) ;
await expect ( m . seance . executer ( 'ajouterParticipant' , { champs : { nom : NOMS [ 0 ] } } ) ) . rejects . toMatchObject ( { code : 'ECRITURE' } ) ;
flushSync ( ) ;
}
test ( 'publié : le message nomme le fichier, le dossier et la rupture ; « Liste » est refusée ; « Écrire dans les Documents » y écrit, et la séance s’ y poursuit' , async ( ) => {
const m = await neuf ( { systeme : { executable : 'E:\\soirees' } } ) ;
expect ( m . seance . etat ( ) . racine . id ) . toBe ( 'portable' ) ;
const base = baseDe ( m ) ;
await rompre ( m , 'portable' ) ;
const rompu = m . seance . etat ( ) . supportRompu ;
expect ( rompu . ailleurs ) . toEqual ( { id : 'documents' , chemin : DOCUMENTS } ) ;
expect ( visible ( message ( m ) ) ) . toBe ( true ) ;
const { ailleurs , ... details } = rompu ;
expect ( message ( m ) . textContent ) . toContain ( t ( 'SUPPORT_ROMPU' , details ) ) ;
expect ( message ( m ) . textContent ) . toContain ( rompu . chemin ) ;
expect ( message ( m ) . textContent ) . toContain ( rompu . dossier ) ;
await expect ( m . seance . executer ( 'ajouterParticipant' , { champs : { nom : NOMS [ 1 ] } } ) ) . rejects . toMatchObject ( {
code : 'SUPPORT_ROMPU' ,
} ) ;
// Le refus posé dans le bandeau ne déplace pas la place fixe, et ne la
// couvre pas.
const place = coin ( fixe ( m , 'lecture' ) ) ;
commandeListe ( m ) . click ( ) ;
await attendre ( ( ) => expect ( dans ( m , '[data-refus="liste"] [data-refus="SUPPORT_ROMPU_NON_SAUVE"]' ) ) . not . toBeNull ( ) ) ;
expect ( coin ( fixe ( m , 'lecture' ) ) ) . toEqual ( place ) ;
expect ( visible ( fixe ( m , 'lecture' ) ) ) . toBe ( true ) ;
expect ( visible ( dans ( m , '[data-refus="liste"]' ) ) ) . toBe ( true ) ;
expect ( m . seance . etat ( ) . evenement . base ) . toBe ( base ) ;
[FIX] event: page never scrolls, broken-support remedies always shown
At 1366 x 700 an open event that shows warnings no longer scrolls the page:
the warnings scroll within their zone and the plan keeps at least 75 % of
its height. A broken support never shrinks, so its three remedies stay in
view, the only way to write the event elsewhere (spec 8.6). Tests now
prove the working-folder path and duplicate nodes are visible, the guard
of « Remplacer » in import, and which header a refused re-alignment shows.
Checked: 2692 node, 499 browser tests, from the index alone.
--- FR ---
[FIX] événement : sans défilement, remèdes du support rompu visibles
À 1366 x 700, un événement ouvert qui montre des avertissements ne fait
plus défiler la page : les avertissements défilent dans leur zone et le
plan garde au moins 75 % de sa hauteur. Un support rompu ne rétrécit
jamais : ses trois remèdes, seul moyen d'écrire l'événement ailleurs
(§ 8.6), restent visibles. Les épreuves prouvent que le chemin et les
doublons du dossier sont visibles, la garde de « Remplacer » à l'import, et
l'en-tête que montre un réalignement refusé. Vérifié : 2692 node, 499.
Assisted-by: Claude Opus 5.5
2026-10-07 15:05:59 -04:00
issuesVisibles ( m , [ 'ecrireDocuments' , 'choisirDossier' , 'abandonner' ] ) ;
[ADD] interface: event list, open event, participants, import, plan
The working screens of an evening. The home screen lists the events of
the working folder, asks its question, loads demos, copies, renames,
imports and deletes, and shows what the trash holds. An open event frames
its mode, offers Modifier, Débloquer and a confirmed lock takeover, and
shows its warnings. Participants get a grid, a form and a file; an import
goes through a preview and a report; tables, the default and tours have
their screens; the plan draws tables, chairs, empty seats and names.
Checked: 2342 node, 356 browser, 60 node-long; the Electron start check.
--- FR ---
[ADD] interface : liste, événement, participants, import, tables, plan
Les écrans de travail d'une soirée. L'accueil liste les événements du
dossier de travail, pose sa question, charge les démonstrations, copie,
renomme, importe et supprime, et montre ce que tient la corbeille. Un
événement ouvert encadre son mode, offre Modifier, Débloquer et une
reprise de verrou confirmée, et montre ses avertissements. Les
participants ont une grille, un formulaire et une fiche ; un import passe
par un aperçu et un rapport ; tables, défaut et tours ont leurs écrans ;
le plan dessine tables, chaises, sièges vides et noms.
Vérifié : 2342 node, 356 navigateur, 60 node-long ; démarrage d'Electron.
Assisted-by: Claude Opus 5.5
2026-10-07 08:12:27 -04:00
message ( m ) . querySelector ( '[data-commande="ecrireDocuments"]' ) . click ( ) ;
await attendre ( ( ) => expect ( m . seance . etat ( ) . supportRompu ) . toBeNull ( ) ) ;
const nouvelle = baseDe ( m ) ;
expect ( m . fs . contenu ( 'documents' , ` ${ nouvelle } ${ SUFFIXES . etat } ` ) ) . not . toBeNull ( ) ;
const entrees = journalDe ( m , 'documents' ) . entrees . length ;
await m . seance . executer ( 'ajouterParticipant' , { champs : { nom : NOMS [ 2 ] } } ) ;
expect ( journalDe ( m , 'documents' ) . entrees . length ) . toBe ( entrees + 1 ) ;
expect ( message ( m ) ) . toBeNull ( ) ;
commandeListe ( m ) . click ( ) ;
await attendre ( ( ) => expect ( ecran ( m ) . dataset . ecran ) . toBe ( 'accueil' ) ) ;
expect ( m . seance . etat ( ) . racine . id ) . toBe ( 'documents' ) ;
await attendre ( ( ) => expect ( dans ( m , ` [data-evenement=" ${ nouvelle } "] ` ) ) . not . toBeNull ( ) ) ;
const dossier = dans ( m , '[data-dossier-travail]' ) ;
expect ( dossier . textContent ) . toBe ( DOCUMENTS ) ;
expect ( visible ( dossier ) ) . toBe ( true ) ;
} ) ;
test ( 'non publié : le message n’ offre que « Choisir un dossier… » ; le dialogue annulé ne change rien' , async ( ) => {
const m = await neuf ( ) ;
await rompre ( m , 'documents' ) ;
expect ( m . seance . etat ( ) . supportRompu . ailleurs ) . toBeNull ( ) ;
[FIX] event: page never scrolls, broken-support remedies always shown
At 1366 x 700 an open event that shows warnings no longer scrolls the page:
the warnings scroll within their zone and the plan keeps at least 75 % of
its height. A broken support never shrinks, so its three remedies stay in
view, the only way to write the event elsewhere (spec 8.6). Tests now
prove the working-folder path and duplicate nodes are visible, the guard
of « Remplacer » in import, and which header a refused re-alignment shows.
Checked: 2692 node, 499 browser tests, from the index alone.
--- FR ---
[FIX] événement : sans défilement, remèdes du support rompu visibles
À 1366 x 700, un événement ouvert qui montre des avertissements ne fait
plus défiler la page : les avertissements défilent dans leur zone et le
plan garde au moins 75 % de sa hauteur. Un support rompu ne rétrécit
jamais : ses trois remèdes, seul moyen d'écrire l'événement ailleurs
(§ 8.6), restent visibles. Les épreuves prouvent que le chemin et les
doublons du dossier sont visibles, la garde de « Remplacer » à l'import, et
l'en-tête que montre un réalignement refusé. Vérifié : 2692 node, 499.
Assisted-by: Claude Opus 5.5
2026-10-07 15:05:59 -04:00
issuesVisibles ( m , [ 'choisirDossier' , 'abandonner' ] ) ;
[ADD] interface: event list, open event, participants, import, plan
The working screens of an evening. The home screen lists the events of
the working folder, asks its question, loads demos, copies, renames,
imports and deletes, and shows what the trash holds. An open event frames
its mode, offers Modifier, Débloquer and a confirmed lock takeover, and
shows its warnings. Participants get a grid, a form and a file; an import
goes through a preview and a report; tables, the default and tours have
their screens; the plan draws tables, chairs, empty seats and names.
Checked: 2342 node, 356 browser, 60 node-long; the Electron start check.
--- FR ---
[ADD] interface : liste, événement, participants, import, tables, plan
Les écrans de travail d'une soirée. L'accueil liste les événements du
dossier de travail, pose sa question, charge les démonstrations, copie,
renomme, importe et supprime, et montre ce que tient la corbeille. Un
événement ouvert encadre son mode, offre Modifier, Débloquer et une
reprise de verrou confirmée, et montre ses avertissements. Les
participants ont une grille, un formulaire et une fiche ; un import passe
par un aperçu et un rapport ; tables, défaut et tours ont leurs écrans ;
le plan dessine tables, chaises, sièges vides et noms.
Vérifié : 2342 node, 356 navigateur, 60 node-long ; démarrage d'Electron.
Assisted-by: Claude Opus 5.5
2026-10-07 08:12:27 -04:00
const choisir = message ( m ) . querySelector ( '[data-commande="choisirDossier"]' ) ;
expect ( choisir . textContent ) . toBe ( t ( 'communs.commande.choisirDossier' ) ) ;
choisir . click ( ) ;
await new Promise ( ( resoudre ) => requestAnimationFrame ( resoudre ) ) ;
await attendre ( ( ) => expect ( m . seance . etat ( ) . supportRompu ) . not . toBeNull ( ) ) ;
expect ( visible ( message ( m ) ) ) . toBe ( true ) ;
} ) ;
test ( 'non publié, « Fermer sans écrire ailleurs », confirmé en place : la liste revient, sans aucune écriture' , async ( ) => {
const m = await neuf ( ) ;
await rompre ( m , 'documents' ) ;
[FIX] event: page never scrolls, broken-support remedies always shown
At 1366 x 700 an open event that shows warnings no longer scrolls the page:
the warnings scroll within their zone and the plan keeps at least 75 % of
its height. A broken support never shrinks, so its three remedies stay in
view, the only way to write the event elsewhere (spec 8.6). Tests now
prove the working-folder path and duplicate nodes are visible, the guard
of « Remplacer » in import, and which header a refused re-alignment shows.
Checked: 2692 node, 499 browser tests, from the index alone.
--- FR ---
[FIX] événement : sans défilement, remèdes du support rompu visibles
À 1366 x 700, un événement ouvert qui montre des avertissements ne fait
plus défiler la page : les avertissements défilent dans leur zone et le
plan garde au moins 75 % de sa hauteur. Un support rompu ne rétrécit
jamais : ses trois remèdes, seul moyen d'écrire l'événement ailleurs
(§ 8.6), restent visibles. Les épreuves prouvent que le chemin et les
doublons du dossier sont visibles, la garde de « Remplacer » à l'import, et
l'en-tête que montre un réalignement refusé. Vérifié : 2692 node, 499.
Assisted-by: Claude Opus 5.5
2026-10-07 15:05:59 -04:00
issuesVisibles ( m , [ 'choisirDossier' , 'abandonner' ] ) ;
[ADD] interface: event list, open event, participants, import, plan
The working screens of an evening. The home screen lists the events of
the working folder, asks its question, loads demos, copies, renames,
imports and deletes, and shows what the trash holds. An open event frames
its mode, offers Modifier, Débloquer and a confirmed lock takeover, and
shows its warnings. Participants get a grid, a form and a file; an import
goes through a preview and a report; tables, the default and tours have
their screens; the plan draws tables, chairs, empty seats and names.
Checked: 2342 node, 356 browser, 60 node-long; the Electron start check.
--- FR ---
[ADD] interface : liste, événement, participants, import, tables, plan
Les écrans de travail d'une soirée. L'accueil liste les événements du
dossier de travail, pose sa question, charge les démonstrations, copie,
renomme, importe et supprime, et montre ce que tient la corbeille. Un
événement ouvert encadre son mode, offre Modifier, Débloquer et une
reprise de verrou confirmée, et montre ses avertissements. Les
participants ont une grille, un formulaire et une fiche ; un import passe
par un aperçu et un rapport ; tables, défaut et tours ont leurs écrans ;
le plan dessine tables, chaises, sièges vides et noms.
Vérifié : 2342 node, 356 navigateur, 60 node-long ; démarrage d'Electron.
Assisted-by: Claude Opus 5.5
2026-10-07 08:12:27 -04:00
const avant = m . trace . length ;
const abandonner = ( ) => message ( m ) . querySelector ( '[data-commande="abandonner"]' ) ;
expect ( abandonner ( ) . textContent ) . toBe ( t ( 'evenement.supportRompu.abandonner' ) ) ;
abandonner ( ) . click ( ) ;
flushSync ( ) ;
expect ( abandonner ( ) . textContent ) . toBe ( t ( 'evenement.supportRompu.confirmerAbandon' ) ) ;
expect ( m . seance . etat ( ) . evenement ) . not . toBeNull ( ) ;
abandonner ( ) . click ( ) ;
await attendre ( ( ) => expect ( ecran ( m ) . dataset . ecran ) . toBe ( 'accueil' ) ) ;
expect ( m . seance . etat ( ) . evenement ) . toBeNull ( ) ;
expect ( m . trace . slice ( avant ) . filter ( ( [ primitive ] ) => primitive !== 'deverrouiller' ) ) . toEqual ( [ ] ) ;
} ) ;
test ( 'non publié, un dossier choisi : état et journal y sont écrits, le geste suivant aussi, et la liste le suit' , async ( ) => {
const m = await neuf ( { systeme : { dossiersChoisis : [ 'D:\\Secours' ] } } ) ;
await rompre ( m , 'documents' ) ;
[FIX] event: page never scrolls, broken-support remedies always shown
At 1366 x 700 an open event that shows warnings no longer scrolls the page:
the warnings scroll within their zone and the plan keeps at least 75 % of
its height. A broken support never shrinks, so its three remedies stay in
view, the only way to write the event elsewhere (spec 8.6). Tests now
prove the working-folder path and duplicate nodes are visible, the guard
of « Remplacer » in import, and which header a refused re-alignment shows.
Checked: 2692 node, 499 browser tests, from the index alone.
--- FR ---
[FIX] événement : sans défilement, remèdes du support rompu visibles
À 1366 x 700, un événement ouvert qui montre des avertissements ne fait
plus défiler la page : les avertissements défilent dans leur zone et le
plan garde au moins 75 % de sa hauteur. Un support rompu ne rétrécit
jamais : ses trois remèdes, seul moyen d'écrire l'événement ailleurs
(§ 8.6), restent visibles. Les épreuves prouvent que le chemin et les
doublons du dossier sont visibles, la garde de « Remplacer » à l'import, et
l'en-tête que montre un réalignement refusé. Vérifié : 2692 node, 499.
Assisted-by: Claude Opus 5.5
2026-10-07 15:05:59 -04:00
issuesVisibles ( m , [ 'choisirDossier' , 'abandonner' ] ) ;
[ADD] interface: event list, open event, participants, import, plan
The working screens of an evening. The home screen lists the events of
the working folder, asks its question, loads demos, copies, renames,
imports and deletes, and shows what the trash holds. An open event frames
its mode, offers Modifier, Débloquer and a confirmed lock takeover, and
shows its warnings. Participants get a grid, a form and a file; an import
goes through a preview and a report; tables, the default and tours have
their screens; the plan draws tables, chairs, empty seats and names.
Checked: 2342 node, 356 browser, 60 node-long; the Electron start check.
--- FR ---
[ADD] interface : liste, événement, participants, import, tables, plan
Les écrans de travail d'une soirée. L'accueil liste les événements du
dossier de travail, pose sa question, charge les démonstrations, copie,
renomme, importe et supprime, et montre ce que tient la corbeille. Un
événement ouvert encadre son mode, offre Modifier, Débloquer et une
reprise de verrou confirmée, et montre ses avertissements. Les
participants ont une grille, un formulaire et une fiche ; un import passe
par un aperçu et un rapport ; tables, défaut et tours ont leurs écrans ;
le plan dessine tables, chaises, sièges vides et noms.
Vérifié : 2342 node, 356 navigateur, 60 node-long ; démarrage d'Electron.
Assisted-by: Claude Opus 5.5
2026-10-07 08:12:27 -04:00
message ( m ) . querySelector ( '[data-commande="choisirDossier"]' ) . click ( ) ;
await attendre ( ( ) => expect ( m . seance . etat ( ) . supportRompu ) . toBeNull ( ) ) ;
expect ( m . seance . etat ( ) . racine ) . toEqual ( { id : 'choisi-1' , chemin : 'D:\\Secours' } ) ;
const base = baseDe ( m ) ;
expect ( m . fs . contenu ( 'choisi-1' , ` ${ base } ${ SUFFIXES . etat } ` ) ) . not . toBeNull ( ) ;
const entrees = journalDe ( m , 'choisi-1' ) . entrees . length ;
await m . seance . executer ( 'ajouterParticipant' , { champs : { nom : NOMS [ 3 ] } } ) ;
expect ( journalDe ( m , 'choisi-1' ) . entrees . length ) . toBe ( entrees + 1 ) ;
commandeListe ( m ) . click ( ) ;
await attendre ( ( ) => expect ( ecran ( m ) . dataset . ecran ) . toBe ( 'accueil' ) ) ;
await attendre ( ( ) => expect ( dans ( m , ` [data-evenement=" ${ base } "] ` ) ) . not . toBeNull ( ) ) ;
} ) ;
} ) ;
describe ( 'le format plus récent (§ 8.8)' , ( ) => {
test ( '« Modifier » est remplacé par l’ explication, et aucune commande de l’ événement ne passe en écriture' , async ( ) => {
const m = await neuf ( ) ;
const base = baseDe ( m ) ;
await m . seance . fermer ( ) ;
const chemin = ` ${ base } ${ SUFFIXES . etat } ` ;
const texte = m . fs . contenu ( 'documents' , chemin ) ;
m . fs . deposer ( 'documents' , chemin , texte . replace ( ` "format": ${ FORMAT } ` , ` "format": ${ FORMAT + 1 } ` ) ) ;
await m . seance . ouvrir ( base ) ;
flushSync ( ) ;
expect ( fixe ( m , 'modifier' ) ) . toBeNull ( ) ;
const explication = dans ( m , '[data-place-fixe] [data-refus="LECTURE_SEULE"]' ) ;
expect ( explication . textContent ) . toContain ( t ( 'LECTURE_SEULE' , { raison : 'FORMAT' , remede : null } ) ) ;
expect ( visible ( explication ) ) . toBe ( true ) ;
const avant = m . trace . length ;
const boutons = tous ( m , '[data-ecran="evenement"] button' ) . filter ( ( bouton ) => bouton . dataset . commande !== 'section' ) ;
expect ( boutons . length ) . toBeGreaterThan ( 0 ) ;
for ( const bouton of boutons ) {
if ( ! bouton . isConnected ) continue ;
bouton . click ( ) ;
bouton . click ( ) ;
await new Promise ( ( resoudre ) => requestAnimationFrame ( resoudre ) ) ;
}
await attendre ( ( ) => expect ( m . seance . etat ( ) . mode ) . toBe ( 'lecture' ) ) ;
expect ( m . trace . length ) . toBe ( avant ) ;
} ) ;
} ) ;