@treelocator/runtime 0.4.7 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/.eslintignore +1 -0
  2. package/dist/_generated_styles.d.ts +1 -1
  3. package/dist/_generated_styles.js +20 -0
  4. package/dist/_generated_tree_icon.d.ts +1 -1
  5. package/dist/adapters/HtmlElementTreeNode.d.ts +2 -2
  6. package/dist/adapters/HtmlElementTreeNode.js +4 -6
  7. package/dist/adapters/createTreeNode.js +17 -44
  8. package/dist/adapters/detectFramework.d.ts +8 -0
  9. package/dist/adapters/detectFramework.js +25 -0
  10. package/dist/adapters/detectFramework.test.d.ts +1 -0
  11. package/dist/adapters/detectFramework.test.js +60 -0
  12. package/dist/adapters/jsx/jsxAdapter.js +54 -89
  13. package/dist/adapters/jsx/jsxAdapter.test.d.ts +1 -0
  14. package/dist/adapters/jsx/jsxAdapter.test.js +273 -0
  15. package/dist/adapters/nextjs/parseNextjsDataAttributes.js +1 -1
  16. package/dist/adapters/nextjs/parseNextjsDataAttributes.test.d.ts +1 -0
  17. package/dist/adapters/nextjs/parseNextjsDataAttributes.test.js +158 -0
  18. package/dist/adapters/react/findFiberByHtmlElement.d.ts +1 -1
  19. package/dist/adapters/react/findFiberByHtmlElement.js +1 -1
  20. package/dist/adapters/react/getAllParentsElementsAndRootComponent.js +4 -0
  21. package/dist/adapters/resolveAdapter.d.ts +1 -1
  22. package/dist/adapters/resolveAdapter.js +4 -8
  23. package/dist/adapters/svelte/svelteAdapter.test.d.ts +1 -0
  24. package/dist/adapters/svelte/svelteAdapter.test.js +280 -0
  25. package/dist/adapters/vue/vueAdapter.test.d.ts +1 -0
  26. package/dist/adapters/vue/vueAdapter.test.js +222 -0
  27. package/dist/browserApi.d.ts +148 -0
  28. package/dist/browserApi.js +146 -5
  29. package/dist/browserApi.test.d.ts +1 -0
  30. package/dist/browserApi.test.js +287 -0
  31. package/dist/components/RecordingPillButton.d.ts +11 -0
  32. package/dist/components/RecordingPillButton.js +202 -0
  33. package/dist/components/RecordingResults.d.ts +2 -0
  34. package/dist/components/RecordingResults.js +213 -78
  35. package/dist/components/Runtime.js +161 -554
  36. package/dist/components/SettingsPanel.d.ts +5 -0
  37. package/dist/components/SettingsPanel.js +312 -0
  38. package/dist/consoleCapture.d.ts +9 -0
  39. package/dist/consoleCapture.js +95 -0
  40. package/dist/dejitter/recorder.d.ts +7 -1
  41. package/dist/dejitter/recorder.js +64 -1
  42. package/dist/functions/cssRuleInspector.d.ts +83 -0
  43. package/dist/functions/cssRuleInspector.js +608 -0
  44. package/dist/functions/cssRuleInspector.test.d.ts +1 -0
  45. package/dist/functions/cssRuleInspector.test.js +439 -0
  46. package/dist/functions/deduplicateLabels.test.d.ts +1 -0
  47. package/dist/functions/deduplicateLabels.test.js +178 -0
  48. package/dist/functions/enrichAncestrySourceMaps.js +0 -1
  49. package/dist/functions/extractComputedStyles.d.ts +51 -0
  50. package/dist/functions/extractComputedStyles.js +447 -0
  51. package/dist/functions/extractComputedStyles.test.d.ts +1 -0
  52. package/dist/functions/extractComputedStyles.test.js +549 -0
  53. package/dist/functions/formatAncestryChain.d.ts +8 -0
  54. package/dist/functions/formatAncestryChain.js +21 -1
  55. package/dist/functions/formatAncestryChain.test.js +18 -0
  56. package/dist/functions/getUsableName.test.d.ts +1 -0
  57. package/dist/functions/getUsableName.test.js +219 -0
  58. package/dist/functions/isCombinationModifiersPressed.test.d.ts +1 -0
  59. package/dist/functions/isCombinationModifiersPressed.test.js +192 -0
  60. package/dist/functions/mergeRects.test.js +210 -1
  61. package/dist/functions/namedSnapshots.d.ts +52 -0
  62. package/dist/functions/namedSnapshots.js +161 -0
  63. package/dist/functions/namedSnapshots.test.d.ts +1 -0
  64. package/dist/functions/namedSnapshots.test.js +85 -0
  65. package/dist/functions/normalizeFilePath.test.d.ts +1 -0
  66. package/dist/functions/normalizeFilePath.test.js +66 -0
  67. package/dist/functions/parseDataId.test.d.ts +1 -0
  68. package/dist/functions/parseDataId.test.js +101 -0
  69. package/dist/hooks/getStorage.d.ts +3 -0
  70. package/dist/hooks/getStorage.js +17 -0
  71. package/dist/hooks/useEventListeners.d.ts +15 -0
  72. package/dist/hooks/useEventListeners.js +56 -0
  73. package/dist/hooks/useLocatorStorage.d.ts +18 -0
  74. package/dist/hooks/useLocatorStorage.js +41 -0
  75. package/dist/hooks/useLocatorStorage.test.d.ts +1 -0
  76. package/dist/hooks/useLocatorStorage.test.js +124 -0
  77. package/dist/hooks/useRecordingState.d.ts +43 -0
  78. package/dist/hooks/useRecordingState.js +387 -0
  79. package/dist/hooks/useSettings.d.ts +13 -0
  80. package/dist/hooks/useSettings.js +66 -0
  81. package/dist/index.d.ts +5 -2
  82. package/dist/index.js +4 -2
  83. package/dist/initRuntime.d.ts +3 -1
  84. package/dist/initRuntime.js +4 -1
  85. package/dist/mcpBridge.d.ts +61 -0
  86. package/dist/mcpBridge.js +534 -0
  87. package/dist/mcpBridge.test.d.ts +1 -0
  88. package/dist/mcpBridge.test.js +248 -0
  89. package/dist/output.css +20 -0
  90. package/dist/visualDiff/diff.d.ts +9 -0
  91. package/dist/visualDiff/diff.js +209 -0
  92. package/dist/visualDiff/diff.test.d.ts +1 -0
  93. package/dist/visualDiff/diff.test.js +253 -0
  94. package/dist/visualDiff/settle.d.ts +3 -0
  95. package/dist/visualDiff/settle.js +50 -0
  96. package/dist/visualDiff/settle.test.d.ts +1 -0
  97. package/dist/visualDiff/settle.test.js +65 -0
  98. package/dist/visualDiff/snapshot.d.ts +4 -0
  99. package/dist/visualDiff/snapshot.js +84 -0
  100. package/dist/visualDiff/snapshot.test.d.ts +1 -0
  101. package/dist/visualDiff/snapshot.test.js +245 -0
  102. package/dist/visualDiff/types.d.ts +37 -0
  103. package/dist/visualDiff/types.js +1 -0
  104. package/package.json +2 -2
  105. package/scripts/wrapCSS.js +1 -1
  106. package/scripts/wrapImage.js +1 -1
  107. package/src/_generated_styles.ts +21 -1
  108. package/src/_generated_tree_icon.ts +1 -1
  109. package/src/adapters/HtmlElementTreeNode.ts +10 -7
  110. package/src/adapters/createTreeNode.ts +12 -51
  111. package/src/adapters/detectFramework.test.ts +73 -0
  112. package/src/adapters/detectFramework.ts +28 -0
  113. package/src/adapters/jsx/jsxAdapter.test.ts +240 -0
  114. package/src/adapters/jsx/jsxAdapter.ts +53 -106
  115. package/src/adapters/nextjs/parseNextjsDataAttributes.test.ts +212 -0
  116. package/src/adapters/nextjs/parseNextjsDataAttributes.ts +1 -1
  117. package/src/adapters/react/findDebugSource.ts +5 -6
  118. package/src/adapters/react/findFiberByHtmlElement.ts +3 -3
  119. package/src/adapters/react/getAllParentsElementsAndRootComponent.ts +3 -0
  120. package/src/adapters/react/reactAdapter.ts +1 -2
  121. package/src/adapters/resolveAdapter.ts +4 -14
  122. package/src/adapters/svelte/svelteAdapter.test.ts +334 -0
  123. package/src/adapters/vue/vueAdapter.test.ts +259 -0
  124. package/src/browserApi.test.ts +329 -0
  125. package/src/browserApi.ts +351 -4
  126. package/src/components/RecordingPillButton.tsx +301 -0
  127. package/src/components/RecordingResults.tsx +114 -13
  128. package/src/components/Runtime.tsx +176 -621
  129. package/src/components/SettingsPanel.tsx +339 -0
  130. package/src/consoleCapture.ts +113 -0
  131. package/src/dejitter/recorder.ts +67 -3
  132. package/src/functions/cssRuleInspector.test.ts +517 -0
  133. package/src/functions/cssRuleInspector.ts +708 -0
  134. package/src/functions/deduplicateLabels.test.ts +115 -0
  135. package/src/functions/enrichAncestrySourceMaps.ts +6 -3
  136. package/src/functions/extractComputedStyles.test.ts +681 -0
  137. package/src/functions/extractComputedStyles.ts +768 -0
  138. package/src/functions/formatAncestryChain.test.ts +23 -1
  139. package/src/functions/formatAncestryChain.ts +22 -1
  140. package/src/functions/getUsableName.test.ts +242 -0
  141. package/src/functions/isCombinationModifiersPressed.test.ts +156 -0
  142. package/src/functions/mergeRects.test.ts +111 -1
  143. package/src/functions/namedSnapshots.test.ts +106 -0
  144. package/src/functions/namedSnapshots.ts +232 -0
  145. package/src/functions/normalizeFilePath.test.ts +80 -0
  146. package/src/functions/parseDataId.test.ts +125 -0
  147. package/src/hooks/getStorage.ts +26 -0
  148. package/src/hooks/useEventListeners.ts +97 -0
  149. package/src/hooks/useLocatorStorage.test.ts +127 -0
  150. package/src/hooks/useLocatorStorage.ts +60 -0
  151. package/src/hooks/useRecordingState.ts +516 -0
  152. package/src/hooks/useSettings.ts +83 -0
  153. package/src/index.ts +10 -5
  154. package/src/initRuntime.ts +5 -0
  155. package/src/mcpBridge.test.ts +260 -0
  156. package/src/mcpBridge.ts +677 -0
  157. package/src/visualDiff/diff.test.ts +167 -0
  158. package/src/visualDiff/diff.ts +242 -0
  159. package/src/visualDiff/settle.test.ts +77 -0
  160. package/src/visualDiff/settle.ts +62 -0
  161. package/src/visualDiff/snapshot.test.ts +200 -0
  162. package/src/visualDiff/snapshot.ts +119 -0
  163. package/src/visualDiff/types.ts +40 -0
  164. package/tsconfig.json +3 -1
  165. package/vitest.config.ts +18 -0
  166. package/jest.config.ts +0 -195
