w-flow-vue 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/.editorconfig +9 -0
  2. package/.eslintignore +3 -0
  3. package/.eslintrc.js +55 -0
  4. package/.jsdoc +25 -0
  5. package/AGENT.md +223 -0
  6. package/LICENSE +21 -0
  7. package/README.md +37 -0
  8. package/SECURITY.md +5 -0
  9. package/babel.config.js +16 -0
  10. package/dist/w-flow-vue.umd.js +15 -0
  11. package/dist/w-flow-vue.umd.js.map +1 -0
  12. package/docs/components_WFlowVue.vue.html +1214 -0
  13. package/docs/examples/app.html +62 -0
  14. package/docs/examples/app.umd.js +20 -0
  15. package/docs/examples/app.umd.js.map +1 -0
  16. package/docs/examples/ex-AppBasic.html +440 -0
  17. package/docs/examples/ex-AppConnectivity.html +131 -0
  18. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  19. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  20. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  21. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  22. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  23. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  24. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  25. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  26. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  27. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
  28. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  29. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  30. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  31. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  32. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
  33. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  34. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  35. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  36. package/docs/global.html +1919 -0
  37. package/docs/index.html +84 -0
  38. package/docs/js_defaults.mjs.html +105 -0
  39. package/docs/js_edge-path.mjs.html +237 -0
  40. package/docs/js_geometry.mjs.html +298 -0
  41. package/docs/js_graph.mjs.html +103 -0
  42. package/docs/js_step-routing.mjs.html +346 -0
  43. package/docs/module-WFlowVue.html +2790 -0
  44. package/docs/scripts/collapse.js +39 -0
  45. package/docs/scripts/commonNav.js +28 -0
  46. package/docs/scripts/linenumber.js +25 -0
  47. package/docs/scripts/nav.js +12 -0
  48. package/docs/scripts/polyfill.js +4 -0
  49. package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
  50. package/docs/scripts/prettify/lang-css.js +2 -0
  51. package/docs/scripts/prettify/prettify.js +28 -0
  52. package/docs/scripts/search.js +99 -0
  53. package/docs/styles/jsdoc.css +776 -0
  54. package/docs/styles/prettify.css +80 -0
  55. package/jest.config.js +20 -0
  56. package/package.json +80 -0
  57. package/public/index.html +38 -0
  58. package/script.txt +22 -0
  59. package/src/App.vue +326 -0
  60. package/src/AppBasic.vue +125 -0
  61. package/src/AppConnectivity.vue +186 -0
  62. package/src/components/WFlowVue.vue +1142 -0
  63. package/src/components/canvas/BackgroundLayer.vue +78 -0
  64. package/src/components/canvas/FlowCanvas.vue +64 -0
  65. package/src/components/canvas/SelectionBox.vue +36 -0
  66. package/src/components/canvas/ViewportTransform.vue +35 -0
  67. package/src/components/edges/ConnectionLine.vue +65 -0
  68. package/src/components/edges/EdgeMarkerDefs.vue +76 -0
  69. package/src/components/edges/EdgeRenderer.vue +120 -0
  70. package/src/components/edges/EdgeWrapper.vue +379 -0
  71. package/src/components/nodes/DefaultNode.vue +276 -0
  72. package/src/components/nodes/Handle.vue +101 -0
  73. package/src/components/nodes/InputNode.vue +47 -0
  74. package/src/components/nodes/NodeBody.vue +103 -0
  75. package/src/components/nodes/NodeFace.vue +128 -0
  76. package/src/components/nodes/NodeRenderer.vue +95 -0
  77. package/src/components/nodes/NodeWrapper.vue +475 -0
  78. package/src/components/nodes/OutputNode.vue +47 -0
  79. package/src/components/ui/ConnSettingsForm.vue +158 -0
  80. package/src/components/ui/Controls.vue +83 -0
  81. package/src/components/ui/NodeSettingsForm.vue +185 -0
  82. package/src/js/defaults.mjs +33 -0
  83. package/src/js/edge-path.mjs +165 -0
  84. package/src/js/geometry.mjs +226 -0
  85. package/src/js/graph.mjs +31 -0
  86. package/src/js/step-routing.mjs +274 -0
  87. package/src/main.js +22 -0
  88. package/test/WFlowVue-features.test.mjs +760 -0
  89. package/test/WFlowVue.test.mjs +421 -0
  90. package/test/components-canvas.test.mjs +102 -0
  91. package/test/components-edge.test.mjs +147 -0
  92. package/test/components-node.test.mjs +174 -0
  93. package/test/components-ui.test.mjs +69 -0
  94. package/test/defaults.test.mjs +86 -0
  95. package/test/edge-path.test.mjs +102 -0
  96. package/test/generate-routing-snapshots.mjs +77 -0
  97. package/test/generate-visual-baselines.mjs +206 -0
  98. package/test/geometry.test.mjs +236 -0
  99. package/test/graph.test.mjs +72 -0
  100. package/test/jsons/routing-snapshots.json +24994 -0
  101. package/test/pics/_check2.png +0 -0
  102. package/test/pics/_check3.png +0 -0
  103. package/test/pics/_check4.png +0 -0
  104. package/test/pics/_check5.png +0 -0
  105. package/test/pics/_v1.png +0 -0
  106. package/test/pics/_v2.png +0 -0
  107. package/test/pics/_v3.png +0 -0
  108. package/test/pics/_v4.png +0 -0
  109. package/test/pics/_v5.png +0 -0
  110. package/test/pics/_v6.png +0 -0
  111. package/test/pics/_v7.png +0 -0
  112. package/test/pics/vb-edge-hovered.png +0 -0
  113. package/test/pics/vb-edges-normal.png +0 -0
  114. package/test/pics/vb-locked-edge-hovered.png +0 -0
  115. package/test/pics/vb-locked-node-hovered.png +0 -0
  116. package/test/pics/vb-locked-node-selected.png +0 -0
  117. package/test/pics/vb-locked-overview.png +0 -0
  118. package/test/pics/vb-node-1.png +0 -0
  119. package/test/pics/vb-node-10.png +0 -0
  120. package/test/pics/vb-node-11.png +0 -0
  121. package/test/pics/vb-node-12.png +0 -0
  122. package/test/pics/vb-node-2.png +0 -0
  123. package/test/pics/vb-node-3.png +0 -0
  124. package/test/pics/vb-node-4.png +0 -0
  125. package/test/pics/vb-node-5.png +0 -0
  126. package/test/pics/vb-node-6.png +0 -0
  127. package/test/pics/vb-node-7.png +0 -0
  128. package/test/pics/vb-node-8.png +0 -0
  129. package/test/pics/vb-node-9.png +0 -0
  130. package/test/pics/vb-node-hovered.png +0 -0
  131. package/test/pics/vb-node-selected.png +0 -0
  132. package/test/pics/vb-overview.png +0 -0
  133. package/test/step-routing-connectivity.test.mjs +78 -0
  134. package/test/step-routing.test.mjs +88 -0
  135. package/test/visual-regression.test.mjs +274 -0
  136. package/toolg/addVersion.mjs +4 -0
  137. package/toolg/cleanFolder.mjs +4 -0
  138. package/toolg/gDistApp.mjs +34 -0
  139. package/toolg/gDistRollupComps.mjs +22 -0
  140. package/toolg/gDocExams.mjs +47 -0
  141. package/toolg/gExtractHtml.mjs +179 -0
  142. package/toolg/modifyReadme.mjs +4 -0
  143. package/vue.config.js +9 -0
  144. package/vue2/344/271/213foreignObject/345/205/247/346/270/262/346/237/223/345/225/217/351/241/214/350/210/207/344/277/256/346/255/243.md +151 -0
