@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,549 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, it, expect, vi } from "vitest";
3
+ import { _collapseFourValues, _processGroupEntries, _formatDiff, extractComputedStyles, LAYOUT_PROPERTIES, VISUAL_PROPERTIES, TYPOGRAPHY_PROPERTIES, INTERACTION_PROPERTIES, SVG_PROPERTIES } from "./extractComputedStyles";
4
+ import { getElementLabel } from "./formatAncestryChain";
5
+ describe("extractComputedStyles", () => {
6
+ describe("collapseFourValues", () => {
7
+ it("collapses when all four values are the same", () => {
8
+ expect(_collapseFourValues("8px", "8px", "8px", "8px")).toBe("8px");
9
+ });
10
+ it("collapses to two values when top=bottom and left=right", () => {
11
+ expect(_collapseFourValues("8px", "16px", "8px", "16px")).toBe("8px 16px");
12
+ });
13
+ it("collapses to three values when left=right", () => {
14
+ expect(_collapseFourValues("8px", "16px", "4px", "16px")).toBe("8px 16px 4px");
15
+ });
16
+ it("outputs all four when all different", () => {
17
+ expect(_collapseFourValues("1px", "2px", "3px", "4px")).toBe("1px 2px 3px 4px");
18
+ });
19
+ it("handles 0px values", () => {
20
+ expect(_collapseFourValues("0px", "0px", "0px", "0px")).toBe("0px");
21
+ });
22
+ });
23
+ describe("processGroupEntries", () => {
24
+ const alwaysNonDefault = (_prop, value) => value !== "" && value !== "default";
25
+ it("collapses margin into shorthand when 2+ sides are non-default", () => {
26
+ const values = {
27
+ "margin-top": "10px",
28
+ "margin-right": "20px",
29
+ "margin-bottom": "10px",
30
+ "margin-left": "20px"
31
+ };
32
+ const entries = _processGroupEntries(LAYOUT_PROPERTIES, values, alwaysNonDefault);
33
+ const marginEntry = entries.find(e => e.name === "margin");
34
+ expect(marginEntry).toBeDefined();
35
+ expect(marginEntry.value).toBe("10px 20px");
36
+ // No individual margin-* entries
37
+ expect(entries.find(e => e.name === "margin-top")).toBeUndefined();
38
+ });
39
+ it("leaves single margin longhand when only one is non-default", () => {
40
+ const isNonDefault = (prop, value) => {
41
+ if (prop === "margin-top" && value === "10px") return true;
42
+ return false;
43
+ };
44
+ const values = {
45
+ "margin-top": "10px",
46
+ "margin-right": "0px",
47
+ "margin-bottom": "0px",
48
+ "margin-left": "0px"
49
+ };
50
+ const entries = _processGroupEntries(LAYOUT_PROPERTIES, values, isNonDefault);
51
+ expect(entries.find(e => e.name === "margin")).toBeUndefined();
52
+ expect(entries.find(e => e.name === "margin-top")).toBeDefined();
53
+ });
54
+ it("collapses padding into shorthand", () => {
55
+ const values = {
56
+ "padding-top": "8px",
57
+ "padding-right": "16px",
58
+ "padding-bottom": "8px",
59
+ "padding-left": "16px"
60
+ };
61
+ const entries = _processGroupEntries(LAYOUT_PROPERTIES, values, alwaysNonDefault);
62
+ const paddingEntry = entries.find(e => e.name === "padding");
63
+ expect(paddingEntry).toBeDefined();
64
+ expect(paddingEntry.value).toBe("8px 16px");
65
+ });
66
+ it("collapses overflow when both axes are the same", () => {
67
+ const values = {
68
+ "overflow-x": "hidden",
69
+ "overflow-y": "hidden"
70
+ };
71
+ const entries = _processGroupEntries(LAYOUT_PROPERTIES, values, alwaysNonDefault);
72
+ const overflowEntry = entries.find(e => e.name === "overflow");
73
+ expect(overflowEntry).toBeDefined();
74
+ expect(overflowEntry.value).toBe("hidden");
75
+ });
76
+ it("collapses overflow when axes differ", () => {
77
+ const values = {
78
+ "overflow-x": "hidden",
79
+ "overflow-y": "scroll"
80
+ };
81
+ const entries = _processGroupEntries(LAYOUT_PROPERTIES, values, alwaysNonDefault);
82
+ const overflowEntry = entries.find(e => e.name === "overflow");
83
+ expect(overflowEntry).toBeDefined();
84
+ expect(overflowEntry.value).toBe("hidden scroll");
85
+ });
86
+ it("collapses flex shorthand when 2+ are non-default", () => {
87
+ const values = {
88
+ "flex-grow": "1",
89
+ "flex-shrink": "0",
90
+ "flex-basis": "auto"
91
+ };
92
+ const entries = _processGroupEntries(LAYOUT_PROPERTIES, values, alwaysNonDefault);
93
+ const flexEntry = entries.find(e => e.name === "flex");
94
+ expect(flexEntry).toBeDefined();
95
+ expect(flexEntry.value).toBe("1 0 auto");
96
+ });
97
+ it("collapses uniform border into shorthand", () => {
98
+ const values = {
99
+ "border-top-width": "1px",
100
+ "border-right-width": "1px",
101
+ "border-bottom-width": "1px",
102
+ "border-left-width": "1px",
103
+ "border-top-style": "solid",
104
+ "border-right-style": "solid",
105
+ "border-bottom-style": "solid",
106
+ "border-left-style": "solid",
107
+ "border-top-color": "rgb(0, 0, 0)",
108
+ "border-right-color": "rgb(0, 0, 0)",
109
+ "border-bottom-color": "rgb(0, 0, 0)",
110
+ "border-left-color": "rgb(0, 0, 0)"
111
+ };
112
+ const entries = _processGroupEntries(VISUAL_PROPERTIES, values, alwaysNonDefault);
113
+ const borderEntry = entries.find(e => e.name === "border");
114
+ expect(borderEntry).toBeDefined();
115
+ expect(borderEntry.value).toBe("1px solid rgb(0, 0, 0)");
116
+ });
117
+ it("uses per-side border shorthands for non-uniform borders", () => {
118
+ const values = {
119
+ "border-top-width": "1px",
120
+ "border-right-width": "0px",
121
+ "border-bottom-width": "2px",
122
+ "border-left-width": "0px",
123
+ "border-top-style": "solid",
124
+ "border-right-style": "none",
125
+ "border-bottom-style": "solid",
126
+ "border-left-style": "none",
127
+ "border-top-color": "rgb(0, 0, 0)",
128
+ "border-right-color": "rgb(0, 0, 0)",
129
+ "border-bottom-color": "rgb(255, 0, 0)",
130
+ "border-left-color": "rgb(0, 0, 0)"
131
+ };
132
+ const entries = _processGroupEntries(VISUAL_PROPERTIES, values, alwaysNonDefault);
133
+ expect(entries.find(e => e.name === "border")).toBeUndefined();
134
+ expect(entries.find(e => e.name === "border-top")).toBeDefined();
135
+ expect(entries.find(e => e.name === "border-bottom")).toBeDefined();
136
+ // Right and left have style: none, so they're skipped
137
+ expect(entries.find(e => e.name === "border-right")).toBeUndefined();
138
+ expect(entries.find(e => e.name === "border-left")).toBeUndefined();
139
+ });
140
+ it("collapses border-radius into shorthand", () => {
141
+ const values = {
142
+ "border-top-left-radius": "6px",
143
+ "border-top-right-radius": "6px",
144
+ "border-bottom-right-radius": "6px",
145
+ "border-bottom-left-radius": "6px"
146
+ };
147
+ const entries = _processGroupEntries(VISUAL_PROPERTIES, values, alwaysNonDefault);
148
+ const radiusEntry = entries.find(e => e.name === "border-radius");
149
+ expect(radiusEntry).toBeDefined();
150
+ expect(radiusEntry.value).toBe("6px");
151
+ });
152
+ it("collapses outline into shorthand", () => {
153
+ const values = {
154
+ "outline-width": "2px",
155
+ "outline-style": "solid",
156
+ "outline-color": "rgb(59, 130, 246)"
157
+ };
158
+ const entries = _processGroupEntries(VISUAL_PROPERTIES, values, alwaysNonDefault);
159
+ const outlineEntry = entries.find(e => e.name === "outline");
160
+ expect(outlineEntry).toBeDefined();
161
+ expect(outlineEntry.value).toBe("2px solid rgb(59, 130, 246)");
162
+ });
163
+ it("skips outline when style is none", () => {
164
+ const values = {
165
+ "outline-width": "0px",
166
+ "outline-style": "none",
167
+ "outline-color": "rgb(0, 0, 0)"
168
+ };
169
+ const entries = _processGroupEntries(VISUAL_PROPERTIES, values, alwaysNonDefault);
170
+ expect(entries.find(e => e.name === "outline")).toBeUndefined();
171
+ // Individual longhands should also be consumed
172
+ expect(entries.find(e => e.name === "outline-width")).toBeUndefined();
173
+ });
174
+ it("outputs individual properties that are not part of shorthands", () => {
175
+ const values = {
176
+ display: "flex",
177
+ position: "relative"
178
+ };
179
+ const entries = _processGroupEntries(LAYOUT_PROPERTIES, values, alwaysNonDefault);
180
+ expect(entries.find(e => e.name === "display")).toBeDefined();
181
+ expect(entries.find(e => e.name === "position")).toBeDefined();
182
+ });
183
+ it("skips properties where isNonDefault returns false", () => {
184
+ const neverNonDefault = () => false;
185
+ const values = {
186
+ display: "block",
187
+ position: "static"
188
+ };
189
+ const entries = _processGroupEntries(LAYOUT_PROPERTIES, values, neverNonDefault);
190
+ expect(entries).toEqual([]);
191
+ });
192
+ it("skips properties with empty values", () => {
193
+ const values = {
194
+ display: "",
195
+ position: ""
196
+ };
197
+ const entries = _processGroupEntries(LAYOUT_PROPERTIES, values, alwaysNonDefault);
198
+ expect(entries).toEqual([]);
199
+ });
200
+ });
201
+ describe("formatDiff", () => {
202
+ it("shows changed properties with arrow notation", () => {
203
+ const prev = {
204
+ properties: {
205
+ opacity: "1",
206
+ "background-color": "transparent"
207
+ },
208
+ boundingRect: {
209
+ x: 0,
210
+ y: 0,
211
+ width: 100,
212
+ height: 40,
213
+ top: 0,
214
+ right: 100,
215
+ bottom: 40,
216
+ left: 0
217
+ }
218
+ };
219
+ const curr = {
220
+ properties: {
221
+ opacity: "0.8",
222
+ "background-color": "rgb(59, 130, 246)"
223
+ },
224
+ boundingRect: {
225
+ x: 0,
226
+ y: 0,
227
+ width: 100,
228
+ height: 40,
229
+ top: 0,
230
+ right: 100,
231
+ bottom: 40,
232
+ left: 0
233
+ }
234
+ };
235
+ const result = _formatDiff(prev, curr, "Button");
236
+ expect(result).toContain("[ComputedStyles \u0394] Button");
237
+ expect(result).toContain("~ opacity: 1 \u2192 0.8");
238
+ expect(result).toContain("~ background-color: transparent \u2192 rgb(59, 130, 246)");
239
+ });
240
+ it("shows added properties with + prefix", () => {
241
+ const prev = {
242
+ properties: {
243
+ opacity: "1"
244
+ },
245
+ boundingRect: {
246
+ x: 0,
247
+ y: 0,
248
+ width: 100,
249
+ height: 40,
250
+ top: 0,
251
+ right: 100,
252
+ bottom: 40,
253
+ left: 0
254
+ }
255
+ };
256
+ const curr = {
257
+ properties: {
258
+ opacity: "1",
259
+ "box-shadow": "0 2px 4px rgba(0,0,0,0.1)"
260
+ },
261
+ boundingRect: {
262
+ x: 0,
263
+ y: 0,
264
+ width: 100,
265
+ height: 40,
266
+ top: 0,
267
+ right: 100,
268
+ bottom: 40,
269
+ left: 0
270
+ }
271
+ };
272
+ const result = _formatDiff(prev, curr, undefined);
273
+ expect(result).toContain("+ box-shadow: 0 2px 4px rgba(0,0,0,0.1)");
274
+ });
275
+ it("shows removed properties with - prefix", () => {
276
+ const prev = {
277
+ properties: {
278
+ transform: "scale(1.05)"
279
+ },
280
+ boundingRect: {
281
+ x: 0,
282
+ y: 0,
283
+ width: 100,
284
+ height: 40,
285
+ top: 0,
286
+ right: 100,
287
+ bottom: 40,
288
+ left: 0
289
+ }
290
+ };
291
+ const curr = {
292
+ properties: {},
293
+ boundingRect: {
294
+ x: 0,
295
+ y: 0,
296
+ width: 100,
297
+ height: 40,
298
+ top: 0,
299
+ right: 100,
300
+ bottom: 40,
301
+ left: 0
302
+ }
303
+ };
304
+ const result = _formatDiff(prev, curr, undefined);
305
+ expect(result).toContain("- transform: scale(1.05)");
306
+ });
307
+ it("shows bounding rect changes", () => {
308
+ const prev = {
309
+ properties: {},
310
+ boundingRect: {
311
+ x: 100,
312
+ y: 200,
313
+ width: 120,
314
+ height: 40,
315
+ top: 200,
316
+ right: 220,
317
+ bottom: 240,
318
+ left: 100
319
+ }
320
+ };
321
+ const curr = {
322
+ properties: {},
323
+ boundingRect: {
324
+ x: 150,
325
+ y: 200,
326
+ width: 120,
327
+ height: 50,
328
+ top: 200,
329
+ right: 270,
330
+ bottom: 250,
331
+ left: 150
332
+ }
333
+ };
334
+ const result = _formatDiff(prev, curr, undefined);
335
+ expect(result).toContain("BoundingRect");
336
+ expect(result).toContain("~ x: 100 \u2192 150");
337
+ expect(result).toContain("~ height: 40 \u2192 50");
338
+ });
339
+ it("shows 'No changes detected' when nothing changed", () => {
340
+ const snapshot = {
341
+ properties: {
342
+ opacity: "1"
343
+ },
344
+ boundingRect: {
345
+ x: 0,
346
+ y: 0,
347
+ width: 100,
348
+ height: 40,
349
+ top: 0,
350
+ right: 100,
351
+ bottom: 40,
352
+ left: 0
353
+ }
354
+ };
355
+ const result = _formatDiff(snapshot, snapshot, undefined);
356
+ expect(result).toContain("No changes detected");
357
+ });
358
+ });
359
+ describe("property group completeness", () => {
360
+ it("has no duplicates within property groups", () => {
361
+ const allProps = [...LAYOUT_PROPERTIES, ...VISUAL_PROPERTIES, ...TYPOGRAPHY_PROPERTIES, ...INTERACTION_PROPERTIES, ...SVG_PROPERTIES];
362
+ const seen = new Set();
363
+ const dupes = [];
364
+ for (const prop of allProps) {
365
+ if (seen.has(prop)) dupes.push(prop);
366
+ seen.add(prop);
367
+ }
368
+ expect(dupes).toEqual([]);
369
+ });
370
+ it("includes key layout properties", () => {
371
+ expect(LAYOUT_PROPERTIES).toContain("display");
372
+ expect(LAYOUT_PROPERTIES).toContain("position");
373
+ expect(LAYOUT_PROPERTIES).toContain("width");
374
+ expect(LAYOUT_PROPERTIES).toContain("height");
375
+ expect(LAYOUT_PROPERTIES).toContain("flex-direction");
376
+ expect(LAYOUT_PROPERTIES).toContain("grid-template-columns");
377
+ });
378
+ it("includes key visual properties", () => {
379
+ expect(VISUAL_PROPERTIES).toContain("background-color");
380
+ expect(VISUAL_PROPERTIES).toContain("color");
381
+ expect(VISUAL_PROPERTIES).toContain("opacity");
382
+ expect(VISUAL_PROPERTIES).toContain("box-shadow");
383
+ expect(VISUAL_PROPERTIES).toContain("transform");
384
+ });
385
+ it("includes key typography properties", () => {
386
+ expect(TYPOGRAPHY_PROPERTIES).toContain("font-family");
387
+ expect(TYPOGRAPHY_PROPERTIES).toContain("font-size");
388
+ expect(TYPOGRAPHY_PROPERTIES).toContain("font-weight");
389
+ expect(TYPOGRAPHY_PROPERTIES).toContain("line-height");
390
+ });
391
+ it("includes SVG-specific properties", () => {
392
+ expect(SVG_PROPERTIES).toContain("fill");
393
+ expect(SVG_PROPERTIES).toContain("stroke");
394
+ expect(SVG_PROPERTIES).toContain("stroke-width");
395
+ });
396
+ });
397
+ describe("forceFull option", () => {
398
+ it("returns full styles on repeat call within diff window when forceFull is true", () => {
399
+ const el = document.createElement("div");
400
+ document.body.appendChild(el);
401
+ try {
402
+ // First call primes the snapshot
403
+ const first = extractComputedStyles(el, "Button");
404
+ expect(first.formatted).toContain("[ComputedStyles]");
405
+ expect(first.formatted).not.toContain("\u0394"); // no diff marker
406
+
407
+ // Second call without forceFull enters diff mode
408
+ const second = extractComputedStyles(el, "Button");
409
+ expect(second.formatted).toContain("\u0394");
410
+
411
+ // Third call with forceFull bypasses diff mode
412
+ const third = extractComputedStyles(el, "Button", {
413
+ forceFull: true
414
+ });
415
+ expect(third.formatted).toContain("[ComputedStyles]");
416
+ expect(third.formatted).not.toContain("\u0394");
417
+ expect(third.formatted).toContain("BoundingRect");
418
+ } finally {
419
+ el.remove();
420
+ }
421
+ });
422
+ it("still updates lastSnapshot so subsequent calls can diff against the forced-full snapshot", () => {
423
+ const el = document.createElement("div");
424
+ document.body.appendChild(el);
425
+ try {
426
+ extractComputedStyles(el);
427
+ extractComputedStyles(el, undefined, {
428
+ forceFull: true
429
+ });
430
+ // Next call should be a diff against the forced-full snapshot,
431
+ // not the original one.
432
+ const after = extractComputedStyles(el);
433
+ expect(after.formatted).toContain("\u0394");
434
+ } finally {
435
+ el.remove();
436
+ }
437
+ });
438
+ });
439
+ describe("includeDefaults option", () => {
440
+ it("includes curated properties even when they match the tag defaults", () => {
441
+ const heading = document.createElement("h1");
442
+ document.body.appendChild(heading);
443
+ const makeStyle = values => ({
444
+ getPropertyValue: prop => values[prop] ?? ""
445
+ });
446
+ const headingStyles = {
447
+ display: "block",
448
+ "font-weight": "700",
449
+ "font-size": "51.2px",
450
+ "line-height": "56.32px"
451
+ };
452
+ const getComputedStyleSpy = vi.spyOn(window, "getComputedStyle").mockImplementation(node => {
453
+ if (node === heading) {
454
+ return makeStyle(headingStyles);
455
+ }
456
+ if (node.tagName === "H1") {
457
+ return makeStyle(headingStyles);
458
+ }
459
+ return makeStyle({});
460
+ });
461
+ try {
462
+ const result = extractComputedStyles(heading, "App heading", {
463
+ forceFull: true,
464
+ includeDefaults: true
465
+ });
466
+ expect(result.formatted).toContain("Layout");
467
+ expect(result.formatted).toContain("display: block");
468
+ expect(result.formatted).toContain("Typography");
469
+ expect(result.formatted).toContain("font-weight: 700");
470
+ expect(result.formatted).toContain("font-size: 51.2px");
471
+ } finally {
472
+ getComputedStyleSpy.mockRestore();
473
+ heading.remove();
474
+ }
475
+ });
476
+ });
477
+ describe("default filtering", () => {
478
+ it("keeps inherited typography when defaults are probed in an isolated shadow root", () => {
479
+ const link = document.createElement("a");
480
+ document.body.appendChild(link);
481
+ const makeStyle = values => ({
482
+ getPropertyValue: prop => values[prop] ?? ""
483
+ });
484
+ const inheritedTypography = {
485
+ "font-family": '"Fira Code", monospace',
486
+ "font-size": "21px",
487
+ "font-style": "italic",
488
+ "font-weight": "700"
489
+ };
490
+ const defaultTypography = {
491
+ "font-family": "Times",
492
+ "font-size": "16px",
493
+ "font-style": "normal",
494
+ "font-weight": "400"
495
+ };
496
+ const getComputedStyleSpy = vi.spyOn(window, "getComputedStyle").mockImplementation(node => {
497
+ if (node === link) {
498
+ return makeStyle(inheritedTypography);
499
+ }
500
+ if (node.tagName === "A") {
501
+ return makeStyle(node.getRootNode() instanceof ShadowRoot ? defaultTypography : inheritedTypography);
502
+ }
503
+ return makeStyle({});
504
+ });
505
+ try {
506
+ const result = extractComputedStyles(link, "NavItem", {
507
+ forceFull: true
508
+ });
509
+ expect(result.formatted).toContain("Typography");
510
+ expect(result.formatted).toMatch(/font-family: .*monospace/i);
511
+ expect(result.formatted).toContain("font-size: 21px");
512
+ expect(result.formatted).toContain("font-style: italic");
513
+ expect(result.formatted).toContain("font-weight: 700");
514
+ } finally {
515
+ getComputedStyleSpy.mockRestore();
516
+ link.remove();
517
+ }
518
+ });
519
+ });
520
+ describe("getElementLabel", () => {
521
+ it("returns empty string for empty ancestry", () => {
522
+ expect(getElementLabel([])).toBe("");
523
+ });
524
+ it("returns component name with file location", () => {
525
+ const label = getElementLabel([{
526
+ elementName: "button",
527
+ componentName: "SubmitButton",
528
+ filePath: "src/components/SubmitButton.tsx",
529
+ line: 12
530
+ }]);
531
+ expect(label).toBe("SubmitButton at src/components/SubmitButton.tsx:12");
532
+ });
533
+ it("falls back to element name when no component name is set", () => {
534
+ const label = getElementLabel([{
535
+ elementName: "div",
536
+ filePath: "src/App.tsx",
537
+ line: 5
538
+ }]);
539
+ expect(label).toBe("div at src/App.tsx:5");
540
+ });
541
+ it("omits location when filePath is missing", () => {
542
+ const label = getElementLabel([{
543
+ elementName: "span",
544
+ componentName: "Label"
545
+ }]);
546
+ expect(label).toBe("Label");
547
+ });
548
+ });
549
+ });
@@ -11,12 +11,20 @@ export interface AncestryItem {
11
11
  filePath?: string;
12
12
  line?: number;
13
13
  id?: string;
14
+ classes?: string[];
14
15
  nthChild?: number;
15
16
  /** All owner components from outermost (Sidebar) to innermost (GlassPanel) */
16
17
  ownerComponents?: OwnerComponentInfo[];
17
18
  /** Server-side components (Phoenix LiveView, Rails, Next.js RSC, etc.) */
18
19
  serverComponents?: ServerComponentInfo[];
19
20
  }
