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