@treelocator/runtime 0.5.2 → 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 (163) 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/functions/cssRuleInspector.d.ts +83 -0
  41. package/dist/functions/cssRuleInspector.js +608 -0
  42. package/dist/functions/cssRuleInspector.test.d.ts +1 -0
  43. package/dist/functions/cssRuleInspector.test.js +439 -0
  44. package/dist/functions/deduplicateLabels.test.d.ts +1 -0
  45. package/dist/functions/deduplicateLabels.test.js +178 -0
  46. package/dist/functions/enrichAncestrySourceMaps.js +0 -1
  47. package/dist/functions/extractComputedStyles.d.ts +51 -0
  48. package/dist/functions/extractComputedStyles.js +447 -0
  49. package/dist/functions/extractComputedStyles.test.d.ts +1 -0
  50. package/dist/functions/extractComputedStyles.test.js +549 -0
  51. package/dist/functions/formatAncestryChain.d.ts +8 -0
  52. package/dist/functions/formatAncestryChain.js +21 -1
  53. package/dist/functions/formatAncestryChain.test.js +18 -0
  54. package/dist/functions/getUsableName.test.d.ts +1 -0
  55. package/dist/functions/getUsableName.test.js +219 -0
  56. package/dist/functions/isCombinationModifiersPressed.test.d.ts +1 -0
  57. package/dist/functions/isCombinationModifiersPressed.test.js +192 -0
  58. package/dist/functions/mergeRects.test.js +210 -1
  59. package/dist/functions/namedSnapshots.d.ts +52 -0
  60. package/dist/functions/namedSnapshots.js +161 -0
  61. package/dist/functions/namedSnapshots.test.d.ts +1 -0
  62. package/dist/functions/namedSnapshots.test.js +85 -0
  63. package/dist/functions/normalizeFilePath.test.d.ts +1 -0
  64. package/dist/functions/normalizeFilePath.test.js +66 -0
  65. package/dist/functions/parseDataId.test.d.ts +1 -0
  66. package/dist/functions/parseDataId.test.js +101 -0
  67. package/dist/hooks/getStorage.d.ts +3 -0
  68. package/dist/hooks/getStorage.js +17 -0
  69. package/dist/hooks/useEventListeners.d.ts +15 -0
  70. package/dist/hooks/useEventListeners.js +56 -0
  71. package/dist/hooks/useLocatorStorage.d.ts +18 -0
  72. package/dist/hooks/useLocatorStorage.js +41 -0
  73. package/dist/hooks/useLocatorStorage.test.d.ts +1 -0
  74. package/dist/hooks/useLocatorStorage.test.js +124 -0
  75. package/dist/hooks/useRecordingState.d.ts +43 -0
  76. package/dist/hooks/useRecordingState.js +387 -0
  77. package/dist/hooks/useSettings.d.ts +13 -0
  78. package/dist/hooks/useSettings.js +66 -0
  79. package/dist/index.d.ts +5 -2
  80. package/dist/index.js +4 -2
  81. package/dist/initRuntime.d.ts +3 -1
  82. package/dist/initRuntime.js +4 -1
  83. package/dist/mcpBridge.d.ts +61 -0
  84. package/dist/mcpBridge.js +534 -0
  85. package/dist/mcpBridge.test.d.ts +1 -0
  86. package/dist/mcpBridge.test.js +248 -0
  87. package/dist/output.css +20 -0
  88. package/dist/visualDiff/diff.d.ts +9 -0
  89. package/dist/visualDiff/diff.js +209 -0
  90. package/dist/visualDiff/diff.test.d.ts +1 -0
  91. package/dist/visualDiff/diff.test.js +253 -0
  92. package/dist/visualDiff/settle.d.ts +3 -0
  93. package/dist/visualDiff/settle.js +50 -0
  94. package/dist/visualDiff/settle.test.d.ts +1 -0
  95. package/dist/visualDiff/settle.test.js +65 -0
  96. package/dist/visualDiff/snapshot.d.ts +4 -0
  97. package/dist/visualDiff/snapshot.js +84 -0
  98. package/dist/visualDiff/snapshot.test.d.ts +1 -0
  99. package/dist/visualDiff/snapshot.test.js +245 -0
  100. package/dist/visualDiff/types.d.ts +37 -0
  101. package/dist/visualDiff/types.js +1 -0
  102. package/package.json +2 -2
  103. package/scripts/wrapCSS.js +1 -1
  104. package/scripts/wrapImage.js +1 -1
  105. package/src/_generated_styles.ts +21 -1
  106. package/src/_generated_tree_icon.ts +1 -1
  107. package/src/adapters/HtmlElementTreeNode.ts +10 -7
  108. package/src/adapters/createTreeNode.ts +12 -51
  109. package/src/adapters/detectFramework.test.ts +73 -0
  110. package/src/adapters/detectFramework.ts +28 -0
  111. package/src/adapters/jsx/jsxAdapter.test.ts +240 -0
  112. package/src/adapters/jsx/jsxAdapter.ts +53 -106
  113. package/src/adapters/nextjs/parseNextjsDataAttributes.test.ts +212 -0
  114. package/src/adapters/nextjs/parseNextjsDataAttributes.ts +1 -1
  115. package/src/adapters/react/findDebugSource.ts +5 -6
  116. package/src/adapters/react/findFiberByHtmlElement.ts +3 -3
  117. package/src/adapters/react/getAllParentsElementsAndRootComponent.ts +3 -0
  118. package/src/adapters/react/reactAdapter.ts +1 -2
  119. package/src/adapters/resolveAdapter.ts +4 -14
  120. package/src/adapters/svelte/svelteAdapter.test.ts +334 -0
  121. package/src/adapters/vue/vueAdapter.test.ts +259 -0
  122. package/src/browserApi.test.ts +329 -0
  123. package/src/browserApi.ts +351 -4
  124. package/src/components/RecordingPillButton.tsx +301 -0
  125. package/src/components/RecordingResults.tsx +114 -13
  126. package/src/components/Runtime.tsx +176 -621
  127. package/src/components/SettingsPanel.tsx +339 -0
  128. package/src/consoleCapture.ts +113 -0
  129. package/src/functions/cssRuleInspector.test.ts +517 -0
  130. package/src/functions/cssRuleInspector.ts +708 -0
  131. package/src/functions/deduplicateLabels.test.ts +115 -0
  132. package/src/functions/enrichAncestrySourceMaps.ts +6 -3
  133. package/src/functions/extractComputedStyles.test.ts +681 -0
  134. package/src/functions/extractComputedStyles.ts +768 -0
  135. package/src/functions/formatAncestryChain.test.ts +23 -1
  136. package/src/functions/formatAncestryChain.ts +22 -1
  137. package/src/functions/getUsableName.test.ts +242 -0
  138. package/src/functions/isCombinationModifiersPressed.test.ts +156 -0
  139. package/src/functions/mergeRects.test.ts +111 -1
  140. package/src/functions/namedSnapshots.test.ts +106 -0
  141. package/src/functions/namedSnapshots.ts +232 -0
  142. package/src/functions/normalizeFilePath.test.ts +80 -0
  143. package/src/functions/parseDataId.test.ts +125 -0
  144. package/src/hooks/getStorage.ts +26 -0
  145. package/src/hooks/useEventListeners.ts +97 -0
  146. package/src/hooks/useLocatorStorage.test.ts +127 -0
  147. package/src/hooks/useLocatorStorage.ts +60 -0
  148. package/src/hooks/useRecordingState.ts +516 -0
  149. package/src/hooks/useSettings.ts +83 -0
  150. package/src/index.ts +10 -5
  151. package/src/initRuntime.ts +5 -0
  152. package/src/mcpBridge.test.ts +260 -0
  153. package/src/mcpBridge.ts +677 -0
  154. package/src/visualDiff/diff.test.ts +167 -0
  155. package/src/visualDiff/diff.ts +242 -0
  156. package/src/visualDiff/settle.test.ts +77 -0
  157. package/src/visualDiff/settle.ts +62 -0
  158. package/src/visualDiff/snapshot.test.ts +200 -0
  159. package/src/visualDiff/snapshot.ts +119 -0
  160. package/src/visualDiff/types.ts +40 -0
  161. package/tsconfig.json +3 -1
  162. package/vitest.config.ts +18 -0
  163. package/jest.config.ts +0 -195
