[FIX] event: page never scrolls, broken-support remedies always shown
At 1366 x 700 an open event that shows warnings no longer scrolls the page: the warnings scroll within their zone and the plan keeps at least 75 % of its height. A broken support never shrinks, so its three remedies stay in view, the only way to write the event elsewhere (spec 8.6). Tests now prove the working-folder path and duplicate nodes are visible, the guard of « Remplacer » in import, and which header a refused re-alignment shows. Checked: 2692 node, 499 browser tests, from the index alone. --- FR --- [FIX] événement : sans défilement, remèdes du support rompu visibles À 1366 x 700, un événement ouvert qui montre des avertissements ne fait plus défiler la page : les avertissements défilent dans leur zone et le plan garde au moins 75 % de sa hauteur. Un support rompu ne rétrécit jamais : ses trois remèdes, seul moyen d'écrire l'événement ailleurs (§ 8.6), restent visibles. Les épreuves prouvent que le chemin et les doublons du dossier sont visibles, la garde de « Remplacer » à l'import, et l'en-tête que montre un réalignement refusé. Vérifié : 2692 node, 499. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
a2812cddb9
commit
8186b06713
8 changed files with 245 additions and 18 deletions
|
|
@ -460,7 +460,11 @@ describe('signalements et récupération (§ 8.1, § 8.8)', () => {
|
||||||
[b, 'Merles A'],
|
[b, 'Merles A'],
|
||||||
]) {
|
]) {
|
||||||
expect(element.textContent).toContain(t('accueil.signalement.doublon', { autres: 1 }));
|
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'));
|
const noeuds = noeudsDeTexte(element).filter((texte) => texte.includes('Merles'));
|
||||||
expect(noeuds).toEqual([autre]);
|
expect(noeuds).toEqual([autre]);
|
||||||
}
|
}
|
||||||
|
|
@ -678,7 +682,11 @@ describe('le dossier de travail (§ 8.5, § 8.6)', () => {
|
||||||
const cheminAutre = `${SOUS_APPDATA}\\data`;
|
const cheminAutre = `${SOUS_APPDATA}\\data`;
|
||||||
expect(proposition.textContent).toContain(t('accueil.autre.proposition', { evenements: 2 }));
|
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.
|
// 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(noeudsDeTexte(proposition).filter((texte) => texte.includes('\\'))).toEqual([cheminAutre]);
|
||||||
expect(proposition.textContent).toContain(t('accueil.autre.originaux'));
|
expect(proposition.textContent).toContain(t('accueil.autre.originaux'));
|
||||||
// En tête : avant la création, les démonstrations et toute ligne.
|
// 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(signalement(base).textContent).toContain(t('accueil.signalement.doublon', { autres: 1 }));
|
||||||
expect(noeudsDeTexte(signalement(base)).filter((texte) => texte.includes('Gala'))).toEqual([autre]);
|
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--espace-2);
|
gap: var(--espace-2);
|
||||||
margin-bottom: var(--espace-3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.avertissement {
|
.avertissement {
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,16 @@
|
||||||
// à droite, la place fixe, le libellé de mode, puis « Défaire » et
|
// à 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
|
// « 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
|
// 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
|
// les annonces, annonces, quand il y en a — support rompu et
|
||||||
// ne la précède dans l'écran de l'événement, si bien qu'un refus posé dans
|
// avertissements —, puis la surface, children. La barre ouvre le cadre :
|
||||||
// la tête, ou un nom qui s'étend sur plusieurs lignes, allonge la tête
|
// rien ne la précède dans l'écran de l'événement, si bien qu'un refus posé
|
||||||
// sans déplacer la place fixe (§ 8.4).
|
// 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) :
|
// La place fixe porte la commande que rend commandeFixe (modele.js) :
|
||||||
// « Modifier », « Débloquer » — confirmé en place, par la Commande d'I2 —,
|
// « Modifier », « Débloquer » — confirmé en place, par la Commande d'I2 —,
|
||||||
|
|
@ -29,7 +35,7 @@
|
||||||
import Refus from '../primitives/Refus.svelte';
|
import Refus from '../primitives/Refus.svelte';
|
||||||
import { cadreDeMode, commandeFixe, repriseOfferte } from './modele.js';
|
import { cadreDeMode, commandeFixe, repriseOfferte } from './modele.js';
|
||||||
|
|
||||||
let { children, tete = null } = $props();
|
let { children, tete = null, annonces = null } = $props();
|
||||||
|
|
||||||
const t = getContext('t');
|
const t = getContext('t');
|
||||||
const seance = getContext('seance');
|
const seance = getContext('seance');
|
||||||
|
|
@ -103,6 +109,11 @@
|
||||||
{@render tete()}
|
{@render tete()}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if annonces !== null}
|
||||||
|
<div class="annonces" class:rompu={$seance.supportRompu !== null} data-annonces>
|
||||||
|
{@render annonces()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="surface">
|
<div class="surface">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -204,8 +215,37 @@
|
||||||
border-bottom: 1px solid var(--couleur-page-filet);
|
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 {
|
.surface {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 75%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: var(--espace-3);
|
padding: var(--espace-3);
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import { FORMAT, analyser, configurationDepuisCharge, etatDeduit } from '../../s
|
||||||
import { lireJournal } from '../../stockage/journal.js';
|
import { lireJournal } from '../../stockage/journal.js';
|
||||||
import { SUFFIXES } from '../../stockage/noms.js';
|
import { SUFFIXES } from '../../stockage/noms.js';
|
||||||
import { VERSION } from '../../version.genere.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 { monterApp } from '../../../test/navigateur/montage.js';
|
||||||
import { visible } from '../../../test/navigateur/rendu.js';
|
import { visible } from '../../../test/navigateur/rendu.js';
|
||||||
import { TABLES_INTERFACE } from '../libelles.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(titre.textContent).toBe(nom);
|
||||||
expect(document.documentElement.scrollWidth, motif).toBeLessThanOrEqual(window.innerWidth);
|
expect(document.documentElement.scrollWidth, motif).toBeLessThanOrEqual(window.innerWidth);
|
||||||
expect(titre.getBoundingClientRect().right, 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(coin(fixe(m, 'modifier')), motif).toEqual(place);
|
||||||
expect(visible(fixe(m, 'modifier')), motif).toBe(true);
|
expect(visible(fixe(m, 'modifier')), motif).toBe(true);
|
||||||
await m.seance.fermer();
|
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)', () => {
|
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 () => {
|
test('cent entrées, la 37ᵉ abîmée : JOURNAL_ECARTEES {n: 64} s’affiche, son texte porte « 64 »', async () => {
|
||||||
const m = await neuf();
|
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 message = (m) => dans(m, '[data-support-rompu]');
|
||||||
const commandeListe = (m) => dans(m, '[data-bandeau] [data-commande="liste"]');
|
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'événement ouvert en écriture, puis le support de racine refuse
|
||||||
// l'écriture d'un geste.
|
// l'écriture d'un geste.
|
||||||
async function rompre(m, racine) {
|
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(fixe(m, 'lecture'))).toBe(true);
|
||||||
expect(visible(dans(m, '[data-refus="liste"]'))).toBe(true);
|
expect(visible(dans(m, '[data-refus="liste"]'))).toBe(true);
|
||||||
expect(m.seance.etat().evenement.base).toBe(base);
|
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();
|
message(m).querySelector('[data-commande="ecrireDocuments"]').click();
|
||||||
await attendre(() => expect(m.seance.etat().supportRompu).toBeNull());
|
await attendre(() => expect(m.seance.etat().supportRompu).toBeNull());
|
||||||
const nouvelle = baseDe(m);
|
const nouvelle = baseDe(m);
|
||||||
|
|
@ -798,7 +930,7 @@ describe('le support rompu (§ 8.6, § 8.2)', () => {
|
||||||
const m = await neuf();
|
const m = await neuf();
|
||||||
await rompre(m, 'documents');
|
await rompre(m, 'documents');
|
||||||
expect(m.seance.etat().supportRompu.ailleurs).toBeNull();
|
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"]');
|
const choisir = message(m).querySelector('[data-commande="choisirDossier"]');
|
||||||
expect(choisir.textContent).toBe(t('communs.commande.choisirDossier'));
|
expect(choisir.textContent).toBe(t('communs.commande.choisirDossier'));
|
||||||
choisir.click();
|
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 () => {
|
test('non publié, « Fermer sans écrire ailleurs », confirmé en place : la liste revient, sans aucune écriture', async () => {
|
||||||
const m = await neuf();
|
const m = await neuf();
|
||||||
await rompre(m, 'documents');
|
await rompre(m, 'documents');
|
||||||
|
issuesVisibles(m, ['choisirDossier', 'abandonner']);
|
||||||
const avant = m.trace.length;
|
const avant = m.trace.length;
|
||||||
const abandonner = () => message(m).querySelector('[data-commande="abandonner"]');
|
const abandonner = () => message(m).querySelector('[data-commande="abandonner"]');
|
||||||
expect(abandonner().textContent).toBe(t('evenement.supportRompu.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 () => {
|
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'] } });
|
const m = await neuf({ systeme: { dossiersChoisis: ['D:\\Secours'] } });
|
||||||
await rompre(m, 'documents');
|
await rompre(m, 'documents');
|
||||||
|
issuesVisibles(m, ['choisirDossier', 'abandonner']);
|
||||||
message(m).querySelector('[data-commande="choisirDossier"]').click();
|
message(m).querySelector('[data-commande="choisirDossier"]').click();
|
||||||
await attendre(() => expect(m.seance.etat().supportRompu).toBeNull());
|
await attendre(() => expect(m.seance.etat().supportRompu).toBeNull());
|
||||||
expect(m.seance.etat().racine).toEqual({ id: 'choisi-1', chemin: 'D:\\Secours' });
|
expect(m.seance.etat().racine).toEqual({ id: 'choisi-1', chemin: 'D:\\Secours' });
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,12 @@
|
||||||
// globale ci-dessous donne pour cela une hauteur à l'ascendance de l'écran
|
// globale ci-dessous donne pour cela une hauteur à l'ascendance de l'écran
|
||||||
// — le conteneur de l'application et son <main> — tant qu'il porte cet
|
// — le conteneur de l'application et son <main> — tant qu'il porte cet
|
||||||
// écran, et à lui seul.
|
// é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 { getContext } from 'svelte';
|
||||||
import Diagnostic from '../diagnostic/Diagnostic.svelte';
|
import Diagnostic from '../diagnostic/Diagnostic.svelte';
|
||||||
import SectionGeneration from '../generation/SectionGeneration.svelte';
|
import SectionGeneration from '../generation/SectionGeneration.svelte';
|
||||||
|
|
@ -30,6 +36,7 @@
|
||||||
import CadreMode from './CadreMode.svelte';
|
import CadreMode from './CadreMode.svelte';
|
||||||
import EnteteEvenement from './EnteteEvenement.svelte';
|
import EnteteEvenement from './EnteteEvenement.svelte';
|
||||||
import SupportRompu from './SupportRompu.svelte';
|
import SupportRompu from './SupportRompu.svelte';
|
||||||
|
import { avertissementsOrdonnes } from './modele.js';
|
||||||
import { SECTIONS, cleDeSection } from './sections.js';
|
import { SECTIONS, cleDeSection } from './sections.js';
|
||||||
|
|
||||||
const t = getContext('t');
|
const t = getContext('t');
|
||||||
|
|
@ -48,6 +55,9 @@
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const Section = $derived(COMPOSANTS.get($ui.section));
|
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);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet tete()}
|
{#snippet tete()}
|
||||||
|
|
@ -65,12 +75,15 @@
|
||||||
</nav>
|
</nav>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet annonces()}
|
||||||
|
{#if $seance.supportRompu !== null}
|
||||||
|
<SupportRompu />
|
||||||
|
{/if}
|
||||||
|
<Avertissements />
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
<div class="evenement" data-ecran="evenement">
|
<div class="evenement" data-ecran="evenement">
|
||||||
<CadreMode tete={$ui.pleinEcran ? null : tete}>
|
<CadreMode tete={$ui.pleinEcran ? null : tete} annonces={annoncer ? annonces : null}>
|
||||||
{#if $seance.supportRompu !== null}
|
|
||||||
<SupportRompu />
|
|
||||||
{/if}
|
|
||||||
<Avertissements />
|
|
||||||
<Section />
|
<Section />
|
||||||
</CadreMode>
|
</CadreMode>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,6 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: var(--espace-2);
|
gap: var(--espace-2);
|
||||||
margin-bottom: var(--espace-3);
|
|
||||||
padding: var(--espace-2) var(--espace-3);
|
padding: var(--espace-2) var(--espace-3);
|
||||||
border-left: 3px solid var(--couleur-refus);
|
border-left: 3px solid var(--couleur-refus);
|
||||||
color: var(--couleur-refus);
|
color: var(--couleur-refus);
|
||||||
|
|
|
||||||
|
|
@ -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(pose.querySelector('[data-commande="modifier"]').textContent).toBe(t('communs.remede.modifier'));
|
||||||
expect(ecritures(e)).toBe(avant);
|
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)', () => {
|
describe('le collage en bloc passe par le même analyseur (§ 10.2, § 14.10)', () => {
|
||||||
|
|
|
||||||
|
|
@ -354,6 +354,12 @@ describe('changer le défaut (§ 6.1, § 6.2, § 19.3 06)', () => {
|
||||||
expect(fenetre()).toBeNull();
|
expect(fenetre()).toBeNull();
|
||||||
const refus = enonce(m).querySelector('[data-refus-realigner]');
|
const refus = enonce(m).querySelector('[data-refus-realigner]');
|
||||||
expect(refus).not.toBeNull();
|
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('Table 2');
|
||||||
expect(refus.textContent).toContain('11 réservations');
|
expect(refus.textContent).toContain('11 réservations');
|
||||||
expect(ecritures(m)).toBe(avant);
|
expect(ecritures(m)).toBe(avant);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue