Evolution incrementale : aucun changement d'architecture, aucune fonctionnalite retiree, compatibilite .mmap v1 et IndexedDB preservee. Modele (format v2) - MindMapNode gagne `layout`, `iconStyle` et `branch`, tous optionnels. - NodeStyle gagne fontFamily, verticalAlign, lineHeight et shadow. - MapSettings gagne layoutSpacing, snapToGrid, snapGrid et alignmentGuides. - Un champ absent signifie "herite du theme" : rien n'est ecrit dans le document pour materialiser un defaut, donc les cartes deja stockees restent lisibles telles quelles. Migration v1 -> v2 : style.width devient layout.width, toujours lu en repli par le resolveur. Mise en page - Nouveau resolveur de style qui fusionne theme et personnalisations. - Taille automatique fidele au contenu, taille personnalisee bornee, redimensionnement a la souris via NodeResizer. - Padding horizontal et vertical, retour a la ligne, largeur maximale du texte, interligne, police, alignement horizontal et vertical, ombre. - Icones : emojis conserves, bibliotheque Lucide ajoutee, position, taille et espacement configurables. Multi-selection - Barre contextuelle d'alignement (6 axes) et de distribution (2 axes). - Application d'un reglage a toute la selection, avec etat "Mixte". Style - Copier / coller / reinitialiser le style, au clavier et par clic droit. Auto-layout - Espacement parent/enfant et entre branches configurable, presets Compact, Normal et Aere. Le layout reste declenche uniquement a la demande. Historique - Une seule entree par redimensionnement complet, par alignement, par distribution, par application multiple et par collage de style. Panneau Proprietes - Reorganise en cinq sections repliables dont l'etat est conserve pendant la session. Vraies icones d'alignement, tooltips et aria-labels partout. Corrections - Largeur des noeuds : word-break et overflow-wrap:anywhere ramenaient la largeur min-content a un caractere, ce qui faisait s'effondrer tous les noeuds automatiques. - Largeur auto en max-content bornee, pour qu'une phrase longue occupe la largeur maximale au lieu de former une colonne etroite. Tests - 85 tests vitest (43 -> 85) et 31 tests pytest (25 -> 31). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
188 lines
5.6 KiB
TypeScript
188 lines
5.6 KiB
TypeScript
import { beforeEach, describe, expect, it } from 'vitest';
|
|
import { createEmptyMap } from '@/services/mapFactory';
|
|
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);
|
|
}
|
|
|
|
/** Cree trois noeuds a des positions connues, avec des tailles mesurees. */
|
|
function threeNodes(): string[] {
|
|
const ids = [rootId()];
|
|
for (let index = 0; index < 2; index += 1) {
|
|
ids.push(store().addChild(rootId()) as string);
|
|
}
|
|
store().setEditingNode(null);
|
|
|
|
const positions = [
|
|
{ x: 0, y: 0 },
|
|
{ x: 60, y: 200 },
|
|
{ x: 300, y: 500 },
|
|
];
|
|
ids.forEach((id, index) => {
|
|
store().moveNodes([{ id, position: positions[index] as { x: number; y: number } }]);
|
|
store().setMeasuredSize(id, { width: 100, height: 40 });
|
|
});
|
|
return ids;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
useEditorStore.getState().loadMap(createEmptyMap('Mise en page'));
|
|
});
|
|
|
|
describe('redimensionnement', () => {
|
|
it('passe le noeud en taille fixe et conserve les dimensions', () => {
|
|
const id = rootId();
|
|
store().beginResize();
|
|
store().resizeNode(id, { width: 260, height: 120 });
|
|
|
|
const node = nodeById(id);
|
|
expect(node?.layout?.widthMode).toBe('fixed');
|
|
expect(node?.layout?.heightMode).toBe('fixed');
|
|
expect(node?.layout?.width).toBe(260);
|
|
expect(node?.layout?.height).toBe(120);
|
|
});
|
|
|
|
it('borne les dimensions saisies', () => {
|
|
const id = rootId();
|
|
store().beginResize();
|
|
store().resizeNode(id, { width: -50, height: 100000 });
|
|
|
|
expect(nodeById(id)?.layout?.width).toBeGreaterThanOrEqual(60);
|
|
expect(nodeById(id)?.layout?.height).toBeLessThanOrEqual(900);
|
|
});
|
|
|
|
it('un resize continu ne cree qu une seule entree d historique', () => {
|
|
const id = rootId();
|
|
const before = store().past.length;
|
|
|
|
store().beginResize();
|
|
for (let width = 200; width <= 260; width += 10) {
|
|
store().resizeNode(id, { width, height: 80 });
|
|
}
|
|
|
|
expect(store().past.length).toBe(before + 1);
|
|
|
|
store().undo();
|
|
expect(nodeById(id)?.layout?.width).toBeUndefined();
|
|
});
|
|
|
|
it('met a jour la position quand la poignee haut-gauche est utilisee', () => {
|
|
const id = rootId();
|
|
store().beginResize();
|
|
store().resizeNode(id, { width: 200, height: 90, position: { x: -40, y: -20 } });
|
|
|
|
expect(nodeById(id)?.position).toEqual({ x: -40, y: -20 });
|
|
});
|
|
|
|
it('repasse en taille automatique avec "adapter au contenu"', () => {
|
|
const id = rootId();
|
|
store().beginResize();
|
|
store().resizeNode(id, { width: 300, height: 150 });
|
|
store().fitNodesToContent([id]);
|
|
|
|
const node = nodeById(id);
|
|
expect(node?.layout?.width).toBeUndefined();
|
|
expect(node?.layout?.height).toBeUndefined();
|
|
expect(node?.layout?.widthMode).toBe('auto');
|
|
});
|
|
});
|
|
|
|
describe('alignement multi-selection', () => {
|
|
it('aligne les bords gauches en une seule entree d historique', () => {
|
|
const ids = threeNodes();
|
|
store().setSelection(ids);
|
|
const before = store().past.length;
|
|
|
|
store().alignNodes('left');
|
|
|
|
expect(store().past.length).toBe(before + 1);
|
|
const xs = ids.map((id) => nodeById(id)?.position.x);
|
|
expect(new Set(xs).size).toBe(1);
|
|
expect(xs[0]).toBe(0);
|
|
});
|
|
|
|
it('aligne les bords droits en tenant compte de la largeur', () => {
|
|
const ids = threeNodes();
|
|
store().setMeasuredSize(ids[1] as string, { width: 200, height: 40 });
|
|
store().setSelection(ids);
|
|
|
|
store().alignNodes('right');
|
|
|
|
const rights = ids.map((id) => {
|
|
const node = nodeById(id);
|
|
const width = id === ids[1] ? 200 : 100;
|
|
return (node?.position.x ?? 0) + width;
|
|
});
|
|
expect(new Set(rights).size).toBe(1);
|
|
});
|
|
|
|
it('centre verticalement la selection', () => {
|
|
const ids = threeNodes();
|
|
store().setSelection(ids);
|
|
|
|
store().alignNodes('vcenter');
|
|
|
|
const centers = ids.map((id) => (nodeById(id)?.position.y ?? 0) + 20);
|
|
expect(new Set(centers).size).toBe(1);
|
|
});
|
|
|
|
it('ne fait rien avec un seul noeud selectionne', () => {
|
|
const ids = threeNodes();
|
|
store().setSelection([ids[0] as string]);
|
|
const before = store().past.length;
|
|
|
|
store().alignNodes('left');
|
|
expect(store().past.length).toBe(before);
|
|
});
|
|
});
|
|
|
|
describe('distribution', () => {
|
|
it('repartit les espaces verticaux en conservant les extremes', () => {
|
|
const ids = threeNodes();
|
|
store().setSelection(ids);
|
|
const before = store().past.length;
|
|
|
|
store().distributeNodes('vertical');
|
|
|
|
expect(store().past.length).toBe(before + 1);
|
|
const ys = ids.map((id) => nodeById(id)?.position.y ?? 0).sort((a, b) => a - b);
|
|
expect(ys[0]).toBe(0);
|
|
expect(ys[2]).toBe(500);
|
|
// Espaces egaux entre les bords : (500 + 40 - 0 - 120) / 2 = 210
|
|
expect((ys[1] as number) - ((ys[0] as number) + 40)).toBeCloseTo(210, 0);
|
|
});
|
|
|
|
it('repartit les espaces horizontaux', () => {
|
|
const ids = threeNodes();
|
|
store().setSelection(ids);
|
|
|
|
store().distributeNodes('horizontal');
|
|
|
|
const xs = ids.map((id) => nodeById(id)?.position.x ?? 0).sort((a, b) => a - b);
|
|
expect(xs[0]).toBe(0);
|
|
expect(xs[2]).toBe(300);
|
|
expect((xs[1] as number) - ((xs[0] as number) + 100)).toBeCloseTo(
|
|
(xs[2] as number) - ((xs[1] as number) + 100),
|
|
0,
|
|
);
|
|
});
|
|
|
|
it('exige au moins trois noeuds', () => {
|
|
const ids = threeNodes();
|
|
store().setSelection([ids[0] as string, ids[1] as string]);
|
|
const before = store().past.length;
|
|
|
|
store().distributeNodes('horizontal');
|
|
expect(store().past.length).toBe(before);
|
|
});
|
|
});
|