[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)
// Épreuves de la section des tables et des tours dans un vrai moteur de
// rendu (§ 4, § 4.3, § 6, § 2.1 étapes 4 et 5, § 19.3 captures 05 et 06).
// La section est montée par mount avec les contextes que pose App —
// 'seance', 't', 'horloge', 'ui' —, sur une séance qui ouvre un fichier
// d'état d'épreuve dans le système de fichiers simulé, à l'horloge pilotée :
// chaque écriture se compte, et chaque entrée se lit dans la révision. Les
// saisies et les clics passent par des entrées que Chromium traite comme
// réelles (test/navigateur/entrees.js). L'épreuve lit le texte rendu, les
// attributs data-… du contrat et la charge que la séance a écrite. Les noms
// d'épreuve sont inventés.
//
// Le projet navigateur emploie expect, et test et describe de Vitest
// (§ 14.8).
import { afterEach , describe , expect , test , vi } from 'vitest' ;
import { flushSync , mount , tick , unmount } from 'svelte' ;
import '../jetons.css' ;
import { controlerCapacite } from '../../application/capacite.js' ;
import { creerHorlogePilotee } from '../../application/horloge.js' ;
import { TABLES _APPLICATION } from '../../application/libelles.js' ;
import { nomAffiche } from '../../application/personnes.js' ;
import { creerSeance } from '../../application/seance.js' ;
import { creerTraduction } from '../../application/traduction.js' ;
import { constante } from '../../geometrie/constantes.js' ;
import { nombrePlaces , normaliser } from '../../moteur/configuration.js' ;
import { serialiser } from '../../stockage/canonique.js' ;
import { capacite , configurationDepuisCharge , creerCharge } from '../../stockage/document.js' ;
import { SUFFIXES } from '../../stockage/noms.js' ;
import { VERSION } from '../../version.genere.js' ;
import { creerFichiersSimules } from '../../../test/fichiers_simules.js' ;
import { pointer , touche } from '../../../test/navigateur/entrees.js' ;
import { visible } from '../../../test/navigateur/rendu.js' ;
import { creerEtatUi } from '../etat_ui.js' ;
import { TABLES _INTERFACE } from '../libelles.js' ;
import SectionTables from './SectionTables.svelte' ;
const { t } = creerTraduction ( [ ... TABLES _APPLICATION , ... TABLES _INTERFACE ] ) ;
const BASE = 'soiree' ;
const NOMS = [
[ 'Ombrelle' , 'Iris' ] ,
[ 'Pervenche' , 'Théo' ] ,
[ 'Lacasse' , 'Ondine' ] ,
[ 'Grisaille' , null ] ,
[ 'Bruyère' , 'Anouk' ] ,
[ 'Sarcelle' , 'Capucine' ] ,
] ;
const personne = ( id , exclu = false ) => ( {
id ,
nom : NOMS [ ( id - 1 ) % NOMS . length ] [ 0 ] ,
prenom : NOMS [ ( id - 1 ) % NOMS . length ] [ 1 ] ,
appartenance : null ,
courriel : null ,
titrePressenti : null ,
notes : null ,
exclu ,
} ) ;
const resa = ( participant , table , siege , tour = null ) => ( { participant , table , siege , portee : tour === null ? 'tous' : 'tour' , tour } ) ;
const suite = ( de , a ) => Array . from ( { length : a - de + 1 } , ( _ , rang ) => de + rang ) ;
// Une charge : les tables données ({id, numero, sieges, forme}), n
// personnes dont les exclus, R tours, un défaut.
function chargeDe ( { tables = [ ] , n = 0 , exclus = [ ] , tours = 2 , defaut = 8 , attribuerSieges = false , reservations = [ ] , titres = [ ] } = { } ) {
const charge = creerCharge ( { id : 'evt-tables' , nom : 'Soirée des Merles' , siegesParDefaut : defaut , tours } ) ;
charge . reglages . attribuerSieges = attribuerSieges ;
charge . participants = Array . from ( { length : n } , ( _ , rang ) => personne ( rang + 1 , exclus . includes ( rang + 1 ) ) ) ;
charge . tables = tables . map ( ( { id , numero = id , sieges = null , forme = 'ronde' } ) => ( { id , numero , sieges , forme , position : { x : 300 * id , y : 0 } } ) ) ;
charge . reservations = reservations ;
charge . titres = titres ;
charge . prochainsIds = { participant : n + 1 , table : Math . max ( 0 , ... tables . map ( ( { id } ) => id ) ) + 1 , proposition : 1 } ;
return charge ;
}
// Ajoute la proposition id, ses listes dans l'ordre de charge.tables, et la
// retient quand retenu est vrai.
function placer ( charge , listes , { id , retenu = false } ) {
const participants = charge . participants . filter ( ( { exclu } ) => ! exclu ) . map ( ( { id : p } ) => p ) ;
const plan = {
siegesAttribues : charge . reglages . attribuerSieges ,
tables : charge . tables . map ( ( { id : table } ) => table ) ,
capacites : charge . tables . map ( ( table ) => capacite ( charge , table ) ) ,
tours : listes . length ,
participants ,
placement : listes . map ( ( tour ) => ( { sieges : tour , reserve : participants . filter ( ( p ) => ! tour . flat ( ) . includes ( p ) ) } ) ) ,
} ;
charge . propositions . push ( { id , graine : id , arret : 40 , historique : 1000 , produitVersion : VERSION . affichee , ... plan } ) ;
charge . prochainsIds . proposition = id + 1 ;
if ( retenu ) {
charge . retenu = { proposition : id , ... structuredClone ( plan ) } ;
charge . evenement . etat = 'retenu' ;
} else if ( charge . evenement . etat === 'brouillon' ) {
charge . evenement . etat = 'propose' ;
}
return charge ;
}
// Treize tables : neuf suivent 8, les tables 3, 6, 9 et 12 sont surchargées
// à 7, 7, 6 et 12 ; 120 personnes, dont 2 exclues.
function treizeTables ( ) {
const surcharges = new Map ( [ [ 3 , 7 ] , [ 6 , 7 ] , [ 9 , 6 ] , [ 12 , 12 ] ] ) ;
const tables = Array . from ( { length : 13 } , ( _ , rang ) => ( { id : rang + 1 , sieges : surcharges . get ( rang + 1 ) ? ? null } ) ) ;
return chargeDe ( { tables , n : 120 , exclus : [ 40 , 80 ] } ) ;
}
// Les composants montés par l'épreuve courante, et leurs conteneurs.
const montes = [ ] ;
afterEach ( ( ) => {
for ( const { composant , cible } of montes . splice ( 0 ) ) {
unmount ( composant ) ;
cible . remove ( ) ;
}
vi . restoreAllMocks ( ) ;
} ) ;
// Le fichier d'état de la charge posé dans le système simulé, ouvert par une
// séance, en écriture sauf mode 'lecture' ; la section montée au début du
// corps, avec les contextes de l'application.
async function preparer ( charge , { mode = 'ecriture' } = { } ) {
const fs = creerFichiersSimules ( ) ;
fs . deposerDossier ( 'documents' ) ;
fs . deposer ( 'documents' , ` ${ BASE } ${ SUFFIXES . etat } ` , serialiser ( charge , { revision : 1 , produitVersion : VERSION . affichee } ) ) ;
const { documents } = await fs . racines ( ) ;
const horloge = creerHorlogePilotee ( ) ;
let tirages = 0 ;
const seance = creerSeance ( {
fs ,
racine : documents ,
horloge ,
identifiant : ( ) => {
tirages += 1 ;
return ` epreuve- ${ tirages } ` ;
} ,
produitVersion : VERSION . affichee ,
planifier : horloge . planifier ,
annuler : horloge . annuler ,
} ) ;
await seance . ouvrir ( BASE ) ;
if ( mode === 'ecriture' ) await seance . passerEnEcriture ( ) ;
const ui = creerEtatUi ( ) ;
const contexte = new Map ( [
[ 'seance' , seance ] ,
[ 't' , t ] ,
[ 'horloge' , horloge ] ,
[ 'ui' , ui ] ,
] ) ;
const cible = document . createElement ( 'div' ) ;
document . body . prepend ( cible ) ;
const composant = mount ( SectionTables , { target : cible , props : { } , context : contexte } ) ;
flushSync ( ) ;
montes . push ( { composant , cible } ) ;
return { fs , seance , ui , cible } ;
}
const section = ( m ) => m . cible . querySelector ( '[data-section="tables"]' ) ;
const charge = ( m ) => m . seance . etat ( ) . charge ;
const revision = ( m ) => m . seance . etat ( ) . evenement . revision ;
const ecritures = ( m ) => m . fs . compteurs . ecritures ;
const ligne = ( m , id ) => section ( m ) . querySelector ( ` [data-table=" ${ id } "] ` ) ;
const champTable = ( m , id ) => ligne ( m , id ) . querySelector ( ` input[data-champ="sieges- ${ id } "] ` ) ;
const champ = ( m , cle ) => section ( m ) . querySelector ( ` input[data-champ=" ${ cle } "] ` ) ;
const commande = ( racine , nom ) => racine . querySelector ( ` [data-commande=" ${ nom } "] ` ) ;
const etatDe = ( m , id ) => ligne ( m , id ) . querySelector ( '[data-etat-table]' ) . textContent ;
const enonce = ( m ) => section ( m ) . querySelector ( '[data-enonce="defaut"]' ) ;
const phrases = ( m ) => [ ... enonce ( m ) . querySelectorAll ( '[data-phrase]' ) ] . map ( ( p ) => p . textContent ) . join ( ' ' ) ;
const fenetre = ( ) => document . querySelector ( 'dialog[open]' ) ;
const elementsFenetre = ( ) => [ ... fenetre ( ) . querySelectorAll ( 'li' ) ] . map ( ( li ) => li . textContent ) ;
// Attend que la file de la séance ait passé tout ce qui s'y trouve, par une
// lecture admise dans tout mode qui n'écrit rien, puis le rendu.
async function attendre ( m ) {
await m . seance . formeNommee ( null ) . catch ( ( ) => undefined ) ;
await tick ( ) ;
flushSync ( ) ;
}
// Un clic réel au centre de l'élément, puis la file de la séance passée.
async function cliquer ( m , element ) {
expect ( element , 'élément à cliquer' ) . not . toBeNull ( ) ;
element . scrollIntoView ( { block : 'center' } ) ;
const boite = element . getBoundingClientRect ( ) ;
const x = boite . left + boite . width / 2 ;
const y = boite . top + boite . height / 2 ;
await pointer ( 'presser' , x , y ) ;
await pointer ( 'relacher' , x , y ) ;
await attendre ( m ) ;
}
// Saisit texte dans le champ, la valeur courante remplacée, puis valide par
// Entrée : le geste s'achève, et la file de la séance passe.
async function saisir ( m , entree , texte ) {
await cliquer ( m , entree ) ;
entree . select ( ) ;
if ( texte === '' ) await touche ( 'Backspace' ) ;
for ( const caractere of texte ) await touche ( caractere ) ;
await touche ( 'Enter' ) ;
await attendre ( m ) ;
}
describe ( 'une table créée (§ 4, § 6.1)' , ( ) => {
test ( 'défaut 8 : la table créée suit le défaut, affichée 8 ; le défaut à 10, elle affiche 10 ; changer de tour affiché ne change ni la valeur ni le contrôle' , async ( ) => {
const m = await preparer ( chargeDe ( { n : 2 } ) ) ;
await cliquer ( m , commande ( section ( m ) , 'creerTableRonde' ) ) ;
expect ( charge ( m ) . tables ) . toHaveLength ( 1 ) ;
const { id , sieges } = charge ( m ) . tables [ 0 ] ;
expect ( sieges ) . toBeNull ( ) ;
expect ( champTable ( m , id ) . value ) . toBe ( '8' ) ;
expect ( etatDe ( m , id ) ) . toBe ( t ( 'tables.etat.suitDefaut' ) ) ;
await saisir ( m , champ ( m , 'siegesParDefaut' ) , '10' ) ;
await cliquer ( m , commande ( enonce ( m ) , 'appliquer' ) ) ;
expect ( charge ( m ) . evenement . siegesParDefaut ) . toBe ( 10 ) ;
expect ( charge ( m ) . tables [ 0 ] . sieges ) . toBeNull ( ) ;
expect ( champTable ( m , id ) . value ) . toBe ( '10' ) ;
const avant = { valeur : champTable ( m , id ) . value , etat : etatDe ( m , id ) , champ : champTable ( m , id ) } ;
m . ui . montrerTour ( 2 ) ;
await attendre ( m ) ;
expect ( champTable ( m , id ) ) . toBe ( avant . champ ) ;
expect ( champTable ( m , id ) . value ) . toBe ( avant . valeur ) ;
expect ( etatDe ( m , id ) ) . toBe ( avant . etat ) ;
} ) ;
test ( 'la forme : la création offre ronde et carrée ; carrée, puis ronde, une entrée chacune' , async ( ) => {
const m = await preparer ( chargeDe ( { n : 2 } ) ) ;
expect ( commande ( section ( m ) , 'creerTableRonde' ) . textContent ) . toBe ( t ( 'tables.creer.ronde' ) ) ;
expect ( commande ( section ( m ) , 'creerTableCarree' ) . textContent ) . toBe ( t ( 'tables.creer.carree' ) ) ;
const r0 = revision ( m ) ;
await cliquer ( m , commande ( section ( m ) , 'creerTableCarree' ) ) ;
expect ( charge ( m ) . tables . map ( ( { forme } ) => forme ) ) . toEqual ( [ 'carree' ] ) ;
expect ( revision ( m ) ) . toBe ( r0 + 1 ) ;
await cliquer ( m , commande ( section ( m ) , 'creerTableRonde' ) ) ;
expect ( charge ( m ) . tables . map ( ( { forme } ) => forme ) ) . toEqual ( [ 'carree' , 'ronde' ] ) ;
expect ( revision ( m ) ) . toBe ( r0 + 2 ) ;
expect ( ligne ( m , charge ( m ) . tables [ 0 ] . id ) . textContent ) . toContain ( t ( 'tables.forme.carree' ) ) ;
expect ( ligne ( m , charge ( m ) . tables [ 1 ] . id ) . textContent ) . toContain ( t ( 'tables.forme.ronde' ) ) ;
} ) ;
} ) ;
describe ( 'l’ état, pas seulement la valeur (§ 6.1, § 19.3 05)' , ( ) => {
test ( 'deux tables à 8, l’ une suit le défaut, l’ autre surchargée à 8 : les états diffèrent, seule la seconde offre « Réaligner » ; réalignée, son état égale celui de la première' , async ( ) => {
const m = await preparer ( chargeDe ( { tables : [ { id : 1 } , { id : 2 , sieges : 8 } ] , n : 4 } ) ) ;
expect ( champTable ( m , 1 ) . value ) . toBe ( '8' ) ;
expect ( champTable ( m , 2 ) . value ) . toBe ( '8' ) ;
expect ( etatDe ( m , 1 ) ) . not . toBe ( etatDe ( m , 2 ) ) ;
expect ( visible ( ligne ( m , 1 ) . querySelector ( '[data-etat-table]' ) ) ) . toBe ( true ) ;
expect ( commande ( ligne ( m , 1 ) , 'realignerTable' ) ) . toBeNull ( ) ;
expect ( commande ( ligne ( m , 2 ) , 'realignerTable' ) . textContent ) . toBe ( t ( 'tables.liste.realigner' ) ) ;
const r0 = revision ( m ) ;
await cliquer ( m , commande ( ligne ( m , 2 ) , 'realignerTable' ) ) ;
expect ( revision ( m ) ) . toBe ( r0 + 1 ) ;
expect ( charge ( m ) . tables [ 1 ] . sieges ) . toBeNull ( ) ;
expect ( etatDe ( m , 2 ) ) . toBe ( etatDe ( m , 1 ) ) ;
expect ( champTable ( m , 2 ) . value ) . toBe ( champTable ( m , 1 ) . value ) ;
expect ( commande ( ligne ( m , 2 ) , 'realignerTable' ) ) . toBeNull ( ) ;
} ) ;
test ( 'surcharge et réalignement : 6 sur une table qui suit 8, surchargée ; le défaut à 10 la laisse à 6 ; « Réaligner » la rend au défaut, une entrée' , async ( ) => {
const m = await preparer ( chargeDe ( { tables : [ { id : 1 } ] , n : 4 } ) ) ;
const r0 = revision ( m ) ;
await saisir ( m , champTable ( m , 1 ) , '6' ) ;
expect ( charge ( m ) . tables [ 0 ] . sieges ) . toBe ( 6 ) ;
expect ( etatDe ( m , 1 ) ) . toBe ( t ( 'tables.etat.surchargee' ) ) ;
expect ( revision ( m ) ) . toBe ( r0 + 1 ) ;
await saisir ( m , champ ( m , 'siegesParDefaut' ) , '10' ) ;
await cliquer ( m , commande ( enonce ( m ) , 'appliquer' ) ) ;
expect ( charge ( m ) . evenement . siegesParDefaut ) . toBe ( 10 ) ;
expect ( charge ( m ) . tables [ 0 ] . sieges ) . toBe ( 6 ) ;
expect ( champTable ( m , 1 ) . value ) . toBe ( '6' ) ;
const r1 = revision ( m ) ;
await cliquer ( m , commande ( ligne ( m , 1 ) , 'realignerTable' ) ) ;
expect ( charge ( m ) . tables [ 0 ] . sieges ) . toBeNull ( ) ;
expect ( champTable ( m , 1 ) . value ) . toBe ( '10' ) ;
expect ( etatDe ( m , 1 ) ) . toBe ( t ( 'tables.etat.suitDefaut' ) ) ;
expect ( revision ( m ) ) . toBe ( r1 + 1 ) ;
} ) ;
} ) ;
describe ( 'changer le défaut (§ 6.1, § 6.2, § 19.3 06)' , ( ) => {
test ( 'treize tables, 120 participants dont 2 exclus ; 10 saisi : l’ énoncé exact, zéro écriture avant le clic ; « Appliquer » : le total annoncé est celui de l’ instance écrite, et 118 son N' , async ( ) => {
const m = await preparer ( treizeTables ( ) ) ;
const avant = ecritures ( m ) ;
const r0 = revision ( m ) ;
await saisir ( m , champ ( m , 'siegesParDefaut' ) , '10' ) ;
expect ( visible ( enonce ( m ) ) ) . toBe ( true ) ;
expect ( phrases ( m ) ) . toBe (
'9 tables suivent le défaut et passent de 8 à 10 places ; 4 tables surchargées (7, 7, 6, 12) ne changent pas. ' +
'Total : 104 \u{2192} 122 sièges pour 118 participants.' ,
) ;
expect ( ecritures ( m ) ) . toBe ( avant ) ;
expect ( commande ( enonce ( m ) , 'appliquer' ) . textContent ) . toBe ( t ( 'tables.defaut.appliquer' ) ) ;
expect ( commande ( enonce ( m ) , 'appliquerRealigner' ) . textContent ) . toBe ( 'Appliquer et réaligner les 4 surchargées' ) ;
await cliquer ( m , commande ( enonce ( m ) , 'appliquer' ) ) ;
expect ( revision ( m ) ) . toBe ( r0 + 1 ) ;
const instance = normaliser ( configurationDepuisCharge ( charge ( m ) ) ) ;
expect ( nombrePlaces ( instance ) ) . toBe ( 122 ) ;
expect ( instance . N ) . toBe ( 118 ) ;
expect ( enonce ( m ) ) . toBeNull ( ) ;
} ) ;
test ( 'appliquer et réaligner : la fenêtre nomme les surcharges perdues ; Annuler n’ écrit rien ; Confirmer : les treize tables suivent 10, une entrée' , async ( ) => {
const m = await preparer ( treizeTables ( ) ) ;
await saisir ( m , champ ( m , 'siegesParDefaut' ) , '10' ) ;
const avant = ecritures ( m ) ;
const r0 = revision ( m ) ;
await cliquer ( m , commande ( enonce ( m ) , 'appliquerRealigner' ) ) ;
expect ( fenetre ( ) ) . not . toBeNull ( ) ;
expect ( elementsFenetre ( ) ) . toEqual ( [ 'Table 3 : 7 places' , 'Table 6 : 7 places' , 'Table 9 : 6 places' , 'Table 12 : 12 places' ] ) ;
await cliquer ( m , commande ( fenetre ( ) , 'renoncer' ) ) ;
expect ( fenetre ( ) ) . toBeNull ( ) ;
expect ( ecritures ( m ) ) . toBe ( avant ) ;
await cliquer ( m , commande ( enonce ( m ) , 'appliquerRealigner' ) ) ;
await cliquer ( m , commande ( fenetre ( ) , 'confirmer' ) ) ;
await attendre ( m ) ;
expect ( fenetre ( ) ) . toBeNull ( ) ;
expect ( revision ( m ) ) . toBe ( r0 + 1 ) ;
expect ( charge ( m ) . evenement . siegesParDefaut ) . toBe ( 10 ) ;
expect ( charge ( m ) . tables ) . toHaveLength ( 13 ) ;
expect ( charge ( m ) . tables . every ( ( { sieges } ) => sieges === null ) ) . toBe ( true ) ;
} ) ;
test ( 'baisse refusée en bloc : la table 2 porte 7 réservations ; 6 saisi : l’ énoncé nomme la table 2 et ses 7 réservations, aucune commande d’ application, rien n’ est écrit' , async ( ) => {
const reservations = suite ( 1 , 7 ) . map ( ( p ) => resa ( p , 2 , null ) ) ;
const m = await preparer ( chargeDe ( { tables : [ { id : 1 } , { id : 2 } , { id : 3 } ] , n : 10 , reservations } ) ) ;
const avant = ecritures ( m ) ;
await saisir ( m , champ ( m , 'siegesParDefaut' ) , '6' ) ;
const texte = enonce ( m ) . textContent ;
expect ( texte ) . toContain ( 'Table 2' ) ;
expect ( texte ) . toContain ( '7 réservations' ) ;
expect ( commande ( enonce ( m ) , 'appliquer' ) ) . toBeNull ( ) ;
expect ( commande ( enonce ( m ) , 'appliquerRealigner' ) ) . toBeNull ( ) ;
expect ( charge ( m ) . evenement . siegesParDefaut ) . toBe ( 8 ) ;
expect ( [ 1 , 2 , 3 ] . map ( ( id ) => champTable ( m , id ) . value ) ) . toEqual ( [ '8' , '8' , '8' ] ) ;
expect ( ecritures ( m ) ) . toBe ( avant ) ;
} ) ;
test ( 'réaligner refusé : la table 2 surchargée à 12 porte 11 réservations ; 10 saisi, « Appliquer et réaligner » : aucune fenêtre, le refus nomme la table 2, rien n’ est écrit' , async ( ) => {
const reservations = suite ( 1 , 11 ) . map ( ( p ) => resa ( p , 2 , null ) ) ;
const m = await preparer ( chargeDe ( { tables : [ { id : 1 } , { id : 2 , sieges : 12 } ] , n : 12 , reservations } ) ) ;
await saisir ( m , champ ( m , 'siegesParDefaut' ) , '10' ) ;
expect ( enonce ( m ) . textContent ) . not . toContain ( 'Table 2' ) ;
const avant = ecritures ( m ) ;
await cliquer ( m , commande ( enonce ( m ) , 'appliquerRealigner' ) ) ;
expect ( fenetre ( ) ) . toBeNull ( ) ;
const refus = enonce ( m ) . querySelector ( '[data-refus-realigner]' ) ;
expect ( refus ) . not . 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
// L'en-tête est celui du réalignement refusé, qui laisse « Appliquer »
// offert, et non celui d'un défaut que rien n'applique.
const tete = refus . querySelector ( '[data-phrase]' ) ;
expect ( tete ? . textContent ) . toBe ( t ( 'tables.defaut.realignementRefuse' , { avant : 8 , apres : 10 } ) ) ;
expect ( visible ( tete ) ) . toBe ( true ) ;
expect ( refus . textContent ) . not . toContain ( t ( 'tables.defaut.refusee' , { avant : 8 , apres : 10 } ) ) ;
[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 ( refus . textContent ) . toContain ( 'Table 2' ) ;
expect ( refus . textContent ) . toContain ( '11 réservations' ) ;
expect ( ecritures ( m ) ) . toBe ( avant ) ;
expect ( charge ( m ) . evenement . siegesParDefaut ) . toBe ( 8 ) ;
expect ( charge ( m ) . tables . find ( ( { id } ) => id === 2 ) . sieges ) . toBe ( 12 ) ;
expect ( commande ( enonce ( m ) , 'appliquer' ) ) . not . toBeNull ( ) ;
} ) ;
test ( '« Garder le défaut actuel » : l’ énoncé se retire, le champ rend la valeur enregistrée, rien n’ est écrit' , async ( ) => {
const m = await preparer ( treizeTables ( ) ) ;
const avant = ecritures ( m ) ;
await saisir ( m , champ ( m , 'siegesParDefaut' ) , '10' ) ;
await cliquer ( m , commande ( enonce ( m ) , 'garder' ) ) ;
expect ( enonce ( m ) ) . toBeNull ( ) ;
expect ( champ ( m , 'siegesParDefaut' ) . value ) . toBe ( '8' ) ;
expect ( ecritures ( m ) ) . toBe ( avant ) ;
} ) ;
test ( 'moins de 2 places par défaut : refusé sur le champ, sans énoncé ni écriture' , async ( ) => {
const m = await preparer ( chargeDe ( { tables : [ { id : 1 } ] , n : 4 } ) ) ;
const avant = ecritures ( m ) ;
await saisir ( m , champ ( m , 'siegesParDefaut' ) , '1' ) ;
const bloc = champ ( m , 'siegesParDefaut' ) . closest ( '[data-defaut]' ) ;
expect ( bloc . querySelector ( '[data-refus="MOINS_DE_DEUX"]' ) . textContent ) . toContain ( t ( 'MOINS_DE_DEUX' ) ) ;
expect ( enonce ( m ) ) . toBeNull ( ) ;
expect ( ecritures ( m ) ) . toBe ( avant ) ;
} ) ;
} ) ;
describe ( 'la capacité d’ une table, saisie au champ (§ 6.2)' , ( ) => {
test ( '1, 0, -3, 2.5, « 8abc », « huit » et vide : refusés sur le champ, leur cause, aucun remède, rien d’ écrit ; 2 accepté' , async ( ) => {
const m = await preparer ( chargeDe ( { tables : [ { id : 1 } ] , n : 4 } ) ) ;
const avant = ecritures ( m ) ;
const saisies = [
[ '1' , 'MOINS_DE_DEUX' ] ,
[ '0' , 'MOINS_DE_DEUX' ] ,
[ '-3' , 'NON_ENTIER' ] ,
[ '2.5' , 'NON_ENTIER' ] ,
[ '8abc' , 'NON_ENTIER' ] ,
[ 'huit' , 'NON_ENTIER' ] ,
[ '' , 'NON_ENTIER' ] ,
] ;
expect ( saisies ) . toHaveLength ( 7 ) ;
for ( const [ saisie , code ] of saisies ) {
await saisir ( m , champTable ( m , 1 ) , saisie ) ;
const refus = ligne ( m , 1 ) . querySelector ( ` [data-refus=" ${ code } "] ` ) ;
expect ( refus , ` refus de « ${ saisie } » ` ) . not . toBeNull ( ) ;
const details = code === 'NON_ENTIER' ? { saisie } : { } ;
expect ( refus . querySelector ( 'p' ) . textContent ) . toBe ( t ( code , details ) ) ;
expect ( refus . querySelector ( '[data-commande]' ) ) . toBeNull ( ) ;
expect ( ecritures ( m ) ) . toBe ( avant ) ;
expect ( charge ( m ) . tables [ 0 ] . sieges ) . toBeNull ( ) ;
}
await saisir ( m , champTable ( m , 1 ) , '2' ) ;
expect ( charge ( m ) . tables [ 0 ] . sieges ) . toBe ( 2 ) ;
expect ( ligne ( m , 1 ) . querySelector ( '[data-refus]' ) ) . toBeNull ( ) ;
} ) ;
test ( 'sous les réservations : 5 réservations « tous » ; 4 refusé contre le champ, qui nomme 5, son remède offert ; 5 accepté, une entrée' , async ( ) => {
const reservations = suite ( 1 , 5 ) . map ( ( p ) => resa ( p , 1 , null ) ) ;
const m = await preparer ( chargeDe ( { tables : [ { id : 1 } ] , n : 6 , reservations } ) ) ;
const avant = ecritures ( m ) ;
await saisir ( m , champTable ( m , 1 ) , '4' ) ;
const refus = ligne ( m , 1 ) . querySelector ( '[data-refus="SOUS_RESERVATIONS"]' ) ;
expect ( refus . textContent ) . toContain ( '5 réservations' ) ;
expect ( commande ( refus , 'afficherTour' ) . textContent ) . toBe ( t ( 'communs.remede.afficherTour' , { tour : 1 } ) ) ;
expect ( ecritures ( m ) ) . toBe ( avant ) ;
const r0 = revision ( m ) ;
await saisir ( m , champTable ( m , 1 ) , '5' ) ;
expect ( charge ( m ) . tables [ 0 ] . sieges ) . toBe ( 5 ) ;
expect ( revision ( m ) ) . toBe ( r0 + 1 ) ;
} ) ;
test ( 'sièges retirés : sièges attribués, Iris Ombrelle au siège 7, un titre « animateur » au siège 8 ; 6 liste le siège 8 puis le siège 7 et sa personne, rien d’ écrit ; 7, le siège 8 seul' , async ( ) => {
const m = await preparer (
chargeDe ( {
tables : [ { id : 1 } ] ,
n : 4 ,
attribuerSieges : true ,
reservations : [ resa ( 1 , 1 , 7 ) ] ,
titres : [ { table : 1 , siege : 8 , libelle : 'animateur' } ] ,
} ) ,
) ;
const avant = ecritures ( m ) ;
await saisir ( m , champTable ( m , 1 ) , '6' ) ;
expect ( ligne ( m , 1 ) . querySelector ( '[data-refus="SIEGES_RETIRES"]' ) ) . not . toBeNull ( ) ;
const retires = ( ) => [ ... ligne ( m , 1 ) . querySelectorAll ( '[data-sieges-retires] li' ) ] . map ( ( li ) => li . textContent ) ;
expect ( retires ( ) ) . toEqual ( [ 'Siège 8 : titre « animateur »' , 'Siège 7 : Iris Ombrelle' ] ) ;
const texte = ligne ( m , 1 ) . textContent ;
expect ( texte . indexOf ( 'animateur' ) ) . toBeLessThan ( texte . indexOf ( 'Iris Ombrelle' ) ) ;
expect ( ecritures ( m ) ) . toBe ( avant ) ;
await saisir ( m , champTable ( m , 1 ) , '7' ) ;
expect ( retires ( ) ) . toEqual ( [ 'Siège 8 : titre « animateur »' ] ) ;
expect ( ligne ( m , 1 ) . textContent ) . not . toContain ( 'Iris Ombrelle' ) ;
expect ( ecritures ( m ) ) . toBe ( avant ) ;
} ) ;
test ( 'surnuméraires et lisibilité : un retenu assied huit personnes à la table 3 au tour 2 ; 6 accepté, l’ avertissement nomme les deux personnes versées en réserve ; S + 1 avertit de S ; S, non' , async ( ) => {
const donnees = chargeDe ( { tables : [ { id : 1 } , { id : 2 } , { id : 3 } ] , n : 18 } ) ;
placer (
donnees ,
[
[ suite ( 1 , 6 ) , suite ( 7 , 12 ) , suite ( 13 , 18 ) ] ,
[ suite ( 1 , 5 ) , suite ( 6 , 10 ) , suite ( 11 , 18 ) ] ,
] ,
{ id : 1 , retenu : true } ,
) ;
const { avertissements } = controlerCapacite ( donnees , 3 , 6 , { origine : 'champ' } ) ;
expect ( avertissements ) . toHaveLength ( 1 ) ;
const noms = avertissements [ 0 ] . details . personnes . map ( ( id ) => nomAffiche ( donnees . participants [ id - 1 ] ) ) ;
expect ( noms ) . toHaveLength ( 2 ) ;
const m = await preparer ( donnees ) ;
const r0 = revision ( m ) ;
await saisir ( m , champTable ( m , 3 ) , '6' ) ;
expect ( charge ( m ) . tables [ 2 ] . sieges ) . toBe ( 6 ) ;
expect ( revision ( m ) ) . toBe ( r0 + 1 ) ;
const surnumeraires = ligne ( m , 3 ) . querySelector ( '[data-avertissement="SURNUMERAIRES"]' ) ;
expect ( surnumeraires . textContent ) . toContain ( t ( 'SURNUMERAIRES' , avertissements [ 0 ] . details ) ) ;
expect ( [ ... surnumeraires . querySelectorAll ( 'li' ) ] . map ( ( li ) => li . textContent ) ) . toEqual ( noms ) ;
const seuil = constante ( 'SEUIL_SIEGES_LISIBLES' ) ;
await saisir ( m , champTable ( m , 1 ) , String ( seuil + 1 ) ) ;
expect ( charge ( m ) . tables [ 0 ] . sieges ) . toBe ( seuil + 1 ) ;
const lisible = ligne ( m , 1 ) . querySelector ( '[data-avertissement="AU_DELA_LISIBLE"]' ) ;
expect ( lisible . textContent ) . toContain ( String ( seuil ) ) ;
expect ( lisible . querySelector ( 'p' ) . textContent ) . toBe ( t ( 'AU_DELA_LISIBLE' , { seuil } ) ) ;
await saisir ( m , champTable ( m , 1 ) , String ( seuil ) ) ;
expect ( charge ( m ) . tables [ 0 ] . sieges ) . toBe ( seuil ) ;
expect ( ligne ( m , 1 ) . querySelector ( '[data-avertissement="AU_DELA_LISIBLE"]' ) ) . toBeNull ( ) ;
} ) ;
} ) ;
describe ( 'les tours (§ 4, § 4.3, § 2.1 étape 5)' , ( ) => {
test ( '0, 2.5 et « x » refusés sur le champ, leur cause, sans fenêtre, rien d’ écrit ; 1 accepté' , async ( ) => {
// La réservation du tour 2 : un changement qui la détruirait ouvre la
// fenêtre, que la saisie refusée n'ouvre pas.
const m = await preparer ( chargeDe ( { tables : [ { id : 1 } ] , n : 4 , reservations : [ resa ( 1 , 1 , null , 2 ) ] } ) ) ;
const avant = ecritures ( m ) ;
const saisies = [
[ '0' , 'MOINS_D_UN_TOUR' , { tours : 0 } ] ,
[ '2.5' , 'NON_ENTIER' , { saisie : '2.5' } ] ,
[ 'x' , 'NON_ENTIER' , { saisie : 'x' } ] ,
] ;
expect ( saisies ) . toHaveLength ( 3 ) ;
for ( const [ saisie , code , details ] of saisies ) {
await saisir ( m , champ ( m , 'tours' ) , saisie ) ;
const refus = champ ( m , 'tours' ) . closest ( '[data-tours]' ) . querySelector ( ` [data-refus=" ${ code } "] ` ) ;
expect ( refus , ` refus de « ${ saisie } » ` ) . not . toBeNull ( ) ;
expect ( refus . querySelector ( 'p' ) . textContent ) . toBe ( t ( code , details ) ) ;
expect ( refus . querySelector ( '[data-commande]' ) ) . toBeNull ( ) ;
expect ( fenetre ( ) ) . toBeNull ( ) ;
expect ( ecritures ( m ) ) . toBe ( avant ) ;
}
const r0 = revision ( m ) ;
await saisir ( m , champ ( m , 'tours' ) , '1' ) ;
expect ( ecritures ( m ) ) . toBe ( avant ) ;
await cliquer ( m , commande ( fenetre ( ) , 'confirmer' ) ) ;
await attendre ( m ) ;
expect ( charge ( m ) . evenement . tours ) . toBe ( 1 ) ;
expect ( charge ( m ) . reservations ) . toEqual ( [ ] ) ;
expect ( revision ( m ) ) . toBe ( r0 + 1 ) ;
} ) ;
test ( 'aucune perte : le nombre de tours change sans fenêtre, une entrée' , async ( ) => {
const m = await preparer ( chargeDe ( { tables : [ { id : 1 } ] , n : 4 , reservations : [ resa ( 1 , 1 , null ) ] } ) ) ;
const r0 = revision ( m ) ;
await saisir ( m , champ ( m , 'tours' ) , '5' ) ;
expect ( fenetre ( ) ) . toBeNull ( ) ;
expect ( charge ( m ) . evenement . tours ) . toBe ( 5 ) ;
expect ( revision ( m ) ) . toBe ( r0 + 1 ) ;
} ) ;
test ( 'R = 4, deux réservations au tour 4, une au tour 2, deux propositions ; 3 : la fenêtre les nomme, rien d’ écrit avant ; Confirmer : les deux retirées, celle du tour 2 gardée, une entrée, le fichier se rouvre' , async ( ) => {
const donnees = chargeDe ( {
tables : [ { id : 1 } , { id : 2 } ] ,
n : 8 ,
tours : 4 ,
reservations : [ resa ( 1 , 2 , null , 4 ) , resa ( 2 , 1 , null , 4 ) , resa ( 3 , 1 , null , 2 ) ] ,
} ) ;
// Chaque proposition honore les réservations : Iris Ombrelle à la table
// 2 et Théo Pervenche à la table 1 au tour 4, Ondine Lacasse à la table
// 1 au tour 2.
const listes = [ [ suite ( 1 , 4 ) , suite ( 5 , 8 ) ] , [ suite ( 1 , 4 ) , suite ( 5 , 8 ) ] , [ suite ( 1 , 4 ) , suite ( 5 , 8 ) ] , [ suite ( 2 , 5 ) , [ 1 , 6 , 7 , 8 ] ] ] ;
placer ( donnees , listes , { id : 1 } ) ;
placer ( donnees , listes , { id : 2 } ) ;
const m = await preparer ( donnees ) ;
const avant = ecritures ( m ) ;
const r0 = revision ( m ) ;
await saisir ( m , champ ( m , 'tours' ) , '3' ) ;
expect ( fenetre ( ) ) . not . toBeNull ( ) ;
expect ( ecritures ( m ) ) . toBe ( avant ) ;
const elements = elementsFenetre ( ) ;
expect ( elements ) . toHaveLength ( 4 ) ;
expect ( elements [ 0 ] ) . toContain ( 'Iris Ombrelle' ) ;
expect ( elements [ 0 ] ) . toContain ( 'table 2' ) ;
expect ( elements [ 1 ] ) . toContain ( 'Théo Pervenche' ) ;
expect ( elements [ 1 ] ) . toContain ( 'table 1' ) ;
expect ( elements . slice ( 2 ) ) . toEqual ( [ t ( 'tables.touchee.proposition' , { id : 1 } ) , t ( 'tables.touchee.proposition' , { id : 2 } ) ] ) ;
await cliquer ( m , commande ( fenetre ( ) , 'confirmer' ) ) ;
await attendre ( m ) ;
expect ( fenetre ( ) ) . toBeNull ( ) ;
expect ( revision ( m ) ) . toBe ( r0 + 1 ) ;
expect ( charge ( m ) . evenement . tours ) . toBe ( 3 ) ;
expect ( charge ( m ) . reservations ) . toEqual ( [ resa ( 3 , 1 , null , 2 ) ] ) ;
await m . seance . fermer ( ) ;
await m . seance . ouvrir ( BASE ) ;
expect ( m . seance . etat ( ) . charge . evenement . tours ) . toBe ( 3 ) ;
expect ( m . seance . etat ( ) . charge . reservations ) . toEqual ( [ resa ( 3 , 1 , null , 2 ) ] ) ;
} ) ;
test ( 'une hausse avec une proposition : sans fenêtre, une entrée, la dérive avertie par la séance' , async ( ) => {
const donnees = chargeDe ( { tables : [ { id : 1 } ] , n : 4 , tours : 2 } ) ;
placer ( donnees , [ [ suite ( 1 , 4 ) ] , [ suite ( 1 , 4 ) ] ] , { id : 1 } ) ;
const m = await preparer ( donnees ) ;
const r0 = revision ( m ) ;
await saisir ( m , champ ( m , 'tours' ) , '3' ) ;
expect ( fenetre ( ) ) . toBeNull ( ) ;
expect ( revision ( m ) ) . toBe ( r0 + 1 ) ;
expect ( charge ( m ) . evenement . tours ) . toBe ( 3 ) ;
expect ( charge ( m ) . propositions ) . toHaveLength ( 1 ) ;
expect ( m . seance . etat ( ) . avertissements . map ( ( { code } ) => code ) ) . toContain ( 'DERIVE_NOUVELLE' ) ;
} ) ;
test ( 'une baisse avec une proposition et sans réservation : la fenêtre nomme la proposition, rien d’ écrit avant' , async ( ) => {
const donnees = chargeDe ( { tables : [ { id : 1 } ] , n : 4 , tours : 3 } ) ;
placer ( donnees , [ [ suite ( 1 , 4 ) ] , [ suite ( 1 , 4 ) ] , [ suite ( 1 , 4 ) ] ] , { id : 1 } ) ;
const m = await preparer ( donnees ) ;
const avant = ecritures ( m ) ;
await saisir ( m , champ ( m , 'tours' ) , '2' ) ;
expect ( fenetre ( ) ) . not . toBeNull ( ) ;
expect ( ecritures ( m ) ) . toBe ( avant ) ;
expect ( elementsFenetre ( ) ) . toEqual ( [ t ( 'tables.touchee.proposition' , { id : 1 } ) ] ) ;
} ) ;
test ( 'Annuler la fenêtre des tours : rien n’ est écrit, et le champ rend la valeur enregistrée' , async ( ) => {
const donnees = chargeDe ( { tables : [ { id : 1 } ] , n : 4 , tours : 4 , reservations : [ resa ( 1 , 1 , null , 4 ) ] } ) ;
const m = await preparer ( donnees ) ;
const avant = ecritures ( m ) ;
await saisir ( m , champ ( m , 'tours' ) , '3' ) ;
await cliquer ( m , commande ( fenetre ( ) , 'renoncer' ) ) ;
expect ( fenetre ( ) ) . toBeNull ( ) ;
expect ( ecritures ( m ) ) . toBe ( avant ) ;
expect ( champ ( m , 'tours' ) . value ) . toBe ( '4' ) ;
expect ( charge ( m ) . reservations ) . toHaveLength ( 1 ) ;
} ) ;
} ) ;
describe ( 'supprimer une table (§ 6.2)' , ( ) => {
test ( 'une table qui porte une réservation « tous » d’ Iris Ombrelle et un titre « animateur » : la fenêtre les nomme ; une table nue : aucune fenêtre, une entrée' , async ( ) => {
const m = await preparer (
chargeDe ( {
tables : [ { id : 1 } , { id : 2 } ] ,
n : 4 ,
reservations : [ resa ( 1 , 1 , null ) ] ,
titres : [ { table : 1 , siege : 1 , libelle : 'animateur' } ] ,
} ) ,
) ;
const avant = ecritures ( m ) ;
const r0 = revision ( m ) ;
await cliquer ( m , commande ( ligne ( m , 1 ) , 'supprimerTable' ) ) ;
expect ( fenetre ( ) ) . not . toBeNull ( ) ;
expect ( ecritures ( m ) ) . toBe ( avant ) ;
const elements = elementsFenetre ( ) ;
expect ( elements ) . toHaveLength ( 2 ) ;
expect ( elements [ 0 ] ) . toContain ( 'Iris Ombrelle' ) ;
expect ( elements [ 1 ] ) . toContain ( '« animateur »' ) ;
await cliquer ( m , commande ( fenetre ( ) , 'confirmer' ) ) ;
await attendre ( m ) ;
expect ( fenetre ( ) ) . toBeNull ( ) ;
expect ( charge ( m ) . tables . map ( ( { id } ) => id ) ) . toEqual ( [ 2 ] ) ;
expect ( charge ( m ) . reservations ) . toEqual ( [ ] ) ;
expect ( charge ( m ) . titres ) . toEqual ( [ ] ) ;
expect ( revision ( m ) ) . toBe ( r0 + 1 ) ;
await cliquer ( m , commande ( ligne ( m , 2 ) , 'supprimerTable' ) ) ;
expect ( fenetre ( ) ) . toBeNull ( ) ;
expect ( charge ( m ) . tables ) . toEqual ( [ ] ) ;
expect ( revision ( m ) ) . toBe ( r0 + 2 ) ;
expect ( section ( m ) . textContent ) . toContain ( t ( 'tables.liste.aucune' ) ) ;
} ) ;
} ) ;
describe ( 'en lecture (§ 8.4)' , ( ) => {
test ( 'créer une table, surcharger, changer le défaut ou les tours : refusés contre la commande ou le champ, le message porte « Modifier », rien d’ écrit' , async ( ) => {
const m = await preparer ( chargeDe ( { tables : [ { id : 1 } ] , n : 4 } ) , { mode : 'lecture' } ) ;
const avant = ecritures ( m ) ;
const modifier = t ( 'communs.remede.modifier' ) ;
await cliquer ( m , commande ( section ( m ) , 'creerTableRonde' ) ) ;
const pres = commande ( section ( m ) , 'creerTableRonde' ) . parentElement ;
expect ( pres . querySelector ( '[data-refus="MODE_LECTURE"]' ) . textContent ) . toContain ( modifier ) ;
const porteurs = [
[ champTable ( m , 1 ) , ( e ) => e . closest ( '[data-table]' ) ] ,
[ champ ( m , 'siegesParDefaut' ) , ( e ) => e . closest ( '[data-defaut]' ) ] ,
[ champ ( m , 'tours' ) , ( e ) => e . closest ( '[data-tours]' ) ] ,
] ;
expect ( porteurs ) . toHaveLength ( 3 ) ;
for ( const [ entree , porteur ] of porteurs ) {
await cliquer ( m , entree ) ;
await touche ( '5' ) ;
await attendre ( m ) ;
const refus = porteur ( entree ) . querySelector ( '[data-refus="MODE_LECTURE"]' ) ;
expect ( refus , entree . dataset . champ ) . not . toBeNull ( ) ;
expect ( refus . textContent ) . toContain ( modifier ) ;
expect ( commande ( refus , 'modifier' ) . textContent ) . toBe ( modifier ) ;
}
expect ( charge ( m ) . tables ) . toHaveLength ( 1 ) ;
expect ( ecritures ( m ) ) . toBe ( avant ) ;
} ) ;
} ) ;