@@ -1,7 +1,11 @@
1
1
  import { AdapterId } from "./consts";
2
2
  import { AncestryItem } from "./functions/formatAncestryChain";
3
+ import { CSSInspectionResult } from "./functions/cssRuleInspector";
4
+ import { ComputedStylesResult, ExtractOptions } from "./functions/extractComputedStyles";
5
+ import { TakeSnapshotResult, SnapshotDiffResult } from "./functions/namedSnapshots";
3
6
  import type { DejitterFinding, DejitterSummary } from "./dejitter/recorder";
4
7
  import type { InteractionEvent } from "./components/RecordingResults";
8
+ import type { DeltaReport, ElementSnapshot } from "./visualDiff/types";
5
9
  export interface LocatorJSAPI {
6
10
  /**
7
11
  * Get formatted ancestry path for an element.
@@ -84,6 +88,36 @@ export interface LocatorJSAPI {
84
88
  path: string;
85
89
  ancestry: AncestryItem[];
86
90
  } | null>;
91
+ /**
92
+ * Get computed styles for an element, formatted for AI consumption.
93
+ * Extracts layout, visual, typography, and interaction styles filtered against browser defaults.
94
+ * Clicking the same element twice within 30s returns a diff of changed properties.
95
+ *
96
+ * @param elementOrSelector - HTMLElement or CSS selector string
97
+ * @param options - Optional flags like { includeDefaults: true } for a fuller dump
98
+ * @returns Object with formatted string and raw snapshot, or null if element not found
99
+ *
100
+ * @example
101
+ * // Get formatted computed styles
102
+ * const result = window.__treelocator__.getStyles('button.submit');
103
+ * console.log(result.formatted);
104
+ * // [ComputedStyles] Button at src/Button.tsx:23
105
+ * // ─────────────────────────────────────────
106
+ * // Layout
107
+ * // display: flex
108
+ * // padding: 8px 16px
109
+ * // ...
110
+ *
111
+ * @example
112
+ * // In Playwright
113
+ * const styles = await page.evaluate(() => {
114
+ * return window.__treelocator__.getStyles('.my-element', {
115
+ * includeDefaults: true,
116
+ * });
117
+ * });
118
+ * console.log(styles?.formatted);
119
+ */
120
+ getStyles(elementOrSelector: HTMLElement | string, options?: ExtractOptions): ComputedStylesResult | null;
87
121
  /**
88
122
  * Display help information about the LocatorJS API.
89
123
  * Shows usage examples and method descriptions for browser automation tools.
@@ -100,6 +134,89 @@ export interface LocatorJSAPI {
100
134
  * console.log(help);
101
135
  */
102
136
  help(): string;
137
+ /**
138
+ * Inspect all CSS rules matching an element, grouped by property.
139
+ * Shows which rule wins for each property with specificity, source, and !important info.
140
+ * Returns structured data for programmatic use.
141
+ *
142
+ * @param elementOrSelector - HTMLElement or CSS selector string
143
+ * @returns Structured CSS inspection result, or null if element not found
144
+ *
145
+ * @example
146
+ * // Get structured CSS data
147
+ * const result = window.__treelocator__.getCSSRules('button.primary');
148
+ * result.properties.forEach(p => {
149
+ * console.log(`${p.property}: ${p.value}`);
150
+ * p.rules.forEach(r => console.log(` ${r.winning ? '✓' : '✗'} ${r.selector}`));
151
+ * });
152
+ *
153
+ * @example
154
+ * // In Playwright - debug why a style isn't applying
155
+ * const css = await page.evaluate(() =>
156
+ * window.__treelocator__.getCSSRules('.my-button')
157
+ * );
158
+ * const colorRules = css?.properties.find(p => p.property === 'color');
159
+ * console.log(colorRules);
160
+ */
161
+ getCSSRules(elementOrSelector: HTMLElement | string): CSSInspectionResult | null;
162
+ /**
163
+ * Get a formatted human-readable report of all CSS rules matching an element.
164
+ * Shows winning/losing rules per property with specificity and source info.
165
+ * Ideal for pasting into AI chat or logging.
166
+ *
167
+ * @param elementOrSelector - HTMLElement or CSS selector string
168
+ * @param options - Optional filter: { properties?: string[] } to limit output to specific properties
169
+ * @returns Formatted string report, or null if element not found
170
+ *
171
+ * @example
172
+ * // Get full CSS report
173
+ * console.log(window.__treelocator__.getCSSReport('button.primary'));
174
+ * // Output:
175
+ * // CSS Rules for button.primary
176
+ * // ════════════════════════════
177
+ * //
178
+ * // color: #333
179
+ * // ✓ .button.primary (0,2,0) — components.css
180
+ * // ✗ .button (0,1,0) — base.css
181
+ * // ✗ button (0,0,1) — reset.css
182
+ *
183
+ * @example
184
+ * // Filter to specific properties
185
+ * console.log(window.__treelocator__.getCSSReport('.card', { properties: ['color', 'background'] }));
186
+ *
187
+ * @example
188
+ * // In Playwright
189
+ * const report = await page.evaluate(() =>
190
+ * window.__treelocator__.getCSSReport('.error-message')
191
+ * );
192
+ * console.log(report);
193
+ */
194
+ getCSSReport(elementOrSelector: HTMLElement | string, options?: {
195
+ properties?: string[];
196
+ }): string | null;
197
+ /**
198
+ * Capture a baseline snapshot of an element's computed styles and persist
199
+ * it in localStorage under `snapshotId`. The baseline survives page reloads
200
+ * and is never mutated until you call `takeSnapshot` again with the same id.
201
+ *
202
+ * @param selector - CSS selector for the element to snapshot
203
+ * @param snapshotId - caller-chosen id used to retrieve the diff later
204
+ * @param options - optional `{ index, label }` (index picks among matches)
205
+ */
206
+ takeSnapshot(selector: string, snapshotId: string, options?: {
207
+ index?: number;
208
+ label?: string;
209
+ }): TakeSnapshotResult;
210
+ /**
211
+ * Diff the current computed styles of the element against the baseline
212
+ * stored under `snapshotId`. Does not overwrite the baseline — safe to call
213
+ * repeatedly while iterating on a change.
214
+ */
215
+ getSnapshotDiff(snapshotId: string): SnapshotDiffResult;
216
+ /**
217
+ * Remove a stored baseline snapshot.
218
+ */
219
+ clearSnapshot(snapshotId: string): void;
103
220
  /**
104
221
  * Replay the last recorded interaction sequence.
105
222
  * Dispatches the recorded clicks at the original positions and timing.
@@ -142,7 +259,38 @@ export interface LocatorJSAPI {
142
259
  summary: DejitterSummary | null;
143
260
  data: any;
144
261
  interactions: InteractionEvent[];
262
+ visualDiff: DeltaReport | null;
145
263
  } | null>;
264
+ /**
265
+ * Visual diff engine — snapshot page state before/after an action and return
266
+ * a compact delta report.
267
+ *
268
+ * @example
269
+ * // In browser console or Playwright
270
+ * const report = await window.__treelocator__.diff.captureDiff(() => {
271
+ * document.querySelector('button.submit')?.click();
272
+ * });
273
+ * console.log(report.text);
274
+ */
275
+ diff: {
276
+ /**
277
+ * Capture a snapshot of all visible viewport elements right now.
278
+ * Pure — no side effects on the page.
279
+ */
280
+ snapshot(): ElementSnapshot[];
281
+ /**
282
+ * Compute the delta between two snapshots.
283
+ */
284
+ computeDiff(before: ElementSnapshot[], after: ElementSnapshot[]): DeltaReport;
285
+ /**
286
+ * Take a before-snapshot, run the action, wait for the page to settle
287
+ * (animations idle + mutations silent for 150ms), take an after-snapshot,
288
+ * and return the computed delta.
289
+ */
290
+ captureDiff(action: () => void | Promise<void>, opts?: {
291
+ settleTimeoutMs?: number;
292
+ }): Promise<DeltaReport>;
293
+ };
146
294
  }