@@ -0,0 +1,248 @@
1
+ import { beforeEach, describe, expect, test, vi } from "vitest";
2
+ import { executeBridgeCommand } from "./mcpBridge";
3
+ import { clearConsoleEntries, installConsoleCapture } from "./consoleCapture";
4
+ function createApiMock() {
5
+ return {
6
+ getPath: vi.fn().mockResolvedValue("path"),
7
+ getAncestry: vi.fn().mockResolvedValue([{
8
+ elementName: "button"
9
+ }]),
10
+ getPathData: vi.fn().mockResolvedValue({
11
+ path: "path",
12
+ ancestry: [{
13
+ elementName: "button"
14
+ }]
15
+ }),
16
+ getStyles: vi.fn().mockReturnValue({
17
+ formatted: "styles",
18
+ snapshot: {}
19
+ }),
20
+ getCSSRules: vi.fn().mockReturnValue({
21
+ properties: []
22
+ }),
23
+ getCSSReport: vi.fn().mockReturnValue("report"),
24
+ takeSnapshot: vi.fn().mockReturnValue({
25
+ snapshotId: "baseline",
26
+ selector: "#b",
27
+ index: 0,
28
+ takenAt: "now",
29
+ propertyCount: 42,
30
+ boundingRect: {}
31
+ }),
32
+ getSnapshotDiff: vi.fn().mockReturnValue({
33
+ snapshotId: "baseline",
34
+ selector: "#b",
35
+ index: 0,
36
+ takenAt: "now",
37
+ formatted: "diff",
38
+ changes: [],
39
+ boundingRectChanges: []
40
+ }),
41
+ clearSnapshot: vi.fn(),
42
+ help: vi.fn().mockReturnValue("help"),
43
+ replay: vi.fn(),
44
+ replayWithRecord: vi.fn().mockResolvedValue(null),
45
+ diff: {
46
+ snapshot: vi.fn().mockReturnValue([]),
47
+ computeDiff: vi.fn(),
48
+ captureDiff: vi.fn()
49
+ }
50
+ };
51
+ }
52
+ describe("mcpBridge executeBridgeCommand", () => {
53
+ test("dispatches get_path to browser API", async () => {
54
+ const api = createApiMock();
55
+ const result = await executeBridgeCommand(api, "get_path", {
56
+ selector: "#target"
57
+ });
58
+ expect(api.getPath).toHaveBeenCalledWith("#target");
59
+ expect(result).toBe("path");
60
+ });
61
+ test("click uses selector + index", async () => {
62
+ document.body.innerHTML = `
63
+ <button class="target">one</button>
64
+ <button class="target">two</button>
65
+ `;
66
+ const buttons = Array.from(document.querySelectorAll(".target"));
67
+ const clickSpy = vi.fn();
68
+ buttons[1]?.addEventListener("click", clickSpy);
69
+ const api = createApiMock();
70
+ const result = await executeBridgeCommand(api, "click", {
71
+ selector: ".target",
72
+ index: 1
73
+ });
74
+ expect(result).toEqual({
75
+ ok: true
76
+ });
77
+ expect(clickSpy).toHaveBeenCalledTimes(1);
78
+ });
79
+ test("hover dispatches mouse events", async () => {
80
+ document.body.innerHTML = `<div id="hover-me"></div>`;
81
+ const element = document.getElementById("hover-me");
82
+ expect(element).not.toBeNull();
83
+ const events = [];
84
+ element?.addEventListener("mouseenter", () => events.push("mouseenter"));
85
+ element?.addEventListener("mouseover", () => events.push("mouseover"));
86
+ element?.addEventListener("mousemove", () => events.push("mousemove"));
87
+ const api = createApiMock();
88
+ await executeBridgeCommand(api, "hover", {
89
+ selector: "#hover-me"
90
+ });
91
+ expect(events).toEqual(["mouseenter", "mouseover", "mousemove"]);
92
+ });
93
+ test("type updates input value and dispatches input event", async () => {
94
+ document.body.innerHTML = `<input id="field" />`;
95
+ const input = document.getElementById("field");
96
+ const inputEventSpy = vi.fn();
97
+ input.addEventListener("input", inputEventSpy);
98
+ const api = createApiMock();
99
+ const result = await executeBridgeCommand(api, "type", {
100
+ selector: "#field",
101
+ text: "hello"
102
+ });
103
+ expect(input.value).toBe("hello");
104
+ expect(result).toEqual({
105
+ value: "hello"
106
+ });
107
+ expect(inputEventSpy).toHaveBeenCalledTimes(1);
108
+ });
109
+ test("take_snapshot forwards selector + snapshotId to browser API", async () => {
110
+ const api = createApiMock();
111
+ const result = await executeBridgeCommand(api, "take_snapshot", {
112
+ selector: "#b",
113
+ snapshotId: "baseline",
114
+ index: 2,
115
+ label: "before fix"
116
+ });
117
+ expect(api.takeSnapshot).toHaveBeenCalledWith("#b", "baseline", {
118
+ index: 2,
119
+ label: "before fix"
120
+ });
121
+ expect(result).toMatchObject({
122
+ snapshotId: "baseline"
123
+ });
124
+ });
125
+ test("take_snapshot rejects missing snapshotId", async () => {
126
+ const api = createApiMock();
127
+ await expect(executeBridgeCommand(api, "take_snapshot", {
128
+ selector: "#b"
129
+ })).rejects.toThrow("snapshotId is required");
130
+ });
131
+ test("get_snapshot_diff forwards snapshotId to browser API", async () => {
132
+ const api = createApiMock();
133
+ const result = await executeBridgeCommand(api, "get_snapshot_diff", {
134
+ snapshotId: "baseline"
135
+ });
136
+ expect(api.getSnapshotDiff).toHaveBeenCalledWith("baseline");
137
+ expect(result).toMatchObject({
138
+ snapshotId: "baseline",
139
+ formatted: "diff"
140
+ });
141
+ });
142
+ test("clear_snapshot forwards snapshotId to browser API", async () => {
143
+ const api = createApiMock();
144
+ const result = await executeBridgeCommand(api, "clear_snapshot", {
145
+ snapshotId: "baseline"
146
+ });
147
+ expect(api.clearSnapshot).toHaveBeenCalledWith("baseline");
148
+ expect(result).toEqual({
149
+ ok: true
150
+ });
151
+ });
152
+ describe("execute_js", () => {
153
+ test("returns serialized value from async function body", async () => {
154
+ const api = createApiMock();
155
+ const result = await executeBridgeCommand(api, "execute_js", {
156
+ code: "return 1 + 2;"
157
+ });
158
+ expect(result).toEqual({
159
+ type: "number",
160
+ value: 3
161
+ });
162
+ });
163
+ test("awaits returned promise", async () => {
164
+ const api = createApiMock();
165
+ const result = await executeBridgeCommand(api, "execute_js", {
166
+ code: "return await Promise.resolve({ hello: 'world' });"
167
+ });
168
+ expect(result).toEqual({
169
+ type: "object",
170
+ value: {
171
+ hello: "world"
172
+ }
173
+ });
174
+ });
175
+ test("can read from the document", async () => {
176
+ document.body.innerHTML = `<div id="greeting">hi</div>`;
177
+ const api = createApiMock();
178
+ const result = await executeBridgeCommand(api, "execute_js", {
179
+ code: "return document.getElementById('greeting').textContent;"
180
+ });
181
+ expect(result).toEqual({
182
+ type: "string",
183
+ value: "hi"
184
+ });
185
+ });
186
+ test("wraps runtime errors with stack details", async () => {
187
+ const api = createApiMock();
188
+ await expect(executeBridgeCommand(api, "execute_js", {
189
+ code: "throw new Error('boom');"
190
+ })).rejects.toThrow("boom");
191
+ });
192
+ test("rejects compile errors", async () => {
193
+ const api = createApiMock();
194
+ await expect(executeBridgeCommand(api, "execute_js", {
195
+ code: "return ("
196
+ })).rejects.toThrow();
197
+ });
198
+ test("rejects empty code", async () => {
199
+ const api = createApiMock();
200
+ await expect(executeBridgeCommand(api, "execute_js", {
201
+ code: ""
202
+ })).rejects.toThrow("code is required");
203
+ });
204
+ });
205
+ describe("get_console", () => {
206
+ beforeEach(() => {
207
+ installConsoleCapture();
208
+ clearConsoleEntries();
209
+ });
210
+ test("captures console.log and returns last N entries", async () => {
211
+ console.log("first");
212
+ console.warn("second");
213
+ console.error("third");
214
+ const api = createApiMock();
215
+ const result = await executeBridgeCommand(api, "get_console", {
216
+ last: 2
217
+ });
218
+ expect(result.count).toBe(2);
219
+ expect(result.entries.map(e => e.level)).toEqual(["warn", "error"]);
220
+ expect(result.entries.map(e => e.message)).toEqual(["second", "third"]);
221
+ });
222
+ test("returns all entries when last is omitted", async () => {
223
+ console.log("a");
224
+ console.log("b");
225
+ const api = createApiMock();
226
+ const result = await executeBridgeCommand(api, "get_console", {});
227
+ expect(result.count).toBe(2);
228
+ });
229
+ test("formats multiple args joined by spaces", async () => {
230
+ console.log("count is", 42, {
231
+ ok: true
232
+ });
233
+ const api = createApiMock();
234
+ const result = await executeBridgeCommand(api, "get_console", {
235
+ last: 1
236
+ });
237
+ expect(result.entries[0]?.message).toBe('count is 42 {"ok":true}');
238
+ });
239
+ });
240
+ test("type rejects unsupported elements", async () => {
241
+ document.body.innerHTML = `<div id="not-input"></div>`;
242
+ const api = createApiMock();
243
+ await expect(executeBridgeCommand(api, "type", {
244
+ selector: "#not-input",
245
+ text: "hello"
246
+ })).rejects.toThrow("type target must be input, textarea, or contenteditable");
247
+ });
248
+ });
package/dist/output.css CHANGED
@@ -815,6 +815,10 @@ input:where([type='file']):focus {
815
815
  visibility: collapse;
816
816
  }
