@treelocator/runtime 0.3.2 → 0.4.1

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.
@@ -1,5 +1,7 @@
1
1
  import { AdapterId } from "./consts";
2
2
  import { AncestryItem } from "./functions/formatAncestryChain";
3
+ import type { DejitterFinding, DejitterSummary } from "./dejitter/recorder";
4
+ import type { InteractionEvent } from "./components/RecordingResults";
3
5
  export interface LocatorJSAPI {
4
6
  /**
5
7
  * Get formatted ancestry path for an element.
@@ -98,6 +100,49 @@ export interface LocatorJSAPI {
98
100
  * console.log(help);
99
101
  */
100
102
  help(): string;
103
+ /**
104
+ * Replay the last recorded interaction sequence.
105
+ * Dispatches the recorded clicks at the original positions and timing.
106
+ * Must have a completed recording with interactions to replay.
107
+ *
108
+ * @example
109
+ * // In browser console
110
+ * window.__treelocator__.replay();
111
+ *
112
+ * @example
113
+ * // In Playwright
114
+ * await page.evaluate(() => window.__treelocator__.replay());
115
+ */
116
+ replay(): void;
117
+ /**
118
+ * Replay the last recorded interaction sequence while recording an element's property changes.
119
+ * Combines replay and dejitter recording: plays back stored clicks at original timing while
120
+ * tracking visual changes (opacity, transform, position, size) on the target element.
121
+ * Returns the dejitter analysis results when replay completes.
122
+ *
123
+ * @param elementOrSelector - HTMLElement or CSS selector for the element to record during replay
124
+ * @returns Promise resolving to recording results with findings, summary, and interaction log
125
+ *
126
+ * @example
127
+ * // Record the sliding panel while replaying user clicks
128
+ * const results = await window.__treelocator__.replayWithRecord('[data-locatorjs-id="SlidingPanel"]');
129
+ * console.log(results.findings); // anomaly analysis
130
+ * console.log(results.path); // component ancestry
131
+ *
132
+ * @example
133
+ * // In Playwright - automated regression test
134
+ * const results = await page.evaluate(async () => {
135
+ * return await window.__treelocator__.replayWithRecord('.my-panel');
136
+ * });
137
+ * expect(results.findings.filter(f => f.severity === 'high')).toHaveLength(0);
138
+ */
139
+ replayWithRecord(elementOrSelector: HTMLElement | string): Promise<{
140
+ path: string;
141
+ findings: DejitterFinding[];
142
+ summary: DejitterSummary | null;
143
+ data: any;
144
+ interactions: InteractionEvent[];
145
+ } | null>;
101
146
  }
102
147
  export declare function createBrowserAPI(adapterIdParam?: AdapterId): LocatorJSAPI;
103
148
  export declare function installBrowserAPI(adapterIdParam?: AdapterId): void;
@@ -63,7 +63,22 @@ METHODS:
63
63
  console.log(data.path) // formatted string
64
64
  console.log(data.ancestry) // structured array
65
65
 
66
- 4. help()
66
+ 4. replay()
67
+ Replays the last recorded interaction sequence as a macro.
68
+
69
+ Usage:
70
+ window.__treelocator__.replay()
71
+
72
+ 5. replayWithRecord(elementOrSelector)
73
+ Replays stored interactions while recording element changes.
74
+ Returns dejitter analysis when replay completes.
75
+
76
+ Usage:
77
+ const results = await window.__treelocator__.replayWithRecord('[data-locatorjs-id="SlidingPanel"]')
78
+ console.log(results.findings) // anomaly analysis
79
+ console.log(results.path) // component ancestry
80
+
81
+ 6. help()
67
82
  Displays this help message.
68
83
 
69
84
  PLAYWRIGHT EXAMPLES:
@@ -148,6 +163,13 @@ export function createBrowserAPI(adapterIdParam) {
148
163
  },
149
164
  help() {
150
165
  return HELP_TEXT;
166
+ },
167
+ replay() {
168
+ // Replaced by Runtime component once mounted
169
+ },
170
+ replayWithRecord() {
171
+ // Replaced by Runtime component once mounted
172
+ return Promise.resolve(null);
151
173
  }
