1+ // @vitest -environment jsdom
2+ // tests/unit/useAutoLayout.spec.ts
3+ import { describe , it , expect , vi , beforeEach } from 'vitest' ;
4+ import { renderHook , act } from '@testing-library/react' ;
5+ import { useAutoLayout } from '../../src/core/hooks/useAutoLayout' ;
6+ import type { Node , Edge } from 'reactflow' ;
7+
8+ /* Isolate the mathematical dependency.
9+ * We mock Dagre using an actual ES6 class so the `new` keyword works perfectly.
10+ */
11+ const mockSetGraph = vi . fn ( ) ;
12+
13+ vi . mock ( 'dagre' , ( ) => {
14+ class MockGraph {
15+ setDefaultEdgeLabel = vi . fn ( ) ;
16+ setGraph = mockSetGraph ;
17+ setNode = vi . fn ( ) ;
18+ setEdge = vi . fn ( ) ;
19+ node = vi . fn ( ( ) => ( { x : 500 , y : 500 } ) ) ; // Force predictable output
20+ }
21+
22+ return {
23+ default : {
24+ graphlib : { Graph : MockGraph } ,
25+ layout : vi . fn ( )
26+ } ,
27+ // Flat exports catch edge cases where the test runner drops the 'default' wrapper
28+ graphlib : { Graph : MockGraph } ,
29+ layout : vi . fn ( )
30+ } ;
31+ } ) ;
32+
33+ describe ( 'useAutoLayout Hook' , ( ) => {
34+
35+ const mockSetNodes = vi . fn ( ) ;
36+ const mockTakeSnapshot = vi . fn ( ) ;
37+
38+ const mockNodes : Node [ ] = [
39+ { id : 'n1' , position : { x : 0 , y : 0 } , data : { } } ,
40+ { id : 'n2' , position : { x : - 100 , y : - 200 } , data : { } }
41+ ] ;
42+
43+ const mockEdges : Edge [ ] = [
44+ { id : 'e1' , source : 'n1' , target : 'n2' }
45+ ] ;
46+
47+ beforeEach ( ( ) => {
48+ vi . clearAllMocks ( ) ;
49+ } ) ;
50+
51+ it ( 'triggers a history snapshot prior to modifying node coordinates' , ( ) => {
52+ const { result } = renderHook ( ( ) => useAutoLayout ( {
53+ nodes : mockNodes ,
54+ edges : mockEdges ,
55+ setNodes : mockSetNodes ,
56+ takeSnapshot : mockTakeSnapshot
57+ } ) ) ;
58+
59+ act ( ( ) => {
60+ result . current . autoLayout ( ) ;
61+ } ) ;
62+
63+ /* The history snapshot MUST fire before the nodes are updated */
64+ expect ( mockTakeSnapshot ) . toHaveBeenCalledTimes ( 1 ) ;
65+ expect ( mockSetNodes ) . toHaveBeenCalledTimes ( 1 ) ;
66+
67+ /* Validates execution order: Snapshot -> Update */
68+ const snapshotOrder = mockTakeSnapshot . mock . invocationCallOrder [ 0 ] ;
69+ const updateOrder = mockSetNodes . mock . invocationCallOrder [ 0 ] ;
70+ expect ( snapshotOrder ) . toBeLessThan ( updateOrder ) ;
71+ } ) ;
72+
73+ it ( 'translates Dagre center coordinates to React Flow top-left coordinates' , ( ) => {
74+ const { result } = renderHook ( ( ) => useAutoLayout ( {
75+ nodes : mockNodes ,
76+ edges : mockEdges ,
77+ setNodes : mockSetNodes ,
78+ takeSnapshot : mockTakeSnapshot
79+ } ) ) ;
80+
81+ act ( ( ) => {
82+ result . current . autoLayout ( ) ;
83+ } ) ;
84+
85+ const appliedNodes = mockSetNodes . mock . calls [ 0 ] [ 0 ] ;
86+
87+ /* Expected offset mathematics:
88+ * Dagre Center: x: 500, y: 500
89+ * Width (220) / 2 = 110. 500 - 110 = 390
90+ * Height (150) / 2 = 75. 500 - 75 = 425
91+ */
92+ expect ( appliedNodes [ 0 ] . position . x ) . toBe ( 390 ) ;
93+ expect ( appliedNodes [ 0 ] . position . y ) . toBe ( 425 ) ;
94+ expect ( appliedNodes [ 1 ] . position . x ) . toBe ( 390 ) ;
95+ expect ( appliedNodes [ 1 ] . position . y ) . toBe ( 425 ) ;
96+ } ) ;
97+
98+ it ( 'respects layout direction overrides' , ( ) => {
99+ const { result } = renderHook ( ( ) => useAutoLayout ( {
100+ nodes : mockNodes ,
101+ edges : mockEdges ,
102+ setNodes : mockSetNodes ,
103+ takeSnapshot : mockTakeSnapshot
104+ } ) ) ;
105+
106+ act ( ( ) => {
107+ result . current . autoLayout ( 'TB' ) ; // Top-to-Bottom
108+ } ) ;
109+
110+ expect ( mockSetGraph ) . toHaveBeenCalledWith (
111+ expect . objectContaining ( { rankdir : 'TB' } )
112+ ) ;
113+ } ) ;
114+ } ) ;
0 commit comments