@noego/wood 0.4.8 → 0.4.10
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/bin/axe.js +313 -98
- package/bin/wood.js +7 -1
- package/dist/automation/actions.cjs +17 -0
- package/dist/automation/actions.cjs.map +1 -0
- package/dist/automation/actions.d.cts +57 -0
- package/dist/automation/actions.d.ts +57 -0
- package/dist/automation/actions.js +1 -0
- package/dist/automation/actions.js.map +1 -0
- package/dist/automation/actions_executor.cjs +101 -0
- package/dist/automation/actions_executor.cjs.map +1 -0
- package/dist/automation/actions_executor.d.cts +16 -0
- package/dist/automation/actions_executor.d.ts +16 -0
- package/dist/automation/actions_executor.js +76 -0
- package/dist/automation/actions_executor.js.map +1 -0
- package/dist/automation/connection.cjs +86 -0
- package/dist/automation/connection.cjs.map +1 -0
- package/dist/automation/connection.d.cts +18 -0
- package/dist/automation/connection.d.ts +18 -0
- package/dist/automation/connection.js +51 -0
- package/dist/automation/connection.js.map +1 -0
- package/dist/automation/errors.cjs +50 -0
- package/dist/automation/errors.cjs.map +1 -0
- package/dist/automation/errors.d.cts +31 -0
- package/dist/automation/errors.d.ts +31 -0
- package/dist/automation/errors.js +24 -0
- package/dist/automation/errors.js.map +1 -0
- package/dist/automation/index.cjs +49 -0
- package/dist/automation/index.cjs.map +1 -0
- package/dist/automation/index.d.cts +16 -0
- package/dist/automation/index.d.ts +16 -0
- package/dist/automation/index.js +15 -0
- package/dist/automation/index.js.map +1 -0
- package/dist/automation/introspection_reader.cjs +75 -0
- package/dist/automation/introspection_reader.cjs.map +1 -0
- package/dist/automation/introspection_reader.d.cts +20 -0
- package/dist/automation/introspection_reader.d.ts +20 -0
- package/dist/automation/introspection_reader.js +48 -0
- package/dist/automation/introspection_reader.js.map +1 -0
- package/dist/automation/locator.cjs +102 -0
- package/dist/automation/locator.cjs.map +1 -0
- package/dist/automation/locator.d.cts +51 -0
- package/dist/automation/locator.d.ts +51 -0
- package/dist/automation/locator.js +75 -0
- package/dist/automation/locator.js.map +1 -0
- package/dist/automation/observation.cjs +17 -0
- package/dist/automation/observation.cjs.map +1 -0
- package/dist/automation/observation.d.cts +44 -0
- package/dist/automation/observation.d.ts +44 -0
- package/dist/automation/observation.js +1 -0
- package/dist/automation/observation.js.map +1 -0
- package/dist/automation/receipts.cjs +17 -0
- package/dist/automation/receipts.cjs.map +1 -0
- package/dist/automation/receipts.d.cts +27 -0
- package/dist/automation/receipts.d.ts +27 -0
- package/dist/automation/receipts.js +1 -0
- package/dist/automation/receipts.js.map +1 -0
- package/dist/automation/scenario.cjs +696 -0
- package/dist/automation/scenario.cjs.map +1 -0
- package/dist/automation/scenario.d.cts +89 -0
- package/dist/automation/scenario.d.ts +89 -0
- package/dist/automation/scenario.js +660 -0
- package/dist/automation/scenario.js.map +1 -0
- package/dist/automation/scrolling.cjs +17 -0
- package/dist/automation/scrolling.cjs.map +1 -0
- package/dist/automation/scrolling.d.cts +51 -0
- package/dist/automation/scrolling.d.ts +51 -0
- package/dist/automation/scrolling.js +1 -0
- package/dist/automation/scrolling.js.map +1 -0
- package/dist/automation/scrolling_executor.cjs +109 -0
- package/dist/automation/scrolling_executor.cjs.map +1 -0
- package/dist/automation/scrolling_executor.d.cts +13 -0
- package/dist/automation/scrolling_executor.d.ts +13 -0
- package/dist/automation/scrolling_executor.js +84 -0
- package/dist/automation/scrolling_executor.js.map +1 -0
- package/dist/automation/target_identity.cjs +17 -0
- package/dist/automation/target_identity.cjs.map +1 -0
- package/dist/automation/target_identity.d.cts +28 -0
- package/dist/automation/target_identity.d.ts +28 -0
- package/dist/automation/target_identity.js +1 -0
- package/dist/automation/target_identity.js.map +1 -0
- package/dist/automation/target_registry.cjs +148 -0
- package/dist/automation/target_registry.cjs.map +1 -0
- package/dist/automation/target_registry.d.cts +32 -0
- package/dist/automation/target_registry.d.ts +32 -0
- package/dist/automation/target_registry.js +122 -0
- package/dist/automation/target_registry.js.map +1 -0
- package/dist/automation/waits.cjs +17 -0
- package/dist/automation/waits.cjs.map +1 -0
- package/dist/automation/waits.d.cts +44 -0
- package/dist/automation/waits.d.ts +44 -0
- package/dist/automation/waits.js +1 -0
- package/dist/automation/waits.js.map +1 -0
- package/dist/automation/waits_executor.cjs +104 -0
- package/dist/automation/waits_executor.cjs.map +1 -0
- package/dist/automation/waits_executor.d.cts +14 -0
- package/dist/automation/waits_executor.d.ts +14 -0
- package/dist/automation/waits_executor.js +79 -0
- package/dist/automation/waits_executor.js.map +1 -0
- package/dist/client/index.d.cts +1 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/codegen/renderer_app_generator.cjs +16 -1
- package/dist/codegen/renderer_app_generator.cjs.map +1 -1
- package/dist/codegen/renderer_app_generator.js +16 -1
- package/dist/codegen/renderer_app_generator.js.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/navigation/automation_introspection.cjs +110 -0
- package/dist/navigation/automation_introspection.cjs.map +1 -0
- package/dist/navigation/automation_introspection.d.cts +60 -0
- package/dist/navigation/automation_introspection.d.ts +60 -0
- package/dist/navigation/automation_introspection.js +85 -0
- package/dist/navigation/automation_introspection.js.map +1 -0
- package/dist/navigation/index.cjs +7 -2
- package/dist/navigation/index.cjs.map +1 -1
- package/dist/navigation/index.d.cts +2 -0
- package/dist/navigation/index.d.ts +2 -0
- package/dist/navigation/index.js +4 -1
- package/dist/navigation/index.js.map +1 -1
- package/dist/testing/automation_assertions.cjs +417 -0
- package/dist/testing/automation_assertions.cjs.map +1 -0
- package/dist/testing/automation_assertions.d.cts +44 -0
- package/dist/testing/automation_assertions.d.ts +44 -0
- package/dist/testing/automation_assertions.js +385 -0
- package/dist/testing/automation_assertions.js.map +1 -0
- package/dist/testing/browser.cjs +26 -34
- package/dist/testing/browser.cjs.map +1 -1
- package/dist/testing/browser.d.cts +2 -1
- package/dist/testing/browser.d.ts +2 -1
- package/dist/testing/browser.js +30 -31
- package/dist/testing/browser.js.map +1 -1
- package/dist/testing/create_controller_harness.d.cts +1 -0
- package/dist/testing/create_controller_harness.d.ts +1 -0
- package/dist/testing/geometry.cjs +109 -0
- package/dist/testing/geometry.cjs.map +1 -0
- package/dist/testing/geometry.d.cts +45 -0
- package/dist/testing/geometry.d.ts +45 -0
- package/dist/testing/geometry.js +79 -0
- package/dist/testing/geometry.js.map +1 -0
- package/dist/testing/index.cjs +31 -2
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +4 -0
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.js +31 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/tracing/trace_hub.cjs +3 -0
- package/dist/tracing/trace_hub.cjs.map +1 -1
- package/dist/tracing/trace_hub.js +3 -0
- package/dist/tracing/trace_hub.js.map +1 -1
- package/package.json +7 -1
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import {
|
|
2
|
+
evenSpacing as evenSpacingRects,
|
|
3
|
+
gaps,
|
|
4
|
+
rectsOverlap,
|
|
5
|
+
sharedEdges as sharedRectEdges
|
|
6
|
+
} from "./geometry.js";
|
|
7
|
+
function failure(message, details) {
|
|
8
|
+
return details ? { pass: false, message, details } : { pass: false, message };
|
|
9
|
+
}
|
|
10
|
+
function success(message, details) {
|
|
11
|
+
return details ? { pass: true, message, details } : { pass: true, message };
|
|
12
|
+
}
|
|
13
|
+
function errorDetails(error) {
|
|
14
|
+
return {
|
|
15
|
+
error: error instanceof Error ? error.message : String(error)
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function locatorFor(page, target) {
|
|
19
|
+
return typeof target === "string" ? page.locator(target).first() : target.first();
|
|
20
|
+
}
|
|
21
|
+
function targetName(target) {
|
|
22
|
+
return typeof target === "string" ? `"${target}"` : "<locator>";
|
|
23
|
+
}
|
|
24
|
+
function isRectBox(target) {
|
|
25
|
+
const candidate = target;
|
|
26
|
+
return typeof candidate.top === "number" && typeof candidate.left === "number" && typeof candidate.right === "number" && typeof candidate.bottom === "number" && typeof candidate.width === "number" && typeof candidate.height === "number";
|
|
27
|
+
}
|
|
28
|
+
async function rectFor(page, target) {
|
|
29
|
+
if (isRectBox(target)) return target;
|
|
30
|
+
const box = await locatorFor(page, target).boundingBox();
|
|
31
|
+
if (!box) return null;
|
|
32
|
+
return {
|
|
33
|
+
top: box.y,
|
|
34
|
+
left: box.x,
|
|
35
|
+
right: box.x + box.width,
|
|
36
|
+
bottom: box.y + box.height,
|
|
37
|
+
width: box.width,
|
|
38
|
+
height: box.height
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function visible(page, target) {
|
|
42
|
+
try {
|
|
43
|
+
const pass = await locatorFor(page, target).isVisible();
|
|
44
|
+
return pass ? success(`Expected ${targetName(target)} to be visible.`) : failure(`Expected ${targetName(target)} to be visible, but it was hidden or missing.`);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return failure(
|
|
47
|
+
`Could not determine whether ${targetName(target)} is visible.`,
|
|
48
|
+
errorDetails(error)
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function hidden(page, target) {
|
|
53
|
+
try {
|
|
54
|
+
const pass = !await locatorFor(page, target).isVisible();
|
|
55
|
+
return pass ? success(`Expected ${targetName(target)} to be hidden.`) : failure(`Expected ${targetName(target)} to be hidden, but it was visible.`);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return failure(
|
|
58
|
+
`Could not determine whether ${targetName(target)} is hidden.`,
|
|
59
|
+
errorDetails(error)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function insideViewport(page, target, options = {}) {
|
|
64
|
+
try {
|
|
65
|
+
const margin = options.margin ?? 0;
|
|
66
|
+
const rect = await rectFor(page, target);
|
|
67
|
+
if (!rect) {
|
|
68
|
+
return failure("Expected target to have a visible bounding box.", { margin });
|
|
69
|
+
}
|
|
70
|
+
const viewport = page.viewportSize() ?? await page.evaluate(() => ({
|
|
71
|
+
width: window.innerWidth,
|
|
72
|
+
height: window.innerHeight
|
|
73
|
+
}));
|
|
74
|
+
const pass = rect.left >= margin && rect.top >= margin && rect.right <= viewport.width - margin && rect.bottom <= viewport.height - margin;
|
|
75
|
+
const details = { rect, viewport, margin };
|
|
76
|
+
return pass ? success("Target is inside the viewport.", details) : failure("Target extends outside the viewport.", details);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return failure("Could not assert viewport bounds.", errorDetails(error));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function notClipped(page, target) {
|
|
82
|
+
try {
|
|
83
|
+
const result = await locatorFor(page, target).evaluate((element) => {
|
|
84
|
+
const rect = element.getBoundingClientRect();
|
|
85
|
+
let parent = element.parentElement;
|
|
86
|
+
while (parent) {
|
|
87
|
+
const style = window.getComputedStyle(parent);
|
|
88
|
+
const clipsX = /(auto|hidden|scroll|clip)/.test(style.overflowX);
|
|
89
|
+
const clipsY = /(auto|hidden|scroll|clip)/.test(style.overflowY);
|
|
90
|
+
if (clipsX || clipsY) {
|
|
91
|
+
const parentRect = parent.getBoundingClientRect();
|
|
92
|
+
const clippedX = clipsX && (rect.left < parentRect.left || rect.right > parentRect.right);
|
|
93
|
+
const clippedY = clipsY && (rect.top < parentRect.top || rect.bottom > parentRect.bottom);
|
|
94
|
+
if (clippedX || clippedY) {
|
|
95
|
+
return {
|
|
96
|
+
pass: false,
|
|
97
|
+
ancestor: {
|
|
98
|
+
tag: parent.tagName,
|
|
99
|
+
id: parent.id || null,
|
|
100
|
+
classes: Array.from(parent.classList)
|
|
101
|
+
},
|
|
102
|
+
rect: {
|
|
103
|
+
top: rect.top,
|
|
104
|
+
left: rect.left,
|
|
105
|
+
right: rect.right,
|
|
106
|
+
bottom: rect.bottom,
|
|
107
|
+
width: rect.width,
|
|
108
|
+
height: rect.height
|
|
109
|
+
},
|
|
110
|
+
ancestorRect: {
|
|
111
|
+
top: parentRect.top,
|
|
112
|
+
left: parentRect.left,
|
|
113
|
+
right: parentRect.right,
|
|
114
|
+
bottom: parentRect.bottom,
|
|
115
|
+
width: parentRect.width,
|
|
116
|
+
height: parentRect.height
|
|
117
|
+
},
|
|
118
|
+
clippedX,
|
|
119
|
+
clippedY
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
parent = parent.parentElement;
|
|
124
|
+
}
|
|
125
|
+
return { pass: true };
|
|
126
|
+
});
|
|
127
|
+
return result.pass ? success(`Expected ${targetName(target)} not to be clipped.`) : failure(
|
|
128
|
+
`Expected ${targetName(target)} not to be clipped, but an overflow ancestor clips it.`,
|
|
129
|
+
result
|
|
130
|
+
);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return failure(
|
|
133
|
+
`Could not determine whether ${targetName(target)} is clipped.`,
|
|
134
|
+
errorDetails(error)
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function notOverlapping(page, firstTarget, secondTarget) {
|
|
139
|
+
try {
|
|
140
|
+
const [first, second] = await Promise.all([
|
|
141
|
+
rectFor(page, firstTarget),
|
|
142
|
+
rectFor(page, secondTarget)
|
|
143
|
+
]);
|
|
144
|
+
if (!first || !second) {
|
|
145
|
+
return failure("Expected both targets to have visible bounding boxes.", {
|
|
146
|
+
first,
|
|
147
|
+
second
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const overlap = rectsOverlap(first, second);
|
|
151
|
+
const details = { first, second };
|
|
152
|
+
return overlap ? failure("Expected targets not to overlap, but their border boxes intersect.", details) : success("Targets do not overlap.", details);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
return failure("Could not compare target overlap.", errorDetails(error));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function hitTestReachable(page, target) {
|
|
158
|
+
try {
|
|
159
|
+
const result = await locatorFor(page, target).evaluate((element) => {
|
|
160
|
+
if (typeof document.elementFromPoint !== "function") {
|
|
161
|
+
return {
|
|
162
|
+
supported: false,
|
|
163
|
+
capability: "DOM document.elementFromPoint"
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const rect = element.getBoundingClientRect();
|
|
167
|
+
const point = {
|
|
168
|
+
x: rect.left + rect.width / 2,
|
|
169
|
+
y: rect.top + rect.height / 2
|
|
170
|
+
};
|
|
171
|
+
const hit = document.elementFromPoint(point.x, point.y);
|
|
172
|
+
const reachable = Boolean(
|
|
173
|
+
hit && (hit === element || element.contains(hit) || hit.contains(element))
|
|
174
|
+
);
|
|
175
|
+
return {
|
|
176
|
+
supported: true,
|
|
177
|
+
reachable,
|
|
178
|
+
point,
|
|
179
|
+
targetRect: {
|
|
180
|
+
top: rect.top,
|
|
181
|
+
left: rect.left,
|
|
182
|
+
right: rect.right,
|
|
183
|
+
bottom: rect.bottom,
|
|
184
|
+
width: rect.width,
|
|
185
|
+
height: rect.height
|
|
186
|
+
},
|
|
187
|
+
hit: hit ? {
|
|
188
|
+
tag: hit.tagName,
|
|
189
|
+
id: hit.id || null,
|
|
190
|
+
classes: Array.from(hit.classList)
|
|
191
|
+
} : null,
|
|
192
|
+
relationship: hit ? {
|
|
193
|
+
isTarget: hit === element,
|
|
194
|
+
isDescendant: element.contains(hit),
|
|
195
|
+
isAncestor: hit.contains(element)
|
|
196
|
+
} : null
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
if (!result.supported) {
|
|
200
|
+
return failure(
|
|
201
|
+
"Hit testing is unsupported on this page surface.",
|
|
202
|
+
{ capability: result.capability }
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return result.reachable ? success(`Center of ${targetName(target)} is hit-test reachable.`, result) : failure(
|
|
206
|
+
`Center of ${targetName(target)} is covered by a foreign element.`,
|
|
207
|
+
result
|
|
208
|
+
);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
return failure(
|
|
211
|
+
`Could not hit-test ${targetName(target)}.`,
|
|
212
|
+
errorDetails(error)
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function sharedEdgesOnPage(page, targets, edge, options) {
|
|
217
|
+
try {
|
|
218
|
+
const rects = await Promise.all(targets.map((target) => rectFor(page, target)));
|
|
219
|
+
if (rects.some((rect) => rect === null)) {
|
|
220
|
+
return failure("Expected every target to have a visible bounding box.", {
|
|
221
|
+
missingIndices: rects.map((rect, index) => rect === null ? index : -1).filter((index) => index >= 0)
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return sharedRectEdges(rects, edge, options);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
return failure("Could not compare target edges.", errorDetails(error));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function sharedEdges(pageOrRects, targetsOrEdge, edgeOrOptions = {}, options = {}) {
|
|
230
|
+
if (Array.isArray(pageOrRects)) {
|
|
231
|
+
return sharedRectEdges(
|
|
232
|
+
pageOrRects,
|
|
233
|
+
targetsOrEdge,
|
|
234
|
+
edgeOrOptions
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return sharedEdgesOnPage(
|
|
238
|
+
pageOrRects,
|
|
239
|
+
targetsOrEdge,
|
|
240
|
+
edgeOrOptions,
|
|
241
|
+
options
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
async function evenSpacingOnPage(page, targets, axis, options) {
|
|
245
|
+
try {
|
|
246
|
+
const rects = await Promise.all(targets.map((target) => rectFor(page, target)));
|
|
247
|
+
if (rects.some((rect) => rect === null)) {
|
|
248
|
+
return failure("Expected every target to have a visible bounding box.", {
|
|
249
|
+
missingIndices: rects.map((rect, index) => rect === null ? index : -1).filter((index) => index >= 0)
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return evenSpacingRects(rects, axis, options);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
return failure("Could not compare target spacing.", errorDetails(error));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function evenSpacing(pageOrRects, targetsOrAxis, axisOrOptions = {}, options = {}) {
|
|
258
|
+
if (Array.isArray(pageOrRects)) {
|
|
259
|
+
return evenSpacingRects(
|
|
260
|
+
pageOrRects,
|
|
261
|
+
targetsOrAxis,
|
|
262
|
+
axisOrOptions
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
return evenSpacingOnPage(
|
|
266
|
+
pageOrRects,
|
|
267
|
+
targetsOrAxis,
|
|
268
|
+
axisOrOptions,
|
|
269
|
+
options
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
async function stableAfterAnimation(page, target, options = {}) {
|
|
273
|
+
const frameCount = options.frameCount ?? 3;
|
|
274
|
+
const tolerance = options.tolerance ?? 0;
|
|
275
|
+
const timeoutMs = options.timeoutMs ?? 2e3;
|
|
276
|
+
if (!Number.isInteger(frameCount) || frameCount < 1) {
|
|
277
|
+
return failure("frameCount must be a positive integer.", { frameCount });
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
const result = await locatorFor(page, target).evaluate(
|
|
281
|
+
async (element, input) => {
|
|
282
|
+
if (typeof window.requestAnimationFrame !== "function") {
|
|
283
|
+
return {
|
|
284
|
+
supported: false,
|
|
285
|
+
capability: "DOM window.requestAnimationFrame"
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const read = () => {
|
|
289
|
+
const rect = element.getBoundingClientRect();
|
|
290
|
+
return {
|
|
291
|
+
top: rect.top,
|
|
292
|
+
left: rect.left,
|
|
293
|
+
right: rect.right,
|
|
294
|
+
bottom: rect.bottom,
|
|
295
|
+
width: rect.width,
|
|
296
|
+
height: rect.height
|
|
297
|
+
};
|
|
298
|
+
};
|
|
299
|
+
const samples = [read()];
|
|
300
|
+
let timedOut = false;
|
|
301
|
+
const timeout = window.setTimeout(() => {
|
|
302
|
+
timedOut = true;
|
|
303
|
+
}, input.timeoutMs);
|
|
304
|
+
for (let index = 0; index < input.frameCount && !timedOut; index += 1) {
|
|
305
|
+
await new Promise((resolve) => {
|
|
306
|
+
window.requestAnimationFrame(() => resolve());
|
|
307
|
+
});
|
|
308
|
+
samples.push(read());
|
|
309
|
+
}
|
|
310
|
+
window.clearTimeout(timeout);
|
|
311
|
+
if (timedOut) {
|
|
312
|
+
return { supported: true, timedOut: true, samples };
|
|
313
|
+
}
|
|
314
|
+
const first = samples[0];
|
|
315
|
+
const keys = ["top", "left", "right", "bottom", "width", "height"];
|
|
316
|
+
let maximumDelta = 0;
|
|
317
|
+
for (const sample of samples.slice(1)) {
|
|
318
|
+
for (const key of keys) {
|
|
319
|
+
maximumDelta = Math.max(maximumDelta, Math.abs(sample[key] - first[key]));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
supported: true,
|
|
324
|
+
timedOut: false,
|
|
325
|
+
stable: maximumDelta <= input.tolerance,
|
|
326
|
+
maximumDelta,
|
|
327
|
+
samples
|
|
328
|
+
};
|
|
329
|
+
},
|
|
330
|
+
{ frameCount, tolerance, timeoutMs }
|
|
331
|
+
);
|
|
332
|
+
if (!result.supported) {
|
|
333
|
+
return failure(
|
|
334
|
+
"Animation stability sampling is unsupported on this page surface.",
|
|
335
|
+
{ capability: result.capability }
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
if (result.timedOut) {
|
|
339
|
+
return failure(
|
|
340
|
+
`Animation stability sampling timed out after ${timeoutMs}ms.`,
|
|
341
|
+
result
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
return result.stable ? success(
|
|
345
|
+
`Bounding box stayed stable across ${frameCount} animation frames.`,
|
|
346
|
+
result
|
|
347
|
+
) : failure(
|
|
348
|
+
`Bounding box changed across ${frameCount} animation frames.`,
|
|
349
|
+
result
|
|
350
|
+
);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
return failure(
|
|
353
|
+
`Could not sample animation stability for ${targetName(target)}.`,
|
|
354
|
+
errorDetails(error)
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const automationAssertions = {
|
|
359
|
+
visible,
|
|
360
|
+
hidden,
|
|
361
|
+
insideViewport,
|
|
362
|
+
notClipped,
|
|
363
|
+
notOverlapping,
|
|
364
|
+
hitTestReachable,
|
|
365
|
+
sharedEdges,
|
|
366
|
+
evenSpacing,
|
|
367
|
+
stableAfterAnimation
|
|
368
|
+
};
|
|
369
|
+
export {
|
|
370
|
+
automationAssertions,
|
|
371
|
+
evenSpacing,
|
|
372
|
+
evenSpacingRects,
|
|
373
|
+
gaps,
|
|
374
|
+
hidden,
|
|
375
|
+
hitTestReachable,
|
|
376
|
+
insideViewport,
|
|
377
|
+
notClipped,
|
|
378
|
+
notOverlapping,
|
|
379
|
+
rectsOverlap,
|
|
380
|
+
sharedEdges,
|
|
381
|
+
sharedRectEdges,
|
|
382
|
+
stableAfterAnimation,
|
|
383
|
+
visible
|
|
384
|
+
};
|
|
385
|
+
//# sourceMappingURL=automation_assertions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/testing/automation_assertions.ts"],"sourcesContent":["import type { Locator, Page } from 'playwright';\nimport {\n evenSpacing as evenSpacingRects,\n gaps,\n rectsOverlap,\n sharedEdges as sharedRectEdges,\n type GeometryVerdict,\n type RectAxis,\n type RectBox,\n type RectEdge,\n} from './geometry.js';\n\nexport {\n gaps,\n rectsOverlap,\n evenSpacingRects,\n sharedRectEdges,\n};\nexport type {\n GeometryVerdict,\n RectAxis,\n RectBox,\n RectEdge,\n};\n\nexport interface AssertionVerdict {\n pass: boolean;\n message: string;\n details?: Record<string, unknown>;\n}\n\nexport type AutomationTarget = string | Locator;\nexport type AutomationRectTarget = AutomationTarget | RectBox;\n\nfunction failure(\n message: string,\n details?: Record<string, unknown>,\n): AssertionVerdict {\n return details ? { pass: false, message, details } : { pass: false, message };\n}\n\nfunction success(\n message: string,\n details?: Record<string, unknown>,\n): AssertionVerdict {\n return details ? { pass: true, message, details } : { pass: true, message };\n}\n\nfunction errorDetails(error: unknown): Record<string, unknown> {\n return {\n error: error instanceof Error ? error.message : String(error),\n };\n}\n\nfunction locatorFor(page: Page, target: AutomationTarget): Locator {\n return typeof target === 'string' ? page.locator(target).first() : target.first();\n}\n\nfunction targetName(target: AutomationTarget): string {\n return typeof target === 'string' ? `\"${target}\"` : '<locator>';\n}\n\nfunction isRectBox(target: AutomationRectTarget): target is RectBox {\n const candidate = target as Partial<RectBox>;\n return typeof candidate.top === 'number'\n && typeof candidate.left === 'number'\n && typeof candidate.right === 'number'\n && typeof candidate.bottom === 'number'\n && typeof candidate.width === 'number'\n && typeof candidate.height === 'number';\n}\n\nasync function rectFor(\n page: Page,\n target: AutomationRectTarget,\n): Promise<RectBox | null> {\n if (isRectBox(target)) return target;\n const box = await locatorFor(page, target).boundingBox();\n if (!box) return null;\n return {\n top: box.y,\n left: box.x,\n right: box.x + box.width,\n bottom: box.y + box.height,\n width: box.width,\n height: box.height,\n };\n}\n\nexport async function visible(\n page: Page,\n target: AutomationTarget,\n): Promise<AssertionVerdict> {\n try {\n const pass = await locatorFor(page, target).isVisible();\n return pass\n ? success(`Expected ${targetName(target)} to be visible.`)\n : failure(`Expected ${targetName(target)} to be visible, but it was hidden or missing.`);\n } catch (error) {\n return failure(\n `Could not determine whether ${targetName(target)} is visible.`,\n errorDetails(error),\n );\n }\n}\n\nexport async function hidden(\n page: Page,\n target: AutomationTarget,\n): Promise<AssertionVerdict> {\n try {\n const pass = !(await locatorFor(page, target).isVisible());\n return pass\n ? success(`Expected ${targetName(target)} to be hidden.`)\n : failure(`Expected ${targetName(target)} to be hidden, but it was visible.`);\n } catch (error) {\n return failure(\n `Could not determine whether ${targetName(target)} is hidden.`,\n errorDetails(error),\n );\n }\n}\n\nexport async function insideViewport(\n page: Page,\n target: AutomationRectTarget,\n options: { margin?: number } = {},\n): Promise<AssertionVerdict> {\n try {\n const margin = options.margin ?? 0;\n const rect = await rectFor(page, target);\n if (!rect) {\n return failure('Expected target to have a visible bounding box.', { margin });\n }\n const viewport = page.viewportSize() ?? await page.evaluate(() => ({\n width: window.innerWidth,\n height: window.innerHeight,\n }));\n const pass = rect.left >= margin\n && rect.top >= margin\n && rect.right <= viewport.width - margin\n && rect.bottom <= viewport.height - margin;\n const details = { rect, viewport, margin };\n return pass\n ? success('Target is inside the viewport.', details)\n : failure('Target extends outside the viewport.', details);\n } catch (error) {\n return failure('Could not assert viewport bounds.', errorDetails(error));\n }\n}\n\nexport async function notClipped(\n page: Page,\n target: AutomationTarget,\n): Promise<AssertionVerdict> {\n try {\n const result = await locatorFor(page, target).evaluate((element: Element) => {\n const rect = element.getBoundingClientRect();\n let parent = element.parentElement;\n while (parent) {\n const style = window.getComputedStyle(parent);\n const clipsX = /(auto|hidden|scroll|clip)/.test(style.overflowX);\n const clipsY = /(auto|hidden|scroll|clip)/.test(style.overflowY);\n if (clipsX || clipsY) {\n const parentRect = parent.getBoundingClientRect();\n const clippedX = clipsX\n && (rect.left < parentRect.left || rect.right > parentRect.right);\n const clippedY = clipsY\n && (rect.top < parentRect.top || rect.bottom > parentRect.bottom);\n if (clippedX || clippedY) {\n return {\n pass: false,\n ancestor: {\n tag: parent.tagName,\n id: parent.id || null,\n classes: Array.from(parent.classList),\n },\n rect: {\n top: rect.top,\n left: rect.left,\n right: rect.right,\n bottom: rect.bottom,\n width: rect.width,\n height: rect.height,\n },\n ancestorRect: {\n top: parentRect.top,\n left: parentRect.left,\n right: parentRect.right,\n bottom: parentRect.bottom,\n width: parentRect.width,\n height: parentRect.height,\n },\n clippedX,\n clippedY,\n };\n }\n }\n parent = parent.parentElement;\n }\n return { pass: true };\n });\n\n return result.pass\n ? success(`Expected ${targetName(target)} not to be clipped.`)\n : failure(\n `Expected ${targetName(target)} not to be clipped, but an overflow ancestor clips it.`,\n result,\n );\n } catch (error) {\n return failure(\n `Could not determine whether ${targetName(target)} is clipped.`,\n errorDetails(error),\n );\n }\n}\n\nexport async function notOverlapping(\n page: Page,\n firstTarget: AutomationRectTarget,\n secondTarget: AutomationRectTarget,\n): Promise<AssertionVerdict> {\n try {\n const [first, second] = await Promise.all([\n rectFor(page, firstTarget),\n rectFor(page, secondTarget),\n ]);\n if (!first || !second) {\n return failure('Expected both targets to have visible bounding boxes.', {\n first,\n second,\n });\n }\n const overlap = rectsOverlap(first, second);\n const details = { first, second };\n return overlap\n ? failure('Expected targets not to overlap, but their border boxes intersect.', details)\n : success('Targets do not overlap.', details);\n } catch (error) {\n return failure('Could not compare target overlap.', errorDetails(error));\n }\n}\n\nexport async function hitTestReachable(\n page: Page,\n target: AutomationTarget,\n): Promise<AssertionVerdict> {\n try {\n const result = await locatorFor(page, target).evaluate((element: Element) => {\n if (typeof document.elementFromPoint !== 'function') {\n return {\n supported: false as const,\n capability: 'DOM document.elementFromPoint',\n };\n }\n const rect = element.getBoundingClientRect();\n const point = {\n x: rect.left + rect.width / 2,\n y: rect.top + rect.height / 2,\n };\n const hit = document.elementFromPoint(point.x, point.y);\n const reachable = Boolean(\n hit\n && (hit === element || element.contains(hit) || hit.contains(element)),\n );\n return {\n supported: true as const,\n reachable,\n point,\n targetRect: {\n top: rect.top,\n left: rect.left,\n right: rect.right,\n bottom: rect.bottom,\n width: rect.width,\n height: rect.height,\n },\n hit: hit ? {\n tag: hit.tagName,\n id: hit.id || null,\n classes: Array.from(hit.classList),\n } : null,\n relationship: hit ? {\n isTarget: hit === element,\n isDescendant: element.contains(hit),\n isAncestor: hit.contains(element),\n } : null,\n };\n });\n\n if (!result.supported) {\n return failure(\n 'Hit testing is unsupported on this page surface.',\n { capability: result.capability },\n );\n }\n return result.reachable\n ? success(`Center of ${targetName(target)} is hit-test reachable.`, result)\n : failure(\n `Center of ${targetName(target)} is covered by a foreign element.`,\n result,\n );\n } catch (error) {\n return failure(\n `Could not hit-test ${targetName(target)}.`,\n errorDetails(error),\n );\n }\n}\n\ntype GeometryOptions = { tolerance?: number };\n\nasync function sharedEdgesOnPage(\n page: Page,\n targets: readonly AutomationRectTarget[],\n edge: RectEdge,\n options: GeometryOptions,\n): Promise<AssertionVerdict> {\n try {\n const rects = await Promise.all(targets.map((target) => rectFor(page, target)));\n if (rects.some((rect) => rect === null)) {\n return failure('Expected every target to have a visible bounding box.', {\n missingIndices: rects\n .map((rect, index) => rect === null ? index : -1)\n .filter((index) => index >= 0),\n });\n }\n return sharedRectEdges(rects as RectBox[], edge, options);\n } catch (error) {\n return failure('Could not compare target edges.', errorDetails(error));\n }\n}\n\nexport function sharedEdges(\n rects: readonly RectBox[],\n edge: RectEdge,\n options?: GeometryOptions,\n): GeometryVerdict;\nexport function sharedEdges(\n page: Page,\n targets: readonly AutomationRectTarget[],\n edge: RectEdge,\n options?: GeometryOptions,\n): Promise<AssertionVerdict>;\nexport function sharedEdges(\n pageOrRects: Page | readonly RectBox[],\n targetsOrEdge: readonly AutomationRectTarget[] | RectEdge,\n edgeOrOptions: RectEdge | GeometryOptions = {},\n options: GeometryOptions = {},\n): GeometryVerdict | Promise<AssertionVerdict> {\n if (Array.isArray(pageOrRects)) {\n return sharedRectEdges(\n pageOrRects,\n targetsOrEdge as RectEdge,\n edgeOrOptions as GeometryOptions,\n );\n }\n return sharedEdgesOnPage(\n pageOrRects as Page,\n targetsOrEdge as readonly AutomationRectTarget[],\n edgeOrOptions as RectEdge,\n options,\n );\n}\n\nasync function evenSpacingOnPage(\n page: Page,\n targets: readonly AutomationRectTarget[],\n axis: RectAxis,\n options: GeometryOptions,\n): Promise<AssertionVerdict> {\n try {\n const rects = await Promise.all(targets.map((target) => rectFor(page, target)));\n if (rects.some((rect) => rect === null)) {\n return failure('Expected every target to have a visible bounding box.', {\n missingIndices: rects\n .map((rect, index) => rect === null ? index : -1)\n .filter((index) => index >= 0),\n });\n }\n return evenSpacingRects(rects as RectBox[], axis, options);\n } catch (error) {\n return failure('Could not compare target spacing.', errorDetails(error));\n }\n}\n\nexport function evenSpacing(\n rects: readonly RectBox[],\n axis: RectAxis,\n options?: GeometryOptions,\n): GeometryVerdict;\nexport function evenSpacing(\n page: Page,\n targets: readonly AutomationRectTarget[],\n axis: RectAxis,\n options?: GeometryOptions,\n): Promise<AssertionVerdict>;\nexport function evenSpacing(\n pageOrRects: Page | readonly RectBox[],\n targetsOrAxis: readonly AutomationRectTarget[] | RectAxis,\n axisOrOptions: RectAxis | GeometryOptions = {},\n options: GeometryOptions = {},\n): GeometryVerdict | Promise<AssertionVerdict> {\n if (Array.isArray(pageOrRects)) {\n return evenSpacingRects(\n pageOrRects,\n targetsOrAxis as RectAxis,\n axisOrOptions as GeometryOptions,\n );\n }\n return evenSpacingOnPage(\n pageOrRects as Page,\n targetsOrAxis as readonly AutomationRectTarget[],\n axisOrOptions as RectAxis,\n options,\n );\n}\n\nexport async function stableAfterAnimation(\n page: Page,\n target: AutomationTarget,\n options: { frameCount?: number; tolerance?: number; timeoutMs?: number } = {},\n): Promise<AssertionVerdict> {\n const frameCount = options.frameCount ?? 3;\n const tolerance = options.tolerance ?? 0;\n const timeoutMs = options.timeoutMs ?? 2000;\n if (!Number.isInteger(frameCount) || frameCount < 1) {\n return failure('frameCount must be a positive integer.', { frameCount });\n }\n\n try {\n const result = await locatorFor(page, target).evaluate(\n async (element: Element, input: {\n frameCount: number;\n tolerance: number;\n timeoutMs: number;\n }) => {\n if (typeof window.requestAnimationFrame !== 'function') {\n return {\n supported: false as const,\n capability: 'DOM window.requestAnimationFrame',\n };\n }\n const read = () => {\n const rect = element.getBoundingClientRect();\n return {\n top: rect.top,\n left: rect.left,\n right: rect.right,\n bottom: rect.bottom,\n width: rect.width,\n height: rect.height,\n };\n };\n const samples = [read()];\n let timedOut = false;\n const timeout = window.setTimeout(() => {\n timedOut = true;\n }, input.timeoutMs);\n for (let index = 0; index < input.frameCount && !timedOut; index += 1) {\n await new Promise<void>((resolve) => {\n window.requestAnimationFrame(() => resolve());\n });\n samples.push(read());\n }\n window.clearTimeout(timeout);\n if (timedOut) {\n return { supported: true as const, timedOut: true, samples };\n }\n\n const first = samples[0];\n const keys = ['top', 'left', 'right', 'bottom', 'width', 'height'] as const;\n let maximumDelta = 0;\n for (const sample of samples.slice(1)) {\n for (const key of keys) {\n maximumDelta = Math.max(maximumDelta, Math.abs(sample[key] - first[key]));\n }\n }\n return {\n supported: true as const,\n timedOut: false,\n stable: maximumDelta <= input.tolerance,\n maximumDelta,\n samples,\n };\n },\n { frameCount, tolerance, timeoutMs },\n );\n\n if (!result.supported) {\n return failure(\n 'Animation stability sampling is unsupported on this page surface.',\n { capability: result.capability },\n );\n }\n if (result.timedOut) {\n return failure(\n `Animation stability sampling timed out after ${timeoutMs}ms.`,\n result,\n );\n }\n return result.stable\n ? success(\n `Bounding box stayed stable across ${frameCount} animation frames.`,\n result,\n )\n : failure(\n `Bounding box changed across ${frameCount} animation frames.`,\n result,\n );\n } catch (error) {\n return failure(\n `Could not sample animation stability for ${targetName(target)}.`,\n errorDetails(error),\n );\n }\n}\n\nexport const automationAssertions = {\n visible,\n hidden,\n insideViewport,\n notClipped,\n notOverlapping,\n hitTestReachable,\n sharedEdges,\n evenSpacing,\n stableAfterAnimation,\n};\n"],"mappings":"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","names":[]}
|
package/dist/testing/browser.cjs
CHANGED
|
@@ -39,6 +39,7 @@ var import_node_path = __toESM(require("node:path"), 1);
|
|
|
39
39
|
var import_node_url = require("node:url");
|
|
40
40
|
var import_tracing = require("../tracing/index.js");
|
|
41
41
|
var import_trace_testing = require("../trace-testing/index.js");
|
|
42
|
+
var import_geometry = require("./geometry.js");
|
|
42
43
|
const DEFAULT_VIEWPORT = {
|
|
43
44
|
width: 1280,
|
|
44
45
|
height: 800
|
|
@@ -808,15 +809,6 @@ function collectMaxPaintOrders(snapshot) {
|
|
|
808
809
|
}
|
|
809
810
|
return paintByBackend;
|
|
810
811
|
}
|
|
811
|
-
function axisEdges(rect, axis) {
|
|
812
|
-
return axis === "horizontal" ? { leading: rect.left, trailing: rect.right } : { leading: rect.top, trailing: rect.bottom };
|
|
813
|
-
}
|
|
814
|
-
function axisGap(first, second, axis) {
|
|
815
|
-
const a = axisEdges(first, axis);
|
|
816
|
-
const b = axisEdges(second, axis);
|
|
817
|
-
const [earlier, later] = a.leading <= b.leading ? [a, b] : [b, a];
|
|
818
|
-
return later.leading - earlier.trailing;
|
|
819
|
-
}
|
|
820
812
|
function describeGapConstraint(constraint) {
|
|
821
813
|
const parts = [];
|
|
822
814
|
if (constraint.equals !== void 0) parts.push(`equals ${constraint.equals}`);
|
|
@@ -836,11 +828,6 @@ function gapConstraintViolation(gap, constraint, tolerance) {
|
|
|
836
828
|
}
|
|
837
829
|
return null;
|
|
838
830
|
}
|
|
839
|
-
function rectsOverlap(a, b) {
|
|
840
|
-
return Boolean(
|
|
841
|
-
a && b && a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top
|
|
842
|
-
);
|
|
843
|
-
}
|
|
844
831
|
const COVERAGE_STYLE_KEYS = [
|
|
845
832
|
"opacity",
|
|
846
833
|
"visibility",
|
|
@@ -858,9 +845,6 @@ function colorAlphaIsZero(color) {
|
|
|
858
845
|
const parts = match[1].split(",").map((s) => s.trim());
|
|
859
846
|
return parts.length >= 4 && parseFloat(parts[3]) === 0;
|
|
860
847
|
}
|
|
861
|
-
function snapshotRectsOverlap(a, b) {
|
|
862
|
-
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
|
|
863
|
-
}
|
|
864
848
|
function indexLayoutNodes(doc, strings) {
|
|
865
849
|
const byNode = /* @__PURE__ */ new Map();
|
|
866
850
|
const nodeIndex = doc.layout?.nodeIndex ?? [];
|
|
@@ -945,7 +929,7 @@ function collectCoverageCandidates(doc, strings, targetNodeIndex) {
|
|
|
945
929
|
if (nodeType[ni] !== 1) continue;
|
|
946
930
|
if (ni === targetNodeIndex) continue;
|
|
947
931
|
if (isSnapshotAncestor(parentIndex, ni, targetNodeIndex) || isSnapshotAncestor(parentIndex, targetNodeIndex, ni)) continue;
|
|
948
|
-
if (!snapshotRectsOverlap(rec.rect, target.rect)) continue;
|
|
932
|
+
if (!(0, import_geometry.snapshotRectsOverlap)(rec.rect, target.rect)) continue;
|
|
949
933
|
const hasContent = snapshotNodeHasOwnContent(nodes, strings, ni);
|
|
950
934
|
if (snapshotNodePaintsNothing(rec.styles, hasContent)) continue;
|
|
951
935
|
covers.push({ description: describeSnapshotNode(nodes, strings, ni), paint: rec.paint, rect: rec.rect });
|
|
@@ -1040,7 +1024,7 @@ function createExpect(page, outputDir) {
|
|
|
1040
1024
|
if (top.paintOrder > lower.paintOrder) return;
|
|
1041
1025
|
const topRect = await firstRect(page, topSelector);
|
|
1042
1026
|
const lowerRect = await firstRect(page, lowerSelector);
|
|
1043
|
-
const overlapNote = rectsOverlap(topRect, lowerRect) ? "" : ` (Note: the rects do not overlap, so there is no shared pixel where front/behind is visually observable \u2014 this is a global paint-order verdict.)`;
|
|
1027
|
+
const overlapNote = (0, import_geometry.rectsOverlap)(topRect, lowerRect) ? "" : ` (Note: the rects do not overlap, so there is no shared pixel where front/behind is visually observable \u2014 this is a global paint-order verdict.)`;
|
|
1044
1028
|
throw new Error(
|
|
1045
1029
|
`Expected "${topSelector}" (paint order ${top.paintOrder}) to paint in front of "${lowerSelector}" (paint order ${lower.paintOrder}), but it paints at or behind it (higher paint order == on top).${overlapNote}`
|
|
1046
1030
|
);
|
|
@@ -1109,8 +1093,7 @@ function createExpect(page, outputDir) {
|
|
|
1109
1093
|
const first = await elementRect(page, firstSelector);
|
|
1110
1094
|
const second = await elementRect(page, secondSelector);
|
|
1111
1095
|
if (!first || !second) throw new Error(`Expected both "${firstSelector}" and "${secondSelector}" to exist.`);
|
|
1112
|
-
|
|
1113
|
-
if (overlaps) {
|
|
1096
|
+
if ((0, import_geometry.rectsOverlap)(first, second)) {
|
|
1114
1097
|
throw new Error(`Expected "${firstSelector}" not to overlap "${secondSelector}".`);
|
|
1115
1098
|
}
|
|
1116
1099
|
},
|
|
@@ -1153,8 +1136,9 @@ function createExpect(page, outputDir) {
|
|
|
1153
1136
|
const first = await firstRect(page, a);
|
|
1154
1137
|
const second = await firstRect(page, b);
|
|
1155
1138
|
if (!first || !second) throw new Error(`Expected both "${a}" and "${b}" to exist.`);
|
|
1156
|
-
const
|
|
1157
|
-
if (
|
|
1139
|
+
const verdict = (0, import_geometry.sharedEdges)([first, second], edge, { tolerance });
|
|
1140
|
+
if (!verdict.pass) {
|
|
1141
|
+
const delta = Number(verdict.details?.maximumDelta);
|
|
1158
1142
|
throw new Error(
|
|
1159
1143
|
`Expected "${a}" and "${b}" to share ${edge} edge within ${tolerance}px, but delta was ${delta.toFixed(2)}px ("${a}".${edge}=${first[edge].toFixed(2)}, "${b}".${edge}=${second[edge].toFixed(2)}). Rects: ${a}=${JSON.stringify(first)}, ${b}=${JSON.stringify(second)}`
|
|
1160
1144
|
);
|
|
@@ -1218,7 +1202,7 @@ function createExpect(page, outputDir) {
|
|
|
1218
1202
|
const first = await firstRect(page, a);
|
|
1219
1203
|
const second = await firstRect(page, b);
|
|
1220
1204
|
if (!first || !second) throw new Error(`Expected both "${a}" and "${b}" to exist.`);
|
|
1221
|
-
const gap = axisGap(first, second, axis);
|
|
1205
|
+
const gap = (0, import_geometry.axisGap)(first, second, axis);
|
|
1222
1206
|
const allowsNegative = options.equals !== void 0 && options.equals < 0 || options.min !== void 0 && options.min < 0;
|
|
1223
1207
|
const rectsNote = `Rects: ${a}=${JSON.stringify(first)}, ${b}=${JSON.stringify(second)}`;
|
|
1224
1208
|
if (gap < -tolerance && !allowsNegative) {
|
|
@@ -1245,16 +1229,24 @@ function createExpect(page, outputDir) {
|
|
|
1245
1229
|
if (!rect) throw new Error(`Expected "${selector}" to exist.`);
|
|
1246
1230
|
boxes.push({ selector, rect });
|
|
1247
1231
|
}
|
|
1248
|
-
const
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1232
|
+
const verdict = (0, import_geometry.evenSpacing)(
|
|
1233
|
+
boxes.map((box) => box.rect),
|
|
1234
|
+
axis,
|
|
1235
|
+
{ tolerance }
|
|
1236
|
+
);
|
|
1237
|
+
if (!verdict.pass) {
|
|
1238
|
+
const sorted = [...boxes].sort(
|
|
1239
|
+
(x, y) => (0, import_geometry.axisEdges)(x.rect, axis).leading - (0, import_geometry.axisEdges)(y.rect, axis).leading
|
|
1240
|
+
);
|
|
1241
|
+
const measuredGaps = verdict.details?.gaps;
|
|
1242
|
+
const gaps = sorted.slice(1).map((box, i) => ({
|
|
1243
|
+
from: sorted[i].selector,
|
|
1244
|
+
to: box.selector,
|
|
1245
|
+
gap: measuredGaps[i]
|
|
1246
|
+
}));
|
|
1247
|
+
const widest = gaps.reduce((acc, g) => g.gap > acc.gap ? g : acc);
|
|
1248
|
+
const narrowest = gaps.reduce((acc, g) => g.gap < acc.gap ? g : acc);
|
|
1249
|
+
const spread = Number(verdict.details?.spread);
|
|
1258
1250
|
const list = gaps.map((g) => `${g.from}\u2192${g.to}=${g.gap.toFixed(2)}px`).join(", ");
|
|
1259
1251
|
throw new Error(
|
|
1260
1252
|
`Expected elements to be evenly spaced along ${axis} within ${tolerance}px, but gap spread was ${spread.toFixed(2)}px (widest ${widest.from}\u2192${widest.to}=${widest.gap.toFixed(2)}px, narrowest ${narrowest.from}\u2192${narrowest.to}=${narrowest.gap.toFixed(2)}px). Gaps: ${list}`
|