@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
@@ -0,0 +1,253 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { computeDiff, formatReport } from "./diff";
3
+ function snap(overrides = {}) {
4
+ return {
5
+ key: "1",
6
+ tagName: "div",
7
+ classes: [],
8
+ x: 0,
9
+ y: 0,
10
+ width: 100,
11
+ height: 100,
12
+ visible: true,
13
+ opacity: 1,
14
+ inViewport: true,
15
+ pointerEvents: "auto",
16
+ disabled: false,
17
+ ...overrides
18
+ };
19
+ }
20
+ describe("computeDiff", () => {
21
+ test("empty snapshots produce empty report", () => {
22
+ const r = computeDiff([], []);
23
+ expect(r.entries).toHaveLength(0);
24
+ expect(r.counts).toEqual({
25
+ added: 0,
26
+ removed: 0,
27
+ changed: 0,
28
+ moved: 0
29
+ });
30
+ });
31
+ test("identical snapshots produce no entries", () => {
32
+ const a = [snap({
33
+ key: "1"
34
+ }), snap({
35
+ key: "2"
36
+ })];
37
+ const b = [snap({
38
+ key: "1"
39
+ }), snap({
40
+ key: "2"
41
+ })];
42
+ const r = computeDiff(a, b);
43
+ expect(r.entries).toHaveLength(0);
44
+ });
45
+ test("+ for keys only in after (when visible)", () => {
46
+ const a = [snap({
47
+ key: "1"
48
+ })];
49
+ const b = [snap({
50
+ key: "1"
51
+ }), snap({
52
+ key: "2",
53
+ id: "new"
54
+ })];
55
+ const r = computeDiff(a, b);
56
+ expect(r.counts.added).toBe(1);
57
+ expect(r.entries[0].type).toBe("+");
58
+ expect(r.entries[0].key).toBe("2");
59
+ });
60
+ test("+ is skipped when newly-added element is invisible", () => {
61
+ const a = [];
62
+ const b = [snap({
63
+ key: "2",
64
+ visible: false
65
+ })];
66
+ const r = computeDiff(a, b);
67
+ expect(r.counts.added).toBe(0);
68
+ });
69
+ test("- for keys only in before (when visible)", () => {
70
+ const a = [snap({
71
+ key: "1"
72
+ }), snap({
73
+ key: "2"
74
+ })];
75
+ const b = [snap({
76
+ key: "1"
77
+ })];
78
+ const r = computeDiff(a, b);
79
+ expect(r.counts.removed).toBe(1);
80
+ expect(r.entries[0].type).toBe("-");
81
+ });
82
+ test("- when element becomes invisible in after", () => {
83
+ const a = [snap({
84
+ key: "1",
85
+ visible: true
86
+ })];
87
+ const b = [snap({
88
+ key: "1",
89
+ visible: false
90
+ })];
91
+ const r = computeDiff(a, b);
92
+ expect(r.counts.removed).toBe(1);
93
+ expect(r.entries[0].type).toBe("-");
94
+ });
95
+ test("~ when opacity changes by >= 0.05", () => {
96
+ const a = [snap({
97
+ key: "1",
98
+ opacity: 1
99
+ })];
100
+ const b = [snap({
101
+ key: "1",
102
+ opacity: 0.95
103
+ })];
104
+ const r = computeDiff(a, b);
105
+ expect(r.counts.changed).toBe(1);
106
+ expect(r.entries[0].type).toBe("~");
107
+ expect(r.entries[0].changedFields).toContain("opacity");
108
+ });
109
+ test("no entry when opacity changes by < 0.05", () => {
110
+ const a = [snap({
111
+ key: "1",
112
+ opacity: 1
113
+ })];
114
+ const b = [snap({
115
+ key: "1",
116
+ opacity: 0.97
117
+ })];
118
+ const r = computeDiff(a, b);
119
+ expect(r.entries).toHaveLength(0);
120
+ });
121
+ test("→ when only x/y change by >= 4px", () => {
122
+ const a = [snap({
123
+ key: "1",
124
+ x: 0,
125
+ y: 0
126
+ })];
127
+ const b = [snap({
128
+ key: "1",
129
+ x: 10,
130
+ y: 20
131
+ })];
132
+ const r = computeDiff(a, b);
133
+ expect(r.counts.moved).toBe(1);
134
+ expect(r.entries[0].type).toBe("→");
135
+ });
136
+ test("no entry when position changes by 3px (under threshold)", () => {
137
+ const a = [snap({
138
+ key: "1",
139
+ x: 0,
140
+ y: 0
141
+ })];
142
+ const b = [snap({
143
+ key: "1",
144
+ x: 3,
145
+ y: 3
146
+ })];
147
+ const r = computeDiff(a, b);
148
+ expect(r.entries).toHaveLength(0);
149
+ });
150
+ test("w/h: 2px delta triggers ~, 1px does not", () => {
151
+ const r1 = computeDiff([snap({
152
+ key: "1",
153
+ width: 100
154
+ })], [snap({
155
+ key: "1",
156
+ width: 102
157
+ })]);
158
+ expect(r1.counts.changed).toBe(1);
159
+ const r2 = computeDiff([snap({
160
+ key: "1",
161
+ width: 100
162
+ })], [snap({
163
+ key: "1",
164
+ width: 101
165
+ })]);
166
+ expect(r2.entries).toHaveLength(0);
167
+ });
168
+ test("mixed position + opacity change produces ~, not →", () => {
169
+ const a = [snap({
170
+ key: "1",
171
+ x: 0,
172
+ opacity: 1
173
+ })];
174
+ const b = [snap({
175
+ key: "1",
176
+ x: 20,
177
+ opacity: 0.5
178
+ })];
179
+ const r = computeDiff(a, b);
180
+ expect(r.counts.changed).toBe(1);
181
+ expect(r.counts.moved).toBe(0);
182
+ expect(r.entries[0].type).toBe("~");
183
+ expect(r.entries[0].changedFields).toContain("x");
184
+ expect(r.entries[0].changedFields).toContain("opacity");
185
+ });
186
+ test("text change is reported", () => {
187
+ const a = [snap({
188
+ key: "1",
189
+ text: "Submit"
190
+ })];
191
+ const b = [snap({
192
+ key: "1",
193
+ text: "Submitting..."
194
+ })];
195
+ const r = computeDiff(a, b);
196
+ expect(r.counts.changed).toBe(1);
197
+ expect(r.entries[0].changedFields).toContain("text");
198
+ });
199
+ test("class addition is reported", () => {
200
+ const a = [snap({
201
+ key: "1",
202
+ classes: ["foo"]
203
+ })];
204
+ const b = [snap({
205
+ key: "1",
206
+ classes: ["foo", "active"]
207
+ })];
208
+ const r = computeDiff(a, b);
209
+ expect(r.counts.changed).toBe(1);
210
+ expect(r.entries[0].changedFields).toContain("classes");
211
+ });
212
+ test("pointer-events change is reported", () => {
213
+ const a = [snap({
214
+ key: "1",
215
+ pointerEvents: "auto"
216
+ })];
217
+ const b = [snap({
218
+ key: "1",
219
+ pointerEvents: "none"
220
+ })];
221
+ const r = computeDiff(a, b);
222
+ expect(r.counts.changed).toBe(1);
223
+ expect(r.entries[0].changedFields).toContain("pointerEvents");
224
+ });
225
+ });
226
+ describe("formatReport", () => {
227
+ test("no-changes report contains header and divider", () => {
228
+ const out = formatReport([], {
229
+ elapsedMs: 100,
230
+ settle: "clean"
231
+ });
232
+ expect(out).toContain("Visual diff");
233
+ expect(out).toContain("(no changes)");
234
+ });
235
+ test("summary line counts match entry types", () => {
236
+ const a = [snap({
237
+ key: "1"
238
+ }), snap({
239
+ key: "2"
240
+ })];
241
+ const b = [snap({
242
+ key: "1",
243
+ x: 20
244
+ }), snap({
245
+ key: "3",
246
+ id: "added"
247
+ })];
248
+ const r = computeDiff(a, b);
249
+ expect(r.text).toContain("1 added");
250
+ expect(r.text).toContain("1 removed");
251
+ expect(r.text).toContain("1 moved");
252
+ });
253
+ });
@@ -0,0 +1,3 @@
1
+ export declare const DEFAULT_SETTLE_TIMEOUT_MS = 3000;
2
+ export declare const MUTATION_SILENCE_MS = 150;
3
+ export declare function waitForSettle(timeoutMs?: number, root?: Node): Promise<"clean" | "timeout">;
@@ -0,0 +1,50 @@
1
+ export const DEFAULT_SETTLE_TIMEOUT_MS = 3000;
2
+ export const MUTATION_SILENCE_MS = 150;
3
+ function nextFrame() {
4
+ return new Promise(resolve => {
5
+ if (typeof requestAnimationFrame === "function") {
6
+ requestAnimationFrame(() => resolve());
7
+ } else {
8
+ setTimeout(() => resolve(), 16);
9
+ }
10
+ });
11
+ }
12
+ function animationsRunning() {
13
+ if (typeof document === "undefined" || typeof document.getAnimations !== "function") {
14
+ return false;
15
+ }
16
+ const anims = document.getAnimations();
17
+ for (const a of anims) {
18
+ if (a.playState === "running") return true;
19
+ }
20
+ return false;
21
+ }
22
+ export async function waitForSettle(timeoutMs = DEFAULT_SETTLE_TIMEOUT_MS, root) {
23
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") {
24
+ return "clean";
25
+ }
26
+ const observeTarget = root ?? document.documentElement;
27
+ const deadline = performance.now() + timeoutMs;
28
+ let lastMutation = performance.now();
29
+ const mo = new MutationObserver(() => {
30
+ lastMutation = performance.now();
31
+ });
32
+ mo.observe(observeTarget, {
33
+ childList: true,
34
+ subtree: true,
35
+ attributes: true,
36
+ characterData: true
37
+ });
38
+ try {
39
+ while (performance.now() < deadline) {
40
+ const sinceMutation = performance.now() - lastMutation;
41
+ if (!animationsRunning() && sinceMutation >= MUTATION_SILENCE_MS) {
42
+ return "clean";
43
+ }
44
+ await nextFrame();
45
+ }
46
+ return "timeout";
47
+ } finally {
48
+ mo.disconnect();
49
+ }
50
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,65 @@
1
+ import { describe, expect, test, beforeEach, afterEach, vi } from "vitest";
2
+ import { waitForSettle } from "./settle";
3
+ describe("waitForSettle", () => {
4
+ beforeEach(() => {
5
+ document.body.innerHTML = "";
6
+ if (typeof document.getAnimations !== "function") {
7
+ document.getAnimations = () => [];
8
+ }
9
+ });
10
+ afterEach(() => {
11
+ vi.restoreAllMocks();
12
+ });
13
+ test("returns 'clean' when no mutations and no animations", async () => {
14
+ const result = await waitForSettle(500);
15
+ expect(result).toBe("clean");
16
+ });
17
+ test("returns 'timeout' when mutations keep firing past the deadline", async () => {
18
+ const interval = setInterval(() => {
19
+ const d = document.createElement("div");
20
+ document.body.appendChild(d);
21
+ setTimeout(() => d.remove(), 0);
22
+ }, 20);
23
+ const result = await waitForSettle(400);
24
+ clearInterval(interval);
25
+ expect(result).toBe("timeout");
26
+ });
27
+ test("settles after mutations stop", async () => {
28
+ const start = performance.now();
29
+ let mutationCount = 0;
30
+ const interval = setInterval(() => {
31
+ if (mutationCount >= 3) {
32
+ clearInterval(interval);
33
+ return;
34
+ }
35
+ const d = document.createElement("div");
36
+ document.body.appendChild(d);
37
+ mutationCount++;
38
+ }, 30);
39
+ const result = await waitForSettle(2000);
40
+ const elapsed = performance.now() - start;
41
+ expect(result).toBe("clean");
42
+ expect(elapsed).toBeLessThan(2000);
43
+ });
44
+ test("ignores mutations outside the provided root", async () => {
45
+ const root = document.createElement("div");
46
+ document.body.appendChild(root);
47
+ const interval = setInterval(() => {
48
+ const d = document.createElement("div");
49
+ document.body.appendChild(d);
50
+ setTimeout(() => d.remove(), 0);
51
+ }, 20);
52
+ const result = await waitForSettle(500, root);
53
+ clearInterval(interval);
54
+ expect(result).toBe("clean");
55
+ });
56
+ test("treats animations as non-idle", async () => {
57
+ const fakeAnim = {
58
+ playState: "running"
59
+ };
60
+ const spy = vi.spyOn(document, "getAnimations").mockReturnValue([fakeAnim]);
61
+ const result = await waitForSettle(300);
62
+ expect(result).toBe("timeout");
63
+ spy.mockRestore();
64
+ });
65
+ });
@@ -0,0 +1,4 @@
1
+ import type { ElementSnapshot } from "./types";
2
+ export declare const MAX_SNAPSHOT_ELEMENTS = 2000;
3
+ export declare function takeSnapshot(root?: HTMLElement | SVGElement | null): ElementSnapshot[];
4
+ export declare function __resetSnapshotWarningForTests(): void;
@@ -0,0 +1,84 @@
1
+ import { getReferenceId } from "../functions/getReferenceId";
2
+ import { isLocatorsOwnElement } from "../functions/isLocatorsOwnElement";
3
+ export const MAX_SNAPSHOT_ELEMENTS = 2000;
4
+ const TEXT_LIMIT = 80;
5
+ let warnedOverflow = false;
6
+ function truncateText(raw) {
7
+ if (!raw) return undefined;
8
+ const trimmed = raw.trim();
9
+ if (!trimmed) return undefined;
10
+ return trimmed.length > TEXT_LIMIT ? trimmed.slice(0, TEXT_LIMIT) : trimmed;
11
+ }
12
+ function snapshotElement(el) {
13
+ const rect = el.getBoundingClientRect();
14
+ const style = getComputedStyle(el);
15
+ const viewportWidth = typeof window !== "undefined" ? window.innerWidth : 0;
16
+ const viewportHeight = typeof window !== "undefined" ? window.innerHeight : 0;
17
+ const inViewport = rect.x + rect.width > 0 && rect.y + rect.height > 0 && rect.x < viewportWidth && rect.y < viewportHeight;
18
+ if (!inViewport) return null;
19
+ const opacity = parseFloat(style.opacity || "1");
20
+ const displayNone = style.display === "none";
21
+ const hidden = style.visibility === "hidden";
22
+ const zeroSize = rect.width <= 0 || rect.height <= 0;
23
+ const visible = !displayNone && !hidden && !zeroSize;
24
+ const rawClasses = typeof el.className === "string" ? el.className.split(/\s+/).filter(Boolean) : Array.from(el.classList);
25
+ const classes = rawClasses.filter(c => !c.startsWith("locatorjs-"));
26
+ const disabled = el.disabled === true;
27
+ const hasElementChildren = el.children.length > 0;
28
+ const text = hasElementChildren ? undefined : truncateText(el.textContent);
29
+ return {
30
+ key: String(getReferenceId(el)),
31
+ tagName: el.tagName.toLowerCase(),
32
+ id: el.id || undefined,
33
+ classes,
34
+ x: rect.x,
35
+ y: rect.y,
36
+ width: rect.width,
37
+ height: rect.height,
38
+ visible,
39
+ opacity: Number.isFinite(opacity) ? opacity : 1,
40
+ inViewport,
41
+ pointerEvents: style.pointerEvents || "auto",
42
+ disabled,
43
+ text
44
+ };
45
+ }
46
+ export function takeSnapshot(root) {
47
+ if (typeof document === "undefined") return [];
48
+ const out = [];
49
+ const candidates = [];
50
+ if (root) {
51
+ if (root instanceof HTMLElement || root instanceof SVGElement) {
52
+ candidates.push(root);
53
+ }
54
+ const descendants = root.querySelectorAll("*");
55
+ for (let i = 0; i < descendants.length; i++) {
56
+ candidates.push(descendants[i]);
57
+ }
58
+ } else {
59
+ const all = document.querySelectorAll("*");
60
+ for (let i = 0; i < all.length; i++) {
61
+ candidates.push(all[i]);
62
+ }
63
+ }
64
+ for (let i = 0; i < candidates.length; i++) {
65
+ const el = candidates[i];
66
+ if (!(el instanceof HTMLElement) && !(el instanceof SVGElement)) continue;
67
+ if (el instanceof HTMLElement && isLocatorsOwnElement(el)) continue;
68
+ const snap = snapshotElement(el);
69
+ if (!snap) continue;
70
+ out.push(snap);
71
+ if (out.length >= MAX_SNAPSHOT_ELEMENTS) {
72
+ if (!warnedOverflow) {
73
+ warnedOverflow = true;
74
+ // eslint-disable-next-line no-console
75
+ console.warn(`[treelocator/visualDiff] snapshot capped at ${MAX_SNAPSHOT_ELEMENTS} elements`);
76
+ }
77
+ break;
78
+ }
79
+ }
80
+ return out;
81
+ }
82
+ export function __resetSnapshotWarningForTests() {
83
+ warnedOverflow = false;
84
+ }
@@ -0,0 +1 @@
1
+ export {};