diff --git a/src/interface/accueil/Accueil.navigateur.test.js b/src/interface/accueil/Accueil.navigateur.test.js
index ffafc6e..909a047 100644
--- a/src/interface/accueil/Accueil.navigateur.test.js
+++ b/src/interface/accueil/Accueil.navigateur.test.js
@@ -460,7 +460,11 @@ describe('signalements et récupération (§ 8.1, § 8.8)', () => {
[b, 'Merles A'],
]) {
expect(element.textContent).toContain(t('accueil.signalement.doublon', { autres: 1 }));
- expect(element.querySelector(`[data-doublon="${CSS.escape(autre)}"]`)?.textContent).toBe(autre);
+ const doublon = element.querySelector(`[data-doublon="${CSS.escape(autre)}"]`);
+ expect(doublon?.textContent).toBe(autre);
+ // La base nommée se lit à l'écran, pas seulement dans l'arbre.
+ expect(visible(doublon), autre).toBe(true);
+ expect(atteint(doublon), autre).toBe(true);
const noeuds = noeudsDeTexte(element).filter((texte) => texte.includes('Merles'));
expect(noeuds).toEqual([autre]);
}
@@ -678,7 +682,11 @@ describe('le dossier de travail (§ 8.5, § 8.6)', () => {
const cheminAutre = `${SOUS_APPDATA}\\data`;
expect(proposition.textContent).toContain(t('accueil.autre.proposition', { evenements: 2 }));
// Le chemin est une donnée : son propre nœud de texte, à part de la phrase.
- expect(dans(m, '[data-autre-chemin]')?.textContent).toBe(cheminAutre);
+ const chemin = dans(m, '[data-autre-chemin]');
+ expect(chemin?.textContent).toBe(cheminAutre);
+ // Le chemin se lit à l'écran, pas seulement dans l'arbre.
+ expect(visible(chemin)).toBe(true);
+ expect(atteint(chemin)).toBe(true);
expect(noeudsDeTexte(proposition).filter((texte) => texte.includes('\\'))).toEqual([cheminAutre]);
expect(proposition.textContent).toContain(t('accueil.autre.originaux'));
// En tête : avant la création, les démonstrations et toute ligne.
@@ -802,6 +810,10 @@ describe('les opérations de fichiers (§ 8.7)', () => {
]) {
expect(signalement(base).textContent).toContain(t('accueil.signalement.doublon', { autres: 1 }));
expect(noeudsDeTexte(signalement(base)).filter((texte) => texte.includes('Gala'))).toEqual([autre]);
+ const doublon = signalement(base).querySelector(`[data-doublon="${CSS.escape(autre)}"]`);
+ expect(doublon?.textContent).toBe(autre);
+ expect(visible(doublon), autre).toBe(true);
+ expect(atteint(doublon), autre).toBe(true);
}
});
diff --git a/src/interface/evenement/Avertissements.svelte b/src/interface/evenement/Avertissements.svelte
index fce3370..72a62ea 100644
--- a/src/interface/evenement/Avertissements.svelte
+++ b/src/interface/evenement/Avertissements.svelte
@@ -74,7 +74,6 @@
display: flex;
flex-direction: column;
gap: var(--espace-2);
- margin-bottom: var(--espace-3);
}
.avertissement {
diff --git a/src/interface/evenement/CadreMode.svelte b/src/interface/evenement/CadreMode.svelte
index d4f6bfa..4ff3533 100644
--- a/src/interface/evenement/CadreMode.svelte
+++ b/src/interface/evenement/CadreMode.svelte
@@ -8,10 +8,16 @@
// à droite, la place fixe, le libellé de mode, puis « Défaire » et
// « Refaire » (§ 8.3). Sous la barre vient la tête, tete, quand elle est
// donnée — l'en-tête de l'événement et la navigation des sections —, puis
- // la surface, children, qui défile seule. La barre ouvre le cadre : rien
- // ne la précède dans l'écran de l'événement, si bien qu'un refus posé dans
- // la tête, ou un nom qui s'étend sur plusieurs lignes, allonge la tête
- // sans déplacer la place fixe (§ 8.4).
+ // les annonces, annonces, quand il y en a — support rompu et
+ // avertissements —, puis la surface, children. La barre ouvre le cadre :
+ // rien ne la précède dans l'écran de l'événement, si bien qu'un refus posé
+ // dans la tête, ou un nom qui s'étend sur plusieurs lignes, allonge la tête
+ // sans déplacer la place fixe (§ 8.4). Tête, annonces et surface défilent
+ // chacune dans sa zone : des avertissements nombreux ne poussent pas la
+ // surface hors du cadre, ni le cadre hors de la fenêtre. La surface porte
+ // le plan, que l'opérateur regarde : elle réclame les trois quarts du
+ // cadre avant que les annonces ne dépassent leur première ligne, sauf
+ // devant un support rompu, dont les issues restent en vue.
//
// La place fixe porte la commande que rend commandeFixe (modele.js) :
// « Modifier », « Débloquer » — confirmé en place, par la Commande d'I2 —,
@@ -29,7 +35,7 @@
import Refus from '../primitives/Refus.svelte';
import { cadreDeMode, commandeFixe, repriseOfferte } from './modele.js';
- let { children, tete = null } = $props();
+ let { children, tete = null, annonces = null } = $props();
const t = getContext('t');
const seance = getContext('seance');
@@ -103,6 +109,11 @@
{@render tete()}
{/if}
+ {#if annonces !== null}
+
+ {@render annonces()}
+
+ {/if}
{@render children()}
@@ -204,8 +215,37 @@
border-bottom: 1px solid var(--couleur-page-filet);
}
+ /* Les annonces ne prennent jamais plus du quart du cadre, et cèdent
+ d'abord leur hauteur à la surface : leur facteur de rétrécissement,
+ mille fois celui de la surface, leur fait absorber presque tout le
+ manque de place, jusqu'à leur hauteur minimale : un avertissement
+ d'une ligne en entier, et le haut du suivant, qui dit que la zone en
+ porte d'autres. Au-delà de leur hauteur, elles défilent dans leur
+ zone. */
+ .annonces {
+ display: flex;
+ flex: 0 1000 auto;
+ flex-direction: column;
+ gap: var(--espace-2);
+ min-height: 3rem;
+ max-height: 25%;
+ overflow-y: auto;
+ padding: var(--espace-2) var(--espace-3);
+ border-bottom: 1px solid var(--couleur-page-filet);
+ }
+
+ /* Le support rompu, en tête des annonces, porte les seules issues de
+ l'opérateur : tant qu'il est là, les annonces ne cèdent rien à la
+ surface, et gardent leur hauteur jusqu'au quart du cadre ; les
+ avertissements qui le suivent défilent sous lui. */
+ .annonces.rompu {
+ flex-shrink: 0;
+ }
+
+ /* La surface part des trois quarts du cadre, s'étend à ce qui reste, et
+ ne rétrécit, après les annonces, que pour la barre et la tête. */
.surface {
- flex: 1 1 auto;
+ flex: 1 1 75%;
min-height: 0;
overflow: auto;
padding: var(--espace-3);
diff --git a/src/interface/evenement/Evenement.navigateur.test.js b/src/interface/evenement/Evenement.navigateur.test.js
index 3be5992..84b594d 100644
--- a/src/interface/evenement/Evenement.navigateur.test.js
+++ b/src/interface/evenement/Evenement.navigateur.test.js
@@ -30,7 +30,7 @@ import { FORMAT, analyser, configurationDepuisCharge, etatDeduit } from '../../s
import { lireJournal } from '../../stockage/journal.js';
import { SUFFIXES } from '../../stockage/noms.js';
import { VERSION } from '../../version.genere.js';
-import { fixerCadre } from '../../../test/navigateur/entrees.js';
+import { fixerCadre, molette } from '../../../test/navigateur/entrees.js';
import { monterApp } from '../../../test/navigateur/montage.js';
import { visible } from '../../../test/navigateur/rendu.js';
import { TABLES_INTERFACE } from '../libelles.js';
@@ -230,6 +230,11 @@ describe('un nom long ou de glyphes larges (point d’attention 3)', () => {
expect(titre.textContent).toBe(nom);
expect(document.documentElement.scrollWidth, motif).toBeLessThanOrEqual(window.innerWidth);
expect(titre.getBoundingClientRect().right, motif).toBeLessThanOrEqual(window.innerWidth);
+ // Ni le titre ni la tête qui le porte ne débordent : la tête défile en
+ // hauteur, et un nom qui ne passerait pas à la ligne s'étendrait en
+ // largeur sous elle sans élargir la page.
+ const tete = titre.closest('header').parentElement;
+ expect([titre.scrollWidth, tete.scrollWidth], motif).toEqual([titre.clientWidth, tete.clientWidth]);
expect(coin(fixe(m, 'modifier')), motif).toEqual(place);
expect(visible(fixe(m, 'modifier')), motif).toBe(true);
await m.seance.fermer();
@@ -578,6 +583,125 @@ describe('les annonces d’ouverture (§ 8.9, § 9)', () => {
});
});
+describe('l’écran tient dans la fenêtre, avertissements compris (§ 8.4)', () => {
+ // Ferme l'événement garni et généré de base, l'abîme, puis le rouvre : une
+ // proposition de longueur fausse, une périmée, et une entrée du journal
+ // abîmée lui donnent trois avertissements à l'ouverture, au moins.
+ async function rouvrirAbime(m, base) {
+ await m.seance.fermer();
+ reecrireEtat(m, base, (charge) => {
+ charge.propositions[0].placement.pop();
+ charge.participants[0].exclu = true;
+ });
+ const chemin = `${base}${SUFFIXES.journal}`;
+ const lignes = m.fs.contenu('documents', chemin).split('\n');
+ const rang = lignes.findIndex((ligne) => ligne.startsWith('{"type":"entree"') && JSON.parse(ligne).revision === 4);
+ expect(rang).toBeGreaterThan(0);
+ lignes[rang] = '{"type":"entree", abîmée';
+ m.fs.deposer('documents', chemin, lignes.join('\n'));
+ await m.seance.ouvrir(base);
+ flushSync();
+ await new Promise((resoudre) => requestAnimationFrame(resoudre));
+ }
+
+ test('à 1 366 × 700, trois avertissements et plus : la page ne défile pas, le plan garde sa hauteur ; les avertissements défilent à la molette, sans rien déplacer', async () => {
+ const m = await neuf();
+ await garnir(m);
+ await generer(m, { retenir: false });
+ const base = baseDe(m);
+ // La hauteur du dessin du plan sans aucune annonce : la mesure de
+ // référence de ce que les avertissements peuvent lui prendre.
+ m.ui.allerA('plan');
+ flushSync();
+ expect(dans(m, '[data-annonces]')).toBeNull();
+ const hauteurSeule = dans(m, '[data-plan]').getBoundingClientRect().height;
+ expect(hauteurSeule).toBeGreaterThan(0);
+ await rouvrirAbime(m, base);
+ expect(m.ui.etat().section).toBe('plan');
+ const montres = tous(m, '[data-avertissement]');
+ expect(montres.length).toBeGreaterThanOrEqual(3);
+ const boite = (element) => element.getBoundingClientRect().toJSON();
+ // La page ne défile pas : le cadre tient dans la fenêtre, et la section
+ // du plan dans le cadre, sans que la surface qui la porte ait à défiler.
+ expect(document.documentElement.scrollHeight).toBeLessThanOrEqual(innerHeight);
+ expect(cadre(m).getBoundingClientRect().bottom).toBeLessThanOrEqual(innerHeight);
+ const section = dans(m, '[data-section]');
+ const porteur = section.parentElement;
+ expect(section.getBoundingClientRect().bottom).toBeLessThanOrEqual(porteur.getBoundingClientRect().bottom);
+ expect(porteur.scrollHeight).toBeLessThanOrEqual(porteur.clientHeight);
+ expect(visible(dans(m, '[data-plan]'))).toBe(true);
+ // Le plan reste la surface de travail : les annonces ne prennent pas
+ // plus du quart de la hauteur de son dessin.
+ expect(dans(m, '[data-plan]').getBoundingClientRect().height).toBeGreaterThanOrEqual(hauteurSeule * 0.75);
+ // Les avertissements débordent leur zone, qui défile seule, à la
+ // molette, comme sous la main de l'opérateur : le dernier, amené dans la
+ // zone, se voit, et ni la page, ni la place fixe, ni la section n'ont
+ // bougé.
+ const zone = montres[0].closest('[data-annonces]');
+ expect(zone).not.toBeNull();
+ for (const montre of montres) expect(zone.contains(montre)).toBe(true);
+ expect(zone.scrollHeight).toBeGreaterThan(zone.clientHeight);
+ expect(['auto', 'scroll']).toContain(getComputedStyle(zone).overflowY);
+ expect(visible(montres[0])).toBe(true);
+ const place = coin(fixe(m, 'modifier'));
+ const avant = boite(section);
+ const surZone = zone.getBoundingClientRect();
+ await molette(surZone.left + surZone.width / 2, surZone.top + surZone.height / 2, zone.scrollHeight);
+ await attendre(() => expect(visible(montres.at(-1))).toBe(true));
+ expect(zone.scrollTop).toBeGreaterThan(0);
+ expect(window.scrollY).toBe(0);
+ expect(coin(fixe(m, 'modifier'))).toEqual(place);
+ expect(boite(section)).toEqual(avant);
+ // Sur chaque section, la page reste sans défilement, et la zone garde
+ // sa boîte.
+ const zoneAvant = boite(zone);
+ for (const nom of SECTIONS) {
+ m.ui.allerA(nom);
+ flushSync();
+ expect(document.documentElement.scrollHeight, nom).toBeLessThanOrEqual(innerHeight);
+ expect(boite(dans(m, '[data-annonces]')), nom).toEqual(zoneAvant);
+ }
+ });
+
+ test('à 1 366 × 700, un support rompu et trois avertissements et plus : chaque issue du support rompu se voit sans défiler, la page ne défile pas, et les avertissements défilent sous lui', async () => {
+ const m = await neuf();
+ await garnir(m);
+ await generer(m, { retenir: false });
+ await rouvrirAbime(m, baseDe(m));
+ expect(tous(m, '[data-avertissement]').length).toBeGreaterThanOrEqual(3);
+ fixe(m, 'modifier').click();
+ await attendre(() => expect(m.seance.etat().mode).toBe('ecriture'));
+ m.fs.pannes.ecriture('documents');
+ await expect(m.seance.executer('ajouterParticipant', { champs: { nom: NOMS[0] } })).rejects.toMatchObject({ code: 'ECRITURE' });
+ flushSync();
+ await new Promise((resoudre) => requestAnimationFrame(resoudre));
+ expect(m.seance.etat().supportRompu).not.toBeNull();
+ const montres = tous(m, '[data-avertissement]');
+ expect(montres.length).toBeGreaterThanOrEqual(3);
+ expect(document.documentElement.scrollHeight).toBeLessThanOrEqual(innerHeight);
+ expect(cadre(m).getBoundingClientRect().bottom).toBeLessThanOrEqual(innerHeight);
+ // Les issues du support rompu, seule sortie de l'opérateur, se voient
+ // sans que rien ne défile.
+ const message = dans(m, '[data-support-rompu]');
+ for (const nom of ['choisirDossier', 'abandonner']) {
+ const issue = message.querySelector(`[data-commande="${nom}"]`);
+ expect(issue, nom).not.toBeNull();
+ expect(visible(issue), nom).toBe(true);
+ }
+ expect(visible(fixe(m, 'lecture'))).toBe(true);
+ expect(visible(dans(m, '[data-plan]'))).toBe(true);
+ // Les avertissements restent dans la zone, qui défile seule : le
+ // dernier, amené à la molette, se voit, et la page n'a pas bougé.
+ const zone = message.closest('[data-annonces]');
+ expect(zone).not.toBeNull();
+ for (const montre of montres) expect(zone.contains(montre)).toBe(true);
+ const surZone = zone.getBoundingClientRect();
+ await molette(surZone.left + surZone.width / 2, surZone.top + surZone.height / 2, zone.scrollHeight);
+ await attendre(() => expect(visible(montres.at(-1))).toBe(true));
+ expect(window.scrollY).toBe(0);
+ });
+});
+
describe('le journal (§ 8.6, § 8.2)', () => {
test('cent entrées, la 37ᵉ abîmée : JOURNAL_ECARTEES {n: 64} s’affiche, son texte porte « 64 »', async () => {
const m = await neuf();
@@ -742,6 +866,14 @@ describe('le support rompu (§ 8.6, § 8.2)', () => {
const message = (m) => dans(m, '[data-support-rompu]');
const commandeListe = (m) => dans(m, '[data-bandeau] [data-commande="liste"]');
+ // Le message offre exactement les issues noms, et chacune se voit au cadre
+ // de l'épreuve : c'est la seule sortie de l'opérateur d'un support rompu.
+ function issuesVisibles(m, noms) {
+ const offertes = [...message(m).querySelectorAll('[data-commande]')].map((issue) => issue.dataset.commande);
+ expect(offertes).toEqual(noms);
+ for (const nom of noms) expect(visible(message(m).querySelector(`[data-commande="${nom}"]`)), nom).toBe(true);
+ }
+
// L'événement ouvert en écriture, puis le support de racine refuse
// l'écriture d'un geste.
async function rompre(m, racine) {
@@ -776,7 +908,7 @@ describe('le support rompu (§ 8.6, § 8.2)', () => {
expect(visible(fixe(m, 'lecture'))).toBe(true);
expect(visible(dans(m, '[data-refus="liste"]'))).toBe(true);
expect(m.seance.etat().evenement.base).toBe(base);
- expect(message(m).querySelector('[data-commande="choisirDossier"]')).not.toBeNull();
+ issuesVisibles(m, ['ecrireDocuments', 'choisirDossier', 'abandonner']);
message(m).querySelector('[data-commande="ecrireDocuments"]').click();
await attendre(() => expect(m.seance.etat().supportRompu).toBeNull());
const nouvelle = baseDe(m);
@@ -798,7 +930,7 @@ describe('le support rompu (§ 8.6, § 8.2)', () => {
const m = await neuf();
await rompre(m, 'documents');
expect(m.seance.etat().supportRompu.ailleurs).toBeNull();
- expect(message(m).querySelector('[data-commande="ecrireDocuments"]')).toBeNull();
+ issuesVisibles(m, ['choisirDossier', 'abandonner']);
const choisir = message(m).querySelector('[data-commande="choisirDossier"]');
expect(choisir.textContent).toBe(t('communs.commande.choisirDossier'));
choisir.click();
@@ -810,6 +942,7 @@ describe('le support rompu (§ 8.6, § 8.2)', () => {
test('non publié, « Fermer sans écrire ailleurs », confirmé en place : la liste revient, sans aucune écriture', async () => {
const m = await neuf();
await rompre(m, 'documents');
+ issuesVisibles(m, ['choisirDossier', 'abandonner']);
const avant = m.trace.length;
const abandonner = () => message(m).querySelector('[data-commande="abandonner"]');
expect(abandonner().textContent).toBe(t('evenement.supportRompu.abandonner'));
@@ -826,6 +959,7 @@ describe('le support rompu (§ 8.6, § 8.2)', () => {
test('non publié, un dossier choisi : état et journal y sont écrits, le geste suivant aussi, et la liste le suit', async () => {
const m = await neuf({ systeme: { dossiersChoisis: ['D:\\Secours'] } });
await rompre(m, 'documents');
+ issuesVisibles(m, ['choisirDossier', 'abandonner']);
message(m).querySelector('[data-commande="choisirDossier"]').click();
await attendre(() => expect(m.seance.etat().supportRompu).toBeNull());
expect(m.seance.etat().racine).toEqual({ id: 'choisi-1', chemin: 'D:\\Secours' });
diff --git a/src/interface/evenement/Evenement.svelte b/src/interface/evenement/Evenement.svelte
index e692746..649e031 100644
--- a/src/interface/evenement/Evenement.svelte
+++ b/src/interface/evenement/Evenement.svelte
@@ -18,6 +18,12 @@
// globale ci-dessous donne pour cela une hauteur à l'ascendance de l'écran
// — le conteneur de l'application et son — tant qu'il porte cet
// écran, et à lui seul.
+ //
+ // Le support rompu et les avertissements se posent dans la zone des
+ // annonces du cadre, qui défile seule, et non au-dessus de la section : la
+ // section garde la hauteur de la surface, plan compris, quel que soit le
+ // nombre des avertissements, et le support rompu, en tête de la zone,
+ // garde ses issues en vue (CadreMode.svelte).
import { getContext } from 'svelte';
import Diagnostic from '../diagnostic/Diagnostic.svelte';
import SectionGeneration from '../generation/SectionGeneration.svelte';
@@ -30,6 +36,7 @@
import CadreMode from './CadreMode.svelte';
import EnteteEvenement from './EnteteEvenement.svelte';
import SupportRompu from './SupportRompu.svelte';
+ import { avertissementsOrdonnes } from './modele.js';
import { SECTIONS, cleDeSection } from './sections.js';
const t = getContext('t');
@@ -48,6 +55,9 @@
]);
const Section = $derived(COMPOSANTS.get($ui.section));
+ // La zone des annonces n'existe que lorsqu'elle a quelque chose à dire :
+ // sans annonce, le cadre ne lui garde ni hauteur ni trait.
+ const annoncer = $derived($seance.supportRompu !== null || avertissementsOrdonnes($seance).length > 0);
{#snippet tete()}
@@ -65,12 +75,15 @@
{/snippet}
+{#snippet annonces()}
+ {#if $seance.supportRompu !== null}
+
+ {/if}
+
+{/snippet}
+
-
- {#if $seance.supportRompu !== null}
-
- {/if}
-
+
diff --git a/src/interface/evenement/SupportRompu.svelte b/src/interface/evenement/SupportRompu.svelte
index 2e1c4b1..14a1ce4 100644
--- a/src/interface/evenement/SupportRompu.svelte
+++ b/src/interface/evenement/SupportRompu.svelte
@@ -52,7 +52,6 @@
flex-direction: column;
align-items: flex-start;
gap: var(--espace-2);
- margin-bottom: var(--espace-3);
padding: var(--espace-2) var(--espace-3);
border-left: 3px solid var(--couleur-refus);
color: var(--couleur-refus);
diff --git a/src/interface/import/Import.navigateur.test.js b/src/interface/import/Import.navigateur.test.js
index 227b41a..884c3e4 100644
--- a/src/interface/import/Import.navigateur.test.js
+++ b/src/interface/import/Import.navigateur.test.js
@@ -731,6 +731,30 @@ describe('importer selon l’état (§ 10.1, § 9)', () => {
expect(pose.querySelector('[data-commande="modifier"]').textContent).toBe(t('communs.remede.modifier'));
expect(ecritures(e)).toBe(avant);
});
+
+ test('sous « Remplacer », bloqué puis en lecture : le refus à côté de la commande, avec son remède, aucune fenêtre, rien d’écrit', async () => {
+ const cas = [
+ ['LECTURE_SEULE', 'debloquer', { prepare: (seance) => seance.executer('changerEtat', { etat: 'bloque' }) }],
+ ['MODE_LECTURE', 'modifier', { ecriture: false }],
+ ];
+ for (const [code, remede, options] of cas) {
+ const e = await ouvrir({ participants: QUATRE(), ...options });
+ expect(e.m.seance.etat().refus?.code, code).toBe(code);
+ const avant = ecritures(e);
+ await choisir(e, 'tardif.csv', enUtf8(FICHIER));
+ choisirMode(e, 'remplacer');
+ commande(e, 'importer').click();
+ await vi.waitFor(() => expect(dans(e, `[data-refus="${code}"]`), code).not.toBeNull());
+ const pose = dans(e, `[data-refus="${code}"]`);
+ expect(commande(e, 'importer').parentElement.contains(pose), code).toBe(true);
+ expect(pose.querySelector(`[data-commande="${remede}"]`)?.textContent, code).toBe(t(`communs.remede.${remede}`));
+ expect(vu(pose), code).toBe(true);
+ expect(dans(e, 'dialog'), code).toBeNull();
+ expect(imports(e), code).toEqual([]);
+ expect(ecritures(e), code).toBe(avant);
+ expect(participantsDe(e).length, code).toBe(4);
+ }
+ });
});
describe('le collage en bloc passe par le même analyseur (§ 10.2, § 14.10)', () => {
diff --git a/src/interface/tables/Tables.navigateur.test.js b/src/interface/tables/Tables.navigateur.test.js
index 3daee75..cf28857 100644
--- a/src/interface/tables/Tables.navigateur.test.js
+++ b/src/interface/tables/Tables.navigateur.test.js
@@ -354,6 +354,12 @@ describe('changer le défaut (§ 6.1, § 6.2, § 19.3 06)', () => {
expect(fenetre()).toBeNull();
const refus = enonce(m).querySelector('[data-refus-realigner]');
expect(refus).not.toBeNull();
+ // L'en-tête est celui du réalignement refusé, qui laisse « Appliquer »
+ // offert, et non celui d'un défaut que rien n'applique.
+ const tete = refus.querySelector('[data-phrase]');
+ expect(tete?.textContent).toBe(t('tables.defaut.realignementRefuse', { avant: 8, apres: 10 }));
+ expect(visible(tete)).toBe(true);
+ expect(refus.textContent).not.toContain(t('tables.defaut.refusee', { avant: 8, apres: 10 }));
expect(refus.textContent).toContain('Table 2');
expect(refus.textContent).toContain('11 réservations');
expect(ecritures(m)).toBe(avant);