gestion_table_tournante_libre/src/interface/raccourcis.test.js
Mathieu Benoit 9d4f203ead [ADD] interface: single sources for settings, shortcuts and CSV columns
The generated documentation must list exactly what the screen shows. The
generation settings, the keyboard shortcuts and the CSV columns each live in
one table that both the interface and the documentation read; the target
field of the proposals section reads its entry too. A guard reports any key
named in the interface without its shortcut entry. The LISTE_SUR_VOISINE
warning now shows its remedy, moving the covered neighbour.
Checked: 3116 node and 643 browser tests green from the index alone.

--- FR ---

[ADD] interface : sources uniques des réglages, raccourcis, colonnes CSV

La documentation engendrée doit lister exactement ce que l'écran montre.
Les réglages de la génération, les raccourcis clavier et les colonnes du
CSV vivent chacun dans une table que lisent l'interface et la
documentation ; le champ de la cible des propositions lit aussi la sienne.
Une garde relève toute touche nommée dans l'interface sans son entrée de
raccourci. L'avertissement LISTE_SUR_VOISINE montre désormais son remède,
le déplacement de la voisine couverte.
Vérifié : 3116 node et 643 navigateur verts depuis l'index seul.

Assisted-by: Claude Opus 5.5
2026-10-09 04:51:45 -04:00

