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,421 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import WFlowVue from '../src/components/WFlowVue.vue'
3
+
4
+ const sampleNodes = [
5
+ { id: '1', type: 'input', name: 'Node 1', position: { x: 250, y: 5 } },
6
+ { id: '2', type: 'output', name: 'Node 2', position: { x: 100, y: 250 } },
7
+ { id: '3', type: 'basic', name: 'Node 3', position: { x: 400, y: 300 } },
8
+ ]
9
+
10
+ const sampleConns = [
11
+ { id: 'e1-2', from: '1', to: '2', animated: true },
12
+ { id: 'e1-3', from: '1', to: '3', name: 'edge with arrowhead', markerEnd: 'arrowclosed' },
13
+ ]
14
+
15
+ function createWrapper(optOverrides = {}) {
16
+ return mount(WFlowVue, {
17
+ propsData: {
18
+ opt: {
19
+ nodes: JSON.parse(JSON.stringify(sampleNodes)),
20
+ conns: JSON.parse(JSON.stringify(sampleConns)),
21
+ ...optOverrides,
22
+ },
23
+ },
24
+ attachTo: document.body,
25
+ })
26
+ }
27
+
28
+ function mockNodeEvent() {
29
+ const el = document.createElement('div')
30
+ el.classList.add('vue-flow__node')
31
+ el.getBoundingClientRect = () => ({ top: 0, left: 0, right: 100, bottom: 40, width: 100, height: 40 })
32
+ return { target: el, clientX: 0, clientY: 0 }
33
+ }
34
+
35
+ describe('WFlowVue', () => {
36
+ afterEach(() => {
37
+ document.body.innerHTML = ''
38
+ })
39
+
40
+ describe('rendering', () => {
41
+ test('mounts successfully', () => {
42
+ const wrapper = createWrapper()
43
+ expect(wrapper.exists()).toBe(true)
44
+ wrapper.destroy()
45
+ })
46
+
47
+ test('renders all visible nodes', () => {
48
+ const wrapper = createWrapper()
49
+ const nodeEls = wrapper.findAll('.vue-flow__node')
50
+ expect(nodeEls).toHaveLength(3)
51
+ wrapper.destroy()
52
+ })
53
+
54
+ test('renders node names', () => {
55
+ const wrapper = createWrapper()
56
+ expect(wrapper.text()).toContain('Node 1')
57
+ expect(wrapper.text()).toContain('Node 2')
58
+ expect(wrapper.text()).toContain('Node 3')
59
+ wrapper.destroy()
60
+ })
61
+
62
+ test('renders conns', () => {
63
+ const wrapper = createWrapper()
64
+ const connGroups = wrapper.findAll('.vue-flow__edge')
65
+ expect(connGroups).toHaveLength(2)
66
+ wrapper.destroy()
67
+ })
68
+
69
+ test('renders conn name', () => {
70
+ const wrapper = createWrapper()
71
+ expect(wrapper.text()).toContain('edge with arrowhead')
72
+ wrapper.destroy()
73
+ })
74
+
75
+ test('renders background', () => {
76
+ const wrapper = createWrapper()
77
+ expect(wrapper.find('.vue-flow__background').exists()).toBe(true)
78
+ wrapper.destroy()
79
+ })
80
+
81
+ test('renders correct node types', () => {
82
+ const wrapper = createWrapper()
83
+ expect(wrapper.find('.vue-flow__node-input').exists()).toBe(true)
84
+ expect(wrapper.find('.vue-flow__node-output').exists()).toBe(true)
85
+ expect(wrapper.find('.vue-flow__node-basic').exists()).toBe(true)
86
+ wrapper.destroy()
87
+ })
88
+
89
+ test('does not render hidden nodes', () => {
90
+ const nodes = [
91
+ ...sampleNodes,
92
+ { id: '4', type: 'basic', name: 'Hidden', position: { x: 0, y: 0 }, hidden: true },
93
+ ]
94
+ const wrapper = createWrapper({ nodes })
95
+ const nodeEls = wrapper.findAll('.vue-flow__node')
96
+ expect(nodeEls).toHaveLength(3)
97
+ wrapper.destroy()
98
+ })
99
+ })
100
+
101
+ describe('node selection', () => {
102
+ test('onNodeClick emits node-click', () => {
103
+ const wrapper = createWrapper()
104
+ const node = wrapper.vm.nodeById('1')
105
+ wrapper.vm.onNodeClick({ node, event: mockNodeEvent() })
106
+ expect(wrapper.emitted('node-click')).toBeTruthy()
107
+ expect(wrapper.emitted('node-click')[0][0].node.id).toBe('1')
108
+ wrapper.destroy()
109
+ })
110
+
111
+ test('onNodeClick selects the node', () => {
112
+ const wrapper = createWrapper()
113
+ const node = wrapper.vm.nodeById('1')
114
+ wrapper.vm.onNodeClick({ node, event: mockNodeEvent() })
115
+ expect(wrapper.vm.selectedNodes).toContain('1')
116
+ wrapper.destroy()
117
+ })
118
+
119
+ test('clearSelection clears all', () => {
120
+ const wrapper = createWrapper()
121
+ const node = wrapper.vm.nodeById('1')
122
+ wrapper.vm.onNodeClick({ node, event: mockNodeEvent() })
123
+ wrapper.vm.clearSelection()
124
+ expect(wrapper.vm.selectedNodes).toHaveLength(0)
125
+ wrapper.destroy()
126
+ })
127
+
128
+ test('selection-change event emitted', () => {
129
+ const wrapper = createWrapper()
130
+ const node = wrapper.vm.nodeById('1')
131
+ wrapper.vm.onNodeClick({ node, event: mockNodeEvent() })
132
+ expect(wrapper.emitted('selection-change')).toBeTruthy()
133
+ wrapper.destroy()
134
+ })
135
+ })
136
+
137
+ describe('conn selection', () => {
138
+ test('onConnClick emits conn-click', () => {
139
+ const wrapper = createWrapper()
140
+ const conn = wrapper.vm.connById('e1-2')
141
+ wrapper.vm.onConnClick({ conn, event: new MouseEvent('click') })
142
+ expect(wrapper.emitted('conn-click')).toBeTruthy()
143
+ wrapper.destroy()
144
+ })
145
+ })
146
+
147
+ describe('viewport', () => {
148
+ test('default viewport is applied', () => {
149
+ const wrapper = createWrapper({ center: [50, 100], zoom: 1.5 })
150
+ expect(wrapper.vm.viewport).toEqual({ x: 50, y: 100, zoom: 1.5 })
151
+ wrapper.destroy()
152
+ })
153
+
154
+ test('zoomIn increases zoom', () => {
155
+ const wrapper = createWrapper()
156
+ const initialZoom = wrapper.vm.viewport.zoom
157
+ wrapper.vm.zoomIn()
158
+ expect(wrapper.vm.viewport.zoom).toBeGreaterThan(initialZoom)
159
+ wrapper.destroy()
160
+ })
161
+
162
+ test('zoomOut decreases zoom', () => {
163
+ const wrapper = createWrapper()
164
+ const initialZoom = wrapper.vm.viewport.zoom
165
+ wrapper.vm.zoomOut()
166
+ expect(wrapper.vm.viewport.zoom).toBeLessThan(initialZoom)
167
+ wrapper.destroy()
168
+ })
169
+
170
+ test('zoomIn respects maxZoom', () => {
171
+ const wrapper = createWrapper({ zoomMax: 1.5 })
172
+ wrapper.vm.zoomIn()
173
+ wrapper.vm.zoomIn()
174
+ wrapper.vm.zoomIn()
175
+ wrapper.vm.zoomIn()
176
+ wrapper.vm.zoomIn()
177
+ expect(wrapper.vm.viewport.zoom).toBeLessThanOrEqual(1.5)
178
+ wrapper.destroy()
179
+ })
180
+
181
+ test('zoomOut respects minZoom', () => {
182
+ const wrapper = createWrapper({ zoomMin: 0.5 })
183
+ wrapper.vm.zoomOut()
184
+ wrapper.vm.zoomOut()
185
+ wrapper.vm.zoomOut()
186
+ wrapper.vm.zoomOut()
187
+ wrapper.vm.zoomOut()
188
+ expect(wrapper.vm.viewport.zoom).toBeGreaterThanOrEqual(0.5)
189
+ wrapper.destroy()
190
+ })
191
+
192
+ test('viewport-change emitted on zoom', () => {
193
+ const wrapper = createWrapper()
194
+ wrapper.vm.zoomIn()
195
+ expect(wrapper.emitted('viewport-change')).toBeTruthy()
196
+ wrapper.destroy()
197
+ })
198
+ })
199
+
200
+ describe('deletion', () => {
201
+ test('deleteSelectedElements removes selected nodes', () => {
202
+ const wrapper = createWrapper()
203
+ wrapper.vm.setSelectedNodes(['3'])
204
+ wrapper.vm.deleteSelectedElements()
205
+ expect(wrapper.vm.nodeById('3')).toBeNull()
206
+ wrapper.destroy()
207
+ })
208
+
209
+ test('deleteSelectedElements removes connected conns', () => {
210
+ const wrapper = createWrapper()
211
+ wrapper.vm.setSelectedNodes(['1'])
212
+ wrapper.vm.deleteSelectedElements()
213
+ // Node 1 removed → e1-2 and e1-3 should also be removed
214
+ expect(wrapper.vm.conns).toHaveLength(0)
215
+ wrapper.destroy()
216
+ })
217
+
218
+ test('deleteSelectedElements emits delete event', () => {
219
+ const wrapper = createWrapper()
220
+ wrapper.vm.setSelectedNodes(['3'])
221
+ wrapper.vm.deleteSelectedElements()
222
+ expect(wrapper.emitted('delete')).toBeTruthy()
223
+ expect(wrapper.emitted('delete')[0][0].nodes).toHaveLength(1)
224
+ wrapper.destroy()
225
+ })
226
+
227
+ test('deleteSelectedElements respects deletable=false', () => {
228
+ const nodes = [
229
+ ...sampleNodes,
230
+ { id: '4', type: 'basic', name: 'Undeletable', position: { x: 0, y: 0 }, deletable: false },
231
+ ]
232
+ const wrapper = createWrapper({ nodes })
233
+ wrapper.vm.setSelectedNodes(['4'])
234
+ wrapper.vm.deleteSelectedElements()
235
+ expect(wrapper.vm.nodeById('4')).not.toBeNull()
236
+ wrapper.destroy()
237
+ })
238
+
239
+ test('delete does nothing when nothing selected', () => {
240
+ const wrapper = createWrapper()
241
+ wrapper.vm.deleteSelectedElements()
242
+ expect(wrapper.emitted('delete')).toBeFalsy()
243
+ wrapper.destroy()
244
+ })
245
+ })
246
+
247
+ describe('getFlowData', () => {
248
+ test('returns deep copy of nodes and conns', () => {
249
+ const wrapper = createWrapper()
250
+ const data = wrapper.vm.getFlowData()
251
+ expect(data.nodes).toHaveLength(3)
252
+ expect(data.conns).toHaveLength(2)
253
+ // Verify it is a copy
254
+ data.nodes[0].name = 'Changed'
255
+ expect(wrapper.vm.nodes[0].name).toBe('Node 1')
256
+ wrapper.destroy()
257
+ })
258
+ })
259
+
260
+ describe('node drag', () => {
261
+ test('onNodeDragStart selects the node', () => {
262
+ const wrapper = createWrapper()
263
+ const node = wrapper.vm.nodeById('1')
264
+ wrapper.vm.onNodeDragStart({
265
+ node,
266
+ event: { clientX: 100, clientY: 100 },
267
+ })
268
+ expect(wrapper.vm.selectedNodes).toContain('1')
269
+ expect(wrapper.emitted('node-drag-start')).toBeTruthy()
270
+ wrapper.destroy()
271
+ })
272
+ })
273
+
274
+ describe('connection', () => {
275
+ test('onConnectStart sets connecting state', () => {
276
+ const wrapper = createWrapper()
277
+ wrapper.vm.onConnectStart({
278
+ nodeId: '1',
279
+ handleId: 'source',
280
+ handleType: 'source',
281
+ handlePosition: 'bottom',
282
+ })
283
+ expect(wrapper.vm.isConnecting).toBe(true)
284
+ expect(wrapper.emitted('connect-start')).toBeTruthy()
285
+ wrapper.destroy()
286
+ })
287
+ })
288
+
289
+ describe('props sync', () => {
290
+ test('nodes change via opt prop', async () => {
291
+ const wrapper = createWrapper()
292
+ const newNodes = [
293
+ { id: 'a', type: 'basic', name: 'A', position: { x: 0, y: 0 } },
294
+ ]
295
+ await wrapper.setProps({ opt: { nodes: newNodes, conns: [] } })
296
+ expect(wrapper.vm.nodes).toHaveLength(1)
297
+ expect(wrapper.vm.nodes[0].id).toBe('a')
298
+ wrapper.destroy()
299
+ })
300
+
301
+ test('conns change via opt prop', async () => {
302
+ const wrapper = createWrapper()
303
+ await wrapper.setProps({ opt: { nodes: sampleNodes, conns: [] } })
304
+ expect(wrapper.vm.conns).toHaveLength(0)
305
+ wrapper.destroy()
306
+ })
307
+ })
308
+
309
+ describe('keyboard', () => {
310
+ test('delete key triggers deletion of selected', () => {
311
+ const wrapper = createWrapper({ deleteKeyEnabled: true })
312
+ wrapper.vm.setSelectedNodes(['3'])
313
+ const event = new KeyboardEvent('keydown', { key: 'Backspace' })
314
+ document.dispatchEvent(event)
315
+ expect(wrapper.vm.nodeById('3')).toBeNull()
316
+ wrapper.destroy()
317
+ })
318
+ })
319
+
320
+ describe('locked', () => {
321
+ test('initially unlocked', () => {
322
+ const wrapper = createWrapper()
323
+ expect(wrapper.vm.locked).toBe(false)
324
+ wrapper.destroy()
325
+ })
326
+
327
+ test('toggleInteractive locks and unlocks', () => {
328
+ const wrapper = createWrapper()
329
+ wrapper.vm.toggleInteractive()
330
+ expect(wrapper.vm.locked).toBe(true)
331
+ wrapper.vm.toggleInteractive()
332
+ expect(wrapper.vm.locked).toBe(false)
333
+ wrapper.destroy()
334
+ })
335
+
336
+ test('locked blocks node drag', () => {
337
+ const wrapper = createWrapper()
338
+ wrapper.vm.toggleInteractive()
339
+ const node = wrapper.vm.nodeById('1')
340
+ const origX = node.position.x
341
+ wrapper.vm.onNodeDragStart({ node, event: { clientX: 0, clientY: 0 } })
342
+ expect(wrapper.vm.isDraggingNode).toBe(false)
343
+ expect(node.position.x).toBe(origX)
344
+ wrapper.destroy()
345
+ })
346
+
347
+ test('locked blocks node resize', () => {
348
+ const wrapper = createWrapper()
349
+ wrapper.vm.toggleInteractive()
350
+ wrapper.vm.onNodeResize({ nodeId: '1', width: 200, height: 100, x: 0, y: 0 })
351
+ expect(wrapper.vm.resizeOverlay).toBeNull()
352
+ wrapper.destroy()
353
+ })
354
+
355
+ test('locked blocks delete key', () => {
356
+ const wrapper = createWrapper({ deleteKeyEnabled: true })
357
+ wrapper.vm.toggleInteractive()
358
+ wrapper.vm.setSelectedNodes(['3'])
359
+ const event = new KeyboardEvent('keydown', { key: 'Backspace' })
360
+ document.dispatchEvent(event)
361
+ expect(wrapper.vm.nodeById('3')).not.toBeNull()
362
+ wrapper.destroy()
363
+ })
364
+
365
+ test('locked passes to NodeRenderer', async () => {
366
+ const wrapper = createWrapper()
367
+ wrapper.vm.toggleInteractive()
368
+ await wrapper.vm.$nextTick()
369
+ const nodeRenderer = wrapper.findComponent({ name: 'NodeRenderer' })
370
+ expect(nodeRenderer.props('locked')).toBe(true)
371
+ wrapper.destroy()
372
+ })
373
+
374
+ test('locked passes to EdgeRenderer', async () => {
375
+ const wrapper = createWrapper()
376
+ wrapper.vm.toggleInteractive()
377
+ await wrapper.vm.$nextTick()
378
+ const edgeRenderer = wrapper.findComponent({ name: 'EdgeRenderer' })
379
+ expect(edgeRenderer.props('locked')).toBe(true)
380
+ wrapper.destroy()
381
+ })
382
+
383
+ test('locked hides node settings icon', async () => {
384
+ const wrapper = createWrapper()
385
+ // Settings icon requires hover + unlocked; locked should hide it
386
+ wrapper.vm.toggleInteractive()
387
+ await wrapper.vm.$nextTick()
388
+ expect(wrapper.findAll('.vue-flow__node-settings').length).toBe(0)
389
+ wrapper.destroy()
390
+ })
391
+
392
+ test('locked hides node resize handles', async () => {
393
+ const wrapper = createWrapper()
394
+ wrapper.vm.toggleInteractive()
395
+ await wrapper.vm.$nextTick()
396
+ expect(wrapper.findAll('.vue-flow__resize--top-left').length).toBe(0)
397
+ expect(wrapper.findAll('.vue-flow__resize--bottom-right').length).toBe(0)
398
+ wrapper.destroy()
399
+ })
400
+
401
+ test('locked hides node handles via CSS class', async () => {
402
+ const wrapper = createWrapper()
403
+ wrapper.vm.toggleInteractive()
404
+ await wrapper.vm.$nextTick()
405
+ const lockedNodes = wrapper.findAll('.vue-flow__node--locked')
406
+ expect(lockedNodes.length).toBeGreaterThan(0)
407
+ wrapper.destroy()
408
+ })
409
+
410
+ test('locked hides edge settings icon', async () => {
411
+ const wrapper = createWrapper()
412
+ wrapper.vm.toggleInteractive()
413
+ await wrapper.vm.$nextTick()
414
+ const edgeWrappers = wrapper.findAllComponents({ name: 'EdgeWrapper' })
415
+ edgeWrappers.wrappers.forEach(ew => {
416
+ expect(ew.props('locked')).toBe(true)
417
+ })
418
+ wrapper.destroy()
419
+ })
420
+ })
421
+ })
@@ -0,0 +1,102 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import ViewportTransform from '../src/components/canvas/ViewportTransform.vue'
3
+ import SelectionBox from '../src/components/canvas/SelectionBox.vue'
4
+ import BackgroundLayer from '../src/components/canvas/BackgroundLayer.vue'
5
+ import FlowCanvas from '../src/components/canvas/FlowCanvas.vue'
6
+
7
+ describe('ViewportTransform', () => {
8
+ test('applies transform style', () => {
9
+ const wrapper = mount(ViewportTransform, {
10
+ propsData: { x: 100, y: 50, zoom: 1.5 },
11
+ slots: { default: '<div class="child">content</div>' },
12
+ })
13
+ expect(wrapper.attributes('style')).toContain('translate(100px, 50px) scale(1.5)')
14
+ })
15
+
16
+ test('default values produce identity transform', () => {
17
+ const wrapper = mount(ViewportTransform, {
18
+ slots: { default: '<div>content</div>' },
19
+ })
20
+ expect(wrapper.attributes('style')).toContain('translate(0px, 0px) scale(1)')
21
+ })
22
+
23
+ test('renders slot content', () => {
24
+ const wrapper = mount(ViewportTransform, {
25
+ slots: { default: '<div class="child">hello</div>' },
26
+ })
27
+ expect(wrapper.find('.child').text()).toBe('hello')
28
+ })
29
+ })
30
+
31
+ describe('SelectionBox', () => {
32
+ test('renders when box is provided', () => {
33
+ const wrapper = mount(SelectionBox, {
34
+ propsData: { box: { x: 10, y: 20, width: 100, height: 50 } },
35
+ })
36
+ const el = wrapper.find('.vue-flow__selection-box')
37
+ expect(el.exists()).toBe(true)
38
+ expect(el.attributes('style')).toContain('left: 10px')
39
+ expect(el.attributes('style')).toContain('top: 20px')
40
+ expect(el.attributes('style')).toContain('width: 100px')
41
+ expect(el.attributes('style')).toContain('height: 50px')
42
+ })
43
+
44
+ test('does not render when box is null', () => {
45
+ const wrapper = mount(SelectionBox, {
46
+ propsData: { box: null },
47
+ })
48
+ expect(wrapper.find('.vue-flow__selection-box').exists()).toBe(false)
49
+ })
50
+ })
51
+
52
+ describe('FlowCanvas', () => {
53
+ test('renders container with vue-flow class', () => {
54
+ const wrapper = mount(FlowCanvas, {
55
+ slots: { default: '<div>child</div>' },
56
+ })
57
+ expect(wrapper.classes()).toContain('vue-flow')
58
+ })
59
+
60
+ test('emits canvas-click on mousedown+mouseup', () => {
61
+ const wrapper = mount(FlowCanvas)
62
+ wrapper.trigger('mousedown', { clientX: 0, clientY: 0 })
63
+ wrapper.trigger('mouseup', { clientX: 0, clientY: 0 })
64
+ expect(wrapper.emitted('canvas-click')).toBeTruthy()
65
+ })
66
+
67
+ test('emits canvas-mousedown', () => {
68
+ const wrapper = mount(FlowCanvas)
69
+ wrapper.trigger('mousedown', { clientX: 0, clientY: 0 })
70
+ expect(wrapper.emitted('canvas-mousedown')).toBeTruthy()
71
+ })
72
+
73
+ test('renders slot content', () => {
74
+ const wrapper = mount(FlowCanvas, {
75
+ slots: { default: '<div class="test-child">hello</div>' },
76
+ })
77
+ expect(wrapper.find('.test-child').text()).toBe('hello')
78
+ })
79
+ })
80
+
81
+ describe('BackgroundLayer', () => {
82
+ test('renders svg with pattern', () => {
83
+ const wrapper = mount(BackgroundLayer)
84
+ expect(wrapper.find('svg').exists()).toBe(true)
85
+ expect(wrapper.find('pattern').exists()).toBe(true)
86
+ })
87
+
88
+ test('renders dots pattern by default', () => {
89
+ const wrapper = mount(BackgroundLayer, {
90
+ propsData: { variant: 'dots' },
91
+ })
92
+ expect(wrapper.find('circle').exists()).toBe(true)
93
+ })
94
+
95
+ test('renders lines pattern', () => {
96
+ const wrapper = mount(BackgroundLayer, {
97
+ propsData: { variant: 'lines' },
98
+ })
99
+ expect(wrapper.find('circle').exists()).toBe(false)
100
+ expect(wrapper.find('path').exists()).toBe(true)
101
+ })
102
+ })
@@ -0,0 +1,147 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import EdgeWrapper from '../src/components/edges/EdgeWrapper.vue'
3
+ import EdgeLabel from '../src/components/edges/EdgeLabel.vue'
4
+ import EdgeMarkerDefs from '../src/components/edges/EdgeMarkerDefs.vue'
5
+
6
+ describe('EdgeWrapper', () => {
7
+ const baseProps = {
8
+ conn: { id: 'e1-2', source: '1', target: '2', type: 'default' },
9
+ sourceX: 100,
10
+ sourceY: 50,
11
+ targetX: 300,
12
+ targetY: 250,
13
+ sourcePosition: 'bottom',
14
+ targetPosition: 'top',
15
+ }
16
+
17
+ test('renders svg group', () => {
18
+ const wrapper = mount(EdgeWrapper, {
19
+ propsData: baseProps,
20
+ stubs: { EdgeLabel: true },
21
+ })
22
+ expect(wrapper.element.tagName.toLowerCase()).toBe('g')
23
+ })
24
+
25
+ test('renders path with d attribute', () => {
26
+ const wrapper = mount(EdgeWrapper, {
27
+ propsData: baseProps,
28
+ stubs: { EdgeLabel: true },
29
+ })
30
+ const paths = wrapper.findAll('path')
31
+ expect(paths.length).toBeGreaterThanOrEqual(1)
32
+ const visiblePath = paths.wrappers.find(p => !p.classes().includes('vue-flow__edge-interaction'))
33
+ expect(visiblePath.attributes('d')).toMatch(/^M /)
34
+ })
35
+
36
+ test('applies selected class', () => {
37
+ const wrapper = mount(EdgeWrapper, {
38
+ propsData: { ...baseProps, selected: true },
39
+ stubs: { EdgeLabel: true },
40
+ })
41
+ expect(wrapper.classes()).toContain('vue-flow__edge--selected')
42
+ })
43
+
44
+ test('applies animated class', () => {
45
+ const wrapper = mount(EdgeWrapper, {
46
+ propsData: {
47
+ ...baseProps,
48
+ conn: { ...baseProps.conn, animated: true },
49
+ },
50
+ stubs: { EdgeLabel: true },
51
+ })
52
+ expect(wrapper.classes()).toContain('vue-flow__edge--animated')
53
+ })
54
+
55
+ test('renders name when provided', () => {
56
+ const wrapper = mount(EdgeWrapper, {
57
+ propsData: {
58
+ ...baseProps,
59
+ conn: { ...baseProps.conn, name: 'test label' },
60
+ },
61
+ })
62
+ expect(wrapper.find('.vue-flow__edge-label').exists()).toBe(true)
63
+ })
64
+
65
+ test('does not render name when not provided', () => {
66
+ const wrapper = mount(EdgeWrapper, {
67
+ propsData: baseProps,
68
+ })
69
+ expect(wrapper.find('.vue-flow__edge-label').exists()).toBe(false)
70
+ })
71
+
72
+ test('emits conn-click on click', () => {
73
+ const wrapper = mount(EdgeWrapper, {
74
+ propsData: baseProps,
75
+ stubs: { EdgeLabel: true },
76
+ })
77
+ const interactionPath = wrapper.find('.vue-flow__edge-interaction')
78
+ interactionPath.trigger('click')
79
+ expect(wrapper.emitted('conn-click')).toBeTruthy()
80
+ expect(wrapper.emitted('conn-click')[0][0].conn.id).toBe('e1-2')
81
+ })
82
+
83
+ test('renders different edge types', () => {
84
+ const types = ['default', 'straight', 'step', 'smoothstep']
85
+ const paths = types.map(type => {
86
+ const wrapper = mount(EdgeWrapper, {
87
+ propsData: {
88
+ ...baseProps,
89
+ conn: { ...baseProps.conn, type },
90
+ },
91
+ stubs: { EdgeLabel: true },
92
+ })
93
+ const visiblePath = wrapper.findAll('path').wrappers.find(
94
+ p => !p.classes().includes('vue-flow__edge-interaction')
95
+ )
96
+ return visiblePath.attributes('d')
97
+ })
98
+ // straight should differ from bezier
99
+ expect(paths[0]).not.toBe(paths[1])
100
+ })
101
+ })
102
+
103
+ describe('EdgeLabel', () => {
104
+ test('renders name text', () => {
105
+ const wrapper = mount(EdgeLabel, {
106
+ propsData: { name: 'hello', x: 100, y: 50 },
107
+ })
108
+ expect(wrapper.text()).toContain('hello')
109
+ })
110
+ })
111
+
112
+ describe('EdgeMarkerDefs', () => {
113
+ test('renders marker defs for conns with markers', () => {
114
+ const conns = [
115
+ { id: 'e1', source: '1', target: '2', markerEnd: 'arrowclosed' },
116
+ { id: 'e2', source: '2', target: '3', markerEnd: 'arrow' },
117
+ ]
118
+ const wrapper = mount(EdgeMarkerDefs, {
119
+ propsData: { conns },
120
+ })
121
+ const markers = wrapper.findAll('marker')
122
+ expect(markers.length).toBe(2)
123
+ })
124
+
125
+ test('deduplicates same marker type', () => {
126
+ const conns = [
127
+ { id: 'e1', source: '1', target: '2', markerEnd: 'arrowclosed' },
128
+ { id: 'e2', source: '2', target: '3', markerEnd: 'arrowclosed' },
129
+ ]
130
+ const wrapper = mount(EdgeMarkerDefs, {
131
+ propsData: { conns },
132
+ })
133
+ const markers = wrapper.findAll('marker')
134
+ expect(markers.length).toBe(1)
135
+ })
136
+
137
+ test('renders no markers when conns have none', () => {
138
+ const conns = [
139
+ { id: 'e1', source: '1', target: '2' },
140
+ ]
141
+ const wrapper = mount(EdgeMarkerDefs, {
142
+ propsData: { conns },
143
+ })
144
+ const markers = wrapper.findAll('marker')
145
+ expect(markers.length).toBe(0)
146
+ })
147
+ })