@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.
- package/.eslintignore +1 -0
- package/dist/_generated_styles.d.ts +1 -1
- package/dist/_generated_styles.js +20 -0
- package/dist/_generated_tree_icon.d.ts +1 -1
- package/dist/adapters/HtmlElementTreeNode.d.ts +2 -2
- package/dist/adapters/HtmlElementTreeNode.js +4 -6
- package/dist/adapters/createTreeNode.js +17 -44
- package/dist/adapters/detectFramework.d.ts +8 -0
- package/dist/adapters/detectFramework.js +25 -0
- package/dist/adapters/detectFramework.test.d.ts +1 -0
- package/dist/adapters/detectFramework.test.js +60 -0
- package/dist/adapters/jsx/jsxAdapter.js +54 -89
- package/dist/adapters/jsx/jsxAdapter.test.d.ts +1 -0
- package/dist/adapters/jsx/jsxAdapter.test.js +273 -0
- package/dist/adapters/nextjs/parseNextjsDataAttributes.js +1 -1
- package/dist/adapters/nextjs/parseNextjsDataAttributes.test.d.ts +1 -0
- package/dist/adapters/nextjs/parseNextjsDataAttributes.test.js +158 -0
- package/dist/adapters/react/findFiberByHtmlElement.d.ts +1 -1
- package/dist/adapters/react/findFiberByHtmlElement.js +1 -1
- package/dist/adapters/react/getAllParentsElementsAndRootComponent.js +4 -0
- package/dist/adapters/resolveAdapter.d.ts +1 -1
- package/dist/adapters/resolveAdapter.js +4 -8
- package/dist/adapters/svelte/svelteAdapter.test.d.ts +1 -0
- package/dist/adapters/svelte/svelteAdapter.test.js +280 -0
- package/dist/adapters/vue/vueAdapter.test.d.ts +1 -0
- package/dist/adapters/vue/vueAdapter.test.js +222 -0
- package/dist/browserApi.d.ts +148 -0
- package/dist/browserApi.js +146 -5
- package/dist/browserApi.test.d.ts +1 -0
- package/dist/browserApi.test.js +287 -0
- package/dist/components/RecordingPillButton.d.ts +11 -0
- package/dist/components/RecordingPillButton.js +202 -0
- package/dist/components/RecordingResults.d.ts +2 -0
- package/dist/components/RecordingResults.js +213 -78
- package/dist/components/Runtime.js +161 -554
- package/dist/components/SettingsPanel.d.ts +5 -0
- package/dist/components/SettingsPanel.js +312 -0
- package/dist/consoleCapture.d.ts +9 -0
- package/dist/consoleCapture.js +95 -0
- package/dist/functions/cssRuleInspector.d.ts +83 -0
- package/dist/functions/cssRuleInspector.js +608 -0
- package/dist/functions/cssRuleInspector.test.d.ts +1 -0
- package/dist/functions/cssRuleInspector.test.js +439 -0
- package/dist/functions/deduplicateLabels.test.d.ts +1 -0
- package/dist/functions/deduplicateLabels.test.js +178 -0
- package/dist/functions/enrichAncestrySourceMaps.js +0 -1
- package/dist/functions/extractComputedStyles.d.ts +51 -0
- package/dist/functions/extractComputedStyles.js +447 -0
- package/dist/functions/extractComputedStyles.test.d.ts +1 -0
- package/dist/functions/extractComputedStyles.test.js +549 -0
- package/dist/functions/formatAncestryChain.d.ts +8 -0
- package/dist/functions/formatAncestryChain.js +21 -1
- package/dist/functions/formatAncestryChain.test.js +18 -0
- package/dist/functions/getUsableName.test.d.ts +1 -0
- package/dist/functions/getUsableName.test.js +219 -0
- package/dist/functions/isCombinationModifiersPressed.test.d.ts +1 -0
- package/dist/functions/isCombinationModifiersPressed.test.js +192 -0
- package/dist/functions/mergeRects.test.js +210 -1
- package/dist/functions/namedSnapshots.d.ts +52 -0
- package/dist/functions/namedSnapshots.js +161 -0
- package/dist/functions/namedSnapshots.test.d.ts +1 -0
- package/dist/functions/namedSnapshots.test.js +85 -0
- package/dist/functions/normalizeFilePath.test.d.ts +1 -0
- package/dist/functions/normalizeFilePath.test.js +66 -0
- package/dist/functions/parseDataId.test.d.ts +1 -0
- package/dist/functions/parseDataId.test.js +101 -0
- package/dist/hooks/getStorage.d.ts +3 -0
- package/dist/hooks/getStorage.js +17 -0
- package/dist/hooks/useEventListeners.d.ts +15 -0
- package/dist/hooks/useEventListeners.js +56 -0
- package/dist/hooks/useLocatorStorage.d.ts +18 -0
- package/dist/hooks/useLocatorStorage.js +41 -0
- package/dist/hooks/useLocatorStorage.test.d.ts +1 -0
- package/dist/hooks/useLocatorStorage.test.js +124 -0
- package/dist/hooks/useRecordingState.d.ts +43 -0
- package/dist/hooks/useRecordingState.js +387 -0
- package/dist/hooks/useSettings.d.ts +13 -0
- package/dist/hooks/useSettings.js +66 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -2
- package/dist/initRuntime.d.ts +3 -1
- package/dist/initRuntime.js +4 -1
- package/dist/mcpBridge.d.ts +61 -0
- package/dist/mcpBridge.js +534 -0
- package/dist/mcpBridge.test.d.ts +1 -0
- package/dist/mcpBridge.test.js +248 -0
- package/dist/output.css +20 -0
- package/dist/visualDiff/diff.d.ts +9 -0
- package/dist/visualDiff/diff.js +209 -0
- package/dist/visualDiff/diff.test.d.ts +1 -0
- package/dist/visualDiff/diff.test.js +253 -0
- package/dist/visualDiff/settle.d.ts +3 -0
- package/dist/visualDiff/settle.js +50 -0
- package/dist/visualDiff/settle.test.d.ts +1 -0
- package/dist/visualDiff/settle.test.js +65 -0
- package/dist/visualDiff/snapshot.d.ts +4 -0
- package/dist/visualDiff/snapshot.js +84 -0
- package/dist/visualDiff/snapshot.test.d.ts +1 -0
- package/dist/visualDiff/snapshot.test.js +245 -0
- package/dist/visualDiff/types.d.ts +37 -0
- package/dist/visualDiff/types.js +1 -0
- package/package.json +2 -2
- package/scripts/wrapCSS.js +1 -1
- package/scripts/wrapImage.js +1 -1
- package/src/_generated_styles.ts +21 -1
- package/src/_generated_tree_icon.ts +1 -1
- package/src/adapters/HtmlElementTreeNode.ts +10 -7
- package/src/adapters/createTreeNode.ts +12 -51
- package/src/adapters/detectFramework.test.ts +73 -0
- package/src/adapters/detectFramework.ts +28 -0
- package/src/adapters/jsx/jsxAdapter.test.ts +240 -0
- package/src/adapters/jsx/jsxAdapter.ts +53 -106
- package/src/adapters/nextjs/parseNextjsDataAttributes.test.ts +212 -0
- package/src/adapters/nextjs/parseNextjsDataAttributes.ts +1 -1
- package/src/adapters/react/findDebugSource.ts +5 -6
- package/src/adapters/react/findFiberByHtmlElement.ts +3 -3
- package/src/adapters/react/getAllParentsElementsAndRootComponent.ts +3 -0
- package/src/adapters/react/reactAdapter.ts +1 -2
- package/src/adapters/resolveAdapter.ts +4 -14
- package/src/adapters/svelte/svelteAdapter.test.ts +334 -0
- package/src/adapters/vue/vueAdapter.test.ts +259 -0
- package/src/browserApi.test.ts +329 -0
- package/src/browserApi.ts +351 -4
- package/src/components/RecordingPillButton.tsx +301 -0
- package/src/components/RecordingResults.tsx +114 -13
- package/src/components/Runtime.tsx +176 -621
- package/src/components/SettingsPanel.tsx +339 -0
- package/src/consoleCapture.ts +113 -0
- package/src/functions/cssRuleInspector.test.ts +517 -0
- package/src/functions/cssRuleInspector.ts +708 -0
- package/src/functions/deduplicateLabels.test.ts +115 -0
- package/src/functions/enrichAncestrySourceMaps.ts +6 -3
- package/src/functions/extractComputedStyles.test.ts +681 -0
- package/src/functions/extractComputedStyles.ts +768 -0
- package/src/functions/formatAncestryChain.test.ts +23 -1
- package/src/functions/formatAncestryChain.ts +22 -1
- package/src/functions/getUsableName.test.ts +242 -0
- package/src/functions/isCombinationModifiersPressed.test.ts +156 -0
- package/src/functions/mergeRects.test.ts +111 -1
- package/src/functions/namedSnapshots.test.ts +106 -0
- package/src/functions/namedSnapshots.ts +232 -0
- package/src/functions/normalizeFilePath.test.ts +80 -0
- package/src/functions/parseDataId.test.ts +125 -0
- package/src/hooks/getStorage.ts +26 -0
- package/src/hooks/useEventListeners.ts +97 -0
- package/src/hooks/useLocatorStorage.test.ts +127 -0
- package/src/hooks/useLocatorStorage.ts +60 -0
- package/src/hooks/useRecordingState.ts +516 -0
- package/src/hooks/useSettings.ts +83 -0
- package/src/index.ts +10 -5
- package/src/initRuntime.ts +5 -0
- package/src/mcpBridge.test.ts +260 -0
- package/src/mcpBridge.ts +677 -0
- package/src/visualDiff/diff.test.ts +167 -0
- package/src/visualDiff/diff.ts +242 -0
- package/src/visualDiff/settle.test.ts +77 -0
- package/src/visualDiff/settle.ts +62 -0
- package/src/visualDiff/snapshot.test.ts +200 -0
- package/src/visualDiff/snapshot.ts +119 -0
- package/src/visualDiff/types.ts +40 -0
- package/tsconfig.json +3 -1
- package/vitest.config.ts +18 -0
- package/jest.config.ts +0 -195
package/dist/browserApi.d.ts
CHANGED
|
@@ -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;
|
package/dist/browserApi.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 {};
|