@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,219 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { getUsableName } from "./getUsableName";
3
+ describe("getUsableName", () => {
4
+ test("returns 'Not found' when fiber is null", () => {
5
+ expect(getUsableName(null)).toBe("Not found");
6
+ });
7
+ test("returns 'Not found' when fiber is undefined", () => {
8
+ expect(getUsableName(undefined)).toBe("Not found");
9
+ });
10
+ test("returns string elementType directly", () => {
11
+ const fiber = {
12
+ elementType: "div"
13
+ };
14
+ expect(getUsableName(fiber)).toBe("div");
15
+ });
16
+ test("returns 'Anonymous' when elementType is null", () => {
17
+ const fiber = {
18
+ elementType: null
19
+ };
20
+ expect(getUsableName(fiber)).toBe("Anonymous");
21
+ });
22
+ test("returns elementType.name when available", () => {
23
+ const fiber = {
24
+ elementType: {
25
+ name: "MyComponent"
26
+ }
27
+ };
28
+ expect(getUsableName(fiber)).toBe("MyComponent");
29
+ });
30
+ test("returns elementType.displayName when name unavailable", () => {
31
+ const fiber = {
32
+ elementType: {
33
+ displayName: "MyComponent"
34
+ }
35
+ };
36
+ expect(getUsableName(fiber)).toBe("MyComponent");
37
+ });
38
+ test("prefers name over displayName", () => {
39
+ const fiber = {
40
+ elementType: {
41
+ name: "FromName",
42
+ displayName: "FromDisplayName"
43
+ }
44
+ };
45
+ expect(getUsableName(fiber)).toBe("FromName");
46
+ });
47
+ test("handles React.memo with type.name", () => {
48
+ const fiber = {
49
+ elementType: {
50
+ type: {
51
+ name: "MemoizedComponent"
52
+ }
53
+ }
54
+ };
55
+ expect(getUsableName(fiber)).toBe("MemoizedComponent");
56
+ });
57
+ test("handles React.memo with type.displayName", () => {
58
+ const fiber = {
59
+ elementType: {
60
+ type: {
61
+ displayName: "MemoizedComponent"
62
+ }
63
+ }
64
+ };
65
+ expect(getUsableName(fiber)).toBe("MemoizedComponent");
66
+ });
67
+ test("prefers type.name over type.displayName", () => {
68
+ const fiber = {
69
+ elementType: {
70
+ type: {
71
+ name: "FromName",
72
+ displayName: "FromDisplayName"
73
+ }
74
+ }
75
+ };
76
+ expect(getUsableName(fiber)).toBe("FromName");
77
+ });
78
+ test("handles React.forwardRef with render.name", () => {
79
+ const fiber = {
80
+ elementType: {
81
+ render: {
82
+ name: "ForwardRefComponent"
83
+ }
84
+ }
85
+ };
86
+ expect(getUsableName(fiber)).toBe("ForwardRefComponent");
87
+ });
88
+ test("handles React.forwardRef with render.displayName", () => {
89
+ const fiber = {
90
+ elementType: {
91
+ render: {
92
+ displayName: "ForwardRefComponent"
93
+ }
94
+ }
95
+ };
96
+ expect(getUsableName(fiber)).toBe("ForwardRefComponent");
97
+ });
98
+ test("prefers render.name over render.displayName", () => {
99
+ const fiber = {
100
+ elementType: {
101
+ render: {
102
+ name: "FromName",
103
+ displayName: "FromDisplayName"
104
+ }
105
+ }
106
+ };
107
+ expect(getUsableName(fiber)).toBe("FromName");
108
+ });
109
+ test("handles React.lazy with _payload._result.name", () => {
110
+ const fiber = {
111
+ elementType: {
112
+ _payload: {
113
+ _result: {
114
+ name: "LazyComponent"
115
+ }
116
+ }
117
+ }
118
+ };
119
+ expect(getUsableName(fiber)).toBe("LazyComponent");
120
+ });
121
+ test("handles React.lazy with _payload._result.displayName", () => {
122
+ const fiber = {
123
+ elementType: {
124
+ _payload: {
125
+ _result: {
126
+ displayName: "LazyComponent"
127
+ }
128
+ }
129
+ }
130
+ };
131
+ expect(getUsableName(fiber)).toBe("LazyComponent");
132
+ });
133
+ test("prefers _payload._result.name over displayName", () => {
134
+ const fiber = {
135
+ elementType: {
136
+ _payload: {
137
+ _result: {
138
+ name: "FromName",
139
+ displayName: "FromDisplayName"
140
+ }
141
+ }
142
+ }
143
+ };
144
+ expect(getUsableName(fiber)).toBe("FromName");
145
+ });
146
+ test("falls back to fiber.type.name when elementType doesn't have name", () => {
147
+ const fiber = {
148
+ elementType: {},
149
+ type: {
150
+ name: "TypeComponent"
151
+ }
152
+ };
153
+ expect(getUsableName(fiber)).toBe("TypeComponent");
154
+ });
155
+ test("ignores fiber.type when it equals elementType", () => {
156
+ const elementType = {
157
+ name: "Component"
158
+ };
159
+ const fiber = {
160
+ elementType,
161
+ type: elementType
162
+ };
163
+ expect(getUsableName(fiber)).toBe("Component");
164
+ });
165
+ test("ignores fiber.type when it is a string", () => {
166
+ const fiber = {
167
+ elementType: {},
168
+ type: "div"
169
+ };
170
+ expect(getUsableName(fiber)).toBe("Anonymous");
171
+ });
172
+ test("returns 'Anonymous' when all fallbacks fail", () => {
173
+ const fiber = {
174
+ elementType: {},
175
+ type: {}
176
+ };
177
+ expect(getUsableName(fiber)).toBe("Anonymous");
178
+ });
179
+ test("follows priority order correctly", () => {
180
+ // elementType.name should be used before type.name
181
+ const fiber = {
182
+ elementType: {
183
+ name: "ElementTypeComponent"
184
+ },
185
+ type: {
186
+ name: "TypeComponent"
187
+ }
188
+ };
189
+ expect(getUsableName(fiber)).toBe("ElementTypeComponent");
190
+ });
191
+ test("handles deeply nested fiber with multiple name sources", () => {
192
+ const fiber = {
193
+ elementType: {
194
+ render: {
195
+ name: "VeryNested"
196
+ }
197
+ }
198
+ };
199
+ expect(getUsableName(fiber)).toBe("VeryNested");
200
+ });
201
+ test("handles missing _payload gracefully", () => {
202
+ const fiber = {
203
+ elementType: {
204
+ _payload: null
205
+ }
206
+ };
207
+ expect(getUsableName(fiber)).toBe("Anonymous");
208
+ });
209
+ test("handles missing _result in _payload gracefully", () => {
210
+ const fiber = {
211
+ elementType: {
212
+ _payload: {
213
+ _result: null
214
+ }
215
+ }
216
+ };
217
+ expect(getUsableName(fiber)).toBe("Anonymous");
218
+ });
219
+ });
@@ -0,0 +1,192 @@
1
+ import { describe, expect, test, beforeEach, afterEach } from "vitest";
2
+ import { isCombinationModifiersPressed, getMouseModifiers } from "./isCombinationModifiersPressed";
3
+ describe("getMouseModifiers", () => {
4
+ afterEach(() => {
5
+ delete document.documentElement.dataset.locatorMouseModifiers;
6
+ });
7
+ test("returns default alt modifier when not set", () => {
8
+ const modifiers = getMouseModifiers();
9
+ expect(modifiers).toEqual({
10
+ alt: true
11
+ });
12
+ });
13
+ test("parses single modifier from dataset", () => {
14
+ document.documentElement.dataset.locatorMouseModifiers = "ctrl";
15
+ const modifiers = getMouseModifiers();
16
+ expect(modifiers).toEqual({
17
+ ctrl: true
18
+ });
19
+ });
20
+ test("parses multiple modifiers separated by +", () => {
21
+ document.documentElement.dataset.locatorMouseModifiers = "ctrl+shift";
22
+ const modifiers = getMouseModifiers();
23
+ expect(modifiers).toEqual({
24
+ ctrl: true,
25
+ shift: true
26
+ });
27
+ });
28
+ test("parses three modifiers", () => {
29
+ document.documentElement.dataset.locatorMouseModifiers = "ctrl+alt+shift";
30
+ const modifiers = getMouseModifiers();
31
+ expect(modifiers).toEqual({
32
+ ctrl: true,
33
+ alt: true,
34
+ shift: true
35
+ });
36
+ });
37
+ test("parses meta modifier", () => {
38
+ document.documentElement.dataset.locatorMouseModifiers = "meta";
39
+ const modifiers = getMouseModifiers();
40
+ expect(modifiers).toEqual({
41
+ meta: true
42
+ });
43
+ });
44
+ });
45
+ describe("isCombinationModifiersPressed", () => {
46
+ afterEach(() => {
47
+ delete document.documentElement.dataset.locatorMouseModifiers;
48
+ });
49
+ describe("default alt modifier", () => {
50
+ test("returns true when altKey is true", () => {
51
+ const event = new KeyboardEvent("keydown", {
52
+ altKey: true
53
+ });
54
+ expect(isCombinationModifiersPressed(event)).toBe(true);
55
+ });
56
+ test("returns false when altKey is false", () => {
57
+ const event = new KeyboardEvent("keydown", {
58
+ altKey: false
59
+ });
60
+ expect(isCombinationModifiersPressed(event)).toBe(false);
61
+ });
62
+ test("returns false when ctrlKey is true", () => {
63
+ const event = new KeyboardEvent("keydown", {
64
+ altKey: true,
65
+ ctrlKey: true
66
+ });
67
+ expect(isCombinationModifiersPressed(event)).toBe(false);
68
+ });
69
+ test("returns false when ctrlKey is true but altKey is false", () => {
70
+ const event = new KeyboardEvent("keydown", {
71
+ altKey: false,
72
+ ctrlKey: true
73
+ });
74
+ expect(isCombinationModifiersPressed(event)).toBe(false);
75
+ });
76
+ test("returns false when metaKey is true but altKey is false", () => {
77
+ const event = new KeyboardEvent("keydown", {
78
+ altKey: false,
79
+ metaKey: true
80
+ });
81
+ expect(isCombinationModifiersPressed(event)).toBe(false);
82
+ });
83
+ });
84
+ describe("custom ctrl+alt modifier", () => {
85
+ beforeEach(() => {
86
+ document.documentElement.dataset.locatorMouseModifiers = "ctrl+alt";
87
+ });
88
+ test("returns true when both ctrl and alt are true", () => {
89
+ const event = new KeyboardEvent("keydown", {
90
+ ctrlKey: true,
91
+ altKey: true
92
+ });
93
+ expect(isCombinationModifiersPressed(event)).toBe(true);
94
+ });
95
+ test("returns false when only ctrl is true", () => {
96
+ const event = new KeyboardEvent("keydown", {
97
+ ctrlKey: true,
98
+ altKey: false
99
+ });
100
+ expect(isCombinationModifiersPressed(event)).toBe(false);
101
+ });
102
+ test("returns false when only alt is true", () => {
103
+ const event = new KeyboardEvent("keydown", {
104
+ ctrlKey: false,
105
+ altKey: true
106
+ });
107
+ expect(isCombinationModifiersPressed(event)).toBe(false);
108
+ });
109
+ test("returns false when neither ctrl nor alt are true", () => {
110
+ const event = new KeyboardEvent("keydown", {
111
+ ctrlKey: false,
112
+ altKey: false
113
+ });
114
+ expect(isCombinationModifiersPressed(event)).toBe(false);
115
+ });
116
+ });
117
+ describe("shift modifier handling", () => {
118
+ test("shift not configured allows shift to be pressed without affecting result", () => {
119
+ document.documentElement.dataset.locatorMouseModifiers = "alt";
120
+ const event = new KeyboardEvent("keydown", {
121
+ altKey: true,
122
+ shiftKey: true
123
+ });
124
+ expect(isCombinationModifiersPressed(event)).toBe(true);
125
+ });
126
+ test("shift configured requires shift to be pressed", () => {
127
+ document.documentElement.dataset.locatorMouseModifiers = "alt+shift";
128
+ const event = new KeyboardEvent("keydown", {
129
+ altKey: true,
130
+ shiftKey: false
131
+ });
132
+ expect(isCombinationModifiersPressed(event)).toBe(false);
133
+ });
134
+ test("shift configured returns true when shift is pressed", () => {
135
+ document.documentElement.dataset.locatorMouseModifiers = "alt+shift";
136
+ const event = new KeyboardEvent("keydown", {
137
+ altKey: true,
138
+ shiftKey: true
139
+ });
140
+ expect(isCombinationModifiersPressed(event)).toBe(true);
141
+ });
142
+ });
143
+ describe("rightClick parameter", () => {
144
+ beforeEach(() => {
145
+ document.documentElement.dataset.locatorMouseModifiers = "alt";
146
+ });
147
+ test("rightClick=true ignores ctrlKey requirement", () => {
148
+ const event = new KeyboardEvent("keydown", {
149
+ altKey: true,
150
+ ctrlKey: false
151
+ });
152
+ expect(isCombinationModifiersPressed(event, true)).toBe(true);
153
+ });
154
+ test("rightClick=true still requires alt", () => {
155
+ const event = new KeyboardEvent("keydown", {
156
+ altKey: false,
157
+ metaKey: false
158
+ });
159
+ expect(isCombinationModifiersPressed(event, true)).toBe(false);
160
+ });
161
+ test("rightClick=true checks metaKey", () => {
162
+ const event = new KeyboardEvent("keydown", {
163
+ altKey: true,
164
+ metaKey: true
165
+ });
166
+ expect(isCombinationModifiersPressed(event, true)).toBe(false);
167
+ });
168
+ test("rightClick=true with meta modifier configured", () => {
169
+ document.documentElement.dataset.locatorMouseModifiers = "meta";
170
+ const event = new KeyboardEvent("keydown", {
171
+ altKey: false,
172
+ metaKey: true
173
+ });
174
+ expect(isCombinationModifiersPressed(event, true)).toBe(true);
175
+ });
176
+ });
177
+ describe("MouseEvent support", () => {
178
+ test("works with MouseEvent", () => {
179
+ const event = new MouseEvent("click", {
180
+ altKey: true
181
+ });
182
+ expect(isCombinationModifiersPressed(event)).toBe(true);
183
+ });
184
+ test("rightClick parameter works with MouseEvent", () => {
185
+ document.documentElement.dataset.locatorMouseModifiers = "alt";
186
+ const event = new MouseEvent("click", {
187
+ altKey: true
188
+ });
189
+ expect(isCombinationModifiersPressed(event, true)).toBe(true);
190
+ });
191
+ });
192
+ });
@@ -1,7 +1,7 @@
1
1
  import { mergeRects } from "./mergeRects";
