@crouton-kit/capture 1.4.24 → 1.4.25
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/capture +97 -20
- package/package.json +1 -1
- package/src/interact.ts +110 -17
package/bin/capture
CHANGED
|
@@ -10944,7 +10944,7 @@ function validateHarAppendBatch(value, source) {
|
|
|
10944
10944
|
return { entries, incompleteLifecycles };
|
|
10945
10945
|
}
|
|
10946
10946
|
function captureVersion() {
|
|
10947
|
-
const version = "1.4.
|
|
10947
|
+
const version = "1.4.25";
|
|
10948
10948
|
return typeof version === "string" && version.length > 0 ? version : "development";
|
|
10949
10949
|
}
|
|
10950
10950
|
function token() {
|
|
@@ -13046,7 +13046,7 @@ function wallTime(value) {
|
|
|
13046
13046
|
}
|
|
13047
13047
|
}
|
|
13048
13048
|
function captureVersion2() {
|
|
13049
|
-
const version = "1.4.
|
|
13049
|
+
const version = "1.4.25";
|
|
13050
13050
|
return typeof version === "string" && version.length > 0 ? version : "development";
|
|
13051
13051
|
}
|
|
13052
13052
|
function deepFreeze(value) {
|
|
@@ -17321,7 +17321,8 @@ function settleAxMatches(kind, input, rawMatches) {
|
|
|
17321
17321
|
}
|
|
17322
17322
|
async function resolveBareTarget(client, input, selector) {
|
|
17323
17323
|
const css = await resolveLiveCss(client, input, selector);
|
|
17324
|
-
if (css.ok
|
|
17324
|
+
if (css.ok && !(CSS_TYPE_SELECTOR.test(selector) && !await hasLayoutBox(client, css.backendNodeId))) return css;
|
|
17325
|
+
if (!css.ok && css.code === "ambiguous") return css;
|
|
17325
17326
|
const normalized2 = normalizeAccessibleName(input);
|
|
17326
17327
|
if (!normalized2) return css;
|
|
17327
17328
|
const matches2 = (await readFullAXTree(client)).filter(
|
|
@@ -17330,6 +17331,52 @@ async function resolveBareTarget(client, input, selector) {
|
|
|
17330
17331
|
const ax = settleAxMatches("ax", input, matches2);
|
|
17331
17332
|
return ax.ok || ax.code === "ambiguous" ? ax : css;
|
|
17332
17333
|
}
|
|
17334
|
+
async function hasLayoutBox(client, backendNodeId2) {
|
|
17335
|
+
try {
|
|
17336
|
+
const response = await client.send("DOM.getBoxModel", { backendNodeId: backendNodeId2 });
|
|
17337
|
+
return isContentQuad(response?.model?.content);
|
|
17338
|
+
} catch (error4) {
|
|
17339
|
+
if (isNoLayoutError(error4)) return false;
|
|
17340
|
+
throw error4;
|
|
17341
|
+
}
|
|
17342
|
+
}
|
|
17343
|
+
function isNoLayoutError(error4) {
|
|
17344
|
+
return error4 instanceof Error && /Node does not have a layout object|Could not compute box model/.test(error4.message);
|
|
17345
|
+
}
|
|
17346
|
+
function notClickable(backendNodeId2, detail, cause) {
|
|
17347
|
+
throw captureError("world", "target_not_clickable", `Resolved target backend:${backendNodeId2} ${detail}`, cause);
|
|
17348
|
+
}
|
|
17349
|
+
function viewportClickPoint(quad, width, height) {
|
|
17350
|
+
const points = [[quad[0], quad[1]], [quad[2], quad[3]], [quad[4], quad[5]], [quad[6], quad[7]]];
|
|
17351
|
+
const cx = points.reduce((sum, p) => sum + p[0], 0) / 4;
|
|
17352
|
+
const cy = points.reduce((sum, p) => sum + p[1], 0) / 4;
|
|
17353
|
+
if (cx >= 0 && cx < width && cy >= 0 && cy < height) return { x: cx, y: cy };
|
|
17354
|
+
const edges = [
|
|
17355
|
+
{ inside: (p) => p[0] >= 0, cut: (a, b) => [0, a[1] + (b[1] - a[1]) * (0 - a[0]) / (b[0] - a[0])] },
|
|
17356
|
+
{ inside: (p) => p[0] <= width, cut: (a, b) => [width, a[1] + (b[1] - a[1]) * (width - a[0]) / (b[0] - a[0])] },
|
|
17357
|
+
{ inside: (p) => p[1] >= 0, cut: (a, b) => [a[0] + (b[0] - a[0]) * (0 - a[1]) / (b[1] - a[1]), 0] },
|
|
17358
|
+
{ inside: (p) => p[1] <= height, cut: (a, b) => [a[0] + (b[0] - a[0]) * (height - a[1]) / (b[1] - a[1]), height] }
|
|
17359
|
+
];
|
|
17360
|
+
let polygon = points;
|
|
17361
|
+
for (const edge of edges) {
|
|
17362
|
+
const input = polygon;
|
|
17363
|
+
polygon = [];
|
|
17364
|
+
input.forEach((current, i) => {
|
|
17365
|
+
const previous = input[(i + input.length - 1) % input.length];
|
|
17366
|
+
if (edge.inside(current)) {
|
|
17367
|
+
if (!edge.inside(previous)) polygon.push(edge.cut(previous, current));
|
|
17368
|
+
polygon.push(current);
|
|
17369
|
+
} else if (edge.inside(previous)) {
|
|
17370
|
+
polygon.push(edge.cut(previous, current));
|
|
17371
|
+
}
|
|
17372
|
+
});
|
|
17373
|
+
if (polygon.length === 0) return null;
|
|
17374
|
+
}
|
|
17375
|
+
if (polygon.length < 3) return null;
|
|
17376
|
+
const x = polygon.reduce((sum, p) => sum + p[0], 0) / polygon.length;
|
|
17377
|
+
const y = polygon.reduce((sum, p) => sum + p[1], 0) / polygon.length;
|
|
17378
|
+
return x >= 0 && x < width && y >= 0 && y < height ? { x, y } : null;
|
|
17379
|
+
}
|
|
17333
17380
|
async function resolveLiveCss(client, input, selector) {
|
|
17334
17381
|
await client.send("DOM.enable");
|
|
17335
17382
|
const { root } = await client.send("DOM.getDocument", { depth: 0 });
|
|
@@ -17404,18 +17451,32 @@ async function axIdentityFor(client, backendNodeId2) {
|
|
|
17404
17451
|
async function clickLiveTarget(client, input, opts = {}) {
|
|
17405
17452
|
const initial = await resolveLiveTarget(client, input);
|
|
17406
17453
|
if (!initial.ok) return initial;
|
|
17407
|
-
await client
|
|
17454
|
+
await scrollIntoView(client, initial.backendNodeId);
|
|
17408
17455
|
const current = await resolveLiveTarget(client, input);
|
|
17409
17456
|
if (!current.ok) return current;
|
|
17410
17457
|
return dispatchResolvedClick(client, current, opts);
|
|
17411
17458
|
}
|
|
17412
17459
|
async function clickResolved(client, resolved, opts = {}) {
|
|
17413
|
-
await client
|
|
17460
|
+
await scrollIntoView(client, resolved.backendNodeId);
|
|
17414
17461
|
return dispatchResolvedClick(client, resolved, opts);
|
|
17415
17462
|
}
|
|
17463
|
+
async function scrollIntoView(client, backendNodeId2) {
|
|
17464
|
+
try {
|
|
17465
|
+
await client.send("DOM.scrollIntoViewIfNeeded", { backendNodeId: backendNodeId2 });
|
|
17466
|
+
} catch (error4) {
|
|
17467
|
+
if (isNoLayoutError(error4)) notClickable(backendNodeId2, "has no layout box to scroll to (hidden, detached, or not rendered).", error4);
|
|
17468
|
+
throw error4;
|
|
17469
|
+
}
|
|
17470
|
+
}
|
|
17416
17471
|
async function dispatchResolvedClick(client, resolved, opts) {
|
|
17417
17472
|
const { backendNodeId: backendNodeId2 } = resolved;
|
|
17418
|
-
|
|
17473
|
+
let boxResponse;
|
|
17474
|
+
try {
|
|
17475
|
+
boxResponse = await client.send("DOM.getBoxModel", { backendNodeId: backendNodeId2 });
|
|
17476
|
+
} catch (error4) {
|
|
17477
|
+
if (isNoLayoutError(error4)) notClickable(backendNodeId2, "has no layout box to click (hidden, detached, or not rendered).", error4);
|
|
17478
|
+
throw error4;
|
|
17479
|
+
}
|
|
17419
17480
|
const quad = boxResponse?.model?.content;
|
|
17420
17481
|
if (!isContentQuad(quad)) {
|
|
17421
17482
|
throw captureError(
|
|
@@ -17425,24 +17486,39 @@ async function dispatchResolvedClick(client, resolved, opts) {
|
|
|
17425
17486
|
{ method: "DOM.getBoxModel", response: boxResponse }
|
|
17426
17487
|
);
|
|
17427
17488
|
}
|
|
17428
|
-
const
|
|
17429
|
-
const
|
|
17489
|
+
const metrics = await client.send("Page.getLayoutMetrics");
|
|
17490
|
+
const viewport2 = metrics.cssVisualViewport;
|
|
17491
|
+
if (!viewport2 || ![viewport2.pageX, viewport2.pageY, viewport2.clientWidth, viewport2.clientHeight].every(Number.isFinite)) {
|
|
17492
|
+
throw captureError("world", "malformed_protocol", "Page.getLayoutMetrics returned no valid CSS visual viewport.", {
|
|
17493
|
+
method: "Page.getLayoutMetrics",
|
|
17494
|
+
response: metrics
|
|
17495
|
+
});
|
|
17496
|
+
}
|
|
17497
|
+
const point = viewportClickPoint(quad, viewport2.clientWidth, viewport2.clientHeight);
|
|
17498
|
+
if (point === null) {
|
|
17499
|
+
notClickable(
|
|
17500
|
+
backendNodeId2,
|
|
17501
|
+
`is outside the ${viewport2.clientWidth}x${viewport2.clientHeight} viewport after scrolling it into view (a fixed or clipped element the page cannot scroll to).`,
|
|
17502
|
+
{ method: "DOM.getBoxModel", response: boxResponse }
|
|
17503
|
+
);
|
|
17504
|
+
}
|
|
17505
|
+
const { x, y } = point;
|
|
17430
17506
|
const dispatchX = Math.round(x);
|
|
17431
17507
|
const dispatchY = Math.round(y);
|
|
17432
17508
|
let hitTestReceiverBackendNodeId;
|
|
17433
17509
|
if (opts.inspectHitTest) {
|
|
17434
|
-
|
|
17435
|
-
|
|
17436
|
-
|
|
17437
|
-
|
|
17438
|
-
|
|
17439
|
-
response: metrics
|
|
17510
|
+
let hitTest;
|
|
17511
|
+
try {
|
|
17512
|
+
hitTest = await client.send("DOM.getNodeForLocation", {
|
|
17513
|
+
x: Math.round(x + viewport2.pageX),
|
|
17514
|
+
y: Math.round(y + viewport2.pageY)
|
|
17440
17515
|
});
|
|
17516
|
+
} catch (error4) {
|
|
17517
|
+
if (error4 instanceof Error && error4.message.includes("No node found at given location")) {
|
|
17518
|
+
notClickable(backendNodeId2, `has no element at its click point x=${dispatchX} y=${dispatchY}; nothing there can receive the click.`, error4);
|
|
17519
|
+
}
|
|
17520
|
+
throw error4;
|
|
17441
17521
|
}
|
|
17442
|
-
const hitTest = await client.send("DOM.getNodeForLocation", {
|
|
17443
|
-
x: Math.round(x + viewport2.pageX),
|
|
17444
|
-
y: Math.round(y + viewport2.pageY)
|
|
17445
|
-
});
|
|
17446
17522
|
if (typeof hitTest.backendNodeId !== "number" || !Number.isSafeInteger(hitTest.backendNodeId) || hitTest.backendNodeId <= 0) {
|
|
17447
17523
|
throw captureError(
|
|
17448
17524
|
"world",
|
|
@@ -17555,7 +17631,7 @@ async function scrollResolved(client, resolved, to, opts = {}) {
|
|
|
17555
17631
|
}
|
|
17556
17632
|
}
|
|
17557
17633
|
}
|
|
17558
|
-
var ACCEPTED_LIVE_PREFIXES, CANDIDATE_LIMIT;
|
|
17634
|
+
var ACCEPTED_LIVE_PREFIXES, CANDIDATE_LIMIT, CSS_TYPE_SELECTOR;
|
|
17559
17635
|
var init_interact = __esm({
|
|
17560
17636
|
"src/interact.ts"() {
|
|
17561
17637
|
init_a11y();
|
|
@@ -17564,6 +17640,7 @@ var init_interact = __esm({
|
|
|
17564
17640
|
init_selector();
|
|
17565
17641
|
ACCEPTED_LIVE_PREFIXES = ["css", "ax:", "axid:", "backend:"];
|
|
17566
17642
|
CANDIDATE_LIMIT = 10;
|
|
17643
|
+
CSS_TYPE_SELECTOR = /^[A-Za-z][A-Za-z0-9-]*$/;
|
|
17567
17644
|
}
|
|
17568
17645
|
});
|
|
17569
17646
|
|
|
@@ -267518,7 +267595,7 @@ I/O contract: flags and positionals on input; one rendered prose block on stdout
|
|
|
267518
267595
|
A CDP-enabled browser must be running; \`capture tab list\` is the probe, and \`capture tab launch\` starts one capture owns and reaps when nothing is (never hand-roll a detached browser \u2014 nothing reaps that). Capture never signals a browser it did not start, so attaching to your own with --port is unchanged. CDP_PORT / CDP_TARGET pin the browser + tab for target-selecting commands \u2014 precedence: explicit flag > active session > env. \`session start\` adopts only an explicit --target. When CRTR_NODE_ID is set (a crtr agent node), a browser command with no --port, no --target and no active session of that node is refused instead of falling back to CDP_PORT, CDP_TARGET or localhost discovery, because the browser found could belong to another node: run \`capture session start --url <url>\`, or pass --port or --target. \`capture tab list\` and commands that only read a recorded artifact are not refused.`;
|
|
267519
267596
|
}
|
|
267520
267597
|
function printVersion() {
|
|
267521
|
-
const declared = "1.4.
|
|
267598
|
+
const declared = "1.4.25";
|
|
267522
267599
|
if (declared) {
|
|
267523
267600
|
console.log(declared);
|
|
267524
267601
|
return;
|
package/package.json
CHANGED
package/src/interact.ts
CHANGED
|
@@ -238,7 +238,11 @@ async function resolveBareTarget(
|
|
|
238
238
|
selector: string,
|
|
239
239
|
): Promise<ResolvedTarget | ResolutionFailure> {
|
|
240
240
|
const css = await resolveLiveCss(client, input, selector);
|
|
241
|
-
|
|
241
|
+
// A one-word accessible name is also a valid CSS type selector: `Title` is
|
|
242
|
+
// the <title> element in <head>, which has no layout box. That match cannot
|
|
243
|
+
// be driven, so it must not shadow the accessible name.
|
|
244
|
+
if (css.ok && !(CSS_TYPE_SELECTOR.test(selector) && !(await hasLayoutBox(client, css.backendNodeId)))) return css;
|
|
245
|
+
if (!css.ok && css.code === 'ambiguous') return css;
|
|
242
246
|
|
|
243
247
|
const normalized = normalizeAccessibleName(input);
|
|
244
248
|
if (!normalized) return css;
|
|
@@ -252,6 +256,67 @@ async function resolveBareTarget(
|
|
|
252
256
|
return ax.ok || ax.code === 'ambiguous' ? ax : css;
|
|
253
257
|
}
|
|
254
258
|
|
|
259
|
+
/** A selector that is a single tag name — the only CSS shape an accessible name can collide with. */
|
|
260
|
+
const CSS_TYPE_SELECTOR = /^[A-Za-z][A-Za-z0-9-]*$/;
|
|
261
|
+
|
|
262
|
+
async function hasLayoutBox(client: LiveClient, backendNodeId: number): Promise<boolean> {
|
|
263
|
+
try {
|
|
264
|
+
const response = (await client.send('DOM.getBoxModel', { backendNodeId })) as { model?: { content?: unknown } } | undefined;
|
|
265
|
+
return isContentQuad(response?.model?.content);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (isNoLayoutError(error)) return false;
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** The renderer's messages for a node with no layout box (`<title>`, `display:none`, detached). */
|
|
273
|
+
function isNoLayoutError(error: unknown): boolean {
|
|
274
|
+
return error instanceof Error && /Node does not have a layout object|Could not compute box model/.test(error.message);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function notClickable(backendNodeId: number, detail: string, cause?: unknown): never {
|
|
278
|
+
throw captureError('world', 'target_not_clickable', `Resolved target backend:${backendNodeId} ${detail}`, cause);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Where to click a content quad: its center when that is inside the viewport,
|
|
283
|
+
* otherwise the center of the part of the quad that is. A tall or wide element
|
|
284
|
+
* (or a fixed one larger than the window) can have its center outside the
|
|
285
|
+
* viewport after scrolling; a mouse event there reaches no element.
|
|
286
|
+
*/
|
|
287
|
+
function viewportClickPoint(quad: number[], width: number, height: number): { x: number; y: number } | null {
|
|
288
|
+
const points: Array<[number, number]> = [[quad[0], quad[1]], [quad[2], quad[3]], [quad[4], quad[5]], [quad[6], quad[7]]];
|
|
289
|
+
const cx = points.reduce((sum, p) => sum + p[0], 0) / 4;
|
|
290
|
+
const cy = points.reduce((sum, p) => sum + p[1], 0) / 4;
|
|
291
|
+
if (cx >= 0 && cx < width && cy >= 0 && cy < height) return { x: cx, y: cy };
|
|
292
|
+
// Sutherland–Hodgman clip of the (convex) quad against the viewport rectangle.
|
|
293
|
+
const edges: Array<{ inside: (p: [number, number]) => boolean; cut: (a: [number, number], b: [number, number]) => [number, number] }> = [
|
|
294
|
+
{ inside: (p) => p[0] >= 0, cut: (a, b) => [0, a[1] + ((b[1] - a[1]) * (0 - a[0])) / (b[0] - a[0])] },
|
|
295
|
+
{ inside: (p) => p[0] <= width, cut: (a, b) => [width, a[1] + ((b[1] - a[1]) * (width - a[0])) / (b[0] - a[0])] },
|
|
296
|
+
{ inside: (p) => p[1] >= 0, cut: (a, b) => [a[0] + ((b[0] - a[0]) * (0 - a[1])) / (b[1] - a[1]), 0] },
|
|
297
|
+
{ inside: (p) => p[1] <= height, cut: (a, b) => [a[0] + ((b[0] - a[0]) * (height - a[1])) / (b[1] - a[1]), height] },
|
|
298
|
+
];
|
|
299
|
+
let polygon = points;
|
|
300
|
+
for (const edge of edges) {
|
|
301
|
+
const input = polygon;
|
|
302
|
+
polygon = [];
|
|
303
|
+
input.forEach((current, i) => {
|
|
304
|
+
const previous = input[(i + input.length - 1) % input.length];
|
|
305
|
+
if (edge.inside(current)) {
|
|
306
|
+
if (!edge.inside(previous)) polygon.push(edge.cut(previous, current));
|
|
307
|
+
polygon.push(current);
|
|
308
|
+
} else if (edge.inside(previous)) {
|
|
309
|
+
polygon.push(edge.cut(previous, current));
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
if (polygon.length === 0) return null;
|
|
313
|
+
}
|
|
314
|
+
if (polygon.length < 3) return null;
|
|
315
|
+
const x = polygon.reduce((sum, p) => sum + p[0], 0) / polygon.length;
|
|
316
|
+
const y = polygon.reduce((sum, p) => sum + p[1], 0) / polygon.length;
|
|
317
|
+
return x >= 0 && x < width && y >= 0 && y < height ? { x, y } : null;
|
|
318
|
+
}
|
|
319
|
+
|
|
255
320
|
async function resolveLiveCss(
|
|
256
321
|
client: LiveClient,
|
|
257
322
|
input: string,
|
|
@@ -368,7 +433,7 @@ export async function clickLiveTarget(
|
|
|
368
433
|
const initial = await resolveLiveTarget(client, input);
|
|
369
434
|
if (!initial.ok) return initial;
|
|
370
435
|
|
|
371
|
-
await client
|
|
436
|
+
await scrollIntoView(client, initial.backendNodeId);
|
|
372
437
|
|
|
373
438
|
const current = await resolveLiveTarget(client, input);
|
|
374
439
|
if (!current.ok) return current;
|
|
@@ -380,19 +445,32 @@ export async function clickResolved(
|
|
|
380
445
|
resolved: ResolvedTarget,
|
|
381
446
|
opts: { mark?: string; inspectHitTest?: boolean; includeHitTestReceiver?: boolean } = {},
|
|
382
447
|
): Promise<ClickDispatch> {
|
|
383
|
-
await client
|
|
448
|
+
await scrollIntoView(client, resolved.backendNodeId);
|
|
384
449
|
return dispatchResolvedClick(client, resolved, opts);
|
|
385
450
|
}
|
|
386
451
|
|
|
452
|
+
async function scrollIntoView(client: LiveClient, backendNodeId: number): Promise<void> {
|
|
453
|
+
try {
|
|
454
|
+
await client.send('DOM.scrollIntoViewIfNeeded', { backendNodeId });
|
|
455
|
+
} catch (error) {
|
|
456
|
+
if (isNoLayoutError(error)) notClickable(backendNodeId, 'has no layout box to scroll to (hidden, detached, or not rendered).', error);
|
|
457
|
+
throw error;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
387
461
|
async function dispatchResolvedClick(
|
|
388
462
|
client: LiveClient,
|
|
389
463
|
resolved: ResolvedTarget,
|
|
390
464
|
opts: { mark?: string; inspectHitTest?: boolean; includeHitTestReceiver?: boolean },
|
|
391
465
|
): Promise<ClickDispatch> {
|
|
392
466
|
const { backendNodeId } = resolved;
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
| undefined;
|
|
467
|
+
let boxResponse: { model?: { content?: unknown } } | undefined;
|
|
468
|
+
try {
|
|
469
|
+
boxResponse = (await client.send('DOM.getBoxModel', { backendNodeId })) as { model?: { content?: unknown } } | undefined;
|
|
470
|
+
} catch (error) {
|
|
471
|
+
if (isNoLayoutError(error)) notClickable(backendNodeId, 'has no layout box to click (hidden, detached, or not rendered).', error);
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
396
474
|
|
|
397
475
|
// Content quad is [x1,y1, x2,y2, x3,y3, x4,y4] — validated here, once, for
|
|
398
476
|
// every click consumer (page click and motion rec's one-shot alike).
|
|
@@ -405,24 +483,39 @@ async function dispatchResolvedClick(
|
|
|
405
483
|
{ method: 'DOM.getBoxModel', response: boxResponse },
|
|
406
484
|
);
|
|
407
485
|
}
|
|
408
|
-
const
|
|
409
|
-
const
|
|
486
|
+
const metrics = (await client.send('Page.getLayoutMetrics')) as { cssVisualViewport?: { pageX: number; pageY: number; clientWidth: number; clientHeight: number } };
|
|
487
|
+
const viewport = metrics.cssVisualViewport;
|
|
488
|
+
if (!viewport || ![viewport.pageX, viewport.pageY, viewport.clientWidth, viewport.clientHeight].every(Number.isFinite)) {
|
|
489
|
+
throw captureError('world', 'malformed_protocol', 'Page.getLayoutMetrics returned no valid CSS visual viewport.', {
|
|
490
|
+
method: 'Page.getLayoutMetrics', response: metrics,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
const point = viewportClickPoint(quad, viewport.clientWidth, viewport.clientHeight);
|
|
494
|
+
if (point === null) {
|
|
495
|
+
notClickable(
|
|
496
|
+
backendNodeId,
|
|
497
|
+
`is outside the ${viewport.clientWidth}x${viewport.clientHeight} viewport after scrolling it into view (a fixed or clipped element the page cannot scroll to).`,
|
|
498
|
+
{ method: 'DOM.getBoxModel', response: boxResponse },
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
const { x, y } = point;
|
|
410
502
|
|
|
411
503
|
const dispatchX = Math.round(x);
|
|
412
504
|
const dispatchY = Math.round(y);
|
|
413
505
|
let hitTestReceiverBackendNodeId: number | undefined;
|
|
414
506
|
if (opts.inspectHitTest) {
|
|
415
507
|
// The box quad and mouse dispatch use viewport coordinates; DOM.getNodeForLocation uses page coordinates.
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
508
|
+
let hitTest: { backendNodeId?: unknown };
|
|
509
|
+
try {
|
|
510
|
+
hitTest = (await client.send('DOM.getNodeForLocation', {
|
|
511
|
+
x: Math.round(x + viewport.pageX), y: Math.round(y + viewport.pageY),
|
|
512
|
+
})) as { backendNodeId?: unknown };
|
|
513
|
+
} catch (error) {
|
|
514
|
+
if (error instanceof Error && error.message.includes('No node found at given location')) {
|
|
515
|
+
notClickable(backendNodeId, `has no element at its click point x=${dispatchX} y=${dispatchY}; nothing there can receive the click.`, error);
|
|
516
|
+
}
|
|
517
|
+
throw error;
|
|
422
518
|
}
|
|
423
|
-
const hitTest = (await client.send('DOM.getNodeForLocation', {
|
|
424
|
-
x: Math.round(x + viewport.pageX), y: Math.round(y + viewport.pageY),
|
|
425
|
-
})) as { backendNodeId?: unknown };
|
|
426
519
|
if (typeof hitTest.backendNodeId !== 'number' || !Number.isSafeInteger(hitTest.backendNodeId) || hitTest.backendNodeId <= 0) {
|
|
427
520
|
throw captureError(
|
|
428
521
|
'world',
|