Tags sur les projets, en version 3 du format
Un projet porte désormais une liste d'étiquettes libres, destinées à filtrer la frise. Il n'y a pas de référentiel à tenir : les tags disponibles sont ceux que portent les projets. normaliserTags() nettoie la liste à l'enregistrement — espaces réduits, vides écartés, doublons fusionnés à la casse près, tri alphabétique pour qu'une ressaisie dans un autre ordre ne produise aucun diff. Les accents, eux, comptent : « éditeur » et « editeur » restent deux tags distincts, contrairement aux identifiants qui doivent tenir dans une URL. Un tag trop long est tronqué plutôt que refusé : sa longueur est cosmétique, et bloquer le chargement d'un planning entier pour un libellé bavard serait disproportionné. La validation ne vérifie donc que le type — un tag mal typé serait un tag qu'on croit poser et qui ne filtre rien. teinteTag() dérive une teinte HSL du nom par hachage : la couleur d'un tag n'est ni stockée ni saisie, et reste la même partout où il apparaît. Le format passe en version 3. Un fichier en version 2 se charge sans rien demander, ses projets recevant une liste vide, et il est réécrit en version 3 à la première sauvegarde. Le bump empêche une version antérieure de l'outil d'effacer silencieusement les tags en réécrivant le fichier. Dix tests couvrent la normalisation, la comparaison, la teinte et le filtre. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,11 +3,14 @@ import assert from 'node:assert/strict';
|
||||
|
||||
import {
|
||||
ErreurValidation,
|
||||
MAX_LONGUEUR_TAG,
|
||||
VERSION_FORMAT,
|
||||
ajouterJours,
|
||||
ajouterPhase,
|
||||
bornesPlanning,
|
||||
bornesProjet,
|
||||
bornesReference,
|
||||
cleTag,
|
||||
creerPhase,
|
||||
creerProjet,
|
||||
dateValide,
|
||||
@@ -22,11 +25,15 @@ import {
|
||||
libererReference,
|
||||
lundiDeLaSemaine,
|
||||
modifierPhase,
|
||||
normaliserTags,
|
||||
planningVide,
|
||||
pourEcriture,
|
||||
projetFiltre,
|
||||
redimensionnerPhase,
|
||||
remplacerPhase,
|
||||
semaineISO,
|
||||
teinteTag,
|
||||
tousLesTags,
|
||||
validerPlanning,
|
||||
} from '../js/model.js';
|
||||
|
||||
@@ -50,6 +57,7 @@ function projet(champs = {}) {
|
||||
id: 'site-web',
|
||||
name: 'Site web',
|
||||
color: '#3b82f6',
|
||||
tags: [],
|
||||
collapsed: false,
|
||||
hidden: false,
|
||||
phases: [],
|
||||
@@ -168,7 +176,11 @@ describe('validerPlanning', () => {
|
||||
projects: [projet({ phases: [phase()] })],
|
||||
});
|
||||
|
||||
assert.equal(resultat.version, 2);
|
||||
// Un fichier plus ancien est relu et remonté à la version courante : c'est
|
||||
// ce qui permet à un planning écrit avant les tags de s'ouvrir sans rien
|
||||
// demander, ses projets recevant simplement une liste de tags vide.
|
||||
assert.equal(resultat.version, VERSION_FORMAT);
|
||||
assert.deepEqual(resultat.projects[0].tags, []);
|
||||
assert.equal(resultat.projects.length, 1);
|
||||
assert.equal(resultat.projects[0].phases[0].id, 'cadrage');
|
||||
});
|
||||
@@ -283,6 +295,85 @@ describe('validerPlanning', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// --- tags -------------------------------------------------------------------
|
||||
|
||||
describe('tags', () => {
|
||||
test('normaliserTags nettoie, dédoublonne et trie', () => {
|
||||
assert.deepEqual(
|
||||
normaliserTags([' web ', 'client', '', ' ', 'Client', 'a\t b']),
|
||||
['a b', 'client', 'web']
|
||||
);
|
||||
});
|
||||
|
||||
test('normaliserTags garde la première graphie rencontrée', () => {
|
||||
assert.deepEqual(normaliserTags(['Client', 'client', 'CLIENT']), ['Client']);
|
||||
});
|
||||
|
||||
test('normaliserTags tronque un tag trop bavard plutôt que de le refuser', () => {
|
||||
const [tag] = normaliserTags(['x'.repeat(MAX_LONGUEUR_TAG + 10)]);
|
||||
assert.equal(tag.length, MAX_LONGUEUR_TAG);
|
||||
});
|
||||
|
||||
test('cleTag ignore la casse et les espaces, mais pas les accents', () => {
|
||||
assert.equal(cleTag(' Sous-Traitance '), cleTag('sous-traitance'));
|
||||
assert.notEqual(cleTag('éditeur'), cleTag('editeur'));
|
||||
});
|
||||
|
||||
test('teinteTag est déterministe et reste dans la roue chromatique', () => {
|
||||
assert.equal(teinteTag('client'), teinteTag('Client'));
|
||||
for (const tag of ['client', 'web', 'interne', 'R&D', 'éditeur']) {
|
||||
const teinte = teinteTag(tag);
|
||||
assert.ok(teinte >= 0 && teinte < 360, `${tag} → ${teinte}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('tousLesTags rassemble ceux du planning, sans doublon', () => {
|
||||
const planning = {
|
||||
projects: [
|
||||
projet({ id: 'a', tags: ['web', 'client'] }),
|
||||
projet({ id: 'b', tags: ['Client', 'interne'] }),
|
||||
projet({ id: 'c' }),
|
||||
],
|
||||
};
|
||||
assert.deepEqual(tousLesTags(planning), ['client', 'interne', 'web']);
|
||||
});
|
||||
|
||||
test('projetFiltre exige tous les tags cochés, pas seulement un', () => {
|
||||
const p = projet({ tags: ['client', 'urgent', 'web'] });
|
||||
|
||||
assert.equal(projetFiltre(p, []), true);
|
||||
assert.equal(projetFiltre(p, ['client']), true);
|
||||
assert.equal(projetFiltre(p, ['client', 'urgent']), true);
|
||||
assert.equal(projetFiltre(p, ['client', 'interne']), false);
|
||||
assert.equal(projetFiltre(projet({ tags: [] }), ['client']), false);
|
||||
});
|
||||
|
||||
test('projetFiltre compare à la casse près', () => {
|
||||
assert.equal(projetFiltre(projet({ tags: ['Client'] }), ['client']), true);
|
||||
});
|
||||
|
||||
test('validerPlanning normalise les tags et rejette ceux qui ne sont pas des chaînes', () => {
|
||||
const resultat = validerPlanning({
|
||||
projects: [projet({ tags: ['web ', 'Client', 'client'] })],
|
||||
});
|
||||
assert.deepEqual(resultat.projects[0].tags, ['Client', 'web']);
|
||||
|
||||
assert.throws(
|
||||
() => validerPlanning({ projects: [projet({ tags: 'client' })] }),
|
||||
(err) => err instanceof ErreurValidation && /tags/.test(err.message)
|
||||
);
|
||||
assert.throws(
|
||||
() => validerPlanning({ projects: [projet({ tags: [42] })] }),
|
||||
(err) => err instanceof ErreurValidation && /Site web/.test(err.message)
|
||||
);
|
||||
});
|
||||
|
||||
test('creerProjet accepte des tags et les normalise', () => {
|
||||
const p = creerProjet(planningVide(), 'Site web', [' web', 'Client', 'client']);
|
||||
assert.deepEqual(p.tags, ['Client', 'web']);
|
||||
});
|
||||
});
|
||||
|
||||
// --- bornes -----------------------------------------------------------------
|
||||
|
||||
describe('bornes', () => {
|
||||
|
||||
Reference in New Issue
Block a user