Files
openmind/frontend/src/stores/editorCircle.test.ts
snoopy a23a68629d Phase 1.1.1 : consolidation et coherence UX
Trois corrections ciblees, sans changement d'architecture ni d'interface.

1. Le style ne transporte plus l'icone
L'icone d'un noeud est du contenu, comme son texte ou sa note. Copier le style
de "Reseau" et le coller sur "Stockage" et "Securite" applique desormais les
couleurs, la forme et la mise en page en laissant chaque noeud avec sa propre
icone. Seule la presentation de l'icone (position, taille, espacement) reste
une propriete de style.

2. Le cercle conserve un rapport 1:1
`keepAspectRatio` sur le redimensionneur, et contrainte cote store pour les
trois autres chemins : redimensionnement direct, saisie d'une largeur ou d'une
hauteur dans le panneau, et conversion d'un noeud rectangulaire en cercle.
Un cercle ne peut plus devenir un ovale.

3. Point d'entree unique des documents
`normalizeMapDocument()` remplace `parseMapDocument()` et devient le seul
chemin par lequel une carte entre dans l'editeur, qu'elle vienne d'un fichier
.mmap, d'IndexedDB ou d'un partage. Le pipeline est toujours validation ->
migration -> normalisation. Les cartes locales passaient jusqu'ici a cote de la
migration : elles y passent maintenant a l'ouverture, sans etre reecrites en
base tant que l'utilisateur ne les modifie pas. Une carte locale videe de ses
noeuds reste ouvrable, et le message d'erreur s'adapte a la provenance.

Egalement
- Zoom minimum abaisse de 0.1 a 0.02 : "Adapter a l'ecran" butait sur ce
  plancher et ne montrait pas toute une carte de 181 noeuds.
- README : pipeline de normalisation, comportement du style et du cercle,
  limitations mesurees.

Tests : 103 vitest (85 -> 103) et 31 pytest, ruff, eslint et tsc au vert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 05:48:22 +02:00

127 lines
3.8 KiB
TypeScript

import { beforeEach, describe, expect, it } from 'vitest';
import { createEmptyMap } from '@/services/mapFactory';
import { getTheme } from '@/features/editor/themes';
import { resolveNodeAppearance } from '@/features/editor/nodeStyleResolver';
import { estimateNodeSize } from '@/utils/nodeSize';
import { useEditorStore } from '@/stores/editorStore';
function store() {
return useEditorStore.getState();
}
function rootId(): string {
return store().map?.nodes[0]?.id as string;
}
function nodeById(id: string) {
return store().map?.nodes.find((node) => node.id === id);
}
function layoutOf(id: string) {
return nodeById(id)?.layout;
}
/** Cree un noeud circulaire et renvoie son identifiant. */
function circleNode(): string {
const id = store().addChild(rootId()) as string;
store().setEditingNode(null);
store().updateNodeText(id, 'Coeur de reseau');
store().updateNodesStyle([id], { shape: 'circle' });
return id;
}
beforeEach(() => {
useEditorStore.getState().loadMap(createEmptyMap('Cercles'));
});
describe('noeud circulaire', () => {
it('reste carre en dimensionnement automatique', () => {
const id = circleNode();
const node = nodeById(id);
const size = estimateNodeSize(node!, getTheme('default'));
expect(size.width).toBe(size.height);
expect(resolveNodeAppearance(node!, 1, getTheme('default')).circle).toBe(true);
});
it('reste carre pendant un redimensionnement a la souris', () => {
const id = circleNode();
store().beginResize();
for (const [width, height] of [
[100, 100],
[150, 148],
[220, 90],
] as Array<[number, number]>) {
store().resizeNode(id, { width, height });
expect(layoutOf(id)?.width).toBe(layoutOf(id)?.height);
}
// Le cote retenu est le plus grand des deux, jamais un ovale.
expect(layoutOf(id)?.width).toBe(220);
expect(layoutOf(id)?.height).toBe(220);
});
it('fait suivre la hauteur quand la largeur change', () => {
const id = circleNode();
store().updateNodesLayout([id], { widthMode: 'fixed', width: 180 });
expect(layoutOf(id)?.width).toBe(180);
expect(layoutOf(id)?.height).toBe(180);
expect(layoutOf(id)?.heightMode).toBe('fixed');
});
it('fait suivre la largeur quand la hauteur change', () => {
const id = circleNode();
store().updateNodesLayout([id], { heightMode: 'fixed', height: 260 });
expect(layoutOf(id)?.width).toBe(260);
expect(layoutOf(id)?.height).toBe(260);
expect(layoutOf(id)?.widthMode).toBe('fixed');
});
it('carre un noeud rectangulaire converti en cercle', () => {
const id = store().addChild(rootId()) as string;
store().setEditingNode(null);
store().updateNodesLayout([id], {
widthMode: 'fixed',
width: 320,
heightMode: 'fixed',
height: 90,
});
store().updateNodesStyle([id], { shape: 'circle' });
expect(layoutOf(id)?.width).toBe(320);
expect(layoutOf(id)?.height).toBe(320);
});
it('annule tout un redimensionnement circulaire en une seule fois', () => {
const id = circleNode();
const before = store().past.length;
store().beginResize();
for (let side = 120; side <= 200; side += 20) {
store().resizeNode(id, { width: side, height: side });
}
expect(store().past.length).toBe(before + 1);
store().undo();
expect(layoutOf(id)?.width).toBeUndefined();
expect(layoutOf(id)?.height).toBeUndefined();
expect(nodeById(id)?.style?.shape).toBe('circle');
});
it('laisse les autres formes libres de leurs proportions', () => {
const id = store().addChild(rootId()) as string;
store().setEditingNode(null);
store().updateNodesStyle([id], { shape: 'rounded' });
store().beginResize();
store().resizeNode(id, { width: 300, height: 80 });
expect(layoutOf(id)?.width).toBe(300);
expect(layoutOf(id)?.height).toBe(80);
});
});