147
295
  export declare function createBrowserAPI(adapterId?: AdapterId): LocatorJSAPI;
148
296
  export declare function installBrowserAPI(adapterIdParam?: AdapterId): void;
@@ -1,9 +1,22 @@
1
1
  import { createTreeNode } from "./adapters/createTreeNode";
2
- import { collectAncestry, formatAncestryChain } from "./functions/formatAncestryChain";
2
+ import { collectAncestry, formatAncestryChain, getElementLabel } from "./functions/formatAncestryChain";
3
3
  import { enrichAncestryWithSourceMaps } from "./functions/enrichAncestrySourceMaps";
4
+ import { inspectCSSRules, formatCSSInspection } from "./functions/cssRuleInspector";
5
+ import { extractComputedStyles } from "./functions/extractComputedStyles";
6
+ import { takeNamedSnapshot, getNamedSnapshotDiff, clearNamedSnapshot } from "./functions/namedSnapshots";
7
+ import { takeSnapshot } from "./visualDiff/snapshot";
8
+ import { computeDiff, formatReport } from "./visualDiff/diff";
9
+ import { waitForSettle } from "./visualDiff/settle";
4
10
  function resolveElement(elementOrSelector) {
5
11
  if (typeof elementOrSelector === "string") {
6
- const element = document.querySelector(elementOrSelector);
12
+ // querySelector throws DOMException for invalid selector strings
13
+ // (e.g. "!!!") — return null instead of crashing the API call.
14
+ let element = null;
15
+ try {
16
+ element = document.querySelector(elementOrSelector);
17
+ } catch {
18
+ return null;
19
+ }
7
20
  return element instanceof HTMLElement ? element : null;
8
21
  }
9
22
  return elementOrSelector;
@@ -62,13 +75,52 @@ METHODS:
62
75
  console.log(data.path) // formatted string
63
76
  console.log(data.ancestry) // structured array
64
77
 
65
- 4. replay()
78
+ 4. getStyles(elementOrSelector, options?)
79
+ Returns computed styles for an element, optimized for AI consumption.
80
+ Filters out browser defaults and groups by category (Layout, Visual, Typography).
81
+ Pass { includeDefaults: true } for a fuller dump closer to DevTools.
82
+ Calling twice on the same element within 30s returns a diff of changes.
83
+
84
+ Usage:
85
+ const result = window.__treelocator__.getStyles('button.submit')
86
+ console.log(result.formatted) // formatted styles string
87
+ console.log(result.snapshot) // raw property values + bounding rect
88
+ const full = window.__treelocator__.getStyles('h1', { includeDefaults: true })
89
+
90
+ 5. getCSSRules(elementOrSelector)
91
+ Returns structured CSS rule data for the element.
92
+ Shows all matching rules grouped by property with specificity and source.
93
+
94
+ Usage:
95
+ const result = window.__treelocator__.getCSSRules('button.primary')
96
+ result.properties.forEach(p => {
97
+ console.log(p.property + ': ' + p.value)
98
+ p.rules.forEach(r => console.log(' ' + (r.winning ? 'WIN' : ' ') + ' ' + r.selector))
99
+ })
100
+
101
+ 6. getCSSReport(elementOrSelector, options?)
102
+ Returns a formatted string showing all CSS rules and which wins per property.
103
+ Pass { properties: ['color', 'font-size'] } to filter to specific properties.
104
+
105
+ Usage:
106
+ console.log(window.__treelocator__.getCSSReport('button.primary'))
107
+ console.log(window.__treelocator__.getCSSReport('.card', { properties: ['color'] }))
108
+
109
+ Returns:
110
+ "CSS Rules for button.primary
111
+ ════════════════════════════
112
+ color: #333
113
+ ✓ .button.primary (0,2,0) — components.css
114
+ ✗ .button (0,1,0) — base.css
115
+ ✗ button (0,0,1) — reset.css"
116
+
117
+ 7. replay()
66
118
  Replays the last recorded interaction sequence as a macro.
67
119
 
68
120
  Usage:
69
121
  window.__treelocator__.replay()
70
122
 
71
- 5. replayWithRecord(elementOrSelector)
123
+ 8. replayWithRecord(elementOrSelector)
72
124
  Replays stored interactions while recording element changes.
73
125
  Returns dejitter analysis when replay completes.
74
126
 
@@ -77,7 +129,17 @@ METHODS:
77
129
  console.log(results.findings) // anomaly analysis
78
130
  console.log(results.path) // component ancestry
79
131
 
80
- 6. help()
132
+ 9. diff.snapshot() / diff.computeDiff(before, after) / diff.captureDiff(action)
133
+ Visual diff engine. Captures viewport element state and returns a compact
134
+ delta showing what appeared, disappeared, moved, or changed.
135
+
136
+ Usage:
137
+ const report = await window.__treelocator__.diff.captureDiff(() => {
138
+ document.querySelector('button.submit').click();
139
+ });
140
+ console.log(report.text);
141
+
142
+ 10. help()
81
143
  Displays this help message.
82
144
 
83
145
  PLAYWRIGHT EXAMPLES:
@@ -102,6 +164,19 @@ async function getComponentPath(page, selector) {
102
164
  }, selector);
