@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.
- 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/dejitter/recorder.d.ts +7 -1
- package/dist/dejitter/recorder.js +64 -1
- 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/dejitter/recorder.ts +67 -3
- 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,681 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect, vi } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
_collapseFourValues,
|
|
5
|
+
_processGroupEntries,
|
|
6
|
+
_formatDiff,
|
|
7
|
+
extractComputedStyles,
|
|
8
|
+
LAYOUT_PROPERTIES,
|
|
9
|
+
VISUAL_PROPERTIES,
|
|
10
|
+
TYPOGRAPHY_PROPERTIES,
|
|
11
|
+
INTERACTION_PROPERTIES,
|
|
12
|
+
SVG_PROPERTIES,
|
|
13
|
+
StyleSnapshot,
|
|
14
|
+
} from "./extractComputedStyles";
|
|
15
|
+
import { getElementLabel } from "./formatAncestryChain";
|
|
16
|
+
|
|
17
|
+
describe("extractComputedStyles", () => {
|
|
18
|
+
describe("collapseFourValues", () => {
|
|
19
|
+
it("collapses when all four values are the same", () => {
|
|
20
|
+
expect(_collapseFourValues("8px", "8px", "8px", "8px")).toBe("8px");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("collapses to two values when top=bottom and left=right", () => {
|
|
24
|
+
expect(_collapseFourValues("8px", "16px", "8px", "16px")).toBe(
|
|
25
|
+
"8px 16px"
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("collapses to three values when left=right", () => {
|
|
30
|
+
expect(_collapseFourValues("8px", "16px", "4px", "16px")).toBe(
|
|
31
|
+
"8px 16px 4px"
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("outputs all four when all different", () => {
|
|
36
|
+
expect(_collapseFourValues("1px", "2px", "3px", "4px")).toBe(
|
|
37
|
+
"1px 2px 3px 4px"
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("handles 0px values", () => {
|
|
42
|
+
expect(_collapseFourValues("0px", "0px", "0px", "0px")).toBe("0px");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("processGroupEntries", () => {
|
|
47
|
+
const alwaysNonDefault = (_prop: string, value: string) =>
|
|
48
|
+
value !== "" && value !== "default";
|
|
49
|
+
|
|
50
|
+
it("collapses margin into shorthand when 2+ sides are non-default", () => {
|
|
51
|
+
const values: Record<string, string> = {
|
|
52
|
+
"margin-top": "10px",
|
|
53
|
+
"margin-right": "20px",
|
|
54
|
+
"margin-bottom": "10px",
|
|
55
|
+
"margin-left": "20px",
|
|
56
|
+
};
|
|
57
|
+
const entries = _processGroupEntries(
|
|
58
|
+
LAYOUT_PROPERTIES,
|
|
59
|
+
values,
|
|
60
|
+
alwaysNonDefault
|
|
61
|
+
);
|
|
62
|
+
const marginEntry = entries.find((e) => e.name === "margin");
|
|
63
|
+
expect(marginEntry).toBeDefined();
|
|
64
|
+
expect(marginEntry!.value).toBe("10px 20px");
|
|
65
|
+
// No individual margin-* entries
|
|
66
|
+
expect(entries.find((e) => e.name === "margin-top")).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("leaves single margin longhand when only one is non-default", () => {
|
|
70
|
+
const isNonDefault = (prop: string, value: string) => {
|
|
71
|
+
if (prop === "margin-top" && value === "10px") return true;
|
|
72
|
+
return false;
|
|
73
|
+
};
|
|
74
|
+
const values: Record<string, string> = {
|
|
75
|
+
"margin-top": "10px",
|
|
76
|
+
"margin-right": "0px",
|
|
77
|
+
"margin-bottom": "0px",
|
|
78
|
+
"margin-left": "0px",
|
|
79
|
+
};
|
|
80
|
+
const entries = _processGroupEntries(
|
|
81
|
+
LAYOUT_PROPERTIES,
|
|
82
|
+
values,
|
|
83
|
+
isNonDefault
|
|
84
|
+
);
|
|
85
|
+
expect(entries.find((e) => e.name === "margin")).toBeUndefined();
|
|
86
|
+
expect(entries.find((e) => e.name === "margin-top")).toBeDefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("collapses padding into shorthand", () => {
|
|
90
|
+
const values: Record<string, string> = {
|
|
91
|
+
"padding-top": "8px",
|
|
92
|
+
"padding-right": "16px",
|
|
93
|
+
"padding-bottom": "8px",
|
|
94
|
+
"padding-left": "16px",
|
|
95
|
+
};
|
|
96
|
+
const entries = _processGroupEntries(
|
|
97
|
+
LAYOUT_PROPERTIES,
|
|
98
|
+
values,
|
|
99
|
+
alwaysNonDefault
|
|
100
|
+
);
|
|
101
|
+
const paddingEntry = entries.find((e) => e.name === "padding");
|
|
102
|
+
expect(paddingEntry).toBeDefined();
|
|
103
|
+
expect(paddingEntry!.value).toBe("8px 16px");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("collapses overflow when both axes are the same", () => {
|
|
107
|
+
const values: Record<string, string> = {
|
|
108
|
+
"overflow-x": "hidden",
|
|
109
|
+
"overflow-y": "hidden",
|
|
110
|
+
};
|
|
111
|
+
const entries = _processGroupEntries(
|
|
112
|
+
LAYOUT_PROPERTIES,
|
|
113
|
+
values,
|
|
114
|
+
alwaysNonDefault
|
|
115
|
+
);
|
|
116
|
+
const overflowEntry = entries.find((e) => e.name === "overflow");
|
|
117
|
+
expect(overflowEntry).toBeDefined();
|
|
118
|
+
expect(overflowEntry!.value).toBe("hidden");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("collapses overflow when axes differ", () => {
|
|
122
|
+
const values: Record<string, string> = {
|
|
123
|
+
"overflow-x": "hidden",
|
|
124
|
+
"overflow-y": "scroll",
|
|
125
|
+
};
|
|
126
|
+
const entries = _processGroupEntries(
|
|
127
|
+
LAYOUT_PROPERTIES,
|
|
128
|
+
values,
|
|
129
|
+
alwaysNonDefault
|
|
130
|
+
);
|
|
131
|
+
const overflowEntry = entries.find((e) => e.name === "overflow");
|
|
132
|
+
expect(overflowEntry).toBeDefined();
|
|
133
|
+
expect(overflowEntry!.value).toBe("hidden scroll");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("collapses flex shorthand when 2+ are non-default", () => {
|
|
137
|
+
const values: Record<string, string> = {
|
|
138
|
+
"flex-grow": "1",
|
|
139
|
+
"flex-shrink": "0",
|
|
140
|
+
"flex-basis": "auto",
|
|
141
|
+
};
|
|
142
|
+
const entries = _processGroupEntries(
|
|
143
|
+
LAYOUT_PROPERTIES,
|
|
144
|
+
values,
|
|
145
|
+
alwaysNonDefault
|
|
146
|
+
);
|
|
147
|
+
const flexEntry = entries.find((e) => e.name === "flex");
|
|
148
|
+
expect(flexEntry).toBeDefined();
|
|
149
|
+
expect(flexEntry!.value).toBe("1 0 auto");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("collapses uniform border into shorthand", () => {
|
|
153
|
+
const values: Record<string, string> = {
|
|
154
|
+
"border-top-width": "1px",
|
|
155
|
+
"border-right-width": "1px",
|
|
156
|
+
"border-bottom-width": "1px",
|
|
157
|
+
"border-left-width": "1px",
|
|
158
|
+
"border-top-style": "solid",
|
|
159
|
+
"border-right-style": "solid",
|
|
160
|
+
"border-bottom-style": "solid",
|
|
161
|
+
"border-left-style": "solid",
|
|
162
|
+
"border-top-color": "rgb(0, 0, 0)",
|
|
163
|
+
"border-right-color": "rgb(0, 0, 0)",
|
|
164
|
+
"border-bottom-color": "rgb(0, 0, 0)",
|
|
165
|
+
"border-left-color": "rgb(0, 0, 0)",
|
|
166
|
+
};
|
|
167
|
+
const entries = _processGroupEntries(
|
|
168
|
+
VISUAL_PROPERTIES,
|
|
169
|
+
values,
|
|
170
|
+
alwaysNonDefault
|
|
171
|
+
);
|
|
172
|
+
const borderEntry = entries.find((e) => e.name === "border");
|
|
173
|
+
expect(borderEntry).toBeDefined();
|
|
174
|
+
expect(borderEntry!.value).toBe("1px solid rgb(0, 0, 0)");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("uses per-side border shorthands for non-uniform borders", () => {
|
|
178
|
+
const values: Record<string, string> = {
|
|
179
|
+
"border-top-width": "1px",
|
|
180
|
+
"border-right-width": "0px",
|
|
181
|
+
"border-bottom-width": "2px",
|
|
182
|
+
"border-left-width": "0px",
|
|
183
|
+
"border-top-style": "solid",
|
|
184
|
+
"border-right-style": "none",
|
|
185
|
+
"border-bottom-style": "solid",
|
|
186
|
+
"border-left-style": "none",
|
|
187
|
+
"border-top-color": "rgb(0, 0, 0)",
|
|
188
|
+
"border-right-color": "rgb(0, 0, 0)",
|
|
189
|
+
"border-bottom-color": "rgb(255, 0, 0)",
|
|
190
|
+
"border-left-color": "rgb(0, 0, 0)",
|
|
191
|
+
};
|
|
192
|
+
const entries = _processGroupEntries(
|
|
193
|
+
VISUAL_PROPERTIES,
|
|
194
|
+
values,
|
|
195
|
+
alwaysNonDefault
|
|
196
|
+
);
|
|
197
|
+
expect(entries.find((e) => e.name === "border")).toBeUndefined();
|
|
198
|
+
expect(entries.find((e) => e.name === "border-top")).toBeDefined();
|
|
199
|
+
expect(entries.find((e) => e.name === "border-bottom")).toBeDefined();
|
|
200
|
+
// Right and left have style: none, so they're skipped
|
|
201
|
+
expect(entries.find((e) => e.name === "border-right")).toBeUndefined();
|
|
202
|
+
expect(entries.find((e) => e.name === "border-left")).toBeUndefined();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("collapses border-radius into shorthand", () => {
|
|
206
|
+
const values: Record<string, string> = {
|
|
207
|
+
"border-top-left-radius": "6px",
|
|
208
|
+
"border-top-right-radius": "6px",
|
|
209
|
+
"border-bottom-right-radius": "6px",
|
|
210
|
+
"border-bottom-left-radius": "6px",
|
|
211
|
+
};
|
|
212
|
+
const entries = _processGroupEntries(
|
|
213
|
+
VISUAL_PROPERTIES,
|
|
214
|
+
values,
|
|
215
|
+
alwaysNonDefault
|
|
216
|
+
);
|
|
217
|
+
const radiusEntry = entries.find((e) => e.name === "border-radius");
|
|
218
|
+
expect(radiusEntry).toBeDefined();
|
|
219
|
+
expect(radiusEntry!.value).toBe("6px");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("collapses outline into shorthand", () => {
|
|
223
|
+
const values: Record<string, string> = {
|
|
224
|
+
"outline-width": "2px",
|
|
225
|
+
"outline-style": "solid",
|
|
226
|
+
"outline-color": "rgb(59, 130, 246)",
|
|
227
|
+
};
|
|
228
|
+
const entries = _processGroupEntries(
|
|
229
|
+
VISUAL_PROPERTIES,
|
|
230
|
+
values,
|
|
231
|
+
alwaysNonDefault
|
|
232
|
+
);
|
|
233
|
+
const outlineEntry = entries.find((e) => e.name === "outline");
|
|
234
|
+
expect(outlineEntry).toBeDefined();
|
|
235
|
+
expect(outlineEntry!.value).toBe("2px solid rgb(59, 130, 246)");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("skips outline when style is none", () => {
|
|
239
|
+
const values: Record<string, string> = {
|
|
240
|
+
"outline-width": "0px",
|
|
241
|
+
"outline-style": "none",
|
|
242
|
+
"outline-color": "rgb(0, 0, 0)",
|
|
243
|
+
};
|
|
244
|
+
const entries = _processGroupEntries(
|
|
245
|
+
VISUAL_PROPERTIES,
|
|
246
|
+
values,
|
|
247
|
+
alwaysNonDefault
|
|
248
|
+
);
|
|
249
|
+
expect(entries.find((e) => e.name === "outline")).toBeUndefined();
|
|
250
|
+
// Individual longhands should also be consumed
|
|
251
|
+
expect(entries.find((e) => e.name === "outline-width")).toBeUndefined();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("outputs individual properties that are not part of shorthands", () => {
|
|
255
|
+
const values: Record<string, string> = {
|
|
256
|
+
display: "flex",
|
|
257
|
+
position: "relative",
|
|
258
|
+
};
|
|
259
|
+
const entries = _processGroupEntries(
|
|
260
|
+
LAYOUT_PROPERTIES,
|
|
261
|
+
values,
|
|
262
|
+
alwaysNonDefault
|
|
263
|
+
);
|
|
264
|
+
expect(entries.find((e) => e.name === "display")).toBeDefined();
|
|
265
|
+
expect(entries.find((e) => e.name === "position")).toBeDefined();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("skips properties where isNonDefault returns false", () => {
|
|
269
|
+
const neverNonDefault = () => false;
|
|
270
|
+
const values: Record<string, string> = {
|
|
271
|
+
display: "block",
|
|
272
|
+
position: "static",
|
|
273
|
+
};
|
|
274
|
+
const entries = _processGroupEntries(
|
|
275
|
+
LAYOUT_PROPERTIES,
|
|
276
|
+
values,
|
|
277
|
+
neverNonDefault
|
|
278
|
+
);
|
|
279
|
+
expect(entries).toEqual([]);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("skips properties with empty values", () => {
|
|
283
|
+
const values: Record<string, string> = {
|
|
284
|
+
display: "",
|
|
285
|
+
position: "",
|
|
286
|
+
};
|
|
287
|
+
const entries = _processGroupEntries(
|
|
288
|
+
LAYOUT_PROPERTIES,
|
|
289
|
+
values,
|
|
290
|
+
alwaysNonDefault
|
|
291
|
+
);
|
|
292
|
+
expect(entries).toEqual([]);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe("formatDiff", () => {
|
|
297
|
+
it("shows changed properties with arrow notation", () => {
|
|
298
|
+
const prev: StyleSnapshot = {
|
|
299
|
+
properties: { opacity: "1", "background-color": "transparent" },
|
|
300
|
+
boundingRect: {
|
|
301
|
+
x: 0,
|
|
302
|
+
y: 0,
|
|
303
|
+
width: 100,
|
|
304
|
+
height: 40,
|
|
305
|
+
top: 0,
|
|
306
|
+
right: 100,
|
|
307
|
+
bottom: 40,
|
|
308
|
+
left: 0,
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
const curr: StyleSnapshot = {
|
|
312
|
+
properties: { opacity: "0.8", "background-color": "rgb(59, 130, 246)" },
|
|
313
|
+
boundingRect: {
|
|
314
|
+
x: 0,
|
|
315
|
+
y: 0,
|
|
316
|
+
width: 100,
|
|
317
|
+
height: 40,
|
|
318
|
+
top: 0,
|
|
319
|
+
right: 100,
|
|
320
|
+
bottom: 40,
|
|
321
|
+
left: 0,
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const result = _formatDiff(prev, curr, "Button");
|
|
326
|
+
expect(result).toContain("[ComputedStyles \u0394] Button");
|
|
327
|
+
expect(result).toContain("~ opacity: 1 \u2192 0.8");
|
|
328
|
+
expect(result).toContain(
|
|
329
|
+
"~ background-color: transparent \u2192 rgb(59, 130, 246)"
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("shows added properties with + prefix", () => {
|
|
334
|
+
const prev: StyleSnapshot = {
|
|
335
|
+
properties: { opacity: "1" },
|
|
336
|
+
boundingRect: {
|
|
337
|
+
x: 0,
|
|
338
|
+
y: 0,
|
|
339
|
+
width: 100,
|
|
340
|
+
height: 40,
|
|
341
|
+
top: 0,
|
|
342
|
+
right: 100,
|
|
343
|
+
bottom: 40,
|
|
344
|
+
left: 0,
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
const curr: StyleSnapshot = {
|
|
348
|
+
properties: {
|
|
349
|
+
opacity: "1",
|
|
350
|
+
"box-shadow": "0 2px 4px rgba(0,0,0,0.1)",
|
|
351
|
+
},
|
|
352
|
+
boundingRect: {
|
|
353
|
+
x: 0,
|
|
354
|
+
y: 0,
|
|
355
|
+
width: 100,
|
|
356
|
+
height: 40,
|
|
357
|
+
top: 0,
|
|
358
|
+
right: 100,
|
|
359
|
+
bottom: 40,
|
|
360
|
+
left: 0,
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const result = _formatDiff(prev, curr, undefined);
|
|
365
|
+
expect(result).toContain("+ box-shadow: 0 2px 4px rgba(0,0,0,0.1)");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("shows removed properties with - prefix", () => {
|
|
369
|
+
const prev: StyleSnapshot = {
|
|
370
|
+
properties: { transform: "scale(1.05)" },
|
|
371
|
+
boundingRect: {
|
|
372
|
+
x: 0,
|
|
373
|
+
y: 0,
|
|
374
|
+
width: 100,
|
|
375
|
+
height: 40,
|
|
376
|
+
top: 0,
|
|
377
|
+
right: 100,
|
|
378
|
+
bottom: 40,
|
|
379
|
+
left: 0,
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
const curr: StyleSnapshot = {
|
|
383
|
+
properties: {},
|
|
384
|
+
boundingRect: {
|
|
385
|
+
x: 0,
|
|
386
|
+
y: 0,
|
|
387
|
+
width: 100,
|
|
388
|
+
height: 40,
|
|
389
|
+
top: 0,
|
|
390
|
+
right: 100,
|
|
391
|
+
bottom: 40,
|
|
392
|
+
left: 0,
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const result = _formatDiff(prev, curr, undefined);
|
|
397
|
+
expect(result).toContain("- transform: scale(1.05)");
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("shows bounding rect changes", () => {
|
|
401
|
+
const prev: StyleSnapshot = {
|
|
402
|
+
properties: {},
|
|
403
|
+
boundingRect: {
|
|
404
|
+
x: 100,
|
|
405
|
+
y: 200,
|
|
406
|
+
width: 120,
|
|
407
|
+
height: 40,
|
|
408
|
+
top: 200,
|
|
409
|
+
right: 220,
|
|
410
|
+
bottom: 240,
|
|
411
|
+
left: 100,
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
const curr: StyleSnapshot = {
|
|
415
|
+
properties: {},
|
|
416
|
+
boundingRect: {
|
|
417
|
+
x: 150,
|
|
418
|
+
y: 200,
|
|
419
|
+
width: 120,
|
|
420
|
+
height: 50,
|
|
421
|
+
top: 200,
|
|
422
|
+
right: 270,
|
|
423
|
+
bottom: 250,
|
|
424
|
+
left: 150,
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const result = _formatDiff(prev, curr, undefined);
|
|
429
|
+
expect(result).toContain("BoundingRect");
|
|
430
|
+
expect(result).toContain("~ x: 100 \u2192 150");
|
|
431
|
+
expect(result).toContain("~ height: 40 \u2192 50");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("shows 'No changes detected' when nothing changed", () => {
|
|
435
|
+
const snapshot: StyleSnapshot = {
|
|
436
|
+
properties: { opacity: "1" },
|
|
437
|
+
boundingRect: {
|
|
438
|
+
x: 0,
|
|
439
|
+
y: 0,
|
|
440
|
+
width: 100,
|
|
441
|
+
height: 40,
|
|
442
|
+
top: 0,
|
|
443
|
+
right: 100,
|
|
444
|
+
bottom: 40,
|
|
445
|
+
left: 0,
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
const result = _formatDiff(snapshot, snapshot, undefined);
|
|
449
|
+
expect(result).toContain("No changes detected");
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
describe("property group completeness", () => {
|
|
454
|
+
it("has no duplicates within property groups", () => {
|
|
455
|
+
const allProps = [
|
|
456
|
+
...LAYOUT_PROPERTIES,
|
|
457
|
+
...VISUAL_PROPERTIES,
|
|
458
|
+
...TYPOGRAPHY_PROPERTIES,
|
|
459
|
+
...INTERACTION_PROPERTIES,
|
|
460
|
+
...SVG_PROPERTIES,
|
|
461
|
+
];
|
|
462
|
+
const seen = new Set<string>();
|
|
463
|
+
const dupes: string[] = [];
|
|
464
|
+
for (const prop of allProps) {
|
|
465
|
+
if (seen.has(prop)) dupes.push(prop);
|
|
466
|
+
seen.add(prop);
|
|
467
|
+
}
|
|
468
|
+
expect(dupes).toEqual([]);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("includes key layout properties", () => {
|
|
472
|
+
expect(LAYOUT_PROPERTIES).toContain("display");
|
|
473
|
+
expect(LAYOUT_PROPERTIES).toContain("position");
|
|
474
|
+
expect(LAYOUT_PROPERTIES).toContain("width");
|
|
475
|
+
expect(LAYOUT_PROPERTIES).toContain("height");
|
|
476
|
+
expect(LAYOUT_PROPERTIES).toContain("flex-direction");
|
|
477
|
+
expect(LAYOUT_PROPERTIES).toContain("grid-template-columns");
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("includes key visual properties", () => {
|
|
481
|
+
expect(VISUAL_PROPERTIES).toContain("background-color");
|
|
482
|
+
expect(VISUAL_PROPERTIES).toContain("color");
|
|
483
|
+
expect(VISUAL_PROPERTIES).toContain("opacity");
|
|
484
|
+
expect(VISUAL_PROPERTIES).toContain("box-shadow");
|
|
485
|
+
expect(VISUAL_PROPERTIES).toContain("transform");
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("includes key typography properties", () => {
|
|
489
|
+
expect(TYPOGRAPHY_PROPERTIES).toContain("font-family");
|
|
490
|
+
expect(TYPOGRAPHY_PROPERTIES).toContain("font-size");
|
|
491
|
+
expect(TYPOGRAPHY_PROPERTIES).toContain("font-weight");
|
|
492
|
+
expect(TYPOGRAPHY_PROPERTIES).toContain("line-height");
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("includes SVG-specific properties", () => {
|
|
496
|
+
expect(SVG_PROPERTIES).toContain("fill");
|
|
497
|
+
expect(SVG_PROPERTIES).toContain("stroke");
|
|
498
|
+
expect(SVG_PROPERTIES).toContain("stroke-width");
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
describe("forceFull option", () => {
|
|
503
|
+
it("returns full styles on repeat call within diff window when forceFull is true", () => {
|
|
504
|
+
const el = document.createElement("div");
|
|
505
|
+
document.body.appendChild(el);
|
|
506
|
+
try {
|
|
507
|
+
// First call primes the snapshot
|
|
508
|
+
const first = extractComputedStyles(el, "Button");
|
|
509
|
+
expect(first.formatted).toContain("[ComputedStyles]");
|
|
510
|
+
expect(first.formatted).not.toContain("\u0394"); // no diff marker
|
|
511
|
+
|
|
512
|
+
// Second call without forceFull enters diff mode
|
|
513
|
+
const second = extractComputedStyles(el, "Button");
|
|
514
|
+
expect(second.formatted).toContain("\u0394");
|
|
515
|
+
|
|
516
|
+
// Third call with forceFull bypasses diff mode
|
|
517
|
+
const third = extractComputedStyles(el, "Button", { forceFull: true });
|
|
518
|
+
expect(third.formatted).toContain("[ComputedStyles]");
|
|
519
|
+
expect(third.formatted).not.toContain("\u0394");
|
|
520
|
+
expect(third.formatted).toContain("BoundingRect");
|
|
521
|
+
} finally {
|
|
522
|
+
el.remove();
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it("still updates lastSnapshot so subsequent calls can diff against the forced-full snapshot", () => {
|
|
527
|
+
const el = document.createElement("div");
|
|
528
|
+
document.body.appendChild(el);
|
|
529
|
+
try {
|
|
530
|
+
extractComputedStyles(el);
|
|
531
|
+
extractComputedStyles(el, undefined, { forceFull: true });
|
|
532
|
+
// Next call should be a diff against the forced-full snapshot,
|
|
533
|
+
// not the original one.
|
|
534
|
+
const after = extractComputedStyles(el);
|
|
535
|
+
expect(after.formatted).toContain("\u0394");
|
|
536
|
+
} finally {
|
|
537
|
+
el.remove();
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
describe("includeDefaults option", () => {
|
|
543
|
+
it("includes curated properties even when they match the tag defaults", () => {
|
|
544
|
+
const heading = document.createElement("h1");
|
|
545
|
+
document.body.appendChild(heading);
|
|
546
|
+
|
|
547
|
+
const makeStyle = (values: Record<string, string>) =>
|
|
548
|
+
({
|
|
549
|
+
getPropertyValue: (prop: string) => values[prop] ?? "",
|
|
550
|
+
}) as CSSStyleDeclaration;
|
|
551
|
+
|
|
552
|
+
const headingStyles = {
|
|
553
|
+
display: "block",
|
|
554
|
+
"font-weight": "700",
|
|
555
|
+
"font-size": "51.2px",
|
|
556
|
+
"line-height": "56.32px",
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const getComputedStyleSpy = vi
|
|
560
|
+
.spyOn(window, "getComputedStyle")
|
|
561
|
+
.mockImplementation((node: Element) => {
|
|
562
|
+
if (node === heading) {
|
|
563
|
+
return makeStyle(headingStyles);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (node.tagName === "H1") {
|
|
567
|
+
return makeStyle(headingStyles);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return makeStyle({});
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
const result = extractComputedStyles(heading, "App heading", {
|
|
575
|
+
forceFull: true,
|
|
576
|
+
includeDefaults: true,
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
expect(result.formatted).toContain("Layout");
|
|
580
|
+
expect(result.formatted).toContain("display: block");
|
|
581
|
+
expect(result.formatted).toContain("Typography");
|
|
582
|
+
expect(result.formatted).toContain("font-weight: 700");
|
|
583
|
+
expect(result.formatted).toContain("font-size: 51.2px");
|
|
584
|
+
} finally {
|
|
585
|
+
getComputedStyleSpy.mockRestore();
|
|
586
|
+
heading.remove();
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
describe("default filtering", () => {
|
|
592
|
+
it("keeps inherited typography when defaults are probed in an isolated shadow root", () => {
|
|
593
|
+
const link = document.createElement("a");
|
|
594
|
+
document.body.appendChild(link);
|
|
595
|
+
|
|
596
|
+
const makeStyle = (values: Record<string, string>) =>
|
|
597
|
+
({
|
|
598
|
+
getPropertyValue: (prop: string) => values[prop] ?? "",
|
|
599
|
+
}) as CSSStyleDeclaration;
|
|
600
|
+
|
|
601
|
+
const inheritedTypography = {
|
|
602
|
+
"font-family": '"Fira Code", monospace',
|
|
603
|
+
"font-size": "21px",
|
|
604
|
+
"font-style": "italic",
|
|
605
|
+
"font-weight": "700",
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
const defaultTypography = {
|
|
609
|
+
"font-family": "Times",
|
|
610
|
+
"font-size": "16px",
|
|
611
|
+
"font-style": "normal",
|
|
612
|
+
"font-weight": "400",
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
const getComputedStyleSpy = vi
|
|
616
|
+
.spyOn(window, "getComputedStyle")
|
|
617
|
+
.mockImplementation((node: Element) => {
|
|
618
|
+
if (node === link) {
|
|
619
|
+
return makeStyle(inheritedTypography);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (node.tagName === "A") {
|
|
623
|
+
return makeStyle(
|
|
624
|
+
node.getRootNode() instanceof ShadowRoot
|
|
625
|
+
? defaultTypography
|
|
626
|
+
: inheritedTypography
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return makeStyle({});
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
try {
|
|
634
|
+
const result = extractComputedStyles(link, "NavItem", {
|
|
635
|
+
forceFull: true,
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
expect(result.formatted).toContain("Typography");
|
|
639
|
+
expect(result.formatted).toMatch(/font-family: .*monospace/i);
|
|
640
|
+
expect(result.formatted).toContain("font-size: 21px");
|
|
641
|
+
expect(result.formatted).toContain("font-style: italic");
|
|
642
|
+
expect(result.formatted).toContain("font-weight: 700");
|
|
643
|
+
} finally {
|
|
644
|
+
getComputedStyleSpy.mockRestore();
|
|
645
|
+
link.remove();
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
describe("getElementLabel", () => {
|
|
651
|
+
it("returns empty string for empty ancestry", () => {
|
|
652
|
+
expect(getElementLabel([])).toBe("");
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it("returns component name with file location", () => {
|
|
656
|
+
const label = getElementLabel([
|
|
657
|
+
{
|
|
658
|
+
elementName: "button",
|
|
659
|
+
componentName: "SubmitButton",
|
|
660
|
+
filePath: "src/components/SubmitButton.tsx",
|
|
661
|
+
line: 12,
|
|
662
|
+
},
|
|
663
|
+
]);
|
|
664
|
+
expect(label).toBe("SubmitButton at src/components/SubmitButton.tsx:12");
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
it("falls back to element name when no component name is set", () => {
|
|
668
|
+
const label = getElementLabel([
|
|
669
|
+
{ elementName: "div", filePath: "src/App.tsx", line: 5 },
|
|
670
|
+
]);
|
|
671
|
+
expect(label).toBe("div at src/App.tsx:5");
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it("omits location when filePath is missing", () => {
|
|
675
|
+
const label = getElementLabel([
|
|
676
|
+
{ elementName: "span", componentName: "Label" },
|
|
677
|
+
]);
|
|
678
|
+
expect(label).toBe("Label");
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
});
|