@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,253 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { computeDiff, formatReport } from "./diff";
|
|
3
|
+
function snap(overrides = {}) {
|
|
4
|
+
return {
|
|
5
|
+
key: "1",
|
|
6
|
+
tagName: "div",
|
|
7
|
+
classes: [],
|
|
8
|
+
x: 0,
|
|
9
|
+
y: 0,
|
|
10
|
+
width: 100,
|
|
11
|
+
height: 100,
|
|
12
|
+
visible: true,
|
|
13
|
+
opacity: 1,
|
|
14
|
+
inViewport: true,
|
|
15
|
+
pointerEvents: "auto",
|
|
16
|
+
disabled: false,
|
|
17
|
+
...overrides
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
describe("computeDiff", () => {
|
|
21
|
+
test("empty snapshots produce empty report", () => {
|
|
22
|
+
const r = computeDiff([], []);
|
|
23
|
+
expect(r.entries).toHaveLength(0);
|
|
24
|
+
expect(r.counts).toEqual({
|
|
25
|
+
added: 0,
|
|
26
|
+
removed: 0,
|
|
27
|
+
changed: 0,
|
|
28
|
+
moved: 0
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
test("identical snapshots produce no entries", () => {
|
|
32
|
+
const a = [snap({
|
|
33
|
+
key: "1"
|
|
34
|
+
}), snap({
|
|
35
|
+
key: "2"
|
|
36
|
+
})];
|
|
37
|
+
const b = [snap({
|
|
38
|
+
key: "1"
|
|
39
|
+
}), snap({
|
|
40
|
+
key: "2"
|
|
41
|
+
})];
|
|
42
|
+
const r = computeDiff(a, b);
|
|
43
|
+
expect(r.entries).toHaveLength(0);
|
|
44
|
+
});
|
|
45
|
+
test("+ for keys only in after (when visible)", () => {
|
|
46
|
+
const a = [snap({
|
|
47
|
+
key: "1"
|
|
48
|
+
})];
|
|
49
|
+
const b = [snap({
|
|
50
|
+
key: "1"
|
|
51
|
+
}), snap({
|
|
52
|
+
key: "2",
|
|
53
|
+
id: "new"
|
|
54
|
+
})];
|
|
55
|
+
const r = computeDiff(a, b);
|
|
56
|
+
expect(r.counts.added).toBe(1);
|
|
57
|
+
expect(r.entries[0].type).toBe("+");
|
|
58
|
+
expect(r.entries[0].key).toBe("2");
|
|
59
|
+
});
|
|
60
|
+
test("+ is skipped when newly-added element is invisible", () => {
|
|
61
|
+
const a = [];
|
|
62
|
+
const b = [snap({
|
|
63
|
+
key: "2",
|
|
64
|
+
visible: false
|
|
65
|
+
})];
|
|
66
|
+
const r = computeDiff(a, b);
|
|
67
|
+
expect(r.counts.added).toBe(0);
|
|
68
|
+
});
|
|
69
|
+
test("- for keys only in before (when visible)", () => {
|
|
70
|
+
const a = [snap({
|
|
71
|
+
key: "1"
|
|
72
|
+
}), snap({
|
|
73
|
+
key: "2"
|
|
74
|
+
})];
|
|
75
|
+
const b = [snap({
|
|
76
|
+
key: "1"
|
|
77
|
+
})];
|
|
78
|
+
const r = computeDiff(a, b);
|
|
79
|
+
expect(r.counts.removed).toBe(1);
|
|
80
|
+
expect(r.entries[0].type).toBe("-");
|
|
81
|
+
});
|
|
82
|
+
test("- when element becomes invisible in after", () => {
|
|
83
|
+
const a = [snap({
|
|
84
|
+
key: "1",
|
|
85
|
+
visible: true
|
|
86
|
+
})];
|
|
87
|
+
const b = [snap({
|
|
88
|
+
key: "1",
|
|
89
|
+
visible: false
|
|
90
|
+
})];
|
|
91
|
+
const r = computeDiff(a, b);
|
|
92
|
+
expect(r.counts.removed).toBe(1);
|
|
93
|
+
expect(r.entries[0].type).toBe("-");
|
|
94
|
+
});
|
|
95
|
+
test("~ when opacity changes by >= 0.05", () => {
|
|
96
|
+
const a = [snap({
|
|
97
|
+
key: "1",
|
|
98
|
+
opacity: 1
|
|
99
|
+
})];
|
|
100
|
+
const b = [snap({
|
|
101
|
+
key: "1",
|
|
102
|
+
opacity: 0.95
|
|
103
|
+
})];
|
|
104
|
+
const r = computeDiff(a, b);
|
|
105
|
+
expect(r.counts.changed).toBe(1);
|
|
106
|
+
expect(r.entries[0].type).toBe("~");
|
|
107
|
+
expect(r.entries[0].changedFields).toContain("opacity");
|
|
108
|
+
});
|
|
109
|
+
test("no entry when opacity changes by < 0.05", () => {
|
|
110
|
+
const a = [snap({
|
|
111
|
+
key: "1",
|
|
112
|
+
opacity: 1
|
|
113
|
+
})];
|
|
114
|
+
const b = [snap({
|
|
115
|
+
key: "1",
|
|
116
|
+
opacity: 0.97
|
|
117
|
+
})];
|
|
118
|
+
const r = computeDiff(a, b);
|
|
119
|
+
expect(r.entries).toHaveLength(0);
|
|
120
|
+
});
|
|
121
|
+
test("→ when only x/y change by >= 4px", () => {
|
|
122
|
+
const a = [snap({
|
|
123
|
+
key: "1",
|
|
124
|
+
x: 0,
|
|
125
|
+
y: 0
|
|
126
|
+
})];
|
|
127
|
+
const b = [snap({
|
|
128
|
+
key: "1",
|
|
129
|
+
x: 10,
|
|
130
|
+
y: 20
|
|
131
|
+
})];
|
|
132
|
+
const r = computeDiff(a, b);
|
|
133
|
+
expect(r.counts.moved).toBe(1);
|
|
134
|
+
expect(r.entries[0].type).toBe("→");
|
|
135
|
+
});
|
|
136
|
+
test("no entry when position changes by 3px (under threshold)", () => {
|
|
137
|
+
const a = [snap({
|
|
138
|
+
key: "1",
|
|
139
|
+
x: 0,
|
|
140
|
+
y: 0
|
|
141
|
+
})];
|
|
142
|
+
const b = [snap({
|
|
143
|
+
key: "1",
|
|
144
|
+
x: 3,
|
|
145
|
+
y: 3
|
|
146
|
+
})];
|
|
147
|
+
const r = computeDiff(a, b);
|
|
148
|
+
expect(r.entries).toHaveLength(0);
|
|
149
|
+
});
|
|
150
|
+
test("w/h: 2px delta triggers ~, 1px does not", () => {
|
|
151
|
+
const r1 = computeDiff([snap({
|
|
152
|
+
key: "1",
|
|
153
|
+
width: 100
|
|
154
|
+
})], [snap({
|
|
155
|
+
key: "1",
|
|
156
|
+
width: 102
|
|
157
|
+
})]);
|
|
158
|
+
expect(r1.counts.changed).toBe(1);
|
|
159
|
+
const r2 = computeDiff([snap({
|
|
160
|
+
key: "1",
|
|
161
|
+
width: 100
|
|
162
|
+
})], [snap({
|
|
163
|
+
key: "1",
|
|
164
|
+
width: 101
|
|
165
|
+
})]);
|
|
166
|
+
expect(r2.entries).toHaveLength(0);
|
|
167
|
+
});
|
|
168
|
+
test("mixed position + opacity change produces ~, not →", () => {
|
|
169
|
+
const a = [snap({
|
|
170
|
+
key: "1",
|
|
171
|
+
x: 0,
|
|
172
|
+
opacity: 1
|
|
173
|
+
})];
|
|
174
|
+
const b = [snap({
|
|
175
|
+
key: "1",
|
|
176
|
+
x: 20,
|
|
177
|
+
opacity: 0.5
|
|
178
|
+
})];
|
|
179
|
+
const r = computeDiff(a, b);
|
|
180
|
+
expect(r.counts.changed).toBe(1);
|
|
181
|
+
expect(r.counts.moved).toBe(0);
|
|
182
|
+
expect(r.entries[0].type).toBe("~");
|
|
183
|
+
expect(r.entries[0].changedFields).toContain("x");
|
|
184
|
+
expect(r.entries[0].changedFields).toContain("opacity");
|
|
185
|
+
});
|
|
186
|
+
test("text change is reported", () => {
|
|
187
|
+
const a = [snap({
|
|
188
|
+
key: "1",
|
|
189
|
+
text: "Submit"
|
|
190
|
+
})];
|
|
191
|
+
const b = [snap({
|
|
192
|
+
key: "1",
|
|
193
|
+
text: "Submitting..."
|
|
194
|
+
})];
|
|
195
|
+
const r = computeDiff(a, b);
|
|
196
|
+
expect(r.counts.changed).toBe(1);
|
|
197
|
+
expect(r.entries[0].changedFields).toContain("text");
|
|
198
|
+
});
|
|
199
|
+
test("class addition is reported", () => {
|
|
200
|
+
const a = [snap({
|
|
201
|
+
key: "1",
|
|
202
|
+
classes: ["foo"]
|
|
203
|
+
})];
|
|
204
|
+
const b = [snap({
|
|
205
|
+
key: "1",
|
|
206
|
+
classes: ["foo", "active"]
|
|
207
|
+
})];
|
|
208
|
+
const r = computeDiff(a, b);
|
|
209
|
+
expect(r.counts.changed).toBe(1);
|
|
210
|
+
expect(r.entries[0].changedFields).toContain("classes");
|
|
211
|
+
});
|
|
212
|
+
test("pointer-events change is reported", () => {
|
|
213
|
+
const a = [snap({
|
|
214
|
+
key: "1",
|
|
215
|
+
pointerEvents: "auto"
|
|
216
|
+
})];
|
|
217
|
+
const b = [snap({
|
|
218
|
+
key: "1",
|
|
219
|
+
pointerEvents: "none"
|
|
220
|
+
})];
|
|
221
|
+
const r = computeDiff(a, b);
|
|
222
|
+
expect(r.counts.changed).toBe(1);
|
|
223
|
+
expect(r.entries[0].changedFields).toContain("pointerEvents");
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
describe("formatReport", () => {
|
|
227
|
+
test("no-changes report contains header and divider", () => {
|
|
228
|
+
const out = formatReport([], {
|
|
229
|
+
elapsedMs: 100,
|
|
230
|
+
settle: "clean"
|
|
231
|
+
});
|
|
232
|
+
expect(out).toContain("Visual diff");
|
|
233
|
+
expect(out).toContain("(no changes)");
|
|
234
|
+
});
|
|
235
|
+
test("summary line counts match entry types", () => {
|
|
236
|
+
const a = [snap({
|
|
237
|
+
key: "1"
|
|
238
|
+
}), snap({
|
|
239
|
+
key: "2"
|
|
240
|
+
})];
|
|
241
|
+
const b = [snap({
|
|
242
|
+
key: "1",
|
|
243
|
+
x: 20
|
|
244
|
+
}), snap({
|
|
245
|
+
key: "3",
|
|
246
|
+
id: "added"
|
|
247
|
+
})];
|
|
248
|
+
const r = computeDiff(a, b);
|
|
249
|
+
expect(r.text).toContain("1 added");
|
|
250
|
+
expect(r.text).toContain("1 removed");
|
|
251
|
+
expect(r.text).toContain("1 moved");
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const DEFAULT_SETTLE_TIMEOUT_MS = 3000;
|
|
2
|
+
export const MUTATION_SILENCE_MS = 150;
|
|
3
|
+
function nextFrame() {
|
|
4
|
+
return new Promise(resolve => {
|
|
5
|
+
if (typeof requestAnimationFrame === "function") {
|
|
6
|
+
requestAnimationFrame(() => resolve());
|
|
7
|
+
} else {
|
|
8
|
+
setTimeout(() => resolve(), 16);
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
function animationsRunning() {
|
|
13
|
+
if (typeof document === "undefined" || typeof document.getAnimations !== "function") {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const anims = document.getAnimations();
|
|
17
|
+
for (const a of anims) {
|
|
18
|
+
if (a.playState === "running") return true;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
export async function waitForSettle(timeoutMs = DEFAULT_SETTLE_TIMEOUT_MS, root) {
|
|
23
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") {
|
|
24
|
+
return "clean";
|
|
25
|
+
}
|
|
26
|
+
const observeTarget = root ?? document.documentElement;
|
|
27
|
+
const deadline = performance.now() + timeoutMs;
|
|
28
|
+
let lastMutation = performance.now();
|
|
29
|
+
const mo = new MutationObserver(() => {
|
|
30
|
+
lastMutation = performance.now();
|
|
31
|
+
});
|
|
32
|
+
mo.observe(observeTarget, {
|
|
33
|
+
childList: true,
|
|
34
|
+
subtree: true,
|
|
35
|
+
attributes: true,
|
|
36
|
+
characterData: true
|
|
37
|
+
});
|
|
38
|
+
try {
|
|
39
|
+
while (performance.now() < deadline) {
|
|
40
|
+
const sinceMutation = performance.now() - lastMutation;
|
|
41
|
+
if (!animationsRunning() && sinceMutation >= MUTATION_SILENCE_MS) {
|
|
42
|
+
return "clean";
|
|
43
|
+
}
|
|
44
|
+
await nextFrame();
|
|
45
|
+
}
|
|
46
|
+
return "timeout";
|
|
47
|
+
} finally {
|
|
48
|
+
mo.disconnect();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { waitForSettle } from "./settle";
|
|
3
|
+
describe("waitForSettle", () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
document.body.innerHTML = "";
|
|
6
|
+
if (typeof document.getAnimations !== "function") {
|
|
7
|
+
document.getAnimations = () => [];
|
|
8
|
+
}
|
|
9
|
+
});
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.restoreAllMocks();
|
|
12
|
+
});
|
|
13
|
+
test("returns 'clean' when no mutations and no animations", async () => {
|
|
14
|
+
const result = await waitForSettle(500);
|
|
15
|
+
expect(result).toBe("clean");
|
|
16
|
+
});
|
|
17
|
+
test("returns 'timeout' when mutations keep firing past the deadline", async () => {
|
|
18
|
+
const interval = setInterval(() => {
|
|
19
|
+
const d = document.createElement("div");
|
|
20
|
+
document.body.appendChild(d);
|
|
21
|
+
setTimeout(() => d.remove(), 0);
|
|
22
|
+
}, 20);
|
|
23
|
+
const result = await waitForSettle(400);
|
|
24
|
+
clearInterval(interval);
|
|
25
|
+
expect(result).toBe("timeout");
|
|
26
|
+
});
|
|
27
|
+
test("settles after mutations stop", async () => {
|
|
28
|
+
const start = performance.now();
|
|
29
|
+
let mutationCount = 0;
|
|
30
|
+
const interval = setInterval(() => {
|
|
31
|
+
if (mutationCount >= 3) {
|
|
32
|
+
clearInterval(interval);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const d = document.createElement("div");
|
|
36
|
+
document.body.appendChild(d);
|
|
37
|
+
mutationCount++;
|
|
38
|
+
}, 30);
|
|
39
|
+
const result = await waitForSettle(2000);
|
|
40
|
+
const elapsed = performance.now() - start;
|
|
41
|
+
expect(result).toBe("clean");
|
|
42
|
+
expect(elapsed).toBeLessThan(2000);
|
|
43
|
+
});
|
|
44
|
+
test("ignores mutations outside the provided root", async () => {
|
|
45
|
+
const root = document.createElement("div");
|
|
46
|
+
document.body.appendChild(root);
|
|
47
|
+
const interval = setInterval(() => {
|
|
48
|
+
const d = document.createElement("div");
|
|
49
|
+
document.body.appendChild(d);
|
|
50
|
+
setTimeout(() => d.remove(), 0);
|
|
51
|
+
}, 20);
|
|
52
|
+
const result = await waitForSettle(500, root);
|
|
53
|
+
clearInterval(interval);
|
|
54
|
+
expect(result).toBe("clean");
|
|
55
|
+
});
|
|
56
|
+
test("treats animations as non-idle", async () => {
|
|
57
|
+
const fakeAnim = {
|
|
58
|
+
playState: "running"
|
|
59
|
+
};
|
|
60
|
+
const spy = vi.spyOn(document, "getAnimations").mockReturnValue([fakeAnim]);
|
|
61
|
+
const result = await waitForSettle(300);
|
|
62
|
+
expect(result).toBe("timeout");
|
|
63
|
+
spy.mockRestore();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { getReferenceId } from "../functions/getReferenceId";
|
|
2
|
+
import { isLocatorsOwnElement } from "../functions/isLocatorsOwnElement";
|
|
3
|
+
export const MAX_SNAPSHOT_ELEMENTS = 2000;
|
|
4
|
+
const TEXT_LIMIT = 80;
|
|
5
|
+
let warnedOverflow = false;
|
|
6
|
+
function truncateText(raw) {
|
|
7
|
+
if (!raw) return undefined;
|
|
8
|
+
const trimmed = raw.trim();
|
|
9
|
+
if (!trimmed) return undefined;
|
|
10
|
+
return trimmed.length > TEXT_LIMIT ? trimmed.slice(0, TEXT_LIMIT) : trimmed;
|
|
11
|
+
}
|
|
12
|
+
function snapshotElement(el) {
|
|
13
|
+
const rect = el.getBoundingClientRect();
|
|
14
|
+
const style = getComputedStyle(el);
|
|
15
|
+
const viewportWidth = typeof window !== "undefined" ? window.innerWidth : 0;
|
|
16
|
+
const viewportHeight = typeof window !== "undefined" ? window.innerHeight : 0;
|
|
17
|
+
const inViewport = rect.x + rect.width > 0 && rect.y + rect.height > 0 && rect.x < viewportWidth && rect.y < viewportHeight;
|
|
18
|
+
if (!inViewport) return null;
|
|
19
|
+
const opacity = parseFloat(style.opacity || "1");
|
|
20
|
+
const displayNone = style.display === "none";
|
|
21
|
+
const hidden = style.visibility === "hidden";
|
|
22
|
+
const zeroSize = rect.width <= 0 || rect.height <= 0;
|
|
23
|
+
const visible = !displayNone && !hidden && !zeroSize;
|
|
24
|
+
const rawClasses = typeof el.className === "string" ? el.className.split(/\s+/).filter(Boolean) : Array.from(el.classList);
|
|
25
|
+
const classes = rawClasses.filter(c => !c.startsWith("locatorjs-"));
|
|
26
|
+
const disabled = el.disabled === true;
|
|
27
|
+
const hasElementChildren = el.children.length > 0;
|
|
28
|
+
const text = hasElementChildren ? undefined : truncateText(el.textContent);
|
|
29
|
+
return {
|
|
30
|
+
key: String(getReferenceId(el)),
|
|
31
|
+
tagName: el.tagName.toLowerCase(),
|
|
32
|
+
id: el.id || undefined,
|
|
33
|
+
classes,
|
|
34
|
+
x: rect.x,
|
|
35
|
+
y: rect.y,
|
|
36
|
+
width: rect.width,
|
|
37
|
+
height: rect.height,
|
|
38
|
+
visible,
|
|
39
|
+
opacity: Number.isFinite(opacity) ? opacity : 1,
|
|
40
|
+
inViewport,
|
|
41
|
+
pointerEvents: style.pointerEvents || "auto",
|
|
42
|
+
disabled,
|
|
43
|
+
text
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function takeSnapshot(root) {
|
|
47
|
+
if (typeof document === "undefined") return [];
|
|
48
|
+
const out = [];
|
|
49
|
+
const candidates = [];
|
|
50
|
+
if (root) {
|
|
51
|
+
if (root instanceof HTMLElement || root instanceof SVGElement) {
|
|
52
|
+
candidates.push(root);
|
|
53
|
+
}
|
|
54
|
+
const descendants = root.querySelectorAll("*");
|
|
55
|
+
for (let i = 0; i < descendants.length; i++) {
|
|
56
|
+
candidates.push(descendants[i]);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
const all = document.querySelectorAll("*");
|
|
60
|
+
for (let i = 0; i < all.length; i++) {
|
|
61
|
+
candidates.push(all[i]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
65
|
+
const el = candidates[i];
|
|
66
|
+
if (!(el instanceof HTMLElement) && !(el instanceof SVGElement)) continue;
|
|
67
|
+
if (el instanceof HTMLElement && isLocatorsOwnElement(el)) continue;
|
|
68
|
+
const snap = snapshotElement(el);
|
|
69
|
+
if (!snap) continue;
|
|
70
|
+
out.push(snap);
|
|
71
|
+
if (out.length >= MAX_SNAPSHOT_ELEMENTS) {
|
|
72
|
+
if (!warnedOverflow) {
|
|
73
|
+
warnedOverflow = true;
|
|
74
|
+
// eslint-disable-next-line no-console
|
|
75
|
+
console.warn(`[treelocator/visualDiff] snapshot capped at ${MAX_SNAPSHOT_ELEMENTS} elements`);
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
export function __resetSnapshotWarningForTests() {
|
|
83
|
+
warnedOverflow = false;
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|