@treelocator/runtime 0.4.7 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +1 -0
- package/dist/_generated_styles.d.ts +1 -1
- package/dist/_generated_styles.js +20 -0
- package/dist/_generated_tree_icon.d.ts +1 -1
- package/dist/adapters/HtmlElementTreeNode.d.ts +2 -2
- package/dist/adapters/HtmlElementTreeNode.js +4 -6
- package/dist/adapters/createTreeNode.js +17 -44
- package/dist/adapters/detectFramework.d.ts +8 -0
- package/dist/adapters/detectFramework.js +25 -0
- package/dist/adapters/detectFramework.test.d.ts +1 -0
- package/dist/adapters/detectFramework.test.js +60 -0
- package/dist/adapters/jsx/jsxAdapter.js +54 -89
- package/dist/adapters/jsx/jsxAdapter.test.d.ts +1 -0
- package/dist/adapters/jsx/jsxAdapter.test.js +273 -0
- package/dist/adapters/nextjs/parseNextjsDataAttributes.js +1 -1
- package/dist/adapters/nextjs/parseNextjsDataAttributes.test.d.ts +1 -0
- package/dist/adapters/nextjs/parseNextjsDataAttributes.test.js +158 -0
- package/dist/adapters/react/findFiberByHtmlElement.d.ts +1 -1
- package/dist/adapters/react/findFiberByHtmlElement.js +1 -1
- package/dist/adapters/react/getAllParentsElementsAndRootComponent.js +4 -0
- package/dist/adapters/resolveAdapter.d.ts +1 -1
- package/dist/adapters/resolveAdapter.js +4 -8
- package/dist/adapters/svelte/svelteAdapter.test.d.ts +1 -0
- package/dist/adapters/svelte/svelteAdapter.test.js +280 -0
- package/dist/adapters/vue/vueAdapter.test.d.ts +1 -0
- package/dist/adapters/vue/vueAdapter.test.js +222 -0
- package/dist/browserApi.d.ts +148 -0
- package/dist/browserApi.js +146 -5
- package/dist/browserApi.test.d.ts +1 -0
- package/dist/browserApi.test.js +287 -0
- package/dist/components/RecordingPillButton.d.ts +11 -0
- package/dist/components/RecordingPillButton.js +202 -0
- package/dist/components/RecordingResults.d.ts +2 -0
- package/dist/components/RecordingResults.js +213 -78
- package/dist/components/Runtime.js +161 -554
- package/dist/components/SettingsPanel.d.ts +5 -0
- package/dist/components/SettingsPanel.js +312 -0
- package/dist/consoleCapture.d.ts +9 -0
- package/dist/consoleCapture.js +95 -0
- package/dist/dejitter/recorder.d.ts +7 -1
- package/dist/dejitter/recorder.js +64 -1
- package/dist/functions/cssRuleInspector.d.ts +83 -0
- package/dist/functions/cssRuleInspector.js +608 -0
- package/dist/functions/cssRuleInspector.test.d.ts +1 -0
- package/dist/functions/cssRuleInspector.test.js +439 -0
- package/dist/functions/deduplicateLabels.test.d.ts +1 -0
- package/dist/functions/deduplicateLabels.test.js +178 -0
- package/dist/functions/enrichAncestrySourceMaps.js +0 -1
- package/dist/functions/extractComputedStyles.d.ts +51 -0
- package/dist/functions/extractComputedStyles.js +447 -0
- package/dist/functions/extractComputedStyles.test.d.ts +1 -0
- package/dist/functions/extractComputedStyles.test.js +549 -0
- package/dist/functions/formatAncestryChain.d.ts +8 -0
- package/dist/functions/formatAncestryChain.js +21 -1
- package/dist/functions/formatAncestryChain.test.js +18 -0
- package/dist/functions/getUsableName.test.d.ts +1 -0
- package/dist/functions/getUsableName.test.js +219 -0
- package/dist/functions/isCombinationModifiersPressed.test.d.ts +1 -0
- package/dist/functions/isCombinationModifiersPressed.test.js +192 -0
- package/dist/functions/mergeRects.test.js +210 -1
- package/dist/functions/namedSnapshots.d.ts +52 -0
- package/dist/functions/namedSnapshots.js +161 -0
- package/dist/functions/namedSnapshots.test.d.ts +1 -0
- package/dist/functions/namedSnapshots.test.js +85 -0
- package/dist/functions/normalizeFilePath.test.d.ts +1 -0
- package/dist/functions/normalizeFilePath.test.js +66 -0
- package/dist/functions/parseDataId.test.d.ts +1 -0
- package/dist/functions/parseDataId.test.js +101 -0
- package/dist/hooks/getStorage.d.ts +3 -0
- package/dist/hooks/getStorage.js +17 -0
- package/dist/hooks/useEventListeners.d.ts +15 -0
- package/dist/hooks/useEventListeners.js +56 -0
- package/dist/hooks/useLocatorStorage.d.ts +18 -0
- package/dist/hooks/useLocatorStorage.js +41 -0
- package/dist/hooks/useLocatorStorage.test.d.ts +1 -0
- package/dist/hooks/useLocatorStorage.test.js +124 -0
- package/dist/hooks/useRecordingState.d.ts +43 -0
- package/dist/hooks/useRecordingState.js +387 -0
- package/dist/hooks/useSettings.d.ts +13 -0
- package/dist/hooks/useSettings.js +66 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -2
- package/dist/initRuntime.d.ts +3 -1
- package/dist/initRuntime.js +4 -1
- package/dist/mcpBridge.d.ts +61 -0
- package/dist/mcpBridge.js +534 -0
- package/dist/mcpBridge.test.d.ts +1 -0
- package/dist/mcpBridge.test.js +248 -0
- package/dist/output.css +20 -0
- package/dist/visualDiff/diff.d.ts +9 -0
- package/dist/visualDiff/diff.js +209 -0
- package/dist/visualDiff/diff.test.d.ts +1 -0
- package/dist/visualDiff/diff.test.js +253 -0
- package/dist/visualDiff/settle.d.ts +3 -0
- package/dist/visualDiff/settle.js +50 -0
- package/dist/visualDiff/settle.test.d.ts +1 -0
- package/dist/visualDiff/settle.test.js +65 -0
- package/dist/visualDiff/snapshot.d.ts +4 -0
- package/dist/visualDiff/snapshot.js +84 -0
- package/dist/visualDiff/snapshot.test.d.ts +1 -0
- package/dist/visualDiff/snapshot.test.js +245 -0
- package/dist/visualDiff/types.d.ts +37 -0
- package/dist/visualDiff/types.js +1 -0
- package/package.json +2 -2
- package/scripts/wrapCSS.js +1 -1
- package/scripts/wrapImage.js +1 -1
- package/src/_generated_styles.ts +21 -1
- package/src/_generated_tree_icon.ts +1 -1
- package/src/adapters/HtmlElementTreeNode.ts +10 -7
- package/src/adapters/createTreeNode.ts +12 -51
- package/src/adapters/detectFramework.test.ts +73 -0
- package/src/adapters/detectFramework.ts +28 -0
- package/src/adapters/jsx/jsxAdapter.test.ts +240 -0
- package/src/adapters/jsx/jsxAdapter.ts +53 -106
- package/src/adapters/nextjs/parseNextjsDataAttributes.test.ts +212 -0
- package/src/adapters/nextjs/parseNextjsDataAttributes.ts +1 -1
- package/src/adapters/react/findDebugSource.ts +5 -6
- package/src/adapters/react/findFiberByHtmlElement.ts +3 -3
- package/src/adapters/react/getAllParentsElementsAndRootComponent.ts +3 -0
- package/src/adapters/react/reactAdapter.ts +1 -2
- package/src/adapters/resolveAdapter.ts +4 -14
- package/src/adapters/svelte/svelteAdapter.test.ts +334 -0
- package/src/adapters/vue/vueAdapter.test.ts +259 -0
- package/src/browserApi.test.ts +329 -0
- package/src/browserApi.ts +351 -4
- package/src/components/RecordingPillButton.tsx +301 -0
- package/src/components/RecordingResults.tsx +114 -13
- package/src/components/Runtime.tsx +176 -621
- package/src/components/SettingsPanel.tsx +339 -0
- package/src/consoleCapture.ts +113 -0
- package/src/dejitter/recorder.ts +67 -3
- package/src/functions/cssRuleInspector.test.ts +517 -0
- package/src/functions/cssRuleInspector.ts +708 -0
- package/src/functions/deduplicateLabels.test.ts +115 -0
- package/src/functions/enrichAncestrySourceMaps.ts +6 -3
- package/src/functions/extractComputedStyles.test.ts +681 -0
- package/src/functions/extractComputedStyles.ts +768 -0
- package/src/functions/formatAncestryChain.test.ts +23 -1
- package/src/functions/formatAncestryChain.ts +22 -1
- package/src/functions/getUsableName.test.ts +242 -0
- package/src/functions/isCombinationModifiersPressed.test.ts +156 -0
- package/src/functions/mergeRects.test.ts +111 -1
- package/src/functions/namedSnapshots.test.ts +106 -0
- package/src/functions/namedSnapshots.ts +232 -0
- package/src/functions/normalizeFilePath.test.ts +80 -0
- package/src/functions/parseDataId.test.ts +125 -0
- package/src/hooks/getStorage.ts +26 -0
- package/src/hooks/useEventListeners.ts +97 -0
- package/src/hooks/useLocatorStorage.test.ts +127 -0
- package/src/hooks/useLocatorStorage.ts +60 -0
- package/src/hooks/useRecordingState.ts +516 -0
- package/src/hooks/useSettings.ts +83 -0
- package/src/index.ts +10 -5
- package/src/initRuntime.ts +5 -0
- package/src/mcpBridge.test.ts +260 -0
- package/src/mcpBridge.ts +677 -0
- package/src/visualDiff/diff.test.ts +167 -0
- package/src/visualDiff/diff.ts +242 -0
- package/src/visualDiff/settle.test.ts +77 -0
- package/src/visualDiff/settle.ts +62 -0
- package/src/visualDiff/snapshot.test.ts +200 -0
- package/src/visualDiff/snapshot.ts +119 -0
- package/src/visualDiff/types.ts +40 -0
- package/tsconfig.json +3 -1
- package/vitest.config.ts +18 -0
- package/jest.config.ts +0 -195
|
@@ -2,7 +2,6 @@ import { describe, it, expect } from "vitest";
|
|
|
2
2
|
import {
|
|
3
3
|
AncestryItem,
|
|
4
4
|
formatAncestryChain,
|
|
5
|
-
collectAncestry,
|
|
6
5
|
truncateAtFirstFile,
|
|
7
6
|
} from "./formatAncestryChain";
|
|
8
7
|
|
|
@@ -49,6 +48,29 @@ describe("formatAncestryChain", () => {
|
|
|
49
48
|
);
|
|
50
49
|
});
|
|
51
50
|
|
|
51
|
+
it("includes classes after the id", () => {
|
|
52
|
+
const items: AncestryItem[] = [
|
|
53
|
+
{
|
|
54
|
+
elementName: "button",
|
|
55
|
+
componentName: "Button",
|
|
56
|
+
id: "save",
|
|
57
|
+
classes: ["btn", "btn-primary"],
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const result = formatAncestryChain(items);
|
|
62
|
+
expect(result).toBe("Button#save.btn.btn-primary");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("includes classes without id", () => {
|
|
66
|
+
const items: AncestryItem[] = [
|
|
67
|
+
{ elementName: "div", classes: ["card", "card--featured"] },
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const result = formatAncestryChain(items);
|
|
71
|
+
expect(result).toBe("div.card.card--featured");
|
|
72
|
+
});
|
|
73
|
+
|
|
52
74
|
it("includes both nth-child and ID at component boundary", () => {
|
|
53
75
|
const items: AncestryItem[] = [
|
|
54
76
|
{
|
|
@@ -16,6 +16,7 @@ export interface AncestryItem {
|
|
|
16
16
|
filePath?: string;
|
|
17
17
|
line?: number;
|
|
18
18
|
id?: string;
|
|
19
|
+
classes?: string[];
|
|
19
20
|
nthChild?: number; // 1-indexed, only set when there are ambiguous siblings
|
|
20
21
|
/** All owner components from outermost (Sidebar) to innermost (GlassPanel) */
|
|
21
22
|
ownerComponents?: OwnerComponentInfo[];
|
|
@@ -60,6 +61,20 @@ function treeNodeComponentToOwnerInfo(
|
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Build a short label for the innermost (clicked) element in an ancestry array.
|
|
66
|
+
* Used as the header for computed-style extraction output.
|
|
67
|
+
*
|
|
68
|
+
* Example: `"Button at src/components/Button.tsx:23"` or `"div"` as a fallback.
|
|
69
|
+
*/
|
|
70
|
+
export function getElementLabel(ancestry: AncestryItem[]): string {
|
|
71
|
+
if (ancestry.length === 0) return "";
|
|
72
|
+
const item = ancestry[0]!;
|
|
73
|
+
const name = item.componentName || item.elementName;
|
|
74
|
+
const location = item.filePath ? ` at ${item.filePath}:${item.line}` : "";
|
|
75
|
+
return `${name}${location}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
63
78
|
export function collectAncestry(node: TreeNode): AncestryItem[] {
|
|
64
79
|
const items: AncestryItem[] = [];
|
|
65
80
|
let current: TreeNode | null = node;
|
|
@@ -84,6 +99,9 @@ export function collectAncestry(node: TreeNode): AncestryItem[] {
|
|
|
84
99
|
if (element.id) {
|
|
85
100
|
item.id = element.id;
|
|
86
101
|
}
|
|
102
|
+
if (element.classList.length > 0) {
|
|
103
|
+
item.classes = Array.from(element.classList);
|
|
104
|
+
}
|
|
87
105
|
const nthChild = getNthChildIfAmbiguous(element);
|
|
88
106
|
if (nthChild !== undefined) {
|
|
89
107
|
item.nthChild = nthChild;
|
|
@@ -254,7 +272,7 @@ export function formatAncestryChain(items: AncestryItem[]): string {
|
|
|
254
272
|
}
|
|
255
273
|
}
|
|
256
274
|
|
|
257
|
-
// Build element selector: displayName:nth-child(n)#id
|
|
275
|
+
// Build element selector: displayName:nth-child(n)#id.class1.class2
|
|
258
276
|
let selector = displayName;
|
|
259
277
|
if (item.nthChild !== undefined) {
|
|
260
278
|
selector += `:nth-child(${item.nthChild})`;
|
|
@@ -262,6 +280,9 @@ export function formatAncestryChain(items: AncestryItem[]): string {
|
|
|
262
280
|
if (item.id) {
|
|
263
281
|
selector += `#${item.id}`;
|
|
264
282
|
}
|
|
283
|
+
if (item.classes && item.classes.length > 0) {
|
|
284
|
+
selector += "." + item.classes.join(".");
|
|
285
|
+
}
|
|
265
286
|
|
|
266
287
|
let description = selector;
|
|
267
288
|
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { getUsableName } from "./getUsableName";
|
|
3
|
+
import { Fiber } from "@locator/shared";
|
|
4
|
+
|
|
5
|
+
describe("getUsableName", () => {
|
|
6
|
+
test("returns 'Not found' when fiber is null", () => {
|
|
7
|
+
expect(getUsableName(null)).toBe("Not found");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("returns 'Not found' when fiber is undefined", () => {
|
|
11
|
+
expect(getUsableName(undefined)).toBe("Not found");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("returns string elementType directly", () => {
|
|
15
|
+
const fiber = {
|
|
16
|
+
elementType: "div",
|
|
17
|
+
} as any as Fiber;
|
|
18
|
+
expect(getUsableName(fiber)).toBe("div");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("returns 'Anonymous' when elementType is null", () => {
|
|
22
|
+
const fiber = {
|
|
23
|
+
elementType: null,
|
|
24
|
+
} as any as Fiber;
|
|
25
|
+
expect(getUsableName(fiber)).toBe("Anonymous");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("returns elementType.name when available", () => {
|
|
29
|
+
const fiber = {
|
|
30
|
+
elementType: {
|
|
31
|
+
name: "MyComponent",
|
|
32
|
+
},
|
|
33
|
+
} as any as Fiber;
|
|
34
|
+
expect(getUsableName(fiber)).toBe("MyComponent");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("returns elementType.displayName when name unavailable", () => {
|
|
38
|
+
const fiber = {
|
|
39
|
+
elementType: {
|
|
40
|
+
displayName: "MyComponent",
|
|
41
|
+
},
|
|
42
|
+
} as any as Fiber;
|
|
43
|
+
expect(getUsableName(fiber)).toBe("MyComponent");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("prefers name over displayName", () => {
|
|
47
|
+
const fiber = {
|
|
48
|
+
elementType: {
|
|
49
|
+
name: "FromName",
|
|
50
|
+
displayName: "FromDisplayName",
|
|
51
|
+
},
|
|
52
|
+
} as any as Fiber;
|
|
53
|
+
expect(getUsableName(fiber)).toBe("FromName");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("handles React.memo with type.name", () => {
|
|
57
|
+
const fiber = {
|
|
58
|
+
elementType: {
|
|
59
|
+
type: {
|
|
60
|
+
name: "MemoizedComponent",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
} as any as Fiber;
|
|
64
|
+
expect(getUsableName(fiber)).toBe("MemoizedComponent");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("handles React.memo with type.displayName", () => {
|
|
68
|
+
const fiber = {
|
|
69
|
+
elementType: {
|
|
70
|
+
type: {
|
|
71
|
+
displayName: "MemoizedComponent",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
} as any as Fiber;
|
|
75
|
+
expect(getUsableName(fiber)).toBe("MemoizedComponent");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("prefers type.name over type.displayName", () => {
|
|
79
|
+
const fiber = {
|
|
80
|
+
elementType: {
|
|
81
|
+
type: {
|
|
82
|
+
name: "FromName",
|
|
83
|
+
displayName: "FromDisplayName",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
} as any as Fiber;
|
|
87
|
+
expect(getUsableName(fiber)).toBe("FromName");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("handles React.forwardRef with render.name", () => {
|
|
91
|
+
const fiber = {
|
|
92
|
+
elementType: {
|
|
93
|
+
render: {
|
|
94
|
+
name: "ForwardRefComponent",
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
} as any as Fiber;
|
|
98
|
+
expect(getUsableName(fiber)).toBe("ForwardRefComponent");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("handles React.forwardRef with render.displayName", () => {
|
|
102
|
+
const fiber = {
|
|
103
|
+
elementType: {
|
|
104
|
+
render: {
|
|
105
|
+
displayName: "ForwardRefComponent",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
} as any as Fiber;
|
|
109
|
+
expect(getUsableName(fiber)).toBe("ForwardRefComponent");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("prefers render.name over render.displayName", () => {
|
|
113
|
+
const fiber = {
|
|
114
|
+
elementType: {
|
|
115
|
+
render: {
|
|
116
|
+
name: "FromName",
|
|
117
|
+
displayName: "FromDisplayName",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
} as any as Fiber;
|
|
121
|
+
expect(getUsableName(fiber)).toBe("FromName");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("handles React.lazy with _payload._result.name", () => {
|
|
125
|
+
const fiber = {
|
|
126
|
+
elementType: {
|
|
127
|
+
_payload: {
|
|
128
|
+
_result: {
|
|
129
|
+
name: "LazyComponent",
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
} as any as Fiber;
|
|
134
|
+
expect(getUsableName(fiber)).toBe("LazyComponent");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("handles React.lazy with _payload._result.displayName", () => {
|
|
138
|
+
const fiber = {
|
|
139
|
+
elementType: {
|
|
140
|
+
_payload: {
|
|
141
|
+
_result: {
|
|
142
|
+
displayName: "LazyComponent",
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
} as any as Fiber;
|
|
147
|
+
expect(getUsableName(fiber)).toBe("LazyComponent");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("prefers _payload._result.name over displayName", () => {
|
|
151
|
+
const fiber = {
|
|
152
|
+
elementType: {
|
|
153
|
+
_payload: {
|
|
154
|
+
_result: {
|
|
155
|
+
name: "FromName",
|
|
156
|
+
displayName: "FromDisplayName",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
} as any as Fiber;
|
|
161
|
+
expect(getUsableName(fiber)).toBe("FromName");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("falls back to fiber.type.name when elementType doesn't have name", () => {
|
|
165
|
+
const fiber = {
|
|
166
|
+
elementType: {},
|
|
167
|
+
type: {
|
|
168
|
+
name: "TypeComponent",
|
|
169
|
+
},
|
|
170
|
+
} as any as Fiber;
|
|
171
|
+
expect(getUsableName(fiber)).toBe("TypeComponent");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("ignores fiber.type when it equals elementType", () => {
|
|
175
|
+
const elementType = { name: "Component" };
|
|
176
|
+
const fiber = {
|
|
177
|
+
elementType,
|
|
178
|
+
type: elementType,
|
|
179
|
+
} as any as Fiber;
|
|
180
|
+
expect(getUsableName(fiber)).toBe("Component");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("ignores fiber.type when it is a string", () => {
|
|
184
|
+
const fiber = {
|
|
185
|
+
elementType: {},
|
|
186
|
+
type: "div",
|
|
187
|
+
} as any as Fiber;
|
|
188
|
+
expect(getUsableName(fiber)).toBe("Anonymous");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("returns 'Anonymous' when all fallbacks fail", () => {
|
|
192
|
+
const fiber = {
|
|
193
|
+
elementType: {},
|
|
194
|
+
type: {},
|
|
195
|
+
} as any as Fiber;
|
|
196
|
+
expect(getUsableName(fiber)).toBe("Anonymous");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("follows priority order correctly", () => {
|
|
200
|
+
// elementType.name should be used before type.name
|
|
201
|
+
const fiber = {
|
|
202
|
+
elementType: {
|
|
203
|
+
name: "ElementTypeComponent",
|
|
204
|
+
},
|
|
205
|
+
type: {
|
|
206
|
+
name: "TypeComponent",
|
|
207
|
+
},
|
|
208
|
+
} as any as Fiber;
|
|
209
|
+
expect(getUsableName(fiber)).toBe("ElementTypeComponent");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("handles deeply nested fiber with multiple name sources", () => {
|
|
213
|
+
const fiber = {
|
|
214
|
+
elementType: {
|
|
215
|
+
render: {
|
|
216
|
+
name: "VeryNested",
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
} as any as Fiber;
|
|
220
|
+
expect(getUsableName(fiber)).toBe("VeryNested");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("handles missing _payload gracefully", () => {
|
|
224
|
+
const fiber = {
|
|
225
|
+
elementType: {
|
|
226
|
+
_payload: null,
|
|
227
|
+
},
|
|
228
|
+
} as any as Fiber;
|
|
229
|
+
expect(getUsableName(fiber)).toBe("Anonymous");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("handles missing _result in _payload gracefully", () => {
|
|
233
|
+
const fiber = {
|
|
234
|
+
elementType: {
|
|
235
|
+
_payload: {
|
|
236
|
+
_result: null,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
} as any as Fiber;
|
|
240
|
+
expect(getUsableName(fiber)).toBe("Anonymous");
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { isCombinationModifiersPressed, getMouseModifiers } from "./isCombinationModifiersPressed";
|
|
3
|
+
|
|
4
|
+
describe("getMouseModifiers", () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
delete document.documentElement.dataset.locatorMouseModifiers;
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("returns default alt modifier when not set", () => {
|
|
10
|
+
const modifiers = getMouseModifiers();
|
|
11
|
+
expect(modifiers).toEqual({ alt: true });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("parses single modifier from dataset", () => {
|
|
15
|
+
document.documentElement.dataset.locatorMouseModifiers = "ctrl";
|
|
16
|
+
const modifiers = getMouseModifiers();
|
|
17
|
+
expect(modifiers).toEqual({ ctrl: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("parses multiple modifiers separated by +", () => {
|
|
21
|
+
document.documentElement.dataset.locatorMouseModifiers = "ctrl+shift";
|
|
22
|
+
const modifiers = getMouseModifiers();
|
|
23
|
+
expect(modifiers).toEqual({ ctrl: true, shift: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("parses three modifiers", () => {
|
|
27
|
+
document.documentElement.dataset.locatorMouseModifiers = "ctrl+alt+shift";
|
|
28
|
+
const modifiers = getMouseModifiers();
|
|
29
|
+
expect(modifiers).toEqual({ ctrl: true, alt: true, shift: true });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("parses meta modifier", () => {
|
|
33
|
+
document.documentElement.dataset.locatorMouseModifiers = "meta";
|
|
34
|
+
const modifiers = getMouseModifiers();
|
|
35
|
+
expect(modifiers).toEqual({ meta: true });
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("isCombinationModifiersPressed", () => {
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
delete document.documentElement.dataset.locatorMouseModifiers;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("default alt modifier", () => {
|
|
45
|
+
test("returns true when altKey is true", () => {
|
|
46
|
+
const event = new KeyboardEvent("keydown", { altKey: true });
|
|
47
|
+
expect(isCombinationModifiersPressed(event)).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("returns false when altKey is false", () => {
|
|
51
|
+
const event = new KeyboardEvent("keydown", { altKey: false });
|
|
52
|
+
expect(isCombinationModifiersPressed(event)).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("returns false when ctrlKey is true", () => {
|
|
56
|
+
const event = new KeyboardEvent("keydown", { altKey: true, ctrlKey: true });
|
|
57
|
+
expect(isCombinationModifiersPressed(event)).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("returns false when ctrlKey is true but altKey is false", () => {
|
|
61
|
+
const event = new KeyboardEvent("keydown", { altKey: false, ctrlKey: true });
|
|
62
|
+
expect(isCombinationModifiersPressed(event)).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("returns false when metaKey is true but altKey is false", () => {
|
|
66
|
+
const event = new KeyboardEvent("keydown", { altKey: false, metaKey: true });
|
|
67
|
+
expect(isCombinationModifiersPressed(event)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("custom ctrl+alt modifier", () => {
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
document.documentElement.dataset.locatorMouseModifiers = "ctrl+alt";
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("returns true when both ctrl and alt are true", () => {
|
|
77
|
+
const event = new KeyboardEvent("keydown", { ctrlKey: true, altKey: true });
|
|
78
|
+
expect(isCombinationModifiersPressed(event)).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("returns false when only ctrl is true", () => {
|
|
82
|
+
const event = new KeyboardEvent("keydown", { ctrlKey: true, altKey: false });
|
|
83
|
+
expect(isCombinationModifiersPressed(event)).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("returns false when only alt is true", () => {
|
|
87
|
+
const event = new KeyboardEvent("keydown", { ctrlKey: false, altKey: true });
|
|
88
|
+
expect(isCombinationModifiersPressed(event)).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("returns false when neither ctrl nor alt are true", () => {
|
|
92
|
+
const event = new KeyboardEvent("keydown", { ctrlKey: false, altKey: false });
|
|
93
|
+
expect(isCombinationModifiersPressed(event)).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("shift modifier handling", () => {
|
|
98
|
+
test("shift not configured allows shift to be pressed without affecting result", () => {
|
|
99
|
+
document.documentElement.dataset.locatorMouseModifiers = "alt";
|
|
100
|
+
const event = new KeyboardEvent("keydown", { altKey: true, shiftKey: true });
|
|
101
|
+
expect(isCombinationModifiersPressed(event)).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("shift configured requires shift to be pressed", () => {
|
|
105
|
+
document.documentElement.dataset.locatorMouseModifiers = "alt+shift";
|
|
106
|
+
const event = new KeyboardEvent("keydown", { altKey: true, shiftKey: false });
|
|
107
|
+
expect(isCombinationModifiersPressed(event)).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("shift configured returns true when shift is pressed", () => {
|
|
111
|
+
document.documentElement.dataset.locatorMouseModifiers = "alt+shift";
|
|
112
|
+
const event = new KeyboardEvent("keydown", { altKey: true, shiftKey: true });
|
|
113
|
+
expect(isCombinationModifiersPressed(event)).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("rightClick parameter", () => {
|
|
118
|
+
beforeEach(() => {
|
|
119
|
+
document.documentElement.dataset.locatorMouseModifiers = "alt";
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("rightClick=true ignores ctrlKey requirement", () => {
|
|
123
|
+
const event = new KeyboardEvent("keydown", { altKey: true, ctrlKey: false });
|
|
124
|
+
expect(isCombinationModifiersPressed(event, true)).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("rightClick=true still requires alt", () => {
|
|
128
|
+
const event = new KeyboardEvent("keydown", { altKey: false, metaKey: false });
|
|
129
|
+
expect(isCombinationModifiersPressed(event, true)).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("rightClick=true checks metaKey", () => {
|
|
133
|
+
const event = new KeyboardEvent("keydown", { altKey: true, metaKey: true });
|
|
134
|
+
expect(isCombinationModifiersPressed(event, true)).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("rightClick=true with meta modifier configured", () => {
|
|
138
|
+
document.documentElement.dataset.locatorMouseModifiers = "meta";
|
|
139
|
+
const event = new KeyboardEvent("keydown", { altKey: false, metaKey: true });
|
|
140
|
+
expect(isCombinationModifiersPressed(event, true)).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("MouseEvent support", () => {
|
|
145
|
+
test("works with MouseEvent", () => {
|
|
146
|
+
const event = new MouseEvent("click", { altKey: true });
|
|
147
|
+
expect(isCombinationModifiersPressed(event)).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("rightClick parameter works with MouseEvent", () => {
|
|
151
|
+
document.documentElement.dataset.locatorMouseModifiers = "alt";
|
|
152
|
+
const event = new MouseEvent("click", { altKey: true });
|
|
153
|
+
expect(isCombinationModifiersPressed(event, true)).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -3,7 +3,7 @@ import { SimpleDOMRect } from "../types/types";
|
|
|
3
3
|
import { describe, expect, test } from "vitest";
|
|
4
4
|
|
|
5
5
|
describe("mergeRects", () => {
|
|
6
|
-
test("basic", () => {
|
|
6
|
+
test("basic overlapping rects", () => {
|
|
7
7
|
const a: SimpleDOMRect = { x: 0, y: 0, width: 10, height: 10 };
|
|
8
8
|
const b: SimpleDOMRect = { x: 5, y: 5, width: 10, height: 10 };
|
|
9
9
|
const res = mergeRects(a, b);
|
|
@@ -12,4 +12,114 @@ describe("mergeRects", () => {
|
|
|
12
12
|
expect(res.width).toEqual(15);
|
|
13
13
|
expect(res.height).toEqual(15);
|
|
14
14
|
});
|
|
15
|
+
|
|
16
|
+
test("non-overlapping rects side by side", () => {
|
|
17
|
+
const a: SimpleDOMRect = { x: 0, y: 0, width: 10, height: 10 };
|
|
18
|
+
const b: SimpleDOMRect = { x: 10, y: 0, width: 10, height: 10 };
|
|
19
|
+
const res = mergeRects(a, b);
|
|
20
|
+
expect(res.x).toEqual(0);
|
|
21
|
+
expect(res.y).toEqual(0);
|
|
22
|
+
expect(res.width).toEqual(20);
|
|
23
|
+
expect(res.height).toEqual(10);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("non-overlapping rects vertically separated", () => {
|
|
27
|
+
const a: SimpleDOMRect = { x: 0, y: 0, width: 10, height: 10 };
|
|
28
|
+
const b: SimpleDOMRect = { x: 0, y: 10, width: 10, height: 10 };
|
|
29
|
+
const res = mergeRects(a, b);
|
|
30
|
+
expect(res.x).toEqual(0);
|
|
31
|
+
expect(res.y).toEqual(0);
|
|
32
|
+
expect(res.width).toEqual(10);
|
|
33
|
+
expect(res.height).toEqual(20);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("identical rects", () => {
|
|
37
|
+
const a: SimpleDOMRect = { x: 5, y: 5, width: 10, height: 10 };
|
|
38
|
+
const b: SimpleDOMRect = { x: 5, y: 5, width: 10, height: 10 };
|
|
39
|
+
const res = mergeRects(a, b);
|
|
40
|
+
expect(res.x).toEqual(5);
|
|
41
|
+
expect(res.y).toEqual(5);
|
|
42
|
+
expect(res.width).toEqual(10);
|
|
43
|
+
expect(res.height).toEqual(10);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("one rect fully contains another", () => {
|
|
47
|
+
const a: SimpleDOMRect = { x: 0, y: 0, width: 20, height: 20 };
|
|
48
|
+
const b: SimpleDOMRect = { x: 5, y: 5, width: 10, height: 10 };
|
|
49
|
+
const res = mergeRects(a, b);
|
|
50
|
+
expect(res.x).toEqual(0);
|
|
51
|
+
expect(res.y).toEqual(0);
|
|
52
|
+
expect(res.width).toEqual(20);
|
|
53
|
+
expect(res.height).toEqual(20);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("small rect contains large rect", () => {
|
|
57
|
+
const a: SimpleDOMRect = { x: 5, y: 5, width: 10, height: 10 };
|
|
58
|
+
const b: SimpleDOMRect = { x: 0, y: 0, width: 20, height: 20 };
|
|
59
|
+
const res = mergeRects(a, b);
|
|
60
|
+
expect(res.x).toEqual(0);
|
|
61
|
+
expect(res.y).toEqual(0);
|
|
62
|
+
expect(res.width).toEqual(20);
|
|
63
|
+
expect(res.height).toEqual(20);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("zero-area rect (point)", () => {
|
|
67
|
+
const a: SimpleDOMRect = { x: 5, y: 5, width: 0, height: 0 };
|
|
68
|
+
const b: SimpleDOMRect = { x: 10, y: 10, width: 10, height: 10 };
|
|
69
|
+
const res = mergeRects(a, b);
|
|
70
|
+
expect(res.x).toEqual(5);
|
|
71
|
+
expect(res.y).toEqual(5);
|
|
72
|
+
expect(res.width).toEqual(15);
|
|
73
|
+
expect(res.height).toEqual(15);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("both rects have zero area", () => {
|
|
77
|
+
const a: SimpleDOMRect = { x: 5, y: 5, width: 0, height: 0 };
|
|
78
|
+
const b: SimpleDOMRect = { x: 10, y: 10, width: 0, height: 0 };
|
|
79
|
+
const res = mergeRects(a, b);
|
|
80
|
+
expect(res.x).toEqual(5);
|
|
81
|
+
expect(res.y).toEqual(5);
|
|
82
|
+
expect(res.width).toEqual(5);
|
|
83
|
+
expect(res.height).toEqual(5);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("negative coordinates", () => {
|
|
87
|
+
const a: SimpleDOMRect = { x: -10, y: -10, width: 20, height: 20 };
|
|
88
|
+
const b: SimpleDOMRect = { x: 5, y: 5, width: 10, height: 10 };
|
|
89
|
+
const res = mergeRects(a, b);
|
|
90
|
+
expect(res.x).toEqual(-10);
|
|
91
|
+
expect(res.y).toEqual(-10);
|
|
92
|
+
expect(res.width).toEqual(25);
|
|
93
|
+
expect(res.height).toEqual(25);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("both rects with negative coordinates", () => {
|
|
97
|
+
const a: SimpleDOMRect = { x: -20, y: -20, width: 10, height: 10 };
|
|
98
|
+
const b: SimpleDOMRect = { x: -15, y: -15, width: 10, height: 10 };
|
|
99
|
+
const res = mergeRects(a, b);
|
|
100
|
+
expect(res.x).toEqual(-20);
|
|
101
|
+
expect(res.y).toEqual(-20);
|
|
102
|
+
expect(res.width).toEqual(15);
|
|
103
|
+
expect(res.height).toEqual(15);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("rects with large coordinates", () => {
|
|
107
|
+
const a: SimpleDOMRect = { x: 1000, y: 1000, width: 100, height: 100 };
|
|
108
|
+
const b: SimpleDOMRect = { x: 1050, y: 1050, width: 100, height: 100 };
|
|
109
|
+
const res = mergeRects(a, b);
|
|
110
|
+
expect(res.x).toEqual(1000);
|
|
111
|
+
expect(res.y).toEqual(1000);
|
|
112
|
+
expect(res.width).toEqual(150);
|
|
113
|
+
expect(res.height).toEqual(150);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("rects with fractional coordinates and dimensions", () => {
|
|
117
|
+
const a: SimpleDOMRect = { x: 1.5, y: 2.5, width: 3.5, height: 4.5 };
|
|
118
|
+
const b: SimpleDOMRect = { x: 4, y: 5, width: 2, height: 2 };
|
|
119
|
+
const res = mergeRects(a, b);
|
|
120
|
+
expect(res.x).toEqual(1.5);
|
|
121
|
+
expect(res.y).toEqual(2.5);
|
|
122
|
+
expect(res.width).toEqual(4.5);
|
|
123
|
+
expect(res.height).toEqual(4.5);
|
|
124
|
+
});
|
|
15
125
|
});
|