@node-edit-utils/core 2.3.3 → 2.3.4

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 (101) hide show
  1. package/dist/lib/viewport/label/getViewportLabelOverlay.d.ts +1 -0
  2. package/dist/lib/viewport/label/index.d.ts +5 -3
  3. package/dist/lib/viewport/label/isViewportDragging.d.ts +2 -0
  4. package/dist/lib/viewport/label/refreshViewportLabel.d.ts +8 -0
  5. package/dist/lib/viewport/label/removeViewportLabel.d.ts +5 -0
  6. package/dist/lib/viewport/label/setupViewportDrag.d.ts +1 -0
  7. package/dist/node-edit-utils.cjs.js +100 -62
  8. package/dist/node-edit-utils.esm.js +100 -62
  9. package/dist/node-edit-utils.umd.js +100 -62
  10. package/dist/node-edit-utils.umd.min.js +1 -1
  11. package/dist/styles.css +1 -1
  12. package/package.json +7 -2
  13. package/src/lib/canvas/createCanvasObserver.test.ts +242 -0
  14. package/src/lib/canvas/disableCanvasKeyboard.test.ts +53 -0
  15. package/src/lib/canvas/disableCanvasKeyboard.ts +1 -1
  16. package/src/lib/canvas/disableCanvasTextMode.test.ts +53 -0
  17. package/src/lib/canvas/disableCanvasTextMode.ts +1 -1
  18. package/src/lib/canvas/enableCanvasKeyboard.test.ts +53 -0
  19. package/src/lib/canvas/enableCanvasKeyboard.ts +1 -1
  20. package/src/lib/canvas/enableCanvasTextMode.test.ts +53 -0
  21. package/src/lib/canvas/enableCanvasTextMode.ts +1 -1
  22. package/src/lib/canvas/helpers/applyCanvasState.test.ts +119 -0
  23. package/src/lib/canvas/helpers/applyCanvasState.ts +1 -1
  24. package/src/lib/canvas/helpers/getCanvasContainer.test.ts +62 -0
  25. package/src/lib/canvas/helpers/getCanvasContainerOrBody.test.ts +51 -0
  26. package/src/lib/canvas/helpers/getCanvasWindowValue.test.ts +116 -0
  27. package/src/lib/helpers/adjustForZoom.test.ts +65 -0
  28. package/src/lib/helpers/createDragHandler.test.ts +325 -0
  29. package/src/lib/helpers/getNodeProvider.test.ts +71 -0
  30. package/src/lib/helpers/getNodeTools.test.ts +50 -0
  31. package/src/lib/helpers/getViewportDimensions.test.ts +93 -0
  32. package/src/lib/helpers/observer/connectMutationObserver.test.ts +127 -0
  33. package/src/lib/helpers/observer/connectResizeObserver.test.ts +147 -0
  34. package/src/lib/helpers/parseTransform.test.ts +117 -0
  35. package/src/lib/helpers/toggleClass.test.ts +71 -0
  36. package/src/lib/helpers/withRAF.test.ts +439 -0
  37. package/src/lib/node-tools/createNodeTools.test.ts +373 -0
  38. package/src/lib/node-tools/events/click/handleNodeClick.test.ts +109 -0
  39. package/src/lib/node-tools/events/setupEventListener.test.ts +136 -0
  40. package/src/lib/node-tools/highlight/clearHighlightFrame.test.ts +88 -0
  41. package/src/lib/node-tools/highlight/createCornerHandles.test.ts +150 -0
  42. package/src/lib/node-tools/highlight/createHighlightFrame.test.ts +237 -0
  43. package/src/lib/node-tools/highlight/createTagLabel.test.ts +135 -0
  44. package/src/lib/node-tools/highlight/createToolsContainer.test.ts +97 -0
  45. package/src/lib/node-tools/highlight/helpers/getElementBounds.test.ts +158 -0
  46. package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.test.ts +78 -0
  47. package/src/lib/node-tools/highlight/helpers/getScreenBounds.test.ts +133 -0
  48. package/src/lib/node-tools/highlight/highlightNode.test.ts +213 -0
  49. package/src/lib/node-tools/highlight/refreshHighlightFrame.test.ts +323 -0
  50. package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.test.ts +110 -0
  51. package/src/lib/node-tools/select/helpers/getElementsFromPoint.test.ts +109 -0
  52. package/src/lib/node-tools/select/helpers/isInsideComponent.test.ts +81 -0
  53. package/src/lib/node-tools/select/helpers/isInsideViewport.test.ts +82 -0
  54. package/src/lib/node-tools/select/helpers/targetSameCandidates.test.ts +81 -0
  55. package/src/lib/node-tools/select/selectNode.test.ts +238 -0
  56. package/src/lib/node-tools/text/events/setupKeydownHandler.test.ts +91 -0
  57. package/src/lib/node-tools/text/events/setupMutationObserver.test.ts +213 -0
  58. package/src/lib/node-tools/text/events/setupNodeListeners.test.ts +133 -0
  59. package/src/lib/node-tools/text/helpers/enterTextEditMode.test.ts +50 -0
  60. package/src/lib/node-tools/text/helpers/handleTextChange.test.ts +201 -0
  61. package/src/lib/node-tools/text/helpers/hasTextContent.test.ts +101 -0
  62. package/src/lib/node-tools/text/helpers/insertLineBreak.test.ts +96 -0
  63. package/src/lib/node-tools/text/helpers/makeNodeEditable.test.ts +56 -0
  64. package/src/lib/node-tools/text/helpers/makeNodeNonEditable.test.ts +57 -0
  65. package/src/lib/node-tools/text/helpers/shouldEnterTextEditMode.test.ts +61 -0
  66. package/src/lib/node-tools/text/nodeText.test.ts +233 -0
  67. package/src/lib/post-message/processPostMessage.test.ts +218 -0
  68. package/src/lib/post-message/sendPostMessage.test.ts +120 -0
  69. package/src/lib/styles/styles.css +2 -2
  70. package/src/lib/viewport/createViewport.test.ts +267 -0
  71. package/src/lib/viewport/createViewport.ts +7 -4
  72. package/src/lib/viewport/events/setupEventListener.test.ts +103 -0
  73. package/src/lib/viewport/label/getViewportLabelOverlay.test.ts +77 -0
  74. package/src/lib/viewport/label/{getViewportLabelsOverlay.ts → getViewportLabelOverlay.ts} +2 -1
  75. package/src/lib/viewport/label/helpers/getLabelPosition.test.ts +51 -0
  76. package/src/lib/viewport/label/helpers/getTransformValues.test.ts +59 -0
  77. package/src/lib/viewport/label/helpers/getZoomValue.test.ts +53 -0
  78. package/src/lib/viewport/label/helpers/selectFirstViewportNode.test.ts +105 -0
  79. package/src/lib/viewport/label/helpers/selectFirstViewportNode.ts +8 -0
  80. package/src/lib/viewport/label/index.ts +5 -3
  81. package/src/lib/viewport/label/isViewportDragging.test.ts +35 -0
  82. package/src/lib/viewport/label/isViewportDragging.ts +9 -0
  83. package/src/lib/viewport/label/refreshViewportLabel.test.ts +105 -0
  84. package/src/lib/viewport/label/refreshViewportLabel.ts +50 -0
  85. package/src/lib/viewport/label/refreshViewportLabels.test.ts +107 -0
  86. package/src/lib/viewport/label/refreshViewportLabels.ts +17 -50
  87. package/src/lib/viewport/label/removeViewportLabel.test.ts +67 -0
  88. package/src/lib/viewport/label/removeViewportLabel.ts +20 -0
  89. package/src/lib/viewport/label/setupViewportDrag.test.ts +249 -0
  90. package/src/lib/viewport/label/{setupViewportLabelDrag.ts → setupViewportDrag.ts} +14 -14
  91. package/src/lib/viewport/resize/createResizeHandle.test.ts +37 -0
  92. package/src/lib/viewport/resize/createResizePresets.test.ts +75 -0
  93. package/src/lib/viewport/resize/updateActivePreset.test.ts +92 -0
  94. package/src/lib/viewport/width/calcConstrainedWidth.test.ts +47 -0
  95. package/src/lib/viewport/width/calcWidth.test.ts +68 -0
  96. package/src/lib/viewport/width/updateWidth.test.ts +78 -0
  97. package/src/lib/window/bindToWindow.test.ts +166 -0
  98. package/dist/lib/viewport/label/getViewportLabelsOverlay.d.ts +0 -1
  99. package/dist/lib/viewport/label/isViewportLabelDragging.d.ts +0 -2
  100. package/dist/lib/viewport/label/setupViewportLabelDrag.d.ts +0 -1
  101. package/src/lib/viewport/label/isViewportLabelDragging.ts +0 -9
