[REF] interface: read every key through the shortcut table
Twelve components still compared key names themselves, so the shortcut table could drift from what they actually did; a guard listed them as pending. Each now reads its effect from RACCOURCIS through effetDe, the pending list and its guard are removed, and the generated shortcut table is unchanged. The keys the old guard pinned without a behaviour test (Escape on the plan root with no active element, ArrowLeft in the grid, and those of the participant and table lists) each gain one. Checked: 3852 node, 766 browser, 101 long tests, from the index alone. --- FR --- [REF] interface : chaque touche lue par la table des raccourcis Douze composants comparaient encore eux-mêmes les noms de touches : la table des raccourcis pouvait s'écarter de ce qu'ils faisaient, et une garde les tenait en attente. Chacun lit désormais son effet dans RACCOURCIS par effetDe ; la liste d'attente et sa garde disparaissent, la table engendrée ne change pas. Les touches que l'ancienne garde figeait sans épreuve de comportement (Échap sur le plan sans élément actif, flèche gauche dans la grille, listes des personnes et des tables) en reçoivent chacune une. Vérifié : 3852 node, 766 navigateur, 101 longues, depuis l'index seul. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
8f1edc83c7
commit
13bf16b5be
17 changed files with 159 additions and 184 deletions
|
|
@ -34,6 +34,7 @@
|
||||||
import { cleNormalisee } from '../../application/csv.js';
|
import { cleNormalisee } from '../../application/csv.js';
|
||||||
import { creerGeste } from '../primitives/geste.js';
|
import { creerGeste } from '../primitives/geste.js';
|
||||||
import Refus from '../primitives/Refus.svelte';
|
import Refus from '../primitives/Refus.svelte';
|
||||||
|
import { effetDe } from '../raccourcis.js';
|
||||||
import { cleDeColonne, entreeValide, orthographeExistante } from './grille.js';
|
import { cleDeColonne, entreeValide, orthographeExistante } from './grille.js';
|
||||||
|
|
||||||
let { participants } = $props();
|
let { participants } = $props();
|
||||||
|
|
@ -113,7 +114,7 @@
|
||||||
// Entrée dans les notes envoie le formulaire ; Maj+Entrée y garde le
|
// Entrée dans les notes envoie le formulaire ; Maj+Entrée y garde le
|
||||||
// saut de ligne de la zone de texte.
|
// saut de ligne de la zone de texte.
|
||||||
function surToucheNotes(evenement) {
|
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();
|
evenement.preventDefault();
|
||||||
formulaire.requestSubmit();
|
formulaire.requestSubmit();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@
|
||||||
import Commande from '../primitives/Commande.svelte';
|
import Commande from '../primitives/Commande.svelte';
|
||||||
import { creerGeste } from '../primitives/geste.js';
|
import { creerGeste } from '../primitives/geste.js';
|
||||||
import Refus from '../primitives/Refus.svelte';
|
import Refus from '../primitives/Refus.svelte';
|
||||||
|
import { effetDe } from '../raccourcis.js';
|
||||||
import { cleDeRaison } from './conflits.js';
|
import { cleDeRaison } from './conflits.js';
|
||||||
import { COLONNES, basculerGroupe, celluleSuivante, cheminDe, cleDeColonne, deplierChemin, entreeValide, lignesVisibles, ligneVoisine } from './grille.js';
|
import { COLONNES, basculerGroupe, celluleSuivante, cheminDe, cleDeColonne, deplierChemin, entreeValide, lignesVisibles, ligneVoisine } from './grille.js';
|
||||||
|
|
||||||
|
|
@ -249,25 +250,41 @@
|
||||||
geste.tenir(machine.enCours);
|
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) {
|
async function surToucheEntree(evenement, rang) {
|
||||||
const touche = evenement.key;
|
const touche = toucheDeGrille(evenement);
|
||||||
if (touche === 'Escape') {
|
const saisie = effetDe('champ', evenement.key);
|
||||||
|
const tabulation = effetDe('grille', touche);
|
||||||
|
if (saisie === 'rendre') {
|
||||||
evenement.preventDefault();
|
evenement.preventDefault();
|
||||||
abandonner();
|
abandonner();
|
||||||
} else if (touche === 'Enter') {
|
} else if (saisie === 'valider') {
|
||||||
if (!entreeValide({ maj: evenement.shiftKey, multiligne: edition.colonne === 'notes' })) return;
|
if (!entreeValide({ maj: evenement.shiftKey, multiligne: edition.colonne === 'notes' })) return;
|
||||||
evenement.preventDefault();
|
evenement.preventDefault();
|
||||||
valider();
|
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
|
// Une saisie refusée garde l'édition et le focus : Tab ne quitte pas
|
||||||
// le champ, dont la sortie validerait une seconde fois.
|
// le champ, dont la sortie validerait une seconde fois.
|
||||||
evenement.preventDefault();
|
evenement.preventDefault();
|
||||||
const { id } = edition;
|
const { id } = edition;
|
||||||
const colonne = COLONNES.indexOf(edition.colonne);
|
const colonne = COLONNES.indexOf(edition.colonne);
|
||||||
const sens = evenement.shiftKey ? 'Maj+Tab' : 'Tab';
|
|
||||||
if (!(await valider())) return;
|
if (!(await valider())) return;
|
||||||
await tick();
|
await tick();
|
||||||
tabulerDepuis(id, rang, colonne, sens);
|
tabulerDepuis(id, rang, colonne, touche);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,7 +298,7 @@
|
||||||
function tabulerDepuis(id, rangAvant, colonne, touche) {
|
function tabulerDepuis(id, rangAvant, colonne, touche) {
|
||||||
const rang = rangs.get(id) ?? -1;
|
const rang = rangs.get(id) ?? -1;
|
||||||
if (rang >= 0) return deplacer(rang, colonne, touche);
|
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;
|
if (suivante.ligne < 0 || suivante.ligne >= visibles.length) return false;
|
||||||
activer(suivante.ligne, suivante.colonne);
|
activer(suivante.ligne, suivante.colonne);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -318,14 +335,15 @@
|
||||||
|
|
||||||
function surToucheCellule(evenement, personne, rang, colonne) {
|
function surToucheCellule(evenement, personne, rang, colonne) {
|
||||||
if (evenement.target !== evenement.currentTarget) return;
|
if (evenement.target !== evenement.currentTarget) return;
|
||||||
const touche = evenement.key === 'Tab' && evenement.shiftKey ? 'Maj+Tab' : evenement.key;
|
const touche = toucheDeGrille(evenement);
|
||||||
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab', 'Maj+Tab'].includes(touche)) {
|
const effet = effetDe('grille', touche);
|
||||||
|
if (DEPLACEMENTS.has(effet)) {
|
||||||
if (deplacer(rang, colonne, touche)) evenement.preventDefault();
|
if (deplacer(rang, colonne, touche)) evenement.preventDefault();
|
||||||
} else if (touche === 'Enter' || touche === 'F2') {
|
} else if (effet === 'editer') {
|
||||||
evenement.preventDefault();
|
evenement.preventDefault();
|
||||||
if (COLONNES[colonne] === 'exclu') basculerExclusion(personne);
|
if (COLONNES[colonne] === 'exclu') basculerExclusion(personne);
|
||||||
else ouvrirEdition(personne, COLONNES[colonne]);
|
else ouvrirEdition(personne, COLONNES[colonne]);
|
||||||
} else if (touche === 'Escape') {
|
} else if (effet === 'effacer') {
|
||||||
refusCellule = null;
|
refusCellule = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -443,6 +443,18 @@ describe('la grille (§ 10.2, § 19.3)', () => {
|
||||||
expect(m.trace.length).toBe(trace);
|
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 () => {
|
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 });
|
const m = await evenementOuvert({ personnes: TROIS });
|
||||||
await cliquer(section(m).querySelector('[data-commande="trier"][data-cible="nom"]'));
|
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)));
|
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 () => {
|
test('une note de deux lignes, retouchée dans la grille, garde son saut de ligne', async () => {
|
||||||
const m = await evenementOuvert();
|
const m = await evenementOuvert();
|
||||||
await m.seance.executer('ajouterParticipant', { champs: { nom: 'Lacasse', prenom: 'Ondine', notes: 'arrive tard\nrepart tôt' } });
|
await m.seance.executer('ajouterParticipant', { champs: { nom: 'Lacasse', prenom: 'Ondine', notes: 'arrive tard\nrepart tôt' } });
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@
|
||||||
// qu'elle reçoit ; la clé normalisée vient de l'appelant, qui la tient de
|
// qu'elle reçoit ; la clé normalisée vient de l'appelant, qui la tient de
|
||||||
// src/application/csv.js.
|
// src/application/csv.js.
|
||||||
|
|
||||||
|
import { effetDe } from '../raccourcis.js';
|
||||||
|
|
||||||
/** Les colonnes de la grille, propriétés d'un participant, dans l'ordre des
|
/** 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. */
|
* 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']);
|
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
|
* 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
|
* grille de lignes × colonnes. La touche se lit par effetDe dans le
|
||||||
* sur place au bord ; Tab avance dans la ligne puis passe au début de la
|
* contexte 'grille' de RACCOURCIS. Les flèches — effets haut, bas, gauche,
|
||||||
* suivante, Maj+Tab recule de même. Tab après la dernière cellule, Maj+Tab
|
* droite — bougent d'une cellule et restent sur place au bord ; Tab (effet
|
||||||
* avant la première, rendent null : le focus quitte la grille, et le
|
* suivante) avance dans la ligne puis passe au début de la suivante,
|
||||||
* navigateur le mène à l'élément suivant. Une autre touche ne bouge rien.
|
* Maj+Tab (effet precedente) recule de même. Tab après la dernière cellule,
|
||||||
* Une grille sans ligne n'a pas de cellule : null.
|
* 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 {{ligne: number, colonne: number}} position
|
||||||
* @param {string} touche 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight',
|
* @param {string} touche la touche telle que RACCOURCIS la nomme :
|
||||||
* 'Tab' ou 'Maj+Tab'
|
* KeyboardEvent.key, « Maj+ » devant quand la majuscule change l'effet
|
||||||
* @param {{lignes: number, colonnes: number}} taille
|
* @param {{lignes: number, colonnes: number}} taille
|
||||||
* @returns {{ligne: number, colonne: number}|null}
|
* @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;
|
if (lignes === 0 || colonnes === 0) return null;
|
||||||
const borner = (valeur, max) => (valeur < 0 ? 0 : valeur > max ? max : valeur);
|
const borner = (valeur, max) => (valeur < 0 ? 0 : valeur > max ? max : valeur);
|
||||||
const rang = ligne * colonnes + colonne;
|
const rang = ligne * colonnes + colonne;
|
||||||
switch (touche) {
|
switch (effetDe('grille', touche)) {
|
||||||
case 'ArrowUp':
|
case 'haut':
|
||||||
return { ligne: borner(ligne - 1, lignes - 1), colonne };
|
return { ligne: borner(ligne - 1, lignes - 1), colonne };
|
||||||
case 'ArrowDown':
|
case 'bas':
|
||||||
return { ligne: borner(ligne + 1, lignes - 1), colonne };
|
return { ligne: borner(ligne + 1, lignes - 1), colonne };
|
||||||
case 'ArrowLeft':
|
case 'gauche':
|
||||||
return { ligne, colonne: borner(colonne - 1, colonnes - 1) };
|
return { ligne, colonne: borner(colonne - 1, colonnes - 1) };
|
||||||
case 'ArrowRight':
|
case 'droite':
|
||||||
return { ligne, colonne: borner(colonne + 1, colonnes - 1) };
|
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 };
|
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 };
|
return rang === 0 ? null : { ligne: Math.floor((rang - 1) / colonnes), colonne: (rang - 1) % colonnes };
|
||||||
default:
|
default:
|
||||||
return { ligne, colonne };
|
return { ligne, colonne };
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
import { getContext, tick } from 'svelte';
|
import { getContext, tick } from 'svelte';
|
||||||
import { conversionsPossibles, pressentisNonReserves, titresNonPourvus } from '../../application/listes_travail.js';
|
import { conversionsPossibles, pressentisNonReserves, titresNonPourvus } from '../../application/listes_travail.js';
|
||||||
import Commande from '../primitives/Commande.svelte';
|
import Commande from '../primitives/Commande.svelte';
|
||||||
|
import { effetDe } from '../raccourcis.js';
|
||||||
import { nomDeCharge } from './vue_plan.js';
|
import { nomDeCharge } from './vue_plan.js';
|
||||||
|
|
||||||
const t = getContext('t');
|
const t = getContext('t');
|
||||||
|
|
@ -63,7 +64,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function surTouche(evenement) {
|
function surTouche(evenement) {
|
||||||
if (evenement.key !== 'Escape' || choix === null) return;
|
if (effetDe('plan', evenement.key) !== 'remonter' || choix === null) return;
|
||||||
evenement.preventDefault();
|
evenement.preventDefault();
|
||||||
ui.choisir(null);
|
ui.choisir(null);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
import { getContext, untrack } from 'svelte';
|
import { getContext, untrack } from 'svelte';
|
||||||
import { libellePropose } from '../../application/places.js';
|
import { libellePropose } from '../../application/places.js';
|
||||||
import Commande from '../primitives/Commande.svelte';
|
import Commande from '../primitives/Commande.svelte';
|
||||||
|
import { effetDe } from '../raccourcis.js';
|
||||||
import { commandeDeDepot } from './depot.js';
|
import { commandeDeDepot } from './depot.js';
|
||||||
import { libellePlace } from './gestes.js';
|
import { libellePlace } from './gestes.js';
|
||||||
import MenuPersonne from './MenuPersonne.svelte';
|
import MenuPersonne from './MenuPersonne.svelte';
|
||||||
|
|
@ -154,7 +155,7 @@
|
||||||
|
|
||||||
// Échap referme le panneau et rend le focus à la racine du plan.
|
// Échap referme le panneau et rend le focus à la racine du plan.
|
||||||
function surTouche(evenement) {
|
function surTouche(evenement) {
|
||||||
if (evenement.key !== 'Escape') return;
|
if (effetDe('fenetre', evenement.key) !== 'fermer') return;
|
||||||
evenement.preventDefault();
|
evenement.preventDefault();
|
||||||
const racine = evenement.currentTarget.closest('[data-section]')?.querySelector('[data-plan]') ?? null;
|
const racine = evenement.currentTarget.closest('[data-section]')?.querySelector('[data-plan]') ?? null;
|
||||||
ui.selectionner(null);
|
ui.selectionner(null);
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
import { comptesTitres } from '../../application/listes_travail.js';
|
import { comptesTitres } from '../../application/listes_travail.js';
|
||||||
import { libellePropose } from '../../application/places.js';
|
import { libellePropose } from '../../application/places.js';
|
||||||
import Commande from '../primitives/Commande.svelte';
|
import Commande from '../primitives/Commande.svelte';
|
||||||
|
import { effetDe } from '../raccourcis.js';
|
||||||
import { libellePlace } from './gestes.js';
|
import { libellePlace } from './gestes.js';
|
||||||
import { executerSurPlace, ligneListe, partiesLigne, textesLigne } from './modele.js';
|
import { executerSurPlace, ligneListe, partiesLigne, textesLigne } from './modele.js';
|
||||||
import { nomDeCharge } from './vue_plan.js';
|
import { nomDeCharge } from './vue_plan.js';
|
||||||
|
|
@ -103,7 +104,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function surTouche(evenement) {
|
function surTouche(evenement) {
|
||||||
if (evenement.key !== 'Escape') return;
|
if (effetDe('fenetre', evenement.key) !== 'fermer') return;
|
||||||
evenement.preventDefault();
|
evenement.preventDefault();
|
||||||
const racine = evenement.currentTarget.closest('[data-section]')?.querySelector('[data-plan]') ?? null;
|
const racine = evenement.currentTarget.closest('[data-section]')?.querySelector('[data-plan]') ?? null;
|
||||||
ui.selectionner(null);
|
ui.selectionner(null);
|
||||||
|
|
|
||||||
|
|
@ -872,6 +872,19 @@ describe('Échap oublie le choix en attente (§ 7.1, § 7.3)', () => {
|
||||||
await touche('Escape');
|
await touche('Escape');
|
||||||
await attendre(() => expect(m.ui.etat().choix).toBeNull());
|
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)', () => {
|
describe('un clic ne déplace pas la vue (§ 7.3, § 8.4)', () => {
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@
|
||||||
import { controlerCapacite } from '../../application/capacite.js';
|
import { controlerCapacite } from '../../application/capacite.js';
|
||||||
import { constante } from '../../geometrie/constantes.js';
|
import { constante } from '../../geometrie/constantes.js';
|
||||||
import { ajuster as vueAjustee, ecranDepuisDessin, transformDe } from '../../geometrie/vue.js';
|
import { ajuster as vueAjustee, ecranDepuisDessin, transformDe } from '../../geometrie/vue.js';
|
||||||
|
import { effetDe } from '../raccourcis.js';
|
||||||
import BandeauPlan from './BandeauPlan.svelte';
|
import BandeauPlan from './BandeauPlan.svelte';
|
||||||
import BarreVue from './BarreVue.svelte';
|
import BarreVue from './BarreVue.svelte';
|
||||||
import { toucheDuPlan, vueQuiMontre } from './clavier.js';
|
import { toucheDuPlan, vueQuiMontre } from './clavier.js';
|
||||||
|
|
@ -544,7 +545,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
function surTouche(evenement) {
|
function surTouche(evenement) {
|
||||||
if (evenement.key !== 'Escape' || etatGeste.phase === 'repos') return;
|
if (effetDe('geste', evenement.key) !== 'annuler' || etatGeste.phase === 'repos') return;
|
||||||
evenement.stopPropagation();
|
evenement.stopPropagation();
|
||||||
evenement.preventDefault();
|
evenement.preventDefault();
|
||||||
annulerGeste('echap');
|
annulerGeste('echap');
|
||||||
|
|
@ -671,7 +672,7 @@
|
||||||
if (etatGeste.phase !== 'repos' || modele === null || evenement.altKey || evenement.ctrlKey || evenement.metaKey) return;
|
if (etatGeste.phase !== 'repos' || modele === null || evenement.altKey || evenement.ctrlKey || evenement.metaKey) return;
|
||||||
auClavier = true;
|
auClavier = true;
|
||||||
const rendu = toucheDuPlan(actif, evenement.key, contexteClavier());
|
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;
|
if (rendu === null) return;
|
||||||
evenement.preventDefault();
|
evenement.preventDefault();
|
||||||
actif = rendu.actif;
|
actif = rendu.actif;
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
// Le panneau lit 'seance' et 'ui' au contexte ; etatTour, l'état des
|
// Le panneau lit 'seance' et 'ui' au contexte ; etatTour, l'état des
|
||||||
// places du tour affiché, que SectionPlan calcule, ou null sans événement.
|
// places du tour affiché, que SectionPlan calcule, ou null sans événement.
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
|
import { effetDe } from '../raccourcis.js';
|
||||||
import { nomDeCharge } from './vue_plan.js';
|
import { nomDeCharge } from './vue_plan.js';
|
||||||
|
|
||||||
let { etatTour } = $props();
|
let { etatTour } = $props();
|
||||||
|
|
@ -35,7 +36,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function surTouche(evenement) {
|
function surTouche(evenement) {
|
||||||
if (evenement.key !== 'Escape' || choix === null) return;
|
if (effetDe('plan', evenement.key) !== 'remonter' || choix === null) return;
|
||||||
evenement.preventDefault();
|
evenement.preventDefault();
|
||||||
ui.choisir(null);
|
ui.choisir(null);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@
|
||||||
// retire pas un refus : son remède, qu'un clic atteint, resterait sinon
|
// retire pas un refus : son remède, qu'un clic atteint, resterait sinon
|
||||||
// hors de portée.
|
// hors de portée.
|
||||||
import { getContext, onDestroy, untrack } from 'svelte';
|
import { getContext, onDestroy, untrack } from 'svelte';
|
||||||
|
import { effetDe } from '../raccourcis.js';
|
||||||
import { creerMachineChamp, estRefusSeance, refusSeanceCourant } from './champ.js';
|
import { creerMachineChamp, estRefusSeance, refusSeanceCourant } from './champ.js';
|
||||||
import { creerGeste } from './geste.js';
|
import { creerGeste } from './geste.js';
|
||||||
import Refus from './Refus.svelte';
|
import Refus from './Refus.svelte';
|
||||||
|
|
@ -168,8 +169,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function surTouche(evenement) {
|
function surTouche(evenement) {
|
||||||
if (evenement.key === 'Escape') abandonner(evenement.currentTarget);
|
const effet = effetDe('champ', evenement.key);
|
||||||
else if (evenement.key === 'Enter') {
|
if (effet === 'rendre') abandonner(evenement.currentTarget);
|
||||||
|
else if (effet === 'valider') {
|
||||||
evenement.preventDefault();
|
evenement.preventDefault();
|
||||||
valider();
|
valider();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
// confirmer se lit au montage : une commande qui change de confirmation se
|
// confirmer se lit au montage : une commande qui change de confirmation se
|
||||||
// remonte.
|
// remonte.
|
||||||
import { getContext, onDestroy, untrack } from 'svelte';
|
import { getContext, onDestroy, untrack } from 'svelte';
|
||||||
|
import { effetDe } from '../raccourcis.js';
|
||||||
import { DELAI_CONFIRMATION_MS, creerMachineCommande } from './commande.js';
|
import { DELAI_CONFIRMATION_MS, creerMachineCommande } from './commande.js';
|
||||||
import Refus from './Refus.svelte';
|
import Refus from './Refus.svelte';
|
||||||
|
|
||||||
|
|
@ -82,7 +83,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function surTouche(evenement) {
|
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);
|
onDestroy(desarmer);
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
// n'est pas placée, elle reste invisible. Une ancre retirée du document la
|
// 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é.
|
// referme. Elle ne capte aucun pointeur : le terme reste survolé.
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
|
import { effetDe } from '../raccourcis.js';
|
||||||
import { placerInfobulle } from './infobulle.js';
|
import { placerInfobulle } from './infobulle.js';
|
||||||
|
|
||||||
let { id, ouverte = null, fermer } = $props();
|
let { id, ouverte = null, fermer } = $props();
|
||||||
|
|
@ -60,7 +61,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
function surTouche(evenement) {
|
function surTouche(evenement) {
|
||||||
if (ouverte !== null && evenement.key === 'Escape') fermer();
|
if (ouverte !== null && effetDe('fenetre', evenement.key) === 'fermer') fermer();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@
|
||||||
|
|
||||||
// La table des raccourcis clavier de l'interface (§ 7.3, § 13.5, § 19.8) :
|
// 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
|
// 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
|
// répond à une touche la lit par effetDe, et la garde de
|
||||||
// test/sources_interface.test.js admet encore nomment leurs touches eux-mêmes,
|
// test/sources_interface.test.js relève toute touche nommée hors d'ici :
|
||||||
// et chacune de ces touches doit avoir son entrée ici, sans quoi la garde la
|
// l'interface ne répond qu'aux touches de la table, et le tableau décrit
|
||||||
// relève comme absente du tableau.
|
// chacune de ses réponses.
|
||||||
//
|
//
|
||||||
// Une entrée est { touche, contexte, effet, cle } :
|
// Une entrée est { touche, contexte, effet, cle } :
|
||||||
// touche la valeur de KeyboardEvent.key, précédée de « Maj+ » quand la
|
// touche la valeur de KeyboardEvent.key, précédée de « Maj+ » quand la
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
import Champ from '../primitives/Champ.svelte';
|
import Champ from '../primitives/Champ.svelte';
|
||||||
import Commande from '../primitives/Commande.svelte';
|
import Commande from '../primitives/Commande.svelte';
|
||||||
import Confirmation from '../primitives/Confirmation.svelte';
|
import Confirmation from '../primitives/Confirmation.svelte';
|
||||||
|
import { effetDe } from '../raccourcis.js';
|
||||||
import {
|
import {
|
||||||
annonceSuppression,
|
annonceSuppression,
|
||||||
avertissementsNommes,
|
avertissementsNommes,
|
||||||
|
|
@ -109,7 +110,7 @@
|
||||||
const arrets = [
|
const arrets = [
|
||||||
on(element, 'input', () => oublier(id)),
|
on(element, 'input', () => oublier(id)),
|
||||||
on(element, 'keydown', (evenement) => {
|
on(element, 'keydown', (evenement) => {
|
||||||
if (evenement.key === 'Escape') oublier(id);
|
if (effetDe('champ', evenement.key) === 'rendre') oublier(id);
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
return () => {
|
return () => {
|
||||||
|
|
|
||||||
|
|
@ -494,6 +494,28 @@ describe('la capacité d’une table, saisie au champ (§ 6.2)', () => {
|
||||||
expect(ecritures(m)).toBe(avant);
|
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 () => {
|
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 });
|
const donnees = chargeDe({ tables: [{ id: 1 }, { id: 2 }, { id: 3 }], n: 18 });
|
||||||
placer(
|
placer(
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ import { TABLES_APPLICATION } from '../src/application/libelles.js';
|
||||||
import { creerTraduction } from '../src/application/traduction.js';
|
import { creerTraduction } from '../src/application/traduction.js';
|
||||||
import { TABLES_INTERFACE } from '../src/interface/libelles.js';
|
import { TABLES_INTERFACE } from '../src/interface/libelles.js';
|
||||||
import { JETONS_CONFLIT } from '../src/interface/marques.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 { DESIGNATIONS } from './navigateur/designations.js';
|
||||||
import { describe, test } from './lanceur.js';
|
import { describe, test } from './lanceur.js';
|
||||||
|
|
||||||
|
|
@ -1079,98 +1078,21 @@ function valeurDeChaine(noeud) {
|
||||||
return null;
|
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
|
// 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
|
// 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 —
|
// 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
|
// 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 "…" ».
|
// 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.
|
// Aucun fichier n'est admis : le code répond à une touche par effetDe.
|
||||||
function releverTouches(racine, enAttenteDe = TOUCHES_EN_ATTENTE) {
|
function releverTouches(racine) {
|
||||||
const chemins = exigerNonVide(
|
const chemins = exigerNonVide(
|
||||||
sources(racine, ['src/interface'], ['.svelte', '.js']).filter((chemin) => chemin !== TABLE_RACCOURCIS),
|
sources(racine, ['src/interface'], ['.svelte', '.js']).filter((chemin) => chemin !== TABLE_RACCOURCIS),
|
||||||
'touches nommées',
|
'touches nommées',
|
||||||
);
|
);
|
||||||
const parFichier = new Map();
|
const releves = [];
|
||||||
for (const chemin of chemins) {
|
for (const chemin of chemins) {
|
||||||
const composant = chemin.endsWith('.svelte');
|
const composant = chemin.endsWith('.svelte');
|
||||||
const { texte, ast } = composant ? analyserComposant(racine, chemin) : analyserModule(racine, chemin);
|
const { texte, ast } = composant ? analyserComposant(racine, chemin) : analyserModule(racine, chemin);
|
||||||
const releves = [];
|
|
||||||
const vus = new Set();
|
const vus = new Set();
|
||||||
const relever = (noeud) => {
|
const relever = (noeud) => {
|
||||||
const valeur = valeurDeChaine(noeud);
|
const valeur = valeurDeChaine(noeud);
|
||||||
|
|
@ -1188,19 +1110,8 @@ function releverTouches(racine, enAttenteDe = TOUCHES_EN_ATTENTE) {
|
||||||
relever(noeud);
|
relever(noeud);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (releves.length > 0) parFichier.set(chemin, releves);
|
|
||||||
}
|
}
|
||||||
const releves = [];
|
return trier(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()];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Arbres d'épreuve ---------------------------------------------------------
|
// --- 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)', () => {
|
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');
|
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/interface/A.test.js': "if (e.key === 'Escape') f();\n",
|
||||||
'src/application/c.js': "export const k = (e) => e.key === 'Escape';\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:2 touche "Enter"',
|
||||||
'src/interface/A.svelte:4 touche "Tab"',
|
'src/interface/A.svelte:4 touche "Tab"',
|
||||||
'src/interface/plan/sous/b.js:2 touche "Escape"',
|
'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', () => {
|
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(() => avecArbre({ 'src/application/a.js': '' }, releverTouches), /touches nommées : aucun fichier examiné/);
|
||||||
assert.throws(
|
assert.throws(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue