@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.
Files changed (166) hide show
  1. package/.eslintignore +1 -0
  2. package/dist/_generated_styles.d.ts +1 -1
  3. package/dist/_generated_styles.js +20 -0
  4. package/dist/_generated_tree_icon.d.ts +1 -1
  5. package/dist/adapters/HtmlElementTreeNode.d.ts +2 -2
  6. package/dist/adapters/HtmlElementTreeNode.js +4 -6
  7. package/dist/adapters/createTreeNode.js +17 -44
  8. package/dist/adapters/detectFramework.d.ts +8 -0
  9. package/dist/adapters/detectFramework.js +25 -0
  10. package/dist/adapters/detectFramework.test.d.ts +1 -0
  11. package/dist/adapters/detectFramework.test.js +60 -0
  12. package/dist/adapters/jsx/jsxAdapter.js +54 -89
  13. package/dist/adapters/jsx/jsxAdapter.test.d.ts +1 -0
  14. package/dist/adapters/jsx/jsxAdapter.test.js +273 -0
  15. package/dist/adapters/nextjs/parseNextjsDataAttributes.js +1 -1
  16. package/dist/adapters/nextjs/parseNextjsDataAttributes.test.d.ts +1 -0
  17. package/dist/adapters/nextjs/parseNextjsDataAttributes.test.js +158 -0
  18. package/dist/adapters/react/findFiberByHtmlElement.d.ts +1 -1
  19. package/dist/adapters/react/findFiberByHtmlElement.js +1 -1
  20. package/dist/adapters/react/getAllParentsElementsAndRootComponent.js +4 -0
  21. package/dist/adapters/resolveAdapter.d.ts +1 -1
  22. package/dist/adapters/resolveAdapter.js +4 -8
  23. package/dist/adapters/svelte/svelteAdapter.test.d.ts +1 -0
  24. package/dist/adapters/svelte/svelteAdapter.test.js +280 -0
  25. package/dist/adapters/vue/vueAdapter.test.d.ts +1 -0
  26. package/dist/adapters/vue/vueAdapter.test.js +222 -0
  27. package/dist/browserApi.d.ts +148 -0
  28. package/dist/browserApi.js +146 -5
  29. package/dist/browserApi.test.d.ts +1 -0
  30. package/dist/browserApi.test.js +287 -0
  31. package/dist/components/RecordingPillButton.d.ts +11 -0
  32. package/dist/components/RecordingPillButton.js +202 -0
  33. package/dist/components/RecordingResults.d.ts +2 -0
  34. package/dist/components/RecordingResults.js +213 -78
  35. package/dist/components/Runtime.js +161 -554
  36. package/dist/components/SettingsPanel.d.ts +5 -0
  37. package/dist/components/SettingsPanel.js +312 -0
  38. package/dist/consoleCapture.d.ts +9 -0
  39. package/dist/consoleCapture.js +95 -0
  40. package/dist/dejitter/recorder.d.ts +7 -1
  41. package/dist/dejitter/recorder.js +64 -1
  42. package/dist/functions/cssRuleInspector.d.ts +83 -0
  43. package/dist/functions/cssRuleInspector.js +608 -0
  44. package/dist/functions/cssRuleInspector.test.d.ts +1 -0
  45. package/dist/functions/cssRuleInspector.test.js +439 -0
  46. package/dist/functions/deduplicateLabels.test.d.ts +1 -0
  47. package/dist/functions/deduplicateLabels.test.js +178 -0
  48. package/dist/functions/enrichAncestrySourceMaps.js +0 -1
  49. package/dist/functions/extractComputedStyles.d.ts +51 -0
  50. package/dist/functions/extractComputedStyles.js +447 -0
  51. package/dist/functions/extractComputedStyles.test.d.ts +1 -0
  52. package/dist/functions/extractComputedStyles.test.js +549 -0
  53. package/dist/functions/formatAncestryChain.d.ts +8 -0
  54. package/dist/functions/formatAncestryChain.js +21 -1
  55. package/dist/functions/formatAncestryChain.test.js +18 -0
  56. package/dist/functions/getUsableName.test.d.ts +1 -0
  57. package/dist/functions/getUsableName.test.js +219 -0
  58. package/dist/functions/isCombinationModifiersPressed.test.d.ts +1 -0
  59. package/dist/functions/isCombinationModifiersPressed.test.js +192 -0
  60. package/dist/functions/mergeRects.test.js +210 -1
  61. package/dist/functions/namedSnapshots.d.ts +52 -0
  62. package/dist/functions/namedSnapshots.js +161 -0
  63. package/dist/functions/namedSnapshots.test.d.ts +1 -0
  64. package/dist/functions/namedSnapshots.test.js +85 -0
  65. package/dist/functions/normalizeFilePath.test.d.ts +1 -0
  66. package/dist/functions/normalizeFilePath.test.js +66 -0
  67. package/dist/functions/parseDataId.test.d.ts +1 -0
  68. package/dist/functions/parseDataId.test.js +101 -0
  69. package/dist/hooks/getStorage.d.ts +3 -0
  70. package/dist/hooks/getStorage.js +17 -0
  71. package/dist/hooks/useEventListeners.d.ts +15 -0
  72. package/dist/hooks/useEventListeners.js +56 -0
  73. package/dist/hooks/useLocatorStorage.d.ts +18 -0
  74. package/dist/hooks/useLocatorStorage.js +41 -0
  75. package/dist/hooks/useLocatorStorage.test.d.ts +1 -0
  76. package/dist/hooks/useLocatorStorage.test.js +124 -0
  77. package/dist/hooks/useRecordingState.d.ts +43 -0
  78. package/dist/hooks/useRecordingState.js +387 -0
  79. package/dist/hooks/useSettings.d.ts +13 -0
  80. package/dist/hooks/useSettings.js +66 -0
  81. package/dist/index.d.ts +5 -2
  82. package/dist/index.js +4 -2
  83. package/dist/initRuntime.d.ts +3 -1
  84. package/dist/initRuntime.js +4 -1
  85. package/dist/mcpBridge.d.ts +61 -0
  86. package/dist/mcpBridge.js +534 -0
  87. package/dist/mcpBridge.test.d.ts +1 -0
  88. package/dist/mcpBridge.test.js +248 -0
  89. package/dist/output.css +20 -0
  90. package/dist/visualDiff/diff.d.ts +9 -0
  91. package/dist/visualDiff/diff.js +209 -0
  92. package/dist/visualDiff/diff.test.d.ts +1 -0
  93. package/dist/visualDiff/diff.test.js +253 -0
  94. package/dist/visualDiff/settle.d.ts +3 -0
  95. package/dist/visualDiff/settle.js +50 -0
  96. package/dist/visualDiff/settle.test.d.ts +1 -0
  97. package/dist/visualDiff/settle.test.js +65 -0
  98. package/dist/visualDiff/snapshot.d.ts +4 -0
  99. package/dist/visualDiff/snapshot.js +84 -0
  100. package/dist/visualDiff/snapshot.test.d.ts +1 -0
  101. package/dist/visualDiff/snapshot.test.js +245 -0
  102. package/dist/visualDiff/types.d.ts +37 -0
  103. package/dist/visualDiff/types.js +1 -0
  104. package/package.json +2 -2
  105. package/scripts/wrapCSS.js +1 -1
  106. package/scripts/wrapImage.js +1 -1
  107. package/src/_generated_styles.ts +21 -1
  108. package/src/_generated_tree_icon.ts +1 -1
  109. package/src/adapters/HtmlElementTreeNode.ts +10 -7
  110. package/src/adapters/createTreeNode.ts +12 -51
  111. package/src/adapters/detectFramework.test.ts +73 -0
  112. package/src/adapters/detectFramework.ts +28 -0
  113. package/src/adapters/jsx/jsxAdapter.test.ts +240 -0
  114. package/src/adapters/jsx/jsxAdapter.ts +53 -106
  115. package/src/adapters/nextjs/parseNextjsDataAttributes.test.ts +212 -0
  116. package/src/adapters/nextjs/parseNextjsDataAttributes.ts +1 -1
  117. package/src/adapters/react/findDebugSource.ts +5 -6
  118. package/src/adapters/react/findFiberByHtmlElement.ts +3 -3
  119. package/src/adapters/react/getAllParentsElementsAndRootComponent.ts +3 -0
  120. package/src/adapters/react/reactAdapter.ts +1 -2
  121. package/src/adapters/resolveAdapter.ts +4 -14
  122. package/src/adapters/svelte/svelteAdapter.test.ts +334 -0
  123. package/src/adapters/vue/vueAdapter.test.ts +259 -0
  124. package/src/browserApi.test.ts +329 -0
  125. package/src/browserApi.ts +351 -4
  126. package/src/components/RecordingPillButton.tsx +301 -0
  127. package/src/components/RecordingResults.tsx +114 -13
  128. package/src/components/Runtime.tsx +176 -621
  129. package/src/components/SettingsPanel.tsx +339 -0
  130. package/src/consoleCapture.ts +113 -0
  131. package/src/dejitter/recorder.ts +67 -3
  132. package/src/functions/cssRuleInspector.test.ts +517 -0
  133. package/src/functions/cssRuleInspector.ts +708 -0
  134. package/src/functions/deduplicateLabels.test.ts +115 -0
  135. package/src/functions/enrichAncestrySourceMaps.ts +6 -3
  136. package/src/functions/extractComputedStyles.test.ts +681 -0
  137. package/src/functions/extractComputedStyles.ts +768 -0
  138. package/src/functions/formatAncestryChain.test.ts +23 -1
  139. package/src/functions/formatAncestryChain.ts +22 -1
  140. package/src/functions/getUsableName.test.ts +242 -0
  141. package/src/functions/isCombinationModifiersPressed.test.ts +156 -0
  142. package/src/functions/mergeRects.test.ts +111 -1
  143. package/src/functions/namedSnapshots.test.ts +106 -0
  144. package/src/functions/namedSnapshots.ts +232 -0
  145. package/src/functions/normalizeFilePath.test.ts +80 -0
  146. package/src/functions/parseDataId.test.ts +125 -0
  147. package/src/hooks/getStorage.ts +26 -0
  148. package/src/hooks/useEventListeners.ts +97 -0
  149. package/src/hooks/useLocatorStorage.test.ts +127 -0
  150. package/src/hooks/useLocatorStorage.ts +60 -0
  151. package/src/hooks/useRecordingState.ts +516 -0
  152. package/src/hooks/useSettings.ts +83 -0
  153. package/src/index.ts +10 -5
  154. package/src/initRuntime.ts +5 -0
  155. package/src/mcpBridge.test.ts +260 -0
  156. package/src/mcpBridge.ts +677 -0
  157. package/src/visualDiff/diff.test.ts +167 -0
  158. package/src/visualDiff/diff.ts +242 -0
  159. package/src/visualDiff/settle.test.ts +77 -0
  160. package/src/visualDiff/settle.ts +62 -0
  161. package/src/visualDiff/snapshot.test.ts +200 -0
  162. package/src/visualDiff/snapshot.ts +119 -0
  163. package/src/visualDiff/types.ts +40 -0
  164. package/tsconfig.json +3 -1
  165. package/vitest.config.ts +18 -0
  166. package/jest.config.ts +0 -195
