Skip to content

Commit 7c87f8c

Browse files
committed
feat: add unit tests for selection and interaction state in vyuh_node_flow package
fix: ensure canvas unlocks after selection drag and node deletion to prevent unexpected locking
1 parent 45c66b3 commit 7c87f8c

3 files changed

Lines changed: 153 additions & 1 deletion

File tree

packages/vyuh_node_flow/lib/src/editor/controller/node_api.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,13 @@ extension NodeApi<T, C> on NodeFlowController<T, C> {
177177
.toList();
178178

179179
runInAction(() {
180+
// Clear drag state if the deleted node is currently being dragged.
181+
// This prevents canvas from staying locked after node deletion.
182+
if (interaction.draggedNodeId.value == nodeId) {
183+
interaction.draggedNodeId.value = null;
184+
interaction.canvasLocked.value = false;
185+
}
186+
180187
// Detach context for nodes with GroupableMixin before removal
181188
// This disposes MobX reactions and cleans up the context
182189
if (nodeToDelete is GroupableMixin<T>) {

packages/vyuh_node_flow/lib/src/nodes/interaction_state.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,14 +356,16 @@ class InteractionState {
356356

357357
/// Finishes the current selection operation.
358358
///
359-
/// Clears the selection rectangle and resets tracking state.
359+
/// Clears the selection rectangle, resets tracking state, and unlocks the canvas.
360360
/// Should be called when the user releases the mouse button after
361361
/// drag-selecting nodes.
362362
void finishSelection() {
363363
runInAction(() {
364364
selectionStart.value = null;
365365
selectionRect.value = null;
366366
_previouslyIntersecting.clear();
367+
// Unlock canvas after selection drag completes
368+
canvasLocked.value = false;
367369
});
368370
}
369371

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/// Unit tests for InteractionState selection-related functionality.
2+
///
3+
/// Tests cover:
4+
/// - Selection drag canvas locking
5+
/// - Canvas unlocking on finishSelection
6+
@Tags(['unit'])
7+
library;
8+
9+
import 'package:flutter_test/flutter_test.dart';
10+
import 'package:vyuh_node_flow/src/nodes/interaction_state.dart';
11+
import 'package:vyuh_node_flow/src/graph/coordinates.dart';
12+
13+
void main() {
14+
late InteractionState state;
15+
16+
setUp(() {
17+
state = InteractionState();
18+
});
19+
20+
group('Selection Drag Canvas Locking', () {
21+
test('canvasLocked is false by default', () {
22+
expect(state.canvasLocked.value, isFalse);
23+
});
24+
25+
test('canvasLocked can be set to true for selection drag', () {
26+
state.canvasLocked.value = true;
27+
expect(state.canvasLocked.value, isTrue);
28+
});
29+
30+
test('finishSelection clears selection start point', () {
31+
// Set up selection drag state
32+
state.updateSelection(
33+
startPoint: const GraphPosition(Offset(100, 100)),
34+
rectangle: GraphRect.fromPoints(
35+
const GraphPosition(Offset(100, 100)),
36+
const GraphPosition(Offset(200, 200)),
37+
),
38+
);
39+
expect(state.selectionStart.value, isNotNull);
40+
41+
state.finishSelection();
42+
43+
expect(state.selectionStart.value, isNull);
44+
});
45+
46+
test('finishSelection clears selection rectangle', () {
47+
// Set up selection drag state
48+
state.updateSelection(
49+
startPoint: const GraphPosition(Offset(100, 100)),
50+
rectangle: GraphRect.fromPoints(
51+
const GraphPosition(Offset(100, 100)),
52+
const GraphPosition(Offset(200, 200)),
53+
),
54+
);
55+
expect(state.selectionRect.value, isNotNull);
56+
57+
state.finishSelection();
58+
59+
expect(state.selectionRect.value, isNull);
60+
});
61+
62+
test('finishSelection unlocks canvas', () {
63+
// Set up selection drag state with canvas locked
64+
state.canvasLocked.value = true;
65+
state.updateSelection(
66+
startPoint: const GraphPosition(Offset(100, 100)),
67+
rectangle: GraphRect.fromPoints(
68+
const GraphPosition(Offset(100, 100)),
69+
const GraphPosition(Offset(200, 200)),
70+
),
71+
);
72+
expect(state.canvasLocked.value, isTrue);
73+
74+
state.finishSelection();
75+
76+
// Canvas should be unlocked after finishing selection
77+
expect(state.canvasLocked.value, isFalse);
78+
});
79+
80+
test('isDrawingSelection returns true during selection drag', () {
81+
expect(state.isDrawingSelection, isFalse);
82+
83+
state.updateSelection(
84+
startPoint: const GraphPosition(Offset(100, 100)),
85+
rectangle: GraphRect.fromPoints(
86+
const GraphPosition(Offset(100, 100)),
87+
const GraphPosition(Offset(200, 200)),
88+
),
89+
);
90+
91+
expect(state.isDrawingSelection, isTrue);
92+
93+
state.finishSelection();
94+
95+
expect(state.isDrawingSelection, isFalse);
96+
});
97+
98+
test('isCanvasLocked getter reflects canvasLocked value', () {
99+
expect(state.isCanvasLocked, isFalse);
100+
101+
state.canvasLocked.value = true;
102+
expect(state.isCanvasLocked, isTrue);
103+
104+
state.finishSelection();
105+
expect(state.isCanvasLocked, isFalse);
106+
});
107+
});
108+
109+
group('Selection State Management', () {
110+
test('updateSelection sets both start point and rectangle', () {
111+
state.updateSelection(
112+
startPoint: const GraphPosition(Offset(50, 50)),
113+
rectangle: GraphRect.fromPoints(
114+
const GraphPosition(Offset(50, 50)),
115+
const GraphPosition(Offset(150, 150)),
116+
),
117+
);
118+
119+
expect(state.selectionStartPoint?.offset, equals(const Offset(50, 50)));
120+
expect(state.currentSelectionRect, isNotNull);
121+
});
122+
123+
test('resetState clears all selection state and unlocks canvas', () {
124+
// Set up various interaction state
125+
state.canvasLocked.value = true;
126+
state.updateSelection(
127+
startPoint: const GraphPosition(Offset(100, 100)),
128+
rectangle: GraphRect.fromPoints(
129+
const GraphPosition(Offset(100, 100)),
130+
const GraphPosition(Offset(200, 200)),
131+
),
132+
);
133+
state.draggedNodeId.value = 'test-node';
134+
135+
state.resetState();
136+
137+
expect(state.canvasLocked.value, isFalse);
138+
expect(state.selectionStart.value, isNull);
139+
expect(state.selectionRect.value, isNull);
140+
expect(state.draggedNodeId.value, isNull);
141+
});
142+
});
143+
}

0 commit comments

Comments
 (0)