@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,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
5
|
+
import { calculateSpecificity, compareSpecificity, formatSpecificity, describeElement, formatCSSInspection, inspectCSSRules } from "./cssRuleInspector";
|
|
6
|
+
describe("calculateSpecificity", () => {
|
|
7
|
+
it("counts element selectors", () => {
|
|
8
|
+
expect(calculateSpecificity("div")).toEqual([0, 0, 1]);
|
|
9
|
+
expect(calculateSpecificity("div span")).toEqual([0, 0, 2]);
|
|
10
|
+
expect(calculateSpecificity("ul li a")).toEqual([0, 0, 3]);
|
|
11
|
+
});
|
|
12
|
+
it("counts class selectors", () => {
|
|
13
|
+
expect(calculateSpecificity(".button")).toEqual([0, 1, 0]);
|
|
14
|
+
expect(calculateSpecificity(".button.primary")).toEqual([0, 2, 0]);
|
|
15
|
+
});
|
|
16
|
+
it("counts ID selectors", () => {
|
|
17
|
+
expect(calculateSpecificity("#main")).toEqual([1, 0, 0]);
|
|
18
|
+
expect(calculateSpecificity("#main #sidebar")).toEqual([2, 0, 0]);
|
|
19
|
+
});
|
|
20
|
+
it("counts mixed selectors", () => {
|
|
21
|
+
expect(calculateSpecificity("div.button")).toEqual([0, 1, 1]);
|
|
22
|
+
expect(calculateSpecificity("#main .button")).toEqual([1, 1, 0]);
|
|
23
|
+
expect(calculateSpecificity("#main div.button")).toEqual([1, 1, 1]);
|
|
24
|
+
});
|
|
25
|
+
it("counts attribute selectors as class-level", () => {
|
|
26
|
+
expect(calculateSpecificity("[type=submit]")).toEqual([0, 1, 0]);
|
|
27
|
+
expect(calculateSpecificity("input[type=submit]")).toEqual([0, 1, 1]);
|
|
28
|
+
});
|
|
29
|
+
it("counts pseudo-classes as class-level", () => {
|
|
30
|
+
expect(calculateSpecificity(":hover")).toEqual([0, 1, 0]);
|
|
31
|
+
expect(calculateSpecificity("a:hover")).toEqual([0, 1, 1]);
|
|
32
|
+
expect(calculateSpecificity("a:first-child")).toEqual([0, 1, 1]);
|
|
33
|
+
});
|
|
34
|
+
it("counts pseudo-elements as element-level", () => {
|
|
35
|
+
expect(calculateSpecificity("::before")).toEqual([0, 0, 1]);
|
|
36
|
+
expect(calculateSpecificity("p::first-line")).toEqual([0, 0, 2]);
|
|
37
|
+
});
|
|
38
|
+
it("normalizes legacy single-colon pseudo-elements to element-level", () => {
|
|
39
|
+
// :before, :after, :first-line, :first-letter are legacy pseudo-elements
|
|
40
|
+
// that should be treated as element-level (0,0,1), not pseudo-class (0,1,0)
|
|
41
|
+
expect(calculateSpecificity(":before")).toEqual([0, 0, 1]);
|
|
42
|
+
expect(calculateSpecificity(":after")).toEqual([0, 0, 1]);
|
|
43
|
+
expect(calculateSpecificity("p:first-line")).toEqual([0, 0, 2]);
|
|
44
|
+
expect(calculateSpecificity("p:first-letter")).toEqual([0, 0, 2]);
|
|
45
|
+
});
|
|
46
|
+
it("handles child and sibling combinators", () => {
|
|
47
|
+
expect(calculateSpecificity("div > span")).toEqual([0, 0, 2]);
|
|
48
|
+
expect(calculateSpecificity("div + span")).toEqual([0, 0, 2]);
|
|
49
|
+
expect(calculateSpecificity("div ~ span")).toEqual([0, 0, 2]);
|
|
50
|
+
});
|
|
51
|
+
it("ignores universal selector", () => {
|
|
52
|
+
expect(calculateSpecificity("*")).toEqual([0, 0, 0]);
|
|
53
|
+
expect(calculateSpecificity("*.button")).toEqual([0, 1, 0]);
|
|
54
|
+
});
|
|
55
|
+
it("handles comma-separated selectors by returning max", () => {
|
|
56
|
+
// .a has specificity (0,1,0), #b has (1,0,0) — should return (1,0,0)
|
|
57
|
+
expect(calculateSpecificity(".a, #b")).toEqual([1, 0, 0]);
|
|
58
|
+
expect(calculateSpecificity("div, .button, #main")).toEqual([1, 0, 0]);
|
|
59
|
+
});
|
|
60
|
+
it("handles :not() by counting its argument", () => {
|
|
61
|
+
// :not(.disabled) = (0,1,0) + the :not itself is not counted
|
|
62
|
+
expect(calculateSpecificity(":not(.disabled)")).toEqual([0, 1, 0]);
|
|
63
|
+
expect(calculateSpecificity("div:not(.hidden)")).toEqual([0, 1, 1]);
|
|
64
|
+
});
|
|
65
|
+
it("handles :is() by counting its argument", () => {
|
|
66
|
+
expect(calculateSpecificity(":is(.a, .b)")).toEqual([0, 1, 0]);
|
|
67
|
+
});
|
|
68
|
+
it("handles :where() as zero specificity", () => {
|
|
69
|
+
expect(calculateSpecificity(":where(.a, .b)")).toEqual([0, 0, 0]);
|
|
70
|
+
expect(calculateSpecificity("div:where(.a)")).toEqual([0, 0, 1]);
|
|
71
|
+
});
|
|
72
|
+
it("handles complex real-world selectors", () => {
|
|
73
|
+
// Bootstrap-style: .btn-group > .btn:not(:first-child)
|
|
74
|
+
// .btn-group = (0,1,0), .btn = (0,1,0), :not(:first-child) -> :first-child = (0,1,0)
|
|
75
|
+
// Total: (0,3,0) ... but we also don't count > combinator
|
|
76
|
+
// Actually let me recalculate: .btn-group > .btn:not(:first-child)
|
|
77
|
+
// .btn-group = class (0,1,0)
|
|
78
|
+
// .btn = class (0,1,0)
|
|
79
|
+
// :not(:first-child) = argument :first-child = pseudo-class (0,1,0)
|
|
80
|
+
// Total = (0,3,0)
|
|
81
|
+
expect(calculateSpecificity(".btn-group > .btn:not(:first-child)")).toEqual([0, 3, 0]);
|
|
82
|
+
});
|
|
83
|
+
describe("nested functional pseudos", () => {
|
|
84
|
+
it("handles :not(:is(.a, .b))", () => {
|
|
85
|
+
// :is(.a, .b) = max(.a, .b) = (0,1,0)
|
|
86
|
+
// :not(:is(.a, .b)) = (0,1,0)
|
|
87
|
+
expect(calculateSpecificity(":not(:is(.a, .b))")).toEqual([0, 1, 0]);
|
|
88
|
+
});
|
|
89
|
+
it("handles :not(:not(.a))", () => {
|
|
90
|
+
// Inner :not(.a) = (0,1,0), outer :not(:not(.a)) = (0,1,0)
|
|
91
|
+
expect(calculateSpecificity(":not(:not(.a))")).toEqual([0, 1, 0]);
|
|
92
|
+
});
|
|
93
|
+
it("handles deeply nested :is and :where", () => {
|
|
94
|
+
// :is(:where(.a), .b) — :where(.a) = 0, .b = (0,1,0), max = (0,1,0)
|
|
95
|
+
expect(calculateSpecificity(":is(:where(.a), .b)")).toEqual([0, 1, 0]);
|
|
96
|
+
});
|
|
97
|
+
it("handles :has(:is(.a, [data-x=\"a,b\"]))", () => {
|
|
98
|
+
// .a = (0,1,0), [data-x="a,b"] = (0,1,0), max = (0,1,0)
|
|
99
|
+
// :has() takes the spec of its argument
|
|
100
|
+
expect(calculateSpecificity(':has(:is(.a, [data-x="a,b"]))')).toEqual([0, 1, 0]);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe("attribute selectors with embedded specials", () => {
|
|
104
|
+
it("handles attribute value containing a comma", () => {
|
|
105
|
+
// [data-value="a,b"] = single attribute selector = (0,1,0)
|
|
106
|
+
expect(calculateSpecificity('[data-value="a,b"]')).toEqual([0, 1, 0]);
|
|
107
|
+
});
|
|
108
|
+
it("handles attribute value containing brackets and dots", () => {
|
|
109
|
+
// [data-x=".a#b"] should still be one attribute selector = (0,1,0)
|
|
110
|
+
// The dot and # inside the string must NOT be counted as class/id.
|
|
111
|
+
expect(calculateSpecificity('[data-x=".a#b"]')).toEqual([0, 1, 0]);
|
|
112
|
+
});
|
|
113
|
+
it("handles multiple attribute selectors", () => {
|
|
114
|
+
expect(calculateSpecificity('[type="submit"][disabled]')).toEqual([0, 2, 0]);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
describe("comma splitting with nested specials", () => {
|
|
118
|
+
it("does not split inside parens", () => {
|
|
119
|
+
// :is(.a, .b) is a single branch, not two
|
|
120
|
+
expect(calculateSpecificity(":is(.a, .b), #c")).toEqual([1, 0, 0]);
|
|
121
|
+
});
|
|
122
|
+
it("does not split inside attribute brackets", () => {
|
|
123
|
+
// [data-x="a,b"] is a single branch
|
|
124
|
+
expect(calculateSpecificity('[data-x="a,b"], #c')).toEqual([1, 0, 0]);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
describe("compareSpecificity", () => {
|
|
129
|
+
it("returns positive when first is higher", () => {
|
|
130
|
+
expect(compareSpecificity([1, 0, 0], [0, 10, 10])).toBeGreaterThan(0);
|
|
131
|
+
expect(compareSpecificity([0, 2, 0], [0, 1, 5])).toBeGreaterThan(0);
|
|
132
|
+
expect(compareSpecificity([0, 0, 3], [0, 0, 2])).toBeGreaterThan(0);
|
|
133
|
+
});
|
|
134
|
+
it("returns negative when first is lower", () => {
|
|
135
|
+
expect(compareSpecificity([0, 0, 1], [0, 1, 0])).toBeLessThan(0);
|
|
136
|
+
expect(compareSpecificity([0, 1, 0], [1, 0, 0])).toBeLessThan(0);
|
|
137
|
+
});
|
|
138
|
+
it("returns zero when equal", () => {
|
|
139
|
+
expect(compareSpecificity([0, 1, 1], [0, 1, 1])).toBe(0);
|
|
140
|
+
expect(compareSpecificity([1, 0, 0], [1, 0, 0])).toBe(0);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe("formatSpecificity", () => {
|
|
144
|
+
it("formats tuple as readable string", () => {
|
|
145
|
+
expect(formatSpecificity([0, 1, 2])).toBe("(0,1,2)");
|
|
146
|
+
expect(formatSpecificity([1, 0, 0])).toBe("(1,0,0)");
|
|
147
|
+
expect(formatSpecificity([0, 0, 0])).toBe("(0,0,0)");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
describe("describeElement", () => {
|
|
151
|
+
it("describes a plain element", () => {
|
|
152
|
+
const el = document.createElement("div");
|
|
153
|
+
expect(describeElement(el)).toBe("div");
|
|
154
|
+
});
|
|
155
|
+
it("includes id", () => {
|
|
156
|
+
const el = document.createElement("div");
|
|
157
|
+
el.id = "main";
|
|
158
|
+
expect(describeElement(el)).toBe("div#main");
|
|
159
|
+
});
|
|
160
|
+
it("includes classes", () => {
|
|
161
|
+
const el = document.createElement("button");
|
|
162
|
+
el.classList.add("btn", "primary");
|
|
163
|
+
expect(describeElement(el)).toBe("button.btn.primary");
|
|
164
|
+
});
|
|
165
|
+
it("includes both id and classes", () => {
|
|
166
|
+
const el = document.createElement("button");
|
|
167
|
+
el.id = "submit";
|
|
168
|
+
el.classList.add("btn", "primary");
|
|
169
|
+
expect(describeElement(el)).toBe("button#submit.btn.primary");
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
describe("formatCSSInspection", () => {
|
|
173
|
+
it("formats empty result", () => {
|
|
174
|
+
const result = {
|
|
175
|
+
element: "div",
|
|
176
|
+
properties: [],
|
|
177
|
+
unreachableSheets: []
|
|
178
|
+
};
|
|
179
|
+
const formatted = formatCSSInspection(result);
|
|
180
|
+
expect(formatted).toContain("CSS Rules for div");
|
|
181
|
+
expect(formatted).toContain("No matching CSS rules found.");
|
|
182
|
+
});
|
|
183
|
+
it("formats properties with winning/losing rules", () => {
|
|
184
|
+
const result = {
|
|
185
|
+
element: "button.primary",
|
|
186
|
+
properties: [{
|
|
187
|
+
property: "color",
|
|
188
|
+
value: "rgb(51, 51, 51)",
|
|
189
|
+
rules: [{
|
|
190
|
+
selector: ".button.primary",
|
|
191
|
+
value: "#333",
|
|
192
|
+
specificity: [0, 2, 0],
|
|
193
|
+
important: false,
|
|
194
|
+
source: "components.css",
|
|
195
|
+
winning: true
|
|
196
|
+
}, {
|
|
197
|
+
selector: ".button",
|
|
198
|
+
value: "#666",
|
|
199
|
+
specificity: [0, 1, 0],
|
|
200
|
+
important: false,
|
|
201
|
+
source: "base.css",
|
|
202
|
+
winning: false
|
|
203
|
+
}]
|
|
204
|
+
}],
|
|
205
|
+
unreachableSheets: []
|
|
206
|
+
};
|
|
207
|
+
const formatted = formatCSSInspection(result);
|
|
208
|
+
expect(formatted).toContain("CSS Rules for button.primary");
|
|
209
|
+
expect(formatted).toContain("color: rgb(51, 51, 51)");
|
|
210
|
+
expect(formatted).toContain("✓ .button.primary");
|
|
211
|
+
expect(formatted).toContain("(0,2,0)");
|
|
212
|
+
expect(formatted).toContain("✗ .button");
|
|
213
|
+
expect(formatted).toContain("(0,1,0)");
|
|
214
|
+
expect(formatted).toContain("[#666]"); // losing rule shows its value
|
|
215
|
+
});
|
|
216
|
+
it("shows !important flag", () => {
|
|
217
|
+
const result = {
|
|
218
|
+
element: "div",
|
|
219
|
+
properties: [{
|
|
220
|
+
property: "display",
|
|
221
|
+
value: "none",
|
|
222
|
+
rules: [{
|
|
223
|
+
selector: ".hidden",
|
|
224
|
+
value: "none",
|
|
225
|
+
specificity: [0, 1, 0],
|
|
226
|
+
important: true,
|
|
227
|
+
source: "utils.css",
|
|
228
|
+
winning: true
|
|
229
|
+
}]
|
|
230
|
+
}],
|
|
231
|
+
unreachableSheets: []
|
|
232
|
+
};
|
|
233
|
+
const formatted = formatCSSInspection(result);
|
|
234
|
+
expect(formatted).toContain("!important");
|
|
235
|
+
});
|
|
236
|
+
it("shows cross-origin warning", () => {
|
|
237
|
+
const result = {
|
|
238
|
+
element: "div",
|
|
239
|
+
properties: [],
|
|
240
|
+
unreachableSheets: ["cdn-styles.css", "external.css"]
|
|
241
|
+
};
|
|
242
|
+
const formatted = formatCSSInspection(result);
|
|
243
|
+
expect(formatted).toContain("Cross-origin");
|
|
244
|
+
expect(formatted).toContain("cdn-styles.css");
|
|
245
|
+
expect(formatted).toContain("external.css");
|
|
246
|
+
});
|
|
247
|
+
it("formats with source-order tie breaking", () => {
|
|
248
|
+
// Two rules with identical specificity — second should win because it
|
|
249
|
+
// appears later in the source order.
|
|
250
|
+
const result = {
|
|
251
|
+
element: "div",
|
|
252
|
+
properties: [{
|
|
253
|
+
property: "color",
|
|
254
|
+
value: "blue",
|
|
255
|
+
rules: [
|
|
256
|
+
// Already sorted by inspectCSSRules — winner first.
|
|
257
|
+
{
|
|
258
|
+
selector: ".later",
|
|
259
|
+
value: "blue",
|
|
260
|
+
specificity: [0, 1, 0],
|
|
261
|
+
important: false,
|
|
262
|
+
source: "later.css",
|
|
263
|
+
winning: true
|
|
264
|
+
}, {
|
|
265
|
+
selector: ".earlier",
|
|
266
|
+
value: "red",
|
|
267
|
+
specificity: [0, 1, 0],
|
|
268
|
+
important: false,
|
|
269
|
+
source: "earlier.css",
|
|
270
|
+
winning: false
|
|
271
|
+
}]
|
|
272
|
+
}],
|
|
273
|
+
unreachableSheets: []
|
|
274
|
+
};
|
|
275
|
+
const formatted = formatCSSInspection(result);
|
|
276
|
+
expect(formatted).toContain("✓ .later");
|
|
277
|
+
expect(formatted).toContain("✗ .earlier");
|
|
278
|
+
});
|
|
279
|
+
it("shows inline source for inline rules", () => {
|
|
280
|
+
const result = {
|
|
281
|
+
element: "div",
|
|
282
|
+
properties: [{
|
|
283
|
+
property: "color",
|
|
284
|
+
value: "red",
|
|
285
|
+
rules: [{
|
|
286
|
+
selector: "element.style",
|
|
287
|
+
value: "red",
|
|
288
|
+
specificity: [1, 0, 0],
|
|
289
|
+
important: false,
|
|
290
|
+
source: "inline",
|
|
291
|
+
winning: true
|
|
292
|
+
}]
|
|
293
|
+
}],
|
|
294
|
+
unreachableSheets: []
|
|
295
|
+
};
|
|
296
|
+
const formatted = formatCSSInspection(result);
|
|
297
|
+
expect(formatted).toContain("(inline)");
|
|
298
|
+
expect(formatted).toContain("— inline");
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
describe("inspectCSSRules (integration)", () => {
|
|
302
|
+
let styleEl = null;
|
|
303
|
+
let testEl = null;
|
|
304
|
+
beforeEach(() => {
|
|
305
|
+
styleEl = document.createElement("style");
|
|
306
|
+
document.head.appendChild(styleEl);
|
|
307
|
+
});
|
|
308
|
+
afterEach(() => {
|
|
309
|
+
if (styleEl?.parentNode) styleEl.parentNode.removeChild(styleEl);
|
|
310
|
+
if (testEl?.parentNode) testEl.parentNode.removeChild(testEl);
|
|
311
|
+
styleEl = null;
|
|
312
|
+
testEl = null;
|
|
313
|
+
});
|
|
314
|
+
function setStyles(css) {
|
|
315
|
+
styleEl.textContent = css;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Build a DOM element using createElement + setAttribute (no innerHTML).
|
|
320
|
+
* Avoids the XSS-pattern lint warning while keeping tests readable.
|
|
321
|
+
*/
|
|
322
|
+
function make(tag, attrs = {}, ...children) {
|
|
323
|
+
const node = document.createElement(tag);
|
|
324
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
325
|
+
node.setAttribute(k, v);
|
|
326
|
+
}
|
|
327
|
+
for (const child of children) {
|
|
328
|
+
node.appendChild(child);
|
|
329
|
+
}
|
|
330
|
+
return node;
|
|
331
|
+
}
|
|
332
|
+
function mount(node) {
|
|
333
|
+
document.body.appendChild(node);
|
|
334
|
+
testEl = node;
|
|
335
|
+
return node;
|
|
336
|
+
}
|
|
337
|
+
it("source-order tie breaks: later rule wins on equal specificity", () => {
|
|
338
|
+
setStyles(`
|
|
339
|
+
.btn { color: red; }
|
|
340
|
+
.btn { color: blue; }
|
|
341
|
+
`);
|
|
342
|
+
const el = mount(make("button", {
|
|
343
|
+
class: "btn"
|
|
344
|
+
}));
|
|
345
|
+
const result = inspectCSSRules(el);
|
|
346
|
+
const colorProp = result.properties.find(p => p.property === "color");
|
|
347
|
+
expect(colorProp).toBeDefined();
|
|
348
|
+
// The winning rule should be the LATER one (blue), not the earlier one (red).
|
|
349
|
+
const winning = colorProp.rules.find(r => r.winning);
|
|
350
|
+
expect(winning.value).toBe("blue");
|
|
351
|
+
});
|
|
352
|
+
it("uses matched-branch specificity for selector lists", () => {
|
|
353
|
+
// Element matches `.btn` (specificity 0,1,0), but the rule's selector
|
|
354
|
+
// text is `.btn, #unrelated`. Specificity must be 0,1,0 (the matching
|
|
355
|
+
// branch), not 1,0,0 (the max of all branches). So a `#main .btn`
|
|
356
|
+
// selector (1,1,0) should still win.
|
|
357
|
+
setStyles(`
|
|
358
|
+
.btn, #unrelated { color: red; }
|
|
359
|
+
#main .btn { color: blue; }
|
|
360
|
+
`);
|
|
361
|
+
const btn = make("button", {
|
|
362
|
+
class: "btn"
|
|
363
|
+
});
|
|
364
|
+
mount(make("div", {
|
|
365
|
+
id: "main"
|
|
366
|
+
}, btn));
|
|
367
|
+
const result = inspectCSSRules(btn);
|
|
368
|
+
const colorProp = result.properties.find(p => p.property === "color");
|
|
369
|
+
const winning = colorProp.rules.find(r => r.winning);
|
|
370
|
+
expect(winning.value).toBe("blue");
|
|
371
|
+
expect(winning.selector).toContain("#main");
|
|
372
|
+
});
|
|
373
|
+
it("higher specificity beats lower regardless of source order", () => {
|
|
374
|
+
setStyles(`
|
|
375
|
+
#main { color: blue; }
|
|
376
|
+
.btn { color: red; }
|
|
377
|
+
`);
|
|
378
|
+
const el = mount(make("div", {
|
|
379
|
+
id: "main",
|
|
380
|
+
class: "btn"
|
|
381
|
+
}));
|
|
382
|
+
const result = inspectCSSRules(el);
|
|
383
|
+
const colorProp = result.properties.find(p => p.property === "color");
|
|
384
|
+
const winning = colorProp.rules.find(r => r.winning);
|
|
385
|
+
expect(winning.value).toBe("blue"); // #main has higher specificity
|
|
386
|
+
});
|
|
387
|
+
it("!important beats higher specificity without !important", () => {
|
|
388
|
+
setStyles(`
|
|
389
|
+
#main { color: blue; }
|
|
390
|
+
.btn { color: red !important; }
|
|
391
|
+
`);
|
|
392
|
+
const el = mount(make("div", {
|
|
393
|
+
id: "main",
|
|
394
|
+
class: "btn"
|
|
395
|
+
}));
|
|
396
|
+
const result = inspectCSSRules(el);
|
|
397
|
+
const colorProp = result.properties.find(p => p.property === "color");
|
|
398
|
+
const winning = colorProp.rules.find(r => r.winning);
|
|
399
|
+
expect(winning.value).toBe("red");
|
|
400
|
+
expect(winning.important).toBe(true);
|
|
401
|
+
});
|
|
402
|
+
it("inline styles beat stylesheet rules of same importance", () => {
|
|
403
|
+
setStyles(`
|
|
404
|
+
#main { color: blue; }
|
|
405
|
+
`);
|
|
406
|
+
const el = mount(make("div", {
|
|
407
|
+
id: "main",
|
|
408
|
+
style: "color: green;"
|
|
409
|
+
}));
|
|
410
|
+
const result = inspectCSSRules(el);
|
|
411
|
+
const colorProp = result.properties.find(p => p.property === "color");
|
|
412
|
+
const winning = colorProp.rules.find(r => r.winning);
|
|
413
|
+
expect(winning.source).toBe("inline");
|
|
414
|
+
});
|
|
415
|
+
it("returns empty properties when no rules match", () => {
|
|
416
|
+
setStyles(`.unrelated { color: red; }`);
|
|
417
|
+
const el = mount(make("div"));
|
|
418
|
+
const result = inspectCSSRules(el);
|
|
419
|
+
expect(result.properties).toEqual([]);
|
|
420
|
+
});
|
|
421
|
+
it("does not crash when getComputedStyle is unavailable", () => {
|
|
422
|
+
setStyles(`.btn { color: red; }`);
|
|
423
|
+
const el = mount(make("button", {
|
|
424
|
+
class: "btn"
|
|
425
|
+
}));
|
|
426
|
+
|
|
427
|
+
// Temporarily clobber getComputedStyle to simulate an environment without it
|
|
428
|
+
const original = window.getComputedStyle;
|
|
429
|
+
window.getComputedStyle = undefined;
|
|
430
|
+
try {
|
|
431
|
+
const result = inspectCSSRules(el);
|
|
432
|
+
const colorProp = result.properties.find(p => p.property === "color");
|
|
433
|
+
expect(colorProp).toBeDefined();
|
|
434
|
+
expect(colorProp.value).toBe("red"); // falls back to winning rule's value
|
|
435
|
+
} finally {
|
|
436
|
+
window.getComputedStyle = original;
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { deduplicateLabels } from "./deduplicateLabels";
|
|
3
|
+
describe("deduplicateLabels", () => {
|
|
4
|
+
test("returns empty array for empty input", () => {
|
|
5
|
+
const result = deduplicateLabels([]);
|
|
6
|
+
expect(result).toEqual([]);
|
|
7
|
+
});
|
|
8
|
+
test("returns same array when no duplicates", () => {
|
|
9
|
+
const labels = [{
|
|
10
|
+
label: "Button",
|
|
11
|
+
link: null
|
|
12
|
+
}, {
|
|
13
|
+
label: "Header",
|
|
14
|
+
link: null
|
|
15
|
+
}];
|
|
16
|
+
const result = deduplicateLabels(labels);
|
|
17
|
+
expect(result).toEqual(labels);
|
|
18
|
+
});
|
|
19
|
+
test("removes duplicate labels with same link", () => {
|
|
20
|
+
const label1 = {
|
|
21
|
+
label: "Button",
|
|
22
|
+
link: null
|
|
23
|
+
};
|
|
24
|
+
const label2 = {
|
|
25
|
+
label: "Button",
|
|
26
|
+
link: null
|
|
27
|
+
};
|
|
28
|
+
const labels = [label1, label2];
|
|
29
|
+
const result = deduplicateLabels(labels);
|
|
30
|
+
expect(result).toHaveLength(1);
|
|
31
|
+
expect(result[0]).toEqual(label1);
|
|
32
|
+
});
|
|
33
|
+
test("keeps labels with different link properties", () => {
|
|
34
|
+
const link1 = {
|
|
35
|
+
filePath: "Button.tsx",
|
|
36
|
+
projectPath: "/src",
|
|
37
|
+
line: 1,
|
|
38
|
+
column: 1
|
|
39
|
+
};
|
|
40
|
+
const link2 = {
|
|
41
|
+
filePath: "Header.tsx",
|
|
42
|
+
projectPath: "/src",
|
|
43
|
+
line: 1,
|
|
44
|
+
column: 1
|
|
45
|
+
};
|
|
46
|
+
const labels = [{
|
|
47
|
+
label: "Button",
|
|
48
|
+
link: link1
|
|
49
|
+
}, {
|
|
50
|
+
label: "Button",
|
|
51
|
+
link: link2
|
|
52
|
+
}];
|
|
53
|
+
const result = deduplicateLabels(labels);
|
|
54
|
+
expect(result).toHaveLength(2);
|
|
55
|
+
});
|
|
56
|
+
test("removes duplicate labels with same link object", () => {
|
|
57
|
+
const link = {
|
|
58
|
+
filePath: "Button.tsx",
|
|
59
|
+
projectPath: "/src",
|
|
60
|
+
line: 1,
|
|
61
|
+
column: 1
|
|
62
|
+
};
|
|
63
|
+
const labels = [{
|
|
64
|
+
label: "Button",
|
|
65
|
+
link
|
|
66
|
+
}, {
|
|
67
|
+
label: "Button",
|
|
68
|
+
link
|
|
69
|
+
}];
|
|
70
|
+
const result = deduplicateLabels(labels);
|
|
71
|
+
expect(result).toHaveLength(1);
|
|
72
|
+
});
|
|
73
|
+
test("handles multiple duplicates", () => {
|
|
74
|
+
const labels = [{
|
|
75
|
+
label: "Button",
|
|
76
|
+
link: null
|
|
77
|
+
}, {
|
|
78
|
+
label: "Button",
|
|
79
|
+
link: null
|
|
80
|
+
}, {
|
|
81
|
+
label: "Header",
|
|
82
|
+
link: null
|
|
83
|
+
}, {
|
|
84
|
+
label: "Header",
|
|
85
|
+
link: null
|
|
86
|
+
}, {
|
|
87
|
+
label: "Button",
|
|
88
|
+
link: null
|
|
89
|
+
}];
|
|
90
|
+
const result = deduplicateLabels(labels);
|
|
91
|
+
expect(result).toHaveLength(2);
|
|
92
|
+
expect(result.map(l => l.label)).toEqual(["Button", "Header"]);
|
|
93
|
+
});
|
|
94
|
+
test("preserves order of first occurrence", () => {
|
|
95
|
+
const labels = [{
|
|
96
|
+
label: "C",
|
|
97
|
+
link: null
|
|
98
|
+
}, {
|
|
99
|
+
label: "A",
|
|
100
|
+
link: null
|
|
101
|
+
}, {
|
|
102
|
+
label: "B",
|
|
103
|
+
link: null
|
|
104
|
+
}, {
|
|
105
|
+
label: "A",
|
|
106
|
+
link: null
|
|
107
|
+
}, {
|
|
108
|
+
label: "C",
|
|
109
|
+
link: null
|
|
110
|
+
}];
|
|
111
|
+
const result = deduplicateLabels(labels);
|
|
112
|
+
expect(result.map(l => l.label)).toEqual(["C", "A", "B"]);
|
|
113
|
+
});
|
|
114
|
+
test("handles complex link objects with same values", () => {
|
|
115
|
+
const labels = [{
|
|
116
|
+
label: "Component",
|
|
117
|
+
link: {
|
|
118
|
+
filePath: "App.tsx",
|
|
119
|
+
projectPath: "/src",
|
|
120
|
+
line: 10,
|
|
121
|
+
column: 5
|
|
122
|
+
}
|
|
123
|
+
}, {
|
|
124
|
+
label: "Component",
|
|
125
|
+
link: {
|
|
126
|
+
filePath: "App.tsx",
|
|
127
|
+
projectPath: "/src",
|
|
128
|
+
line: 10,
|
|
129
|
+
column: 5
|
|
130
|
+
}
|
|
131
|
+
}];
|
|
132
|
+
const result = deduplicateLabels(labels);
|
|
133
|
+
expect(result).toHaveLength(1);
|
|
134
|
+
});
|
|
135
|
+
test("distinguishes between link null and link with values", () => {
|
|
136
|
+
const labels = [{
|
|
137
|
+
label: "Button",
|
|
138
|
+
link: null
|
|
139
|
+
}, {
|
|
140
|
+
label: "Button",
|
|
141
|
+
link: {
|
|
142
|
+
filePath: "Button.tsx",
|
|
143
|
+
projectPath: "/src",
|
|
144
|
+
line: 1,
|
|
145
|
+
column: 1
|
|
146
|
+
}
|
|
147
|
+
}];
|
|
148
|
+
const result = deduplicateLabels(labels);
|
|
149
|
+
expect(result).toHaveLength(2);
|
|
150
|
+
});
|
|
151
|
+
test("handles single label", () => {
|
|
152
|
+
const labels = [{
|
|
153
|
+
label: "Button",
|
|
154
|
+
link: null
|
|
155
|
+
}];
|
|
156
|
+
const result = deduplicateLabels(labels);
|
|
157
|
+
expect(result).toEqual(labels);
|
|
158
|
+
});
|
|
159
|
+
test("returns correct instances", () => {
|
|
160
|
+
const label1 = {
|
|
161
|
+
label: "Button",
|
|
162
|
+
link: null
|
|
163
|
+
};
|
|
164
|
+
const label2 = {
|
|
165
|
+
label: "Header",
|
|
166
|
+
link: null
|
|
167
|
+
};
|
|
168
|
+
const label3 = {
|
|
169
|
+
label: "Button",
|
|
170
|
+
link: null
|
|
171
|
+
};
|
|
172
|
+
const labels = [label1, label2, label3];
|
|
173
|
+
const result = deduplicateLabels(labels);
|
|
174
|
+
expect(result).toHaveLength(2);
|
|
175
|
+
expect(result[0]).toBe(label1);
|
|
176
|
+
expect(result[1]).toBe(label2);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { resolveSourceLocation, parseDebugStack } from "../adapters/react/resolveSourceMap";
|
|
2
2
|
import { normalizeFilePath } from "./normalizeFilePath";
|
|
3
|
-
|
|
4
3
|
/**
|
|
5
4
|
* Check if any DOM element has React 19 fibers (with _debugStack instead of _debugSource).
|
|
6
5
|
* Must walk the _debugOwner chain because DOM element fibers (HostComponent) never have
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts and formats computed styles from a DOM element.
|
|
3
|
+
* Output is optimized for AI consumption — minimal tokens, maximum signal.
|
|
4
|
+
*/
|
|
5
|
+
export interface StyleSnapshot {
|
|
6
|
+
properties: Record<string, string>;
|
|
7
|
+
boundingRect: {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
top: number;
|
|
13
|
+
right: number;
|
|
14
|
+
bottom: number;
|
|
15
|
+
left: number;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export interface ComputedStylesResult {
|
|
19
|
+
formatted: string;
|
|
20
|
+
snapshot: StyleSnapshot;
|
|
21
|
+
}
|
|
22
|
+
declare const LAYOUT_PROPERTIES: string[];
|
|
23
|
+
declare const VISUAL_PROPERTIES: string[];
|
|
24
|
+
declare const TYPOGRAPHY_PROPERTIES: string[];
|
|
25
|
+
declare const INTERACTION_PROPERTIES: string[];
|
|
26
|
+
declare const SVG_PROPERTIES: string[];
|
|
27
|
+
declare function collapseFourValues(top: string, right: string, bottom: string, left: string): string;
|
|
28
|
+
interface GroupedEntry {
|
|
29
|
+
name: string;
|
|
30
|
+
value: string;
|
|
31
|
+
}
|
|
32
|
+
declare function processGroupEntries(groupProps: string[], values: Record<string, string>, isNonDefault: (prop: string, value: string) => boolean): GroupedEntry[];
|
|
33
|
+
declare function formatDiff(prev: StyleSnapshot, curr: StyleSnapshot, elementLabel: string | undefined): string;
|
|
34
|
+
export interface ExtractOptions {
|
|
35
|
+
/**
|
|
36
|
+
* Skip diff-mode detection and always return the full formatted styles.
|
|
37
|
+
* Used by internal call sites that re-extract for the same element within
|
|
38
|
+
* the diff window (e.g. source-map enrichment re-runs in Runtime.tsx) where
|
|
39
|
+
* returning a "No changes detected" delta would be incorrect.
|
|
40
|
+
*/
|
|
41
|
+
forceFull?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Include curated properties even when they match browser defaults for the
|
|
44
|
+
* element's tag. Useful when you want a fuller dump closer to DevTools.
|
|
45
|
+
*/
|
|
46
|
+
includeDefaults?: boolean;
|
|
47
|
+
}
|
|
48
|
+
export declare function readSnapshot(element: Element): StyleSnapshot;
|
|
49
|
+
export declare function extractComputedStyles(element: Element, elementLabel?: string, options?: ExtractOptions): ComputedStylesResult;
|
|
50
|
+
export { formatDiff as formatSnapshotDiff };
|
|
51
|
+
export { collapseFourValues as _collapseFourValues, processGroupEntries as _processGroupEntries, formatDiff as _formatDiff, LAYOUT_PROPERTIES, VISUAL_PROPERTIES, TYPOGRAPHY_PROPERTIES, INTERACTION_PROPERTIES, SVG_PROPERTIES, };
|