@@ -0,0 +1,212 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { collectNextjsServerComponents, parseNextjsServerComponents } from "./parseNextjsDataAttributes";
3
+
4
+ describe("collectNextjsServerComponents", () => {
5
+ test("extracts component name from layout.tsx", () => {
6
+ const element = document.createElement("div");
7
+ element.setAttribute("data-locatorjs", "/apps/next-16/app/layout.tsx:27:4");
8
+
9
+ const result = collectNextjsServerComponents(element);
10
+
11
+ expect(result).toHaveLength(1);
12
+ expect(result[0]).toEqual({
13
+ name: "RootLayout",
14
+ filePath: "app/layout.tsx",
15
+ line: 27,
16
+ type: "component",
17
+ });
18
+ });
19
+
20
+ test("extracts component name from page.tsx", () => {
21
+ const element = document.createElement("div");
22
+ element.setAttribute("data-locatorjs", "/apps/next-16/app/page.tsx:5:4");
23
+
24
+ const result = collectNextjsServerComponents(element);
25
+
26
+ expect(result).toHaveLength(1);
27
+ expect(result[0]).toEqual({
28
+ name: "Page",
29
+ filePath: "app/page.tsx",
30
+ line: 5,
31
+ type: "component",
32
+ });
33
+ });
34
+
35
+ test("extracts custom component name from Header.tsx", () => {
36
+ const element = document.createElement("div");
37
+ element.setAttribute("data-locatorjs", "/apps/next-16/app/components/Header.tsx:10:2");
38
+
39
+ const result = collectNextjsServerComponents(element);
40
+
41
+ expect(result).toHaveLength(1);
42
+ expect(result[0]).toEqual({
43
+ name: "Header",
44
+ filePath: "app/components/Header.tsx",
45
+ line: 10,
46
+ type: "component",
47
+ });
48
+ });
49
+
50
+ test("extracts custom component name from Button.tsx", () => {
51
+ const element = document.createElement("div");
52
+ element.setAttribute("data-locatorjs", "/home/project/src/components/Button.tsx:15:5");
53
+
54
+ const result = collectNextjsServerComponents(element);
55
+
56
+ expect(result).toHaveLength(1);
57
+ expect(result[0]).toEqual({
58
+ name: "Button",
59
+ filePath: "src/components/Button.tsx",
60
+ line: 15,
61
+ type: "component",
62
+ });
63
+ });
64
+
65
+ test("returns empty array when element has no data-locatorjs attribute", () => {
66
+ const element = document.createElement("div");
67
+
68
+ const result = collectNextjsServerComponents(element);
69
+
70
+ expect(result).toEqual([]);
71
+ });
72
+
73
+ test("handles nested layout files", () => {
74
+ const element = document.createElement("div");
75
+ element.setAttribute("data-locatorjs", "/workspace/app/dashboard/layout.tsx:3:2");
76
+
77
+ const result = collectNextjsServerComponents(element);
78
+
79
+ expect(result).toHaveLength(1);
80
+ expect(result[0]!.name).toBe("RootLayout");
81
+ });
82
+
83
+ test("handles TypeScript file extension", () => {
84
+ const element = document.createElement("div");
85
+ element.setAttribute("data-locatorjs", "/home/project/app/layout.ts:10:1");
86
+
87
+ const result = collectNextjsServerComponents(element);
88
+
89
+ expect(result).toHaveLength(1);
90
+ expect(result[0]).toEqual({
91
+ name: "RootLayout",
92
+ filePath: "app/layout.ts",
93
+ line: 10,
94
+ type: "component",
95
+ });
96
+ });
97
+
98
+ test("handles JSX file extension", () => {
99
+ const element = document.createElement("div");
100
+ element.setAttribute("data-locatorjs", "/home/project/app/page.jsx:5:1");
101
+
102
+ const result = collectNextjsServerComponents(element);
103
+
104
+ expect(result).toHaveLength(1);
105
+ expect(result[0]!.name).toBe("Page");
106
+ });
107
+
108
+ test("handles JavaScript file extension", () => {
109
+ const element = document.createElement("div");
110
+ element.setAttribute("data-locatorjs", "/home/project/app/layout.js:5:1");
111
+
112
+ const result = collectNextjsServerComponents(element);
113
+
114
+ expect(result).toHaveLength(1);
115
+ expect(result[0]!.name).toBe("RootLayout");
116
+ });
117
+
118
+ test("parses line and column numbers correctly", () => {
119
+ const element = document.createElement("div");
120
+ element.setAttribute("data-locatorjs", "/path/app/page.tsx:123:45");
121
+
122
+ const result = collectNextjsServerComponents(element);
123
+
124
+ expect(result[0]!.line).toBe(123);
125
+ });
126
+
127
+ test("handles empty data-locatorjs attribute", () => {
128
+ const element = document.createElement("div");
129
+ element.setAttribute("data-locatorjs", "");
130
+
131
+ const result = collectNextjsServerComponents(element);
132
+
133
+ expect(result).toEqual([]);
134
+ });
135
+
136
+ test("handles malformed data-locatorjs attribute with missing colons", () => {
137
+ const element = document.createElement("div");
138
+ element.setAttribute("data-locatorjs", "/path/to/file.tsx");
139
+
140
+ const result = collectNextjsServerComponents(element);
141
+
142
+ expect(result).toEqual([]);
143
+ });
144
+
145
+ test("handles malformed data-locatorjs attribute with non-numeric line", () => {
146
+ const element = document.createElement("div");
147
+ element.setAttribute("data-locatorjs", "/path/to/file.tsx:abc:5");
148
+
149
+ const result = collectNextjsServerComponents(element);
150
+
151
+ // parseInt("abc", 10) returns NaN, but the function still returns a component
152
+ // This test validates current behavior - the parser doesn't validate numeric values
153
+ expect(result).toHaveLength(1);
154
+ expect(Number.isNaN(result[0]!.line)).toBe(true);
155
+ });
156
+ });
157
+
158
+ describe("parseNextjsServerComponents", () => {
159
+ test("returns server component info when data-locatorjs present", () => {
160
+ const element = document.createElement("div");
161
+ element.setAttribute("data-locatorjs", "/apps/next-16/app/page.tsx:5:4");
162
+
163
+ const result = parseNextjsServerComponents(element);
164
+
165
+ expect(result).not.toBeNull();
166
+ expect(result).toHaveLength(1);
167
+ expect(result![0]!.name).toBe("Page");
168
+ });
169
+
170
+ test("returns null when element has no data-locatorjs attribute", () => {
171
+ const element = document.createElement("div");
172
+
173
+ const result = parseNextjsServerComponents(element);
174
+
175
+ expect(result).toBeNull();
176
+ });
177
+
178
+ test("returns null when data-locatorjs is empty", () => {
179
+ const element = document.createElement("div");
180
+ element.setAttribute("data-locatorjs", "");
181
+
182
+ const result = parseNextjsServerComponents(element);
183
+
184
+ expect(result).toBeNull();
185
+ });
186
+
187
+ test("returns null when data-locatorjs is malformed", () => {
188
+ const element = document.createElement("div");
189
+ element.setAttribute("data-locatorjs", "/path/to/file.tsx");
190
+
191
+ const result = parseNextjsServerComponents(element);
192
+
193
+ // Parser returns null for malformed attributes (less than 2 parts after split)
194
+ expect(result).toBeNull();
195
+ });
196
+
197
+ test("returns correct server component info from parseNextjsServerComponents", () => {
198
+ const element = document.createElement("div");
199
+ element.setAttribute("data-locatorjs", "/workspace/app/dashboard/layout.tsx:42:10");
200
+
201
+ const result = parseNextjsServerComponents(element);
202
+
203
+ expect(result).toEqual([
204
+ {
205
+ name: "RootLayout",
206
+ filePath: "app/dashboard/layout.tsx",
207
+ line: 42,
208
+ type: "component",
209
+ },
210
+ ]);
211
+ });
212
+ });
@@ -50,7 +50,7 @@ function parseDataLocatorjsValue(value: string): ServerComponentInfo | null {
50
50
  if (parts.length < 2) return null;
51
51
 
52
52
  // Last two parts are column and line (in reverse order)
53
- const column = parts.pop();
53
+ parts.pop(); // discard column
54
54
  const line = parts.pop();
55
55
 
56
56
  // Everything else is the file path (which may contain colons on Windows)
@@ -1,8 +1,7 @@
1
1
  import { Fiber, Source } from "@locator/shared";
2
- import {
3
- resolveSourceFromDebugStack,
4
- parseDebugStack,
5
- } from "./resolveSourceMap";
2
+ import { resolveSourceFromDebugStack } from "./resolveSourceMap";
3
+
4
+ type React19Fiber = Fiber & { _debugStack?: { stack?: string } };
6
5
 
7
6
  export function findDebugSource(
8
7
  fiber: Fiber
@@ -34,7 +33,7 @@ export async function findDebugSourceAsync(
34
33
  // React 19: try resolving via _debugStack + source maps
35
34
  let current: Fiber | null = fiber;
36
35
  while (current) {
37
- const debugStack = (current as any)._debugStack;
36
+ const debugStack = (current as React19Fiber)._debugStack;
38
37
  if (debugStack?.stack) {
39
38
  const source = await resolveSourceFromDebugStack(debugStack);
40
39
  if (source) {
@@ -51,5 +50,5 @@ export async function findDebugSourceAsync(
51
50
  * Check if this is a React 19+ environment (has _debugStack but not _debugSource).
52
51
  */
53
52
  export function isReact19Fiber(fiber: Fiber): boolean {
54
- return !fiber._debugSource && !!(fiber as any)._debugStack;
53
+ return !fiber._debugSource && !!(fiber as React19Fiber)._debugStack;
55
54
  }
@@ -5,18 +5,18 @@ import { findDebugSource } from "./findDebugSource";
5
5
  * Find the React fiber key on a DOM element (e.g., "__reactFiber$abc123").
6
6
  * Works across all React versions that attach fibers to DOM nodes.
7
7
  */
8
- function findFiberFromDOMElement(element: HTMLElement): Fiber | null {
8
+ function findFiberFromDOMElement(element: HTMLElement | SVGElement): Fiber | null {
9
9
  const fiberKey = Object.keys(element).find((k) =>
10
10
  k.startsWith("__reactFiber$")
11
11
  );
12
12
  if (fiberKey) {
13
- return (element as any)[fiberKey] as Fiber;
13
+ return (element as unknown as Record<string, Fiber>)[fiberKey] ?? null;
14
14
  }
15
15
  return null;
16
16
  }
17
17
 
18
18
  export function findFiberByHtmlElement(
19
- target: HTMLElement,
19
+ target: HTMLElement | SVGElement,
20
20
  shouldHaveDebugSource: boolean
21
21
  ): Fiber | null {
22
22
  // Try via DevTools renderers first (available when React DevTools extension is installed)
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-console */
1
2
  import { Fiber } from "@locator/shared";
2
3
  import { getUsableName } from "../../functions/getUsableName";
3
4
  import { mergeRects } from "../../functions/mergeRects";
@@ -14,6 +15,7 @@ export function getAllParentsElementsAndRootComponent(fiber: Fiber): {
14
15
  const parentElements: ElementInfo[] = [];
15
16
  const deepestElement = fiber.stateNode;
16
17
  if (!deepestElement || !(deepestElement instanceof Element)) {
18
+ // eslint-disable-next-line no-console
17
19
  console.warn("[TreeLocator] Skipping fiber with non-Element stateNode:", fiber.type, fiber.stateNode);
18
20
  return null;
19
21
  }
@@ -47,6 +49,7 @@ export function getAllParentsElementsAndRootComponent(fiber: Fiber): {
47
49
  link: null,
48
50
  });
49
51
  }
52
+ // eslint-disable-next-line no-console
50
53
  console.warn("[TreeLocator] Could not find root component for fiber:", fiber.type);
51
54
  return null;
52
55
  }
@@ -1,6 +1,5 @@
1
- import { findDebugSource, findDebugSourceAsync, isReact19Fiber } from "./findDebugSource";
1
+ import { findDebugSource } from "./findDebugSource";
2
2
  import { findFiberByHtmlElement } from "./findFiberByHtmlElement";
3
- import { resolveSourceFromDebugStack } from "./resolveSourceMap";
4
3
  import { getFiberLabel } from "./getFiberLabel";
5
4
  import { getAllWrappingParents } from "./getAllWrappingParents";
6
5
  import { deduplicateLabels } from "../../functions/deduplicateLabels";
@@ -2,14 +2,9 @@ import reactAdapter from "./react/reactAdapter";
2
2
  import jsxAdapter from "./jsx/jsxAdapter";
3
3
  import svelteAdapter from "./svelte/svelteAdapter";
4
4
  import vueAdapter from "./vue/vueAdapter";
5
- import {
6
- detectJSX,
7
- detectReact,
8
- detectSvelte,
9
- detectVue,
10
- } from "@locator/shared";
11
5
  import type { AdapterObject } from "./adapterApi";
12
6
  import type { AdapterId } from "../consts";
7
+ import { detectFramework } from "./detectFramework";
13
8
 
14
9
  const adapterMap: Record<AdapterId, AdapterObject> = {
15
10
  react: reactAdapter,
@@ -21,18 +16,13 @@ const adapterMap: Record<AdapterId, AdapterObject> = {
21
16
  /**
22
17
  * Resolve the framework adapter to use.
23
18
  * If an explicit adapterId is given, return that adapter.
24
- * Otherwise, auto-detect the framework in priority order.
19
+ * Otherwise, auto-detect the framework.
25
20
  */
26
21
  export function resolveAdapter(adapterId?: AdapterId): AdapterObject | null {
27
22
  if (adapterId) {
28
23
  return adapterMap[adapterId] ?? null;
29
24
  }
30
25
 
31
- if (detectSvelte()) return svelteAdapter;
32
- if (detectVue()) return vueAdapter;
33
- if (detectReact()) return reactAdapter;
34
- // Must be last: global data leaks from the LocatorJS extension (SolidJS + JSX plugin in dev)
35
- if (detectJSX()) return jsxAdapter;
36
-
37
- return null;
26
+ const framework = detectFramework();
27
+ return framework ? (adapterMap[framework] ?? null) : null;
38
28
  }
@@ -0,0 +1,334 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { getElementInfo, SvelteTreeNodeElement } from "./svelteAdapter";
3
+
4
+ function createMockSvelteElement(
5
+ metadata?: { char: number; column: number; file: string; line: number } | null
6
+ ): HTMLElement & { __svelte_meta?: { loc: any } } {
7
+ const el = document.createElement("div") as any;
8
+ el.getBoundingClientRect = () => ({
9
+ x: 0,
10
+ y: 0,
11
+ width: 100,
12
+ height: 50,
13
+ top: 0,
14
+ left: 0,
15
+ right: 100,
16
+ bottom: 50,
17
+ toJSON: () => ({}),
18
+ });
19
+
20
+ if (metadata) {
21
+ el.__svelte_meta = {
22
+ loc: metadata,
23
+ };
24
+ }
25
+
26
+ return el;
27
+ }
28
+
29
+ describe("Svelte Adapter - getElementInfo", () => {
30
+ test("returns null when element has no __svelte_meta", () => {
31
+ const el = createMockSvelteElement(null);
32
+ expect(getElementInfo(el)).toBeNull();
33
+ });
34
+
35
+ test("returns FullElementInfo with correct filePath from __svelte_meta.loc.file", () => {
36
+ const el = createMockSvelteElement({
37
+ char: 0,
38
+ column: 5,
39
+ file: "src/App.svelte",
40
+ line: 10,
41
+ });
42
+
43
+ const info = getElementInfo(el);
44
+ expect(info).not.toBeNull();
45
+ expect(info!.thisElement.link!.filePath).toBe("src/App.svelte");
46
+ });
47
+
48
+ test("returns FullElementInfo with line number incremented by 1 from __svelte_meta.loc.line", () => {
49
+ const el = createMockSvelteElement({
50
+ char: 0,
51
+ column: 5,
52
+ file: "src/App.svelte",
53
+ line: 10,
54
+ });
55
+
56
+ const info = getElementInfo(el);
57
+ expect(info).not.toBeNull();
58
+ expect(info!.thisElement.link!.line).toBe(11); // 10 + 1
59
+ });
60
+
61
+ test("returns FullElementInfo with column number incremented by 1 from __svelte_meta.loc.column", () => {
62
+ const el = createMockSvelteElement({
63
+ char: 0,
64
+ column: 5,
65
+ file: "src/App.svelte",
66
+ line: 10,
67
+ });
68
+
69
+ const info = getElementInfo(el);
70
+ expect(info).not.toBeNull();
71
+ expect(info!.thisElement.link!.column).toBe(6); // 5 + 1
72
+ });
73
+
74
+ test("returns FullElementInfo with empty componentsLabels array", () => {
75
+ const el = createMockSvelteElement({
76
+ char: 0,
77
+ column: 0,
78
+ file: "src/Button.svelte",
79
+ line: 5,
80
+ });
81
+
82
+ const info = getElementInfo(el);
83
+ expect(info).not.toBeNull();
84
+ expect(info!.componentsLabels).toEqual([]);
85
+ });
86
+
87
+ test("returns FullElementInfo with htmlElement pointing to the correct element", () => {
88
+ const el = createMockSvelteElement({
89
+ char: 0,
90
+ column: 0,
91
+ file: "src/Card.svelte",
92
+ line: 15,
93
+ });
94
+
95
+ const info = getElementInfo(el);
96
+ expect(info).not.toBeNull();
97
+ expect(info!.htmlElement).toBe(el);
98
+ });
99
+
100
+ test("returns FullElementInfo with element label as lowercase nodeName", () => {
101
+ const el = createMockSvelteElement({
102
+ char: 0,
103
+ column: 0,
104
+ file: "src/Button.svelte",
105
+ line: 5,
106
+ });
107
+
108
+ const info = getElementInfo(el);
109
+ expect(info).not.toBeNull();
110
+ expect(info!.thisElement.label).toBe("div"); // createElement creates a div
111
+ });
112
+
113
+ test("returns FullElementInfo with parentElements as empty array", () => {
114
+ const el = createMockSvelteElement({
115
+ char: 0,
116
+ column: 0,
117
+ file: "src/Layout.svelte",
118
+ line: 20,
119
+ });
120
+
121
+ const info = getElementInfo(el);
122
+ expect(info).not.toBeNull();
123
+ expect(info!.parentElements).toEqual([]);
124
+ });
125
+
126
+ test("handles different line and column values correctly", () => {
127
+ const el1 = createMockSvelteElement({
128
+ char: 0,
129
+ column: 0,
130
+ file: "src/File1.svelte",
131
+ line: 0,
132
+ });
133
+ const el2 = createMockSvelteElement({
134
+ char: 100,
135
+ column: 20,
136
+ file: "src/File2.svelte",
137
+ line: 99,
138
+ });
139
+
140
+ const info1 = getElementInfo(el1);
141
+ const info2 = getElementInfo(el2);
142
+
143
+ expect(info1!.thisElement.link!.line).toBe(1); // 0 + 1
144
+ expect(info1!.thisElement.link!.column).toBe(1); // 0 + 1
145
+ expect(info2!.thisElement.link!.line).toBe(100); // 99 + 1
146
+ expect(info2!.thisElement.link!.column).toBe(21); // 20 + 1
147
+ });
148
+
149
+ test("returns FullElementInfo with projectPath as empty string", () => {
150
+ const el = createMockSvelteElement({
151
+ char: 0,
152
+ column: 0,
153
+ file: "src/App.svelte",
154
+ line: 10,
155
+ });
156
+
157
+ const info = getElementInfo(el);
158
+ expect(info).not.toBeNull();
159
+ expect(info!.thisElement.link!.projectPath).toBe("");
160
+ });
161
+ });
162
+
163
+ describe("SvelteTreeNodeElement.getSource", () => {
164
+ test("returns null when element has no __svelte_meta", () => {
165
+ const el = createMockSvelteElement(null);
166
+ const node = new SvelteTreeNodeElement(el);
167
+ expect(node.getSource()).toBeNull();
168
+ });
169
+
170
+ test("returns Source with correct fileName from __svelte_meta.loc.file", () => {
171
+ const el = createMockSvelteElement({
172
+ char: 0,
173
+ column: 5,
174
+ file: "src/App.svelte",
175
+ line: 10,
176
+ });
177
+
178
+ const node = new SvelteTreeNodeElement(el);
179
+ const source = node.getSource();
180
+
181
+ expect(source).not.toBeNull();
182
+ expect(source!.fileName).toBe("src/App.svelte");
183
+ });
184
+
185
+ test("returns Source with lineNumber incremented by 1 from __svelte_meta.loc.line", () => {
186
+ const el = createMockSvelteElement({
187
+ char: 0,
188
+ column: 5,
189
+ file: "src/App.svelte",
190
+ line: 10,
191
+ });
192
+
193
+ const node = new SvelteTreeNodeElement(el);
194
+ const source = node.getSource();
195
+
196
+ expect(source).not.toBeNull();
197
+ expect(source!.lineNumber).toBe(11); // 10 + 1
198
+ });
199
+
200
+ test("returns Source with columnNumber incremented by 1 from __svelte_meta.loc.column", () => {
201
+ const el = createMockSvelteElement({
202
+ char: 0,
203
+ column: 5,
204
+ file: "src/App.svelte",
205
+ line: 10,
206
+ });
207
+
208
+ const node = new SvelteTreeNodeElement(el);
209
+ const source = node.getSource();
210
+
211
+ expect(source).not.toBeNull();
212
+ expect(source!.columnNumber).toBe(6); // 5 + 1
213
+ });
214
+
215
+ test("returns Source from different Svelte components", () => {
216
+ const el1 = createMockSvelteElement({
217
+ char: 0,
218
+ column: 0,
219
+ file: "src/Header.svelte",
220
+ line: 5,
221
+ });
222
+ const el2 = createMockSvelteElement({
223
+ char: 0,
224
+ column: 0,
225
+ file: "src/Footer.svelte",
226
+ line: 20,
227
+ });
228
+
229
+ const node1 = new SvelteTreeNodeElement(el1);
230
+ const node2 = new SvelteTreeNodeElement(el2);
231
+
232
+ const source1 = node1.getSource();
233
+ const source2 = node2.getSource();
234
+
235
+ expect(source1!.fileName).toBe("src/Header.svelte");
236
+ expect(source2!.fileName).toBe("src/Footer.svelte");
237
+ expect(source1!.lineNumber).toBe(6); // 5 + 1
238
+ expect(source2!.lineNumber).toBe(21); // 20 + 1
239
+ });
240
+
241
+ test("handles line and column value of 0 correctly (increments to 1)", () => {
242
+ const el = createMockSvelteElement({
243
+ char: 0,
244
+ column: 0,
245
+ file: "src/App.svelte",
246
+ line: 0,
247
+ });
248
+
249
+ const node = new SvelteTreeNodeElement(el);
250
+ const source = node.getSource();
251
+
252
+ expect(source).not.toBeNull();
253
+ expect(source!.lineNumber).toBe(1);
254
+ expect(source!.columnNumber).toBe(1);
255
+ });
256
+ });
257
+
258
+ describe("SvelteTreeNodeElement.getComponent", () => {
259
+ test("always returns null", () => {
260
+ const el = createMockSvelteElement({
261
+ char: 0,
262
+ column: 5,
263
+ file: "src/App.svelte",
264
+ line: 10,
265
+ });
266
+
267
+ const node = new SvelteTreeNodeElement(el);
268
+ expect(node.getComponent()).toBeNull();
269
+ });
270
+
271
+ test("returns null even when __svelte_meta is present", () => {
272
+ const el = createMockSvelteElement({
273
+ char: 0,
274
+ column: 0,
275
+ file: "src/Button.svelte",
276
+ line: 5,
277
+ });
278
+
279
+ const node = new SvelteTreeNodeElement(el);
280
+ expect(node.getComponent()).toBeNull();
281
+ });
282
+
283
+ test("returns null when element has no __svelte_meta", () => {
284
+ const el = createMockSvelteElement(null);
285
+ const node = new SvelteTreeNodeElement(el);
286
+ expect(node.getComponent()).toBeNull();
287
+ });
288
+ });
289
+
290
+ describe("SvelteTreeNodeElement inheritance and structure", () => {
291
+ test("extends HtmlElementTreeNode with custom getSource and getComponent", () => {
292
+ const el = createMockSvelteElement({
293
+ char: 0,
294
+ column: 0,
295
+ file: "src/App.svelte",
296
+ line: 10,
297
+ });
298
+
299
+ const node = new SvelteTreeNodeElement(el);
300
+
301
+ // Should inherit from HtmlElementTreeNode
302
+ expect(node.type).toBe("element");
303
+ expect(node.name).toBe("div");
304
+ expect(node.element).toBe(el);
305
+ });
306
+
307
+ test("getBox returns element's bounding client rect", () => {
308
+ const el = createMockSvelteElement({
309
+ char: 0,
310
+ column: 0,
311
+ file: "src/App.svelte",
312
+ line: 10,
313
+ });
314
+
315
+ const node = new SvelteTreeNodeElement(el);
316
+ const box = node.getBox();
317
+
318
+ expect(box).not.toBeNull();
319
+ expect(box!.width).toBe(100);
320
+ expect(box!.height).toBe(50);
321
+ });
322
+
323
+ test("getElement returns the underlying HTMLElement", () => {
324
+ const el = createMockSvelteElement({
325
+ char: 0,
326
+ column: 0,
327
+ file: "src/App.svelte",
328
+ line: 10,
329
+ });
330
+
331
+ const node = new SvelteTreeNodeElement(el);
332
+ expect(node.getElement()).toBe(el);
333
+ });
334
+ });