[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)
// La liste des participants dans un vrai moteur de rendu (§ 4, § 4.2, § 4.4,
// § 5.9, § 7.4, § 10.2) : la saisie par le seul nom, l'appartenance
// collante, la grille au clavier, le tri, le filtre et le compte,
// l'exclusion, le manque de places à l'ajout et à la réintégration, la fiche
// et l'itinéraire, l'export et le collage. Chaque épreuve monte App par
// monterApp, sur le système de fichiers simulé et l'horloge pilotée ; chaque
// écriture se relève (trace) et chaque entrée se compte dans le journal. Les
// gestes passent par des entrées que Chromium traite comme réelles
// (test/navigateur/entrees.js). Les noms d'épreuve sont inventés.
//
// Le projet navigateur emploie expect, et test et describe de Vitest
// (§ 14.8).
import { afterEach , beforeEach , describe , expect , test , vi } from 'vitest' ;
import { cdp } from 'vitest/browser' ;
import { flushSync , tick } from 'svelte' ;
import { bilanChangementDefaut } from '../../application/bilans.js' ;
import { TABLES _APPLICATION } from '../../application/libelles.js' ;
import { creerTraduction } from '../../application/traduction.js' ;
import { apercevoir , decoder , exporterParticipants } from '../../application/csv.js' ;
import { STATUT , nombrePlacesManquantes , normaliser } from '../../moteur/configuration.js' ;
import { configurationDepuisCharge } from '../../stockage/document.js' ;
import { lireJournal } from '../../stockage/journal.js' ;
import { SUFFIXES } from '../../stockage/noms.js' ;
import { formeNommee } from '../../stockage/placements.js' ;
import { monterApp } from '../../../test/navigateur/montage.js' ;
import { pointer , touche } from '../../../test/navigateur/entrees.js' ;
import { visible } from '../../../test/navigateur/rendu.js' ;
import CAS _LIMITES from '../../../exemples/participants_cas_limites.csv?url' ;
import { TABLES _INTERFACE } from '../libelles.js' ;
import { COLONNES , cleDeStatut } from './grille.js' ;
const { t } = creerTraduction ( [ ... TABLES _APPLICATION , ... TABLES _INTERFACE ] ) ;
const EVENEMENT = 'Soirée des Merles' ;
const CLUB = 'Club des Merles' ;
// Des noms inventés, hors du réservoir du générateur.
const NOMS = [
'Quenouillet' ,
'Brindelle' ,
'Calvaille' ,
'Doucemer' ,
'Esquirol' ,
'Faverolle' ,
'Glanduc' ,
'Houblonnet' ,
'Isardin' ,
'Joncaille' ,
'Kerbouet' ,
'Lisonnet' ,
'Moucheron' ,
'Nivelette' ,
'Orbelin' ,
'Pistachon' ,
'Rouvrelle' ,
] ;
// Les montages de l'épreuve en cours, démontés après elle ; le collecteur
// n'a rien reçu pendant l'épreuve.
let montes = [ ] ;
let erreursAvant = 0 ;
beforeEach ( ( ) => {
erreursAvant = globalThis . _ _gttErreurs ? . length ? ? 0 ;
} ) ;
afterEach ( ( ) => {
for ( const m of montes . reverse ( ) ) m . demonter ( ) ;
montes = [ ] ;
vi . restoreAllMocks ( ) ;
const recues = globalThis . _ _gttErreurs ? . slice ( erreursAvant ) ? ? [ ] ;
expect ( recues , 'erreurs reçues par le collecteur' ) . toEqual ( [ ] ) ;
} ) ;
async function rendu ( ) {
flushSync ( ) ;
await tick ( ) ;
}
// Attend que la file de la séance ait passé ce que le geste y a posé, par
// une lecture admise dans tout mode qui n'écrit rien, puis le rendu.
async function attendreSeance ( m ) {
await m . seance . formeNommee ( null ) . catch ( ( ) => undefined ) ;
await rendu ( ) ;
await rendu ( ) ;
}
// Un CSV d'épreuve, séparé par « ; » : nom, prénom, appartenance, exclu,
// titre pressenti, notes.
const csv = ( personnes ) =>
[
'nom;prenom;appartenance;exclu;titre_pressenti;notes' ,
... personnes . map ( ( p ) => [ p . nom , p . prenom ? ? '' , p . appartenance ? ? '' , p . exclu ? 'oui' : '' , p . titre ? ? '' , p . notes ? ? '' ] . join ( ';' ) ) ,
] . join ( '\n' ) ;
// Importe un texte par la commande, comme le ferait l'aperçu (I6).
const importer = ( m , texte ) => m . seance . executer ( 'importerParticipants' , { apercu : apercevoir ( texte ) , mode : 'ajouter' } ) ;
// Les octets d'un CSV livré, lus par son adresse.
async function octetsLivres ( url ) {
const reponse = await fetch ( url ) ;
return new Uint8Array ( await reponse . arrayBuffer ( ) ) ;
}
/ * *
* Un montage , un événement créé — sieges places par défaut , tours tours ,
* tables tables rondes , personnes importées — , en écriture , la section des
* participants ouverte .
* /
async function evenementOuvert ( { sieges = 8 , tours = 2 , tables = 0 , personnes = [ ] } = { } ) {
const m = await monterApp ( ) ;
montes . push ( m ) ;
await m . seance . creer ( { nom : EVENEMENT , date : null , siegesParDefaut : sieges , tours } ) ;
await m . seance . passerEnEcriture ( ) ;
for ( let i = 0 ; i < tables ; i += 1 ) await m . seance . executer ( 'creerTable' , { forme : 'ronde' } ) ;
if ( personnes . length > 0 ) await importer ( m , csv ( personnes ) ) ;
m . ui . allerA ( 'participants' ) ;
await rendu ( ) ;
return m ;
}
const base = ( m ) => m . seance . etat ( ) . evenement . base ;
const texteJournal = ( m ) => m . fs . contenu ( 'documents' , ` ${ base ( m ) } ${ SUFFIXES . journal } ` ) ;
const entrees = ( m ) => lireJournal ( texteJournal ( m ) ) . entrees ;
const texteEtat = ( m ) => m . fs . contenu ( 'documents' , ` ${ base ( m ) } ${ SUFFIXES . etat } ` ) ;
const participants = ( m ) => m . seance . etat ( ) . charge . participants ;
const idDe = ( m , nom ) => participants ( m ) . find ( ( p ) => p . nom === nom ) . id ;
const section = ( m ) => m . racine . querySelector ( '[data-section="participants"]' ) ;
const formulaire = ( m ) => section ( m ) . querySelector ( 'form' ) ;
const champ = ( m , cle ) => formulaire ( m ) . querySelector ( ` [data-champ=" ${ cle } "] ` ) ;
const grille = ( m ) => section ( m ) . querySelector ( '[role="grid"]' ) ;
const lignes = ( m ) => [ ... grille ( m ) . querySelectorAll ( '[data-participant]' ) ] ;
const idsAffiches = ( m ) => lignes ( m ) . map ( ( ligne ) => Number ( ligne . dataset . participant ) ) ;
const ligne = ( m , id ) => grille ( m ) . querySelector ( ` [data-participant=" ${ id } "] ` ) ;
const cellule = ( m , id , colonne ) => ligne ( m , id ) . querySelector ( ` [data-champ=" ${ colonne } "] ` ) ;
const commandeDeLigne = ( m , id , nom ) => ligne ( m , id ) . querySelector ( ` [data-commande=" ${ nom } "] ` ) ;
const fiche = ( m ) => section ( m ) . querySelector ( '[data-fiche]' ) ;
const centre = ( element ) => {
const b = element . getBoundingClientRect ( ) ;
return { x : b . left + b . width / 2 , y : b . top + b . height / 2 } ;
} ;
// Un clic réel au centre de l'élément, amené d'abord dans la fenêtre.
async function cliquer ( element ) {
element . scrollIntoView ( { block : 'center' } ) ;
const { x , y } = centre ( element ) ;
await pointer ( 'presser' , x , y ) ;
await pointer ( 'relacher' , x , y ) ;
await rendu ( ) ;
}
// Remplace le texte d'un champ par une saisie réelle : le champ prend le
// focus, son texte est sélectionné, puis remplacé par l'insertion du texte,
// ou effacé.
async function saisir ( entree , texte ) {
entree . focus ( ) ;
entree . select ( ) ;
if ( texte === '' ) await touche ( 'Backspace' ) ;
else await cdp ( ) . send ( 'Input.insertText' , { text : texte } ) ;
await rendu ( ) ;
}
// Ajoute une personne par le formulaire : chaque champ donné saisi, puis
// Entrée dans le champ du nom.
async function ajouter ( m , champs ) {
for ( const [ cle , valeur ] of Object . entries ( champs ) ) await saisir ( champ ( m , cle ) , valeur ) ;
champ ( m , 'nom' ) . focus ( ) ;
await touche ( 'Enter' ) ;
await attendreSeance ( m ) ;
}
// Maj+Tab réel : touche() ne porte pas de modificateur ; 8 est le masque de
// Maj dans Input.dispatchKeyEvent.
async function majTab ( ) {
const commun = { key : 'Tab' , code : 'Tab' , windowsVirtualKeyCode : 9 , nativeVirtualKeyCode : 9 , modifiers : 8 } ;
await cdp ( ) . send ( 'Input.dispatchKeyEvent' , { type : 'rawKeyDown' , ... commun } ) ;
await cdp ( ) . send ( 'Input.dispatchKeyEvent' , { type : 'keyUp' , ... commun } ) ;
}
[FIX] iteration 3 review: seat targets, gestures, warnings, settings
The wheel no longer zooms during a gesture, so a drop lands under the
ghost. A seat is a target the size of its pitch. A floor drop refuses to
remove an every-tour reservation. A seat reservation the retained plan
does not hold is flagged at the seat, with its swap as remedy. DERIVE names
the person; capacities have an upper bound; Generate waits for a pending
setting; a worker that fails to load shows a refusal; § 5.3 constraints
get their switches. Commands are not re-entrant and a double click does not
confirm. Checked: 2755 node, 550 browser tests, from the index alone.
--- FR ---
[FIX] revue de l'itération 3 : cibles, gestes, avertissements, réglages
La molette ne zoome plus pendant un geste : le dépôt tombe sous le fantôme.
Un siège est une cible de la taille de son pas. Un dépôt sur le sol refuse
de retirer une réservation de tous les tours. Une réservation à un siège
que le retenu ne tient pas est relevée au siège, l'échange en remède. La
dérive nomme la personne ; les capacités ont une borne ; Générer attend un
réglage en cours ; un travailleur qui ne se charge pas donne un refus ; le
§ 5.3 a ses bascules. Les commandes ne sont pas ré-entrantes et un double
clic ne confirme pas. Vérifié : 2755 node, 550 navigateur, index seul.
Assisted-by: Claude Opus 5.5
2026-10-07 17:50:35 -04:00
// Maj+Entrée réel, le saut de ligne d'une zone de texte ; 8 est le masque
// de Maj.
async function majEntree ( ) {
const commun = { key : 'Enter' , code : 'Enter' , windowsVirtualKeyCode : 13 , nativeVirtualKeyCode : 13 , modifiers : 8 } ;
await cdp ( ) . send ( 'Input.dispatchKeyEvent' , { type : 'keyDown' , ... commun , text : '\r' , unmodifiedText : '\r' } ) ;
await cdp ( ) . send ( 'Input.dispatchKeyEvent' , { type : 'keyUp' , ... commun } ) ;
await rendu ( ) ;
}
[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
// La valeur de la fiche que nomme l'étiquette de clé donnée.
function valeurDeFiche ( m , cle ) {
const etiquette = [ ... fiche ( m ) . querySelectorAll ( 'dt' ) ] . find ( ( dt ) => dt . textContent === t ( cle ) ) ;
expect ( etiquette , cle ) . toBeDefined ( ) ;
return etiquette . nextElementSibling ;
}
// Le tableau de la fiche que titre la légende de clé donnée.
const tableauDeFiche = ( m , cle ) => [ ... fiche ( m ) . querySelectorAll ( 'table' ) ] . find ( ( tableau ) => tableau . caption ? . textContent === t ( cle ) ) ;
// L'écart entre deux boîtes : 0 quand elles se touchent ou se chevauchent.
function ecart ( a , b ) {
const ba = a . getBoundingClientRect ( ) ;
const bb = b . getBoundingClientRect ( ) ;
const dx = Math . max ( 0 , ba . left - bb . right , bb . left - ba . right ) ;
const dy = Math . max ( 0 , ba . top - bb . bottom , bb . top - ba . bottom ) ;
return Math . max ( dx , dy ) ;
}
[FIX] iteration 3 review: seat targets, gestures, warnings, settings
The wheel no longer zooms during a gesture, so a drop lands under the
ghost. A seat is a target the size of its pitch. A floor drop refuses to
remove an every-tour reservation. A seat reservation the retained plan
does not hold is flagged at the seat, with its swap as remedy. DERIVE names
the person; capacities have an upper bound; Generate waits for a pending
setting; a worker that fails to load shows a refusal; § 5.3 constraints
get their switches. Commands are not re-entrant and a double click does not
confirm. Checked: 2755 node, 550 browser tests, from the index alone.
--- FR ---
[FIX] revue de l'itération 3 : cibles, gestes, avertissements, réglages
La molette ne zoome plus pendant un geste : le dépôt tombe sous le fantôme.
Un siège est une cible de la taille de son pas. Un dépôt sur le sol refuse
de retirer une réservation de tous les tours. Une réservation à un siège
que le retenu ne tient pas est relevée au siège, l'échange en remède. La
dérive nomme la personne ; les capacités ont une borne ; Générer attend un
réglage en cours ; un travailleur qui ne se charge pas donne un refus ; le
§ 5.3 a ses bascules. Les commandes ne sont pas ré-entrantes et un double
clic ne confirme pas. Vérifié : 2755 node, 550 navigateur, index seul.
Assisted-by: Claude Opus 5.5
2026-10-07 17:50:35 -04:00
// Retient les ajouts de ligne au journal du système simulé jusqu'à
// liberer() : l'écriture d'un geste reste en cours, comme sur un support
// lent, sans délai ni minuterie. L'application lit la primitive sur fs au
// moment du geste.
function retenirJournal ( m ) {
const ajouterLigne = m . fs . ajouterLigne ;
let liberer ;
const porte = new Promise ( ( resoudre ) => {
liberer = resoudre ;
} ) ;
m . fs . ajouterLigne = async ( ... parametres ) => {
await porte ;
return ajouterLigne . apply ( m . fs , parametres ) ;
} ;
return ( ) => liberer ( ) ;
}
[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 aucuneFenetre = ( ) => expect ( document . querySelectorAll ( 'dialog[open], [role=dialog], [role=alertdialog]' ) . length ) . toBe ( 0 ) ;
describe ( 'la saisie (§ 4, § 10.2)' , ( ) => {
test ( 'seul le nom : « Pervenche » enregistre une personne aux autres champs null, en une entrée ; un nom tapé en NFD est enregistré en NFC' , async ( ) => {
const m = await evenementOuvert ( ) ;
const avant = entrees ( m ) . length ;
await ajouter ( m , { nom : 'Pervenche' } ) ;
expect ( entrees ( m ) ) . toHaveLength ( avant + 1 ) ;
const [ { id , ... champs } ] = participants ( m ) ;
expect ( Number . isInteger ( id ) ) . toBe ( true ) ;
expect ( champs ) . toEqual ( {
nom : 'Pervenche' ,
prenom : null ,
appartenance : null ,
courriel : null ,
titrePressenti : null ,
notes : null ,
exclu : false ,
} ) ;
expect ( champ ( m , 'nom' ) . value ) . toBe ( '' ) ;
const decompose = 'Ombre\u{0301}lle' ;
expect ( decompose . length ) . toBe ( 9 ) ;
await ajouter ( m , { nom : decompose } ) ;
expect ( entrees ( m ) ) . toHaveLength ( avant + 2 ) ;
expect ( participants ( m ) . at ( - 1 ) . nom ) . toBe ( 'Ombr\u{00E9}lle' ) ;
} ) ;
[FIX] iteration 3 review: seat targets, gestures, warnings, settings
The wheel no longer zooms during a gesture, so a drop lands under the
ghost. A seat is a target the size of its pitch. A floor drop refuses to
remove an every-tour reservation. A seat reservation the retained plan
does not hold is flagged at the seat, with its swap as remedy. DERIVE names
the person; capacities have an upper bound; Generate waits for a pending
setting; a worker that fails to load shows a refusal; § 5.3 constraints
get their switches. Commands are not re-entrant and a double click does not
confirm. Checked: 2755 node, 550 browser tests, from the index alone.
--- FR ---
[FIX] revue de l'itération 3 : cibles, gestes, avertissements, réglages
La molette ne zoome plus pendant un geste : le dépôt tombe sous le fantôme.
Un siège est une cible de la taille de son pas. Un dépôt sur le sol refuse
de retirer une réservation de tous les tours. Une réservation à un siège
que le retenu ne tient pas est relevée au siège, l'échange en remède. La
dérive nomme la personne ; les capacités ont une borne ; Générer attend un
réglage en cours ; un travailleur qui ne se charge pas donne un refus ; le
§ 5.3 a ses bascules. Les commandes ne sont pas ré-entrantes et un double
clic ne confirme pas. Vérifié : 2755 node, 550 navigateur, index seul.
Assisted-by: Claude Opus 5.5
2026-10-07 17:50:35 -04:00
test ( 'une seconde Entrée pendant l’ écriture n’ ajoute rien ; ce qui est tapé pendant l’ écriture reste, ce qui a été envoyé se vide (§ 10.2)' , async ( ) => {
const m = await evenementOuvert ( ) ;
const avant = entrees ( m ) . length ;
const liberer = retenirJournal ( m ) ;
const ajout = ( ) => section ( m ) . querySelector ( 'form [data-commande="ajouter"]' ) ;
expect ( ajout ( ) ) . not . toBeNull ( ) ;
const styleRepos = getComputedStyle ( ajout ( ) ) . borderTopStyle ;
expect ( ajout ( ) . getAttribute ( 'aria-busy' ) ) . toBeNull ( ) ;
expect ( ajout ( ) . getAttribute ( 'aria-disabled' ) ) . toBeNull ( ) ;
await saisir ( champ ( m , 'nom' ) , 'Ombrelle' ) ;
await saisir ( champ ( m , 'notes' ) , 'arrive tard' ) ;
champ ( m , 'nom' ) . focus ( ) ;
await touche ( 'Enter' ) ;
await rendu ( ) ;
// L'écriture retenue : « Ajouter » se dit occupé, d'un filet tireté.
expect ( ajout ( ) . getAttribute ( 'aria-busy' ) ) . toBe ( 'true' ) ;
expect ( ajout ( ) . getAttribute ( 'aria-disabled' ) ) . toBe ( 'true' ) ;
expect ( getComputedStyle ( ajout ( ) ) . borderTopStyle ) . toBe ( 'dashed' ) ;
expect ( getComputedStyle ( ajout ( ) ) . borderTopStyle ) . not . toBe ( styleRepos ) ;
await touche ( 'Enter' ) ;
await rendu ( ) ;
// La personne suivante se tape pendant l'écriture de la première.
await saisir ( champ ( m , 'prenom' ) , 'Théo' ) ;
await saisir ( champ ( m , 'nom' ) , 'Pervenche' ) ;
liberer ( ) ;
await vi . waitFor ( ( ) => expect ( participants ( m ) ) . toHaveLength ( 1 ) ) ;
await attendreSeance ( m ) ;
await rendu ( ) ;
expect ( ajout ( ) . getAttribute ( 'aria-busy' ) ) . toBeNull ( ) ;
expect ( ajout ( ) . getAttribute ( 'aria-disabled' ) ) . toBeNull ( ) ;
expect ( getComputedStyle ( ajout ( ) ) . borderTopStyle ) . toBe ( styleRepos ) ;
expect ( participants ( m ) ) . toHaveLength ( 1 ) ;
expect ( participants ( m ) [ 0 ] ) . toMatchObject ( { nom : 'Ombrelle' , prenom : null , notes : 'arrive tard' } ) ;
expect ( entrees ( m ) ) . toHaveLength ( avant + 1 ) ;
expect ( champ ( m , 'nom' ) . value ) . toBe ( 'Pervenche' ) ;
expect ( champ ( m , 'prenom' ) . value ) . toBe ( 'Théo' ) ;
expect ( champ ( m , 'notes' ) . value ) . toBe ( '' ) ;
// La saisie gardée s'envoie à son tour.
champ ( m , 'nom' ) . focus ( ) ;
await touche ( 'Enter' ) ;
await attendreSeance ( m ) ;
expect ( participants ( m ) . map ( ( p ) => [ p . nom , p . prenom ] ) ) . toEqual ( [
[ 'Ombrelle' , null ] ,
[ 'Pervenche' , 'Théo' ] ,
] ) ;
} ) ;
[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
test ( 'nom vide : « ␠␠ » pose sur le champ aria-invalid et un message visible qui le jouxte ; aucune fenêtre, zéro écriture' , async ( ) => {
const m = await evenementOuvert ( ) ;
const trace = m . trace . length ;
const etat = texteEtat ( m ) ;
await ajouter ( m , { nom : ' ' } ) ;
const entree = champ ( m , 'nom' ) ;
expect ( entree . getAttribute ( 'aria-invalid' ) ) . toBe ( 'true' ) ;
const message = document . getElementById ( entree . getAttribute ( 'aria-describedby' ) ) ;
expect ( message ) . not . toBeNull ( ) ;
expect ( message . textContent ) . toContain ( t ( 'NOM_REQUIS' , { } ) ) ;
expect ( visible ( message ) ) . toBe ( true ) ;
expect ( ecart ( entree , message ) ) . toBeLessThanOrEqual ( 16 ) ;
aucuneFenetre ( ) ;
expect ( m . trace . length ) . toBe ( trace ) ;
expect ( texteEtat ( m ) ) . toBe ( etat ) ;
expect ( participants ( m ) ) . toHaveLength ( 0 ) ;
} ) ;
test ( 'appartenance collante : trois personnes portent « Club des Merles » ; le champ vidé, la quatrième n’ en porte aucune' , async ( ) => {
const m = await evenementOuvert ( ) ;
await ajouter ( m , { nom : 'Ombrelle' , prenom : 'Iris' , appartenance : CLUB } ) ;
expect ( champ ( m , 'appartenance' ) . value ) . toBe ( CLUB ) ;
expect ( champ ( m , 'prenom' ) . value ) . toBe ( '' ) ;
await ajouter ( m , { nom : 'Pervenche' } ) ;
await ajouter ( m , { nom : 'Lacasse' } ) ;
expect ( participants ( m ) . map ( ( p ) => [ p . nom , p . prenom , p . appartenance ] ) ) . toEqual ( [
[ 'Ombrelle' , 'Iris' , CLUB ] ,
[ 'Pervenche' , null , CLUB ] ,
[ 'Lacasse' , null , CLUB ] ,
] ) ;
await saisir ( champ ( m , 'appartenance' ) , '' ) ;
await ajouter ( m , { nom : 'Grisaille' } ) ;
expect ( participants ( m ) . at ( - 1 ) . appartenance ) . toBeNull ( ) ;
} ) ;
[FIX] iteration 3 review: seat targets, gestures, warnings, settings
The wheel no longer zooms during a gesture, so a drop lands under the
ghost. A seat is a target the size of its pitch. A floor drop refuses to
remove an every-tour reservation. A seat reservation the retained plan
does not hold is flagged at the seat, with its swap as remedy. DERIVE names
the person; capacities have an upper bound; Generate waits for a pending
setting; a worker that fails to load shows a refusal; § 5.3 constraints
get their switches. Commands are not re-entrant and a double click does not
confirm. Checked: 2755 node, 550 browser tests, from the index alone.
--- FR ---
[FIX] revue de l'itération 3 : cibles, gestes, avertissements, réglages
La molette ne zoome plus pendant un geste : le dépôt tombe sous le fantôme.
Un siège est une cible de la taille de son pas. Un dépôt sur le sol refuse
de retirer une réservation de tous les tours. Une réservation à un siège
que le retenu ne tient pas est relevée au siège, l'échange en remède. La
dérive nomme la personne ; les capacités ont une borne ; Générer attend un
réglage en cours ; un travailleur qui ne se charge pas donne un refus ; le
§ 5.3 a ses bascules. Les commandes ne sont pas ré-entrantes et un double
clic ne confirme pas. Vérifié : 2755 node, 550 navigateur, index seul.
Assisted-by: Claude Opus 5.5
2026-10-07 17:50:35 -04:00
test ( 'l’ appartenance collante survit à un aller-retour par le plan et par « À propos » ; un autre événement repart d’ un champ vide' , async ( ) => {
const m = await evenementOuvert ( ) ;
await ajouter ( m , { nom : 'Ombrelle' , appartenance : CLUB } ) ;
m . ui . allerA ( 'plan' ) ;
await rendu ( ) ;
m . ui . allerA ( 'participants' ) ;
await rendu ( ) ;
expect ( champ ( m , 'appartenance' ) . value ) . toBe ( CLUB ) ;
m . ui . ouvrirApropos ( ) ;
await rendu ( ) ;
m . ui . fermerApropos ( ) ;
await rendu ( ) ;
expect ( champ ( m , 'appartenance' ) . value ) . toBe ( CLUB ) ;
await ajouter ( m , { nom : 'Pervenche' } ) ;
expect ( participants ( m ) . at ( - 1 ) . appartenance ) . toBe ( CLUB ) ;
await m . seance . fermer ( ) ;
await m . seance . creer ( { nom : 'Bal des Grisailles' , date : null , siegesParDefaut : 8 , tours : 2 } ) ;
m . ui . allerA ( 'participants' ) ;
await rendu ( ) ;
expect ( champ ( m , 'appartenance' ) . value ) . toBe ( '' ) ;
} ) ;
[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
test ( 'une appartenance qui ne diffère d’ une existante que par la casse et les blancs est signalée sur le champ, l’ orthographe existante à reprendre ; rien ne fusionne seul' , async ( ) => {
const m = await evenementOuvert ( { personnes : [ { nom : 'Ombrelle' , prenom : 'Iris' , appartenance : CLUB } ] } ) ;
const trace = m . trace . length ;
await saisir ( champ ( m , 'appartenance' ) , 'club des merles' ) ;
const signal = formulaire ( m ) . querySelector ( '[data-avertissement]' ) ;
expect ( signal ) . not . toBeNull ( ) ;
expect ( visible ( signal ) ) . toBe ( true ) ;
expect ( signal . textContent ) . toContain ( CLUB ) ;
expect ( champ ( m , 'appartenance' ) . getAttribute ( 'aria-describedby' ) ) . toContain ( signal . id ) ;
expect ( champ ( m , 'appartenance' ) . value ) . toBe ( 'club des merles' ) ;
await cliquer ( signal . querySelector ( '[data-commande="reprendreOrthographe"]' ) ) ;
expect ( champ ( m , 'appartenance' ) . value ) . toBe ( CLUB ) ;
expect ( formulaire ( m ) . querySelector ( '[data-avertissement]' ) ) . toBeNull ( ) ;
expect ( m . trace . length ) . toBe ( trace ) ;
} ) ;
} ) ;
describe ( 'la grille (§ 10.2, § 19.3)' , ( ) => {
const TROIS = [
{ nom : 'Ombrelle' , prenom : 'Iris' , appartenance : CLUB } ,
{ nom : 'Pervenche' , prenom : 'Théo' , appartenance : 'CLUB DES MERLES' } ,
{ nom : 'Lacasse' , prenom : 'Ondine' } ,
{ nom : 'Grisaille' } ,
] ;
test ( 'au clavier seul : le courriel d’ Ondine Lacasse changé en une entrée ; Échap rend la valeur, valider sans changement n’ écrit rien' , async ( ) => {
const m = await evenementOuvert ( { personnes : TROIS } ) ;
const ondine = idDe ( m , 'Lacasse' ) ;
const rang = idsAffiches ( m ) . indexOf ( ondine ) ;
const colonne = COLONNES . indexOf ( 'courriel' ) ;
expect ( rang ) . toBeGreaterThan ( 0 ) ;
expect ( colonne ) . toBeGreaterThan ( 0 ) ;
const premiere = grille ( m ) . querySelector ( '[tabindex="0"]' ) ;
expect ( premiere ) . toBe ( cellule ( m , idsAffiches ( m ) [ 0 ] , COLONNES [ 0 ] ) ) ;
premiere . focus ( ) ;
for ( let i = 0 ; i < rang ; i += 1 ) await touche ( 'ArrowDown' ) ;
for ( let i = 0 ; i < colonne ; i += 1 ) await touche ( 'ArrowRight' ) ;
await rendu ( ) ;
const cible = cellule ( m , ondine , 'courriel' ) ;
expect ( document . activeElement ) . toBe ( cible ) ;
const avant = entrees ( m ) . length ;
await touche ( 'Enter' ) ;
await rendu ( ) ;
for ( const caractere of 'ondine@exemple.test' ) await touche ( caractere ) ;
await touche ( 'Enter' ) ;
await attendreSeance ( m ) ;
expect ( entrees ( m ) ) . toHaveLength ( avant + 1 ) ;
expect ( entrees ( m ) . at ( - 1 ) . libelle ) . toBe ( 'Modifié Ondine Lacasse : courriel' ) ;
expect ( participants ( m ) . find ( ( p ) => p . id === ondine ) . courriel ) . toBe ( 'ondine@exemple.test' ) ;
expect ( cellule ( m , ondine , 'courriel' ) . textContent . trim ( ) ) . toBe ( 'ondine@exemple.test' ) ;
// Échap en cours d'édition : rien ne s'écrit, la valeur d'origine revient.
const trace = m . trace . length ;
cellule ( m , ondine , 'courriel' ) . focus ( ) ;
await touche ( 'Enter' ) ;
await rendu ( ) ;
for ( const caractere of 'zz' ) await touche ( caractere ) ;
await touche ( 'Escape' ) ;
await attendreSeance ( m ) ;
expect ( entrees ( m ) ) . toHaveLength ( avant + 1 ) ;
expect ( cellule ( m , ondine , 'courriel' ) . querySelector ( 'input' ) ) . toBeNull ( ) ;
expect ( cellule ( m , ondine , 'courriel' ) . textContent . trim ( ) ) . toBe ( 'ondine@exemple.test' ) ;
// Valider sans changement : aucune commande ne part, donc aucun refus
// SANS_EFFET ne se pose sur la cellule, et rien ne s'écrit.
const executer = vi . spyOn ( m . seance , 'executer' ) ;
cellule ( m , ondine , 'courriel' ) . focus ( ) ;
await touche ( 'Enter' ) ;
await rendu ( ) ;
await touche ( 'Enter' ) ;
await attendreSeance ( m ) ;
expect ( executer ) . not . toHaveBeenCalled ( ) ;
expect ( cellule ( m , ondine , 'courriel' ) . querySelector ( '[data-refus]' ) ) . toBeNull ( ) ;
expect ( cellule ( m , ondine , 'courriel' ) . querySelector ( '[aria-invalid]' ) ) . toBeNull ( ) ;
expect ( cellule ( m , ondine , 'courriel' ) . querySelector ( 'input' ) ) . toBeNull ( ) ;
expect ( entrees ( m ) ) . toHaveLength ( avant + 1 ) ;
expect ( m . trace . length ) . toBe ( trace ) ;
} ) ;
test ( 'Tab après une saisie qui déplace la ligne au tri mène à la cellule suivante de la même personne' , async ( ) => {
const m = await evenementOuvert ( { personnes : TROIS } ) ;
await cliquer ( section ( m ) . querySelector ( '[data-commande="trier"][data-cible="nom"]' ) ) ;
const premier = idsAffiches ( m ) [ 0 ] ;
expect ( participants ( m ) . find ( ( p ) => p . id === premier ) . nom ) . toBe ( 'Grisaille' ) ;
cellule ( m , premier , 'nom' ) . focus ( ) ;
await touche ( 'Enter' ) ;
await rendu ( ) ;
await saisir ( cellule ( m , premier , 'nom' ) . querySelector ( 'input' ) , 'Zed' ) ;
await touche ( 'Tab' ) ;
await attendreSeance ( m ) ;
expect ( participants ( m ) . find ( ( p ) => p . id === premier ) . nom ) . toBe ( 'Zed' ) ;
expect ( idsAffiches ( m ) . at ( - 1 ) ) . toBe ( premier ) ;
expect ( document . activeElement ) . toBe ( cellule ( m , premier , COLONNES [ 1 ] ) ) ;
} ) ;
test ( 'Tab après une saisie qui retire la ligne du filtre mène à la première cellule de la personne qui la suivait' , async ( ) => {
const m = await evenementOuvert ( { personnes : TROIS } ) ;
await cliquer ( section ( m ) . querySelector ( '[data-commande="trier"][data-cible="nom"]' ) ) ;
await saisir ( section ( m ) . querySelector ( '[data-champ="filtre"]' ) , 'merles' ) ;
const [ premier , second ] = idsAffiches ( m ) ;
expect ( idsAffiches ( m ) ) . toHaveLength ( 2 ) ;
cellule ( m , premier , 'appartenance' ) . focus ( ) ;
await touche ( 'Enter' ) ;
await rendu ( ) ;
await saisir ( cellule ( m , premier , 'appartenance' ) . querySelector ( 'input' ) , 'Cercle des Pinsons' ) ;
await touche ( 'Tab' ) ;
await attendreSeance ( m ) ;
expect ( idsAffiches ( m ) ) . toEqual ( [ second ] ) ;
expect ( document . activeElement ) . toBe ( cellule ( m , second , COLONNES [ 0 ] ) ) ;
} ) ;
test ( 'Maj+Tab après une saisie qui retire la ligne du filtre mène à la dernière cellule de la personne qui la précédait' , async ( ) => {
const m = await evenementOuvert ( { personnes : TROIS } ) ;
await cliquer ( section ( m ) . querySelector ( '[data-commande="trier"][data-cible="nom"]' ) ) ;
await saisir ( section ( m ) . querySelector ( '[data-champ="filtre"]' ) , 'merles' ) ;
const [ premier , second ] = idsAffiches ( m ) ;
expect ( idsAffiches ( m ) ) . toHaveLength ( 2 ) ;
cellule ( m , second , 'appartenance' ) . focus ( ) ;
await touche ( 'Enter' ) ;
await rendu ( ) ;
await saisir ( cellule ( m , second , 'appartenance' ) . querySelector ( 'input' ) , 'Cercle des Pinsons' ) ;
await majTab ( ) ;
await attendreSeance ( m ) ;
expect ( idsAffiches ( m ) ) . toEqual ( [ premier ] ) ;
expect ( document . activeElement ) . toBe ( cellule ( m , premier , COLONNES . at ( - 1 ) ) ) ;
} ) ;
test ( 'une note de deux lignes, retouchée dans la grille, garde son saut de ligne' , async ( ) => {
const m = await evenementOuvert ( ) ;
await m . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Lacasse' , prenom : 'Ondine' , notes : 'arrive tard\nrepart tôt' } } ) ;
await rendu ( ) ;
const id = idDe ( m , 'Lacasse' ) ;
const avant = entrees ( m ) . length ;
cellule ( m , id , 'notes' ) . focus ( ) ;
await touche ( 'Enter' ) ;
await rendu ( ) ;
for ( const caractere of ' x' ) await touche ( caractere ) ;
await touche ( 'Enter' ) ;
await attendreSeance ( m ) ;
expect ( entrees ( m ) ) . toHaveLength ( avant + 1 ) ;
expect ( participants ( m ) . find ( ( p ) => p . id === id ) . notes ) . toBe ( 'arrive tard\nrepart tôt x' ) ;
} ) ;
[FIX] iteration 3 review: seat targets, gestures, warnings, settings
The wheel no longer zooms during a gesture, so a drop lands under the
ghost. A seat is a target the size of its pitch. A floor drop refuses to
remove an every-tour reservation. A seat reservation the retained plan
does not hold is flagged at the seat, with its swap as remedy. DERIVE names
the person; capacities have an upper bound; Generate waits for a pending
setting; a worker that fails to load shows a refusal; § 5.3 constraints
get their switches. Commands are not re-entrant and a double click does not
confirm. Checked: 2755 node, 550 browser tests, from the index alone.
--- FR ---
[FIX] revue de l'itération 3 : cibles, gestes, avertissements, réglages
La molette ne zoome plus pendant un geste : le dépôt tombe sous le fantôme.
Un siège est une cible de la taille de son pas. Un dépôt sur le sol refuse
de retirer une réservation de tous les tours. Une réservation à un siège
que le retenu ne tient pas est relevée au siège, l'échange en remède. La
dérive nomme la personne ; les capacités ont une borne ; Générer attend un
réglage en cours ; un travailleur qui ne se charge pas donne un refus ; le
§ 5.3 a ses bascules. Les commandes ne sont pas ré-entrantes et un double
clic ne confirme pas. Vérifié : 2755 node, 550 navigateur, index seul.
Assisted-by: Claude Opus 5.5
2026-10-07 17:50:35 -04:00
test ( 'une note saisie sur deux lignes : Maj+Entrée insère le saut de ligne, dans la grille comme au formulaire ; Entrée valide (§ 10.2, § 10.3)' , async ( ) => {
const m = await evenementOuvert ( ) ;
// Au formulaire : la note de deux lignes, puis Entrée dans les notes envoie.
await saisir ( champ ( m , 'nom' ) , 'Lacasse' ) ;
champ ( m , 'notes' ) . focus ( ) ;
for ( const caractere of 'a' ) await touche ( caractere ) ;
await majEntree ( ) ;
for ( const caractere of 'b' ) await touche ( caractere ) ;
await touche ( 'Enter' ) ;
await attendreSeance ( m ) ;
expect ( participants ( m ) ) . toHaveLength ( 1 ) ;
expect ( participants ( m ) [ 0 ] . notes ) . toBe ( 'a\nb' ) ;
// Dans la grille : la cellule des notes, éditée, reçoit une troisième ligne.
const id = idDe ( m , 'Lacasse' ) ;
const avant = entrees ( m ) . length ;
cellule ( m , id , 'notes' ) . focus ( ) ;
await touche ( 'Enter' ) ;
await rendu ( ) ;
await majEntree ( ) ;
for ( const caractere of 'c' ) await touche ( caractere ) ;
await rendu ( ) ;
expect ( entrees ( m ) ) . toHaveLength ( avant ) ;
await touche ( 'Enter' ) ;
await attendreSeance ( m ) ;
expect ( entrees ( m ) ) . toHaveLength ( avant + 1 ) ;
expect ( participants ( m ) [ 0 ] . notes ) . toBe ( 'a\nb\nc' ) ;
} ) ;
[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
test ( 'trier par nom, croissant puis décroissant, filtrer « merles » : l’ ordre et les lignes suivent ; rien ne s’ écrit, en écriture comme en lecture' , async ( ) => {
const m = await evenementOuvert ( { personnes : TROIS } ) ;
const parNom = ( sens ) =>
[ ... participants ( m ) ]
. sort ( ( a , b ) => ( a . nom < b . nom ? - 1 : a . nom > b . nom ? 1 : a . id - b . id ) * ( sens === 'croissant' ? 1 : - 1 ) )
. map ( ( p ) => p . id ) ;
const merles = participants ( m )
. filter ( ( p ) => p . appartenance !== null )
. map ( ( p ) => p . id ) ;
expect ( merles ) . toHaveLength ( 2 ) ;
for ( const mode of [ 'ecriture' , 'lecture' ] ) {
if ( mode === 'lecture' ) {
await m . seance . passerEnLecture ( ) ;
await rendu ( ) ;
}
const journal = texteJournal ( m ) ;
const etat = texteEtat ( m ) ;
const trace = m . trace . length ;
const trier = section ( m ) . querySelector ( '[data-commande="trier"][data-cible="nom"]' ) ;
await cliquer ( trier ) ;
expect ( idsAffiches ( m ) , mode ) . toEqual ( parNom ( 'croissant' ) ) ;
await cliquer ( section ( m ) . querySelector ( '[data-commande="trier"][data-cible="nom"]' ) ) ;
expect ( idsAffiches ( m ) , mode ) . toEqual ( parNom ( 'decroissant' ) ) ;
await saisir ( section ( m ) . querySelector ( '[data-champ="filtre"]' ) , 'merles' ) ;
expect ( [ ... idsAffiches ( m ) ] . sort ( ( a , b ) => a - b ) , mode ) . toEqual ( merles ) ;
expect ( section ( m ) . querySelector ( 'output' ) . textContent ) . toBe ( t ( 'participants.grille.compte' , { total : 4 , affiches : 2 } ) ) ;
await saisir ( section ( m ) . querySelector ( '[data-champ="filtre"]' ) , '' ) ;
expect ( idsAffiches ( m ) ) . toHaveLength ( 4 ) ;
await attendreSeance ( m ) ;
expect ( texteJournal ( m ) , mode ) . toBe ( journal ) ;
expect ( texteEtat ( m ) , mode ) . toBe ( etat ) ;
expect ( m . trace . length , mode ) . toBe ( trace ) ;
}
} ) ;
test ( 'après l’ import des cas limites, le compte affiché égale le nombre de lignes importées ; une personne exclue reste visible' , async ( ) => {
const m = await evenementOuvert ( ) ;
const octets = await octetsLivres ( CAS _LIMITES ) ;
const { resume } = await m . seance . executer ( 'importerParticipants' , {
apercu : apercevoir ( decoder ( octets ) . texte ) ,
mode : 'ajouter' ,
} ) ;
await rendu ( ) ;
expect ( resume . ajoutes ) . toBeGreaterThan ( 0 ) ;
expect ( participants ( m ) ) . toHaveLength ( resume . ajoutes ) ;
expect ( lignes ( m ) ) . toHaveLength ( resume . ajoutes ) ;
expect ( section ( m ) . querySelector ( 'output' ) . textContent ) . toBe (
t ( 'participants.grille.compte' , { total : resume . ajoutes , affiches : resume . ajoutes } ) ,
) ;
const exclue = participants ( m ) . find ( ( p ) => p . exclu ) ;
expect ( exclue ) . toBeDefined ( ) ;
expect ( cellule ( m , exclue . id , 'exclu' ) . textContent . trim ( ) ) . toBe ( t ( 'participants.grille.exclue' ) ) ;
} ) ;
test ( 'exclusion : douze personnes, en exclure une → douze lignes, la sienne porte « exclue » en texte ; le bilan compte onze participants' , async ( ) => {
const m = await evenementOuvert ( { personnes : NOMS . slice ( 0 , 12 ) . map ( ( nom ) => ( { nom } ) ) } ) ;
const id = idDe ( m , NOMS [ 3 ] ) ;
const avant = entrees ( m ) . length ;
await cliquer ( commandeDeLigne ( m , id , 'exclureParticipant' ) ) ;
await attendreSeance ( m ) ;
expect ( entrees ( m ) ) . toHaveLength ( avant + 1 ) ;
expect ( lignes ( m ) ) . toHaveLength ( 12 ) ;
expect ( cellule ( m , id , 'exclu' ) . textContent . trim ( ) ) . toBe ( t ( 'participants.grille.exclue' ) ) ;
const autres = participants ( m ) . filter ( ( p ) => p . id !== id ) ;
expect ( autres ) . toHaveLength ( 11 ) ;
// La marque est un texte, distinct de celui d'une personne présente.
for ( const p of autres ) expect ( cellule ( m , p . id , 'exclu' ) . textContent . trim ( ) ) . toBe ( t ( 'participants.grille.presente' ) ) ;
expect ( t ( 'participants.grille.presente' ) ) . not . toBe ( t ( 'participants.grille.exclue' ) ) ;
expect ( bilanChangementDefaut ( m . seance . etat ( ) . charge , 8 ) . participants ) . toBe ( 11 ) ;
} ) ;
test ( 'notes jamais interprétées : « <b>x</b> & =1+1 » s’ affiche tel quel dans la grille et sur la fiche, sans élément <b>' , async ( ) => {
const m = await evenementOuvert ( ) ;
const notes = '<b>x</b> & =1+1' ;
await m . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ombrelle' , notes } } ) ;
await rendu ( ) ;
const id = idDe ( m , 'Ombrelle' ) ;
expect ( cellule ( m , id , 'notes' ) . textContent . trim ( ) ) . toBe ( notes ) ;
m . ui . ouvrirFiche ( id ) ;
await rendu ( ) ;
expect ( valeurDeFiche ( m , 'participants.colonne.notes' ) . textContent ) . toBe ( notes ) ;
expect ( section ( m ) . querySelectorAll ( 'b' ) ) . toHaveLength ( 0 ) ;
} ) ;
test ( 'titre pressenti : visible dans la grille et sur la fiche' , async ( ) => {
const m = await evenementOuvert ( { personnes : [ { nom : 'Pervenche' , prenom : 'Théo' , titre : 'hôtesse' } ] } ) ;
const id = idDe ( m , 'Pervenche' ) ;
expect ( cellule ( m , id , 'titrePressenti' ) . textContent . trim ( ) ) . toBe ( 'hôtesse' ) ;
cellule ( m , id , 'titrePressenti' ) . scrollIntoView ( { block : 'center' , inline : 'center' } ) ;
expect ( visible ( cellule ( m , id , 'titrePressenti' ) ) ) . toBe ( true ) ;
m . ui . ouvrirFiche ( id ) ;
await rendu ( ) ;
const valeur = valeurDeFiche ( m , 'participants.colonne.titrePressenti' ) ;
expect ( valeur . textContent ) . toBe ( 'hôtesse' ) ;
expect ( visible ( valeur ) ) . toBe ( true ) ;
} ) ;
test ( 'supprimer sans réservation : aucune fenêtre, une entrée ; avec réservations, la fenêtre les nomme, et Annuler ne détruit rien' , async ( ) => {
const m = await evenementOuvert ( { tables : 1 , personnes : [ { nom : 'Ombrelle' } , { nom : 'Pervenche' } ] } ) ;
const iris = idDe ( m , 'Ombrelle' ) ;
const theo = idDe ( m , 'Pervenche' ) ;
const table = m . seance . etat ( ) . charge . tables [ 0 ] ;
await m . seance . executer ( 'reserver' , { participant : theo , table : table . id , siege : 2 , portee : 'tous' , tour : null } ) ;
await rendu ( ) ;
const avant = entrees ( m ) . length ;
await cliquer ( commandeDeLigne ( m , iris , 'supprimerParticipant' ) ) ;
await attendreSeance ( m ) ;
aucuneFenetre ( ) ;
expect ( entrees ( m ) ) . toHaveLength ( avant + 1 ) ;
expect ( participants ( m ) . map ( ( p ) => p . id ) ) . toEqual ( [ theo ] ) ;
await cliquer ( commandeDeLigne ( m , theo , 'supprimerParticipant' ) ) ;
const fenetre = document . querySelector ( 'dialog[open]' ) ;
expect ( fenetre ) . not . toBeNull ( ) ;
const elements = [ ... fenetre . querySelectorAll ( 'li' ) ] . map ( ( li ) => li . textContent ) ;
expect ( elements ) . toEqual ( [
t ( 'participants.suppression.reservation' , { numero : table . numero , siege : 2 , portee : 'tous' , tour : null } ) ,
] ) ;
await cliquer ( fenetre . querySelector ( '[data-commande="renoncer"]' ) ) ;
await attendreSeance ( m ) ;
expect ( entrees ( m ) ) . toHaveLength ( avant + 1 ) ;
expect ( participants ( m ) . map ( ( p ) => p . id ) ) . toEqual ( [ theo ] ) ;
await cliquer ( commandeDeLigne ( m , theo , 'supprimerParticipant' ) ) ;
await cliquer ( document . querySelector ( 'dialog[open] [data-commande="confirmer"]' ) ) ;
await attendreSeance ( m ) ;
expect ( entrees ( m ) ) . toHaveLength ( avant + 2 ) ;
expect ( participants ( m ) ) . toEqual ( [ ] ) ;
} ) ;
} ) ;
describe ( 'le manque de places (§ 5.9, § 4.4)' , ( ) => {
test ( 'deux tables de 8 et seize personnes : la dix-septième est acceptée, une entrée, et avertit PLACES_MANQUANTES de 1' , async ( ) => {
const m = await evenementOuvert ( { tables : 2 , personnes : NOMS . slice ( 0 , 16 ) . map ( ( nom ) => ( { nom } ) ) } ) ;
const executer = vi . spyOn ( m . seance , 'executer' ) ;
const avant = entrees ( m ) . length ;
await ajouter ( m , { nom : NOMS [ 16 ] } ) ;
expect ( entrees ( m ) ) . toHaveLength ( avant + 1 ) ;
expect ( participants ( m ) ) . toHaveLength ( 17 ) ;
expect ( executer ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( executer . mock . calls [ 0 ] [ 0 ] ) . toBe ( 'ajouterParticipant' ) ;
const rendus = await executer . mock . results [ 0 ] . value ;
const attendu = nombrePlacesManquantes ( normaliser ( configurationDepuisCharge ( m . seance . etat ( ) . charge ) ) ) ;
expect ( attendu ) . toBe ( 1 ) ;
const manque = rendus . avertissements . find ( ( { code } ) => code === 'PLACES_MANQUANTES' ) ;
expect ( manque ? . details . placesManquantes ) . toBe ( attendu ) ;
const etat = m . seance . etat ( ) . avertissements . find ( ( { code } ) => code === 'PLACES_MANQUANTES' ) ;
expect ( etat ? . details . placesManquantes ) . toBe ( attendu ) ;
} ) ;
test ( 'deux tables de 8, dix-sept personnes dont une exclue : la réintégrer est accepté, une entrée, et avertit PLACES_MANQUANTES de 1' , async ( ) => {
const personnes = NOMS . slice ( 0 , 17 ) . map ( ( nom , rang ) => ( { nom , exclu : rang === 5 } ) ) ;
const m = await evenementOuvert ( { tables : 2 , personnes } ) ;
const id = idDe ( m , NOMS [ 5 ] ) ;
const executer = vi . spyOn ( m . seance , 'executer' ) ;
const avant = entrees ( m ) . length ;
await cliquer ( commandeDeLigne ( m , id , 'reintegrerParticipant' ) ) ;
await attendreSeance ( m ) ;
expect ( entrees ( m ) ) . toHaveLength ( avant + 1 ) ;
expect ( participants ( m ) . find ( ( p ) => p . id === id ) . exclu ) . toBe ( false ) ;
expect ( executer ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( executer . mock . calls [ 0 ] ) . toEqual ( [ 'reintegrerParticipant' , { id } ] ) ;
const rendus = await executer . mock . results [ 0 ] . value ;
const attendu = nombrePlacesManquantes ( normaliser ( configurationDepuisCharge ( m . seance . etat ( ) . charge ) ) ) ;
expect ( attendu ) . toBe ( 1 ) ;
expect ( rendus . avertissements . find ( ( { code } ) => code === 'PLACES_MANQUANTES' ) ? . details . placesManquantes ) . toBe ( attendu ) ;
expect ( m . seance . etat ( ) . avertissements . find ( ( { code } ) => code === 'PLACES_MANQUANTES' ) ? . details . placesManquantes ) . toBe (
attendu ,
) ;
} ) ;
} ) ;
describe ( 'la fiche (§ 4.2, § 7.4)' , ( ) => {
test ( 'statuts : A, B, C, D → ancré, ancré, partiellement fixé, mobile, égaux au statut du moteur ; aucun contrôle de saisie de statut' , async ( ) => {
const m = await evenementOuvert ( {
sieges : 4 ,
tours : 3 ,
tables : 2 ,
personnes : [
{ nom : 'Ombrelle' , prenom : 'Iris' } ,
{ nom : 'Pervenche' , prenom : 'Théo' } ,
{ nom : 'Lacasse' , prenom : 'Ondine' } ,
{ nom : 'Grisaille' } ,
] ,
} ) ;
const [ t1 , t2 ] = m . seance . etat ( ) . charge . tables . map ( ( { id } ) => id ) ;
const [ a , b , c , d ] = [ 'Ombrelle' , 'Pervenche' , 'Lacasse' , 'Grisaille' ] . map ( ( nom ) => idDe ( m , nom ) ) ;
const reserver = ( participant , table , portee , tour = null ) =>
m . seance . executer ( 'reserver' , { participant , table , siege : null , portee , tour } ) ;
await reserver ( a , t1 , 'tous' ) ;
for ( const tour of [ 1 , 2 , 3 ] ) await reserver ( b , t1 , 'tour' , tour ) ;
await reserver ( c , t1 , 'tour' , 1 ) ;
await reserver ( c , t2 , 'tour' , 2 ) ;
await rendu ( ) ;
const instance = normaliser ( configurationDepuisCharge ( m . seance . etat ( ) . charge ) ) ;
const NOMS _STATUTS = new Map ( [
[ STATUT . ANCRE , 'ancre' ] ,
[ STATUT . PARTIELLEMENT _FIXE , 'partiel' ] ,
[ STATUT . MOBILE , 'mobile' ] ,
] ) ;
const attendus = [
[ a , 'ancre' ] ,
[ b , 'ancre' ] ,
[ c , 'partiel' ] ,
[ d , 'mobile' ] ,
] ;
for ( const [ id , statut ] of attendus ) {
expect ( NOMS _STATUTS . get ( instance . statut [ instance . indexDe . get ( id ) ] ) ) . toBe ( statut ) ;
m . ui . ouvrirFiche ( id ) ;
await rendu ( ) ;
expect ( fiche ( m ) . dataset . fiche ) . toBe ( String ( id ) ) ;
expect ( valeurDeFiche ( m , 'participants.fiche.statut' ) . textContent ) . toBe ( t ( cleDeStatut ( statut ) ) ) ;
expect ( fiche ( m ) . querySelectorAll ( 'input, select, textarea' ) ) . toHaveLength ( 0 ) ;
}
} ) ;
test ( 'ouverte depuis sa ligne, puis par ui.ouvrirFiche : visible ; sur un retenu, son itinéraire égale, tour par tour, la forme nommée' , async ( ) => {
const m = await evenementOuvert ( { sieges : 4 , tours : 3 , tables : 2 , personnes : NOMS . slice ( 0 , 7 ) . map ( ( nom ) => ( { nom } ) ) } ) ;
await m . seance . executer ( 'modifierReglage' , { cle : 'attribuerSieges' , valeur : true } ) ;
await m . app . generation ( ) . generer ( { graine : 1 , nombre : 1 , arret : 50 , historique : 1000 } ) ;
const [ proposition ] = m . seance . etat ( ) . charge . propositions ;
await m . seance . executer ( 'retenirProposition' , { id : proposition . id } ) ;
// Une personne quitte sa table au tour 2 : la réserve figure à l'itinéraire.
await m . seance . executer ( 'retirerDuTour' , { tour : 2 , participant : idDe ( m , NOMS [ 2 ] ) } ) ;
await rendu ( ) ;
const charge = m . seance . etat ( ) . charge ;
const forme = formeNommee ( charge . retenu , charge ) ;
expect ( forme . some ( ( ligneForme ) => ligneForme . reserve ) ) . toBe ( true ) ;
const id = idDe ( m , NOMS [ 2 ] ) ;
await cliquer ( commandeDeLigne ( m , id , 'fiche' ) ) ;
expect ( m . ui . etat ( ) . fiche ) . toBe ( id ) ;
expect ( fiche ( m ) . dataset . fiche ) . toBe ( String ( id ) ) ;
expect ( visible ( fiche ( m ) ) ) . toBe ( true ) ;
await cliquer ( fiche ( m ) . querySelector ( '[data-commande="fermerFiche"]' ) ) ;
expect ( fiche ( m ) ) . toBeNull ( ) ;
expect ( m . ui . etat ( ) . section ) . toBe ( 'participants' ) ;
// Chaque personne : l'itinéraire de la fiche, ligne par ligne.
expect ( charge . participants ) . toHaveLength ( 7 ) ;
for ( const { id : personne } of charge . participants ) {
m . ui . ouvrirFiche ( personne ) ;
await rendu ( ) ;
expect ( visible ( fiche ( m ) ) ) . toBe ( true ) ;
const tableau = tableauDeFiche ( m , 'participants.fiche.itineraire' ) ;
expect ( tableau ) . toBeDefined ( ) ;
const lus = [ ... tableau . tBodies [ 0 ] . rows ] . map ( ( rangee ) => [ ... rangee . cells ] . map ( ( c ) => c . textContent . trim ( ) ) ) ;
const attendus = forme
. filter ( ( l ) => l . participant === personne )
. map ( ( l ) => ( l . reserve ? [ String ( l . tour ) , t ( 'participants.fiche.enReserve' ) ] : [ String ( l . tour ) , String ( l . numero ) , String ( l . siege ) ] ) ) ;
expect ( attendus ) . toHaveLength ( 3 ) ;
expect ( lus ) . toEqual ( attendus ) ;
}
} ) ;
[FIX] iteration 3 review: seat targets, gestures, warnings, settings
The wheel no longer zooms during a gesture, so a drop lands under the
ghost. A seat is a target the size of its pitch. A floor drop refuses to
remove an every-tour reservation. A seat reservation the retained plan
does not hold is flagged at the seat, with its swap as remedy. DERIVE names
the person; capacities have an upper bound; Generate waits for a pending
setting; a worker that fails to load shows a refusal; § 5.3 constraints
get their switches. Commands are not re-entrant and a double click does not
confirm. Checked: 2755 node, 550 browser tests, from the index alone.
--- FR ---
[FIX] revue de l'itération 3 : cibles, gestes, avertissements, réglages
La molette ne zoome plus pendant un geste : le dépôt tombe sous le fantôme.
Un siège est une cible de la taille de son pas. Un dépôt sur le sol refuse
de retirer une réservation de tous les tours. Une réservation à un siège
que le retenu ne tient pas est relevée au siège, l'échange en remède. La
dérive nomme la personne ; les capacités ont une borne ; Générer attend un
réglage en cours ; un travailleur qui ne se charge pas donne un refus ; le
§ 5.3 a ses bascules. Les commandes ne sont pas ré-entrantes et un double
clic ne confirme pas. Vérifié : 2755 node, 550 navigateur, index seul.
Assisted-by: Claude Opus 5.5
2026-10-07 17:50:35 -04:00
test ( 'retenu à 3 tours, l’ événement passé à 4 : la fiche garde les tours du retenu, et dit le quatrième hors du retenu (§ 9)' , async ( ) => {
const m = await evenementOuvert ( { sieges : 3 , tours : 3 , tables : 2 , personnes : NOMS . slice ( 0 , 6 ) . map ( ( nom ) => ( { nom } ) ) } ) ;
await m . seance . executer ( 'modifierReglage' , { cle : 'attribuerSieges' , valeur : true } ) ;
await m . app . generation ( ) . generer ( { graine : 1 , nombre : 1 , arret : 50 , historique : 1000 } ) ;
const [ proposition ] = m . seance . etat ( ) . charge . propositions ;
await m . seance . executer ( 'retenirProposition' , { id : proposition . id } ) ;
await m . seance . executer ( 'changerTours' , { tours : 4 } ) ;
await rendu ( ) ;
const charge = m . seance . etat ( ) . charge ;
expect ( charge . evenement . tours ) . toBe ( 4 ) ;
expect ( charge . retenu . placement ) . toHaveLength ( 3 ) ;
const forme = formeNommee ( charge . retenu , charge ) ;
expect ( charge . participants ) . toHaveLength ( 6 ) ;
for ( const { id : personne } of charge . participants ) {
m . ui . ouvrirFiche ( personne ) ;
await rendu ( ) ;
expect ( fiche ( m ) . textContent ) . not . toContain ( t ( 'participants.fiche.aucunRetenu' ) ) ;
const tableau = tableauDeFiche ( m , 'participants.fiche.itineraire' ) ;
expect ( tableau ) . toBeDefined ( ) ;
const lus = [ ... tableau . tBodies [ 0 ] . rows ] . map ( ( rangee ) => [ ... rangee . cells ] . map ( ( c ) => c . textContent . trim ( ) ) ) ;
const attendus = forme
. filter ( ( l ) => l . participant === personne )
. map ( ( l ) => ( l . reserve ? [ String ( l . tour ) , t ( 'participants.fiche.enReserve' ) ] : [ String ( l . tour ) , String ( l . numero ) , String ( l . siege ) ] ) ) ;
expect ( attendus ) . toHaveLength ( 3 ) ;
expect ( lus ) . toEqual ( [ ... attendus , [ '4' , t ( 'participants.fiche.horsRetenu' ) ] ] ) ;
}
} ) ;
[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
test ( 'un nom de deux cents caractères se lit entier sur la fiche, replié, sans marque ni débordement' , async ( ) => {
const long = 'Ombrelle' . repeat ( 25 ) ;
expect ( long ) . toHaveLength ( 200 ) ;
const m = await evenementOuvert ( ) ;
await m . seance . executer ( 'ajouterParticipant' , { champs : { nom : long } } ) ;
const id = idDe ( m , long ) ;
m . ui . ouvrirFiche ( id ) ;
await rendu ( ) ;
const porteur = fiche ( m ) ;
const valeur = valeurDeFiche ( m , 'participants.colonne.nom' ) ;
expect ( valeur . textContent ) . toBe ( long ) ;
expect ( porteur . textContent ) . not . toContain ( '\u{2026}' ) ;
expect ( porteur . scrollWidth ) . toBeLessThanOrEqual ( porteur . clientWidth ) ;
expect ( valeur . scrollWidth ) . toBeLessThanOrEqual ( valeur . clientWidth ) ;
const interligne = Number . parseFloat ( getComputedStyle ( valeur ) . lineHeight ) ;
expect ( interligne ) . toBeGreaterThan ( 0 ) ;
expect ( valeur . getBoundingClientRect ( ) . height ) . toBeGreaterThanOrEqual ( 2 * interligne ) ;
} ) ;
} ) ;
describe ( 'export et collage (§ 10.2)' , ( ) => {
test ( '« Exporter la liste » remet les octets d’ exporterParticipants ; réimportés dans un événement neuf, ils rendent les mêmes participants, exclu compris' , async ( ) => {
const m = await evenementOuvert ( ) ;
const octets = await octetsLivres ( CAS _LIMITES ) ;
await m . seance . executer ( 'importerParticipants' , { apercu : apercevoir ( decoder ( octets ) . texte ) , mode : 'ajouter' } ) ;
await rendu ( ) ;
const releves = [ ] ;
m . fs . enregistrerSous = async ( nomPropose , contenu ) => {
releves . push ( { nomPropose , octets : contenu } ) ;
return nomPropose ;
} ;
const avant = participants ( m ) ;
expect ( avant . some ( ( p ) => p . exclu ) ) . toBe ( true ) ;
const trace = m . trace . length ;
await cliquer ( section ( m ) . querySelector ( '[data-commande="exporter"]' ) ) ;
await attendreSeance ( m ) ;
expect ( releves ) . toHaveLength ( 1 ) ;
expect ( releves [ 0 ] . nomPropose ) . toBe ( ` ${ base ( m ) } -participants.csv ` ) ;
expect ( [ ... releves [ 0 ] . octets ] ) . toEqual ( [ ... exporterParticipants ( avant ) ] ) ;
expect ( m . trace . length ) . toBe ( trace ) ;
await m . seance . fermer ( ) ;
await m . seance . creer ( { nom : 'Soirée des Grisailles' , date : null , siegesParDefaut : 8 , tours : 2 } ) ;
await m . seance . passerEnEcriture ( ) ;
await m . seance . executer ( 'importerParticipants' , { apercu : apercevoir ( decoder ( releves [ 0 ] . octets ) . texte ) , mode : 'ajouter' } ) ;
const sansId = ( liste ) => liste . map ( ( { id , ... champs } ) => champs ) ;
expect ( avant . length ) . toBeGreaterThan ( 0 ) ;
expect ( sansId ( participants ( m ) ) ) . toEqual ( sansId ( avant ) ) ;
} ) ;
test ( 'un collage sur la grille ouvre l’ aperçu d’ import : importEnAttente porte le texte intact, origine collage, et la section import ; zéro écriture' , async ( ) => {
const m = await evenementOuvert ( { personnes : [ { nom : 'Ombrelle' } ] } ) ;
const etats = [ ] ;
const desabonner = m . ui . subscribe ( ( etat ) => etats . push ( etat ) ) ;
const trace = m . trace . length ;
const texte = 'nom;prénom\r\nPervenche;Théo\n"Lacasse; Ondine";\u{00A0}x\t' ;
grille ( m ) . querySelector ( '[tabindex="0"]' ) . focus ( ) ;
const presse = new DataTransfer ( ) ;
presse . setData ( 'text/plain' , texte ) ;
grille ( m ) . querySelector ( '[tabindex="0"]' ) . dispatchEvent (
new ClipboardEvent ( 'paste' , { clipboardData : presse , bubbles : true , cancelable : true } ) ,
) ;
await rendu ( ) ;
desabonner ( ) ;
expect ( etats . some ( ( etat ) => etat . importEnAttente ? . texte === texte && etat . importEnAttente . origine === 'collage' && etat . section === 'import' ) ) . toBe (
true ,
) ;
expect ( m . ui . etat ( ) . section ) . toBe ( 'import' ) ;
await attendreSeance ( m ) ;
expect ( m . trace . length ) . toBe ( trace ) ;
} ) ;
} ) ;