w-flow-vue 1.0.21 → 1.0.23

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.
@@ -1,421 +1,432 @@
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
- })
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
+ //選取已移至onNodeDragPrepare(節點mousedown當下), 使拖曳延後啟動不影響選取時機
262
+ test('onNodeDragPrepare selects the node', () => {
263
+ const wrapper = createWrapper()
264
+ const node = wrapper.vm.nodeById('1')
265
+ wrapper.vm.onNodeDragPrepare({ node })
266
+ expect(wrapper.vm.selectedNodes).toContain('1')
267
+ expect(wrapper.emitted('node-drag-start')).toBeFalsy()
268
+ wrapper.destroy()
269
+ })
270
+
271
+ test('onNodeDragStart starts the drag and emits node-drag-start', () => {
272
+ const wrapper = createWrapper()
273
+ const node = wrapper.vm.nodeById('1')
274
+ wrapper.vm.onNodeDragStart({
275
+ node,
276
+ event: { clientX: 100, clientY: 100 },
277
+ })
278
+ expect(wrapper.vm.isDraggingNode).toBe(true)
279
+ expect(wrapper.vm.draggingNodeId).toBe('1')
280
+ expect(wrapper.emitted('node-drag-start')).toBeTruthy()
281
+ wrapper.destroy()
282
+ })
283
+ })
284
+
285
+ describe('connection', () => {
286
+ test('onConnectStart sets connecting state', () => {
287
+ const wrapper = createWrapper()
288
+ wrapper.vm.onConnectStart({
289
+ nodeId: '1',
290
+ handleId: 'source',
291
+ handleType: 'source',
292
+ handlePosition: 'bottom',
293
+ })
294
+ expect(wrapper.vm.isConnecting).toBe(true)
295
+ expect(wrapper.emitted('connect-start')).toBeTruthy()
296
+ wrapper.destroy()
297
+ })
298
+ })
299
+
300
+ describe('props sync', () => {
301
+ test('nodes change via opt prop', async () => {
302
+ const wrapper = createWrapper()
303
+ const newNodes = [
304
+ { id: 'a', type: 'basic', name: 'A', position: { x: 0, y: 0 } },
305
+ ]
306
+ await wrapper.setProps({ opt: { nodes: newNodes, conns: [] } })
307
+ expect(wrapper.vm.nodes).toHaveLength(1)
308
+ expect(wrapper.vm.nodes[0].id).toBe('a')
309
+ wrapper.destroy()
310
+ })
311
+
312
+ test('conns change via opt prop', async () => {
313
+ const wrapper = createWrapper()
314
+ await wrapper.setProps({ opt: { nodes: sampleNodes, conns: [] } })
315
+ expect(wrapper.vm.conns).toHaveLength(0)
316
+ wrapper.destroy()
317
+ })
318
+ })
319
+
320
+ describe('keyboard', () => {
321
+ test('delete key triggers deletion of selected', () => {
322
+ const wrapper = createWrapper({ deleteKeyEnabled: true })
323
+ wrapper.vm.setSelectedNodes(['3'])
324
+ const event = new KeyboardEvent('keydown', { key: 'Backspace' })
325
+ document.dispatchEvent(event)
326
+ expect(wrapper.vm.nodeById('3')).toBeNull()
327
+ wrapper.destroy()
328
+ })
329
+ })
330
+
331
+ describe('locked', () => {
332
+ test('initially unlocked', () => {
333
+ const wrapper = createWrapper()
334
+ expect(wrapper.vm.locked).toBe(false)
335
+ wrapper.destroy()
336
+ })
337
+
338
+ test('toggleInteractive locks and unlocks', () => {
339
+ const wrapper = createWrapper()
340
+ wrapper.vm.toggleInteractive()
341
+ expect(wrapper.vm.locked).toBe(true)
342
+ wrapper.vm.toggleInteractive()
343
+ expect(wrapper.vm.locked).toBe(false)
344
+ wrapper.destroy()
345
+ })
346
+
347
+ test('locked blocks node drag', () => {
348
+ const wrapper = createWrapper()
349
+ wrapper.vm.toggleInteractive()
350
+ const node = wrapper.vm.nodeById('1')
351
+ const origX = node.position.x
352
+ wrapper.vm.onNodeDragStart({ node, event: { clientX: 0, clientY: 0 } })
353
+ expect(wrapper.vm.isDraggingNode).toBe(false)
354
+ expect(node.position.x).toBe(origX)
355
+ wrapper.destroy()
356
+ })
357
+
358
+ test('locked blocks node resize', () => {
359
+ const wrapper = createWrapper()
360
+ wrapper.vm.toggleInteractive()
361
+ wrapper.vm.onNodeResize({ nodeId: '1', width: 200, height: 100, x: 0, y: 0 })
362
+ expect(wrapper.vm.dragPositions['1']).toBeFalsy()
363
+ wrapper.destroy()
364
+ })
365
+
366
+ test('locked blocks delete key', () => {
367
+ const wrapper = createWrapper({ deleteKeyEnabled: true })
368
+ wrapper.vm.toggleInteractive()
369
+ wrapper.vm.setSelectedNodes(['3'])
370
+ const event = new KeyboardEvent('keydown', { key: 'Backspace' })
371
+ document.dispatchEvent(event)
372
+ expect(wrapper.vm.nodeById('3')).not.toBeNull()
373
+ wrapper.destroy()
374
+ })
375
+
376
+ test('locked passes to NodeRenderer', async () => {
377
+ const wrapper = createWrapper()
378
+ wrapper.vm.toggleInteractive()
379
+ await wrapper.vm.$nextTick()
380
+ const nodeRenderer = wrapper.findComponent({ name: 'NodeRenderer' })
381
+ expect(nodeRenderer.props('locked')).toBe(true)
382
+ wrapper.destroy()
383
+ })
384
+
385
+ test('locked passes to EdgeRenderer', async () => {
386
+ const wrapper = createWrapper()
387
+ wrapper.vm.toggleInteractive()
388
+ await wrapper.vm.$nextTick()
389
+ const edgeRenderer = wrapper.findComponent({ name: 'EdgeRenderer' })
390
+ expect(edgeRenderer.props('locked')).toBe(true)
391
+ wrapper.destroy()
392
+ })
393
+
394
+ test('locked hides node settings icon', async () => {
395
+ const wrapper = createWrapper()
396
+ // Settings icon requires hover + unlocked; locked should hide it
397
+ wrapper.vm.toggleInteractive()
398
+ await wrapper.vm.$nextTick()
399
+ expect(wrapper.findAll('.vue-flow__node-settings').length).toBe(0)
400
+ wrapper.destroy()
401
+ })
402
+
403
+ test('locked hides node resize handles', async () => {
404
+ const wrapper = createWrapper()
405
+ wrapper.vm.toggleInteractive()
406
+ await wrapper.vm.$nextTick()
407
+ expect(wrapper.findAll('.vue-flow__resize--top-left').length).toBe(0)
408
+ expect(wrapper.findAll('.vue-flow__resize--bottom-right').length).toBe(0)
409
+ wrapper.destroy()
410
+ })
411
+
412
+ test('locked hides node handles via CSS class', async () => {
413
+ const wrapper = createWrapper()
414
+ wrapper.vm.toggleInteractive()
415
+ await wrapper.vm.$nextTick()
416
+ const lockedNodes = wrapper.findAll('.vue-flow__node--locked')
417
+ expect(lockedNodes.length).toBeGreaterThan(0)
418
+ wrapper.destroy()
419
+ })
420
+
421
+ test('locked hides edge settings icon', async () => {
422
+ const wrapper = createWrapper()
423
+ wrapper.vm.toggleInteractive()
424
+ await wrapper.vm.$nextTick()
425
+ const edgeWrappers = wrapper.findAllComponents({ name: 'EdgeWrapper' })
426
+ edgeWrappers.wrappers.forEach(ew => {
427
+ expect(ew.props('locked')).toBe(true)
428
+ })
429
+ wrapper.destroy()
430
+ })
431
+ })
432
+ })