2
2
  import { describe, expect, test } from "vitest";
3
3
  describe("mergeRects", () => {
4
- test("basic", () => {
4
+ test("basic overlapping rects", () => {
5
5
  const a = {
6
6
  x: 0,
7
7
  y: 0,
@@ -20,4 +20,213 @@ describe("mergeRects", () => {
20
20
  expect(res.width).toEqual(15);
21
21
  expect(res.height).toEqual(15);
22
22
  });
23
+ test("non-overlapping rects side by side", () => {
24
+ const a = {
25
+ x: 0,
26
+ y: 0,
27
+ width: 10,
28
+ height: 10
29
+ };
30
+ const b = {
31
+ x: 10,
32
+ y: 0,
33
+ width: 10,
34
+ height: 10
35
+ };
36
+ const res = mergeRects(a, b);
37
+ expect(res.x).toEqual(0);
38
+ expect(res.y).toEqual(0);
39
+ expect(res.width).toEqual(20);
40
+ expect(res.height).toEqual(10);
41
+ });
42
+ test("non-overlapping rects vertically separated", () => {
43
+ const a = {
44
+ x: 0,
45
+ y: 0,
46
+ width: 10,
47
+ height: 10
48
+ };
49
+ const b = {
50
+ x: 0,
51
+ y: 10,
52
+ width: 10,
53
+ height: 10
54
+ };
55
+ const res = mergeRects(a, b);
56
+ expect(res.x).toEqual(0);
57
+ expect(res.y).toEqual(0);
58
+ expect(res.width).toEqual(10);
59
+ expect(res.height).toEqual(20);
60
+ });
61
+ test("identical rects", () => {
62
+ const a = {
63
+ x: 5,
64
+ y: 5,
65
+ width: 10,
66
+ height: 10
67
+ };
68
+ const b = {
69
+ x: 5,
70
+ y: 5,
71
+ width: 10,
72
+ height: 10
73
+ };
74
+ const res = mergeRects(a, b);
75
+ expect(res.x).toEqual(5);
76
+ expect(res.y).toEqual(5);
77
+ expect(res.width).toEqual(10);
78
+ expect(res.height).toEqual(10);
79
+ });
80
+ test("one rect fully contains another", () => {
81
+ const a = {
82
+ x: 0,
83
+ y: 0,
84
+ width: 20,
85
+ height: 20
86
+ };
87
+ const b = {
88
+ x: 5,
89
+ y: 5,
90
+ width: 10,
91
+ height: 10
92
+ };
93
+ const res = mergeRects(a, b);
94
+ expect(res.x).toEqual(0);
95
+ expect(res.y).toEqual(0);
96
+ expect(res.width).toEqual(20);
97
+ expect(res.height).toEqual(20);
98
+ });
99
+ test("small rect contains large rect", () => {
100
+ const a = {
101
+ x: 5,
102
+ y: 5,
103
+ width: 10,
104
+ height: 10
105
+ };
106
+ const b = {
107
+ x: 0,
108
+ y: 0,
109
+ width: 20,
110
+ height: 20
111
+ };
112
+ const res = mergeRects(a, b);
113
+ expect(res.x).toEqual(0);
114
+ expect(res.y).toEqual(0);
115
+ expect(res.width).toEqual(20);
116
+ expect(res.height).toEqual(20);
117
+ });
118
+ test("zero-area rect (point)", () => {
119
+ const a = {
120
+ x: 5,
121
+ y: 5,
122
+ width: 0,
123
+ height: 0
124
+ };
125
+ const b = {
126
+ x: 10,
127
+ y: 10,
128
+ width: 10,
129
+ height: 10
130
+ };
131
+ const res = mergeRects(a, b);
132
+ expect(res.x).toEqual(5);
133
+ expect(res.y).toEqual(5);
134
+ expect(res.width).toEqual(15);
135
+ expect(res.height).toEqual(15);
136
+ });
137
+ test("both rects have zero area", () => {
138
+ const a = {
139
+ x: 5,
140
+ y: 5,
141
+ width: 0,
142
+ height: 0
143
+ };
144
+ const b = {
145
+ x: 10,
146
+ y: 10,
147
+ width: 0,
148
+ height: 0
149
+ };
150
+ const res = mergeRects(a, b);
151
+ expect(res.x).toEqual(5);
152
+ expect(res.y).toEqual(5);
153
+ expect(res.width).toEqual(5);
154
+ expect(res.height).toEqual(5);
155
+ });
156
+ test("negative coordinates", () => {
157
+ const a = {
158
+ x: -10,
159
+ y: -10,
160
+ width: 20,
161
+ height: 20
162
+ };
163
+ const b = {
164
+ x: 5,
165
+ y: 5,
166
+ width: 10,
167
+ height: 10
168
+ };
169
+ const res = mergeRects(a, b);
170
+ expect(res.x).toEqual(-10);
171
+ expect(res.y).toEqual(-10);
172
+ expect(res.width).toEqual(25);
173
+ expect(res.height).toEqual(25);
174
+ });
175
+ test("both rects with negative coordinates", () => {
176
+ const a = {
177
+ x: -20,
178
+ y: -20,
179
+ width: 10,
180
+ height: 10
181
+ };
182
+ const b = {
183
+ x: -15,
184
+ y: -15,
185
+ width: 10,
186
+ height: 10
187
+ };
188
+ const res = mergeRects(a, b);
189
+ expect(res.x).toEqual(-20);
190
+ expect(res.y).toEqual(-20);
191
+ expect(res.width).toEqual(15);
192
+ expect(res.height).toEqual(15);
193
+ });
194
+ test("rects with large coordinates", () => {
195
+ const a = {
196
+ x: 1000,
197
+ y: 1000,
198
+ width: 100,
199
+ height: 100
200
+ };
201
+ const b = {
202
+ x: 1050,
203
+ y: 1050,
204
+ width: 100,
205
+ height: 100
206
+ };
207
+ const res = mergeRects(a, b);
208
+ expect(res.x).toEqual(1000);
209
+ expect(res.y).toEqual(1000);
210
+ expect(res.width).toEqual(150);
211
+ expect(res.height).toEqual(150);
212
+ });
213
+ test("rects with fractional coordinates and dimensions", () => {
214
+ const a = {
215
+ x: 1.5,
216
+ y: 2.5,
217
+ width: 3.5,
218
+ height: 4.5
219
+ };
220
+ const b = {
221
+ x: 4,
222
+ y: 5,
223
+ width: 2,
224
+ height: 2
225
+ };
226
+ const res = mergeRects(a, b);
227
+ expect(res.x).toEqual(1.5);
228
+ expect(res.y).toEqual(2.5);
229
+ expect(res.width).toEqual(4.5);
230
+ expect(res.height).toEqual(4.5);
231
+ });
23
232
  });
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Named element snapshots persisted in localStorage.
3
+ *
4
+ * Workflow: takeSnapshot(selector, id) → change code → getSnapshotDiff(id).
5
+ * Stored snapshots are immutable — getSnapshotDiff never overwrites the
6
+ * baseline, so iterating on a fix always diffs against the original state.
7
+ */
8
+ import { StyleSnapshot } from "./extractComputedStyles";
9
+ export interface StoredSnapshot {
10
+ snapshotId: string;
11
+ selector: string;
12
+ index: number;
13
+ label?: string;
14
+ takenAt: string;
15
+ snapshot: StyleSnapshot;
16
+ }
17
+ export interface TakeSnapshotResult {
18
+ snapshotId: string;
19
+ selector: string;
20
+ index: number;
21
+ takenAt: string;
22
+ propertyCount: number;
23
+ boundingRect: StyleSnapshot["boundingRect"];
24
+ }
25
+ export interface SnapshotDiffResult {
26
+ snapshotId: string;
27
+ selector: string;
28
+ index: number;
29
+ takenAt: string;
30
+ formatted: string;
31
+ changes: Array<{
32
+ type: "added" | "removed" | "changed";
33
+ property: string;
34
+ before?: string;
35
+ after?: string;
36
+ }>;
37
+ boundingRectChanges: Array<{
38
+ key: "x" | "y" | "width" | "height";
39
+ before: number;
40
+ after: number;
41
+ }>;
42
+ }
43
+ export declare class NamedSnapshotError extends Error {
44
+ readonly code: string;
45
+ constructor(code: string, message: string);
46
+ }
47
+ export declare function takeNamedSnapshot(selector: string, snapshotId: string, options?: {
48
+ index?: number;
49
+ label?: string;
50
+ }): TakeSnapshotResult;
51
+ export declare function getNamedSnapshotDiff(snapshotId: string): SnapshotDiffResult;
52
+ export declare function clearNamedSnapshot(snapshotId: string): void;