@@ -0,0 +1,82 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
+ import { isInsideViewport } from "./isInsideViewport";
3
+
4
+ describe("isInsideViewport", () => {
5
+ let nodeProvider: HTMLElement;
6
+ let viewport: HTMLElement;
7
+ let element: HTMLElement;
8
+
9
+ beforeEach(() => {
10
+ nodeProvider = document.createElement("div");
11
+ nodeProvider.setAttribute("data-role", "node-provider");
12
+ document.body.appendChild(nodeProvider);
13
+
14
+ viewport = document.createElement("div");
15
+ viewport.classList.add("viewport");
16
+ nodeProvider.appendChild(viewport);
17
+
18
+ element = document.createElement("div");
19
+ viewport.appendChild(element);
20
+ });
21
+
22
+ afterEach(() => {
23
+ if (document.body.contains(nodeProvider)) {
24
+ document.body.removeChild(nodeProvider);
25
+ }
26
+ });
27
+
28
+ it("should return true when element is inside viewport", () => {
29
+ const result = isInsideViewport(element);
30
+
31
+ expect(result).toBe(true);
32
+ });
33
+
34
+ it("should return true when element is nested inside viewport", () => {
35
+ const nested = document.createElement("div");
36
+ element.appendChild(nested);
37
+
38
+ const result = isInsideViewport(nested);
39
+
40
+ expect(result).toBe(true);
41
+ });
42
+
43
+ it("should return false when element is not inside viewport", () => {
44
+ const outsideElement = document.createElement("div");
45
+ document.body.appendChild(outsideElement);
46
+
47
+ const result = isInsideViewport(outsideElement);
48
+
49
+ expect(result).toBe(false);
50
+ document.body.removeChild(outsideElement);
51
+ });
52
+
53
+ it("should stop at node-provider", () => {
54
+ const elementOutsideViewport = document.createElement("div");
55
+ nodeProvider.appendChild(elementOutsideViewport);
56
+
57
+ const result = isInsideViewport(elementOutsideViewport);
58
+
59
+ expect(result).toBe(false);
60
+ });
61
+
62
+ it("should return false when element is direct child of node-provider", () => {
63
+ const directChild = document.createElement("div");
64
+ nodeProvider.appendChild(directChild);
65
+
66
+ const result = isInsideViewport(directChild);
67
+
68
+ expect(result).toBe(false);
69
+ });
70
+
71
+ it("should handle multiple viewports", () => {
72
+ const viewport2 = document.createElement("div");
73
+ viewport2.classList.add("viewport");
74
+ nodeProvider.appendChild(viewport2);
75
+ const element2 = document.createElement("div");
76
+ viewport2.appendChild(element2);
77
+
78
+ expect(isInsideViewport(element)).toBe(true);
79
+ expect(isInsideViewport(element2)).toBe(true);
80
+ });
81
+ });
82
+
@@ -0,0 +1,81 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { targetSameCandidates } from "./targetSameCandidates";
3
+
4
+ describe("targetSameCandidates", () => {
5
+ const element1 = document.createElement("div");
6
+ const element2 = document.createElement("div");
7
+ const element3 = document.createElement("div");
8
+
9
+ it("should return true when arrays are identical", () => {
10
+ const cache = [element1, element2, element3];
11
+ const current = [element1, element2, element3];
12
+
13
+ const result = targetSameCandidates(cache, current);
14
+
15
+ expect(result).toBe(true);
16
+ });
17
+
18
+ it("should return false when arrays have different lengths", () => {
19
+ const cache = [element1, element2];
20
+ const current = [element1, element2, element3];
21
+
22
+ const result = targetSameCandidates(cache, current);
23
+
24
+ expect(result).toBe(false);
25
+ });
26
+
27
+ it("should return false when arrays have same length but different elements", () => {
28
+ const cache = [element1, element2];
29
+ const current = [element2, element3];
30
+
31
+ const result = targetSameCandidates(cache, current);
32
+
33
+ expect(result).toBe(false);
34
+ });
35
+
36
+ it("should return false when arrays have same elements in different order", () => {
37
+ const cache = [element1, element2];
38
+ const current = [element2, element1];
39
+
40
+ const result = targetSameCandidates(cache, current);
41
+
42
+ expect(result).toBe(false);
43
+ });
44
+
45
+ it("should return true for empty arrays", () => {
46
+ const cache: Element[] = [];
47
+ const current: Element[] = [];
48
+
49
+ const result = targetSameCandidates(cache, current);
50
+
51
+ expect(result).toBe(true);
52
+ });
53
+
54
+ it("should return false when one array is empty", () => {
55
+ const cache: Element[] = [];
56
+ const current = [element1];
57
+
58
+ const result = targetSameCandidates(cache, current);
59
+
60
+ expect(result).toBe(false);
61
+ });
62
+
63
+ it("should return true for single element arrays with same element", () => {
64
+ const cache = [element1];
65
+ const current = [element1];
66
+
67
+ const result = targetSameCandidates(cache, current);
68
+
69
+ expect(result).toBe(true);
70
+ });
71
+
72
+ it("should return false for single element arrays with different elements", () => {
73
+ const cache = [element1];
74
+ const current = [element2];
75
+
76
+ const result = targetSameCandidates(cache, current);
77
+
78
+ expect(result).toBe(false);
79
+ });
80
+ });
81
+
@@ -0,0 +1,238 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import * as enterTextEditModeModule from "../text/helpers/enterTextEditMode";
3
+ import type { NodeText } from "../text/types";
4
+ import * as getElementsFromPointModule from "./helpers/getElementsFromPoint";
5
+ import * as isInsideComponentModule from "./helpers/isInsideComponent";
6
+ import * as isInsideViewportModule from "./helpers/isInsideViewport";
7
+ import * as targetSameCandidatesModule from "./helpers/targetSameCandidates";
8
+ import { selectNode } from "./selectNode";
9
+
10
+ vi.mock("../text/helpers/enterTextEditMode");
11
+ vi.mock("./helpers/getElementsFromPoint");
12
+ vi.mock("./helpers/isInsideComponent");
13
+ vi.mock("./helpers/isInsideViewport");
14
+ vi.mock("./helpers/targetSameCandidates");
15
+
16
+ describe("selectNode", () => {
17
+ let nodeProvider: HTMLElement;
18
+ let node1: HTMLElement;
19
+ let node2: HTMLElement;
20
+ let mockText: NodeText;
21
+ let mockEvent: MouseEvent;
22
+
23
+ beforeEach(() => {
24
+ nodeProvider = document.createElement("div");
25
+ nodeProvider.setAttribute("data-role", "node-provider");
26
+ document.body.appendChild(nodeProvider);
27
+
28
+ node1 = document.createElement("div");
29
+ node1.setAttribute("data-node-id", "node-1");
30
+ nodeProvider.appendChild(node1);
31
+
32
+ node2 = document.createElement("div");
33
+ node2.setAttribute("data-node-id", "node-2");
34
+ nodeProvider.appendChild(node2);
35
+
36
+ mockText = {
37
+ getEditableNode: vi.fn().mockReturnValue(null),
38
+ enableEditMode: vi.fn(),
39
+ blurEditMode: vi.fn(),
40
+ isEditing: vi.fn().mockReturnValue(false),
41
+ };
42
+
43
+ mockEvent = new MouseEvent("click", {
44
+ clientX: 100,
45
+ clientY: 200,
46
+ bubbles: true,
47
+ });
48
+
49
+ vi.mocked(getElementsFromPointModule.getElementsFromPoint).mockReturnValue([node1, node2]);
50
+ vi.mocked(isInsideComponentModule.isInsideComponent).mockReturnValue(false);
51
+ vi.mocked(isInsideViewportModule.isInsideViewport).mockReturnValue(true);
52
+ vi.mocked(targetSameCandidatesModule.targetSameCandidates).mockReturnValue(false);
53
+ vi.mocked(enterTextEditModeModule.enterTextEditMode).mockImplementation(() => {});
54
+ });
55
+
56
+ afterEach(() => {
57
+ if (document.body.contains(nodeProvider)) {
58
+ document.body.removeChild(nodeProvider);
59
+ }
60
+ vi.clearAllMocks();
61
+ });
62
+
63
+ it("should return null when no candidates", () => {
64
+ vi.mocked(getElementsFromPointModule.getElementsFromPoint).mockReturnValue([]);
65
+
66
+ const result = selectNode(mockEvent, nodeProvider, mockText);
67
+
68
+ expect(result).toBeNull();
69
+ });
70
+
71
+ it("should return first candidate when clickThrough is true (metaKey)", () => {
72
+ const event = new MouseEvent("click", {
73
+ clientX: 100,
74
+ clientY: 200,
75
+ metaKey: true,
76
+ bubbles: true,
77
+ });
78
+
79
+ const result = selectNode(event, nodeProvider, mockText);
80
+
81
+ expect(result).toBe(node1);
82
+ });
83
+
84
+ it("should return first candidate when clickThrough is true (ctrlKey)", () => {
85
+ const event = new MouseEvent("click", {
86
+ clientX: 100,
87
+ clientY: 200,
88
+ ctrlKey: true,
89
+ bubbles: true,
90
+ });
91
+
92
+ const result = selectNode(event, nodeProvider, mockText);
93
+
94
+ expect(result).toBe(node1);
95
+ });
96
+
97
+ it("should enter text edit mode when clicking same node with clickThrough", () => {
98
+ const event = new MouseEvent("click", {
99
+ clientX: 100,
100
+ clientY: 200,
101
+ metaKey: true,
102
+ bubbles: true,
103
+ });
104
+
105
+ // First click
106
+ selectNode(event, nodeProvider, mockText);
107
+ vi.clearAllMocks();
108
+
109
+ // Second click on same node
110
+ selectNode(event, nodeProvider, mockText);
111
+
112
+ expect(enterTextEditModeModule.enterTextEditMode).toHaveBeenCalledWith(node1, nodeProvider, mockText);
113
+ });
114
+
115
+ it("should return editable node if it's in candidates", () => {
116
+ mockText.getEditableNode = vi.fn().mockReturnValue(node1);
117
+
118
+ const result = selectNode(mockEvent, nodeProvider, mockText);
119
+
120
+ expect(result).toBe(node1);
121
+ });
122
+
123
+ it("should cycle through candidates on repeated clicks", () => {
124
+ // First click - targetSameCandidates returns false initially, so attempt=0
125
+ // nodeIndex = 2-1-0 = 1, should get last candidate (node2)
126
+ const result1 = selectNode(mockEvent, nodeProvider, mockText);
127
+ expect(result1).toBe(node2);
128
+
129
+ // Second click - targetSameCandidates returns true, attempt increments to 1
130
+ // nodeIndex = 2-1-1 = 0, should get first candidate (node1)
131
+ vi.mocked(targetSameCandidatesModule.targetSameCandidates).mockReturnValue(true);
132
+ const result2 = selectNode(mockEvent, nodeProvider, mockText);
133
+ expect(result2).toBe(node1);
134
+
135
+ // Third click - attempt stays at 1 (clamped at candidates.length-2=0),
136
+ // but 1 <= 0 is false, so attempt doesn't increment further
137
+ // nodeIndex = 2-1-1 = 0, should get first candidate again (node1)
138
+ const result3 = selectNode(mockEvent, nodeProvider, mockText);
139
+ expect(result3).toBe(node1);
140
+ });
141
+
142
+ it("should reset attempt when candidates change", () => {
143
+ vi.mocked(targetSameCandidatesModule.targetSameCandidates).mockReturnValue(false);
144
+
145
+ selectNode(mockEvent, nodeProvider, mockText);
146
+
147
+ // Should start from last candidate
148
+ expect(vi.mocked(getElementsFromPointModule.getElementsFromPoint).mock.results[0].value[1]).toBe(node2);
149
+ });
150
+
151
+ it("should enter text edit mode when clicking same node", () => {
152
+ // Use a single node to ensure we select the same one
153
+ vi.mocked(getElementsFromPointModule.getElementsFromPoint).mockReturnValue([node1]);
154
+
155
+ // First click - select node1
156
+ const result1 = selectNode(mockEvent, nodeProvider, mockText);
157
+ expect(result1).toBe(node1);
158
+ vi.clearAllMocks();
159
+
160
+ // Second click on same node - should enter text edit mode
161
+ vi.mocked(targetSameCandidatesModule.targetSameCandidates).mockReturnValue(true);
162
+ const result2 = selectNode(mockEvent, nodeProvider, mockText);
163
+
164
+ expect(result2).toBe(node1);
165
+ expect(enterTextEditModeModule.enterTextEditMode).toHaveBeenCalledWith(node1, nodeProvider, mockText);
166
+ });
167
+
168
+ it("should filter out ignored DOM elements", () => {
169
+ const svgPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
170
+ vi.mocked(getElementsFromPointModule.getElementsFromPoint).mockReturnValue([svgPath, node1]);
171
+
172
+ const result = selectNode(mockEvent, nodeProvider, mockText);
173
+
174
+ expect(result).toBe(node1);
175
+ });
176
+
177
+ it("should filter out select-none elements", () => {
178
+ node1.classList.add("select-none");
179
+ vi.mocked(getElementsFromPointModule.getElementsFromPoint).mockReturnValue([node1, node2]);
180
+
181
+ const result = selectNode(mockEvent, nodeProvider, mockText);
182
+
183
+ expect(result).toBe(node2);
184
+ });
185
+
186
+ it("should filter out content-layer elements", () => {
187
+ node1.classList.add("content-layer");
188
+ vi.mocked(getElementsFromPointModule.getElementsFromPoint).mockReturnValue([node1, node2]);
189
+
190
+ const result = selectNode(mockEvent, nodeProvider, mockText);
191
+
192
+ expect(result).toBe(node2);
193
+ });
194
+
195
+ it("should filter out resize-handle elements", () => {
196
+ node1.classList.add("resize-handle");
197
+ vi.mocked(getElementsFromPointModule.getElementsFromPoint).mockReturnValue([node1, node2]);
198
+
199
+ const result = selectNode(mockEvent, nodeProvider, mockText);
200
+
201
+ expect(result).toBe(node2);
202
+ });
203
+
204
+ it("should filter out resize-presets elements", () => {
205
+ node1.classList.add("resize-presets");
206
+ vi.mocked(getElementsFromPointModule.getElementsFromPoint).mockReturnValue([node1, node2]);
207
+
208
+ const result = selectNode(mockEvent, nodeProvider, mockText);
209
+
210
+ expect(result).toBe(node2);
211
+ });
212
+
213
+ it("should filter out elements inside components", () => {
214
+ vi.mocked(isInsideComponentModule.isInsideComponent).mockImplementation((el) => el === node1);
215
+
216
+ const result = selectNode(mockEvent, nodeProvider, mockText);
217
+
218
+ expect(result).toBe(node2);
219
+ });
220
+
221
+ it("should filter out elements not inside viewport", () => {
222
+ vi.mocked(isInsideViewportModule.isInsideViewport).mockImplementation((el) => el === node2);
223
+
224
+ const result = selectNode(mockEvent, nodeProvider, mockText);
225
+
226
+ expect(result).toBe(node2);
227
+ });
228
+
229
+ it("should handle multiple candidates correctly", () => {
230
+ const node3 = document.createElement("div");
231
+ nodeProvider.appendChild(node3);
232
+ vi.mocked(getElementsFromPointModule.getElementsFromPoint).mockReturnValue([node1, node2, node3]);
233
+
234
+ const result = selectNode(mockEvent, nodeProvider, mockText);
235
+
236
+ expect(result).toBe(node3); // Last candidate
237
+ });
238
+ });
@@ -0,0 +1,91 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import * as insertLineBreakModule from "../helpers/insertLineBreak";
3
+ import { setupKeydownHandler } from "./setupKeydownHandler";
4
+
5
+ vi.mock("../helpers/insertLineBreak");
6
+
7
+ describe("setupKeydownHandler", () => {
8
+ let node: HTMLElement;
9
+
10
+ beforeEach(() => {
11
+ node = document.createElement("div");
12
+ node.contentEditable = "true";
13
+ document.body.appendChild(node);
14
+
15
+ vi.mocked(insertLineBreakModule.insertLineBreak).mockImplementation(() => {});
16
+ });
17
+
18
+ afterEach(() => {
19
+ if (document.body.contains(node)) {
20
+ document.body.removeChild(node);
21
+ }
22
+ vi.clearAllMocks();
23
+ });
24
+
25
+ it("should return cleanup function", () => {
26
+ const cleanup = setupKeydownHandler(node);
27
+
28
+ expect(typeof cleanup).toBe("function");
29
+ });
30
+
31
+ it("should call insertLineBreak on Enter key", () => {
32
+ setupKeydownHandler(node);
33
+
34
+ const event = new KeyboardEvent("keydown", { key: "Enter" });
35
+ node.dispatchEvent(event);
36
+
37
+ expect(insertLineBreakModule.insertLineBreak).toHaveBeenCalled();
38
+ });
39
+
40
+ it("should prevent default on Enter key", () => {
41
+ setupKeydownHandler(node);
42
+
43
+ const event = new KeyboardEvent("keydown", { key: "Enter", cancelable: true });
44
+ const preventDefaultSpy = vi.spyOn(event, "preventDefault");
45
+ node.dispatchEvent(event);
46
+
47
+ expect(preventDefaultSpy).toHaveBeenCalled();
48
+ });
49
+
50
+ it("should stop propagation on Enter key", () => {
51
+ setupKeydownHandler(node);
52
+
53
+ const event = new KeyboardEvent("keydown", { key: "Enter" });
54
+ const stopPropagationSpy = vi.spyOn(event, "stopPropagation");
55
+ node.dispatchEvent(event);
56
+
57
+ expect(stopPropagationSpy).toHaveBeenCalled();
58
+ });
59
+
60
+ it("should not call insertLineBreak on other keys", () => {
61
+ setupKeydownHandler(node);
62
+
63
+ const event = new KeyboardEvent("keydown", { key: "a" });
64
+ node.dispatchEvent(event);
65
+
66
+ expect(insertLineBreakModule.insertLineBreak).not.toHaveBeenCalled();
67
+ });
68
+
69
+ it("should remove event listener on cleanup", () => {
70
+ const cleanup = setupKeydownHandler(node);
71
+
72
+ cleanup();
73
+
74
+ const event = new KeyboardEvent("keydown", { key: "Enter" });
75
+ node.dispatchEvent(event);
76
+
77
+ expect(insertLineBreakModule.insertLineBreak).not.toHaveBeenCalled();
78
+ });
79
+
80
+ it("should handle multiple Enter key presses", () => {
81
+ setupKeydownHandler(node);
82
+
83
+ const event1 = new KeyboardEvent("keydown", { key: "Enter" });
84
+ const event2 = new KeyboardEvent("keydown", { key: "Enter" });
85
+ node.dispatchEvent(event1);
86
+ node.dispatchEvent(event2);
87
+
88
+ expect(insertLineBreakModule.insertLineBreak).toHaveBeenCalledTimes(2);
89
+ });
90
+ });
91
+
@@ -0,0 +1,213 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import * as connectMutationObserverModule from "../../../helpers/observer/connectMutationObserver";
3
+ import * as refreshHighlightFrameModule from "../../highlight/refreshHighlightFrame";
4
+ import * as handleTextChangeModule from "../helpers/handleTextChange";
5
+ import { setupMutationObserver } from "./setupMutationObserver";
6
+
7
+ vi.mock("../../../helpers/observer/connectMutationObserver");
8
+ vi.mock("../../highlight/refreshHighlightFrame");
9
+ vi.mock("../helpers/handleTextChange");
10
+
11
+ describe("setupMutationObserver", () => {
12
+ let node: HTMLElement;
13
+ let nodeProvider: HTMLElement;
14
+ let mockDisconnect: ReturnType<typeof vi.fn>;
15
+ let mockObserver: { disconnect: ReturnType<typeof vi.fn> };
16
+
17
+ beforeEach(() => {
18
+ node = document.createElement("div");
19
+ node.setAttribute("data-node-id", "test-node");
20
+ node.textContent = "Initial text";
21
+ document.body.appendChild(node);
22
+
23
+ nodeProvider = document.createElement("div");
24
+ document.body.appendChild(nodeProvider);
25
+
26
+ mockDisconnect = vi.fn();
27
+ mockObserver = { disconnect: mockDisconnect };
28
+
29
+ vi.mocked(connectMutationObserverModule.connectMutationObserver).mockReturnValue(
30
+ mockObserver as unknown as ReturnType<typeof connectMutationObserverModule.connectMutationObserver>
31
+ );
32
+ vi.mocked(refreshHighlightFrameModule.refreshHighlightFrame).mockImplementation(() => {});
33
+ vi.mocked(handleTextChangeModule.handleTextChange).mockImplementation(() => {});
34
+
35
+ // Mock requestAnimationFrame
36
+ global.requestAnimationFrame = vi.fn((cb) => {
37
+ setTimeout(cb, 0);
38
+ return 1;
39
+ }) as unknown as typeof requestAnimationFrame;
40
+ global.cancelAnimationFrame = vi.fn() as unknown as typeof cancelAnimationFrame;
41
+ });
42
+
43
+ afterEach(() => {
44
+ if (document.body.contains(node)) {
45
+ document.body.removeChild(node);
46
+ }
47
+ if (document.body.contains(nodeProvider)) {
48
+ document.body.removeChild(nodeProvider);
49
+ }
50
+ vi.clearAllMocks();
51
+ });
52
+
53
+ it("should return cleanup function", () => {
54
+ const cleanup = setupMutationObserver(node, nodeProvider);
55
+
56
+ expect(typeof cleanup).toBe("function");
57
+ });
58
+
59
+ it("should call connectMutationObserver", () => {
60
+ setupMutationObserver(node, nodeProvider);
61
+
62
+ expect(connectMutationObserverModule.connectMutationObserver).toHaveBeenCalledWith(
63
+ node,
64
+ expect.any(Function)
65
+ );
66
+ });
67
+
68
+ it("should call refreshHighlightFrame on mutation", () => {
69
+ setupMutationObserver(node, nodeProvider);
70
+
71
+ const callback = vi.mocked(connectMutationObserverModule.connectMutationObserver).mock.calls[0][1];
72
+ const mutations: MutationRecord[] = [
73
+ {
74
+ type: "characterData",
75
+ target: node.firstChild!,
76
+ addedNodes: [] as NodeList,
77
+ removedNodes: [] as NodeList,
78
+ previousSibling: null,
79
+ nextSibling: null,
80
+ attributeName: null,
81
+ attributeNamespace: null,
82
+ oldValue: null,
83
+ } as MutationRecord,
84
+ ];
85
+
86
+ callback(mutations);
87
+
88
+ expect(refreshHighlightFrameModule.refreshHighlightFrame).toHaveBeenCalledWith(node, nodeProvider, "canvas");
89
+ });
90
+
91
+ it("should use custom canvas name", () => {
92
+ setupMutationObserver(node, nodeProvider, "custom-canvas");
93
+
94
+ const callback = vi.mocked(connectMutationObserverModule.connectMutationObserver).mock.calls[0][1];
95
+ const mutations: MutationRecord[] = [
96
+ {
97
+ type: "characterData",
98
+ target: node.firstChild!,
99
+ addedNodes: [] as NodeList,
100
+ removedNodes: [] as NodeList,
101
+ previousSibling: null,
102
+ nextSibling: null,
103
+ attributeName: null,
104
+ attributeNamespace: null,
105
+ oldValue: null,
106
+ } as MutationRecord,
107
+ ];
108
+
109
+ callback(mutations);
110
+
111
+ expect(refreshHighlightFrameModule.refreshHighlightFrame).toHaveBeenCalledWith(node, nodeProvider, "custom-canvas");
112
+ });
113
+
114
+ it("should accumulate mutations", async () => {
115
+ setupMutationObserver(node, nodeProvider);
116
+
117
+ const callback = vi.mocked(connectMutationObserverModule.connectMutationObserver).mock.calls[0][1];
118
+ const mutations1: MutationRecord[] = [
119
+ {
120
+ type: "characterData",
121
+ target: node.firstChild!,
122
+ addedNodes: [] as NodeList,
123
+ removedNodes: [] as NodeList,
124
+ previousSibling: null,
125
+ nextSibling: null,
126
+ attributeName: null,
127
+ attributeNamespace: null,
128
+ oldValue: null,
129
+ } as MutationRecord,
130
+ ];
131
+ const mutations2: MutationRecord[] = [
132
+ {
133
+ type: "characterData",
134
+ target: node.firstChild!,
135
+ addedNodes: [] as NodeList,
136
+ removedNodes: [] as NodeList,
137
+ previousSibling: null,
138
+ nextSibling: null,
139
+ attributeName: null,
140
+ attributeNamespace: null,
141
+ oldValue: null,
142
+ } as MutationRecord,
143
+ ];
144
+
145
+ callback(mutations1);
146
+ callback(mutations2);
147
+
148
+ // Wait for RAF callbacks
149
+ await new Promise((resolve) => setTimeout(resolve, 10));
150
+
151
+ expect(handleTextChangeModule.handleTextChange).toHaveBeenCalled();
152
+ });
153
+
154
+ it("should disconnect observer on cleanup", () => {
155
+ const cleanup = setupMutationObserver(node, nodeProvider);
156
+
157
+ cleanup();
158
+
159
+ expect(mockDisconnect).toHaveBeenCalled();
160
+ });
161
+
162
+ it("should cancel pending animation frames on cleanup", () => {
163
+ const cleanup = setupMutationObserver(node, nodeProvider);
164
+
165
+ // Trigger a mutation to schedule RAF
166
+ const callback = vi.mocked(connectMutationObserverModule.connectMutationObserver).mock.calls[0][1];
167
+ const mutations: MutationRecord[] = [
168
+ {
169
+ type: "characterData",
170
+ target: node.firstChild!,
171
+ addedNodes: [] as NodeList,
172
+ removedNodes: [] as NodeList,
173
+ previousSibling: null,
174
+ nextSibling: null,
175
+ attributeName: null,
176
+ attributeNamespace: null,
177
+ oldValue: null,
178
+ } as MutationRecord,
179
+ ];
180
+ callback(mutations);
181
+
182
+ cleanup();
183
+
184
+ expect(global.cancelAnimationFrame).toHaveBeenCalled();
185
+ });
186
+
187
+ it("should process mutations after double RAF", async () => {
188
+ setupMutationObserver(node, nodeProvider);
189
+
190
+ const callback = vi.mocked(connectMutationObserverModule.connectMutationObserver).mock.calls[0][1];
191
+ const mutations: MutationRecord[] = [
192
+ {
193
+ type: "characterData",
194
+ target: node.firstChild!,
195
+ addedNodes: [] as NodeList,
196
+ removedNodes: [] as NodeList,
197
+ previousSibling: null,
198
+ nextSibling: null,
199
+ attributeName: null,
200
+ attributeNamespace: null,
201
+ oldValue: null,
202
+ } as MutationRecord,
203
+ ];
204
+
205
+ callback(mutations);
206
+
207
+ // Wait for double RAF
208
+ await new Promise((resolve) => setTimeout(resolve, 10));
209
+
210
+ expect(handleTextChangeModule.handleTextChange).toHaveBeenCalledWith(node, mutations, false);
211
+ });
212
+ });
213
+