817
817
 
818
+ .static {
819
+ position: static;
820
+ }
821
+
818
822
  .fixed {
819
823
  position: fixed;
820
824
  }
@@ -891,6 +895,10 @@ input:where([type='file']):focus {
891
895
  top: 0.25rem;
892
896
  }
893
897
 
898
+ .isolate {
899
+ isolation: isolate;
900
+ }
901
+
894
902
  .z-10 {
895
903
  z-index: 10;
896
904
  }
@@ -981,6 +989,10 @@ input:where([type='file']):focus {
981
989
  display: contents;
982
990
  }
983
991
 
992
+ .\!hidden {
993
+ display: none !important;
994
+ }
995
+
984
996
  .hidden {
985
997
  display: none;
986
998
  }
@@ -1113,6 +1125,10 @@ input:where([type='file']):focus {
1113
1125
  flex-direction: column;
1114
1126
  }
1115
1127
 
1128
+ .flex-wrap {
1129
+ flex-wrap: wrap;
1130
+ }
1131
+
1116
1132
  .items-center {
1117
1133
  align-items: center;
1118
1134
  }
@@ -1558,6 +1574,10 @@ input:where([type='file']):focus {
1558
1574
  text-transform: lowercase;
1559
1575
  }
1560
1576
 
1577
+ .italic {
1578
+ font-style: italic;
1579
+ }
1580
+
1561
1581
  .text-black {
1562
1582
  --tw-text-opacity: 1;
1563
1583
  color: rgb(0 0 0 / var(--tw-text-opacity, 1));
@@ -0,0 +1,9 @@
1
+ import type { DeltaEntry, DeltaReport, ElementSnapshot } from "./types";
2
+ export declare const POS_THRESHOLD = 4;
3
+ export declare const SIZE_THRESHOLD = 2;
4
+ export declare const OPACITY_THRESHOLD = 0.05;
5
+ export declare function computeDiff(before: ElementSnapshot[], after: ElementSnapshot[]): DeltaReport;
6
+ export declare function formatReport(entries: DeltaEntry[], meta: {
7
+ elapsedMs: number;
8
+ settle: "clean" | "timeout";
9
+ }): string;
@@ -0,0 +1,209 @@
1
+ export const POS_THRESHOLD = 4;
2
+ export const SIZE_THRESHOLD = 2;
3
+ export const OPACITY_THRESHOLD = 0.05;
4
+ function arrayEq(a, b) {
5
+ if (a.length !== b.length) return false;
6
+ const set = new Set(b);
7
+ for (const v of a) if (!set.has(v)) return false;
8
+ return true;
9
+ }
10
+ function labelFor(snap) {
11
+ let label = snap.tagName;
12
+ if (snap.id) label += "#" + snap.id;
13
+ if (snap.classes.length > 0) label += "." + snap.classes[0];
14
+ if (snap.text) {
15
+ const t = snap.text.length > 30 ? snap.text.slice(0, 30) + "…" : snap.text;
16
+ label += ` "${t}"`;
17
+ }
18
+ return label;
19
+ }
20
+ function comparePair(before, after) {
21
+ const changed = [];
22
+ if (Math.abs(after.x - before.x) >= POS_THRESHOLD) changed.push("x");
23
+ if (Math.abs(after.y - before.y) >= POS_THRESHOLD) changed.push("y");
24
+ if (Math.abs(after.width - before.width) >= SIZE_THRESHOLD) {
25
+ changed.push("width");
26
+ }
27
+ if (Math.abs(after.height - before.height) >= SIZE_THRESHOLD) {
28
+ changed.push("height");
29
+ }
30
+ if (Math.abs(after.opacity - before.opacity) >= OPACITY_THRESHOLD) {
31
+ changed.push("opacity");
32
+ }
33
+ if (after.text !== before.text) changed.push("text");
34
+ if (after.disabled !== before.disabled) changed.push("disabled");
35
+ if (after.pointerEvents !== before.pointerEvents) {
36
+ changed.push("pointerEvents");
37
+ }
38
+ if (!arrayEq(after.classes, before.classes)) changed.push("classes");
39
+ if (after.inViewport !== before.inViewport) changed.push("inViewport");
40
+ return {
41
+ changed
42
+ };
43
+ }
44
+ export function computeDiff(before, after) {
45
+ const beforeMap = new Map();
46
+ const afterMap = new Map();
47
+ for (const s of before) beforeMap.set(s.key, s);
48
+ for (const s of after) afterMap.set(s.key, s);
49
+ const entries = [];
50
+ let added = 0;
51
+ let removed = 0;
52
+ let changed = 0;
53
+ let moved = 0;
54
+ for (const [key, afterSnap] of afterMap) {
55
+ const beforeSnap = beforeMap.get(key);
56
+ if (!beforeSnap) {
57
+ if (afterSnap.visible) {
58
+ entries.push({
59
+ type: "+",
60
+ key,
61
+ label: labelFor(afterSnap),
62
+ after: afterSnap
63
+ });
64
+ added++;
65
+ }
66
+ continue;
67
+ }
68
+ if (beforeSnap.visible && !afterSnap.visible) {
69
+ entries.push({
70
+ type: "-",
71
+ key,
72
+ label: labelFor(beforeSnap),
73
+ before: beforeSnap
74
+ });
75
+ removed++;
76
+ continue;
77
+ }
78
+ const {
79
+ changed: changedFields
80
+ } = comparePair(beforeSnap, afterSnap);
81
+ if (changedFields.length === 0) continue;
82
+ const onlyPosition = changedFields.length > 0 && changedFields.every(f => f === "x" || f === "y");
83
+ if (onlyPosition) {
84
+ entries.push({
85
+ type: "→",
86
+ key,
87
+ label: labelFor(afterSnap),
88
+ before: beforeSnap,
89
+ after: afterSnap,
90
+ changedFields
91
+ });
92
+ moved++;
93
+ } else {
94
+ entries.push({
95
+ type: "~",
96
+ key,
97
+ label: labelFor(afterSnap),
98
+ before: beforeSnap,
99
+ after: afterSnap,
100
+ changedFields
101
+ });
102
+ changed++;
103
+ }
104
+ }
105
+ for (const [key, beforeSnap] of beforeMap) {
106
+ if (afterMap.has(key)) continue;
107
+ if (!beforeSnap.visible) continue;
108
+ entries.push({
109
+ type: "-",
110
+ key,
111
+ label: labelFor(beforeSnap),
112
+ before: beforeSnap
113
+ });
114
+ removed++;
115
+ }
116
+ const meta = {
117
+ elapsedMs: 0,
118
+ settle: "clean"
119
+ };
120
+ const text = formatReport(entries, meta);
121
+ return {
122
+ elapsedMs: 0,
123
+ settle: "clean",
124
+ counts: {
125
+ added,
126
+ removed,
127
+ changed,
128
+ moved
129
+ },
130
+ entries,
131
+ text
132
+ };
133
+ }
134
+ function fmtNum(n) {
135
+ return Math.round(n).toString();
136
+ }
137
+ function fieldDelta(field, before, after) {
138
+ switch (field) {
139
+ case "x":
140
+ return `x ${fmtNum(before.x)}→${fmtNum(after.x)}`;
141
+ case "y":
142
+ return `y ${fmtNum(before.y)}→${fmtNum(after.y)}`;
143
+ case "width":
144
+ return `w ${fmtNum(before.width)}→${fmtNum(after.width)}`;
145
+ case "height":
146
+ return `h ${fmtNum(before.height)}→${fmtNum(after.height)}`;
147
+ case "opacity":
148
+ return `opacity ${before.opacity.toFixed(2)}→${after.opacity.toFixed(2)}`;
149
+ case "text":
150
+ return `text "${before.text ?? ""}"→"${after.text ?? ""}"`;
151
+ case "disabled":
152
+ return `disabled ${before.disabled}→${after.disabled}`;
153
+ case "pointerEvents":
154
+ return `pointer-events ${before.pointerEvents}→${after.pointerEvents}`;
155
+ case "classes":
156
+ return `classes [${before.classes.join(" ")}]→[${after.classes.join(" ")}]`;
157
+ case "inViewport":
158
+ return `inViewport ${before.inViewport}→${after.inViewport}`;
159
+ default:
160
+ return field;
161
+ }
162
+ }
163
+ export function formatReport(entries, meta) {
164
+ const header = `Visual diff (${Math.round(meta.elapsedMs)}ms, settle: ${meta.settle})`;
165
+ const divider = "────────────────────────────────";
166
+ if (entries.length === 0) {
167
+ return `${header}\n${divider}\n(no changes)\n${divider}`;
168
+ }
169
+ const lines = [header, divider];
170
+ let added = 0;
171
+ let removed = 0;
172
+ let changed = 0;
173
+ let moved = 0;
174
+ for (const entry of entries) {
175
+ switch (entry.type) {
176
+ case "+":
177
+ {
178
+ added++;
179
+ const s = entry.after;
180
+ lines.push(`+ ${entry.label} (${fmtNum(s.x)},${fmtNum(s.y)}) ${fmtNum(s.width)}×${fmtNum(s.height)}`);
181
+ break;
182
+ }
183
+ case "-":
184
+ {
185
+ removed++;
186
+ lines.push(`- ${entry.label}`);
187
+ break;
188
+ }
189
+ case "~":
190
+ {
191
+ changed++;
192
+ const deltas = (entry.changedFields ?? []).map(f => fieldDelta(f, entry.before, entry.after)).join(", ");
193
+ lines.push(`~ ${entry.label}: ${deltas}`);
194
+ break;
195
+ }
196
+ case "→":
197
+ {
198
+ moved++;
199
+ const b = entry.before;
200
+ const a = entry.after;
201
+ lines.push(`→ ${entry.label}: (${fmtNum(b.x)},${fmtNum(b.y)})→(${fmtNum(a.x)},${fmtNum(a.y)})`);
202
+ break;
203
+ }
204
+ }
205
+ }
206
+ lines.push(divider);
207
+ lines.push(`${entries.length} changes: ${added} added, ${removed} removed, ${changed} changed, ${moved} moved`);
208
+ return lines.join("\n");
209
+ }
@@ -0,0 +1 @@
1
+ export {};