@node-edit-utils/core 2.3.2 → 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.
- package/dist/lib/canvas/helpers/getCanvasContainerOrBody.d.ts +1 -0
- package/dist/lib/canvas/helpers/getCanvasWindowValue.d.ts +1 -1
- package/dist/lib/helpers/adjustForZoom.d.ts +1 -0
- package/dist/lib/helpers/createDragHandler.d.ts +69 -0
- package/dist/lib/helpers/getNodeProvider.d.ts +1 -0
- package/dist/lib/helpers/getNodeTools.d.ts +2 -0
- package/dist/lib/helpers/getViewportDimensions.d.ts +4 -0
- package/dist/lib/helpers/index.d.ts +9 -1
- package/dist/lib/helpers/parseTransform.d.ts +8 -0
- package/dist/lib/helpers/toggleClass.d.ts +1 -0
- package/dist/lib/viewport/label/getViewportLabelOverlay.d.ts +1 -0
- package/dist/lib/viewport/label/helpers/selectFirstViewportNode.d.ts +5 -0
- package/dist/lib/viewport/label/index.d.ts +5 -3
- package/dist/lib/viewport/label/isViewportDragging.d.ts +2 -0
- package/dist/lib/viewport/label/refreshViewportLabel.d.ts +8 -0
- package/dist/lib/viewport/label/removeViewportLabel.d.ts +5 -0
- package/dist/lib/viewport/label/setupViewportDrag.d.ts +1 -0
- package/dist/node-edit-utils.cjs.js +342 -280
- package/dist/node-edit-utils.esm.js +342 -280
- package/dist/node-edit-utils.umd.js +342 -280
- package/dist/node-edit-utils.umd.min.js +1 -1
- package/dist/styles.css +1 -1
- package/package.json +7 -2
- package/src/lib/canvas/createCanvasObserver.test.ts +242 -0
- package/src/lib/canvas/createCanvasObserver.ts +2 -2
- package/src/lib/canvas/disableCanvasKeyboard.test.ts +53 -0
- package/src/lib/canvas/disableCanvasKeyboard.ts +1 -1
- package/src/lib/canvas/disableCanvasTextMode.test.ts +53 -0
- package/src/lib/canvas/disableCanvasTextMode.ts +1 -1
- package/src/lib/canvas/enableCanvasKeyboard.test.ts +53 -0
- package/src/lib/canvas/enableCanvasKeyboard.ts +1 -1
- package/src/lib/canvas/enableCanvasTextMode.test.ts +53 -0
- package/src/lib/canvas/enableCanvasTextMode.ts +1 -1
- package/src/lib/canvas/helpers/applyCanvasState.test.ts +119 -0
- package/src/lib/canvas/helpers/applyCanvasState.ts +1 -1
- package/src/lib/canvas/helpers/getCanvasContainer.test.ts +62 -0
- package/src/lib/canvas/helpers/getCanvasContainerOrBody.test.ts +51 -0
- package/src/lib/canvas/helpers/getCanvasContainerOrBody.ts +6 -0
- package/src/lib/canvas/helpers/getCanvasWindowValue.test.ts +116 -0
- package/src/lib/canvas/helpers/getCanvasWindowValue.ts +2 -3
- package/src/lib/helpers/adjustForZoom.test.ts +65 -0
- package/src/lib/helpers/adjustForZoom.ts +4 -0
- package/src/lib/helpers/createDragHandler.test.ts +325 -0
- package/src/lib/helpers/createDragHandler.ts +171 -0
- package/src/lib/helpers/getNodeProvider.test.ts +71 -0
- package/src/lib/helpers/getNodeProvider.ts +4 -0
- package/src/lib/helpers/getNodeTools.test.ts +50 -0
- package/src/lib/helpers/getNodeTools.ts +6 -0
- package/src/lib/helpers/getViewportDimensions.test.ts +93 -0
- package/src/lib/helpers/getViewportDimensions.ts +7 -0
- package/src/lib/helpers/index.ts +9 -1
- package/src/lib/helpers/observer/connectMutationObserver.test.ts +127 -0
- package/src/lib/helpers/observer/connectResizeObserver.test.ts +147 -0
- package/src/lib/helpers/parseTransform.test.ts +117 -0
- package/src/lib/helpers/parseTransform.ts +9 -0
- package/src/lib/helpers/toggleClass.test.ts +71 -0
- package/src/lib/helpers/toggleClass.ts +9 -0
- package/src/lib/helpers/withRAF.test.ts +439 -0
- package/src/lib/node-tools/createNodeTools.test.ts +373 -0
- package/src/lib/node-tools/createNodeTools.ts +0 -1
- package/src/lib/node-tools/events/click/handleNodeClick.test.ts +109 -0
- package/src/lib/node-tools/events/setupEventListener.test.ts +136 -0
- package/src/lib/node-tools/highlight/clearHighlightFrame.test.ts +88 -0
- package/src/lib/node-tools/highlight/clearHighlightFrame.ts +2 -3
- package/src/lib/node-tools/highlight/createCornerHandles.test.ts +150 -0
- package/src/lib/node-tools/highlight/createHighlightFrame.test.ts +237 -0
- package/src/lib/node-tools/highlight/createHighlightFrame.ts +5 -9
- package/src/lib/node-tools/highlight/createTagLabel.test.ts +135 -0
- package/src/lib/node-tools/highlight/createToolsContainer.test.ts +97 -0
- package/src/lib/node-tools/highlight/createToolsContainer.ts +3 -6
- package/src/lib/node-tools/highlight/helpers/getElementBounds.test.ts +158 -0
- package/src/lib/node-tools/highlight/helpers/getElementBounds.ts +6 -5
- package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.test.ts +78 -0
- package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.ts +2 -3
- package/src/lib/node-tools/highlight/helpers/getScreenBounds.test.ts +133 -0
- package/src/lib/node-tools/highlight/highlightNode.test.ts +213 -0
- package/src/lib/node-tools/highlight/highlightNode.ts +7 -15
- package/src/lib/node-tools/highlight/refreshHighlightFrame.test.ts +323 -0
- package/src/lib/node-tools/highlight/refreshHighlightFrame.ts +12 -42
- package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.test.ts +110 -0
- package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.ts +2 -3
- package/src/lib/node-tools/select/helpers/getElementsFromPoint.test.ts +109 -0
- package/src/lib/node-tools/select/helpers/isInsideComponent.test.ts +81 -0
- package/src/lib/node-tools/select/helpers/isInsideViewport.test.ts +82 -0
- package/src/lib/node-tools/select/helpers/targetSameCandidates.test.ts +81 -0
- package/src/lib/node-tools/select/selectNode.test.ts +238 -0
- package/src/lib/node-tools/text/events/setupKeydownHandler.test.ts +91 -0
- package/src/lib/node-tools/text/events/setupMutationObserver.test.ts +213 -0
- package/src/lib/node-tools/text/events/setupNodeListeners.test.ts +133 -0
- package/src/lib/node-tools/text/helpers/enterTextEditMode.test.ts +50 -0
- package/src/lib/node-tools/text/helpers/handleTextChange.test.ts +201 -0
- package/src/lib/node-tools/text/helpers/hasTextContent.test.ts +101 -0
- package/src/lib/node-tools/text/helpers/insertLineBreak.test.ts +96 -0
- package/src/lib/node-tools/text/helpers/makeNodeEditable.test.ts +56 -0
- package/src/lib/node-tools/text/helpers/makeNodeNonEditable.test.ts +57 -0
- package/src/lib/node-tools/text/helpers/shouldEnterTextEditMode.test.ts +61 -0
- package/src/lib/node-tools/text/nodeText.test.ts +233 -0
- package/src/lib/post-message/processPostMessage.test.ts +218 -0
- package/src/lib/post-message/sendPostMessage.test.ts +120 -0
- package/src/lib/styles/styles.css +3 -3
- package/src/lib/viewport/createViewport.test.ts +267 -0
- package/src/lib/viewport/createViewport.ts +51 -51
- package/src/lib/viewport/events/setupEventListener.test.ts +103 -0
- package/src/lib/viewport/label/getViewportLabelOverlay.test.ts +77 -0
- package/src/lib/viewport/label/{getViewportLabelsOverlay.ts → getViewportLabelOverlay.ts} +6 -6
- package/src/lib/viewport/label/helpers/getLabelPosition.test.ts +51 -0
- package/src/lib/viewport/label/helpers/getLabelPosition.ts +3 -5
- package/src/lib/viewport/label/helpers/getTransformValues.test.ts +59 -0
- package/src/lib/viewport/label/helpers/getTransformValues.ts +3 -5
- package/src/lib/viewport/label/helpers/getZoomValue.test.ts +53 -0
- package/src/lib/viewport/label/helpers/selectFirstViewportNode.test.ts +105 -0
- package/src/lib/viewport/label/helpers/selectFirstViewportNode.ts +26 -0
- package/src/lib/viewport/label/index.ts +5 -3
- package/src/lib/viewport/label/isViewportDragging.test.ts +35 -0
- package/src/lib/viewport/label/isViewportDragging.ts +9 -0
- package/src/lib/viewport/label/refreshViewportLabel.test.ts +105 -0
- package/src/lib/viewport/label/refreshViewportLabel.ts +50 -0
- package/src/lib/viewport/label/refreshViewportLabels.test.ts +107 -0
- package/src/lib/viewport/label/refreshViewportLabels.ts +19 -52
- package/src/lib/viewport/label/removeViewportLabel.test.ts +67 -0
- package/src/lib/viewport/label/removeViewportLabel.ts +20 -0
- package/src/lib/viewport/label/setupViewportDrag.test.ts +249 -0
- package/src/lib/viewport/label/setupViewportDrag.ts +70 -0
- package/src/lib/viewport/resize/createResizeHandle.test.ts +37 -0
- package/src/lib/viewport/resize/createResizePresets.test.ts +75 -0
- package/src/lib/viewport/resize/updateActivePreset.test.ts +92 -0
- package/src/lib/viewport/width/calcConstrainedWidth.test.ts +47 -0
- package/src/lib/viewport/width/calcWidth.test.ts +68 -0
- package/src/lib/viewport/width/updateWidth.test.ts +78 -0
- package/src/lib/window/bindToWindow.test.ts +166 -0
- package/src/lib/window/bindToWindow.ts +1 -2
- package/dist/lib/viewport/label/getViewportLabelsOverlay.d.ts +0 -1
- package/dist/lib/viewport/label/isViewportLabelDragging.d.ts +0 -2
- package/dist/lib/viewport/label/setupViewportLabelDrag.d.ts +0 -1
- package/src/lib/viewport/label/isViewportLabelDragging.ts +0 -9
- package/src/lib/viewport/label/setupViewportLabelDrag.ts +0 -98
|
@@ -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
|
+
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import * as setupKeydownHandlerModule from "./setupKeydownHandler";
|
|
3
|
+
import * as setupMutationObserverModule from "./setupMutationObserver";
|
|
4
|
+
import { setupNodeListeners } from "./setupNodeListeners";
|
|
5
|
+
|
|
6
|
+
vi.mock("./setupKeydownHandler");
|
|
7
|
+
vi.mock("./setupMutationObserver");
|
|
8
|
+
|
|
9
|
+
describe("setupNodeListeners", () => {
|
|
10
|
+
let node: HTMLElement;
|
|
11
|
+
let nodeProvider: HTMLElement;
|
|
12
|
+
let blurHandler: ReturnType<typeof vi.fn>;
|
|
13
|
+
let keydownCleanup: ReturnType<typeof vi.fn>;
|
|
14
|
+
let mutationCleanup: ReturnType<typeof vi.fn>;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
node = document.createElement("div");
|
|
18
|
+
node.contentEditable = "true";
|
|
19
|
+
document.body.appendChild(node);
|
|
20
|
+
|
|
21
|
+
nodeProvider = document.createElement("div");
|
|
22
|
+
document.body.appendChild(nodeProvider);
|
|
23
|
+
|
|
24
|
+
blurHandler = vi.fn();
|
|
25
|
+
keydownCleanup = vi.fn();
|
|
26
|
+
mutationCleanup = vi.fn();
|
|
27
|
+
|
|
28
|
+
vi.mocked(setupKeydownHandlerModule.setupKeydownHandler).mockReturnValue(keydownCleanup);
|
|
29
|
+
vi.mocked(setupMutationObserverModule.setupMutationObserver).mockReturnValue(mutationCleanup);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
if (document.body.contains(node)) {
|
|
34
|
+
document.body.removeChild(node);
|
|
35
|
+
}
|
|
36
|
+
if (document.body.contains(nodeProvider)) {
|
|
37
|
+
document.body.removeChild(nodeProvider);
|
|
38
|
+
}
|
|
39
|
+
vi.clearAllMocks();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should return cleanup function", () => {
|
|
43
|
+
const cleanup = setupNodeListeners(node, nodeProvider, blurHandler);
|
|
44
|
+
|
|
45
|
+
expect(typeof cleanup).toBe("function");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should return no-op cleanup when nodeProvider is null", () => {
|
|
49
|
+
const cleanup = setupNodeListeners(node, null, blurHandler);
|
|
50
|
+
|
|
51
|
+
expect(typeof cleanup).toBe("function");
|
|
52
|
+
expect(setupKeydownHandlerModule.setupKeydownHandler).not.toHaveBeenCalled();
|
|
53
|
+
expect(setupMutationObserverModule.setupMutationObserver).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should setup blur listener", () => {
|
|
57
|
+
setupNodeListeners(node, nodeProvider, blurHandler);
|
|
58
|
+
|
|
59
|
+
const blurEvent = new FocusEvent("blur");
|
|
60
|
+
node.dispatchEvent(blurEvent);
|
|
61
|
+
|
|
62
|
+
expect(blurHandler).toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should setup keydown handler", () => {
|
|
66
|
+
setupNodeListeners(node, nodeProvider, blurHandler);
|
|
67
|
+
|
|
68
|
+
expect(setupKeydownHandlerModule.setupKeydownHandler).toHaveBeenCalledWith(node);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should setup mutation observer", () => {
|
|
72
|
+
setupNodeListeners(node, nodeProvider, blurHandler);
|
|
73
|
+
|
|
74
|
+
expect(setupMutationObserverModule.setupMutationObserver).toHaveBeenCalledWith(node, nodeProvider, "canvas");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should use custom canvas name", () => {
|
|
78
|
+
setupNodeListeners(node, nodeProvider, blurHandler, "custom-canvas");
|
|
79
|
+
|
|
80
|
+
expect(setupMutationObserverModule.setupMutationObserver).toHaveBeenCalledWith(node, nodeProvider, "custom-canvas");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should remove blur listener on cleanup", () => {
|
|
84
|
+
const cleanup = setupNodeListeners(node, nodeProvider, blurHandler);
|
|
85
|
+
|
|
86
|
+
cleanup();
|
|
87
|
+
|
|
88
|
+
const blurEvent = new FocusEvent("blur");
|
|
89
|
+
node.dispatchEvent(blurEvent);
|
|
90
|
+
|
|
91
|
+
expect(blurHandler).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should call keydown cleanup on cleanup", () => {
|
|
95
|
+
const cleanup = setupNodeListeners(node, nodeProvider, blurHandler);
|
|
96
|
+
|
|
97
|
+
cleanup();
|
|
98
|
+
|
|
99
|
+
expect(keydownCleanup).toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should call mutation cleanup on cleanup", () => {
|
|
103
|
+
const cleanup = setupNodeListeners(node, nodeProvider, blurHandler);
|
|
104
|
+
|
|
105
|
+
cleanup();
|
|
106
|
+
|
|
107
|
+
expect(mutationCleanup).toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should handle cleanup when mutation cleanup is undefined", () => {
|
|
111
|
+
vi.mocked(setupMutationObserverModule.setupMutationObserver).mockReturnValue(undefined);
|
|
112
|
+
|
|
113
|
+
const cleanup = setupNodeListeners(node, nodeProvider, blurHandler);
|
|
114
|
+
|
|
115
|
+
expect(() => {
|
|
116
|
+
cleanup();
|
|
117
|
+
}).not.toThrow();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should cleanup all listeners", () => {
|
|
121
|
+
const cleanup = setupNodeListeners(node, nodeProvider, blurHandler);
|
|
122
|
+
|
|
123
|
+
cleanup();
|
|
124
|
+
|
|
125
|
+
expect(keydownCleanup).toHaveBeenCalled();
|
|
126
|
+
expect(mutationCleanup).toHaveBeenCalled();
|
|
127
|
+
|
|
128
|
+
const blurEvent = new FocusEvent("blur");
|
|
129
|
+
node.dispatchEvent(blurEvent);
|
|
130
|
+
expect(blurHandler).not.toHaveBeenCalled();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|