@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
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { getElementInfo, VueTreeNodeElement } from "./vueAdapter";
|
|
3
|
+
import type { ComponentInternalInstance } from "vue";
|
|
4
|
+
|
|
5
|
+
function createMockVueElement(
|
|
6
|
+
props?: { file?: string; name?: string } | null
|
|
7
|
+
): HTMLElement & { __vueParentComponent?: ComponentInternalInstance } {
|
|
8
|
+
const el = document.createElement("div") as any;
|
|
9
|
+
el.getBoundingClientRect = () => ({
|
|
10
|
+
x: 0,
|
|
11
|
+
y: 0,
|
|
12
|
+
width: 100,
|
|
13
|
+
height: 50,
|
|
14
|
+
top: 0,
|
|
15
|
+
left: 0,
|
|
16
|
+
right: 100,
|
|
17
|
+
bottom: 50,
|
|
18
|
+
toJSON: () => ({}),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (props) {
|
|
22
|
+
el.__vueParentComponent = {
|
|
23
|
+
type: {
|
|
24
|
+
__file: props.file,
|
|
25
|
+
__name: props.name,
|
|
26
|
+
},
|
|
27
|
+
} as ComponentInternalInstance;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return el;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("Vue Adapter - getElementInfo", () => {
|
|
34
|
+
test("returns null when element has no __vueParentComponent", () => {
|
|
35
|
+
const el = createMockVueElement(null);
|
|
36
|
+
expect(getElementInfo(el)).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("returns FullElementInfo with correct filePath when __vueParentComponent has type.__file", () => {
|
|
40
|
+
const el = createMockVueElement({
|
|
41
|
+
file: "/src/MyComponent.vue",
|
|
42
|
+
name: "MyComponent",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const info = getElementInfo(el);
|
|
46
|
+
expect(info).not.toBeNull();
|
|
47
|
+
expect(info!.thisElement.link!.filePath).toBe("/src/MyComponent.vue");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("returns FullElementInfo with correct component name when __vueParentComponent has type.__name", () => {
|
|
51
|
+
const el = createMockVueElement({
|
|
52
|
+
file: "/src/Button.vue",
|
|
53
|
+
name: "Button",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const info = getElementInfo(el);
|
|
57
|
+
expect(info).not.toBeNull();
|
|
58
|
+
expect(info!.componentsLabels[0]!.label).toBe("Button");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("returns null when __vueParentComponent.type is missing", () => {
|
|
62
|
+
const el = createMockVueElement(null);
|
|
63
|
+
el.__vueParentComponent = {} as ComponentInternalInstance;
|
|
64
|
+
expect(getElementInfo(el)).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("returns null when __vueParentComponent.type.__file is missing", () => {
|
|
68
|
+
const el = createMockVueElement(null);
|
|
69
|
+
el.__vueParentComponent = {
|
|
70
|
+
type: { __name: "MyComponent" },
|
|
71
|
+
} as ComponentInternalInstance;
|
|
72
|
+
expect(getElementInfo(el)).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("returns null when __vueParentComponent.type.__name is missing", () => {
|
|
76
|
+
const el = createMockVueElement(null);
|
|
77
|
+
el.__vueParentComponent = {
|
|
78
|
+
type: { __file: "/src/MyComponent.vue" },
|
|
79
|
+
} as ComponentInternalInstance;
|
|
80
|
+
expect(getElementInfo(el)).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("returns FullElementInfo with htmlElement pointing to the correct element", () => {
|
|
84
|
+
const el = createMockVueElement({
|
|
85
|
+
file: "/src/Card.vue",
|
|
86
|
+
name: "Card",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const info = getElementInfo(el);
|
|
90
|
+
expect(info).not.toBeNull();
|
|
91
|
+
expect(info!.htmlElement).toBe(el);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("returns FullElementInfo with element label as lowercase nodeName", () => {
|
|
95
|
+
const el = createMockVueElement({
|
|
96
|
+
file: "/src/Button.vue",
|
|
97
|
+
name: "Button",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const info = getElementInfo(el);
|
|
101
|
+
expect(info).not.toBeNull();
|
|
102
|
+
expect(info!.thisElement.label).toBe("div"); // createElement creates a div
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("returns FullElementInfo with default line and column of 1", () => {
|
|
106
|
+
const el = createMockVueElement({
|
|
107
|
+
file: "/src/App.vue",
|
|
108
|
+
name: "App",
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const info = getElementInfo(el);
|
|
112
|
+
expect(info).not.toBeNull();
|
|
113
|
+
expect(info!.thisElement.link!.line).toBe(1);
|
|
114
|
+
expect(info!.thisElement.link!.column).toBe(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("handles nested Vue component structure with subTree", () => {
|
|
118
|
+
const el = createMockVueElement({
|
|
119
|
+
file: "/src/Layout.vue",
|
|
120
|
+
name: "Layout",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Add a mock subTree (though getElementInfo doesn't use it in the basic case)
|
|
124
|
+
(el.__vueParentComponent as any).subTree = { el };
|
|
125
|
+
|
|
126
|
+
const info = getElementInfo(el);
|
|
127
|
+
expect(info).not.toBeNull();
|
|
128
|
+
expect(info!.componentsLabels[0]!.label).toBe("Layout");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("VueTreeNodeElement.getSource", () => {
|
|
133
|
+
test("returns null when element has no __vueParentComponent", () => {
|
|
134
|
+
const el = createMockVueElement(null);
|
|
135
|
+
const node = new VueTreeNodeElement(el);
|
|
136
|
+
expect(node.getSource()).toBeNull();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("returns Source with correct fileName from __vueParentComponent.type.__file", () => {
|
|
140
|
+
const el = createMockVueElement({
|
|
141
|
+
file: "/src/MyComponent.vue",
|
|
142
|
+
name: "MyComponent",
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const node = new VueTreeNodeElement(el);
|
|
146
|
+
const source = node.getSource();
|
|
147
|
+
|
|
148
|
+
expect(source).not.toBeNull();
|
|
149
|
+
expect(source!.fileName).toBe("/src/MyComponent.vue");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("returns Source with lineNumber and columnNumber of 1", () => {
|
|
153
|
+
const el = createMockVueElement({
|
|
154
|
+
file: "/src/Button.vue",
|
|
155
|
+
name: "Button",
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const node = new VueTreeNodeElement(el);
|
|
159
|
+
const source = node.getSource();
|
|
160
|
+
|
|
161
|
+
expect(source).not.toBeNull();
|
|
162
|
+
expect(source!.lineNumber).toBe(1);
|
|
163
|
+
expect(source!.columnNumber).toBe(1);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("returns null when __vueParentComponent.type is missing", () => {
|
|
167
|
+
const el = createMockVueElement(null);
|
|
168
|
+
el.__vueParentComponent = {} as ComponentInternalInstance;
|
|
169
|
+
const node = new VueTreeNodeElement(el);
|
|
170
|
+
expect(node.getSource()).toBeNull();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("returns null when __vueParentComponent.type.__file is missing", () => {
|
|
174
|
+
const el = createMockVueElement(null);
|
|
175
|
+
el.__vueParentComponent = {
|
|
176
|
+
type: { __name: "MyComponent" },
|
|
177
|
+
} as ComponentInternalInstance;
|
|
178
|
+
const node = new VueTreeNodeElement(el);
|
|
179
|
+
expect(node.getSource()).toBeNull();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("returns Source from different Vue components", () => {
|
|
183
|
+
const el1 = createMockVueElement({
|
|
184
|
+
file: "/src/Header.vue",
|
|
185
|
+
name: "Header",
|
|
186
|
+
});
|
|
187
|
+
const el2 = createMockVueElement({
|
|
188
|
+
file: "/src/Footer.vue",
|
|
189
|
+
name: "Footer",
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const node1 = new VueTreeNodeElement(el1);
|
|
193
|
+
const node2 = new VueTreeNodeElement(el2);
|
|
194
|
+
|
|
195
|
+
const source1 = node1.getSource();
|
|
196
|
+
const source2 = node2.getSource();
|
|
197
|
+
|
|
198
|
+
expect(source1!.fileName).toBe("/src/Header.vue");
|
|
199
|
+
expect(source2!.fileName).toBe("/src/Footer.vue");
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("VueTreeNodeElement.getComponent", () => {
|
|
204
|
+
test("always returns null", () => {
|
|
205
|
+
const el = createMockVueElement({
|
|
206
|
+
file: "/src/MyComponent.vue",
|
|
207
|
+
name: "MyComponent",
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const node = new VueTreeNodeElement(el);
|
|
211
|
+
expect(node.getComponent()).toBeNull();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("returns null even when __vueParentComponent is present", () => {
|
|
215
|
+
const el = createMockVueElement({
|
|
216
|
+
file: "/src/Button.vue",
|
|
217
|
+
name: "Button",
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const node = new VueTreeNodeElement(el);
|
|
221
|
+
expect(node.getComponent()).toBeNull();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("returns null when element has no __vueParentComponent", () => {
|
|
225
|
+
const el = createMockVueElement(null);
|
|
226
|
+
const node = new VueTreeNodeElement(el);
|
|
227
|
+
expect(node.getComponent()).toBeNull();
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe("VueTreeNodeElement inheritance and structure", () => {
|
|
232
|
+
test("extends HtmlElementTreeNode with custom getSource and getComponent", () => {
|
|
233
|
+
const el = createMockVueElement({
|
|
234
|
+
file: "/src/App.vue",
|
|
235
|
+
name: "App",
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const node = new VueTreeNodeElement(el);
|
|
239
|
+
|
|
240
|
+
// Should inherit from HtmlElementTreeNode
|
|
241
|
+
expect(node.type).toBe("element");
|
|
242
|
+
expect(node.name).toBe("div");
|
|
243
|
+
expect(node.element).toBe(el);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("getBox returns element's bounding client rect", () => {
|
|
247
|
+
const el = createMockVueElement({
|
|
248
|
+
file: "/src/App.vue",
|
|
249
|
+
name: "App",
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const node = new VueTreeNodeElement(el);
|
|
253
|
+
const box = node.getBox();
|
|
254
|
+
|
|
255
|
+
expect(box).not.toBeNull();
|
|
256
|
+
expect(box!.width).toBe(100);
|
|
257
|
+
expect(box!.height).toBe(50);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { describe, expect, test, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { createBrowserAPI, installBrowserAPI } from "./browserApi";
|
|
3
|
+
|
|
4
|
+
// Mock the heavy dependencies
|
|
5
|
+
vi.mock("./adapters/createTreeNode", () => ({
|
|
6
|
+
createTreeNode: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
vi.mock("./functions/enrichAncestrySourceMaps", () => ({
|
|
9
|
+
enrichAncestryWithSourceMaps: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import { createTreeNode } from "./adapters/createTreeNode";
|
|
13
|
+
import { enrichAncestryWithSourceMaps } from "./functions/enrichAncestrySourceMaps";
|
|
14
|
+
|
|
15
|
+
describe("browserApi", () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
// Reset window.__treelocator__ before each test
|
|
18
|
+
delete (window as any).__treelocator__;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("createBrowserAPI", () => {
|
|
26
|
+
test("returns an object with all expected method names", () => {
|
|
27
|
+
const api = createBrowserAPI();
|
|
28
|
+
expect(api).toHaveProperty("getPath");
|
|
29
|
+
expect(api).toHaveProperty("getAncestry");
|
|
30
|
+
expect(api).toHaveProperty("getPathData");
|
|
31
|
+
expect(api).toHaveProperty("help");
|
|
32
|
+
expect(api).toHaveProperty("replay");
|
|
33
|
+
expect(api).toHaveProperty("replayWithRecord");
|
|
34
|
+
expect(api).toHaveProperty("diff");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("all methods are functions", () => {
|
|
38
|
+
const api = createBrowserAPI();
|
|
39
|
+
expect(typeof api.getPath).toBe("function");
|
|
40
|
+
expect(typeof api.getAncestry).toBe("function");
|
|
41
|
+
expect(typeof api.getPathData).toBe("function");
|
|
42
|
+
expect(typeof api.help).toBe("function");
|
|
43
|
+
expect(typeof api.replay).toBe("function");
|
|
44
|
+
expect(typeof api.replayWithRecord).toBe("function");
|
|
45
|
+
expect(typeof api.diff.snapshot).toBe("function");
|
|
46
|
+
expect(typeof api.diff.computeDiff).toBe("function");
|
|
47
|
+
expect(typeof api.diff.captureDiff).toBe("function");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("diff namespace", () => {
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
if (typeof (document as any).getAnimations !== "function") {
|
|
54
|
+
(document as any).getAnimations = () => [];
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("snapshot() returns an array", () => {
|
|
59
|
+
const api = createBrowserAPI();
|
|
60
|
+
const snaps = api.diff.snapshot();
|
|
61
|
+
expect(Array.isArray(snaps)).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("computeDiff of identical snapshots has empty entries", () => {
|
|
65
|
+
const api = createBrowserAPI();
|
|
66
|
+
const snaps = api.diff.snapshot();
|
|
67
|
+
const report = api.diff.computeDiff(snaps, snaps);
|
|
68
|
+
expect(report.entries).toHaveLength(0);
|
|
69
|
+
expect(report.counts.added).toBe(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("captureDiff returns a DeltaReport with settle and text populated", async () => {
|
|
73
|
+
const api = createBrowserAPI();
|
|
74
|
+
const report = await api.diff.captureDiff(() => {
|
|
75
|
+
// no-op action
|
|
76
|
+
}, { settleTimeoutMs: 200 });
|
|
77
|
+
expect(report).toHaveProperty("entries");
|
|
78
|
+
expect(report).toHaveProperty("counts");
|
|
79
|
+
expect(report).toHaveProperty("text");
|
|
80
|
+
expect(typeof report.text).toBe("string");
|
|
81
|
+
expect(["clean", "timeout"]).toContain(report.settle);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("help()", () => {
|
|
86
|
+
test("returns a non-empty string", () => {
|
|
87
|
+
const api = createBrowserAPI();
|
|
88
|
+
const help = api.help();
|
|
89
|
+
expect(typeof help).toBe("string");
|
|
90
|
+
expect(help.length).toBeGreaterThan(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("help text contains 'TreeLocatorJS'", () => {
|
|
94
|
+
const api = createBrowserAPI();
|
|
95
|
+
const help = api.help();
|
|
96
|
+
expect(help).toContain("TreeLocatorJS");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("help text contains method descriptions", () => {
|
|
100
|
+
const api = createBrowserAPI();
|
|
101
|
+
const help = api.help();
|
|
102
|
+
expect(help).toContain("getPath");
|
|
103
|
+
expect(help).toContain("getAncestry");
|
|
104
|
+
expect(help).toContain("getPathData");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("replay()", () => {
|
|
109
|
+
test("does not throw when called", () => {
|
|
110
|
+
const api = createBrowserAPI();
|
|
111
|
+
expect(() => api.replay()).not.toThrow();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("is a stub that does nothing", () => {
|
|
115
|
+
const api = createBrowserAPI();
|
|
116
|
+
// Should not throw and should return undefined
|
|
117
|
+
const result = api.replay();
|
|
118
|
+
expect(result).toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("replayWithRecord()", () => {
|
|
123
|
+
test("returns a Promise that resolves to null (stub)", async () => {
|
|
124
|
+
const api = createBrowserAPI();
|
|
125
|
+
const result = api.replayWithRecord("div");
|
|
126
|
+
expect(result).toBeInstanceOf(Promise);
|
|
127
|
+
const resolved = await result;
|
|
128
|
+
expect(resolved).toBeNull();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("handles both element and selector arguments without error", async () => {
|
|
132
|
+
const api = createBrowserAPI();
|
|
133
|
+
const div = document.createElement("div");
|
|
134
|
+
|
|
135
|
+
// Test with selector
|
|
136
|
+
const result1 = api.replayWithRecord("div");
|
|
137
|
+
expect(await result1).toBeNull();
|
|
138
|
+
|
|
139
|
+
// Test with element
|
|
140
|
+
const result2 = api.replayWithRecord(div);
|
|
141
|
+
expect(await result2).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("getPath()", () => {
|
|
146
|
+
test("returns Promise<string | null>", async () => {
|
|
147
|
+
const api = createBrowserAPI();
|
|
148
|
+
const result = api.getPath(document.body);
|
|
149
|
+
expect(result).toBeInstanceOf(Promise);
|
|
150
|
+
const resolved = await result;
|
|
151
|
+
expect(typeof resolved === "string" || resolved === null).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("returns null when selector doesn't match any element", async () => {
|
|
155
|
+
const api = createBrowserAPI();
|
|
156
|
+
const result = await api.getPath(".non-existent-selector-xyz");
|
|
157
|
+
expect(result).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("returns null when passed invalid selector string", async () => {
|
|
161
|
+
const api = createBrowserAPI();
|
|
162
|
+
// querySelector with invalid selector will throw; the function should handle it
|
|
163
|
+
const result = await api.getPath("div > > div");
|
|
164
|
+
expect(result).toBeNull();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("returns null when createTreeNode returns null", async () => {
|
|
168
|
+
const mockCreateTreeNode = createTreeNode as any;
|
|
169
|
+
const mockEnrich = enrichAncestryWithSourceMaps as any;
|
|
170
|
+
|
|
171
|
+
mockCreateTreeNode.mockReturnValue(null);
|
|
172
|
+
mockEnrich.mockResolvedValue(null);
|
|
173
|
+
|
|
174
|
+
const api = createBrowserAPI();
|
|
175
|
+
const div = document.createElement("div");
|
|
176
|
+
const result = await api.getPath(div);
|
|
177
|
+
expect(result).toBeNull();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("getAncestry()", () => {
|
|
182
|
+
test("returns Promise<AncestryItem[] | null>", async () => {
|
|
183
|
+
const api = createBrowserAPI();
|
|
184
|
+
const result = api.getAncestry(document.body);
|
|
185
|
+
expect(result).toBeInstanceOf(Promise);
|
|
186
|
+
const resolved = await result;
|
|
187
|
+
expect(Array.isArray(resolved) || resolved === null).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("returns null when selector doesn't match any element", async () => {
|
|
191
|
+
const api = createBrowserAPI();
|
|
192
|
+
const result = await api.getAncestry(".non-existent-selector-xyz");
|
|
193
|
+
expect(result).toBeNull();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("returns null when createTreeNode returns null", async () => {
|
|
197
|
+
const mockCreateTreeNode = createTreeNode as any;
|
|
198
|
+
const mockEnrich = enrichAncestryWithSourceMaps as any;
|
|
199
|
+
|
|
200
|
+
mockCreateTreeNode.mockReturnValue(null);
|
|
201
|
+
mockEnrich.mockResolvedValue(null);
|
|
202
|
+
|
|
203
|
+
const api = createBrowserAPI();
|
|
204
|
+
const div = document.createElement("div");
|
|
205
|
+
const result = await api.getAncestry(div);
|
|
206
|
+
expect(result).toBeNull();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("getPathData()", () => {
|
|
211
|
+
test("returns Promise with path and ancestry properties or null", async () => {
|
|
212
|
+
const api = createBrowserAPI();
|
|
213
|
+
const result = api.getPathData(document.body);
|
|
214
|
+
expect(result).toBeInstanceOf(Promise);
|
|
215
|
+
const resolved = await result;
|
|
216
|
+
if (resolved !== null) {
|
|
217
|
+
expect(resolved).toHaveProperty("path");
|
|
218
|
+
expect(resolved).toHaveProperty("ancestry");
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("returns null when selector doesn't match any element", async () => {
|
|
223
|
+
const api = createBrowserAPI();
|
|
224
|
+
const result = await api.getPathData(".non-existent-selector-xyz");
|
|
225
|
+
expect(result).toBeNull();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("returns null when createTreeNode returns null", async () => {
|
|
229
|
+
const mockCreateTreeNode = createTreeNode as any;
|
|
230
|
+
const mockEnrich = enrichAncestryWithSourceMaps as any;
|
|
231
|
+
|
|
232
|
+
mockCreateTreeNode.mockReturnValue(null);
|
|
233
|
+
mockEnrich.mockResolvedValue(null);
|
|
234
|
+
|
|
235
|
+
const api = createBrowserAPI();
|
|
236
|
+
const div = document.createElement("div");
|
|
237
|
+
const result = await api.getPathData(div);
|
|
238
|
+
expect(result).toBeNull();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe("installBrowserAPI()", () => {
|
|
243
|
+
test("sets window.__treelocator__ to the created API object", () => {
|
|
244
|
+
expect((window as any).__treelocator__).toBeUndefined();
|
|
245
|
+
installBrowserAPI();
|
|
246
|
+
expect((window as any).__treelocator__).toBeDefined();
|
|
247
|
+
expect(typeof (window as any).__treelocator__.getPath).toBe("function");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("window.__treelocator__ has all expected methods after installation", () => {
|
|
251
|
+
installBrowserAPI();
|
|
252
|
+
const api = (window as any).__treelocator__;
|
|
253
|
+
expect(api).toHaveProperty("getPath");
|
|
254
|
+
expect(api).toHaveProperty("getAncestry");
|
|
255
|
+
expect(api).toHaveProperty("getPathData");
|
|
256
|
+
expect(api).toHaveProperty("help");
|
|
257
|
+
expect(api).toHaveProperty("replay");
|
|
258
|
+
expect(api).toHaveProperty("replayWithRecord");
|
|
259
|
+
expect(api).toHaveProperty("diff");
|
|
260
|
+
expect(typeof api.diff.snapshot).toBe("function");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("installBrowserAPI passes adapterId to createBrowserAPI", () => {
|
|
264
|
+
// We can't directly test this without spying on createBrowserAPI,
|
|
265
|
+
// but we can verify that the API is created with or without an adapterId
|
|
266
|
+
installBrowserAPI("react");
|
|
267
|
+
expect((window as any).__treelocator__).toBeDefined();
|
|
268
|
+
expect(typeof (window as any).__treelocator__.help).toBe("function");
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("API accepts HTMLElement or selector string", () => {
|
|
273
|
+
test("getPath accepts HTMLElement", async () => {
|
|
274
|
+
const api = createBrowserAPI();
|
|
275
|
+
const div = document.createElement("div");
|
|
276
|
+
document.body.appendChild(div);
|
|
277
|
+
|
|
278
|
+
const mockCreateTreeNode = createTreeNode as any;
|
|
279
|
+
const mockEnrich = enrichAncestryWithSourceMaps as any;
|
|
280
|
+
mockCreateTreeNode.mockReturnValue(null);
|
|
281
|
+
mockEnrich.mockResolvedValue(null);
|
|
282
|
+
|
|
283
|
+
const result = await api.getPath(div);
|
|
284
|
+
// With null ancestry, should return null
|
|
285
|
+
expect(result).toBeNull();
|
|
286
|
+
|
|
287
|
+
document.body.removeChild(div);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("getPath accepts CSS selector string", async () => {
|
|
291
|
+
const api = createBrowserAPI();
|
|
292
|
+
const result = await api.getPath("body");
|
|
293
|
+
// Should not throw, result is either string or null
|
|
294
|
+
expect(typeof result === "string" || result === null).toBe(true);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("getAncestry accepts HTMLElement", async () => {
|
|
298
|
+
const api = createBrowserAPI();
|
|
299
|
+
const div = document.createElement("div");
|
|
300
|
+
const result = await api.getAncestry(div);
|
|
301
|
+
expect(result === null || Array.isArray(result)).toBe(true);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("getAncestry accepts CSS selector string", async () => {
|
|
305
|
+
const api = createBrowserAPI();
|
|
306
|
+
const result = await api.getAncestry("div");
|
|
307
|
+
expect(result === null || Array.isArray(result)).toBe(true);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("getPathData accepts HTMLElement", async () => {
|
|
311
|
+
const api = createBrowserAPI();
|
|
312
|
+
const div = document.createElement("div");
|
|
313
|
+
const result = await api.getPathData(div);
|
|
314
|
+
if (result !== null) {
|
|
315
|
+
expect(result).toHaveProperty("path");
|
|
316
|
+
expect(result).toHaveProperty("ancestry");
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("getPathData accepts CSS selector string", async () => {
|
|
321
|
+
const api = createBrowserAPI();
|
|
322
|
+
const result = await api.getPathData("body");
|
|
323
|
+
if (result !== null) {
|
|
324
|
+
expect(result).toHaveProperty("path");
|
|
325
|
+
expect(result).toHaveProperty("ancestry");
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
});
|