119 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Épreuves de la table des raccourcis clavier (§ 7.3, § 13.5, § 19.8) : sa
// forme, figée ; une touche par contexte au plus une fois ; les contextes et
// les effets que le plan, la grille des participants, un champ, une
// commande, un geste et une fenêtre lisent ; et effetDe, qui rend l'effet
// d'une touche dans un contexte ; le libellé de chaque clé, que le tableau
// engendré lit par la traduction.
import assert from 'node:assert/strict';
import { describe, test } from '../../test/lanceur.js';
import { TABLES_APPLICATION } from '../application/libelles.js';
import { creerTraduction } from '../application/traduction.js';
import { TABLES_INTERFACE } from './libelles.js';
import { CONTEXTES_RACCOURCIS, RACCOURCIS, effetDe } from './raccourcis.js';
const { t, cles: clesTraduites } = creerTraduction([...TABLES_APPLICATION, ...TABLES_INTERFACE]);
describe('RACCOURCIS : la table, seule source des touches (§ 19.8)', () => {
test('chaque entrée porte touche, contexte, effet et cle, dans cet ordre, et la table comme chaque entrée sont figées', () => {
assert.ok(RACCOURCIS.length > 0, 'table vide');
assert.ok(Object.isFrozen(RACCOURCIS));
for (const entree of RACCOURCIS) {
assert.deepEqual(Object.keys(entree), ['touche', 'contexte', 'effet', 'cle']);
assert.ok(Object.isFrozen(entree), JSON.stringify(entree));
assert.equal(typeof entree.touche, 'string');
assert.ok(entree.touche.length > 0);
assert.match(entree.effet, /^[a-z][A-Za-z]*$/);
}
assert.ok(Object.isFrozen(CONTEXTES_RACCOURCIS));
assert.throws(() => RACCOURCIS.push({}), TypeError);
});
test('les six contextes, dans l’ordre du tableau ; chaque entrée en porte un, et chacun a ses entrées, groupées', () => {
assert.deepEqual(CONTEXTES_RACCOURCIS, ['plan', 'grille', 'champ', 'commande', 'geste', 'fenetre']);
const ordre = RACCOURCIS.map(({ contexte }) => CONTEXTES_RACCOURCIS.indexOf(contexte));
assert.ok(!ordre.includes(-1), 'contexte hors de la liste');
assert.deepEqual(ordre, [...ordre].sort((a, b) => a - b));
assert.deepEqual([...new Set(ordre)], [0, 1, 2, 3, 4, 5]);
});
test('une touche au plus une fois par contexte : son effet n’est jamais ambigu', () => {
const paires = RACCOURCIS.map(({ contexte, touche }) => `${contexte}:${touche}`);
assert.equal(new Set(paires).size, paires.length);
});
test('la clé du libellé est raccourcis.<contexte>.<effet> : deux touches d’un même effet la partagent', () => {
for (const { contexte, effet, cle } of RACCOURCIS) assert.equal(cle, `raccourcis.${contexte}.${effet}`);
const partagees = (contexte, effet) => RACCOURCIS.filter((entree) => entree.contexte === contexte && entree.effet === effet).map(({ touche }) => touche);
assert.deepEqual(partagees('plan', 'entrer'), ['Enter', ' ']);
assert.deepEqual(partagees('grille', 'editer'), ['Enter', 'F2']);
assert.deepEqual(partagees('commande', 'activer'), ['Enter', ' ']);
});
test('chaque clé a son libellé dans les tables de l’application et de l’interface', () => {
const cles = [...new Set(RACCOURCIS.map(({ cle }) => cle))];
const traduites = new Set(clesTraduites());
assert.deepEqual(cles.filter((cle) => !traduites.has(cle)), []);
for (const cle of cles) assert.ok(t(cle).length > 0, cle);
});
test('les raccourcis du plan, de la grille, d’un champ, d’une commande, d’un geste et d’une fenêtre (§ 7.3)', () => {
assert.deepEqual(
RACCOURCIS.map(({ touche, contexte, effet }) => [contexte, touche, effet]),
[
['plan', 'ArrowRight', 'droite'],
['plan', 'ArrowDown', 'bas'],
['plan', 'ArrowLeft', 'gauche'],
['plan', 'ArrowUp', 'haut'],
['plan', 'Enter', 'entrer'],
['plan', ' ', 'entrer'],
['plan', 'Escape', 'remonter'],
['grille', 'ArrowRight', 'droite'],
['grille', 'ArrowDown', 'bas'],
['grille', 'ArrowLeft', 'gauche'],
['grille', 'ArrowUp', 'haut'],
['grille', 'Tab', 'suivante'],
['grille', 'Maj+Tab', 'precedente'],
['grille', 'Enter', 'editer'],
['grille', 'F2', 'editer'],
['grille', 'Escape', 'effacer'],
['champ', 'Enter', 'valider'],
['champ', 'Escape', 'rendre'],
['commande', 'Enter', 'activer'],
['commande', ' ', 'activer'],
['geste', 'Escape', 'annuler'],
['fenetre', 'Escape', 'fermer'],
],
);
});
});
describe('effetDe : l’effet d’une touche dans un contexte', () => {
test('chaque entrée se retrouve par son contexte et sa touche', () => {
for (const { contexte, touche, effet } of RACCOURCIS) assert.equal(effetDe(contexte, touche), effet);
});
test('une même touche agit selon son contexte : Échap remonte, efface, rend, annule ou ferme ; une commande ne la prend pas', () => {
assert.deepEqual(
CONTEXTES_RACCOURCIS.map((contexte) => effetDe(contexte, 'Escape')),
['remonter', 'effacer', 'rendre', null, 'annuler', 'fermer'],
);
});
test('une touche que le contexte ne prend pas : null, majuscule et code physique compris', () => {
assert.equal(effetDe('champ', ' '), null);
assert.equal(effetDe('plan', 'a'), null);
assert.equal(effetDe('plan', 'escape'), null);
assert.equal(effetDe('plan', 'Space'), null);
assert.equal(effetDe('geste', 'Enter'), null);
assert.equal(effetDe('grille', 'Shift+Tab'), null);
assert.equal(effetDe('plan', 'Tab'), null);
});
test('un contexte inconnu lève, et nomme le contexte', () => {
assert.throws(() => effetDe('tiroir', 'Escape'), { name: 'RangeError', message: /"tiroir"/ });
assert.throws(() => effetDe('toString', 'Escape'), RangeError);
});
});