1056 lines
48 KiB
JavaScript
1056 lines
48 KiB
JavaScript
|
|
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|||
|
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
|||
|
|
|
|||
|
|
// Les gestes du pointeur sur le plan, dans un vrai moteur de rendu (§ 5.8,
|
|||
|
|
// § 5.9, § 6.1, § 6.2, § 7.1, § 7.2, § 7.3, § 8.4, § 8.5, § 9) : la capture
|
|||
|
|
// sur la racine, le modèle confié au relâcher, aucune lecture de mise en page
|
|||
|
|
// pendant un glissement, un seul écouteur, le déplacement de vue, le dépôt
|
|||
|
|
// par la géométrie, les annulations, les gestes d'une personne sous les deux
|
|||
|
|
// états des données, la table glissée, la poignée et ses refus, la lecture,
|
|||
|
|
// l'inactivité, le dépôt refusé sur une table pleine.
|
|||
|
|
//
|
|||
|
|
// Chaque épreuve monte App par monterApp, sur le système de fichiers simulé
|
|||
|
|
// où un fichier d'état d'épreuve est posé puis ouvert, au cadre de
|
|||
|
|
// 1 366 × 700, et agit par des entrées réelles (test/navigateur/entrees.js) ;
|
|||
|
|
// elle lit le rendu — attributs data-…, transform, boîte, texte — et le
|
|||
|
|
// disque simulé : « une entrée » est une entrée du journal relu de révision
|
|||
|
|
// supérieure à celle de l'état avant le geste, l'en-tête de l'état relu
|
|||
|
|
// portant cette révision plus un. Les points visés se calculent par la
|
|||
|
|
// géométrie des tables et la vue relue de l'attribut, sollicitées une
|
|||
|
|
// seconde fois (§ 19.1). 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 } from 'vitest';
|
|||
|
|
import { flushSync } from 'svelte';
|
|||
|
|
import { appliquerCommande } from '../../application/commandes.js';
|
|||
|
|
import { etatPlaces } from '../../application/places.js';
|
|||
|
|
import { DELAI_REGLAGES_MS, INACTIVITE_MS } from '../../application/seance.js';
|
|||
|
|
import { constante } from '../../geometrie/constantes.js';
|
|||
|
|
import { surLeSol } from '../../geometrie/designation.js';
|
|||
|
|
import { diametre, geometrieTable } from '../../geometrie/tables.js';
|
|||
|
|
import { dessinDepuisEcran, ecranDepuisDessin, kAjuste, transformDe, vueDepuisTransform } from '../../geometrie/vue.js';
|
|||
|
|
import { serialiser, serialiserCharge } from '../../stockage/canonique.js';
|
|||
|
|
import { analyser, creerCharge } from '../../stockage/document.js';
|
|||
|
|
import { lireJournal } from '../../stockage/journal.js';
|
|||
|
|
import { SUFFIXES } from '../../stockage/noms.js';
|
|||
|
|
import { VERSION } from '../../version.genere.js';
|
|||
|
|
import TEXTE_HUIT from '../../../test/fixtures/plan/huit_tables_retenu.gtt.json?raw';
|
|||
|
|
import { fixerCadre, glisser, pointer, touche } from '../../../test/navigateur/entrees.js';
|
|||
|
|
import { monterApp } from '../../../test/navigateur/montage.js';
|
|||
|
|
import { cadreDepuisRect, englobantDuTour } from './vue_plan.js';
|
|||
|
|
|
|||
|
|
const BASE = 'soiree';
|
|||
|
|
const SEUIL = constante('SEUIL_GLISSER_PX');
|
|||
|
|
|
|||
|
|
// --- Les configurations d'épreuve ---------------------------------------------
|
|||
|
|
|
|||
|
|
const NOMS = [
|
|||
|
|
['Ombrelle', 'Iris'], ['Pervenche', 'Théo'], ['Lacasse', 'Ondine'], ['Grisaille', null],
|
|||
|
|
['Tourmaline', 'Basile'], ['Arrosoir', 'Capucine'], ['Cerfeuil', 'Aurèle'], ['Bruyère', 'Mélisande'],
|
|||
|
|
['Galet', 'Firmin'], ['Mistral', 'Clélia'], ['Houblon', 'Anatole'], ['Sarcelle', 'Solveig'],
|
|||
|
|
['Ravenelle', 'Octave'], ['Pimprenelle', 'Albane'], ['Coquelicot', 'Gaspard'], ['Lavandière', 'Ysaline'],
|
|||
|
|
['Châtaigne', 'Bérénice'], ['Murier', 'Lazare'],
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// La charge du fichier d'épreuve : huit rondes de trois, vingt personnes,
|
|||
|
|
// trois tours, la proposition 1 retenue ; Iris Ombrelle (1) à la table 4
|
|||
|
|
// au tour 2, la table 7 assied 19 et 20 à ce tour.
|
|||
|
|
const huitRetenue = () => analyser(TEXTE_HUIT).charge;
|
|||
|
|
|
|||
|
|
// La même, avant la retenue : la proposition 1 seule, le plan proposé.
|
|||
|
|
function huitAvantRetenue() {
|
|||
|
|
const charge = huitRetenue();
|
|||
|
|
charge.retenu = null;
|
|||
|
|
charge.evenement.etat = 'propose';
|
|||
|
|
return charge;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const executer = (charge, nom, args) => appliquerCommande(nom, charge, args).charge;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Une charge d'épreuve : des rondes posées sur une ligne, aux places données
|
|||
|
|
* — null suit le défaut —, les personnes de NOMS, et, quand tours est donné,
|
|||
|
|
* une proposition à sièges non attribués de ces listes, retenue si retenir.
|
|||
|
|
*/
|
|||
|
|
function chargeDe({ id, defaut = 8, places, ecart = 300, personnes, nombreTours = 3, tours = null, retenir = true, attribuer = false }) {
|
|||
|
|
const charge = creerCharge({ id, nom: 'Club des Merles', siegesParDefaut: defaut, tours: nombreTours });
|
|||
|
|
charge.reglages.attribuerSieges = attribuer;
|
|||
|
|
charge.participants = NOMS.slice(0, personnes).map(([nom, prenom], rang) => ({
|
|||
|
|
id: rang + 1,
|
|||
|
|
nom,
|
|||
|
|
prenom,
|
|||
|
|
appartenance: null,
|
|||
|
|
courriel: null,
|
|||
|
|
titrePressenti: null,
|
|||
|
|
notes: null,
|
|||
|
|
exclu: false,
|
|||
|
|
}));
|
|||
|
|
charge.tables = places.map((sieges, rang) => ({ id: rang + 1, numero: rang + 1, sieges, forme: 'ronde', position: { x: ecart * rang, y: 0 } }));
|
|||
|
|
charge.prochainsIds = { participant: personnes + 1, table: places.length + 1, proposition: 1 };
|
|||
|
|
if (tours === null) return charge;
|
|||
|
|
charge.propositions = [
|
|||
|
|
{
|
|||
|
|
id: 1,
|
|||
|
|
graine: 1,
|
|||
|
|
arret: 40,
|
|||
|
|
historique: 1000,
|
|||
|
|
produitVersion: 'epreuve',
|
|||
|
|
siegesAttribues: false,
|
|||
|
|
tables: charge.tables.map((table) => table.id),
|
|||
|
|
capacites: places.map((sieges) => sieges ?? defaut),
|
|||
|
|
tours: nombreTours,
|
|||
|
|
participants: charge.participants.map((personne) => personne.id),
|
|||
|
|
placement: tours.map((listes) => ({ sieges: listes, reserve: [] })),
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
charge.prochainsIds.proposition = 2;
|
|||
|
|
charge.evenement.etat = 'propose';
|
|||
|
|
return retenir ? executer(charge, 'retenirProposition', { id: 1 }) : charge;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const suite = (de, a) => Array.from({ length: a - de + 1 }, (_, i) => de + i);
|
|||
|
|
|
|||
|
|
// Trois tables de huit, dix-huit personnes, retenu : la table 3 assied
|
|||
|
|
// sept personnes au seul tour 3, six aux deux autres ; la table 1, six,
|
|||
|
|
// six, puis cinq.
|
|||
|
|
const troisDeHuit = () =>
|
|||
|
|
chargeDe({
|
|||
|
|
id: 'evt-trois-de-huit',
|
|||
|
|
places: [null, null, null],
|
|||
|
|
personnes: 18,
|
|||
|
|
tours: [
|
|||
|
|
[suite(1, 6), suite(7, 12), suite(13, 18)],
|
|||
|
|
[suite(13, 18), suite(1, 6), suite(7, 12)],
|
|||
|
|
[suite(1, 5), suite(6, 11), suite(12, 18)],
|
|||
|
|
],
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// --- Le montage ---------------------------------------------------------------
|
|||
|
|
|
|||
|
|
let montes = [];
|
|||
|
|
let nettoyages = [];
|
|||
|
|
let erreursDemontees = [];
|
|||
|
|
beforeEach(async () => {
|
|||
|
|
await fixerCadre(1366, 700);
|
|||
|
|
});
|
|||
|
|
afterEach(async () => {
|
|||
|
|
for (const nettoyer of nettoyages.reverse()) nettoyer();
|
|||
|
|
nettoyages = [];
|
|||
|
|
const erreurs = [...erreursDemontees, ...montes.flatMap((m) => m.erreurs())];
|
|||
|
|
erreursDemontees = [];
|
|||
|
|
for (const m of montes.reverse()) m.demonter();
|
|||
|
|
montes = [];
|
|||
|
|
window.scrollTo(0, 0);
|
|||
|
|
await fixerCadre(1366, 700);
|
|||
|
|
expect(erreurs, 'erreurs reçues par le collecteur').toEqual([]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Démonte un montage avant la fin de l'épreuve, et relève son collecteur :
|
|||
|
|
// le montage suivant prend sa place dans la fenêtre.
|
|||
|
|
function demonter(m) {
|
|||
|
|
erreursDemontees.push(...m.erreurs());
|
|||
|
|
m.demonter();
|
|||
|
|
montes = montes.filter((autre) => autre !== m);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Attend que l'assertion passe, le rendu de Svelte à jour, une image après
|
|||
|
|
// l'autre.
|
|||
|
|
async function attendre(assertion) {
|
|||
|
|
for (let essai = 0; ; essai += 1) {
|
|||
|
|
flushSync();
|
|||
|
|
try {
|
|||
|
|
return assertion();
|
|||
|
|
} catch (erreur) {
|
|||
|
|
if (essai >= 200) throw erreur;
|
|||
|
|
await new Promise((resoudre) => requestAnimationFrame(resoudre));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const uneImage = () => new Promise((resoudre) => requestAnimationFrame(resoudre));
|
|||
|
|
async function images(n = 3) {
|
|||
|
|
for (let i = 0; i < n; i += 1) await uneImage();
|
|||
|
|
flushSync();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// La charge posée en fichier d'état, ouverte — en écriture sauf mode
|
|||
|
|
// 'lecture' —, la section du plan ouverte et sa vue posée.
|
|||
|
|
async function ouvrir(charge, { mode = 'ecriture', horloge } = {}) {
|
|||
|
|
const texte = serialiser(charge, { revision: 1, produitVersion: VERSION.affichee });
|
|||
|
|
const m = await monterApp({ fichiers: [{ racine: 'documents', chemin: `${BASE}${SUFFIXES.etat}`, texte }], horloge });
|
|||
|
|
montes.push(m);
|
|||
|
|
await m.seance.ouvrir(BASE);
|
|||
|
|
if (mode === 'ecriture') await m.seance.passerEnEcriture();
|
|||
|
|
m.ui.allerA('plan');
|
|||
|
|
flushSync();
|
|||
|
|
await attendre(() => expect(groupe(m).getAttribute('transform')).not.toBeNull());
|
|||
|
|
await attendre(() => expect(tous(m, '[data-table]').length).toBe(charge.tables.length));
|
|||
|
|
return m;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Pose la vue par le cadrage de la séance, puis remonte le plan, qui la
|
|||
|
|
// restitue.
|
|||
|
|
async function avecVue(m, vue) {
|
|||
|
|
m.seance.demanderCadrage(vue);
|
|||
|
|
m.ui.allerA('tables');
|
|||
|
|
flushSync();
|
|||
|
|
m.ui.allerA('plan');
|
|||
|
|
flushSync();
|
|||
|
|
await attendre(() => expect(groupe(m).getAttribute('transform')).toBe(transformDe(vue)));
|
|||
|
|
await images(2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- Les lectures du rendu et du disque ---------------------------------------
|
|||
|
|
|
|||
|
|
const dans = (m, selecteur) => m.racine.querySelector(selecteur);
|
|||
|
|
const tous = (m, selecteur) => [...m.racine.querySelectorAll(selecteur)];
|
|||
|
|
const racinePlan = (m) => dans(m, '[data-plan]');
|
|||
|
|
const groupe = (m) => dans(m, '[data-vue]');
|
|||
|
|
const tableEl = (m, id) => dans(m, `[data-table="${id}"]`);
|
|||
|
|
const fantomeEl = (m) => dans(m, '[data-fantome]');
|
|||
|
|
// Le fantôme se montre : son style calculé n'est pas display: none, et il
|
|||
|
|
// porte son dessin. checkVisibility ne sert pas ici : Chromium le rend vrai
|
|||
|
|
// pour un <g> SVG en display: none.
|
|||
|
|
const fantomeMontre = (m) => getComputedStyle(fantomeEl(m)).display !== 'none' && fantomeEl(m).childElementCount > 0;
|
|||
|
|
const cadreDe = (m) => cadreDepuisRect(racinePlan(m).getBoundingClientRect());
|
|||
|
|
const vueLue = (m) => vueDepuisTransform(groupe(m).getAttribute('transform'));
|
|||
|
|
const charge = (m) => m.seance.etat().charge;
|
|||
|
|
const texteDe = (element) => element.textContent.replace(/\s+/g, ' ').trim();
|
|||
|
|
const chaisesDe = (m, id) => tous(m, `[data-siege^="${id}:"]`);
|
|||
|
|
|
|||
|
|
// L'état des places du tour montré, tel que le plan le calcule.
|
|||
|
|
const etatMontre = (m) => etatPlaces(charge(m), { tour: m.ui.etat().tour, proposition: m.ui.etat().proposition });
|
|||
|
|
|
|||
|
|
// La géométrie d'une table du tour montré, à sa capacité.
|
|||
|
|
function geometrieDe(m, id) {
|
|||
|
|
const table = etatMontre(m).tables.find((candidate) => candidate.id === id);
|
|||
|
|
return geometrieTable(table, table.capacite);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Le point écran d'un point de dessin, sous la vue relue.
|
|||
|
|
const ecranDe = (m, { x, y }) => ecranDepuisDessin(vueLue(m), cadreDe(m), x, y);
|
|||
|
|
const pointTable = (m, id) => ecranDe(m, geometrieDe(m, id).centre);
|
|||
|
|
const pointChaise = (m, id, siege) => ecranDe(m, geometrieDe(m, id).chaises[siege - 1]);
|
|||
|
|
const pointPoignee = (m, id) => ecranDe(m, geometrieDe(m, id).poignee);
|
|||
|
|
|
|||
|
|
// Un point du sol : près du coin haut gauche du cadre, hors de toute emprise.
|
|||
|
|
function pointSol(m) {
|
|||
|
|
const cadre = cadreDe(m);
|
|||
|
|
const point = { x: cadre.gauche + 6, y: cadre.haut + 6 };
|
|||
|
|
const geometries = etatMontre(m).tables.map((table) => geometrieTable(table, table.capacite));
|
|||
|
|
expect(surLeSol(dessinDepuisEcran(vueLue(m), cadre, point.x, point.y), geometries)).toBe(true);
|
|||
|
|
expect(document.elementFromPoint(point.x, point.y)).toBe(racinePlan(m));
|
|||
|
|
return point;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Un point à (dx, dy) d'un autre.
|
|||
|
|
const decale = ({ x, y }, dx, dy) => ({ x: x + dx, y: y + dy });
|
|||
|
|
|
|||
|
|
// Le texte de l'état et du journal de l'événement ouvert, et sa révision.
|
|||
|
|
function fichiers(m) {
|
|||
|
|
return {
|
|||
|
|
etat: m.fs.contenu('documents', `${BASE}${SUFFIXES.etat}`),
|
|||
|
|
journal: m.fs.contenu('documents', `${BASE}${SUFFIXES.journal}`),
|
|||
|
|
revision: m.seance.etat().evenement.revision,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Les entrées du journal relu de révision supérieure à r.
|
|||
|
|
function entreesApres(m, r) {
|
|||
|
|
const texte = m.fs.contenu('documents', `${BASE}${SUFFIXES.journal}`);
|
|||
|
|
return texte === null ? [] : lireJournal(texte).entrees.filter((entree) => entree.revision > r);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Attend exactement une entrée après la révision r, l'en-tête de l'état relu
|
|||
|
|
// à r + 1 ; rend son libellé.
|
|||
|
|
async function uneEntree(m, r) {
|
|||
|
|
await attendre(() => expect(m.seance.etat().evenement.revision).toBe(r + 1));
|
|||
|
|
const entrees = entreesApres(m, r);
|
|||
|
|
expect(entrees).toHaveLength(1);
|
|||
|
|
expect(analyser(m.fs.contenu('documents', `${BASE}${SUFFIXES.etat}`)).entete.revision).toBe(r + 1);
|
|||
|
|
return entrees[0].libelle;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Rien n'a été écrit depuis avant : l'état, le journal et la révision.
|
|||
|
|
async function rienEcrit(m, avant) {
|
|||
|
|
await images(4);
|
|||
|
|
expect(fichiers(m)).toEqual(avant);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Les refus posés sur le plan, et celui que désigne aria-describedby d'un élément.
|
|||
|
|
const decrit = (element) => {
|
|||
|
|
const id = element.getAttribute('aria-describedby');
|
|||
|
|
return id === null ? null : document.getElementById(id);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Les boîtes de deux éléments se recoupent.
|
|||
|
|
function recoupe(a, b) {
|
|||
|
|
const p = a.getBoundingClientRect();
|
|||
|
|
const q = b.getBoundingClientRect();
|
|||
|
|
return p.left < q.right && q.left < p.right && p.top < q.bottom && q.top < p.bottom;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Le pointerId du prochain appui sur la racine du plan.
|
|||
|
|
function pointeurDuProchainAppui(m) {
|
|||
|
|
const lu = { id: null };
|
|||
|
|
racinePlan(m).addEventListener('pointerdown', (evenement) => {
|
|||
|
|
lu.id = evenement.pointerId;
|
|||
|
|
}, { once: true });
|
|||
|
|
return lu;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Glisse par pas, une image entre deux pas, sans relâcher.
|
|||
|
|
async function presserEtBouger(depuis, vers, { pas = 6 } = {}) {
|
|||
|
|
await pointer('presser', depuis.x, depuis.y);
|
|||
|
|
for (let i = 1; i <= pas; i += 1) {
|
|||
|
|
const t = i / pas;
|
|||
|
|
await pointer('bouger', depuis.x + (vers.x - depuis.x) * t, depuis.y + (vers.y - depuis.y) * t);
|
|||
|
|
await uneImage();
|
|||
|
|
}
|
|||
|
|
flushSync();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Espionne les appels d'une méthode d'un prototype ; rend le compteur.
|
|||
|
|
function espionner(prototype, methode) {
|
|||
|
|
const originale = prototype[methode];
|
|||
|
|
const compte = { appels: 0 };
|
|||
|
|
prototype[methode] = function espion(...parametres) {
|
|||
|
|
compte.appels += 1;
|
|||
|
|
return Reflect.apply(originale, this, parametres);
|
|||
|
|
};
|
|||
|
|
nettoyages.push(() => {
|
|||
|
|
prototype[methode] = originale;
|
|||
|
|
});
|
|||
|
|
return compte;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Relève les commandes que le plan confie à la séance.
|
|||
|
|
function releverCommandes(m) {
|
|||
|
|
const seance = m.seance;
|
|||
|
|
const originale = seance.executer;
|
|||
|
|
const appels = [];
|
|||
|
|
seance.executer = (nom, args) => {
|
|||
|
|
appels.push({ nom, args });
|
|||
|
|
return originale(nom, args);
|
|||
|
|
};
|
|||
|
|
nettoyages.push(() => {
|
|||
|
|
seance.executer = originale;
|
|||
|
|
});
|
|||
|
|
return appels;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- La capture et le modèle au relâcher (§ 7.3, § 8.2) ---------------------
|
|||
|
|
|
|||
|
|
describe('la capture du pointeur et le modèle au relâcher (§ 7.3, § 8.2)', () => {
|
|||
|
|
test('après un appui sur une table, la racine capture le pointeur, la table non ; un mouvement hors de la fenêtre et un relâcher dehors terminent le glissement', async () => {
|
|||
|
|
const m = await ouvrir(huitRetenue());
|
|||
|
|
const pointeur = pointeurDuProchainAppui(m);
|
|||
|
|
const depart = pointTable(m, 1);
|
|||
|
|
await pointer('presser', depart.x, depart.y);
|
|||
|
|
expect(pointeur.id).not.toBeNull();
|
|||
|
|
expect(racinePlan(m).hasPointerCapture(pointeur.id)).toBe(true);
|
|||
|
|
expect(tableEl(m, 1).hasPointerCapture(pointeur.id)).toBe(false);
|
|||
|
|
const r = fichiers(m).revision;
|
|||
|
|
await pointer('bouger', depart.x + 30, depart.y + 10);
|
|||
|
|
await pointer('bouger', innerWidth + 300, innerHeight + 300);
|
|||
|
|
await pointer('relacher', innerWidth + 300, innerHeight + 300);
|
|||
|
|
await uneEntree(m, r);
|
|||
|
|
await attendre(() => expect(tableEl(m, 1).getAttribute('transform')).toBeNull());
|
|||
|
|
expect(racinePlan(m).hasPointerCapture(pointeur.id)).toBe(false);
|
|||
|
|
const position = { ...charge(m).tables[0].position };
|
|||
|
|
await pointer('bouger', depart.x + 80, depart.y + 80);
|
|||
|
|
await images(3);
|
|||
|
|
expect(tableEl(m, 1).getAttribute('transform')).toBeNull();
|
|||
|
|
expect(charge(m).tables[0].position).toEqual(position);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('pendant le glissement d’une table, ni la charge, ni la révision, ni le disque ne bougent ; le transform change ; racine et table restent les mêmes nœuds ; au relâcher, une entrée « Déplacé la table … »', async () => {
|
|||
|
|
const m = await ouvrir(huitRetenue());
|
|||
|
|
const racine = racinePlan(m);
|
|||
|
|
const table = tableEl(m, 3);
|
|||
|
|
const avant = fichiers(m);
|
|||
|
|
const texteCharge = serialiserCharge(charge(m));
|
|||
|
|
const ecritures = m.fs.compteurs.ecritures;
|
|||
|
|
const depart = pointTable(m, 3);
|
|||
|
|
await presserEtBouger(depart, decale(depart, 50, 30));
|
|||
|
|
expect(table.getAttribute('transform')).not.toBeNull();
|
|||
|
|
expect(serialiserCharge(charge(m))).toBe(texteCharge);
|
|||
|
|
expect(fichiers(m)).toEqual(avant);
|
|||
|
|
expect(m.fs.compteurs.ecritures).toBe(ecritures);
|
|||
|
|
expect(racinePlan(m)).toBe(racine);
|
|||
|
|
expect(tableEl(m, 3)).toBe(table);
|
|||
|
|
await pointer('relacher', depart.x + 50, depart.y + 30);
|
|||
|
|
expect(await uneEntree(m, avant.revision)).toBe('Déplacé la table 3');
|
|||
|
|
await attendre(() => expect(table.getAttribute('transform')).toBeNull());
|
|||
|
|
expect(tableEl(m, 3)).toBe(table);
|
|||
|
|
expect(racinePlan(m)).toBe(racine);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// --- Aucune lecture de mise en page, un écouteur (§ 7.2, § 7.3) --------------
|
|||
|
|
|
|||
|
|
describe('aucune lecture de mise en page pendant un glissement ; un seul écouteur (§ 7.2, § 7.3)', () => {
|
|||
|
|
test('glissement réel d’une table, puis de la vue, dix mouvements chacun : aucun getBBox, getScreenCTM, getBoundingClientRect', async () => {
|
|||
|
|
const m = await ouvrir(huitRetenue());
|
|||
|
|
for (const [nom, depart] of [['table', pointTable(m, 2)], ['vue', pointSol(m)]]) {
|
|||
|
|
const transformAvant = (nom === 'table' ? tableEl(m, 2) : groupe(m)).getAttribute('transform');
|
|||
|
|
await pointer('presser', depart.x, depart.y);
|
|||
|
|
// Les espions se posent après l'appui, qui relit le cadre, et avant le
|
|||
|
|
// premier mouvement : celui qui franchit le seuil et fait de l'appui un
|
|||
|
|
// glissement est relevé comme les suivants.
|
|||
|
|
const espions = [
|
|||
|
|
espionner(SVGGraphicsElement.prototype, 'getBBox'),
|
|||
|
|
espionner(SVGGraphicsElement.prototype, 'getScreenCTM'),
|
|||
|
|
espionner(Element.prototype, 'getBoundingClientRect'),
|
|||
|
|
];
|
|||
|
|
await pointer('bouger', depart.x + 8, depart.y + 4);
|
|||
|
|
await uneImage();
|
|||
|
|
for (let i = 1; i <= 10; i += 1) {
|
|||
|
|
await pointer('bouger', depart.x + 8 + 3 * i, depart.y + 4 + 2 * i);
|
|||
|
|
await uneImage();
|
|||
|
|
}
|
|||
|
|
const transformApres = (nom === 'table' ? tableEl(m, 2) : groupe(m)).getAttribute('transform');
|
|||
|
|
const appels = espions.map(({ appels }) => appels);
|
|||
|
|
for (const nettoyer of nettoyages.splice(0).reverse()) nettoyer();
|
|||
|
|
expect(appels, nom).toEqual([0, 0, 0]);
|
|||
|
|
expect(transformApres, nom).not.toBe(transformAvant);
|
|||
|
|
await pointer('relacher', depart.x + 38, depart.y + 24);
|
|||
|
|
await images(3);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('l’espion d’addEventListener : sur les tables et les chaises, aucun écouteur de pointeur ni de molette ; sur la racine, les cinq, la molette non passive ; dix pointermove d’une même tâche n’écrivent qu’une fois le transform', async () => {
|
|||
|
|
const releves = [];
|
|||
|
|
const originale = EventTarget.prototype.addEventListener;
|
|||
|
|
EventTarget.prototype.addEventListener = function espion(type, ecouteur, options) {
|
|||
|
|
releves.push({ cible: this, type, options });
|
|||
|
|
return Reflect.apply(originale, this, [type, ecouteur, options]);
|
|||
|
|
};
|
|||
|
|
nettoyages.push(() => {
|
|||
|
|
EventTarget.prototype.addEventListener = originale;
|
|||
|
|
});
|
|||
|
|
const m = await ouvrir(huitRetenue());
|
|||
|
|
const capacites = charge(m).tables.length * 3;
|
|||
|
|
const elements = [...tous(m, '[data-table]'), ...tous(m, '[data-siege]')];
|
|||
|
|
expect(elements.length).toBe(charge(m).tables.length + capacites);
|
|||
|
|
expect(elements.length).toBeGreaterThan(0);
|
|||
|
|
const DU_POINTEUR = /^(pointer|wheel)/;
|
|||
|
|
const surDessin = releves.filter(({ cible, type }) => elements.includes(cible) && DU_POINTEUR.test(type));
|
|||
|
|
expect(surDessin).toEqual([]);
|
|||
|
|
for (const element of elements) {
|
|||
|
|
for (const propriete of ['onpointerdown', 'onpointermove', 'onpointerup', 'onpointercancel', 'onwheel']) {
|
|||
|
|
expect(element[propriete], propriete).toBeNull();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const surRacine = releves.filter(({ cible }) => cible === racinePlan(m));
|
|||
|
|
const types = surRacine.map(({ type }) => type);
|
|||
|
|
for (const type of ['pointerdown', 'pointermove', 'pointerup', 'pointercancel', 'wheel']) expect(types, type).toContain(type);
|
|||
|
|
expect(surRacine.find(({ type }) => type === 'wheel').options).toEqual({ passive: false });
|
|||
|
|
// Contrôle d'échec : un écouteur que l'épreuve pose sur une chaise est relevé.
|
|||
|
|
const chaise = dans(m, '[data-siege]');
|
|||
|
|
chaise.addEventListener('pointerdown', () => {});
|
|||
|
|
expect(releves.filter(({ cible, type }) => cible === chaise && DU_POINTEUR.test(type))).toHaveLength(1);
|
|||
|
|
|
|||
|
|
const table = tableEl(m, 2);
|
|||
|
|
const pointeur = pointeurDuProchainAppui(m);
|
|||
|
|
const depart = pointTable(m, 2);
|
|||
|
|
await pointer('presser', depart.x, depart.y);
|
|||
|
|
await images(1);
|
|||
|
|
const ecritures = [];
|
|||
|
|
const observateur = new MutationObserver((liste) => ecritures.push(...liste.filter(({ attributeName }) => attributeName === 'transform')));
|
|||
|
|
observateur.observe(table, { attributes: true });
|
|||
|
|
for (let i = 1; i <= 10; i += 1) {
|
|||
|
|
racinePlan(m).dispatchEvent(
|
|||
|
|
new PointerEvent('pointermove', { pointerId: pointeur.id, clientX: depart.x + 10 * i, clientY: depart.y, bubbles: true }),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
await Promise.resolve();
|
|||
|
|
expect(ecritures.length).toBeLessThanOrEqual(1);
|
|||
|
|
await uneImage();
|
|||
|
|
await Promise.resolve();
|
|||
|
|
expect(ecritures.length).toBeLessThanOrEqual(1);
|
|||
|
|
expect(table.getAttribute('transform')).not.toBeNull();
|
|||
|
|
observateur.disconnect();
|
|||
|
|
await pointer('relacher', depart.x + 100, depart.y);
|
|||
|
|
await images(3);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// --- Le déplacement de vue (§ 7.1, § 7.2, § 8.5) -------------------------------
|
|||
|
|
|
|||
|
|
describe('le déplacement de vue, geste du sol (§ 7.1, § 7.2, § 8.5)', () => {
|
|||
|
|
test('(60, −40) px avancent tx et ty d’autant, k inchangé ; seul le transform du groupe change ; le cadrage s’écrit après DELAI_REGLAGES_MS ; en écriture, en lecture, sur un plan bloqué', async () => {
|
|||
|
|
const m = await ouvrir(huitRetenue());
|
|||
|
|
const parcourir = async (nom) => {
|
|||
|
|
const avant = fichiers(m);
|
|||
|
|
const ecritures = m.fs.compteurs.ecritures;
|
|||
|
|
const vue = vueLue(m);
|
|||
|
|
const depart = pointSol(m);
|
|||
|
|
const mutations = [];
|
|||
|
|
const observateur = new MutationObserver((liste) => mutations.push(...liste));
|
|||
|
|
observateur.observe(racinePlan(m), { attributes: true, childList: true, subtree: true, characterData: true });
|
|||
|
|
await glisser(depart, decale(depart, 60, -40), { pas: 10 });
|
|||
|
|
await images(2);
|
|||
|
|
observateur.disconnect();
|
|||
|
|
expect(mutations.length, nom).toBeGreaterThan(0);
|
|||
|
|
for (const mutation of mutations) {
|
|||
|
|
expect(mutation.type, nom).toBe('attributes');
|
|||
|
|
expect(mutation.attributeName, nom).toBe('transform');
|
|||
|
|
expect(mutation.target, nom).toBe(groupe(m));
|
|||
|
|
}
|
|||
|
|
const apres = vueLue(m);
|
|||
|
|
expect(apres.k, nom).toBe(vue.k);
|
|||
|
|
expect(apres.tx, nom).toBeCloseTo(vue.tx + 60, 6);
|
|||
|
|
expect(apres.ty, nom).toBeCloseTo(vue.ty - 40, 6);
|
|||
|
|
expect(fichiers(m), nom).toEqual(avant);
|
|||
|
|
expect(m.fs.compteurs.ecritures, nom).toBe(ecritures);
|
|||
|
|
m.horloge.avancer(DELAI_REGLAGES_MS - 1);
|
|||
|
|
await images(2);
|
|||
|
|
expect(m.fs.compteurs.ecritures, nom).toBe(ecritures);
|
|||
|
|
m.horloge.avancer(1);
|
|||
|
|
await attendre(() => expect(m.fs.compteurs.ecritures, nom).toBe(ecritures + 1));
|
|||
|
|
const ecrit = JSON.parse(m.fs.contenu('documents', 'reglages_locaux.json')).cadrages.find(
|
|||
|
|
({ evenement }) => evenement === charge(m).evenement.id,
|
|||
|
|
);
|
|||
|
|
expect({ k: ecrit.k, tx: ecrit.tx, ty: ecrit.ty }, nom).toEqual(vueLue(m));
|
|||
|
|
expect(fichiers(m), nom).toEqual(avant);
|
|||
|
|
};
|
|||
|
|
await parcourir('écriture');
|
|||
|
|
await m.seance.passerEnLecture();
|
|||
|
|
flushSync();
|
|||
|
|
await parcourir('lecture');
|
|||
|
|
await m.seance.passerEnEcriture();
|
|||
|
|
await m.seance.executer('changerEtat', { etat: 'bloque' });
|
|||
|
|
flushSync();
|
|||
|
|
await parcourir('bloqué');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('un pointercancel en cours de glissement remet le transform d’origine', async () => {
|
|||
|
|
const m = await ouvrir(huitRetenue());
|
|||
|
|
const transform = groupe(m).getAttribute('transform');
|
|||
|
|
const pointeur = pointeurDuProchainAppui(m);
|
|||
|
|
const depart = pointSol(m);
|
|||
|
|
await presserEtBouger(depart, decale(depart, 70, 50));
|
|||
|
|
expect(groupe(m).getAttribute('transform')).not.toBe(transform);
|
|||
|
|
racinePlan(m).dispatchEvent(new PointerEvent('pointercancel', { pointerId: pointeur.id, bubbles: true }));
|
|||
|
|
flushSync();
|
|||
|
|
expect(groupe(m).getAttribute('transform')).toBe(transform);
|
|||
|
|
await pointer('relacher', depart.x + 70, depart.y + 50);
|
|||
|
|
await images(2);
|
|||
|
|
expect(groupe(m).getAttribute('transform')).toBe(transform);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// --- Le dépôt par la géométrie (§ 7.3, § 14.10) ------------------------------
|
|||
|
|
|
|||
|
|
describe('le dépôt se désigne par la géométrie (§ 7.3)', () => {
|
|||
|
|
test('relâchée au centre d’une chaise libre d’une autre table, la commande nomme cette table et ce siège ; sur une chaise que recouvre une liste, de même', async () => {
|
|||
|
|
const m = await ouvrir(huitRetenue());
|
|||
|
|
const commandes = releverCommandes(m);
|
|||
|
|
// Au tour 1, la table 8 n'assied que la personne 20 : ses sièges 2 et 3 sont libres.
|
|||
|
|
const r = fichiers(m).revision;
|
|||
|
|
await glisser(pointChaise(m, 2, 1), pointChaise(m, 8, 2));
|
|||
|
|
await uneEntree(m, r);
|
|||
|
|
expect(commandes).toEqual([{ nom: 'deplacerDansRetenu', args: { tour: 1, participant: 4, table: 8, siege: 2 } }]);
|
|||
|
|
|
|||
|
|
// La liste de la table 5 couvre les chaises de la table 6 : la personne 13
|
|||
|
|
// va au siège 3, libre, de la table 6.
|
|||
|
|
const liste = dans(m, '[data-liste="5"]');
|
|||
|
|
const visee = pointChaise(m, 6, 3);
|
|||
|
|
const boite = liste.getBoundingClientRect();
|
|||
|
|
expect(visee.x > boite.left && visee.x < boite.right && visee.y > boite.top && visee.y < boite.bottom).toBe(true);
|
|||
|
|
await glisser(pointChaise(m, 5, 1), visee);
|
|||
|
|
await uneEntree(m, r + 1);
|
|||
|
|
expect(commandes.at(-1)).toEqual({ nom: 'deplacerDansRetenu', args: { tour: 1, participant: 13, table: 6, siege: 3 } });
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// --- Annuler n'est pas finir (§ 7.3, § 8.4) ---------------------------------------
|
|||
|
|
|
|||
|
|
describe('annuler n’est pas finir (§ 7.3, § 8.4)', () => {
|
|||
|
|
const verrou = (m) => m.fs.contenu('documents', `${BASE}${SUFFIXES.verrou}`);
|
|||
|
|
|
|||
|
|
// Trois annulations d'un geste entamé : un pointercancel sur la racine,
|
|||
|
|
// Échap le focus sur <body>, le retour en lecture. montrer entame le
|
|||
|
|
// geste et rend la fonction qui vérifie qu'il est montré ; revenu vérifie
|
|||
|
|
// que tout est revenu.
|
|||
|
|
async function annulerTrois(m, { depart, vers, montre, revenu }) {
|
|||
|
|
for (const cause of ['pointercancel', 'echap', 'lecture']) {
|
|||
|
|
if (m.seance.etat().mode !== 'ecriture') await m.seance.passerEnEcriture();
|
|||
|
|
flushSync();
|
|||
|
|
const avant = fichiers(m);
|
|||
|
|
const ecritures = m.fs.compteurs.ecritures;
|
|||
|
|
const pointeur = pointeurDuProchainAppui(m);
|
|||
|
|
await presserEtBouger(depart(), vers());
|
|||
|
|
montre(cause);
|
|||
|
|
if (cause === 'pointercancel') {
|
|||
|
|
racinePlan(m).dispatchEvent(new PointerEvent('pointercancel', { pointerId: pointeur.id, bubbles: true }));
|
|||
|
|
} else if (cause === 'echap') {
|
|||
|
|
document.activeElement?.blur();
|
|||
|
|
expect(document.activeElement).toBe(document.body);
|
|||
|
|
await touche('Escape');
|
|||
|
|
} else {
|
|||
|
|
await m.seance.passerEnLecture();
|
|||
|
|
}
|
|||
|
|
flushSync();
|
|||
|
|
revenu(cause);
|
|||
|
|
await pointer('relacher', vers().x, vers().y);
|
|||
|
|
await images(3);
|
|||
|
|
revenu(cause);
|
|||
|
|
expect(fichiers(m), cause).toEqual(avant);
|
|||
|
|
if (cause === 'lecture') {
|
|||
|
|
expect(m.fs.compteurs.ecritures, cause).toBe(ecritures + 1);
|
|||
|
|
expect(verrou(m), cause).toBeNull();
|
|||
|
|
} else {
|
|||
|
|
expect(m.fs.compteurs.ecritures, cause).toBe(ecritures);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
test('une table : pointercancel, Échap, passage en lecture → le transform d’origine, rien d’écrit', async () => {
|
|||
|
|
const m = await ouvrir(huitRetenue());
|
|||
|
|
await annulerTrois(m, {
|
|||
|
|
depart: () => pointTable(m, 2),
|
|||
|
|
vers: () => decale(pointTable(m, 2), 60, 40),
|
|||
|
|
montre: (cause) => expect(tableEl(m, 2).getAttribute('transform'), cause).not.toBeNull(),
|
|||
|
|
revenu: (cause) => expect(tableEl(m, 2).getAttribute('transform'), cause).toBeNull(),
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('une personne : le fantôme se montre, puis se cache ; rien d’écrit', async () => {
|
|||
|
|
const m = await ouvrir(huitRetenue());
|
|||
|
|
await annulerTrois(m, {
|
|||
|
|
depart: () => pointChaise(m, 2, 1),
|
|||
|
|
vers: () => pointChaise(m, 8, 2),
|
|||
|
|
montre: (cause) => expect(fantomeMontre(m), cause).toBe(true),
|
|||
|
|
revenu: (cause) => expect(fantomeMontre(m), cause).toBe(false),
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('une poignée : les chaises tirées disparaissent, la table revient à sa capacité ; rien d’écrit', async () => {
|
|||
|
|
const m = await ouvrir(troisDeHuit());
|
|||
|
|
const k = vueLue(m).k;
|
|||
|
|
const pas = (60 / (2 * Math.PI)) * k / Math.SQRT2;
|
|||
|
|
await annulerTrois(m, {
|
|||
|
|
depart: () => pointPoignee(m, 1),
|
|||
|
|
vers: () => decale(pointPoignee(m, 1), 3 * pas, 3 * pas),
|
|||
|
|
montre: (cause) => expect(chaisesDe(m, 1).length, cause).toBe(11),
|
|||
|
|
revenu: (cause) => expect(chaisesDe(m, 1).length, cause).toBe(8),
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// --- Les gestes d'une personne, les deux états des données (§ 5.8, § 7.1) -------
|
|||
|
|
|
|||
|
|
describe('les gestes d’une personne (§ 7.1, § 8.2, § 5.8)', () => {
|
|||
|
|
test('sur un retenu, au tour 2 : déplacer, échanger, retirer → une entrée chacun ; le retenu ne change qu’au tour 2 ; la proposition reste identique', async () => {
|
|||
|
|
const m = await ouvrir(huitRetenue());
|
|||
|
|
m.ui.montrerTour(2);
|
|||
|
|
flushSync();
|
|||
|
|
await images(2);
|
|||
|
|
const origine = structuredClone(charge(m));
|
|||
|
|
let r = fichiers(m).revision;
|
|||
|
|
|
|||
|
|
await glisser(pointChaise(m, 4, 1), pointChaise(m, 7, 3));
|
|||
|
|
expect(await uneEntree(m, r)).toBe('Déplacé Iris Ombrelle de la table 4 à la table 7, tour 2');
|
|||
|
|
r += 1;
|
|||
|
|
// Théo Pervenche (2), à la table 1 au tour 2, prend la place d'Ondine
|
|||
|
|
// Lacasse (3), à la table 2, et l'inverse.
|
|||
|
|
await glisser(pointChaise(m, 1, 1), pointChaise(m, 2, 1));
|
|||
|
|
expect(await uneEntree(m, r)).toMatch(/^Échangé Théo Pervenche .* et Ondine Lacasse .*, tour 2$/);
|
|||
|
|
r += 1;
|
|||
|
|
await glisser(pointChaise(m, 3, 1), pointSol(m));
|
|||
|
|
expect(await uneEntree(m, r)).toMatch(/^Retiré Grisaille de la table 3, tour 2$/);
|
|||
|
|
|
|||
|
|
const apres = charge(m);
|
|||
|
|
expect(apres.propositions).toEqual(origine.propositions);
|
|||
|
|
expect(apres.reservations).toEqual(origine.reservations);
|
|||
|
|
expect(apres.retenu.placement[0]).toEqual(origine.retenu.placement[0]);
|
|||
|
|
expect(apres.retenu.placement[2]).toEqual(origine.retenu.placement[2]);
|
|||
|
|
expect(apres.retenu.placement[1]).not.toEqual(origine.retenu.placement[1]);
|
|||
|
|
expect(etatPlaces(apres, { tour: 2 }).reserve).toContain(4);
|
|||
|
|
for (const tour of [1, 3]) expect(etatPlaces(apres, { tour }).reserve, `tour ${tour}`).not.toContain(4);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('le même geste, deux états : avant la retenue une réservation, sur le retenu le retenu ; une proposition montrée sur le retenu refuse sur la place', async () => {
|
|||
|
|
// Avant la retenue, la proposition 1 montrée : Grisaille (4), non réservée,
|
|||
|
|
// vers le siège 2, libre, de la table 8 au tour 1.
|
|||
|
|
const avantRetenue = await ouvrir(huitAvantRetenue());
|
|||
|
|
avantRetenue.ui.montrerProposition(1);
|
|||
|
|
flushSync();
|
|||
|
|
await images(2);
|
|||
|
|
const origine = structuredClone(charge(avantRetenue));
|
|||
|
|
const r = fichiers(avantRetenue).revision;
|
|||
|
|
await presserEtBouger(pointChaise(avantRetenue, 2, 1), pointChaise(avantRetenue, 8, 2));
|
|||
|
|
expect(fantomeMontre(avantRetenue)).toBe(true);
|
|||
|
|
const libelleReservation = texteDe(fantomeEl(avantRetenue).querySelector('[data-partie="etat"]'));
|
|||
|
|
expect(libelleReservation).toBe('pose une réservation');
|
|||
|
|
expect(texteDe(fantomeEl(avantRetenue).querySelector('[data-partie="nom"]'))).toBe('Grisaille');
|
|||
|
|
const vers = pointChaise(avantRetenue, 8, 2);
|
|||
|
|
await pointer('relacher', vers.x, vers.y);
|
|||
|
|
await uneEntree(avantRetenue, r);
|
|||
|
|
expect(charge(avantRetenue).reservations).toEqual([{ participant: 4, table: 8, siege: 2, portee: 'tour', tour: 1 }]);
|
|||
|
|
expect(charge(avantRetenue).propositions).toEqual(origine.propositions);
|
|||
|
|
|
|||
|
|
// Sur le retenu, le même glissement modifie le retenu.
|
|||
|
|
demonter(avantRetenue);
|
|||
|
|
const retenue = await ouvrir(huitRetenue());
|
|||
|
|
const origineRetenue = structuredClone(charge(retenue));
|
|||
|
|
const rr = fichiers(retenue).revision;
|
|||
|
|
await presserEtBouger(pointChaise(retenue, 2, 1), pointChaise(retenue, 8, 2));
|
|||
|
|
const libelleRetenu = texteDe(fantomeEl(retenue).querySelector('[data-partie="etat"]'));
|
|||
|
|
expect(libelleRetenu).toBe('déplace dans le placement retenu');
|
|||
|
|
expect(libelleRetenu).not.toBe(libelleReservation);
|
|||
|
|
const versRetenu = pointChaise(retenue, 8, 2);
|
|||
|
|
await pointer('relacher', versRetenu.x, versRetenu.y);
|
|||
|
|
await uneEntree(retenue, rr);
|
|||
|
|
expect(charge(retenue).reservations).toEqual(origineRetenue.reservations);
|
|||
|
|
expect(charge(retenue).propositions).toEqual(origineRetenue.propositions);
|
|||
|
|
expect(charge(retenue).retenu.placement[0]).not.toEqual(origineRetenue.retenu.placement[0]);
|
|||
|
|
|
|||
|
|
// Le retenu, la proposition 1 montrée : la personne revient, le refus est
|
|||
|
|
// sur la place, et son remède montre le retenu.
|
|||
|
|
retenue.ui.montrerProposition(1);
|
|||
|
|
flushSync();
|
|||
|
|
await images(2);
|
|||
|
|
const avant = fichiers(retenue);
|
|||
|
|
await glisser(pointChaise(retenue, 3, 1), pointChaise(retenue, 7, 3));
|
|||
|
|
const place = dans(retenue, '[data-siege="7:3"]');
|
|||
|
|
await attendre(() => expect(decrit(place)).not.toBeNull());
|
|||
|
|
const refus = decrit(place).querySelector('[data-refus="PROPOSITION_MONTREE"]');
|
|||
|
|
expect(refus).not.toBeNull();
|
|||
|
|
const remede = refus.querySelector('[data-commande="afficherTour"]');
|
|||
|
|
expect(remede).not.toBeNull();
|
|||
|
|
await rienEcrit(retenue, avant);
|
|||
|
|
expect(fantomeMontre(retenue)).toBe(false);
|
|||
|
|
remede.click();
|
|||
|
|
flushSync();
|
|||
|
|
expect(retenue.ui.etat().proposition).toBeNull();
|
|||
|
|
await rienEcrit(retenue, avant);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// --- Glisser une table (§ 7.1, § 9) ---------------------------------------------
|
|||
|
|
|
|||
|
|
describe('glisser une table (§ 7.1, § 9)', () => {
|
|||
|
|
test('(40, −20) px à k = 2 ajoutent (20, −10) cm à sa position, une entrée ; sur un plan bloqué, la table revient, un refus nomme « Débloquer », rien d’écrit', async () => {
|
|||
|
|
const m = await ouvrir(troisDeHuit());
|
|||
|
|
const cadre = cadreDe(m);
|
|||
|
|
await avecVue(m, { k: 2, tx: cadre.largeur / 2 - 2 * 300, ty: cadre.hauteur / 2 });
|
|||
|
|
const r = fichiers(m).revision;
|
|||
|
|
const position = { ...charge(m).tables[1].position };
|
|||
|
|
const depart = pointTable(m, 2);
|
|||
|
|
await glisser(depart, decale(depart, 40, -20));
|
|||
|
|
await uneEntree(m, r);
|
|||
|
|
expect(charge(m).tables[1].position).toEqual({ x: position.x + 20, y: position.y - 10 });
|
|||
|
|
|
|||
|
|
await m.seance.executer('changerEtat', { etat: 'bloque' });
|
|||
|
|
flushSync();
|
|||
|
|
const avant = fichiers(m);
|
|||
|
|
const ici = pointTable(m, 2);
|
|||
|
|
await glisser(ici, decale(ici, 40, -20));
|
|||
|
|
const table = tableEl(m, 2);
|
|||
|
|
await attendre(() => expect(decrit(table)).not.toBeNull());
|
|||
|
|
const refus = decrit(table).querySelector('[data-refus="LECTURE_SEULE"]');
|
|||
|
|
expect(refus).not.toBeNull();
|
|||
|
|
expect(texteDe(refus.querySelector('[data-commande="debloquer"]'))).toBe('Débloquer');
|
|||
|
|
expect(table.getAttribute('transform')).toBeNull();
|
|||
|
|
await rienEcrit(m, avant);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('pendant l’écriture d’un déplacement, un second appui sur la table ne fait rien : une seule commande, la table au premier écart, aucun refus ; la vue, elle, se déplace', async () => {
|
|||
|
|
const m = await ouvrir(huitRetenue());
|
|||
|
|
const commandes = releverCommandes(m);
|
|||
|
|
// L'écriture de l'état reste retenue jusqu'à liberer() : la séance ne
|
|||
|
|
// publie la charge nouvelle qu'une fois l'écriture faite.
|
|||
|
|
const ecrireAtomique = m.fs.ecrireAtomique;
|
|||
|
|
let liberer = null;
|
|||
|
|
const retenue = new Promise((resoudre) => {
|
|||
|
|
liberer = resoudre;
|
|||
|
|
});
|
|||
|
|
m.fs.ecrireAtomique = async (...parametres) => {
|
|||
|
|
await retenue;
|
|||
|
|
return Reflect.apply(ecrireAtomique, m.fs, parametres);
|
|||
|
|
};
|
|||
|
|
nettoyages.push(() => {
|
|||
|
|
m.fs.ecrireAtomique = ecrireAtomique;
|
|||
|
|
});
|
|||
|
|
const r = fichiers(m).revision;
|
|||
|
|
const position = { ...charge(m).tables[0].position };
|
|||
|
|
const depart = pointTable(m, 1);
|
|||
|
|
const ici = decale(depart, 40, 0);
|
|||
|
|
await glisser(depart, ici);
|
|||
|
|
await attendre(() => expect(commandes).toHaveLength(1));
|
|||
|
|
const voulue = commandes[0].args.position;
|
|||
|
|
expect(voulue).not.toEqual(position);
|
|||
|
|
const deplacee = tableEl(m, 1).getAttribute('transform');
|
|||
|
|
expect(deplacee).not.toBeNull();
|
|||
|
|
|
|||
|
|
await glisser(ici, decale(ici, 40, 0));
|
|||
|
|
await images(3);
|
|||
|
|
expect(commandes).toHaveLength(1);
|
|||
|
|
expect(tableEl(m, 1).getAttribute('transform')).toBe(deplacee);
|
|||
|
|
const vueAvant = groupe(m).getAttribute('transform');
|
|||
|
|
const sol = pointSol(m);
|
|||
|
|
await glisser(sol, decale(sol, 30, 20));
|
|||
|
|
await attendre(() => expect(groupe(m).getAttribute('transform')).not.toBe(vueAvant));
|
|||
|
|
|
|||
|
|
liberer();
|
|||
|
|
await uneEntree(m, r);
|
|||
|
|
await images(4);
|
|||
|
|
expect(commandes).toHaveLength(1);
|
|||
|
|
expect(charge(m).tables[0].position).toEqual(voulue);
|
|||
|
|
await attendre(() => expect(tableEl(m, 1).getAttribute('transform')).toBeNull());
|
|||
|
|
expect(decrit(tableEl(m, 1))).toBeNull();
|
|||
|
|
expect(m.racine.querySelector('[data-refus]')).toBeNull();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// --- La poignée (§ 7.1, § 7.7, § 6.1, § 6.2) ----------------------------------------
|
|||
|
|
|
|||
|
|
describe('la poignée (§ 7.1, § 7.7, § 6.2)', () => {
|
|||
|
|
// Le décalage écran, sur chaque axe, d'un écart radial de n places.
|
|||
|
|
const pasDe = (m, n) => ((n * 60) / (2 * Math.PI)) * vueLue(m).k / Math.SQRT2;
|
|||
|
|
|
|||
|
|
test('une table de 8 tirée dehors puis dedans : des nombres entiers successifs de chaises, le rayon rendu à l’échelle, rien d’écrit avant le relâcher, puis une entrée', async () => {
|
|||
|
|
const m = await ouvrir(troisDeHuit());
|
|||
|
|
const k = vueLue(m).k;
|
|||
|
|
const avant = fichiers(m);
|
|||
|
|
const depart = pointPoignee(m, 1);
|
|||
|
|
await pointer('presser', depart.x, depart.y);
|
|||
|
|
const vus = [];
|
|||
|
|
const plateau = () => dans(m, '[data-table="1"] [data-marque="contourTable"], [data-table="1"] [data-marque="contourConflit"]');
|
|||
|
|
const lire = () => {
|
|||
|
|
flushSync();
|
|||
|
|
const n = chaisesDe(m, 1).length;
|
|||
|
|
if (vus.at(-1) !== n) vus.push(n);
|
|||
|
|
expect(plateau().getBoundingClientRect().width / 2).toBeCloseTo((diametre(n) / 2) * k, 0);
|
|||
|
|
};
|
|||
|
|
for (const n of [0.4, 1, 1.6, 2, 2.6, 3, 2.4, 2, 1.4, 1, 0.5, 0, -0.6]) {
|
|||
|
|
await pointer('bouger', depart.x + pasDe(m, n), depart.y + pasDe(m, n));
|
|||
|
|
await images(1);
|
|||
|
|
lire();
|
|||
|
|
}
|
|||
|
|
expect(fichiers(m)).toEqual(avant);
|
|||
|
|
expect(vus.length).toBeGreaterThan(3);
|
|||
|
|
for (let i = 1; i < vus.length; i += 1) expect(Math.abs(vus[i] - vus[i - 1]), `${vus}`).toBe(1);
|
|||
|
|
expect(Math.max(...vus)).toBe(11);
|
|||
|
|
expect(vus.at(-1)).toBe(7);
|
|||
|
|
await pointer('relacher', depart.x + pasDe(m, -0.6), depart.y + pasDe(m, -0.6));
|
|||
|
|
expect(await uneEntree(m, avant.revision)).toBe('Table 1 : 7 places');
|
|||
|
|
expect(charge(m).tables[0].sieges).toBe(7);
|
|||
|
|
expect(Number.isInteger(charge(m).tables[0].sieges)).toBe(true);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('retenu, sept personnes à la table 3 au seul tour 3 : tirée vers 5, elle s’arrête à 7, et le message sur la table porte « 7 » et « tour 3 »', async () => {
|
|||
|
|
const m = await ouvrir(troisDeHuit());
|
|||
|
|
const depart = pointPoignee(m, 3);
|
|||
|
|
await presserEtBouger(depart, decale(depart, pasDe(m, -3), pasDe(m, -3)));
|
|||
|
|
expect(chaisesDe(m, 3).length).toBe(7);
|
|||
|
|
const table = tableEl(m, 3);
|
|||
|
|
await attendre(() => expect(decrit(table)).not.toBeNull());
|
|||
|
|
const refus = decrit(table).querySelector('[data-refus="SOUS_OCCUPES"]');
|
|||
|
|
expect(texteDe(refus)).toContain('7');
|
|||
|
|
expect(texteDe(refus)).toContain('tour 3');
|
|||
|
|
expect(recoupe(decrit(table), table)).toBe(true);
|
|||
|
|
const r = fichiers(m).revision;
|
|||
|
|
await pointer('relacher', depart.x + pasDe(m, -3), depart.y + pasDe(m, -3));
|
|||
|
|
expect(await uneEntree(m, r)).toBe('Table 3 : 7 places');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('sièges attribués, un titre « animateur » au siège 8 et une réservation au siège 7 : tirée vers 6, elle s’arrête à 8 ; le message liste le siège 8 et son titre ; relâchée à 8, aucune entrée', async () => {
|
|||
|
|
let base = chargeDe({ id: 'evt-attribues', places: [null, null], personnes: 4, attribuer: true });
|
|||
|
|
base = executer(base, 'poserTitre', { table: 1, siege: 8, libelle: 'animateur' });
|
|||
|
|
base = executer(base, 'reserver', { participant: 1, table: 1, siege: 7, portee: 'tous', tour: null });
|
|||
|
|
const m = await ouvrir(base);
|
|||
|
|
const avant = fichiers(m);
|
|||
|
|
const depart = pointPoignee(m, 1);
|
|||
|
|
const vers = decale(depart, pasDe(m, -2), pasDe(m, -2));
|
|||
|
|
await presserEtBouger(depart, vers);
|
|||
|
|
expect(chaisesDe(m, 1).length).toBe(8);
|
|||
|
|
const table = tableEl(m, 1);
|
|||
|
|
await attendre(() => expect(decrit(table)).not.toBeNull());
|
|||
|
|
const refus = decrit(table).querySelector('[data-refus="SIEGES_RETIRES"]');
|
|||
|
|
expect(texteDe(refus)).toContain('siège 8');
|
|||
|
|
expect(texteDe(refus)).toContain('animateur');
|
|||
|
|
await pointer('relacher', vers.x, vers.y);
|
|||
|
|
await rienEcrit(m, avant);
|
|||
|
|
expect(chaisesDe(m, 1).length).toBe(8);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('surcharge par la poignée : une table qui suit un défaut de 8, passée à 9 puis ramenée à 8, se dit surchargée, jusqu’au réalignement', async () => {
|
|||
|
|
const m = await ouvrir(chargeDe({ id: 'evt-surcharge', places: [null, null], personnes: 4 }));
|
|||
|
|
const nom = (id) => tableEl(m, id).getAttribute('aria-label');
|
|||
|
|
expect(nom(1)).toBe(nom(2).replace('Table 2', 'Table 1'));
|
|||
|
|
let r = fichiers(m).revision;
|
|||
|
|
for (const n of [1, -1]) {
|
|||
|
|
const depart = pointPoignee(m, 1);
|
|||
|
|
await glisser(depart, decale(depart, pasDe(m, n), pasDe(m, n)));
|
|||
|
|
await uneEntree(m, r);
|
|||
|
|
r += 1;
|
|||
|
|
}
|
|||
|
|
expect(charge(m).tables[0].sieges).toBe(8);
|
|||
|
|
await attendre(() => expect(nom(1)).toContain('surchargée'));
|
|||
|
|
expect(nom(2)).not.toContain('surchargée');
|
|||
|
|
expect(chaisesDe(m, 1).length).toBe(chaisesDe(m, 2).length);
|
|||
|
|
await m.seance.executer('realignerTable', { id: 1 });
|
|||
|
|
await attendre(() => expect(nom(1)).not.toContain('surchargée'));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('lisibilité : au-delà de SEUIL_SIEGES_LISIBLES, l’avertissement s’affiche sur la table, sans arrêter le nombre', async () => {
|
|||
|
|
const m = await ouvrir(chargeDe({ id: 'evt-lisible', places: [null, null], personnes: 4 }));
|
|||
|
|
const seuil = constante('SEUIL_SIEGES_LISIBLES');
|
|||
|
|
const depart = pointPoignee(m, 1);
|
|||
|
|
const n = seuil + 1 - 8;
|
|||
|
|
await presserEtBouger(depart, decale(depart, pasDe(m, n), pasDe(m, n)), { pas: 10 });
|
|||
|
|
expect(chaisesDe(m, 1).length).toBe(seuil + 1);
|
|||
|
|
const table = tableEl(m, 1);
|
|||
|
|
await attendre(() => expect(decrit(table)).not.toBeNull());
|
|||
|
|
const avertissement = decrit(table).querySelector('[data-avertissement="AU_DELA_LISIBLE"]');
|
|||
|
|
expect(avertissement).not.toBeNull();
|
|||
|
|
expect(avertissement.checkVisibility()).toBe(true);
|
|||
|
|
const r = fichiers(m).revision;
|
|||
|
|
await pointer('relacher', depart.x + pasDe(m, n), depart.y + pasDe(m, n));
|
|||
|
|
expect(await uneEntree(m, r)).toBe(`Table 1 : ${seuil + 1} places`);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// --- Un clic n'est pas un glisser ; la lecture ; l'inactivité (§ 8.4) ---------------
|
|||
|
|
|
|||
|
|
describe('un clic n’est pas un glisser, la lecture, l’inactivité (§ 8.4)', () => {
|
|||
|
|
test('appui, mouvement sous le seuil, relâcher → aucune entrée ; au-delà → une entrée ; Échap pendant le glissement → objet remis, aucune entrée', async () => {
|
|||
|
|
expect(SEUIL).toBeGreaterThan(0);
|
|||
|
|
const m = await ouvrir(huitRetenue());
|
|||
|
|
const avant = fichiers(m);
|
|||
|
|
const depart = pointTable(m, 5);
|
|||
|
|
await glisser(depart, decale(depart, SEUIL - 1, 0), { pas: 2 });
|
|||
|
|
await rienEcrit(m, avant);
|
|||
|
|
expect(m.ui.etat().selection).toEqual({ table: 5 });
|
|||
|
|
await glisser(depart, decale(depart, SEUIL + 6, 0), { pas: 2 });
|
|||
|
|
await uneEntree(m, avant.revision);
|
|||
|
|
const apres = fichiers(m);
|
|||
|
|
const ici = pointTable(m, 5);
|
|||
|
|
await presserEtBouger(ici, decale(ici, 40, 0));
|
|||
|
|
await touche('Escape');
|
|||
|
|
flushSync();
|
|||
|
|
expect(tableEl(m, 5).getAttribute('transform')).toBeNull();
|
|||
|
|
await pointer('relacher', ici.x + 40, ici.y);
|
|||
|
|
await rienEcrit(m, apres);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('en lecture : glisser une personne, une table, tirer une poignée → charge inchangée, rien d’écrit, objets revenus ; le zoom et le changement de tour opèrent', async () => {
|
|||
|
|
const m = await ouvrir(troisDeHuit(), { mode: 'lecture' });
|
|||
|
|
const texteCharge = serialiserCharge(charge(m));
|
|||
|
|
const avant = fichiers(m);
|
|||
|
|
const ecritures = m.fs.compteurs.ecritures;
|
|||
|
|
await glisser(pointChaise(m, 1, 1), pointChaise(m, 3, 8));
|
|||
|
|
await glisser(pointTable(m, 2), decale(pointTable(m, 2), 50, 50));
|
|||
|
|
const poignee = pointPoignee(m, 1);
|
|||
|
|
await glisser(poignee, decale(poignee, pasDeLecture(m), pasDeLecture(m)));
|
|||
|
|
await images(4);
|
|||
|
|
expect(serialiserCharge(charge(m))).toBe(texteCharge);
|
|||
|
|
await rienEcrit(m, avant);
|
|||
|
|
expect(m.fs.compteurs.ecritures).toBe(ecritures);
|
|||
|
|
expect(tableEl(m, 2).getAttribute('transform')).toBeNull();
|
|||
|
|
expect(chaisesDe(m, 1).length).toBe(8);
|
|||
|
|
expect(fantomeMontre(m)).toBe(false);
|
|||
|
|
const transform = groupe(m).getAttribute('transform');
|
|||
|
|
dans(m, '[data-commande="zoomer"]').click();
|
|||
|
|
flushSync();
|
|||
|
|
expect(groupe(m).getAttribute('transform')).not.toBe(transform);
|
|||
|
|
const select = dans(m, '[data-champ="tour"]');
|
|||
|
|
select.value = '2';
|
|||
|
|
select.dispatchEvent(new Event('change', { bubbles: true }));
|
|||
|
|
flushSync();
|
|||
|
|
expect(m.ui.etat().tour).toBe(2);
|
|||
|
|
expect(m.seance.etat().mode).toBe('lecture');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('inactivité : le délai dépassé pendant un glissement laisse en écriture ; après le relâcher et le délai, lecture ; un déplacement de vue relance le compte ; « Revenir en lecture » au clavier ramène la table', async () => {
|
|||
|
|
const m = await ouvrir(huitRetenue());
|
|||
|
|
const depart = pointTable(m, 6);
|
|||
|
|
await presserEtBouger(depart, decale(depart, 30, 20));
|
|||
|
|
m.horloge.avancer(INACTIVITE_MS + 1);
|
|||
|
|
await images(2);
|
|||
|
|
expect(m.seance.etat().mode).toBe('ecriture');
|
|||
|
|
expect(tableEl(m, 6).getAttribute('transform')).not.toBeNull();
|
|||
|
|
const r = fichiers(m).revision;
|
|||
|
|
await pointer('bouger', depart.x + 40, depart.y + 20);
|
|||
|
|
await pointer('relacher', depart.x + 40, depart.y + 20);
|
|||
|
|
await uneEntree(m, r);
|
|||
|
|
m.horloge.avancer(INACTIVITE_MS + 1);
|
|||
|
|
await attendre(() => expect(m.seance.etat().mode).toBe('lecture'));
|
|||
|
|
|
|||
|
|
await m.seance.passerEnEcriture();
|
|||
|
|
flushSync();
|
|||
|
|
m.horloge.avancer(INACTIVITE_MS - 1000);
|
|||
|
|
const sol = pointSol(m);
|
|||
|
|
await glisser(sol, decale(sol, 30, 30));
|
|||
|
|
await images(2);
|
|||
|
|
m.horloge.avancer(INACTIVITE_MS - 1);
|
|||
|
|
await images(2);
|
|||
|
|
expect(m.seance.etat().mode).toBe('ecriture');
|
|||
|
|
|
|||
|
|
const avant = fichiers(m);
|
|||
|
|
const ici = pointTable(m, 6);
|
|||
|
|
await presserEtBouger(ici, decale(ici, 50, 0));
|
|||
|
|
expect(tableEl(m, 6).getAttribute('transform')).not.toBeNull();
|
|||
|
|
const lecture = dans(m, '[data-commande="lecture"]');
|
|||
|
|
lecture.focus();
|
|||
|
|
expect(document.activeElement).toBe(lecture);
|
|||
|
|
await touche('Enter');
|
|||
|
|
await attendre(() => expect(m.seance.etat().mode).toBe('lecture'));
|
|||
|
|
expect(tableEl(m, 6).getAttribute('transform')).toBeNull();
|
|||
|
|
await pointer('relacher', ici.x + 50, ici.y);
|
|||
|
|
await images(3);
|
|||
|
|
expect(entreesApres(m, avant.revision)).toEqual([]);
|
|||
|
|
expect(fichiers(m).etat).toBe(avant.etat);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// L'écart d'une poignée tirée de trois places, au zoom relu.
|
|||
|
|
const pasDeLecture = (m) => ((3 * 60) / (2 * Math.PI)) * vueLue(m).k / Math.SQRT2;
|
|||
|
|
|
|||
|
|
// --- Le dépôt sur une table pleine (§ 5.9, § 7.3) -----------------------------------
|
|||
|
|
|
|||
|
|
describe('le dépôt sur une table pleine (§ 5.9, § 7.3)', () => {
|
|||
|
|
test('k = 0,39 : la ronde de 3 en régime sièges, les tables de 8 en régime table ; une personne déposée sur une table de 8 pleine revient, un refus rattaché à la table porte sa cause et son numéro, rien d’écrit', async () => {
|
|||
|
|
const base = chargeDe({
|
|||
|
|
id: 'evt-pleine',
|
|||
|
|
places: [3, null, null],
|
|||
|
|
ecart: 1200,
|
|||
|
|
personnes: 16,
|
|||
|
|
nombreTours: 2,
|
|||
|
|
tours: [
|
|||
|
|
[[1, 2, 3], suite(4, 11), suite(12, 16)],
|
|||
|
|
[[4, 5, 6], [1, 2, 3, 12, 13, 14, 15, 16], suite(7, 11)],
|
|||
|
|
],
|
|||
|
|
});
|
|||
|
|
const m = await ouvrir(base);
|
|||
|
|
const cadre = cadreDe(m);
|
|||
|
|
const kA = kAjuste(englobantDuTour(etatMontre(m)), cadre);
|
|||
|
|
expect(0.39).toBeGreaterThanOrEqual(kA / 2);
|
|||
|
|
const englobant = englobantDuTour(etatMontre(m));
|
|||
|
|
const k = 0.39;
|
|||
|
|
await avecVue(m, {
|
|||
|
|
k,
|
|||
|
|
tx: cadre.largeur / 2 - (k * (englobant.minX + englobant.maxX)) / 2,
|
|||
|
|
ty: cadre.hauteur / 2 - (k * (englobant.minY + englobant.maxY)) / 2,
|
|||
|
|
});
|
|||
|
|
expect(chaisesDe(m, 1).length).toBe(3);
|
|||
|
|
expect(chaisesDe(m, 2).length).toBe(0);
|
|||
|
|
const avant = fichiers(m);
|
|||
|
|
await glisser(pointChaise(m, 1, 1), pointTable(m, 2));
|
|||
|
|
const table = tableEl(m, 2);
|
|||
|
|
await attendre(() => expect(decrit(table)).not.toBeNull());
|
|||
|
|
const refus = decrit(table);
|
|||
|
|
expect(refus.querySelector('[data-refus="TABLE_PLEINE"]')).not.toBeNull();
|
|||
|
|
expect(texteDe(refus)).toContain('Table 2');
|
|||
|
|
expect(texteDe(refus)).toContain('pleine');
|
|||
|
|
expect(recoupe(refus, table)).toBe(true);
|
|||
|
|
expect(fantomeMontre(m)).toBe(false);
|
|||
|
|
await rienEcrit(m, avant);
|
|||
|
|
});
|
|||
|
|
});
|