103
165
  }
104
166
 
167
+ // Debug CSS specificity conflicts
168
+ const report = await page.evaluate(() => {
169
+ return window.__treelocator__.getCSSReport('.my-button', { properties: ['color', 'background'] });
170
+ });
171
+ console.log(report);
172
+
173
+ // Get structured CSS data for assertions
174
+ const css = await page.evaluate(() => {
175
+ return window.__treelocator__.getCSSRules('.my-button');
176
+ });
177
+ const colorRules = css?.properties.find(p => p.property === 'color');
178
+ console.log('Winning rule:', colorRules?.rules.find(r => r.winning));
179
+
105
180
  PUPPETEER EXAMPLES:
106
181
  ------------------
107
182
 
@@ -159,6 +234,49 @@ export function createBrowserAPI(adapterId) {
159
234
  ancestry
160
235
  } : null);
161
236
  },
237
+ getStyles(elementOrSelector, options) {
238
+ const element = resolveElement(elementOrSelector);
239
+ if (!element) return null;
240
+ const ancestry = getAncestryForElement(element, adapterId);
241
+ const label = ancestry ? getElementLabel(ancestry) : undefined;
242
+ return extractComputedStyles(element, label || undefined, options);
243
+ },
244
+ getCSSRules(elementOrSelector) {
245
+ let element = null;
246
+ try {
247
+ element = resolveElement(elementOrSelector);
248
+ } catch {
249
+ return null;
250
+ }
251
+ if (!element) return null;
252
+ return inspectCSSRules(element);
253
+ },
254
+ getCSSReport(elementOrSelector, options) {
255
+ let element = null;
256
+ try {
257
+ element = resolveElement(elementOrSelector);
258
+ } catch {
259
+ return null;
260
+ }
261
+ if (!element) return null;
262
+ const result = inspectCSSRules(element);
263
+
264
+ // Filter to requested properties if specified
265
+ if (options?.properties && options.properties.length > 0) {
266
+ const filterSet = new Set(options.properties.map(p => p.toLowerCase()));
267
+ result.properties = result.properties.filter(p => filterSet.has(p.property.toLowerCase()));
268
+ }
269
+ return formatCSSInspection(result);
270
+ },
271
+ takeSnapshot(selector, snapshotId, options) {
272
+ return takeNamedSnapshot(selector, snapshotId, options);
273
+ },
274
+ getSnapshotDiff(snapshotId) {
275
+ return getNamedSnapshotDiff(snapshotId);
276
+ },
277
+ clearSnapshot(snapshotId) {
278
+ clearNamedSnapshot(snapshotId);
279
+ },
162
280
  help() {
163
281
  return HELP_TEXT;
164
282
  },
