diff --git a/src/interface/participants/Formulaire.svelte b/src/interface/participants/Formulaire.svelte index bb387be..4be701c 100644 --- a/src/interface/participants/Formulaire.svelte +++ b/src/interface/participants/Formulaire.svelte @@ -34,6 +34,7 @@ import { cleNormalisee } from '../../application/csv.js'; import { creerGeste } from '../primitives/geste.js'; import Refus from '../primitives/Refus.svelte'; + import { effetDe } from '../raccourcis.js'; import { cleDeColonne, entreeValide, orthographeExistante } from './grille.js'; let { participants } = $props(); @@ -113,7 +114,7 @@ // Entrée dans les notes envoie le formulaire ; Maj+Entrée y garde le // saut de ligne de la zone de texte. function surToucheNotes(evenement) { - if (evenement.key !== 'Enter' || !entreeValide({ maj: evenement.shiftKey, multiligne: true })) return; + if (effetDe('champ', evenement.key) !== 'valider' || !entreeValide({ maj: evenement.shiftKey, multiligne: true })) return; evenement.preventDefault(); formulaire.requestSubmit(); } diff --git a/src/interface/participants/Grille.svelte b/src/interface/participants/Grille.svelte index b22f5cb..5e90c0a 100644 --- a/src/interface/participants/Grille.svelte +++ b/src/interface/participants/Grille.svelte @@ -61,6 +61,7 @@ import Commande from '../primitives/Commande.svelte'; import { creerGeste } from '../primitives/geste.js'; import Refus from '../primitives/Refus.svelte'; + import { effetDe } from '../raccourcis.js'; import { cleDeRaison } from './conflits.js'; import { COLONNES, basculerGroupe, celluleSuivante, cheminDe, cleDeColonne, deplierChemin, entreeValide, lignesVisibles, ligneVoisine } from './grille.js'; @@ -249,25 +250,41 @@ geste.tenir(machine.enCours); } + // Les effets de la grille qui mènent à une autre cellule (celluleSuivante). + const DEPLACEMENTS = new Set(['haut', 'bas', 'gauche', 'droite', 'suivante', 'precedente']); + + // La touche d'un appui telle que RACCOURCIS la nomme dans la grille : + // « Maj+ » devant la touche quand la table donne à la majuscule son effet + // propre (Maj+Tab), la touche seule sinon — Maj+flèche se lit comme la + // flèche. + function toucheDeGrille(evenement) { + const majuscule = `Maj+${evenement.key}`; + return evenement.shiftKey && effetDe('grille', majuscule) !== null ? majuscule : evenement.key; + } + + // Dans une cellule en édition, les effets du contexte 'champ' — valider, + // rendre — passent d'abord ; seuls suivante et precedente de la grille + // s'y ajoutent. async function surToucheEntree(evenement, rang) { - const touche = evenement.key; - if (touche === 'Escape') { + const touche = toucheDeGrille(evenement); + const saisie = effetDe('champ', evenement.key); + const tabulation = effetDe('grille', touche); + if (saisie === 'rendre') { evenement.preventDefault(); abandonner(); - } else if (touche === 'Enter') { + } else if (saisie === 'valider') { if (!entreeValide({ maj: evenement.shiftKey, multiligne: edition.colonne === 'notes' })) return; evenement.preventDefault(); valider(); - } else if (touche === 'Tab') { + } else if (tabulation === 'suivante' || tabulation === 'precedente') { // Une saisie refusée garde l'édition et le focus : Tab ne quitte pas // le champ, dont la sortie validerait une seconde fois. evenement.preventDefault(); const { id } = edition; const colonne = COLONNES.indexOf(edition.colonne); - const sens = evenement.shiftKey ? 'Maj+Tab' : 'Tab'; if (!(await valider())) return; await tick(); - tabulerDepuis(id, rang, colonne, sens); + tabulerDepuis(id, rang, colonne, touche); } } @@ -281,7 +298,7 @@ function tabulerDepuis(id, rangAvant, colonne, touche) { const rang = rangs.get(id) ?? -1; if (rang >= 0) return deplacer(rang, colonne, touche); - const suivante = touche === 'Tab' ? { ligne: rangAvant, colonne: 0 } : { ligne: rangAvant - 1, colonne: COLONNES.length - 1 }; + const suivante = effetDe('grille', touche) === 'suivante' ? { ligne: rangAvant, colonne: 0 } : { ligne: rangAvant - 1, colonne: COLONNES.length - 1 }; if (suivante.ligne < 0 || suivante.ligne >= visibles.length) return false; activer(suivante.ligne, suivante.colonne); return true; @@ -318,14 +335,15 @@ function surToucheCellule(evenement, personne, rang, colonne) { if (evenement.target !== evenement.currentTarget) return; - const touche = evenement.key === 'Tab' && evenement.shiftKey ? 'Maj+Tab' : evenement.key; - if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab', 'Maj+Tab'].includes(touche)) { + const touche = toucheDeGrille(evenement); + const effet = effetDe('grille', touche); + if (DEPLACEMENTS.has(effet)) { if (deplacer(rang, colonne, touche)) evenement.preventDefault(); - } else if (touche === 'Enter' || touche === 'F2') { + } else if (effet === 'editer') { evenement.preventDefault(); if (COLONNES[colonne] === 'exclu') basculerExclusion(personne); else ouvrirEdition(personne, COLONNES[colonne]); - } else if (touche === 'Escape') { + } else if (effet === 'effacer') { refusCellule = null; } } diff --git a/src/interface/participants/Participants.navigateur.test.js b/src/interface/participants/Participants.navigateur.test.js index ff47fe8..6edb346 100644 --- a/src/interface/participants/Participants.navigateur.test.js +++ b/src/interface/participants/Participants.navigateur.test.js @@ -443,6 +443,18 @@ describe('la grille (§ 10.2, § 19.3)', () => { expect(m.trace.length).toBe(trace); }); + test('Flèche gauche sur une cellule hors édition mène à la cellule précédente de la ligne, et s’arrête à la première', async () => { + const m = await evenementOuvert({ personnes: TROIS }); + const premier = idsAffiches(m)[0]; + cellule(m, premier, COLONNES[1]).focus(); + await touche('ArrowLeft'); + await rendu(); + expect(document.activeElement).toBe(cellule(m, premier, COLONNES[0])); + await touche('ArrowLeft'); + await rendu(); + expect(document.activeElement).toBe(cellule(m, premier, COLONNES[0])); + }); + test('Tab après une saisie qui déplace la ligne au tri mène à la cellule suivante de la même personne', async () => { const m = await evenementOuvert({ personnes: TROIS }); await cliquer(section(m).querySelector('[data-commande="trier"][data-cible="nom"]')); @@ -532,6 +544,37 @@ describe('la grille (§ 10.2, § 19.3)', () => { expect(document.activeElement).toBe(cellule(m, premier, COLONNES.at(-1))); }); + test('hors édition, Maj+Tab mène de la première cellule d’une ligne à la dernière de la ligne d’avant, Tab l’y ramène', async () => { + const m = await evenementOuvert({ personnes: TROIS }); + const [premier, second] = idsAffiches(m); + cellule(m, second, COLONNES[0]).focus(); + await rendu(); + await majTab(); + await rendu(); + expect(document.activeElement).toBe(cellule(m, premier, COLONNES.at(-1))); + expect(cellule(m, premier, COLONNES.at(-1)).querySelector('input, textarea')).toBeNull(); + await touche('Tab'); + await rendu(); + expect(document.activeElement).toBe(cellule(m, second, COLONNES[0])); + }); + + test('hors édition, Échap efface le refus de la cellule : Entrée en lecture le pose sans ouvrir de champ, la cellule garde le focus', async () => { + const m = await evenementOuvert({ personnes: TROIS }); + await m.seance.passerEnLecture(); + await rendu(); + const cible = cellule(m, idsAffiches(m)[0], 'nom'); + cible.focus(); + await touche('Enter'); + await rendu(); + expect(cible.querySelector('input')).toBeNull(); + expect(cible.querySelector('[data-refus]')).not.toBeNull(); + expect(document.activeElement).toBe(cible); + await touche('Escape'); + await rendu(); + expect(cible.querySelector('[data-refus]')).toBeNull(); + expect(cible.hasAttribute('aria-describedby')).toBe(false); + }); + test('une note de deux lignes, retouchée dans la grille, garde son saut de ligne', async () => { const m = await evenementOuvert(); await m.seance.executer('ajouterParticipant', { champs: { nom: 'Lacasse', prenom: 'Ondine', notes: 'arrive tard\nrepart tôt' } }); diff --git a/src/interface/participants/grille.js b/src/interface/participants/grille.js index 21ba952..13a3de5 100644 --- a/src/interface/participants/grille.js +++ b/src/interface/participants/grille.js @@ -11,6 +11,8 @@ // qu'elle reçoit ; la clé normalisée vient de l'appelant, qui la tient de // src/application/csv.js. +import { effetDe } from '../raccourcis.js'; + /** Les colonnes de la grille, propriétés d'un participant, dans l'ordre des * colonnes de l'export : « l'ordre de la grille » que relit l'import. */ export const COLONNES = Object.freeze(['nom', 'prenom', 'appartenance', 'courriel', 'titrePressenti', 'exclu', 'notes']); @@ -420,16 +422,18 @@ export function cheminDe(groupes, id) { /** * La cellule qu'atteint une touche depuis la cellule position, dans une - * grille de lignes × colonnes. Les flèches bougent d'une cellule et restent - * sur place au bord ; Tab avance dans la ligne puis passe au début de la - * suivante, Maj+Tab recule de même. Tab après la dernière cellule, Maj+Tab - * avant la première, rendent null : le focus quitte la grille, et le - * navigateur le mène à l'élément suivant. Une autre touche ne bouge rien. - * Une grille sans ligne n'a pas de cellule : null. + * grille de lignes × colonnes. La touche se lit par effetDe dans le + * contexte 'grille' de RACCOURCIS. Les flèches — effets haut, bas, gauche, + * droite — bougent d'une cellule et restent sur place au bord ; Tab (effet + * suivante) avance dans la ligne puis passe au début de la suivante, + * Maj+Tab (effet precedente) recule de même. Tab après la dernière cellule, + * Maj+Tab avant la première, rendent null : le focus quitte la grille, et + * le navigateur le mène à l'élément suivant. Une autre touche ne bouge + * rien. Une grille sans ligne n'a pas de cellule : null. * * @param {{ligne: number, colonne: number}} position - * @param {string} touche 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', - * 'Tab' ou 'Maj+Tab' + * @param {string} touche la touche telle que RACCOURCIS la nomme : + * KeyboardEvent.key, « Maj+ » devant quand la majuscule change l'effet * @param {{lignes: number, colonnes: number}} taille * @returns {{ligne: number, colonne: number}|null} */ @@ -437,18 +441,18 @@ export function celluleSuivante({ ligne, colonne }, touche, { lignes, colonnes } if (lignes === 0 || colonnes === 0) return null; const borner = (valeur, max) => (valeur < 0 ? 0 : valeur > max ? max : valeur); const rang = ligne * colonnes + colonne; - switch (touche) { - case 'ArrowUp': + switch (effetDe('grille', touche)) { + case 'haut': return { ligne: borner(ligne - 1, lignes - 1), colonne }; - case 'ArrowDown': + case 'bas': return { ligne: borner(ligne + 1, lignes - 1), colonne }; - case 'ArrowLeft': + case 'gauche': return { ligne, colonne: borner(colonne - 1, colonnes - 1) }; - case 'ArrowRight': + case 'droite': return { ligne, colonne: borner(colonne + 1, colonnes - 1) }; - case 'Tab': + case 'suivante': return rang + 1 >= lignes * colonnes ? null : { ligne: Math.floor((rang + 1) / colonnes), colonne: (rang + 1) % colonnes }; - case 'Maj+Tab': + case 'precedente': return rang === 0 ? null : { ligne: Math.floor((rang - 1) / colonnes), colonne: (rang - 1) % colonnes }; default: return { ligne, colonne }; diff --git a/src/interface/plan/ListesTravail.svelte b/src/interface/plan/ListesTravail.svelte index 16f8af9..36752ab 100644 --- a/src/interface/plan/ListesTravail.svelte +++ b/src/interface/plan/ListesTravail.svelte @@ -26,6 +26,7 @@ import { getContext, tick } from 'svelte'; import { conversionsPossibles, pressentisNonReserves, titresNonPourvus } from '../../application/listes_travail.js'; import Commande from '../primitives/Commande.svelte'; + import { effetDe } from '../raccourcis.js'; import { nomDeCharge } from './vue_plan.js'; const t = getContext('t'); @@ -63,7 +64,7 @@ } function surTouche(evenement) { - if (evenement.key !== 'Escape' || choix === null) return; + if (effetDe('plan', evenement.key) !== 'remonter' || choix === null) return; evenement.preventDefault(); ui.choisir(null); } diff --git a/src/interface/plan/PanneauPlace.svelte b/src/interface/plan/PanneauPlace.svelte index 7ce97c4..fa4b396 100644 --- a/src/interface/plan/PanneauPlace.svelte +++ b/src/interface/plan/PanneauPlace.svelte @@ -27,6 +27,7 @@ import { getContext, untrack } from 'svelte'; import { libellePropose } from '../../application/places.js'; import Commande from '../primitives/Commande.svelte'; + import { effetDe } from '../raccourcis.js'; import { commandeDeDepot } from './depot.js'; import { libellePlace } from './gestes.js'; import MenuPersonne from './MenuPersonne.svelte'; @@ -154,7 +155,7 @@ // Échap referme le panneau et rend le focus à la racine du plan. function surTouche(evenement) { - if (evenement.key !== 'Escape') return; + if (effetDe('fenetre', evenement.key) !== 'fermer') return; evenement.preventDefault(); const racine = evenement.currentTarget.closest('[data-section]')?.querySelector('[data-plan]') ?? null; ui.selectionner(null); diff --git a/src/interface/plan/PanneauTable.svelte b/src/interface/plan/PanneauTable.svelte index 173b3a2..751bf93 100644 --- a/src/interface/plan/PanneauTable.svelte +++ b/src/interface/plan/PanneauTable.svelte @@ -28,6 +28,7 @@ import { comptesTitres } from '../../application/listes_travail.js'; import { libellePropose } from '../../application/places.js'; import Commande from '../primitives/Commande.svelte'; + import { effetDe } from '../raccourcis.js'; import { libellePlace } from './gestes.js'; import { executerSurPlace, ligneListe, partiesLigne, textesLigne } from './modele.js'; import { nomDeCharge } from './vue_plan.js'; @@ -103,7 +104,7 @@ } function surTouche(evenement) { - if (evenement.key !== 'Escape') return; + if (effetDe('fenetre', evenement.key) !== 'fermer') return; evenement.preventDefault(); const racine = evenement.currentTarget.closest('[data-section]')?.querySelector('[data-plan]') ?? null; ui.selectionner(null); diff --git a/src/interface/plan/Panneaux.navigateur.test.js b/src/interface/plan/Panneaux.navigateur.test.js index bbf28f0..5de3afd 100644 --- a/src/interface/plan/Panneaux.navigateur.test.js +++ b/src/interface/plan/Panneaux.navigateur.test.js @@ -872,6 +872,19 @@ describe('Échap oublie le choix en attente (§ 7.1, § 7.3)', () => { await touche('Escape'); await attendre(() => expect(m.ui.etat().choix).toBeNull()); }); + + test('choisie d’un vrai clic dans la réserve : Échap sur le plan sans élément actif l’oublie', async () => { + const m = await ouvrir(sansProposition()); + // Le vrai pointeur ne rend pas le focus suivant focus-visible : la + // racine du plan, focalisée ensuite, n'a aucun élément actif. + await presserBouton(dans(m, `[data-reserve] [data-participant="${THEO}"]`)); + await attendre(() => expect(m.ui.etat().choix?.participant).toBe(THEO)); + racinePlan(m).focus(); + flushSync(); + expect(actif(m)).toBeNull(); + await touche('Escape'); + await attendre(() => expect(m.ui.etat().choix).toBeNull()); + }); }); describe('un clic ne déplace pas la vue (§ 7.3, § 8.4)', () => { diff --git a/src/interface/plan/Plan.svelte b/src/interface/plan/Plan.svelte index a003c65..f48c06e 100644 --- a/src/interface/plan/Plan.svelte +++ b/src/interface/plan/Plan.svelte @@ -94,6 +94,7 @@ import { controlerCapacite } from '../../application/capacite.js'; import { constante } from '../../geometrie/constantes.js'; import { ajuster as vueAjustee, ecranDepuisDessin, transformDe } from '../../geometrie/vue.js'; + import { effetDe } from '../raccourcis.js'; import BandeauPlan from './BandeauPlan.svelte'; import BarreVue from './BarreVue.svelte'; import { toucheDuPlan, vueQuiMontre } from './clavier.js'; @@ -544,7 +545,7 @@ }; function surTouche(evenement) { - if (evenement.key !== 'Escape' || etatGeste.phase === 'repos') return; + if (effetDe('geste', evenement.key) !== 'annuler' || etatGeste.phase === 'repos') return; evenement.stopPropagation(); evenement.preventDefault(); annulerGeste('echap'); @@ -671,7 +672,7 @@ if (etatGeste.phase !== 'repos' || modele === null || evenement.altKey || evenement.ctrlKey || evenement.metaKey) return; auClavier = true; const rendu = toucheDuPlan(actif, evenement.key, contexteClavier()); - if (evenement.key === 'Escape' && (rendu === null || rendu.effet !== null)) ui.choisir(null); + if (effetDe('plan', evenement.key) === 'remonter' && (rendu === null || rendu.effet !== null)) ui.choisir(null); if (rendu === null) return; evenement.preventDefault(); actif = rendu.actif; diff --git a/src/interface/plan/Reserve.svelte b/src/interface/plan/Reserve.svelte index 8ae9a7c..2bf3968 100644 --- a/src/interface/plan/Reserve.svelte +++ b/src/interface/plan/Reserve.svelte @@ -14,6 +14,7 @@ // Le panneau lit 'seance' et 'ui' au contexte ; etatTour, l'état des // places du tour affiché, que SectionPlan calcule, ou null sans événement. import { getContext } from 'svelte'; + import { effetDe } from '../raccourcis.js'; import { nomDeCharge } from './vue_plan.js'; let { etatTour } = $props(); @@ -35,7 +36,7 @@ } function surTouche(evenement) { - if (evenement.key !== 'Escape' || choix === null) return; + if (effetDe('plan', evenement.key) !== 'remonter' || choix === null) return; evenement.preventDefault(); ui.choisir(null); } diff --git a/src/interface/primitives/Champ.svelte b/src/interface/primitives/Champ.svelte index 321f9c1..5602372 100644 --- a/src/interface/primitives/Champ.svelte +++ b/src/interface/primitives/Champ.svelte @@ -32,6 +32,7 @@ // retire pas un refus : son remède, qu'un clic atteint, resterait sinon // hors de portée. import { getContext, onDestroy, untrack } from 'svelte'; + import { effetDe } from '../raccourcis.js'; import { creerMachineChamp, estRefusSeance, refusSeanceCourant } from './champ.js'; import { creerGeste } from './geste.js'; import Refus from './Refus.svelte'; @@ -168,8 +169,9 @@ } function surTouche(evenement) { - if (evenement.key === 'Escape') abandonner(evenement.currentTarget); - else if (evenement.key === 'Enter') { + const effet = effetDe('champ', evenement.key); + if (effet === 'rendre') abandonner(evenement.currentTarget); + else if (effet === 'valider') { evenement.preventDefault(); valider(); } diff --git a/src/interface/primitives/Commande.svelte b/src/interface/primitives/Commande.svelte index 8847c76..57f7a9a 100644 --- a/src/interface/primitives/Commande.svelte +++ b/src/interface/primitives/Commande.svelte @@ -27,6 +27,7 @@ // confirmer se lit au montage : une commande qui change de confirmation se // remonte. import { getContext, onDestroy, untrack } from 'svelte'; + import { effetDe } from '../raccourcis.js'; import { DELAI_CONFIRMATION_MS, creerMachineCommande } from './commande.js'; import Refus from './Refus.svelte'; @@ -82,7 +83,7 @@ } function surTouche(evenement) { - if (evenement.repeat && (evenement.key === 'Enter' || evenement.key === ' ')) evenement.preventDefault(); + if (evenement.repeat && effetDe('commande', evenement.key) === 'activer') evenement.preventDefault(); } onDestroy(desarmer); diff --git a/src/interface/primitives/Infobulle.svelte b/src/interface/primitives/Infobulle.svelte index 4ff7074..46cadaa 100644 --- a/src/interface/primitives/Infobulle.svelte +++ b/src/interface/primitives/Infobulle.svelte @@ -23,6 +23,7 @@ // n'est pas placée, elle reste invisible. Une ancre retirée du document la // referme. Elle ne capte aucun pointeur : le terme reste survolé. import { getContext } from 'svelte'; + import { effetDe } from '../raccourcis.js'; import { placerInfobulle } from './infobulle.js'; let { id, ouverte = null, fermer } = $props(); @@ -60,7 +61,7 @@ }); function surTouche(evenement) { - if (ouverte !== null && evenement.key === 'Escape') fermer(); + if (ouverte !== null && effetDe('fenetre', evenement.key) === 'fermer') fermer(); } diff --git a/src/interface/raccourcis.js b/src/interface/raccourcis.js index 3901b76..5ec8d49 100644 --- a/src/interface/raccourcis.js +++ b/src/interface/raccourcis.js @@ -3,10 +3,10 @@ // La table des raccourcis clavier de l'interface (§ 7.3, § 13.5, § 19.8) : // la seule source du tableau des raccourcis de la documentation. Le code qui -// répond à une touche la lit par effetDe ; les fichiers que la garde de -// test/sources_interface.test.js admet encore nomment leurs touches eux-mêmes, -// et chacune de ces touches doit avoir son entrée ici, sans quoi la garde la -// relève comme absente du tableau. +// répond à une touche la lit par effetDe, et la garde de +// test/sources_interface.test.js relève toute touche nommée hors d'ici : +// l'interface ne répond qu'aux touches de la table, et le tableau décrit +// chacune de ses réponses. // // Une entrée est { touche, contexte, effet, cle } : // touche la valeur de KeyboardEvent.key, précédée de « Maj+ » quand la diff --git a/src/interface/tables/ListeTables.svelte b/src/interface/tables/ListeTables.svelte index 89e949d..e01e930 100644 --- a/src/interface/tables/ListeTables.svelte +++ b/src/interface/tables/ListeTables.svelte @@ -27,6 +27,7 @@ import Champ from '../primitives/Champ.svelte'; import Commande from '../primitives/Commande.svelte'; import Confirmation from '../primitives/Confirmation.svelte'; + import { effetDe } from '../raccourcis.js'; import { annonceSuppression, avertissementsNommes, @@ -109,7 +110,7 @@ const arrets = [ on(element, 'input', () => oublier(id)), on(element, 'keydown', (evenement) => { - if (evenement.key === 'Escape') oublier(id); + if (effetDe('champ', evenement.key) === 'rendre') oublier(id); }), ]; return () => { diff --git a/src/interface/tables/Tables.navigateur.test.js b/src/interface/tables/Tables.navigateur.test.js index 73cc0c1..f996056 100644 --- a/src/interface/tables/Tables.navigateur.test.js +++ b/src/interface/tables/Tables.navigateur.test.js @@ -494,6 +494,28 @@ describe('la capacité d’une table, saisie au champ (§ 6.2)', () => { expect(ecritures(m)).toBe(avant); }); + test('sièges retirés puis Échap dans le champ : le refus et la liste des sièges retirés partent ensemble ; rien d’écrit', async () => { + const m = await preparer( + chargeDe({ + tables: [{ id: 1 }], + n: 4, + attribuerSieges: true, + reservations: [resa(1, 1, 7)], + titres: [{ table: 1, siege: 8, libelle: 'animateur' }], + }), + ); + const avant = ecritures(m); + await saisir(m, champTable(m, 1), '6'); + expect(ligne(m, 1).querySelector('[data-refus="SIEGES_RETIRES"]')).not.toBeNull(); + expect(ligne(m, 1).querySelector('[data-sieges-retires]')).not.toBeNull(); + expect(document.activeElement).toBe(champTable(m, 1)); + await touche('Escape'); + await attendre(m); + expect(ligne(m, 1).querySelector('[data-refus="SIEGES_RETIRES"]')).toBeNull(); + expect(ligne(m, 1).querySelector('[data-sieges-retires]')).toBeNull(); + expect(ecritures(m)).toBe(avant); + }); + test('surnuméraires et lisibilité : un retenu assied huit personnes à la table 3 au tour 2 ; 6 accepté, l’avertissement nomme les deux personnes versées en réserve ; S + 1 avertit de S ; S, non', async () => { const donnees = chargeDe({ tables: [{ id: 1 }, { id: 2 }, { id: 3 }], n: 18 }); placer( diff --git a/test/sources_interface.test.js b/test/sources_interface.test.js index 24add4f..baac44d 100644 --- a/test/sources_interface.test.js +++ b/test/sources_interface.test.js @@ -33,7 +33,6 @@ import { TABLES_APPLICATION } from '../src/application/libelles.js'; import { creerTraduction } from '../src/application/traduction.js'; import { TABLES_INTERFACE } from '../src/interface/libelles.js'; import { JETONS_CONFLIT } from '../src/interface/marques.js'; -import { CONTEXTES_RACCOURCIS, RACCOURCIS } from '../src/interface/raccourcis.js'; import { DESIGNATIONS } from './navigateur/designations.js'; import { describe, test } from './lanceur.js'; @@ -1079,98 +1078,21 @@ function valeurDeChaine(noeud) { return null; } -// Les fichiers de src/interface qui nomment encore leurs touches, chacun avec -// les contextes de RACCOURCIS où il agit et le relevé de ses touches, ligne -// omise, trié : la garde les admet tels quels. Un écart — une touche de -// plus, une de moins — relève les touches du fichier et demande de revoir -// son entrée ; une entrée dont le fichier ne nomme plus rien est sans objet. -// Faire lire RACCOURCIS à l'un d'eux retire son entrée. Tant qu'il ne la lit -// pas, chacune de ses touches a son entrée dans RACCOURCIS sous l'un de ses -// contextes : le tableau engendré décrit ce que le fichier fait, et une -// entrée retirée ou renommée dans la table se relève ici. -const enAttente = (contextes, touches) => Object.freeze({ contextes: Object.freeze(contextes), touches: Object.freeze(touches) }); -const TOUCHES_EN_ATTENTE = new Map([ - ['src/interface/participants/Formulaire.svelte', enAttente(['champ'], ['touche "Enter"'])], - [ - 'src/interface/participants/Grille.svelte', - enAttente( - ['grille', 'champ'], - [ - 'touche "ArrowDown"', - 'touche "ArrowLeft"', - 'touche "ArrowRight"', - 'touche "ArrowUp"', - 'touche "Enter"', - 'touche "Enter"', - 'touche "Escape"', - 'touche "Escape"', - 'touche "F2"', - 'touche "Maj+Tab"', - 'touche "Maj+Tab"', - 'touche "Maj+Tab"', - 'touche "Tab"', - 'touche "Tab"', - 'touche "Tab"', - 'touche "Tab"', - 'touche "Tab"', - ], - ), - ], - [ - 'src/interface/participants/grille.js', - enAttente( - ['grille'], - ['touche "ArrowDown"', 'touche "ArrowLeft"', 'touche "ArrowRight"', 'touche "ArrowUp"', 'touche "Maj+Tab"', 'touche "Tab"'], - ), - ], - ['src/interface/plan/ListesTravail.svelte', enAttente(['plan'], ['touche "Escape"'])], - ['src/interface/plan/PanneauPlace.svelte', enAttente(['fenetre'], ['touche "Escape"'])], - ['src/interface/plan/PanneauTable.svelte', enAttente(['fenetre'], ['touche "Escape"'])], - ['src/interface/plan/Plan.svelte', enAttente(['plan', 'geste'], ['touche "Escape"', 'touche "Escape"'])], - ['src/interface/plan/Reserve.svelte', enAttente(['plan'], ['touche "Escape"'])], - ['src/interface/primitives/Champ.svelte', enAttente(['champ'], ['touche "Enter"', 'touche "Escape"'])], - ['src/interface/primitives/Commande.svelte', enAttente(['commande'], ['touche " "', 'touche "Enter"'])], - ['src/interface/primitives/Infobulle.svelte', enAttente(['fenetre'], ['touche "Escape"'])], - ['src/interface/tables/ListeTables.svelte', enAttente(['champ'], ['touche "Escape"'])], -]); - -// Les touches des fichiers en attente qu'aucune entrée de raccourcis ne -// décrit sous un des contextes du fichier : « chemin touche "…" hors de -// RACCOURCIS (contextes) », trié. Un contexte qui n'est pas dans -// contextesConnus se relève aussi. -function touchesSansEntree(raccourcis, contextesConnus, enAttenteDe = TOUCHES_EN_ATTENTE) { - const decrites = new Set(raccourcis.map(({ contexte, touche }) => `${contexte}:${touche}`)); - const releves = []; - for (const [chemin, { contextes, touches }] of enAttenteDe) { - for (const contexte of contextes) { - if (!contextesConnus.includes(contexte)) releves.push(`${chemin} contexte ${JSON.stringify(contexte)} inconnu`); - } - for (const motif of new Set(touches)) { - const touche = JSON.parse(motif.slice('touche '.length)); - if (!contextes.some((contexte) => decrites.has(`${contexte}:${touche}`))) { - releves.push(`${chemin} ${motif} hors de RACCOURCIS (${contextes.join(', ')})`); - } - } - } - return releves.sort(); -} - // Relevé des touches nommées dans les composants et les modules de // src/interface, hors de la table des raccourcis : une chaîne qui nomme une // touche (TOUCHE_NOMMEE), où qu'elle soit dans le code, et toute chaîne — // l'espace, une lettre — comparée à une lecture de key ou cas d'un switch // sur elle. Une touche se relève une fois, sous la forme « touche "…" ». -// Puis les fichiers en attente : admis quand leur relevé est le leur. -function releverTouches(racine, enAttenteDe = TOUCHES_EN_ATTENTE) { +// Aucun fichier n'est admis : le code répond à une touche par effetDe. +function releverTouches(racine) { const chemins = exigerNonVide( sources(racine, ['src/interface'], ['.svelte', '.js']).filter((chemin) => chemin !== TABLE_RACCOURCIS), 'touches nommées', ); - const parFichier = new Map(); + const releves = []; for (const chemin of chemins) { const composant = chemin.endsWith('.svelte'); const { texte, ast } = composant ? analyserComposant(racine, chemin) : analyserModule(racine, chemin); - const releves = []; const vus = new Set(); const relever = (noeud) => { const valeur = valeurDeChaine(noeud); @@ -1188,19 +1110,8 @@ function releverTouches(racine, enAttenteDe = TOUCHES_EN_ATTENTE) { relever(noeud); } }); - if (releves.length > 0) parFichier.set(chemin, releves); } - const releves = []; - const aRevoir = []; - for (const [chemin, deFichier] of parFichier) { - const admis = enAttenteDe.get(chemin)?.touches; - const motifs = deFichier.map(({ texte }) => texte).sort(); - if (admis !== undefined && JSON.stringify(motifs) === JSON.stringify([...admis].sort())) continue; - releves.push(...deFichier); - if (admis !== undefined) aRevoir.push(`${chemin} : en attente, entrée à revoir`); - } - const sansObjet = [...enAttenteDe.keys()].filter((chemin) => !parFichier.has(chemin)).map((chemin) => `${chemin} : en attente sans objet`); - return [...trier(releves), ...[...aRevoir, ...sansObjet].sort()]; + return trier(releves); } // --- Arbres d'épreuve --------------------------------------------------------- @@ -1842,7 +1753,7 @@ describe('sources de l’interface : les désignations stables (contrainte 16)', }); describe('sources de l’interface : les touches nommées (§ 7.3, § 19.8)', () => { - test('aucune touche nommée sous src/interface hors de raccourcis.js, sauf les fichiers en attente, tels quels', () => { + test('aucune touche nommée sous src/interface hors de raccourcis.js : chaque fichier lit ses touches par effetDe', () => { exigerVide(releverTouches(RACINE), 'touches nommées'); }); @@ -1867,7 +1778,7 @@ describe('sources de l’interface : les touches nommées (§ 7.3, § 19.8)', () 'src/interface/A.test.js': "if (e.key === 'Escape') f();\n", 'src/application/c.js': "export const k = (e) => e.key === 'Escape';\n", }; - assert.deepEqual(avecArbre(fichiers, (racine) => releverTouches(racine, new Map())), [ + assert.deepEqual(avecArbre(fichiers, releverTouches), [ 'src/interface/A.svelte:2 touche "Enter"', 'src/interface/A.svelte:4 touche "Tab"', 'src/interface/plan/sous/b.js:2 touche "Escape"', @@ -1880,53 +1791,6 @@ describe('sources de l’interface : les touches nommées (§ 7.3, § 19.8)', () ]); }); - test('un fichier en attente n’est admis qu’avec son relevé exact ; un écart relève ses touches, une entrée que rien ne sert est sans objet', () => { - const fichiers = { - 'src/interface/x.js': "export const f = (e) => e.key === 'Escape' || e.key === 'Enter';\n", - 'src/interface/y.js': "export const g = (e) => e.key === 'Escape';\n", - 'src/interface/z.js': 'export const h = 1;\n', - }; - const attente = new Map([ - ['src/interface/x.js', enAttente(['champ'], ['touche "Escape"', 'touche "Enter"'])], - ['src/interface/y.js', enAttente(['champ'], ['touche "Escape"', 'touche "Tab"'])], - ['src/interface/z.js', enAttente(['champ'], ['touche "Escape"'])], - ['src/interface/w.js', enAttente(['champ'], ['touche "Escape"'])], - ]); - assert.deepEqual(avecArbre(fichiers, (racine) => releverTouches(racine, attente)), [ - 'src/interface/y.js:1 touche "Escape"', - 'src/interface/w.js : en attente sans objet', - 'src/interface/y.js : en attente, entrée à revoir', - 'src/interface/z.js : en attente sans objet', - ]); - }); - - test('chaque touche d’un fichier en attente a son entrée dans RACCOURCIS sous un des contextes du fichier : le tableau engendré décrit ce qu’il fait', () => { - assert.ok(TOUCHES_EN_ATTENTE.size > 0 || RACCOURCIS.length > 0); - exigerVide(touchesSansEntree(RACCOURCIS, CONTEXTES_RACCOURCIS), 'touches en attente sans entrée'); - }); - - test('une touche en attente que la table ne décrit plus sous les contextes du fichier se relève, comme un contexte inconnu', () => { - const raccourcis = [ - { touche: 'Escape', contexte: 'fenetre', effet: 'fermer' }, - { touche: 'Enter', contexte: 'champ', effet: 'valider' }, - { touche: 'q', contexte: 'geste', effet: 'annuler' }, - ]; - const attente = new Map([ - ['src/interface/a.svelte', enAttente(['fenetre'], ['touche "Escape"', 'touche "Escape"'])], - ['src/interface/b.svelte', enAttente(['geste', 'champ'], ['touche "Enter"', 'touche "Escape"'])], - ['src/interface/c.svelte', enAttente(['fenetre', 'tiroir'], ['touche "Escape"'])], - ]); - assert.deepEqual(touchesSansEntree(raccourcis, ['champ', 'geste', 'fenetre'], attente), [ - 'src/interface/b.svelte touche "Escape" hors de RACCOURCIS (geste, champ)', - 'src/interface/c.svelte contexte "tiroir" inconnu', - ]); - // La table où Échap de la fenêtre devient « q » : le fichier qui ferme sur Échap se relève. - const deplacee = raccourcis.map((entree) => (entree.contexte === 'fenetre' ? { ...entree, touche: 'q' } : entree)); - assert.deepEqual(touchesSansEntree(deplacee, ['champ', 'geste', 'fenetre'], new Map([...attente].slice(0, 1))), [ - 'src/interface/a.svelte touche "Escape" hors de RACCOURCIS (fenetre)', - ]); - }); - test('un arbre sans source d’interface, ou réduit à la table des raccourcis, fait échouer la garde', () => { assert.throws(() => avecArbre({ 'src/application/a.js': '' }, releverTouches), /touches nommées : aucun fichier examiné/); assert.throws(