@pixerate/editor-svelte 0.3.12 → 0.5.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.
@@ -0,0 +1,136 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { createCanvasDocking } from '../createCanvasDocking.svelte.js';
3
+ import type { CanvasNode } from '../types.js';
4
+
5
+ describe('createCanvasDocking', () => {
6
+ const createSampleNode = (id: string, type = 'default', x = 0, y = 0): CanvasNode => ({
7
+ id,
8
+ type,
9
+ position: { x, y },
10
+ data: {}
11
+ });
12
+
13
+ it('initializes with null drag and connect states', () => {
14
+ const docking = createCanvasDocking();
15
+
16
+ expect(docking.isDragging).toBe(false);
17
+ expect(docking.draggedNode).toBeNull();
18
+ expect(docking.draggedInitialPosition).toBeNull();
19
+ expect(docking.connectingHandle).toBe('');
20
+ expect(docking.connectingNode).toBeNull();
21
+ });
22
+
23
+ it('tracks drag start, records initial position, and sets isDragging', () => {
24
+ const docking = createCanvasDocking();
25
+ const node = createSampleNode('n1', 'default', 100, 200);
26
+
27
+ docking.handleNodeDragStart({ targetNode: node });
28
+
29
+ expect(docking.isDragging).toBe(true);
30
+ expect(docking.draggedNode).toBe(node);
31
+ expect(docking.draggedInitialPosition).toEqual({ x: 100, y: 200 });
32
+ });
33
+
34
+ it('fires onDockHover and onDockLeave as intersecting dock targets change', () => {
35
+ const onDockHover = vi.fn();
36
+ const onDockLeave = vi.fn();
37
+ const docking = createCanvasDocking({
38
+ isDockTarget: (n) => n.type === 'dockArea',
39
+ onDockHover,
40
+ onDockLeave
41
+ });
42
+
43
+ const draggedNode = createSampleNode('dragged', 'item', 50, 50);
44
+ const dockNode1 = createSampleNode('dock-1', 'dockArea', 0, 0);
45
+ const dockNode2 = createSampleNode('dock-2', 'dockArea', 200, 200);
46
+ const normalNode = createSampleNode('normal', 'item', 50, 50);
47
+
48
+ docking.handleNodeDragStart({ targetNode: draggedNode });
49
+
50
+ // Drag intersecting with dockNode1 and a non-dock normalNode
51
+ docking.handleNodeDrag(
52
+ { targetNode: draggedNode },
53
+ () => [dockNode1, normalNode]
54
+ );
55
+
56
+ expect(onDockHover).toHaveBeenCalledTimes(1);
57
+ expect(onDockHover).toHaveBeenCalledWith(dockNode1, draggedNode);
58
+ expect(onDockLeave).not.toHaveBeenCalled();
59
+
60
+ // Drag moves over dockNode2, leaving dockNode1
61
+ docking.handleNodeDrag(
62
+ { targetNode: draggedNode },
63
+ () => [dockNode2]
64
+ );
65
+
66
+ expect(onDockLeave).toHaveBeenCalledTimes(1);
67
+ expect(onDockLeave).toHaveBeenCalledWith(expect.objectContaining({ id: 'dock-1' }), draggedNode);
68
+ expect(onDockHover).toHaveBeenCalledTimes(2);
69
+ expect(onDockHover).toHaveBeenLastCalledWith(dockNode2, draggedNode);
70
+ });
71
+
72
+ it('fires onDockDrop and restores position on dock stop', () => {
73
+ const onDockDrop = vi.fn();
74
+ const onSave = vi.fn();
75
+ const updateNodePosition = vi.fn();
76
+
77
+ const docking = createCanvasDocking({
78
+ isDockTarget: (n) => n.type === 'dockArea',
79
+ restorePositionOnDock: true,
80
+ onDockDrop,
81
+ onSave
82
+ });
83
+
84
+ const draggedNode = createSampleNode('dragged', 'item', 100, 100);
85
+ const dockNode = createSampleNode('dock', 'dockArea', 0, 0);
86
+
87
+ docking.handleNodeDragStart({ targetNode: draggedNode });
88
+
89
+ // Node was dragged to new position
90
+ draggedNode.position = { x: 300, y: 300 };
91
+
92
+ docking.handleNodeDragStop(
93
+ { targetNode: draggedNode },
94
+ () => [dockNode],
95
+ updateNodePosition
96
+ );
97
+
98
+ expect(onDockDrop).toHaveBeenCalledWith(dockNode, draggedNode);
99
+ expect(updateNodePosition).toHaveBeenCalledWith('dragged', { x: 100, y: 100 });
100
+ expect(onSave).toHaveBeenCalled();
101
+ expect(docking.isDragging).toBe(false);
102
+ expect(docking.draggedNode).toBeNull();
103
+ });
104
+
105
+ it('tracks handle connect start and end', () => {
106
+ const docking = createCanvasDocking();
107
+ const node = createSampleNode('source-node');
108
+ const getNode = (id: string) => (id === 'source-node' ? node : undefined);
109
+
110
+ docking.handleConnectStart(
111
+ { nodeId: 'source-node', handleId: 'h1', handleType: 'source' },
112
+ getNode
113
+ );
114
+
115
+ expect(docking.connectingHandle).toBe('source');
116
+ expect(docking.connectingNode).toBe(node);
117
+
118
+ docking.handleConnectEnd();
119
+
120
+ expect(docking.connectingHandle).toBe('');
121
+ expect(docking.connectingNode).toBeNull();
122
+ });
123
+
124
+ it('resets all state cleanly', () => {
125
+ const docking = createCanvasDocking();
126
+ const node = createSampleNode('test');
127
+
128
+ docking.handleNodeDragStart({ targetNode: node });
129
+ expect(docking.isDragging).toBe(true);
130
+
131
+ docking.reset();
132
+ expect(docking.isDragging).toBe(false);
133
+ expect(docking.draggedNode).toBeNull();
134
+ expect(docking.draggedInitialPosition).toBeNull();
135
+ });
136
+ });
@@ -0,0 +1,249 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import {
3
+ linearTrajectory,
4
+ createFanOutTrajectory,
5
+ createBezierTrajectory,
6
+ runMultiNodeTransition
7
+ } from '../createCanvasTrajectory.js';
8
+ import {
9
+ computeNodesBoundingBox,
10
+ calculateDirectionalDisplacement,
11
+ calculateReflowDisplacement
12
+ } from '../createCanvasDisplacement.js';
13
+ import { createCanvasExplosion } from '../createCanvasExplosion.svelte.js';
14
+ import { createCanvasGraph } from '../createCanvasGraph.svelte.js';
15
+ import type { CanvasNode, CanvasEdge } from '../types.js';
16
+
17
+ describe('Canvas Trajectory Math & Transition Runner', () => {
18
+ it('evaluates linear trajectory at progress intervals', () => {
19
+ const origin = { x: 100, y: 100 };
20
+ const target = { x: 300, y: 200 };
21
+
22
+ const start = linearTrajectory(origin, target, 0, 0, 1);
23
+ expect(start.position).toEqual({ x: 100, y: 100 });
24
+ expect(start.scale).toBe(0);
25
+
26
+ const mid = linearTrajectory(origin, target, 0.5, 0, 1);
27
+ expect(mid.position).toEqual({ x: 200, y: 150 });
28
+ expect(mid.scale).toBe(0.5);
29
+
30
+ const end = linearTrajectory(origin, target, 1, 0, 1);
31
+ expect(end.position).toEqual({ x: 300, y: 200 });
32
+ expect(end.scale).toBe(1);
33
+ });
34
+
35
+ it('creates fan-out curved trajectory with scale and opacity', () => {
36
+ const origin = { x: 50, y: 100 };
37
+ const targetTop = { x: 250, y: 50 };
38
+ const targetBottom = { x: 250, y: 150 };
39
+
40
+ const trajectory = createFanOutTrajectory({
41
+ curvature: 0.5,
42
+ startScale: 0.2,
43
+ endScale: 1
44
+ });
45
+
46
+ const topPoint = trajectory(origin, targetTop, 0.5, 0, 2);
47
+ const bottomPoint = trajectory(origin, targetBottom, 0.5, 1, 2);
48
+
49
+ expect(topPoint.scale).toBeCloseTo(0.6);
50
+ expect(bottomPoint.scale).toBeCloseTo(0.6);
51
+ // Trajectory arc deflects top and bottom differently based on normalized index
52
+ expect(topPoint.position.y).not.toBe(bottomPoint.position.y);
53
+ });
54
+
55
+ it('creates custom bezier trajectory with control point calculator', () => {
56
+ const trajectory = createBezierTrajectory((orig, tgt) => ({
57
+ x: orig.x,
58
+ y: tgt.y
59
+ }));
60
+
61
+ const pt = trajectory({ x: 0, y: 0 }, { x: 100, y: 100 }, 0.5, 0, 1);
62
+ // Quadratic Bezier at t=0.5 with P0=(0,0), P1=(0,100), P2=(100,100)
63
+ // x = 0.25*0 + 0.5*0 + 0.25*100 = 25
64
+ // y = 0.25*0 + 0.5*100 + 0.25*100 = 75
65
+ expect(pt.position.x).toBe(25);
66
+ expect(pt.position.y).toBe(75);
67
+ });
68
+
69
+ it('runs multi-node transitions and notifies on completion', () => {
70
+ const node: CanvasNode = {
71
+ id: 'n1',
72
+ position: { x: 0, y: 0 },
73
+ data: {}
74
+ };
75
+
76
+ const onComplete = vi.fn();
77
+ const cancel = runMultiNodeTransition(
78
+ [
79
+ {
80
+ node,
81
+ from: { x: 0, y: 0 },
82
+ to: { x: 100, y: 100 },
83
+ duration: 0 // Immediate completion for testing
84
+ }
85
+ ],
86
+ { onComplete }
87
+ );
88
+
89
+ expect(node.position).toEqual({ x: 100, y: 100 });
90
+ expect(onComplete).toHaveBeenCalled();
91
+ cancel();
92
+ });
93
+ });
94
+
95
+ describe('Spatial Displacement Solvers', () => {
96
+ const createNode = (id: string, x: number, y: number, w = 150, h = 50): CanvasNode => ({
97
+ id,
98
+ position: { x, y },
99
+ measured: { width: w, height: h },
100
+ data: {}
101
+ });
102
+
103
+ it('computes bounding box for a set of nodes', () => {
104
+ const nodes = [createNode('a', 10, 20, 100, 40), createNode('b', 50, 100, 200, 80)];
105
+ const bbox = computeNodesBoundingBox(nodes);
106
+
107
+ expect(bbox.x).toBe(10);
108
+ expect(bbox.y).toBe(20);
109
+ expect(bbox.width).toBe(240); // 50 + 200 - 10 = 240
110
+ expect(bbox.height).toBe(160); // 100 + 80 - 20 = 160
111
+ });
112
+
113
+ it('calculates directional displacement shifting downstream nodes to make space', () => {
114
+ const origin = createNode('origin', 100, 100, 100, 50);
115
+ const downstream = createNode('downstream', 300, 100, 100, 50);
116
+ const upstream = createNode('upstream', 0, 100, 100, 50);
117
+
118
+ // Child cluster with width=200
119
+ const children = [
120
+ createNode('c1', 150, 80, 200, 40),
121
+ createNode('c2', 150, 120, 200, 40)
122
+ ];
123
+
124
+ const displaced = calculateDirectionalDisplacement({
125
+ existingNodes: [origin, downstream, upstream],
126
+ originNode: origin,
127
+ childNodes: children,
128
+ options: { direction: 'right', gap: 40 }
129
+ });
130
+
131
+ // Child cluster width is 200, gap is 40 -> deltaPrimary = 240
132
+ expect(displaced.has('downstream')).toBe(true);
133
+ expect(displaced.get('downstream')?.x).toBe(300 + 240);
134
+ // Upstream node remains untouched
135
+ expect(displaced.has('upstream')).toBe(false);
136
+ });
137
+
138
+ it('calculates Dagre reflow displacement anchoring origin node position', () => {
139
+ const origin = createNode('origin', 100, 100, 120, 60);
140
+ const downstream = createNode('downstream', 300, 100, 120, 60);
141
+ const edges: CanvasEdge[] = [{ id: 'e1', source: 'origin', target: 'downstream' }];
142
+
143
+ const children = [createNode('c1', 0, 0, 120, 60), createNode('c2', 0, 0, 120, 60)];
144
+ const childEdges: CanvasEdge[] = [
145
+ { id: 'ec1', source: 'origin', target: 'c1' },
146
+ { id: 'ec2', source: 'c1', target: 'downstream' }
147
+ ];
148
+
149
+ const displaced = calculateReflowDisplacement({
150
+ existingNodes: [origin, downstream],
151
+ existingEdges: edges,
152
+ originNode: origin,
153
+ childNodes: children,
154
+ childEdges,
155
+ options: { direction: 'LR', ranksep: 100 }
156
+ });
157
+
158
+ // Downstream node should be pushed further to make space for child rank
159
+ expect(displaced.has('downstream')).toBe(true);
160
+ expect(displaced.get('downstream')!.x).toBeGreaterThan(origin.position.x);
161
+ });
162
+ });
163
+
164
+ describe('createCanvasExplosion Rune Lifecycle & Reversibility', () => {
165
+ const createNode = (id: string, x: number, y: number, w = 150, h = 50): CanvasNode => ({
166
+ id,
167
+ position: { x, y },
168
+ measured: { width: w, height: h },
169
+ data: {}
170
+ });
171
+
172
+ it('explodes node, connects children, shifts downstream nodes, and collapses reversibly', () => {
173
+ const origin = createNode('origin', 100, 100);
174
+ const downstream = createNode('downstream', 300, 100);
175
+ const initialEdge: CanvasEdge = { id: 'e1', source: 'origin', target: 'downstream' };
176
+
177
+ const graph = createCanvasGraph({
178
+ initialNodes: [origin, downstream],
179
+ initialEdges: [initialEdge]
180
+ });
181
+
182
+ const explosion = createCanvasExplosion(graph);
183
+
184
+ expect(explosion.isExploded('origin')).toBe(false);
185
+
186
+ const child1 = createNode('c1', 250, 70);
187
+ const child2 = createNode('c2', 250, 130);
188
+
189
+ // 1. Explode
190
+ const exploded = explosion.explodeNode('origin', {
191
+ childNodes: [child1, child2],
192
+ duration: 0, // instantaneous for sync unit testing
193
+ connectParentToChildren: 'all',
194
+ directionalOptions: { direction: 'right', gap: 50 }
195
+ });
196
+
197
+ expect(exploded).toBe(true);
198
+ expect(explosion.isExploded('origin')).toBe(true);
199
+
200
+ // Check that children were added
201
+ expect(graph.nodes.some((n) => n.id === 'c1')).toBe(true);
202
+ expect(graph.nodes.some((n) => n.id === 'c2')).toBe(true);
203
+
204
+ // Check that parent was connected to children
205
+ expect(graph.edges.some((e) => e.source === 'origin' && e.target === 'c1')).toBe(true);
206
+ expect(graph.edges.some((e) => e.source === 'origin' && e.target === 'c2')).toBe(true);
207
+
208
+ // Check that downstream node was displaced rightward
209
+ const displacedDownstream = graph.nodes.find((n) => n.id === 'downstream')!;
210
+ expect(displacedDownstream.position.x).toBeGreaterThan(300);
211
+
212
+ // 2. Collapse
213
+ const collapsed = explosion.collapseNode('origin', {
214
+ duration: 0
215
+ });
216
+
217
+ expect(collapsed).toBe(true);
218
+ expect(explosion.isExploded('origin')).toBe(false);
219
+
220
+ // Check that children were removed from graph
221
+ expect(graph.nodes.some((n) => n.id === 'c1')).toBe(false);
222
+ expect(graph.nodes.some((n) => n.id === 'c2')).toBe(false);
223
+
224
+ // Check that child edges were pruned
225
+ expect(graph.edges.some((e) => e.source === 'origin' && e.target === 'c1')).toBe(false);
226
+
227
+ // Check that downstream node returned to its exact pre-explosion baseline coordinate
228
+ const restoredDownstream = graph.nodes.find((n) => n.id === 'downstream')!;
229
+ expect(restoredDownstream.position.x).toBe(300);
230
+ expect(restoredDownstream.position.y).toBe(100);
231
+ });
232
+
233
+ it('toggles explosion with toggleExplode', () => {
234
+ const origin = createNode('origin', 100, 100);
235
+ const graph = createCanvasGraph({ initialNodes: [origin] });
236
+ const explosion = createCanvasExplosion(graph);
237
+
238
+ const child = createNode('c1', 200, 100);
239
+
240
+ explosion.toggleExplode('origin', {
241
+ childNodes: [child],
242
+ duration: 0
243
+ });
244
+ expect(explosion.isExploded('origin')).toBe(true);
245
+
246
+ explosion.toggleExplode('origin', { childNodes: [child], duration: 0 }, { duration: 0 });
247
+ expect(explosion.isExploded('origin')).toBe(false);
248
+ });
249
+ });
@@ -0,0 +1,159 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import {
3
+ createCanvasGraph,
4
+ calculateGraphDifferences,
5
+ replaceNodeInGraph
6
+ } from '../createCanvasGraph.svelte.js';
7
+ import type { CanvasNode, CanvasEdge } from '../types.js';
8
+
9
+ describe('createCanvasGraph & Graph Operations', () => {
10
+ const createSampleNode = (id: string, x = 0, y = 0): CanvasNode => ({
11
+ id,
12
+ position: { x, y },
13
+ data: { label: `Node ${id}` }
14
+ });
15
+
16
+ const createSampleEdge = (id: string, source: string, target: string): CanvasEdge => ({
17
+ id,
18
+ source,
19
+ target
20
+ });
21
+
22
+ it('initializes with default empty or provided nodes and edges', () => {
23
+ const graph = createCanvasGraph({
24
+ initialNodes: [createSampleNode('1')],
25
+ initialEdges: [createSampleEdge('e1', '1', '2')]
26
+ });
27
+
28
+ expect(graph.nodes).toHaveLength(1);
29
+ expect(graph.edges).toHaveLength(1);
30
+ expect(graph.nodes[0].id).toBe('1');
31
+ });
32
+
33
+ it('adds nodes and ignores duplicates', () => {
34
+ const onNodesChange = vi.fn();
35
+ const graph = createCanvasGraph({ onNodesChange });
36
+
37
+ graph.addNodeOrNodes(createSampleNode('1'));
38
+ expect(graph.nodes).toHaveLength(1);
39
+ expect(onNodesChange).toHaveBeenCalledTimes(1);
40
+
41
+ // Adding existing node does not duplicate
42
+ graph.addNodeOrNodes(createSampleNode('1'));
43
+ expect(graph.nodes).toHaveLength(1);
44
+
45
+ // Adding multiple nodes
46
+ graph.addNodeOrNodes([createSampleNode('2'), createSampleNode('3')]);
47
+ expect(graph.nodes).toHaveLength(3);
48
+ });
49
+
50
+ it('adds and removes edges cleanly', () => {
51
+ const onEdgesChange = vi.fn();
52
+ const graph = createCanvasGraph({ onEdgesChange });
53
+
54
+ const e1 = createSampleEdge('e1', '1', '2');
55
+ const e2 = createSampleEdge('e2', '2', '3');
56
+
57
+ graph.addEdge(e1);
58
+ expect(graph.edges).toHaveLength(1);
59
+
60
+ graph.addEdges([e2]);
61
+ expect(graph.edges).toHaveLength(2);
62
+
63
+ graph.removeEdges([e1]);
64
+ expect(graph.edges).toHaveLength(1);
65
+ expect(graph.edges[0].id).toBe('e2');
66
+ });
67
+
68
+ it('removes nodes by reference or ID matching', () => {
69
+ const n1 = createSampleNode('1');
70
+ const n2 = createSampleNode('2');
71
+ const graph = createCanvasGraph({ initialNodes: [n1, n2] });
72
+
73
+ graph.removeNodes([n1]);
74
+ expect(graph.nodes).toHaveLength(1);
75
+ expect(graph.nodes[0].id).toBe('2');
76
+ });
77
+
78
+ it('replaces a node and preserves connected edges when keepEdges is true', () => {
79
+ const n1 = createSampleNode('1');
80
+ const n2 = createSampleNode('2');
81
+ const e1 = createSampleEdge('e1', '1', '2');
82
+ const n1Replacement = createSampleNode('new-1', 10, 10);
83
+
84
+ const result = replaceNodeInGraph([n1, n2], [e1], n1, n1Replacement, true);
85
+ expect(result.nodes.some((n) => n.id === 'new-1')).toBe(true);
86
+ expect(result.nodes.some((n) => n.id === '1')).toBe(false);
87
+
88
+ // Edge should have updated source to new-1
89
+ expect(result.edges).toHaveLength(1);
90
+ expect(result.edges[0].source).toBe('new-1');
91
+ expect(result.edges[0].target).toBe('2');
92
+ });
93
+
94
+ it('replaces a node without preserving edges when keepEdges is false', () => {
95
+ const n1 = createSampleNode('1');
96
+ const n2 = createSampleNode('2');
97
+ const e1 = createSampleEdge('e1', '1', '2');
98
+ const n1Replacement = createSampleNode('new-1', 10, 10);
99
+
100
+ const result = replaceNodeInGraph([n1, n2], [e1], n1, n1Replacement, false);
101
+ expect(result.nodes.some((n) => n.id === 'new-1')).toBe(true);
102
+ expect(result.edges).toHaveLength(1);
103
+ expect(result.edges[0].source).toBe('1'); // Unchanged
104
+ });
105
+
106
+ it('adds connected nodes connecting inbound and outbound targets', () => {
107
+ const graph = createCanvasGraph();
108
+ const inNode = createSampleNode('in');
109
+ const outNode = createSampleNode('out');
110
+ const midNode = createSampleNode('mid');
111
+
112
+ graph.addNodeOrNodes([inNode, outNode]);
113
+
114
+ graph.addConnectedNodes({
115
+ nodesToAdd: [midNode],
116
+ inboundNodes: [inNode],
117
+ outboundNodes: [outNode],
118
+ getEdgeLabel: (src, tgt) => `${src.id}->${tgt.id}`
119
+ });
120
+
121
+ expect(graph.nodes).toHaveLength(3);
122
+ expect(graph.edges).toHaveLength(2);
123
+ expect(graph.edges.find((e) => e.source === 'in' && e.target === 'mid')?.label).toBe('in->mid');
124
+ expect(graph.edges.find((e) => e.source === 'mid' && e.target === 'out')?.label).toBe('mid->out');
125
+ });
126
+
127
+ it('accurately calculates graph differences against a baseline', () => {
128
+ const n1 = createSampleNode('1', 0, 0);
129
+ const n2 = createSampleNode('2', 50, 50);
130
+ const e1 = createSampleEdge('e1', '1', '2');
131
+
132
+ const baseline = {
133
+ nodes: [n1, n2],
134
+ edges: [e1]
135
+ };
136
+
137
+ // Node 2 position changed, node 3 added, edge 1 removed, edge 2 added
138
+ const n2Changed = createSampleNode('2', 100, 100);
139
+ const n3Added = createSampleNode('3', 200, 200);
140
+ const e2Added = createSampleEdge('e2', '2', '3');
141
+
142
+ const current = {
143
+ nodes: [n1, n2Changed, n3Added],
144
+ edges: [e2Added]
145
+ };
146
+
147
+ const diff = calculateGraphDifferences(current, baseline);
148
+
149
+ expect(diff.changedNodes).toHaveLength(2); // n2 changed + n3 newly added
150
+ expect(diff.changedNodes.map((n) => n.id)).toContain('2');
151
+ expect(diff.changedNodes.map((n) => n.id)).toContain('3');
152
+ expect(diff.removedNodes).toHaveLength(0);
153
+
154
+ expect(diff.removedEdges).toHaveLength(1);
155
+ expect(diff.removedEdges[0].id).toBe('e1');
156
+ expect(diff.changedEdges).toHaveLength(1);
157
+ expect(diff.changedEdges[0].id).toBe('e2');
158
+ });
159
+ });
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import {
3
+ createCanvasInteractions,
4
+ defaultSvelteFlowPreset,
5
+ miroCompatiblePreset,
6
+ restorePanelPointerEvents
7
+ } from '../createCanvasInteractions.svelte.js';
8
+
9
+ describe('createCanvasInteractions', () => {
10
+ it('exposes valid default presets', () => {
11
+ expect(defaultSvelteFlowPreset.snapGrid).toEqual([25, 25]);
12
+ expect(defaultSvelteFlowPreset.panOnScroll).toBe(false);
13
+ expect(defaultSvelteFlowPreset.selectionOnDrag).toBe(false);
14
+
15
+ expect(miroCompatiblePreset.selectionOnDrag).toBe(true);
16
+ expect(miroCompatiblePreset.panOnDrag).toEqual([1, 2]);
17
+ });
18
+
19
+ it('toggles spacebar panning mode on Space keydown and keyup', () => {
20
+ const target = new EventTarget();
21
+ const onPanModeChange = vi.fn();
22
+
23
+ const interactions = createCanvasInteractions({
24
+ targetElement: target as unknown as HTMLElement,
25
+ onPanModeChange
26
+ });
27
+
28
+ const cleanup = interactions.attachListeners(target as unknown as HTMLElement);
29
+
30
+ // Press Space
31
+ const downEvent = new KeyboardEvent('keydown', { key: ' ' });
32
+ target.dispatchEvent(downEvent);
33
+
34
+ expect(interactions.isSpacebarPanning).toBe(true);
35
+ expect(onPanModeChange).toHaveBeenCalledWith(true);
36
+
37
+ // Release Space
38
+ const upEvent = new KeyboardEvent('keyup', { key: ' ' });
39
+ target.dispatchEvent(upEvent);
40
+
41
+ expect(interactions.isSpacebarPanning).toBe(false);
42
+ expect(onPanModeChange).toHaveBeenCalledWith(false);
43
+
44
+ cleanup();
45
+ });
46
+
47
+ it('detects trackpad pinch on wheel event with ctrlKey', () => {
48
+ const target = new EventTarget();
49
+ const onTrackpadDetected = vi.fn();
50
+
51
+ const interactions = createCanvasInteractions({
52
+ targetElement: target as unknown as HTMLElement,
53
+ onTrackpadDetected
54
+ });
55
+
56
+ const cleanup = interactions.attachListeners(target as unknown as HTMLElement);
57
+
58
+ expect(interactions.trackpadDetected).toBe(false);
59
+
60
+ // Normal wheel without ctrlKey
61
+ const wheelEvent = new WheelEvent('wheel', { ctrlKey: false });
62
+ target.dispatchEvent(wheelEvent);
63
+ expect(interactions.trackpadDetected).toBe(false);
64
+ expect(onTrackpadDetected).not.toHaveBeenCalled();
65
+
66
+ // Trackpad pinch with ctrlKey
67
+ const pinchEvent = new WheelEvent('wheel', { ctrlKey: true });
68
+ target.dispatchEvent(pinchEvent);
69
+ expect(interactions.trackpadDetected).toBe(true);
70
+ expect(onTrackpadDetected).toHaveBeenCalledTimes(1);
71
+
72
+ cleanup();
73
+ });
74
+
75
+ it('restores pointer events on DOM panels', () => {
76
+ const panel = document.createElement('div');
77
+ panel.className = 'svelte-flow__panel';
78
+ panel.style.pointerEvents = 'none';
79
+ document.body.appendChild(panel);
80
+
81
+ restorePanelPointerEvents();
82
+
83
+ expect(panel.style.pointerEvents).toBe('auto');
84
+ document.body.removeChild(panel);
85
+ });
86
+ });