@@ -0,0 +1,174 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import Handle from '../src/components/nodes/Handle.vue'
3
+ import DefaultNode from '../src/components/nodes/DefaultNode.vue'
4
+ import InputNode from '../src/components/nodes/InputNode.vue'
5
+ import OutputNode from '../src/components/nodes/OutputNode.vue'
6
+ import NodeBody from '../src/components/nodes/NodeBody.vue'
7
+ import NodeWrapper from '../src/components/nodes/NodeWrapper.vue'
8
+
9
+ describe('Handle', () => {
10
+ test('renders with correct classes', () => {
11
+ const wrapper = mount(Handle, {
12
+ propsData: { type: 'source', position: 'bottom' },
13
+ })
14
+ expect(wrapper.classes()).toContain('vue-flow__handle')
15
+ expect(wrapper.classes()).toContain('vue-flow__handle--bottom')
16
+ expect(wrapper.classes()).toContain('vue-flow__handle--source')
17
+ })
18
+
19
+ test('emits connect-start on mousedown', () => {
20
+ const wrapper = mount(Handle, {
21
+ propsData: { type: 'source', position: 'bottom', connectable: true },
22
+ })
23
+ wrapper.trigger('mousedown')
24
+ expect(wrapper.emitted('connect-start')).toBeTruthy()
25
+ expect(wrapper.emitted('connect-start')[0][0]).toHaveProperty('handleType', 'source')
26
+ })
27
+
28
+ test('does not emit when not connectable', () => {
29
+ const wrapper = mount(Handle, {
30
+ propsData: { type: 'source', position: 'bottom', connectable: false },
31
+ })
32
+ wrapper.trigger('mousedown')
33
+ expect(wrapper.emitted('connect-start')).toBeFalsy()
34
+ })
35
+ })
36
+
37
+ describe('DefaultNode', () => {
38
+ const node = { id: '1', type: 'basic', name: 'Test', position: { x: 0, y: 0 } }
39
+
40
+ test('renders label', () => {
41
+ const wrapper = mount(DefaultNode, { propsData: { node } })
42
+ expect(wrapper.text()).toContain('Test')
43
+ })
44
+
45
+ test('has two handles', () => {
46
+ const wrapper = mount(DefaultNode, { propsData: { node } })
47
+ const handles = wrapper.findAllComponents(Handle)
48
+ expect(handles).toHaveLength(2)
49
+ })
50
+
51
+ test('has target handle on top and source on bottom', () => {
52
+ const wrapper = mount(DefaultNode, { propsData: { node } })
53
+ const handles = wrapper.findAllComponents(Handle)
54
+ const types = handles.wrappers.map(h => h.props('type'))
55
+ expect(types).toContain('target')
56
+ expect(types).toContain('source')
57
+ })
58
+ })
59
+
60
+ describe('InputNode', () => {
61
+ const node = { id: '1', type: 'input', name: 'Start', position: { x: 0, y: 0 } }
62
+
63
+ test('renders label', () => {
64
+ const wrapper = mount(InputNode, { propsData: { node } })
65
+ expect(wrapper.text()).toContain('Start')
66
+ })
67
+
68
+ test('has one source handle', () => {
69
+ const wrapper = mount(InputNode, { propsData: { node } })
70
+ const handles = wrapper.findAllComponents(Handle)
71
+ expect(handles).toHaveLength(1)
72
+ expect(handles.at(0).props('type')).toBe('source')
73
+ })
74
+ })
75
+
76
+ describe('OutputNode', () => {
77
+ const node = { id: '1', type: 'output', name: 'End', position: { x: 0, y: 0 } }
78
+
79
+ test('renders label', () => {
80
+ const wrapper = mount(OutputNode, { propsData: { node } })
81
+ expect(wrapper.text()).toContain('End')
82
+ })
83
+
84
+ test('has one target handle', () => {
85
+ const wrapper = mount(OutputNode, { propsData: { node } })
86
+ const handles = wrapper.findAllComponents(Handle)
87
+ expect(handles).toHaveLength(1)
88
+ expect(handles.at(0).props('type')).toBe('target')
89
+ })
90
+ })
91
+
92
+ describe('NodeBody', () => {
93
+ test('renders DefaultNode for type basic', () => {
94
+ const node = { id: '1', type: 'basic', name: 'Test', position: { x: 0, y: 0 } }
95
+ const wrapper = mount(NodeBody, { propsData: { node } })
96
+ expect(wrapper.findComponent(DefaultNode).exists()).toBe(true)
97
+ })
98
+
99
+ test('renders InputNode for type input', () => {
100
+ const node = { id: '1', type: 'input', name: 'Test', position: { x: 0, y: 0 } }
101
+ const wrapper = mount(NodeBody, { propsData: { node } })
102
+ expect(wrapper.findComponent(InputNode).exists()).toBe(true)
103
+ })
104
+
105
+ test('renders OutputNode for type output', () => {
106
+ const node = { id: '1', type: 'output', name: 'Test', position: { x: 0, y: 0 } }
107
+ const wrapper = mount(NodeBody, { propsData: { node } })
108
+ expect(wrapper.findComponent(OutputNode).exists()).toBe(true)
109
+ })
110
+
111
+ test('falls back to DefaultNode for unknown type', () => {
112
+ const node = { id: '1', type: 'unknown', name: 'Test', position: { x: 0, y: 0 } }
113
+ const wrapper = mount(NodeBody, { propsData: { node } })
114
+ expect(wrapper.findComponent(DefaultNode).exists()).toBe(true)
115
+ })
116
+ })
117
+
118
+ describe('NodeWrapper', () => {
119
+ const node = { id: '1', type: 'basic', name: 'Test', position: { x: 100, y: 50 } }
120
+
121
+ test('renders with correct transform', () => {
122
+ const wrapper = mount(NodeWrapper, { propsData: { node } })
123
+ expect(wrapper.attributes('style')).toContain('translate(100px, 50px)')
124
+ })
125
+
126
+ test('applies selected class', () => {
127
+ const wrapper = mount(NodeWrapper, { propsData: { node, selected: true } })
128
+ expect(wrapper.classes()).toContain('vue-flow__node--selected')
129
+ })
130
+
131
+ test('does not render when hidden', () => {
132
+ const hiddenNode = { ...node, hidden: true }
133
+ const wrapper = mount(NodeWrapper, { propsData: { node: hiddenNode } })
134
+ expect(wrapper.html()).toBe('')
135
+ })
136
+
137
+ test('applies custom class', () => {
138
+ const styledNode = { ...node, class: 'my-class' }
139
+ const wrapper = mount(NodeWrapper, { propsData: { node: styledNode } })
140
+ expect(wrapper.classes()).toContain('my-class')
141
+ })
142
+
143
+ test('applies custom style', () => {
144
+ const styledNode = { ...node, style: { border: '2px solid red' } }
145
+ const wrapper = mount(NodeWrapper, { propsData: { node: styledNode } })
146
+ expect(wrapper.attributes('style')).toContain('border: 2px solid red')
147
+ })
148
+
149
+ test('emits node-click on mousedown+mouseup', () => {
150
+ const wrapper = mount(NodeWrapper, { propsData: { node } })
151
+ wrapper.trigger('mousedown', { clientX: 0, clientY: 0 })
152
+ wrapper.trigger('mouseup', { clientX: 0, clientY: 0 })
153
+ expect(wrapper.emitted('node-click')).toBeTruthy()
154
+ expect(wrapper.emitted('node-click')[0][0].node.id).toBe('1')
155
+ })
156
+
157
+ test('emits drag-start on mousedown when draggable', () => {
158
+ const wrapper = mount(NodeWrapper, { propsData: { node, draggable: true } })
159
+ wrapper.trigger('mousedown')
160
+ expect(wrapper.emitted('drag-start')).toBeTruthy()
161
+ })
162
+
163
+ test('does not emit drag-start when not draggable', () => {
164
+ const wrapper = mount(NodeWrapper, { propsData: { node, draggable: false } })
165
+ wrapper.trigger('mousedown')
166
+ expect(wrapper.emitted('drag-start')).toBeFalsy()
167
+ })
168
+
169
+ test('applies zIndex from node', () => {
170
+ const zNode = { ...node, zIndex: 10 }
171
+ const wrapper = mount(NodeWrapper, { propsData: { node: zNode } })
172
+ expect(wrapper.attributes('style')).toContain('z-index: 10')
173
+ })
174
+ })
@@ -0,0 +1,69 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import Controls from '../src/components/ui/Controls.vue'
3
+
4
+ describe('Controls', () => {
5
+ test('renders all buttons by default', () => {
6
+ const wrapper = mount(Controls)
7
+ const buttons = wrapper.findAll('button')
8
+ expect(buttons).toHaveLength(4) // zoom in, zoom out, fit view, lock
9
+ })
10
+
11
+ test('hides zoom buttons when showZoom=false', () => {
12
+ const wrapper = mount(Controls, { propsData: { showZoom: false } })
13
+ const buttons = wrapper.findAll('button')
14
+ expect(buttons).toHaveLength(2) // fit view, lock
15
+ })
16
+
17
+ test('hides fit view button when showFitView=false', () => {
18
+ const wrapper = mount(Controls, { propsData: { showFitView: false } })
19
+ const buttons = wrapper.findAll('button')
20
+ expect(buttons).toHaveLength(3) // zoom in, zoom out, lock
21
+ })
22
+
23
+ test('hides interactive button when showInteractive=false', () => {
24
+ const wrapper = mount(Controls, { propsData: { showInteractive: false } })
25
+ const buttons = wrapper.findAll('button')
26
+ expect(buttons).toHaveLength(3) // zoom in, zoom out, fit view
27
+ })
28
+
29
+ test('emits zoom-in on click', () => {
30
+ const wrapper = mount(Controls)
31
+ const buttons = wrapper.findAll('button')
32
+ buttons.at(0).trigger('click') // first button is zoom in
33
+ expect(wrapper.emitted('zoom-in')).toBeTruthy()
34
+ })
35
+
36
+ test('emits zoom-out on click', () => {
37
+ const wrapper = mount(Controls)
38
+ const buttons = wrapper.findAll('button')
39
+ buttons.at(1).trigger('click')
40
+ expect(wrapper.emitted('zoom-out')).toBeTruthy()
41
+ })
42
+
43
+ test('emits fit-view on click', () => {
44
+ const wrapper = mount(Controls)
45
+ const buttons = wrapper.findAll('button')
46
+ buttons.at(2).trigger('click')
47
+ expect(wrapper.emitted('fit-view')).toBeTruthy()
48
+ })
49
+
50
+ test('emits toggle-interactive on click', () => {
51
+ const wrapper = mount(Controls)
52
+ const buttons = wrapper.findAll('button')
53
+ buttons.at(3).trigger('click')
54
+ expect(wrapper.emitted('toggle-interactive')).toBeTruthy()
55
+ })
56
+
57
+ test('applies position class', () => {
58
+ const wrapper = mount(Controls, { propsData: { position: 'bottom-right' } })
59
+ expect(wrapper.classes()).toContain('vue-flow__panel--bottom-right')
60
+ })
61
+
62
+ test('renders slot content', () => {
63
+ const wrapper = mount(Controls, {
64
+ slots: { default: '<button class="custom-btn">Custom</button>' },
65
+ })
66
+ expect(wrapper.find('.custom-btn').exists()).toBe(true)
67
+ })
68
+ })
69
+
@@ -0,0 +1,86 @@
1
+ import { NODE_DEFAULTS, CONN_DEFAULTS } from '../src/js/defaults'
2
+
3
+ describe('defaults', () => {
4
+ describe('NODE_DEFAULTS', () => {
5
+ test('has all required keys', () => {
6
+ const keys = [
7
+ 'type', 'shape', 'width', 'height',
8
+ 'fontSize', 'fontSizeMin', 'fontSizeMax', 'fontColor',
9
+ 'faceColor', 'edgeColor', 'edgeWidth',
10
+ 'toPosition', 'fromPosition', 'popupDirection',
11
+ ]
12
+ keys.forEach(k => {
13
+ expect(NODE_DEFAULTS).toHaveProperty(k)
14
+ })
15
+ })
16
+
17
+ test('type is basic', () => {
18
+ expect(NODE_DEFAULTS.type).toBe('basic')
19
+ })
20
+
21
+ test('shape is rectangle', () => {
22
+ expect(NODE_DEFAULTS.shape).toBe('rectangle')
23
+ })
24
+
25
+ test('dimensions are positive numbers', () => {
26
+ expect(NODE_DEFAULTS.width).toBeGreaterThan(0)
27
+ expect(NODE_DEFAULTS.height).toBeGreaterThan(0)
28
+ })
29
+
30
+ test('fontSizeMin <= fontSize <= fontSizeMax', () => {
31
+ expect(NODE_DEFAULTS.fontSizeMin).toBeLessThanOrEqual(NODE_DEFAULTS.fontSize)
32
+ expect(NODE_DEFAULTS.fontSize).toBeLessThanOrEqual(NODE_DEFAULTS.fontSizeMax)
33
+ })
34
+
35
+ test('edgeWidth is a positive number', () => {
36
+ expect(NODE_DEFAULTS.edgeWidth).toBeGreaterThan(0)
37
+ })
38
+
39
+ test('positions are valid', () => {
40
+ const valid = ['top', 'bottom', 'left', 'right']
41
+ expect(valid).toContain(NODE_DEFAULTS.toPosition)
42
+ expect(valid).toContain(NODE_DEFAULTS.fromPosition)
43
+ expect(valid).toContain(NODE_DEFAULTS.popupDirection)
44
+ })
45
+
46
+ test('colors are hex strings', () => {
47
+ const hexPattern = /^#[0-9a-fA-F]{6}$/
48
+ expect(NODE_DEFAULTS.fontColor).toMatch(hexPattern)
49
+ expect(NODE_DEFAULTS.faceColor).toMatch(hexPattern)
50
+ expect(NODE_DEFAULTS.edgeColor).toMatch(hexPattern)
51
+ })
52
+ })
53
+
54
+ describe('CONN_DEFAULTS', () => {
55
+ test('has all required keys', () => {
56
+ const keys = [
57
+ 'type', 'fontSize', 'fontSizeMin', 'fontSizeMax', 'fontColor',
58
+ 'edgeColor', 'edgeWidth', 'markerEnd', 'animated', 'defOffset',
59
+ ]
60
+ keys.forEach(k => {
61
+ expect(CONN_DEFAULTS).toHaveProperty(k)
62
+ })
63
+ })
64
+
65
+ test('type is bezier', () => {
66
+ expect(CONN_DEFAULTS.type).toBe('bezier')
67
+ })
68
+
69
+ test('animated is boolean false', () => {
70
+ expect(CONN_DEFAULTS.animated).toBe(false)
71
+ })
72
+
73
+ test('fontSizeMin <= fontSize <= fontSizeMax', () => {
74
+ expect(CONN_DEFAULTS.fontSizeMin).toBeLessThanOrEqual(CONN_DEFAULTS.fontSize)
75
+ expect(CONN_DEFAULTS.fontSize).toBeLessThanOrEqual(CONN_DEFAULTS.fontSizeMax)
76
+ })
77
+
78
+ test('defOffset is a positive number', () => {
79
+ expect(CONN_DEFAULTS.defOffset).toBeGreaterThan(0)
80
+ })
81
+
82
+ test('markerEnd is a string', () => {
83
+ expect(typeof CONN_DEFAULTS.markerEnd).toBe('string')
84
+ })
85
+ })
86
+ })
@@ -0,0 +1,102 @@
1
+ import { getBezierPath, getStraightPath, getStepPath, getSmoothStepPath } from '../src/js/edge-path'
2
+
3
+ describe('edge-path', () => {
4
+ const source = { sourceX: 100, sourceY: 50 }
5
+ const target = { targetX: 300, targetY: 250 }
6
+
7
+ describe('getBezierPath', () => {
8
+ test('returns path string starting with M', () => {
9
+ const result = getBezierPath({ ...source, ...target })
10
+ expect(result.path).toMatch(/^M /)
11
+ expect(result.path).toContain('C ')
12
+ })
13
+
14
+ test('returns label position', () => {
15
+ const result = getBezierPath({ ...source, ...target })
16
+ expect(typeof result.labelX).toBe('number')
17
+ expect(typeof result.labelY).toBe('number')
18
+ })
19
+
20
+ test('label position is between source and target', () => {
21
+ const result = getBezierPath({ ...source, ...target })
22
+ expect(result.labelX).toBeGreaterThanOrEqual(source.sourceX)
23
+ expect(result.labelX).toBeLessThanOrEqual(target.targetX)
24
+ })
25
+
26
+ test('supports different positions', () => {
27
+ const r1 = getBezierPath({ ...source, ...target, sourcePosition: 'right', targetPosition: 'left' })
28
+ const r2 = getBezierPath({ ...source, ...target, sourcePosition: 'bottom', targetPosition: 'top' })
29
+ expect(r1.path).not.toBe(r2.path)
30
+ })
31
+
32
+ test('supports custom curvature', () => {
33
+ const r1 = getBezierPath({ ...source, ...target, curvature: 0.1 })
34
+ const r2 = getBezierPath({ ...source, ...target, curvature: 0.5 })
35
+ expect(r1.path).not.toBe(r2.path)
36
+ })
37
+
38
+ test('handles same position (zero distance)', () => {
39
+ const result = getBezierPath({ sourceX: 100, sourceY: 100, targetX: 100, targetY: 100 })
40
+ expect(result.path).toMatch(/^M /)
41
+ })
42
+ })
43
+
44
+ describe('getStraightPath', () => {
45
+ test('returns path with M and L', () => {
46
+ const result = getStraightPath({ ...source, ...target })
47
+ expect(result.path).toBe('M 100,50 L 300,250')
48
+ })
49
+
50
+ test('label is at midpoint', () => {
51
+ const result = getStraightPath({ ...source, ...target })
52
+ expect(result.labelX).toBe(200)
53
+ expect(result.labelY).toBe(150)
54
+ })
55
+ })
56
+
57
+ describe('getStepPath', () => {
58
+ test('returns path with only M and L commands', () => {
59
+ const result = getStepPath({ ...source, ...target })
60
+ expect(result.path).toMatch(/^M /)
61
+ expect(result.path).toContain('L ')
62
+ expect(result.path).not.toContain('C ')
63
+ })
64
+
65
+ test('returns label position', () => {
66
+ const result = getStepPath({ ...source, ...target })
67
+ expect(typeof result.labelX).toBe('number')
68
+ expect(typeof result.labelY).toBe('number')
69
+ })
70
+
71
+ test('horizontal source/target produces correct segments', () => {
72
+ const result = getStepPath({
73
+ sourceX: 100,
74
+ sourceY: 50,
75
+ sourcePosition: 'right',
76
+ targetX: 300,
77
+ targetY: 250,
78
+ targetPosition: 'left',
79
+ })
80
+ expect(result.path).toMatch(/^M /)
81
+ })
82
+ })
83
+
84
+ describe('getSmoothStepPath', () => {
85
+ test('returns path with Q commands for rounded corners', () => {
86
+ const result = getSmoothStepPath({ ...source, ...target })
87
+ expect(result.path).toContain('Q ')
88
+ })
89
+
90
+ test('returns label position', () => {
91
+ const result = getSmoothStepPath({ ...source, ...target })
92
+ expect(typeof result.labelX).toBe('number')
93
+ expect(typeof result.labelY).toBe('number')
94
+ })
95
+
96
+ test('supports custom borderRadius', () => {
97
+ const r1 = getSmoothStepPath({ ...source, ...target, borderRadius: 2 })
98
+ const r2 = getSmoothStepPath({ ...source, ...target, borderRadius: 10 })
99
+ expect(r1.path).not.toBe(r2.path)
100
+ })
101
+ })
102
+ })
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Generate routing snapshot data for all 4×4×36 = 576 combinations.
3
+ * - 4 source handle positions (top/bottom/left/right)
4
+ * - 4 target handle positions (top/bottom/left/right)
5
+ * - 36 angles (0°–350° in 10° steps): B position around A
6
+ *
7
+ * Based on AppExamConnectivity.vue defaults:
8
+ * A center: (350, 250), Node size: 100×40, Offset: 200
9
+ */
10
+ import { calculateStepPoints, clearStepCache } from '../src/js/step-routing.mjs'
11
+ import fs from 'fs'
12
+ import path from 'path'
13
+ import { fileURLToPath } from 'url'
14
+
15
+ const __filename = fileURLToPath(import.meta.url)
16
+ const __dirname = path.dirname(__filename)
17
+
18
+ const CENTER_X = 350
19
+ const CENTER_Y = 250
20
+ const OFFSET = 200
21
+ const NODE_W = 100
22
+ const NODE_H = 40
23
+ const BUF = 24
24
+
25
+ const positions = ['top', 'bottom', 'left', 'right']
26
+
27
+ // Handle position on node edge
28
+ function handleXY(pos, nx, ny, w, h) {
29
+ switch (pos) {
30
+ case 'top': return { x: nx + w / 2, y: ny }
31
+ case 'bottom': return { x: nx + w / 2, y: ny + h }
32
+ case 'left': return { x: nx, y: ny + h / 2 }
33
+ case 'right': return { x: nx + w, y: ny + h / 2 }
34
+ }
35
+ }
36
+
37
+ let allCases = []
38
+
39
+ for (let srcPos of positions) {
40
+ for (let tgtPos of positions) {
41
+ for (let deg = 0; deg < 360; deg += 10) {
42
+ let rad = deg * Math.PI / 180
43
+ let bx = Math.round(CENTER_X + OFFSET * Math.cos(rad))
44
+ let by = Math.round(CENTER_Y - OFFSET * Math.sin(rad)) // Y-axis inverted
45
+
46
+ let sH = handleXY(srcPos, CENTER_X, CENTER_Y, NODE_W, NODE_H)
47
+ let tH = handleXY(tgtPos, bx, by, NODE_W, NODE_H)
48
+
49
+ let nodes = [
50
+ { id: 'A', position: { x: CENTER_X, y: CENTER_Y }, width: NODE_W, height: NODE_H },
51
+ { id: 'B', position: { x: bx, y: by }, width: NODE_W, height: NODE_H },
52
+ ]
53
+
54
+ clearStepCache()
55
+ let pts = calculateStepPoints(
56
+ sH.x, sH.y, srcPos,
57
+ tH.x, tH.y, tgtPos,
58
+ BUF, nodes, {}, 'A', 'B'
59
+ )
60
+
61
+ allCases.push({
62
+ srcPos,
63
+ tgtPos,
64
+ deg,
65
+ aPos: { x: CENTER_X, y: CENTER_Y },
66
+ bPos: { x: bx, y: by },
67
+ sourceXY: sH,
68
+ targetXY: tH,
69
+ pts,
70
+ })
71
+ }
72
+ }
73
+ }
74
+
75
+ let outPath = path.join(__dirname, 'jsons', 'routing-snapshots.json')
76
+ fs.writeFileSync(outPath, JSON.stringify(allCases, null, 2))
77
+ console.log(`Generated ${allCases.length} cases → ${outPath}`)