@@ -168,6 +286,29 @@ export function createBrowserAPI(adapterId) {
168
286
  replayWithRecord() {
169
287
  // Replaced by Runtime component once mounted
170
288
  return Promise.resolve(null);
289
+ },
290
+ diff: {
291
+ snapshot() {
292
+ return takeSnapshot();
293
+ },
294
+ computeDiff(before, after) {
295
+ return computeDiff(before, after);
296
+ },
297
+ async captureDiff(action, opts) {
298
+ const started = performance.now();
299
+ const before = takeSnapshot();
300
+ await action();
301
+ const settle = await waitForSettle(opts?.settleTimeoutMs);
302
+ const after = takeSnapshot();
303
+ const report = computeDiff(before, after);
304
+ report.elapsedMs = performance.now() - started;
305
+ report.settle = settle;
306
+ report.text = formatReport(report.entries, {
307
+ elapsedMs: report.elapsedMs,
308
+ settle
309
+ });
310
+ return report;
311
+ }
171
312
  }
172
313
  };
173
314
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,287 @@
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
+ import { createTreeNode } from "./adapters/createTreeNode";
12
+ import { enrichAncestryWithSourceMaps } from "./functions/enrichAncestrySourceMaps";
13
+ describe("browserApi", () => {
14
+ beforeEach(() => {
15
+ // Reset window.__treelocator__ before each test
16
+ delete window.__treelocator__;
17
+ });
18
+ afterEach(() => {
19
+ vi.clearAllMocks();
20
+ });
21
+ describe("createBrowserAPI", () => {
22
+ test("returns an object with all expected method names", () => {
23
+ const api = createBrowserAPI();
24
+ expect(api).toHaveProperty("getPath");
25
+ expect(api).toHaveProperty("getAncestry");
26
+ expect(api).toHaveProperty("getPathData");
27
+ expect(api).toHaveProperty("help");
28
+ expect(api).toHaveProperty("replay");
29
+ expect(api).toHaveProperty("replayWithRecord");
30
+ expect(api).toHaveProperty("diff");
31
+ });
32
+ test("all methods are functions", () => {
33
+ const api = createBrowserAPI();
34
+ expect(typeof api.getPath).toBe("function");
35
+ expect(typeof api.getAncestry).toBe("function");
36
+ expect(typeof api.getPathData).toBe("function");
37
+ expect(typeof api.help).toBe("function");
38
+ expect(typeof api.replay).toBe("function");
39
+ expect(typeof api.replayWithRecord).toBe("function");
40
+ expect(typeof api.diff.snapshot).toBe("function");
41
+ expect(typeof api.diff.computeDiff).toBe("function");
42
+ expect(typeof api.diff.captureDiff).toBe("function");
43
+ });
44
+ });
45
+ describe("diff namespace", () => {
46
+ beforeEach(() => {
47
+ if (typeof document.getAnimations !== "function") {
48
+ document.getAnimations = () => [];
49
+ }
50
+ });
51
+ test("snapshot() returns an array", () => {
52
+ const api = createBrowserAPI();
53
+ const snaps = api.diff.snapshot();
54
+ expect(Array.isArray(snaps)).toBe(true);
55
+ });
56
+ test("computeDiff of identical snapshots has empty entries", () => {
57
+ const api = createBrowserAPI();
58
+ const snaps = api.diff.snapshot();
59
+ const report = api.diff.computeDiff(snaps, snaps);
60
+ expect(report.entries).toHaveLength(0);
61
+ expect(report.counts.added).toBe(0);
62
+ });
63
+ test("captureDiff returns a DeltaReport with settle and text populated", async () => {
64
+ const api = createBrowserAPI();
65
+ const report = await api.diff.captureDiff(() => {
66
+ // no-op action
67
+ }, {
68
+ settleTimeoutMs: 200
69
+ });
70
+ expect(report).toHaveProperty("entries");
71
+ expect(report).toHaveProperty("counts");
72
+ expect(report).toHaveProperty("text");
73
+ expect(typeof report.text).toBe("string");
74
+ expect(["clean", "timeout"]).toContain(report.settle);
75
+ });
76
+ });
77
+ describe("help()", () => {
78
+ test("returns a non-empty string", () => {
79
+ const api = createBrowserAPI();
80
+ const help = api.help();
81
+ expect(typeof help).toBe("string");
82
+ expect(help.length).toBeGreaterThan(0);
83
+ });
84
+ test("help text contains 'TreeLocatorJS'", () => {
85
+ const api = createBrowserAPI();
86
+ const help = api.help();
87
+ expect(help).toContain("TreeLocatorJS");
88
+ });
89
+ test("help text contains method descriptions", () => {
90
+ const api = createBrowserAPI();
91
+ const help = api.help();
92
+ expect(help).toContain("getPath");
93
+ expect(help).toContain("getAncestry");
94
+ expect(help).toContain("getPathData");
95
+ });
96
+ });
97
+ describe("replay()", () => {
98
+ test("does not throw when called", () => {
99
+ const api = createBrowserAPI();
100
+ expect(() => api.replay()).not.toThrow();
101
+ });
102
+ test("is a stub that does nothing", () => {
103
+ const api = createBrowserAPI();
104
+ // Should not throw and should return undefined
105
+ const result = api.replay();
106
+ expect(result).toBeUndefined();
107
+ });
108
+ });
109
+ describe("replayWithRecord()", () => {
110
+ test("returns a Promise that resolves to null (stub)", async () => {
111
+ const api = createBrowserAPI();
112
+ const result = api.replayWithRecord("div");
113
+ expect(result).toBeInstanceOf(Promise);
114
+ const resolved = await result;
115
+ expect(resolved).toBeNull();
116
+ });
117
+ test("handles both element and selector arguments without error", async () => {
118
+ const api = createBrowserAPI();
119
+ const div = document.createElement("div");
120
+
121
+ // Test with selector
122
+ const result1 = api.replayWithRecord("div");
123
+ expect(await result1).toBeNull();
124
+
125
+ // Test with element
126
+ const result2 = api.replayWithRecord(div);
127
+ expect(await result2).toBeNull();
128
+ });
129
+ });
130
+ describe("getPath()", () => {
131
+ test("returns Promise<string | null>", async () => {
132
+ const api = createBrowserAPI();
133
+ const result = api.getPath(document.body);
134
+ expect(result).toBeInstanceOf(Promise);
135
+ const resolved = await result;
136
+ expect(typeof resolved === "string" || resolved === null).toBe(true);
137
+ });
138
+ test("returns null when selector doesn't match any element", async () => {
139
+ const api = createBrowserAPI();
140
+ const result = await api.getPath(".non-existent-selector-xyz");
141
+ expect(result).toBeNull();
142
+ });
143
+ test("returns null when passed invalid selector string", async () => {
144
+ const api = createBrowserAPI();
145
+ // querySelector with invalid selector will throw; the function should handle it
146
+ const result = await api.getPath("div > > div");
147
+ expect(result).toBeNull();
148
+ });
149
+ test("returns null when createTreeNode returns null", async () => {
150
+ const mockCreateTreeNode = createTreeNode;
151
+ const mockEnrich = enrichAncestryWithSourceMaps;
152
+ mockCreateTreeNode.mockReturnValue(null);
153
+ mockEnrich.mockResolvedValue(null);
154
+ const api = createBrowserAPI();
155
+ const div = document.createElement("div");
156
+ const result = await api.getPath(div);
157
+ expect(result).toBeNull();
158
+ });
159
+ });
160
+ describe("getAncestry()", () => {
161
+ test("returns Promise<AncestryItem[] | null>", async () => {
162
+ const api = createBrowserAPI();
163
+ const result = api.getAncestry(document.body);
164
+ expect(result).toBeInstanceOf(Promise);
165
+ const resolved = await result;
166
+ expect(Array.isArray(resolved) || resolved === null).toBe(true);
167
+ });
168
+ test("returns null when selector doesn't match any element", async () => {
169
+ const api = createBrowserAPI();
170
+ const result = await api.getAncestry(".non-existent-selector-xyz");
171
+ expect(result).toBeNull();
172
+ });
173
+ test("returns null when createTreeNode returns null", async () => {
174
+ const mockCreateTreeNode = createTreeNode;
175
+ const mockEnrich = enrichAncestryWithSourceMaps;
176
+ mockCreateTreeNode.mockReturnValue(null);
177
+ mockEnrich.mockResolvedValue(null);
178
+ const api = createBrowserAPI();
179
+ const div = document.createElement("div");
180
+ const result = await api.getAncestry(div);
181
+ expect(result).toBeNull();
182
+ });
183
+ });
184
+ describe("getPathData()", () => {
185
+ test("returns Promise with path and ancestry properties or null", async () => {
186
+ const api = createBrowserAPI();
187
+ const result = api.getPathData(document.body);
188
+ expect(result).toBeInstanceOf(Promise);
189
+ const resolved = await result;
190
+ if (resolved !== null) {
191
+ expect(resolved).toHaveProperty("path");
192
+ expect(resolved).toHaveProperty("ancestry");
193
+ }
194
+ });
195
+ test("returns null when selector doesn't match any element", async () => {
196
+ const api = createBrowserAPI();
197
+ const result = await api.getPathData(".non-existent-selector-xyz");
198
+ expect(result).toBeNull();
199
+ });
200
+ test("returns null when createTreeNode returns null", async () => {
201
+ const mockCreateTreeNode = createTreeNode;
202
+ const mockEnrich = enrichAncestryWithSourceMaps;
203
+ mockCreateTreeNode.mockReturnValue(null);
204
+ mockEnrich.mockResolvedValue(null);
205
+ const api = createBrowserAPI();
206
+ const div = document.createElement("div");
207
+ const result = await api.getPathData(div);
208
+ expect(result).toBeNull();
209
+ });
210
+ });
211
+ describe("installBrowserAPI()", () => {
212
+ test("sets window.__treelocator__ to the created API object", () => {
213
+ expect(window.__treelocator__).toBeUndefined();
214
+ installBrowserAPI();
215
+ expect(window.__treelocator__).toBeDefined();
216
+ expect(typeof window.__treelocator__.getPath).toBe("function");
217
+ });
218
+ test("window.__treelocator__ has all expected methods after installation", () => {
219
+ installBrowserAPI();
220
+ const api = window.__treelocator__;
221
+ expect(api).toHaveProperty("getPath");
222
+ expect(api).toHaveProperty("getAncestry");
223
+ expect(api).toHaveProperty("getPathData");
224
+ expect(api).toHaveProperty("help");
225
+ expect(api).toHaveProperty("replay");
226
+ expect(api).toHaveProperty("replayWithRecord");
227
+ expect(api).toHaveProperty("diff");
228
+ expect(typeof api.diff.snapshot).toBe("function");
229
+ });
230
+ test("installBrowserAPI passes adapterId to createBrowserAPI", () => {
231
+ // We can't directly test this without spying on createBrowserAPI,
232
+ // but we can verify that the API is created with or without an adapterId
233
+ installBrowserAPI("react");
234
+ expect(window.__treelocator__).toBeDefined();
235
+ expect(typeof window.__treelocator__.help).toBe("function");
236
+ });
237
+ });
238
+ describe("API accepts HTMLElement or selector string", () => {
239
+ test("getPath accepts HTMLElement", async () => {
240
+ const api = createBrowserAPI();
241
+ const div = document.createElement("div");
242
+ document.body.appendChild(div);
243
+ const mockCreateTreeNode = createTreeNode;
244
+ const mockEnrich = enrichAncestryWithSourceMaps;
245
+ mockCreateTreeNode.mockReturnValue(null);
246
+ mockEnrich.mockResolvedValue(null);
247
+ const result = await api.getPath(div);
248
+ // With null ancestry, should return null
249
+ expect(result).toBeNull();
250
+ document.body.removeChild(div);
251
+ });
252
+ test("getPath accepts CSS selector string", async () => {
253
+ const api = createBrowserAPI();
254
+ const result = await api.getPath("body");
255
+ // Should not throw, result is either string or null
256
+ expect(typeof result === "string" || result === null).toBe(true);
257
+ });
258
+ test("getAncestry accepts HTMLElement", async () => {
259
+ const api = createBrowserAPI();
260
+ const div = document.createElement("div");
261
+ const result = await api.getAncestry(div);
262
+ expect(result === null || Array.isArray(result)).toBe(true);
263
+ });
264
+ test("getAncestry accepts CSS selector string", async () => {
265
+ const api = createBrowserAPI();
266
+ const result = await api.getAncestry("div");
267
+ expect(result === null || Array.isArray(result)).toBe(true);
268
+ });
269
+ test("getPathData accepts HTMLElement", async () => {
270
+ const api = createBrowserAPI();
271
+ const div = document.createElement("div");
272
+ const result = await api.getPathData(div);
273
+ if (result !== null) {
274
+ expect(result).toHaveProperty("path");
275
+ expect(result).toHaveProperty("ancestry");
276
+ }
277
+ });
278
+ test("getPathData accepts CSS selector string", async () => {
279
+ const api = createBrowserAPI();
280
+ const result = await api.getPathData("body");
281
+ if (result !== null) {
282
+ expect(result).toHaveProperty("path");
283
+ expect(result).toHaveProperty("ancestry");
284
+ }
285
+ });
286
+ });
287
+ });
@@ -0,0 +1,11 @@
1
+ import type { RecordingState } from "../hooks/useRecordingState";
2
+ type RecordingPillButtonProps = {
3
+ locatorActive: boolean;
4
+ recordingState: RecordingState;
5
+ settingsOpen: boolean;
6
+ onLocatorToggle: () => void;
7
+ onRecordClick: () => void;
8
+ onSettingsClick: () => void;
9
+ };
10
+ export declare function RecordingPillButton(props: RecordingPillButtonProps): import("solid-js").JSX.Element;
11
+ export {};