152
174
  };
153
175
  }
@@ -0,0 +1,5 @@
1
+ type RecordingOutlineProps = {
2
+ element: HTMLElement;
3
+ };
4
+ export declare function RecordingOutline(props: RecordingOutlineProps): import("solid-js").JSX.Element;
5
+ export {};
@@ -0,0 +1,53 @@
1
+ import { template as _$template } from "solid-js/web";
2
+ import { effect as _$effect } from "solid-js/web";
3
+ import { setStyleProperty as _$setStyleProperty } from "solid-js/web";
4
+ var _tmpl$ = /*#__PURE__*/_$template(`<div style=z-index:2;border-radius:2px;pointer-events:none><div style="align-items:center;border-radius:4px;font-size:10px;font-family:system-ui, sans-serif;font-weight:600;letter-spacing:0.5px;white-space:nowrap"><div style=border-radius:50%></div>REC`);
5
+ import { createSignal, onCleanup, onMount } from "solid-js";
6
+ export function RecordingOutline(props) {
7
+ const [box, setBox] = createSignal(props.element.getBoundingClientRect());
8
+ let rafId;
9
+ const updateBox = () => {
10
+ setBox(props.element.getBoundingClientRect());
11
+ rafId = requestAnimationFrame(updateBox);
12
+ };
13
+ onMount(() => {
14
+ rafId = requestAnimationFrame(updateBox);
15
+ });
16
+ onCleanup(() => cancelAnimationFrame(rafId));
17
+ return (() => {
18
+ var _el$ = _tmpl$(),
19
+ _el$2 = _el$.firstChild,
20
+ _el$3 = _el$2.firstChild;
21
+ _$setStyleProperty(_el$, "position", "fixed");
22
+ _$setStyleProperty(_el$, "border", "2px dashed #ef4444");
23
+ _$setStyleProperty(_el$2, "position", "absolute");
24
+ _$setStyleProperty(_el$2, "top", "-22px");
25
+ _$setStyleProperty(_el$2, "left", "4px");
26
+ _$setStyleProperty(_el$2, "display", "flex");
27
+ _$setStyleProperty(_el$2, "gap", "4px");
28
+ _$setStyleProperty(_el$2, "padding", "2px 8px");
29
+ _$setStyleProperty(_el$2, "background", "rgba(239, 68, 68, 0.9)");
30
+ _$setStyleProperty(_el$2, "color", "#fff");
31
+ _$setStyleProperty(_el$3, "width", "6px");
32
+ _$setStyleProperty(_el$3, "height", "6px");
33
+ _$setStyleProperty(_el$3, "background", "#fff");
34
+ _$setStyleProperty(_el$3, "animation", "treelocator-rec-pulse 1s ease-in-out infinite");
35
+ _$effect(_p$ => {
36
+ var _v$ = box().x + "px",
37
+ _v$2 = box().y + "px",
38
+ _v$3 = box().width + "px",
39
+ _v$4 = box().height + "px";
40
+ _v$ !== _p$.e && _$setStyleProperty(_el$, "left", _p$.e = _v$);
41
+ _v$2 !== _p$.t && _$setStyleProperty(_el$, "top", _p$.t = _v$2);
42
+ _v$3 !== _p$.a && _$setStyleProperty(_el$, "width", _p$.a = _v$3);
43
+ _v$4 !== _p$.o && _$setStyleProperty(_el$, "height", _p$.o = _v$4);
44
+ return _p$;
45
+ }, {
46
+ e: undefined,
47
+ t: undefined,
48
+ a: undefined,
49
+ o: undefined
50
+ });
51
+ return _el$;
52
+ })();
53
+ }
@@ -0,0 +1,25 @@
1
+ import type { DejitterFinding, DejitterSummary } from "../dejitter/recorder";
2
+ export type InteractionEvent = {
3
+ t: number;
4
+ type: string;
5
+ target: string;
6
+ x: number;
7
+ y: number;
8
+ };
9
+ type RecordingResultsProps = {
10
+ findings: DejitterFinding[];
11
+ summary: DejitterSummary | null;
12
+ data: any;
13
+ elementPath: string;
14
+ interactions: InteractionEvent[];
15
+ onDismiss: () => void;
16
+ onReplay?: () => void;
17
+ replaying?: boolean;
18
+ onToast?: (msg: string) => void;
19
+ hasPrevious?: boolean;
20
+ onLoadPrevious?: () => void;
21
+ hasNext?: boolean;
22
+ onLoadNext?: () => void;
23
+ };
24
+ export declare function RecordingResults(props: RecordingResultsProps): import("solid-js").JSX.Element;
25
+ export {};
@@ -0,0 +1,272 @@
1
+ import { template as _$template } from "solid-js/web";
2
+ import { delegateEvents as _$delegateEvents } from "solid-js/web";
3
+ import { style as _$style } from "solid-js/web";
4
+ import { effect as _$effect } from "solid-js/web";
5
+ import { createComponent as _$createComponent } from "solid-js/web";
6
+ import { addEventListener as _$addEventListener } from "solid-js/web";
7
+ import { memo as _$memo } from "solid-js/web";
8
+ import { insert as _$insert } from "solid-js/web";
9
+ import { setStyleProperty as _$setStyleProperty } from "solid-js/web";
10
+ var _tmpl$ = /*#__PURE__*/_$template(`<div style=margin-bottom:4px;font-size:11px;text-transform:uppercase;letter-spacing:0.5px>Findings (<!>)`),
11
+ _tmpl$2 = /*#__PURE__*/_$template(`<div style="border-top:1px solid rgba(255, 255, 255, 0.08)"><div style=margin-bottom:4px;font-size:11px;text-transform:uppercase;letter-spacing:0.5px>Interactions (<!>)`),
12
+ _tmpl$3 = /*#__PURE__*/_$template(`<div style="z-index:2147483646;max-height:400px;overflow-y:auto;backdrop-filter:blur(12px);border-radius:12px;box-shadow:0 8px 32px rgba(0, 0, 0, 0.4);font-family:system-ui, -apple-system, sans-serif;font-size:12px;pointer-events:auto"><div style="align-items:center;justify-content:space-between;border-bottom:1px solid rgba(255, 255, 255, 0.08)"><div><div style=font-weight:600;font-size:13px>Recording Results</div><div style=margin-top:2px>ms &middot; <!> frames</div></div><div style=align-items:center><div>Copy</div><div style=border-radius:4px;font-size:16px;line-height:1>&times;</div></div></div><div>`),
13
+ _tmpl$4 = /*#__PURE__*/_$template(`<div>`),
14
+ _tmpl$5 = /*#__PURE__*/_$template(`<div>Prev`),
15
+ _tmpl$6 = /*#__PURE__*/_$template(`<div>Next`),
16
+ _tmpl$7 = /*#__PURE__*/_$template(`<div style=align-items:center><span style=font-size:14px>&#10003;</span>No anomalies detected`),
17
+ _tmpl$8 = /*#__PURE__*/_$template(`<div style="align-items:flex-start;border-bottom:1px solid rgba(255, 255, 255, 0.05)"><div style=border-radius:50%;flex-shrink:0;margin-top:3px></div><div style=min-width:0><div style=align-items:center><span style=font-weight:600></span><span style=font-size:11px></span></div><div style=margin-top:2px;line-height:1.4;word-break:break-word>`),
18
+ _tmpl$9 = /*#__PURE__*/_$template(`<div style=font-family:monospace;font-size:11px><span>ms</span> <span></span> <span>`);
19
+ import { For, Show } from "solid-js";
20
+ const SEVERITY_COLORS = {
21
+ high: "#ef4444",
22
+ medium: "#f59e0b",
23
+ low: "#eab308",
24
+ info: "#9ca3af"
25
+ };
26
+ function formatDataForClipboard(data, elementPath, summary, findings, interactions) {
27
+ const lines = [];
28
+
29
+ // Element ancestry path from treelocator
30
+ if (elementPath) {
31
+ lines.push(`Element: ${elementPath}`);
32
+ }
33
+
34
+ // Header
35
+ lines.push(`Recording: ${Math.round(summary?.duration ?? 0)}ms, ${summary?.rawFrameCount ?? 0} frames`);
36
+ lines.push('');
37
+
38
+ // Property changes
39
+ if (data?.propStats?.props && data.propStats.props.length > 0) {
40
+ lines.push('Changed properties:');
41
+ for (const p of data.propStats.props) {
42
+ if (p.raw === 0) continue;
43
+ lines.push(` ${p.prop}: ${p.raw} changes (${p.mode})`);
44
+ }
45
+ lines.push('');
46
+ }
47
+
48
+ // Samples with actual values — replace dejitter element IDs with the ancestry path
49
+ if (data?.samples && data.samples.length > 0) {
50
+ lines.push('Timeline:');
51
+ for (const frame of data.samples) {
52
+ for (const change of frame.changes) {
53
+ const {
54
+ id,
55
+ ...props
56
+ } = change;
57
+ const propEntries = Object.entries(props);
58
+ if (propEntries.length > 0) {
59
+ const vals = propEntries.map(([k, v]) => `${k}=${v}`).join(', ');
60
+ lines.push(` ${frame.t}ms: ${vals}`);
61
+ }
62
+ }
63
+ }
64
+ lines.push('');
65
+ }
66
+
67
+ // Findings — replace dejitter element IDs in descriptions
68
+ if (findings.length > 0) {
69
+ lines.push('Anomalies:');
70
+ for (const f of findings) {
71
+ lines.push(` [${f.severity}] ${f.type}: ${f.description}`);
72
+ }
73
+ lines.push('');
74
+ }
75
+
76
+ // Interactions
77
+ if (interactions.length > 0) {
78
+ lines.push('User interactions:');
79
+ for (const evt of interactions) {
80
+ lines.push(` ${evt.t}ms ${evt.type} ${evt.target} (${evt.x},${evt.y})`);
81
+ }
82
+ }
83
+ return lines.join('\n');
84
+ }
85
+ const buttonStyle = active => ({
86
+ cursor: "pointer",
87
+ padding: "4px 10px",
88
+ "border-radius": "4px",
89
+ background: active ? "rgba(59, 130, 246, 0.2)" : "rgba(255, 255, 255, 0.08)",
90
+ color: active ? "#60a5fa" : "#9ca3af",
91
+ "font-size": "11px",
92
+ "font-weight": "600",
93
+ "line-height": "1.4",
94
+ transition: "background 0.15s, color 0.15s"
95
+ });
96
+ export function RecordingResults(props) {
97
+ const duration = () => props.summary?.duration ?? 0;
98
+ const frameCount = () => props.summary?.rawFrameCount ?? 0;
99
+ function handleCopy() {
100
+ const text = formatDataForClipboard(props.data, props.elementPath, props.summary, props.findings, props.interactions);
101
+ navigator.clipboard.writeText(text).then(() => {
102
+ props.onToast?.("Copied to clipboard");
103
+ });
104
+ }
105
+ return (() => {
106
+ var _el$ = _tmpl$3(),
107
+ _el$2 = _el$.firstChild,
108
+ _el$3 = _el$2.firstChild,
109
+ _el$4 = _el$3.firstChild,
110
+ _el$5 = _el$4.nextSibling,
111
+ _el$6 = _el$5.firstChild,
112
+ _el$8 = _el$6.nextSibling,
113
+ _el$7 = _el$8.nextSibling,
114
+ _el$9 = _el$3.nextSibling,
115
+ _el$0 = _el$9.firstChild,
116
+ _el$1 = _el$0.nextSibling,
117
+ _el$10 = _el$2.nextSibling;
118
+ _$setStyleProperty(_el$, "position", "fixed");
119
+ _$setStyleProperty(_el$, "bottom", "84px");
120
+ _$setStyleProperty(_el$, "right", "20px");
121
+ _$setStyleProperty(_el$, "width", "340px");
122
+ _$setStyleProperty(_el$, "background", "rgba(15, 15, 15, 0.92)");
123
+ _$setStyleProperty(_el$, "border", "1px solid rgba(255, 255, 255, 0.1)");
124
+ _$setStyleProperty(_el$, "color", "#e5e5e5");
125
+ _$setStyleProperty(_el$2, "display", "flex");
126
+ _$setStyleProperty(_el$2, "padding", "12px 14px 8px");
127
+ _$setStyleProperty(_el$4, "color", "#fff");
128
+ _$setStyleProperty(_el$5, "color", "#9ca3af");
129
+ _$insert(_el$5, () => Math.round(duration()), _el$6);
130
+ _$insert(_el$5, frameCount, _el$8);
131
+ _$setStyleProperty(_el$9, "display", "flex");
132
+ _$setStyleProperty(_el$9, "gap", "4px");
133
+ _el$0.$$click = handleCopy;
134
+ _$insert(_el$9, (() => {
135
+ var _c$ = _$memo(() => !!props.onReplay);
136
+ return () => _c$() && (() => {
137
+ var _el$20 = _tmpl$4();
138
+ _$addEventListener(_el$20, "click", props.onReplay, true);
139
+ _$insert(_el$20, () => props.replaying ? "Replaying..." : "Replay");
140
+ _$effect(_$p => _$style(_el$20, buttonStyle(props.replaying), _$p));
141
+ return _el$20;
142
+ })();
143
+ })(), _el$1);
144
+ _$insert(_el$9, (() => {
145
+ var _c$2 = _$memo(() => !!(props.hasPrevious && props.onLoadPrevious));
146
+ return () => _c$2() && (() => {
147
+ var _el$21 = _tmpl$5();
148
+ _$addEventListener(_el$21, "click", props.onLoadPrevious, true);
149
+ _$effect(_$p => _$style(_el$21, buttonStyle(), _$p));
150
+ return _el$21;
151
+ })();
152
+ })(), _el$1);
153
+ _$insert(_el$9, (() => {
154
+ var _c$3 = _$memo(() => !!(props.hasNext && props.onLoadNext));
155
+ return () => _c$3() && (() => {
156
+ var _el$22 = _tmpl$6();
157
+ _$addEventListener(_el$22, "click", props.onLoadNext, true);
158
+ _$effect(_$p => _$style(_el$22, buttonStyle(), _$p));
159
+ return _el$22;
160
+ })();
161
+ })(), _el$1);
162
+ _$addEventListener(_el$1, "click", props.onDismiss, true);
163
+ _$setStyleProperty(_el$1, "cursor", "pointer");
164
+ _$setStyleProperty(_el$1, "padding", "4px 8px");
165
+ _$setStyleProperty(_el$1, "color", "#9ca3af");
166
+ _$setStyleProperty(_el$10, "padding", "8px 14px");
167
+ _$insert(_el$10, _$createComponent(Show, {
168
+ get when() {
169
+ return props.findings.length > 0;
170
+ },
171
+ get fallback() {
172
+ return (() => {
173
+ var _el$23 = _tmpl$7(),
174
+ _el$24 = _el$23.firstChild;
175
+ _$setStyleProperty(_el$23, "display", "flex");
176
+ _$setStyleProperty(_el$23, "gap", "6px");
177
+ _$setStyleProperty(_el$23, "padding", "8px 0");
178
+ _$setStyleProperty(_el$23, "color", "#4ade80");
179
+ return _el$23;
180
+ })();
181
+ },
182
+ get children() {
183
+ return [(() => {
184
+ var _el$11 = _tmpl$(),
185
+ _el$12 = _el$11.firstChild,
186
+ _el$14 = _el$12.nextSibling,
187
+ _el$13 = _el$14.nextSibling;
188
+ _$setStyleProperty(_el$11, "color", "#9ca3af");
189
+ _$insert(_el$11, () => props.findings.length, _el$14);
190
+ return _el$11;
191
+ })(), _$createComponent(For, {
192
+ get each() {
193
+ return props.findings;
194
+ },
195
+ children: finding => (() => {
196
+ var _el$25 = _tmpl$8(),
197
+ _el$26 = _el$25.firstChild,
198
+ _el$27 = _el$26.nextSibling,
199
+ _el$28 = _el$27.firstChild,
200
+ _el$29 = _el$28.firstChild,
201
+ _el$30 = _el$29.nextSibling,
202
+ _el$31 = _el$28.nextSibling;
203
+ _$setStyleProperty(_el$25, "display", "flex");
204
+ _$setStyleProperty(_el$25, "gap", "8px");
205
+ _$setStyleProperty(_el$25, "padding", "6px 0");
206
+ _$setStyleProperty(_el$26, "width", "8px");
207
+ _$setStyleProperty(_el$26, "height", "8px");
208
+ _$setStyleProperty(_el$28, "display", "flex");
209
+ _$setStyleProperty(_el$28, "gap", "6px");
210
+ _$setStyleProperty(_el$29, "color", "#fff");
211
+ _$insert(_el$29, () => finding.type);
212
+ _$insert(_el$30, () => finding.severity);
213
+ _$setStyleProperty(_el$31, "color", "#a1a1aa");
214
+ _$insert(_el$31, () => finding.description);
215
+ _$effect(_p$ => {
216
+ var _v$ = SEVERITY_COLORS[finding.severity] || "#9ca3af",
217
+ _v$2 = SEVERITY_COLORS[finding.severity];
218
+ _v$ !== _p$.e && _$setStyleProperty(_el$26, "background", _p$.e = _v$);
219
+ _v$2 !== _p$.t && _$setStyleProperty(_el$30, "color", _p$.t = _v$2);
220
+ return _p$;
221
+ }, {
222
+ e: undefined,
223
+ t: undefined
224
+ });
225
+ return _el$25;
226
+ })()
227
+ })];
228
+ }
229
+ }));
230
+ _$insert(_el$, _$createComponent(Show, {
231
+ get when() {
232
+ return props.interactions.length > 0;
233
+ },
234
+ get children() {
235
+ var _el$15 = _tmpl$2(),
236
+ _el$16 = _el$15.firstChild,
237
+ _el$17 = _el$16.firstChild,
238
+ _el$19 = _el$17.nextSibling,
239
+ _el$18 = _el$19.nextSibling;
240
+ _$setStyleProperty(_el$15, "padding", "8px 14px 12px");
241
+ _$setStyleProperty(_el$16, "color", "#9ca3af");
242
+ _$insert(_el$16, () => props.interactions.length, _el$19);
243
+ _$insert(_el$15, _$createComponent(For, {
244
+ get each() {
245
+ return props.interactions;
246
+ },
247
+ children: evt => (() => {
248
+ var _el$32 = _tmpl$9(),
249
+ _el$33 = _el$32.firstChild,
250
+ _el$34 = _el$33.firstChild,
251
+ _el$35 = _el$33.nextSibling,
252
+ _el$36 = _el$35.nextSibling,
253
+ _el$37 = _el$36.nextSibling,
254
+ _el$38 = _el$37.nextSibling;
255
+ _$setStyleProperty(_el$32, "padding", "3px 0");
256
+ _$setStyleProperty(_el$32, "color", "#a1a1aa");
257
+ _$setStyleProperty(_el$33, "color", "#9ca3af");
258
+ _$insert(_el$33, () => evt.t, _el$34);
259
+ _$setStyleProperty(_el$36, "color", "#60a5fa");
260
+ _$insert(_el$36, () => evt.type);
261
+ _$insert(_el$38, () => evt.target);
262
+ return _el$32;
263
+ })()
264
+ }), null);
265
+ return _el$15;
266
+ }
267
+ }), null);
268
+ _$effect(_$p => _$style(_el$0, buttonStyle(), _$p));
269
+ return _el$;
270
+ })();
271
+ }
272
+ _$delegateEvents(["click"]);