@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.
Files changed (149) hide show
  1. package/bin/axe.js +313 -98
  2. package/bin/wood.js +7 -1
  3. package/dist/automation/actions.cjs +17 -0
  4. package/dist/automation/actions.cjs.map +1 -0
  5. package/dist/automation/actions.d.cts +57 -0
  6. package/dist/automation/actions.d.ts +57 -0
  7. package/dist/automation/actions.js +1 -0
  8. package/dist/automation/actions.js.map +1 -0
  9. package/dist/automation/actions_executor.cjs +101 -0
  10. package/dist/automation/actions_executor.cjs.map +1 -0
  11. package/dist/automation/actions_executor.d.cts +16 -0
  12. package/dist/automation/actions_executor.d.ts +16 -0
  13. package/dist/automation/actions_executor.js +76 -0
  14. package/dist/automation/actions_executor.js.map +1 -0
  15. package/dist/automation/connection.cjs +86 -0
  16. package/dist/automation/connection.cjs.map +1 -0
  17. package/dist/automation/connection.d.cts +18 -0
  18. package/dist/automation/connection.d.ts +18 -0
  19. package/dist/automation/connection.js +51 -0
  20. package/dist/automation/connection.js.map +1 -0
  21. package/dist/automation/errors.cjs +50 -0
  22. package/dist/automation/errors.cjs.map +1 -0
  23. package/dist/automation/errors.d.cts +31 -0
  24. package/dist/automation/errors.d.ts +31 -0
  25. package/dist/automation/errors.js +24 -0
  26. package/dist/automation/errors.js.map +1 -0
  27. package/dist/automation/index.cjs +49 -0
  28. package/dist/automation/index.cjs.map +1 -0
  29. package/dist/automation/index.d.cts +16 -0
  30. package/dist/automation/index.d.ts +16 -0
  31. package/dist/automation/index.js +15 -0
  32. package/dist/automation/index.js.map +1 -0
  33. package/dist/automation/introspection_reader.cjs +75 -0
  34. package/dist/automation/introspection_reader.cjs.map +1 -0
  35. package/dist/automation/introspection_reader.d.cts +20 -0
  36. package/dist/automation/introspection_reader.d.ts +20 -0
  37. package/dist/automation/introspection_reader.js +48 -0
  38. package/dist/automation/introspection_reader.js.map +1 -0
  39. package/dist/automation/locator.cjs +102 -0
  40. package/dist/automation/locator.cjs.map +1 -0
  41. package/dist/automation/locator.d.cts +51 -0
  42. package/dist/automation/locator.d.ts +51 -0
  43. package/dist/automation/locator.js +75 -0
  44. package/dist/automation/locator.js.map +1 -0
  45. package/dist/automation/observation.cjs +17 -0
  46. package/dist/automation/observation.cjs.map +1 -0
  47. package/dist/automation/observation.d.cts +44 -0
  48. package/dist/automation/observation.d.ts +44 -0
  49. package/dist/automation/observation.js +1 -0
  50. package/dist/automation/observation.js.map +1 -0
  51. package/dist/automation/receipts.cjs +17 -0
  52. package/dist/automation/receipts.cjs.map +1 -0
  53. package/dist/automation/receipts.d.cts +27 -0
  54. package/dist/automation/receipts.d.ts +27 -0
  55. package/dist/automation/receipts.js +1 -0
  56. package/dist/automation/receipts.js.map +1 -0
  57. package/dist/automation/scenario.cjs +696 -0
  58. package/dist/automation/scenario.cjs.map +1 -0
  59. package/dist/automation/scenario.d.cts +89 -0
  60. package/dist/automation/scenario.d.ts +89 -0
  61. package/dist/automation/scenario.js +660 -0
  62. package/dist/automation/scenario.js.map +1 -0
  63. package/dist/automation/scrolling.cjs +17 -0
  64. package/dist/automation/scrolling.cjs.map +1 -0
  65. package/dist/automation/scrolling.d.cts +51 -0
  66. package/dist/automation/scrolling.d.ts +51 -0
  67. package/dist/automation/scrolling.js +1 -0
  68. package/dist/automation/scrolling.js.map +1 -0
  69. package/dist/automation/scrolling_executor.cjs +109 -0
  70. package/dist/automation/scrolling_executor.cjs.map +1 -0
  71. package/dist/automation/scrolling_executor.d.cts +13 -0
  72. package/dist/automation/scrolling_executor.d.ts +13 -0
  73. package/dist/automation/scrolling_executor.js +84 -0
  74. package/dist/automation/scrolling_executor.js.map +1 -0
  75. package/dist/automation/target_identity.cjs +17 -0
  76. package/dist/automation/target_identity.cjs.map +1 -0
  77. package/dist/automation/target_identity.d.cts +28 -0
  78. package/dist/automation/target_identity.d.ts +28 -0
  79. package/dist/automation/target_identity.js +1 -0
  80. package/dist/automation/target_identity.js.map +1 -0
  81. package/dist/automation/target_registry.cjs +148 -0
  82. package/dist/automation/target_registry.cjs.map +1 -0
  83. package/dist/automation/target_registry.d.cts +32 -0
  84. package/dist/automation/target_registry.d.ts +32 -0
  85. package/dist/automation/target_registry.js +122 -0
  86. package/dist/automation/target_registry.js.map +1 -0
  87. package/dist/automation/waits.cjs +17 -0
  88. package/dist/automation/waits.cjs.map +1 -0
  89. package/dist/automation/waits.d.cts +44 -0
  90. package/dist/automation/waits.d.ts +44 -0
  91. package/dist/automation/waits.js +1 -0
  92. package/dist/automation/waits.js.map +1 -0
  93. package/dist/automation/waits_executor.cjs +104 -0
  94. package/dist/automation/waits_executor.cjs.map +1 -0
  95. package/dist/automation/waits_executor.d.cts +14 -0
  96. package/dist/automation/waits_executor.d.ts +14 -0
  97. package/dist/automation/waits_executor.js +79 -0
  98. package/dist/automation/waits_executor.js.map +1 -0
  99. package/dist/client/index.d.cts +1 -0
  100. package/dist/client/index.d.ts +1 -0
  101. package/dist/codegen/renderer_app_generator.cjs +16 -1
  102. package/dist/codegen/renderer_app_generator.cjs.map +1 -1
  103. package/dist/codegen/renderer_app_generator.js +16 -1
  104. package/dist/codegen/renderer_app_generator.js.map +1 -1
  105. package/dist/index.d.cts +1 -0
  106. package/dist/index.d.ts +1 -0
  107. package/dist/navigation/automation_introspection.cjs +110 -0
  108. package/dist/navigation/automation_introspection.cjs.map +1 -0
  109. package/dist/navigation/automation_introspection.d.cts +60 -0
  110. package/dist/navigation/automation_introspection.d.ts +60 -0
  111. package/dist/navigation/automation_introspection.js +85 -0
  112. package/dist/navigation/automation_introspection.js.map +1 -0
  113. package/dist/navigation/index.cjs +7 -2
  114. package/dist/navigation/index.cjs.map +1 -1
  115. package/dist/navigation/index.d.cts +2 -0
  116. package/dist/navigation/index.d.ts +2 -0
  117. package/dist/navigation/index.js +4 -1
  118. package/dist/navigation/index.js.map +1 -1
  119. package/dist/testing/automation_assertions.cjs +417 -0
  120. package/dist/testing/automation_assertions.cjs.map +1 -0
  121. package/dist/testing/automation_assertions.d.cts +44 -0
  122. package/dist/testing/automation_assertions.d.ts +44 -0
  123. package/dist/testing/automation_assertions.js +385 -0
  124. package/dist/testing/automation_assertions.js.map +1 -0
  125. package/dist/testing/browser.cjs +26 -34
  126. package/dist/testing/browser.cjs.map +1 -1
  127. package/dist/testing/browser.d.cts +2 -1
  128. package/dist/testing/browser.d.ts +2 -1
  129. package/dist/testing/browser.js +30 -31
  130. package/dist/testing/browser.js.map +1 -1
  131. package/dist/testing/create_controller_harness.d.cts +1 -0
  132. package/dist/testing/create_controller_harness.d.ts +1 -0
  133. package/dist/testing/geometry.cjs +109 -0
  134. package/dist/testing/geometry.cjs.map +1 -0
  135. package/dist/testing/geometry.d.cts +45 -0
  136. package/dist/testing/geometry.d.ts +45 -0
  137. package/dist/testing/geometry.js +79 -0
  138. package/dist/testing/geometry.js.map +1 -0
  139. package/dist/testing/index.cjs +31 -2
  140. package/dist/testing/index.cjs.map +1 -1
  141. package/dist/testing/index.d.cts +4 -0
  142. package/dist/testing/index.d.ts +4 -0
  143. package/dist/testing/index.js +31 -1
  144. package/dist/testing/index.js.map +1 -1
  145. package/dist/tracing/trace_hub.cjs +3 -0
  146. package/dist/tracing/trace_hub.cjs.map +1 -1
  147. package/dist/tracing/trace_hub.js +3 -0
  148. package/dist/tracing/trace_hub.js.map +1 -1
  149. 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":[]}
@@ -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
- const overlaps = first.left < second.right && first.right > second.left && first.top < second.bottom && first.bottom > second.top;
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 delta = Math.abs(first[edge] - second[edge]);
1157
- if (delta > tolerance) {
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 sorted = [...boxes].sort((x, y) => axisEdges(x.rect, axis).leading - axisEdges(y.rect, axis).leading);
1249
- const gaps = sorted.slice(1).map((box, i) => ({
1250
- from: sorted[i].selector,
1251
- to: box.selector,
1252
- gap: axisGap(sorted[i].rect, box.rect, axis)
1253
- }));
1254
- const widest = gaps.reduce((acc, g) => g.gap > acc.gap ? g : acc);
1255
- const narrowest = gaps.reduce((acc, g) => g.gap < acc.gap ? g : acc);
1256
- const spread = widest.gap - narrowest.gap;
1257
- if (spread > tolerance) {
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}`