@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.
- package/dist/lib/viewport/label/getViewportLabelOverlay.d.ts +1 -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 +100 -62
- package/dist/node-edit-utils.esm.js +100 -62
- package/dist/node-edit-utils.umd.js +100 -62
- 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/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/getCanvasWindowValue.test.ts +116 -0
- package/src/lib/helpers/adjustForZoom.test.ts +65 -0
- package/src/lib/helpers/createDragHandler.test.ts +325 -0
- package/src/lib/helpers/getNodeProvider.test.ts +71 -0
- package/src/lib/helpers/getNodeTools.test.ts +50 -0
- package/src/lib/helpers/getViewportDimensions.test.ts +93 -0
- 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/toggleClass.test.ts +71 -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/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/createCornerHandles.test.ts +150 -0
- package/src/lib/node-tools/highlight/createHighlightFrame.test.ts +237 -0
- 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/helpers/getElementBounds.test.ts +158 -0
- package/src/lib/node-tools/highlight/helpers/getHighlightFrameElement.test.ts +78 -0
- 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/refreshHighlightFrame.test.ts +323 -0
- package/src/lib/node-tools/highlight/updateHighlightFrameVisibility.test.ts +110 -0
- 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 +2 -2
- package/src/lib/viewport/createViewport.test.ts +267 -0
- package/src/lib/viewport/createViewport.ts +7 -4
- 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} +2 -1
- package/src/lib/viewport/label/helpers/getLabelPosition.test.ts +51 -0
- package/src/lib/viewport/label/helpers/getTransformValues.test.ts +59 -0
- 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 +8 -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 +17 -50
- 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/{setupViewportLabelDrag.ts → setupViewportDrag.ts} +14 -14
- 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/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
|
@@ -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
|
+
|