673 lines
31 KiB
JavaScript
673 lines
31 KiB
JavaScript
|
|
// © 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();
|
|||
|
|
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);
|
|||
|
|
});
|
|||
|
|
});
|