21
+ /**
22
+ * Build a short label for the innermost (clicked) element in an ancestry array.
23
+ * Used as the header for computed-style extraction output.
24
+ *
25
+ * Example: `"Button at src/components/Button.tsx:23"` or `"div"` as a fallback.
26
+ */
27
+ export declare function getElementLabel(ancestry: AncestryItem[]): string;
20
28
  export declare function collectAncestry(node: TreeNode): AncestryItem[];
21
29
  /**
22
30
  * Truncate ancestry to keep only the local context.
@@ -29,6 +29,20 @@ function treeNodeComponentToOwnerInfo(comp) {
29
29
  line: comp.callLink?.lineNumber
30
30
  };
31
31
  }
32
+
33
+ /**
34
+ * Build a short label for the innermost (clicked) element in an ancestry array.
35
+ * Used as the header for computed-style extraction output.
36
+ *
37
+ * Example: `"Button at src/components/Button.tsx:23"` or `"div"` as a fallback.
38
+ */
39
+ export function getElementLabel(ancestry) {
40
+ if (ancestry.length === 0) return "";
41
+ const item = ancestry[0];
42
+ const name = item.componentName || item.elementName;
43
+ const location = item.filePath ? ` at ${item.filePath}:${item.line}` : "";
44
+ return `${name}${location}`;
45
+ }
32
46
  export function collectAncestry(node) {
33
47
  const items = [];
34
48
  let current = node;
@@ -50,6 +64,9 @@ export function collectAncestry(node) {
50
64
  if (element.id) {
51
65
  item.id = element.id;
52
66
  }
67
+ if (element.classList.length > 0) {
68
+ item.classes = Array.from(element.classList);
69
+ }
53
70
  const nthChild = getNthChildIfAmbiguous(element);
54
71
  if (nthChild !== undefined) {
55
72
  item.nthChild = nthChild;
@@ -205,7 +222,7 @@ export function formatAncestryChain(items) {
205
222
  }
206
223
  }
207
224
 
208
- // Build element selector: displayName:nth-child(n)#id
225
+ // Build element selector: displayName:nth-child(n)#id.class1.class2
209
226
  let selector = displayName;
210
227
  if (item.nthChild !== undefined) {
211
228
  selector += `:nth-child(${item.nthChild})`;
@@ -213,6 +230,9 @@ export function formatAncestryChain(items) {
213
230
  if (item.id) {
214
231
  selector += `#${item.id}`;
215
232
  }
233
+ if (item.classes && item.classes.length > 0) {
234
+ selector += "." + item.classes.join(".");
235
+ }
216
236
  let description = selector;
217
237
 
218
238
  // Build component description parts
@@ -42,6 +42,24 @@ describe("formatAncestryChain", () => {
42
42
  expect(result).toBe(`List
43
43
  └─ ListItem:nth-child(3)`);
44
44
  });
45
+ it("includes classes after the id", () => {
46
+ const items = [{
47
+ elementName: "button",
48
+ componentName: "Button",
49
+ id: "save",
50
+ classes: ["btn", "btn-primary"]
51
+ }];
52
+ const result = formatAncestryChain(items);
53
+ expect(result).toBe("Button#save.btn.btn-primary");
54
+ });
55
+ it("includes classes without id", () => {
56
+ const items = [{
57
+ elementName: "div",
58
+ classes: ["card", "card--featured"]
59
+ }];
60
+ const result = formatAncestryChain(items);
61
+ expect(result).toBe("div.card.card--featured");
62
+ });
45
63
  it("includes both nth-child and ID at component boundary", () => {
46
64
  const items = [{
47
65
  elementName: "li",
@@ -0,0 +1 @@
1
+ export {};