browser-pilot 0.0.9 → 0.0.11
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/README.md +1 -0
- package/dist/actions.cjs +16 -7
- package/dist/actions.d.cts +2 -2
- package/dist/actions.d.ts +2 -2
- package/dist/actions.mjs +1 -1
- package/dist/browser.cjs +1619 -303
- package/dist/browser.d.cts +22 -5
- package/dist/browser.d.ts +22 -5
- package/dist/browser.mjs +3 -3
- package/dist/{chunk-R3PS4PCM.mjs → chunk-BRAFQUMG.mjs} +34 -12
- package/dist/{chunk-KKW2SZLV.mjs → chunk-FAUNIZR7.mjs} +18 -8
- package/dist/{chunk-7OSR2CAE.mjs → chunk-JHAF52FA.mjs} +1611 -301
- package/dist/cli.mjs +2349 -351
- package/dist/index.cjs +1669 -327
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +3 -3
- package/dist/providers.cjs +34 -12
- package/dist/providers.mjs +1 -1
- package/dist/{types-DOGsEYQa.d.ts → types-DtGF3yGl.d.ts} +39 -11
- package/dist/{types-CYw-7vx1.d.cts → types-GWuQJs_e.d.cts} +39 -11
- package/package.json +1 -1
package/dist/browser.cjs
CHANGED
|
@@ -463,7 +463,12 @@ var ElementNotFoundError = class extends Error {
|
|
|
463
463
|
hints;
|
|
464
464
|
constructor(selectors, hints) {
|
|
465
465
|
const selectorList = Array.isArray(selectors) ? selectors : [selectors];
|
|
466
|
-
|
|
466
|
+
let msg = `Element not found: ${selectorList.join(", ")}`;
|
|
467
|
+
if (hints?.length) {
|
|
468
|
+
msg += `. Did you mean: ${hints.slice(0, 3).map((h) => `${h.element.ref} (${h.element.role} "${h.element.name}")`).join(", ")}`;
|
|
469
|
+
}
|
|
470
|
+
msg += `. Run 'bp snapshot' to see available elements.`;
|
|
471
|
+
super(msg);
|
|
467
472
|
this.name = "ElementNotFoundError";
|
|
468
473
|
this.selectors = selectorList;
|
|
469
474
|
this.hints = hints;
|
|
@@ -471,7 +476,8 @@ var ElementNotFoundError = class extends Error {
|
|
|
471
476
|
};
|
|
472
477
|
var TimeoutError = class extends Error {
|
|
473
478
|
constructor(message = "Operation timed out") {
|
|
474
|
-
|
|
479
|
+
const msg = message.includes("bp snapshot") ? message : `${message}. Run 'bp snapshot' to check current page state.`;
|
|
480
|
+
super(msg);
|
|
475
481
|
this.name = "TimeoutError";
|
|
476
482
|
}
|
|
477
483
|
};
|
|
@@ -554,7 +560,7 @@ var BatchExecutor = class {
|
|
|
554
560
|
}
|
|
555
561
|
case "click": {
|
|
556
562
|
if (!step.selector) throw new Error("click requires selector");
|
|
557
|
-
if (step.waitForNavigation) {
|
|
563
|
+
if (step.waitForNavigation === true) {
|
|
558
564
|
const navPromise = this.page.waitForNavigation({ timeout, optional });
|
|
559
565
|
await this.page.click(step.selector, { timeout, optional });
|
|
560
566
|
await navPromise;
|
|
@@ -569,7 +575,6 @@ var BatchExecutor = class {
|
|
|
569
575
|
await this.page.fill(step.selector, step.value, {
|
|
570
576
|
timeout,
|
|
571
577
|
optional,
|
|
572
|
-
clear: step.clear ?? true,
|
|
573
578
|
blur: step.blur
|
|
574
579
|
});
|
|
575
580
|
return { selectorUsed: this.getUsedSelector(step.selector) };
|
|
@@ -617,7 +622,8 @@ var BatchExecutor = class {
|
|
|
617
622
|
await this.page.submit(step.selector, {
|
|
618
623
|
timeout,
|
|
619
624
|
optional,
|
|
620
|
-
method: step.method ?? "enter+click"
|
|
625
|
+
method: step.method ?? "enter+click",
|
|
626
|
+
waitForNavigation: step.waitForNavigation
|
|
621
627
|
});
|
|
622
628
|
return { selectorUsed: this.getUsedSelector(step.selector) };
|
|
623
629
|
}
|
|
@@ -2347,6 +2353,335 @@ async function waitForNetworkIdle(cdp, options = {}) {
|
|
|
2347
2353
|
});
|
|
2348
2354
|
}
|
|
2349
2355
|
|
|
2356
|
+
// src/browser/actionability.ts
|
|
2357
|
+
var ActionabilityError = class extends Error {
|
|
2358
|
+
failureType;
|
|
2359
|
+
coveringElement;
|
|
2360
|
+
constructor(message, failureType, coveringElement) {
|
|
2361
|
+
super(message);
|
|
2362
|
+
this.name = "ActionabilityError";
|
|
2363
|
+
this.failureType = failureType;
|
|
2364
|
+
this.coveringElement = coveringElement;
|
|
2365
|
+
}
|
|
2366
|
+
};
|
|
2367
|
+
var CHECK_VISIBLE = `function() {
|
|
2368
|
+
// checkVisibility handles display:none, visibility:hidden, content-visibility up the tree
|
|
2369
|
+
if (typeof this.checkVisibility === 'function' && !this.checkVisibility()) {
|
|
2370
|
+
return { actionable: false, reason: 'Element is not visible (checkVisibility failed). Try scrolling or check if a prior action is needed to reveal it.' };
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
var style = getComputedStyle(this);
|
|
2374
|
+
|
|
2375
|
+
if (style.visibility !== 'visible') {
|
|
2376
|
+
return { actionable: false, reason: 'Element has visibility: ' + style.visibility + '. Try scrolling or check if a prior action is needed to reveal it.' };
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
// display:contents elements have no box themselves \u2014 check children
|
|
2380
|
+
if (style.display === 'contents') {
|
|
2381
|
+
var children = this.children;
|
|
2382
|
+
if (children.length === 0) {
|
|
2383
|
+
return { actionable: false, reason: 'Element has display:contents with no children. Try scrolling or check if a prior action is needed to reveal it.' };
|
|
2384
|
+
}
|
|
2385
|
+
for (var i = 0; i < children.length; i++) {
|
|
2386
|
+
var childRect = children[i].getBoundingClientRect();
|
|
2387
|
+
if (childRect.width > 0 && childRect.height > 0) {
|
|
2388
|
+
return { actionable: true };
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
return { actionable: false, reason: 'Element has display:contents but no visible children. Try scrolling or check if a prior action is needed to reveal it.' };
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
var rect = this.getBoundingClientRect();
|
|
2395
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
2396
|
+
return { actionable: false, reason: 'Element has zero size (' + rect.width + 'x' + rect.height + '). Try scrolling or check if a prior action is needed to reveal it.' };
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
return { actionable: true };
|
|
2400
|
+
}`;
|
|
2401
|
+
var CHECK_ENABLED = `function() {
|
|
2402
|
+
// Native disabled property
|
|
2403
|
+
var disableable = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTION', 'OPTGROUP'];
|
|
2404
|
+
if (disableable.indexOf(this.tagName) !== -1 && this.disabled) {
|
|
2405
|
+
return { actionable: false, reason: 'Element is disabled. Check if a prerequisite field needs to be filled first.' };
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
// Check ancestor FIELDSET[disabled]
|
|
2409
|
+
var parent = this.parentElement;
|
|
2410
|
+
while (parent) {
|
|
2411
|
+
if (parent.tagName === 'FIELDSET' && parent.disabled) {
|
|
2412
|
+
// Exception: elements inside the first <legend> of a disabled fieldset are NOT disabled
|
|
2413
|
+
var legend = parent.querySelector(':scope > legend');
|
|
2414
|
+
if (!legend || !legend.contains(this)) {
|
|
2415
|
+
return { actionable: false, reason: 'Element is inside a disabled fieldset. Check if a prerequisite field needs to be filled first.' };
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
parent = parent.parentElement;
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
// aria-disabled="true" walking up ancestor chain (crosses shadow DOM)
|
|
2422
|
+
var node = this;
|
|
2423
|
+
while (node) {
|
|
2424
|
+
if (node.nodeType === 1 && node.getAttribute && node.getAttribute('aria-disabled') === 'true') {
|
|
2425
|
+
return { actionable: false, reason: 'Element or ancestor has aria-disabled="true". Check if a prerequisite field needs to be filled first.' };
|
|
2426
|
+
}
|
|
2427
|
+
if (node.parentElement) {
|
|
2428
|
+
node = node.parentElement;
|
|
2429
|
+
} else if (node.getRootNode && node.getRootNode() !== node) {
|
|
2430
|
+
// Cross shadow DOM boundary
|
|
2431
|
+
var root = node.getRootNode();
|
|
2432
|
+
node = root.host || null;
|
|
2433
|
+
} else {
|
|
2434
|
+
break;
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
return { actionable: true };
|
|
2439
|
+
}`;
|
|
2440
|
+
var CHECK_STABLE = `function() {
|
|
2441
|
+
var self = this;
|
|
2442
|
+
return new Promise(function(resolve) {
|
|
2443
|
+
var maxFrames = 30;
|
|
2444
|
+
var prev = null;
|
|
2445
|
+
var frame = 0;
|
|
2446
|
+
var resolved = false;
|
|
2447
|
+
|
|
2448
|
+
var fallbackTimer = setTimeout(function() {
|
|
2449
|
+
if (!resolved) {
|
|
2450
|
+
resolved = true;
|
|
2451
|
+
resolve({ actionable: false, reason: 'Element stability check timed out (tab may be backgrounded)' });
|
|
2452
|
+
}
|
|
2453
|
+
}, 2000);
|
|
2454
|
+
|
|
2455
|
+
function check() {
|
|
2456
|
+
if (resolved) return;
|
|
2457
|
+
frame++;
|
|
2458
|
+
if (frame > maxFrames) {
|
|
2459
|
+
resolved = true;
|
|
2460
|
+
clearTimeout(fallbackTimer);
|
|
2461
|
+
resolve({ actionable: false, reason: 'Element position not stable after ' + maxFrames + ' frames' });
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
var rect = self.getBoundingClientRect();
|
|
2466
|
+
var cur = { x: rect.x, y: rect.y, w: rect.width, h: rect.height };
|
|
2467
|
+
|
|
2468
|
+
if (prev !== null &&
|
|
2469
|
+
prev.x === cur.x && prev.y === cur.y &&
|
|
2470
|
+
prev.w === cur.w && prev.h === cur.h) {
|
|
2471
|
+
resolved = true;
|
|
2472
|
+
clearTimeout(fallbackTimer);
|
|
2473
|
+
resolve({ actionable: true });
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
prev = cur;
|
|
2478
|
+
requestAnimationFrame(check);
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
requestAnimationFrame(check);
|
|
2482
|
+
});
|
|
2483
|
+
}`;
|
|
2484
|
+
var CHECK_HIT_TARGET = `function(x, y) {
|
|
2485
|
+
// Compute click center if coordinates not provided
|
|
2486
|
+
if (x === undefined || y === undefined) {
|
|
2487
|
+
var rect = this.getBoundingClientRect();
|
|
2488
|
+
x = rect.x + rect.width / 2;
|
|
2489
|
+
y = rect.y + rect.height / 2;
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
function checkPoint(root, px, py) {
|
|
2493
|
+
var method = root.elementsFromPoint || root.msElementsFromPoint;
|
|
2494
|
+
if (!method) return [];
|
|
2495
|
+
return method.call(root, px, py) || [];
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
// Follow only the top-most hit through nested shadow roots.
|
|
2499
|
+
// Accepting any hit in the stack creates false positives for covered elements.
|
|
2500
|
+
var root = document;
|
|
2501
|
+
var topHits = [];
|
|
2502
|
+
var seenRoots = [];
|
|
2503
|
+
while (root && seenRoots.indexOf(root) === -1) {
|
|
2504
|
+
seenRoots.push(root);
|
|
2505
|
+
var hits = checkPoint(root, x, y);
|
|
2506
|
+
if (!hits.length) break;
|
|
2507
|
+
var top = hits[0];
|
|
2508
|
+
topHits.push(top);
|
|
2509
|
+
if (top && top.shadowRoot) {
|
|
2510
|
+
root = top.shadowRoot;
|
|
2511
|
+
continue;
|
|
2512
|
+
}
|
|
2513
|
+
break;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
// Target must be the top-most hit element or an ancestor/descendant
|
|
2517
|
+
for (var j = 0; j < topHits.length; j++) {
|
|
2518
|
+
var hit = topHits[j];
|
|
2519
|
+
if (hit === this || this.contains(hit) || hit.contains(this)) {
|
|
2520
|
+
return { actionable: true };
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
// Report the covering element
|
|
2525
|
+
var top = topHits.length > 0 ? topHits[topHits.length - 1] : null;
|
|
2526
|
+
if (top) {
|
|
2527
|
+
return {
|
|
2528
|
+
actionable: false,
|
|
2529
|
+
reason: 'Element is covered by <' + top.tagName.toLowerCase() + '>' +
|
|
2530
|
+
(top.id ? '#' + top.id : '') +
|
|
2531
|
+
(top.className && typeof top.className === 'string' ? '.' + top.className.split(' ').join('.') : '') +
|
|
2532
|
+
'. Try dismissing overlays first.',
|
|
2533
|
+
coveringElement: {
|
|
2534
|
+
tag: top.tagName.toLowerCase(),
|
|
2535
|
+
id: top.id || undefined,
|
|
2536
|
+
className: (typeof top.className === 'string' && top.className) || undefined
|
|
2537
|
+
}
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
return { actionable: false, reason: 'No element found at click point (' + x + ', ' + y + '). Try scrolling the element into view first.' };
|
|
2542
|
+
}`;
|
|
2543
|
+
var CHECK_EDITABLE = `function() {
|
|
2544
|
+
// Must be an editable element type
|
|
2545
|
+
var tag = this.tagName;
|
|
2546
|
+
var isEditable = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' ||
|
|
2547
|
+
this.isContentEditable;
|
|
2548
|
+
if (!isEditable) {
|
|
2549
|
+
return { actionable: false, reason: 'Element is not an editable type (<' + tag.toLowerCase() + '>). Target an <input>, <textarea>, <select>, or [contenteditable] element instead.' };
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
// Check disabled
|
|
2553
|
+
var disableable = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTION', 'OPTGROUP'];
|
|
2554
|
+
if (disableable.indexOf(tag) !== -1 && this.disabled) {
|
|
2555
|
+
return { actionable: false, reason: 'Element is disabled. Check if a prerequisite field needs to be filled first.' };
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
// Check ancestor FIELDSET[disabled]
|
|
2559
|
+
var parent = this.parentElement;
|
|
2560
|
+
while (parent) {
|
|
2561
|
+
if (parent.tagName === 'FIELDSET' && parent.disabled) {
|
|
2562
|
+
var legend = parent.querySelector(':scope > legend');
|
|
2563
|
+
if (!legend || !legend.contains(this)) {
|
|
2564
|
+
return { actionable: false, reason: 'Element is inside a disabled fieldset. Check if a prerequisite field needs to be filled first.' };
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
parent = parent.parentElement;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
// aria-disabled walking up (crosses shadow DOM)
|
|
2571
|
+
var node = this;
|
|
2572
|
+
while (node) {
|
|
2573
|
+
if (node.nodeType === 1 && node.getAttribute && node.getAttribute('aria-disabled') === 'true') {
|
|
2574
|
+
return { actionable: false, reason: 'Element or ancestor has aria-disabled="true". Check if a prerequisite field needs to be filled first.' };
|
|
2575
|
+
}
|
|
2576
|
+
if (node.parentElement) {
|
|
2577
|
+
node = node.parentElement;
|
|
2578
|
+
} else if (node.getRootNode && node.getRootNode() !== node) {
|
|
2579
|
+
var root = node.getRootNode();
|
|
2580
|
+
node = root.host || null;
|
|
2581
|
+
} else {
|
|
2582
|
+
break;
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
// Check readonly
|
|
2587
|
+
if (this.hasAttribute && this.hasAttribute('readonly')) {
|
|
2588
|
+
return { actionable: false, reason: 'Cannot fill a readonly input. Remove the readonly attribute or target a different element.' };
|
|
2589
|
+
}
|
|
2590
|
+
if (this.getAttribute && this.getAttribute('aria-readonly') === 'true') {
|
|
2591
|
+
return { actionable: false, reason: 'Cannot fill a readonly input (aria-readonly="true"). Remove the attribute or target a different element.' };
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
return { actionable: true };
|
|
2595
|
+
}`;
|
|
2596
|
+
function sleep3(ms) {
|
|
2597
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2598
|
+
}
|
|
2599
|
+
var BACKOFF = [0, 20, 100, 100];
|
|
2600
|
+
async function runCheck(cdp, objectId, check, options) {
|
|
2601
|
+
let script;
|
|
2602
|
+
let awaitPromise = false;
|
|
2603
|
+
const args = [];
|
|
2604
|
+
switch (check) {
|
|
2605
|
+
case "visible":
|
|
2606
|
+
script = CHECK_VISIBLE;
|
|
2607
|
+
break;
|
|
2608
|
+
case "enabled":
|
|
2609
|
+
script = CHECK_ENABLED;
|
|
2610
|
+
break;
|
|
2611
|
+
case "stable":
|
|
2612
|
+
script = CHECK_STABLE;
|
|
2613
|
+
awaitPromise = true;
|
|
2614
|
+
break;
|
|
2615
|
+
case "hitTarget":
|
|
2616
|
+
script = CHECK_HIT_TARGET;
|
|
2617
|
+
if (options?.coordinates) {
|
|
2618
|
+
args.push({ value: options.coordinates.x });
|
|
2619
|
+
args.push({ value: options.coordinates.y });
|
|
2620
|
+
} else {
|
|
2621
|
+
args.push({ value: void 0 });
|
|
2622
|
+
args.push({ value: void 0 });
|
|
2623
|
+
}
|
|
2624
|
+
break;
|
|
2625
|
+
case "editable":
|
|
2626
|
+
script = CHECK_EDITABLE;
|
|
2627
|
+
break;
|
|
2628
|
+
default: {
|
|
2629
|
+
const _exhaustive = check;
|
|
2630
|
+
throw new Error(`Unknown actionability check: ${_exhaustive}`);
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
const params = {
|
|
2634
|
+
functionDeclaration: script,
|
|
2635
|
+
objectId,
|
|
2636
|
+
returnByValue: true,
|
|
2637
|
+
arguments: args
|
|
2638
|
+
};
|
|
2639
|
+
if (awaitPromise) {
|
|
2640
|
+
params["awaitPromise"] = true;
|
|
2641
|
+
}
|
|
2642
|
+
const response = await cdp.send("Runtime.callFunctionOn", params);
|
|
2643
|
+
if (response.exceptionDetails) {
|
|
2644
|
+
return {
|
|
2645
|
+
actionable: false,
|
|
2646
|
+
reason: `Check "${check}" threw: ${response.exceptionDetails.text}`,
|
|
2647
|
+
failureType: check
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
const result = response.result.value;
|
|
2651
|
+
if (!result.actionable) {
|
|
2652
|
+
result.failureType = check;
|
|
2653
|
+
}
|
|
2654
|
+
return result;
|
|
2655
|
+
}
|
|
2656
|
+
async function runChecks(cdp, objectId, checks, options) {
|
|
2657
|
+
for (const check of checks) {
|
|
2658
|
+
const result = await runCheck(cdp, objectId, check, options);
|
|
2659
|
+
if (!result.actionable) {
|
|
2660
|
+
return result;
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
return { actionable: true };
|
|
2664
|
+
}
|
|
2665
|
+
async function ensureActionable(cdp, objectId, checks, options) {
|
|
2666
|
+
const timeout = options?.timeout ?? 3e4;
|
|
2667
|
+
const start = Date.now();
|
|
2668
|
+
let attempt = 0;
|
|
2669
|
+
while (true) {
|
|
2670
|
+
const result = await runChecks(cdp, objectId, checks, options);
|
|
2671
|
+
if (result.actionable) return;
|
|
2672
|
+
if (Date.now() - start >= timeout) {
|
|
2673
|
+
throw new ActionabilityError(
|
|
2674
|
+
`Element not actionable: ${result.reason}`,
|
|
2675
|
+
result.failureType,
|
|
2676
|
+
result.coveringElement
|
|
2677
|
+
);
|
|
2678
|
+
}
|
|
2679
|
+
const delay = attempt < BACKOFF.length ? BACKOFF[attempt] ?? 0 : 500;
|
|
2680
|
+
if (delay > 0) await sleep3(delay);
|
|
2681
|
+
attempt++;
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2350
2685
|
// src/browser/fuzzy-match.ts
|
|
2351
2686
|
function jaroWinkler(a, b) {
|
|
2352
2687
|
if (a.length === 0 && b.length === 0) return 0;
|
|
@@ -2592,8 +2927,180 @@ async function generateHints(page, failedSelectors, actionType, maxHints = 3) {
|
|
|
2592
2927
|
return diversifyHints(matches, maxHints);
|
|
2593
2928
|
}
|
|
2594
2929
|
|
|
2930
|
+
// src/browser/keyboard.ts
|
|
2931
|
+
var US_KEYBOARD = {
|
|
2932
|
+
// Letters (lowercase)
|
|
2933
|
+
a: { key: "a", code: "KeyA", keyCode: 65, text: "a" },
|
|
2934
|
+
b: { key: "b", code: "KeyB", keyCode: 66, text: "b" },
|
|
2935
|
+
c: { key: "c", code: "KeyC", keyCode: 67, text: "c" },
|
|
2936
|
+
d: { key: "d", code: "KeyD", keyCode: 68, text: "d" },
|
|
2937
|
+
e: { key: "e", code: "KeyE", keyCode: 69, text: "e" },
|
|
2938
|
+
f: { key: "f", code: "KeyF", keyCode: 70, text: "f" },
|
|
2939
|
+
g: { key: "g", code: "KeyG", keyCode: 71, text: "g" },
|
|
2940
|
+
h: { key: "h", code: "KeyH", keyCode: 72, text: "h" },
|
|
2941
|
+
i: { key: "i", code: "KeyI", keyCode: 73, text: "i" },
|
|
2942
|
+
j: { key: "j", code: "KeyJ", keyCode: 74, text: "j" },
|
|
2943
|
+
k: { key: "k", code: "KeyK", keyCode: 75, text: "k" },
|
|
2944
|
+
l: { key: "l", code: "KeyL", keyCode: 76, text: "l" },
|
|
2945
|
+
m: { key: "m", code: "KeyM", keyCode: 77, text: "m" },
|
|
2946
|
+
n: { key: "n", code: "KeyN", keyCode: 78, text: "n" },
|
|
2947
|
+
o: { key: "o", code: "KeyO", keyCode: 79, text: "o" },
|
|
2948
|
+
p: { key: "p", code: "KeyP", keyCode: 80, text: "p" },
|
|
2949
|
+
q: { key: "q", code: "KeyQ", keyCode: 81, text: "q" },
|
|
2950
|
+
r: { key: "r", code: "KeyR", keyCode: 82, text: "r" },
|
|
2951
|
+
s: { key: "s", code: "KeyS", keyCode: 83, text: "s" },
|
|
2952
|
+
t: { key: "t", code: "KeyT", keyCode: 84, text: "t" },
|
|
2953
|
+
u: { key: "u", code: "KeyU", keyCode: 85, text: "u" },
|
|
2954
|
+
v: { key: "v", code: "KeyV", keyCode: 86, text: "v" },
|
|
2955
|
+
w: { key: "w", code: "KeyW", keyCode: 87, text: "w" },
|
|
2956
|
+
x: { key: "x", code: "KeyX", keyCode: 88, text: "x" },
|
|
2957
|
+
y: { key: "y", code: "KeyY", keyCode: 89, text: "y" },
|
|
2958
|
+
z: { key: "z", code: "KeyZ", keyCode: 90, text: "z" },
|
|
2959
|
+
// Letters (uppercase)
|
|
2960
|
+
A: { key: "A", code: "KeyA", keyCode: 65, text: "A" },
|
|
2961
|
+
B: { key: "B", code: "KeyB", keyCode: 66, text: "B" },
|
|
2962
|
+
C: { key: "C", code: "KeyC", keyCode: 67, text: "C" },
|
|
2963
|
+
D: { key: "D", code: "KeyD", keyCode: 68, text: "D" },
|
|
2964
|
+
E: { key: "E", code: "KeyE", keyCode: 69, text: "E" },
|
|
2965
|
+
F: { key: "F", code: "KeyF", keyCode: 70, text: "F" },
|
|
2966
|
+
G: { key: "G", code: "KeyG", keyCode: 71, text: "G" },
|
|
2967
|
+
H: { key: "H", code: "KeyH", keyCode: 72, text: "H" },
|
|
2968
|
+
I: { key: "I", code: "KeyI", keyCode: 73, text: "I" },
|
|
2969
|
+
J: { key: "J", code: "KeyJ", keyCode: 74, text: "J" },
|
|
2970
|
+
K: { key: "K", code: "KeyK", keyCode: 75, text: "K" },
|
|
2971
|
+
L: { key: "L", code: "KeyL", keyCode: 76, text: "L" },
|
|
2972
|
+
M: { key: "M", code: "KeyM", keyCode: 77, text: "M" },
|
|
2973
|
+
N: { key: "N", code: "KeyN", keyCode: 78, text: "N" },
|
|
2974
|
+
O: { key: "O", code: "KeyO", keyCode: 79, text: "O" },
|
|
2975
|
+
P: { key: "P", code: "KeyP", keyCode: 80, text: "P" },
|
|
2976
|
+
Q: { key: "Q", code: "KeyQ", keyCode: 81, text: "Q" },
|
|
2977
|
+
R: { key: "R", code: "KeyR", keyCode: 82, text: "R" },
|
|
2978
|
+
S: { key: "S", code: "KeyS", keyCode: 83, text: "S" },
|
|
2979
|
+
T: { key: "T", code: "KeyT", keyCode: 84, text: "T" },
|
|
2980
|
+
U: { key: "U", code: "KeyU", keyCode: 85, text: "U" },
|
|
2981
|
+
V: { key: "V", code: "KeyV", keyCode: 86, text: "V" },
|
|
2982
|
+
W: { key: "W", code: "KeyW", keyCode: 87, text: "W" },
|
|
2983
|
+
X: { key: "X", code: "KeyX", keyCode: 88, text: "X" },
|
|
2984
|
+
Y: { key: "Y", code: "KeyY", keyCode: 89, text: "Y" },
|
|
2985
|
+
Z: { key: "Z", code: "KeyZ", keyCode: 90, text: "Z" },
|
|
2986
|
+
// Numbers
|
|
2987
|
+
"0": { key: "0", code: "Digit0", keyCode: 48, text: "0" },
|
|
2988
|
+
"1": { key: "1", code: "Digit1", keyCode: 49, text: "1" },
|
|
2989
|
+
"2": { key: "2", code: "Digit2", keyCode: 50, text: "2" },
|
|
2990
|
+
"3": { key: "3", code: "Digit3", keyCode: 51, text: "3" },
|
|
2991
|
+
"4": { key: "4", code: "Digit4", keyCode: 52, text: "4" },
|
|
2992
|
+
"5": { key: "5", code: "Digit5", keyCode: 53, text: "5" },
|
|
2993
|
+
"6": { key: "6", code: "Digit6", keyCode: 54, text: "6" },
|
|
2994
|
+
"7": { key: "7", code: "Digit7", keyCode: 55, text: "7" },
|
|
2995
|
+
"8": { key: "8", code: "Digit8", keyCode: 56, text: "8" },
|
|
2996
|
+
"9": { key: "9", code: "Digit9", keyCode: 57, text: "9" },
|
|
2997
|
+
// Punctuation
|
|
2998
|
+
" ": { key: " ", code: "Space", keyCode: 32, text: " " },
|
|
2999
|
+
".": { key: ".", code: "Period", keyCode: 190, text: "." },
|
|
3000
|
+
",": { key: ",", code: "Comma", keyCode: 188, text: "," },
|
|
3001
|
+
"/": { key: "/", code: "Slash", keyCode: 191, text: "/" },
|
|
3002
|
+
";": { key: ";", code: "Semicolon", keyCode: 186, text: ";" },
|
|
3003
|
+
"'": { key: "'", code: "Quote", keyCode: 222, text: "'" },
|
|
3004
|
+
"[": { key: "[", code: "BracketLeft", keyCode: 219, text: "[" },
|
|
3005
|
+
"]": { key: "]", code: "BracketRight", keyCode: 221, text: "]" },
|
|
3006
|
+
"\\": { key: "\\", code: "Backslash", keyCode: 220, text: "\\" },
|
|
3007
|
+
"-": { key: "-", code: "Minus", keyCode: 189, text: "-" },
|
|
3008
|
+
"=": { key: "=", code: "Equal", keyCode: 187, text: "=" },
|
|
3009
|
+
"`": { key: "`", code: "Backquote", keyCode: 192, text: "`" },
|
|
3010
|
+
// Shifted punctuation
|
|
3011
|
+
"!": { key: "!", code: "Digit1", keyCode: 49, text: "!" },
|
|
3012
|
+
"@": { key: "@", code: "Digit2", keyCode: 50, text: "@" },
|
|
3013
|
+
"#": { key: "#", code: "Digit3", keyCode: 51, text: "#" },
|
|
3014
|
+
$: { key: "$", code: "Digit4", keyCode: 52, text: "$" },
|
|
3015
|
+
"%": { key: "%", code: "Digit5", keyCode: 53, text: "%" },
|
|
3016
|
+
"^": { key: "^", code: "Digit6", keyCode: 54, text: "^" },
|
|
3017
|
+
"&": { key: "&", code: "Digit7", keyCode: 55, text: "&" },
|
|
3018
|
+
"*": { key: "*", code: "Digit8", keyCode: 56, text: "*" },
|
|
3019
|
+
"(": { key: "(", code: "Digit9", keyCode: 57, text: "(" },
|
|
3020
|
+
")": { key: ")", code: "Digit0", keyCode: 48, text: ")" },
|
|
3021
|
+
_: { key: "_", code: "Minus", keyCode: 189, text: "_" },
|
|
3022
|
+
"+": { key: "+", code: "Equal", keyCode: 187, text: "+" },
|
|
3023
|
+
"{": { key: "{", code: "BracketLeft", keyCode: 219, text: "{" },
|
|
3024
|
+
"}": { key: "}", code: "BracketRight", keyCode: 221, text: "}" },
|
|
3025
|
+
"|": { key: "|", code: "Backslash", keyCode: 220, text: "|" },
|
|
3026
|
+
":": { key: ":", code: "Semicolon", keyCode: 186, text: ":" },
|
|
3027
|
+
'"': { key: '"', code: "Quote", keyCode: 222, text: '"' },
|
|
3028
|
+
"<": { key: "<", code: "Comma", keyCode: 188, text: "<" },
|
|
3029
|
+
">": { key: ">", code: "Period", keyCode: 190, text: ">" },
|
|
3030
|
+
"?": { key: "?", code: "Slash", keyCode: 191, text: "?" },
|
|
3031
|
+
"~": { key: "~", code: "Backquote", keyCode: 192, text: "~" },
|
|
3032
|
+
// Special keys (non-text: use rawKeyDown, no text field)
|
|
3033
|
+
Enter: { key: "Enter", code: "Enter", keyCode: 13 },
|
|
3034
|
+
Tab: { key: "Tab", code: "Tab", keyCode: 9 },
|
|
3035
|
+
Backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
|
|
3036
|
+
Delete: { key: "Delete", code: "Delete", keyCode: 46 },
|
|
3037
|
+
Escape: { key: "Escape", code: "Escape", keyCode: 27 },
|
|
3038
|
+
ArrowUp: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
|
|
3039
|
+
ArrowDown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
|
|
3040
|
+
ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
|
|
3041
|
+
ArrowRight: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 },
|
|
3042
|
+
Home: { key: "Home", code: "Home", keyCode: 36 },
|
|
3043
|
+
End: { key: "End", code: "End", keyCode: 35 },
|
|
3044
|
+
PageUp: { key: "PageUp", code: "PageUp", keyCode: 33 },
|
|
3045
|
+
PageDown: { key: "PageDown", code: "PageDown", keyCode: 34 }
|
|
3046
|
+
};
|
|
3047
|
+
|
|
2595
3048
|
// src/browser/page.ts
|
|
2596
3049
|
var DEFAULT_TIMEOUT2 = 3e4;
|
|
3050
|
+
var EVENT_LISTENER_TRACKER_SCRIPT = `(() => {
|
|
3051
|
+
if (globalThis.__bpEventListenerTrackerInstalled) return;
|
|
3052
|
+
Object.defineProperty(globalThis, '__bpEventListenerTrackerInstalled', {
|
|
3053
|
+
value: true,
|
|
3054
|
+
configurable: true,
|
|
3055
|
+
});
|
|
3056
|
+
|
|
3057
|
+
const storeKey = '__bpEventListeners';
|
|
3058
|
+
const originalAddEventListener = EventTarget.prototype.addEventListener;
|
|
3059
|
+
const originalRemoveEventListener = EventTarget.prototype.removeEventListener;
|
|
3060
|
+
|
|
3061
|
+
function ensureStore(target) {
|
|
3062
|
+
if (!Object.prototype.hasOwnProperty.call(target, storeKey)) {
|
|
3063
|
+
Object.defineProperty(target, storeKey, {
|
|
3064
|
+
value: Object.create(null),
|
|
3065
|
+
configurable: true,
|
|
3066
|
+
});
|
|
3067
|
+
}
|
|
3068
|
+
return target[storeKey];
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
EventTarget.prototype.addEventListener = function(type, listener, options) {
|
|
3072
|
+
try {
|
|
3073
|
+
if (listener) {
|
|
3074
|
+
const store = ensureStore(this);
|
|
3075
|
+
const bucket = store[type] || (store[type] = []);
|
|
3076
|
+
const capture =
|
|
3077
|
+
typeof options === 'boolean' ? options : !!(options && options.capture);
|
|
3078
|
+
const exists = bucket.some((entry) => entry.listener === listener && entry.capture === capture);
|
|
3079
|
+
if (!exists) {
|
|
3080
|
+
bucket.push({ listener, capture });
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
} catch {}
|
|
3084
|
+
|
|
3085
|
+
return originalAddEventListener.call(this, type, listener, options);
|
|
3086
|
+
};
|
|
3087
|
+
|
|
3088
|
+
EventTarget.prototype.removeEventListener = function(type, listener, options) {
|
|
3089
|
+
try {
|
|
3090
|
+
const store = this[storeKey];
|
|
3091
|
+
const bucket = store && store[type];
|
|
3092
|
+
const capture =
|
|
3093
|
+
typeof options === 'boolean' ? options : !!(options && options.capture);
|
|
3094
|
+
if (Array.isArray(bucket)) {
|
|
3095
|
+
store[type] = bucket.filter((entry) => {
|
|
3096
|
+
return !(entry.listener === listener && entry.capture === capture);
|
|
3097
|
+
});
|
|
3098
|
+
}
|
|
3099
|
+
} catch {}
|
|
3100
|
+
|
|
3101
|
+
return originalRemoveEventListener.call(this, type, listener, options);
|
|
3102
|
+
};
|
|
3103
|
+
})();`;
|
|
2597
3104
|
var Page = class {
|
|
2598
3105
|
cdp;
|
|
2599
3106
|
_targetId;
|
|
@@ -2617,6 +3124,8 @@ var Page = class {
|
|
|
2617
3124
|
currentFrameContextId = null;
|
|
2618
3125
|
/** Last matched selector from findElement (for selectorUsed tracking) */
|
|
2619
3126
|
_lastMatchedSelector;
|
|
3127
|
+
/** Last snapshot for stale ref recovery */
|
|
3128
|
+
lastSnapshot;
|
|
2620
3129
|
/** Audio input controller (lazy-initialized) */
|
|
2621
3130
|
_audioInput;
|
|
2622
3131
|
/** Audio output controller (lazy-initialized) */
|
|
@@ -2661,6 +3170,9 @@ var Page = class {
|
|
|
2661
3170
|
for (const [frameId, ctxId] of this.frameExecutionContexts.entries()) {
|
|
2662
3171
|
if (ctxId === contextId) {
|
|
2663
3172
|
this.frameExecutionContexts.delete(frameId);
|
|
3173
|
+
if (this.currentFrameContextId === contextId) {
|
|
3174
|
+
this.currentFrameContextId = null;
|
|
3175
|
+
}
|
|
2664
3176
|
break;
|
|
2665
3177
|
}
|
|
2666
3178
|
}
|
|
@@ -2672,6 +3184,18 @@ var Page = class {
|
|
|
2672
3184
|
this.cdp.send("Runtime.enable"),
|
|
2673
3185
|
this.cdp.send("Network.enable")
|
|
2674
3186
|
]);
|
|
3187
|
+
await this.installEventListenerTracker();
|
|
3188
|
+
}
|
|
3189
|
+
async installEventListenerTracker() {
|
|
3190
|
+
await this.cdp.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
3191
|
+
source: EVENT_LISTENER_TRACKER_SCRIPT
|
|
3192
|
+
});
|
|
3193
|
+
try {
|
|
3194
|
+
await this.cdp.send("Runtime.evaluate", {
|
|
3195
|
+
expression: EVENT_LISTENER_TRACKER_SCRIPT
|
|
3196
|
+
});
|
|
3197
|
+
} catch {
|
|
3198
|
+
}
|
|
2675
3199
|
}
|
|
2676
3200
|
// ============ Navigation ============
|
|
2677
3201
|
/**
|
|
@@ -2687,6 +3211,9 @@ var Page = class {
|
|
|
2687
3211
|
}
|
|
2688
3212
|
this.rootNodeId = null;
|
|
2689
3213
|
this.refMap.clear();
|
|
3214
|
+
this.currentFrame = null;
|
|
3215
|
+
this.currentFrameContextId = null;
|
|
3216
|
+
this.frameContexts.clear();
|
|
2690
3217
|
}
|
|
2691
3218
|
/**
|
|
2692
3219
|
* Get the current URL
|
|
@@ -2757,8 +3284,9 @@ var Page = class {
|
|
|
2757
3284
|
/**
|
|
2758
3285
|
* Click an element (supports multi-selector)
|
|
2759
3286
|
*
|
|
2760
|
-
* Uses CDP mouse events
|
|
2761
|
-
*
|
|
3287
|
+
* Uses CDP mouse events (mouseMoved + mousePressed + mouseReleased) to
|
|
3288
|
+
* simulate a real click. Real mouse events on submit buttons naturally
|
|
3289
|
+
* trigger native form submission — no JS dispatch needed.
|
|
2762
3290
|
*/
|
|
2763
3291
|
async click(selector, options = {}) {
|
|
2764
3292
|
return this.withStaleNodeRetry(async () => {
|
|
@@ -2770,27 +3298,45 @@ var Page = class {
|
|
|
2770
3298
|
throw new ElementNotFoundError(selector, hints);
|
|
2771
3299
|
}
|
|
2772
3300
|
await this.scrollIntoView(element.nodeId);
|
|
2773
|
-
const
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
3301
|
+
const objectId = await this.resolveObjectId(element.nodeId);
|
|
3302
|
+
try {
|
|
3303
|
+
await ensureActionable(this.cdp, objectId, ["visible", "enabled", "stable"], {
|
|
3304
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
3305
|
+
});
|
|
3306
|
+
} catch (e) {
|
|
3307
|
+
if (options.optional) return false;
|
|
3308
|
+
throw e;
|
|
3309
|
+
}
|
|
3310
|
+
let clickX;
|
|
3311
|
+
let clickY;
|
|
3312
|
+
try {
|
|
3313
|
+
const { quads } = await this.cdp.send("DOM.getContentQuads", {
|
|
3314
|
+
objectId
|
|
3315
|
+
});
|
|
3316
|
+
if (quads?.length > 0) {
|
|
3317
|
+
const quad = quads[0];
|
|
3318
|
+
clickX = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
|
|
3319
|
+
clickY = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
|
|
3320
|
+
} else {
|
|
3321
|
+
throw new Error("No quads");
|
|
3322
|
+
}
|
|
3323
|
+
} catch {
|
|
3324
|
+
const box = await this.getBoxModel(element.nodeId);
|
|
3325
|
+
if (!box) throw new Error("Could not get element position");
|
|
3326
|
+
clickX = box.content[0] + box.width / 2;
|
|
3327
|
+
clickY = box.content[1] + box.height / 2;
|
|
3328
|
+
}
|
|
3329
|
+
const hitTargetCoordinates = this.currentFrame ? void 0 : { x: clickX, y: clickY };
|
|
3330
|
+
try {
|
|
3331
|
+
await ensureActionable(this.cdp, objectId, ["hitTarget"], {
|
|
3332
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2,
|
|
3333
|
+
coordinates: hitTargetCoordinates
|
|
3334
|
+
});
|
|
3335
|
+
} catch (e) {
|
|
3336
|
+
if (options.optional) return false;
|
|
3337
|
+
throw e;
|
|
2793
3338
|
}
|
|
3339
|
+
await this.clickElement(element.nodeId);
|
|
2794
3340
|
return true;
|
|
2795
3341
|
});
|
|
2796
3342
|
}
|
|
@@ -2798,7 +3344,7 @@ var Page = class {
|
|
|
2798
3344
|
* Fill an input field (clears first by default)
|
|
2799
3345
|
*/
|
|
2800
3346
|
async fill(selector, value, options = {}) {
|
|
2801
|
-
const {
|
|
3347
|
+
const { blur = false } = options;
|
|
2802
3348
|
return this.withStaleNodeRetry(async () => {
|
|
2803
3349
|
const element = await this.findElement(selector, options);
|
|
2804
3350
|
if (!element) {
|
|
@@ -2807,71 +3353,152 @@ var Page = class {
|
|
|
2807
3353
|
const hints = await generateHints(this, selectorList, "fill");
|
|
2808
3354
|
throw new ElementNotFoundError(selector, hints);
|
|
2809
3355
|
}
|
|
2810
|
-
await this.cdp.send("DOM.
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
}));
|
|
2822
|
-
}
|
|
2823
|
-
})()`
|
|
2824
|
-
);
|
|
3356
|
+
const { object } = await this.cdp.send("DOM.resolveNode", {
|
|
3357
|
+
nodeId: element.nodeId
|
|
3358
|
+
});
|
|
3359
|
+
const objectId = object.objectId;
|
|
3360
|
+
try {
|
|
3361
|
+
await ensureActionable(this.cdp, objectId, ["visible", "enabled", "editable"], {
|
|
3362
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
3363
|
+
});
|
|
3364
|
+
} catch (e) {
|
|
3365
|
+
if (options.optional) return false;
|
|
3366
|
+
throw e;
|
|
2825
3367
|
}
|
|
2826
|
-
await this.cdp.send("
|
|
2827
|
-
|
|
2828
|
-
`(
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
3368
|
+
const tagInfo = await this.cdp.send("Runtime.callFunctionOn", {
|
|
3369
|
+
objectId,
|
|
3370
|
+
functionDeclaration: `function() {
|
|
3371
|
+
return { tagName: this.tagName?.toLowerCase() || '', inputType: (this.type || '').toLowerCase() };
|
|
3372
|
+
}`,
|
|
3373
|
+
returnByValue: true
|
|
3374
|
+
});
|
|
3375
|
+
const { tagName, inputType } = tagInfo.result.value;
|
|
3376
|
+
const specialInputTypes = /* @__PURE__ */ new Set([
|
|
3377
|
+
"date",
|
|
3378
|
+
"datetime-local",
|
|
3379
|
+
"month",
|
|
3380
|
+
"week",
|
|
3381
|
+
"time",
|
|
3382
|
+
"color",
|
|
3383
|
+
"range",
|
|
3384
|
+
"file"
|
|
3385
|
+
]);
|
|
3386
|
+
const isSpecialInput = tagName === "input" && specialInputTypes.has(inputType);
|
|
3387
|
+
if (isSpecialInput) {
|
|
3388
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
3389
|
+
objectId,
|
|
3390
|
+
functionDeclaration: `function(val) {
|
|
3391
|
+
this.value = val;
|
|
3392
|
+
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
3393
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
3394
|
+
}`,
|
|
3395
|
+
arguments: [{ value }],
|
|
3396
|
+
returnByValue: true
|
|
3397
|
+
});
|
|
3398
|
+
} else {
|
|
3399
|
+
await this.selectEditableContent(objectId);
|
|
3400
|
+
if (value === "") {
|
|
3401
|
+
await this.dispatchKey("Delete");
|
|
3402
|
+
} else {
|
|
3403
|
+
await this.cdp.send("Input.insertText", { text: value });
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
if (options.verify !== false) {
|
|
3407
|
+
let actualValue = await this.readEditableValue(objectId);
|
|
3408
|
+
if (actualValue !== value && !isSpecialInput) {
|
|
3409
|
+
if (value === "") {
|
|
3410
|
+
await this.clearEditableSelection(objectId, "Backspace");
|
|
3411
|
+
} else {
|
|
3412
|
+
await this.typeEditableFallback(element.nodeId, objectId, value);
|
|
2838
3413
|
}
|
|
2839
|
-
|
|
2840
|
-
|
|
3414
|
+
actualValue = await this.readEditableValue(objectId);
|
|
3415
|
+
}
|
|
3416
|
+
if (actualValue !== value) {
|
|
3417
|
+
if (options.optional) return false;
|
|
3418
|
+
throw new Error(
|
|
3419
|
+
`Fill value did not stick. Expected ${JSON.stringify(value)} but got ${JSON.stringify(actualValue)}.`
|
|
3420
|
+
);
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
2841
3423
|
if (blur) {
|
|
2842
|
-
await this.
|
|
2843
|
-
|
|
2844
|
-
|
|
3424
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
3425
|
+
objectId,
|
|
3426
|
+
functionDeclaration: "function() { this.blur(); }"
|
|
3427
|
+
});
|
|
2845
3428
|
}
|
|
2846
3429
|
return true;
|
|
2847
3430
|
});
|
|
2848
3431
|
}
|
|
2849
3432
|
/**
|
|
2850
3433
|
* Type text character by character (for autocomplete fields, etc.)
|
|
3434
|
+
*
|
|
3435
|
+
* Uses proper keyDown/rawKeyDown distinction with US keyboard layout.
|
|
3436
|
+
* Printable chars use 'keyDown' with text, non-text keys use 'rawKeyDown',
|
|
3437
|
+
* and non-layout chars (emoji, CJK) fall back to Input.insertText.
|
|
2851
3438
|
*/
|
|
2852
3439
|
async type(selector, text, options = {}) {
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
if (
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
|
|
2860
|
-
for (const char of text) {
|
|
2861
|
-
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
2862
|
-
type: "keyDown",
|
|
2863
|
-
key: char,
|
|
2864
|
-
text: char
|
|
2865
|
-
});
|
|
2866
|
-
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
2867
|
-
type: "keyUp",
|
|
2868
|
-
key: char
|
|
2869
|
-
});
|
|
2870
|
-
if (delay > 0) {
|
|
2871
|
-
await sleep3(delay);
|
|
3440
|
+
return this.withStaleNodeRetry(async () => {
|
|
3441
|
+
const { delay = 50 } = options;
|
|
3442
|
+
const element = await this.findElement(selector, options);
|
|
3443
|
+
if (!element) {
|
|
3444
|
+
if (options.optional) return false;
|
|
3445
|
+
throw new ElementNotFoundError(selector);
|
|
2872
3446
|
}
|
|
2873
|
-
|
|
2874
|
-
|
|
3447
|
+
try {
|
|
3448
|
+
const objectId = await this.resolveObjectId(element.nodeId);
|
|
3449
|
+
await ensureActionable(this.cdp, objectId, ["visible", "enabled"], {
|
|
3450
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
3451
|
+
});
|
|
3452
|
+
} catch (e) {
|
|
3453
|
+
if (options.optional) return false;
|
|
3454
|
+
throw e;
|
|
3455
|
+
}
|
|
3456
|
+
await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
|
|
3457
|
+
for (const char of text) {
|
|
3458
|
+
const def = US_KEYBOARD[char];
|
|
3459
|
+
if (def) {
|
|
3460
|
+
if (def.text !== void 0) {
|
|
3461
|
+
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
3462
|
+
type: "keyDown",
|
|
3463
|
+
key: def.key,
|
|
3464
|
+
code: def.code,
|
|
3465
|
+
text: def.text,
|
|
3466
|
+
unmodifiedText: def.text,
|
|
3467
|
+
windowsVirtualKeyCode: def.keyCode,
|
|
3468
|
+
modifiers: 0,
|
|
3469
|
+
autoRepeat: false,
|
|
3470
|
+
location: def.location ?? 0,
|
|
3471
|
+
isKeypad: false
|
|
3472
|
+
});
|
|
3473
|
+
} else {
|
|
3474
|
+
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
3475
|
+
type: "rawKeyDown",
|
|
3476
|
+
key: def.key,
|
|
3477
|
+
code: def.code,
|
|
3478
|
+
windowsVirtualKeyCode: def.keyCode,
|
|
3479
|
+
modifiers: 0,
|
|
3480
|
+
autoRepeat: false,
|
|
3481
|
+
location: def.location ?? 0,
|
|
3482
|
+
isKeypad: false
|
|
3483
|
+
});
|
|
3484
|
+
}
|
|
3485
|
+
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
3486
|
+
type: "keyUp",
|
|
3487
|
+
key: def.key,
|
|
3488
|
+
code: def.code,
|
|
3489
|
+
windowsVirtualKeyCode: def.keyCode,
|
|
3490
|
+
modifiers: 0,
|
|
3491
|
+
location: def.location ?? 0
|
|
3492
|
+
});
|
|
3493
|
+
} else {
|
|
3494
|
+
await this.cdp.send("Input.insertText", { text: char });
|
|
3495
|
+
}
|
|
3496
|
+
if (delay > 0) {
|
|
3497
|
+
await sleep4(delay);
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
return true;
|
|
3501
|
+
});
|
|
2875
3502
|
}
|
|
2876
3503
|
async select(selectorOrConfig, valueOrOptions, maybeOptions) {
|
|
2877
3504
|
if (typeof selectorOrConfig === "object" && !Array.isArray(selectorOrConfig) && "trigger" in selectorOrConfig) {
|
|
@@ -2880,108 +3507,227 @@ var Page = class {
|
|
|
2880
3507
|
const selector = selectorOrConfig;
|
|
2881
3508
|
const value = valueOrOptions;
|
|
2882
3509
|
const options = maybeOptions ?? {};
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
if (
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
3510
|
+
return this.withStaleNodeRetry(async () => {
|
|
3511
|
+
const element = await this.findElement(selector, options);
|
|
3512
|
+
if (!element) {
|
|
3513
|
+
if (options.optional) return false;
|
|
3514
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
3515
|
+
const hints = await generateHints(this, selectorList, "select");
|
|
3516
|
+
throw new ElementNotFoundError(selector, hints);
|
|
3517
|
+
}
|
|
3518
|
+
const values = Array.isArray(value) ? value : [value];
|
|
3519
|
+
const objectId = await this.resolveObjectId(element.nodeId);
|
|
3520
|
+
try {
|
|
3521
|
+
await this.scrollIntoView(element.nodeId);
|
|
3522
|
+
await ensureActionable(this.cdp, objectId, ["visible", "enabled"], {
|
|
3523
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
3524
|
+
});
|
|
3525
|
+
} catch (e) {
|
|
3526
|
+
if (options.optional) return false;
|
|
3527
|
+
throw e;
|
|
3528
|
+
}
|
|
3529
|
+
const metadata = await this.getNativeSelectMetadata(objectId, values);
|
|
3530
|
+
if (!metadata.isSelect) {
|
|
3531
|
+
throw new Error("select() target must be a native <select> element");
|
|
3532
|
+
}
|
|
3533
|
+
if (metadata.missing.length > 0) {
|
|
3534
|
+
throw new Error(`No option found for: ${metadata.missing.join(", ")}`);
|
|
3535
|
+
}
|
|
3536
|
+
if (metadata.disabled.length > 0) {
|
|
3537
|
+
throw new Error(`Cannot select disabled option(s): ${metadata.disabled.join(", ")}`);
|
|
3538
|
+
}
|
|
3539
|
+
if (!metadata.multiple && metadata.targetIndexes.length > 1) {
|
|
3540
|
+
throw new Error("Cannot select multiple values on a single-select element");
|
|
3541
|
+
}
|
|
3542
|
+
const expectedValues = metadata.targetIndexes.map((idx) => metadata.options[idx].value);
|
|
3543
|
+
if (this.selectValuesMatch(metadata.selectedValues, expectedValues, metadata.multiple)) {
|
|
2900
3544
|
return true;
|
|
2901
|
-
}
|
|
2902
|
-
|
|
3545
|
+
}
|
|
3546
|
+
if (!metadata.multiple && metadata.targetIndexes.length === 1) {
|
|
3547
|
+
await this.applyNativeSelectByKeyboard(
|
|
3548
|
+
element.nodeId,
|
|
3549
|
+
objectId,
|
|
3550
|
+
metadata.currentIndex,
|
|
3551
|
+
metadata.targetIndexes[0]
|
|
3552
|
+
);
|
|
3553
|
+
}
|
|
3554
|
+
let selectedValues = await this.readNativeSelectValues(objectId);
|
|
3555
|
+
if (!this.selectValuesMatch(selectedValues, expectedValues, metadata.multiple)) {
|
|
3556
|
+
await this.applyNativeSelectFallback(objectId, metadata.targetIndexes);
|
|
3557
|
+
selectedValues = await this.readNativeSelectValues(objectId);
|
|
3558
|
+
}
|
|
3559
|
+
if (!this.selectValuesMatch(selectedValues, expectedValues, metadata.multiple)) {
|
|
3560
|
+
await this.applyRecordedSelectFallback(objectId, metadata.targetIndexes);
|
|
3561
|
+
selectedValues = await this.readNativeSelectValues(objectId);
|
|
3562
|
+
}
|
|
3563
|
+
if (!this.selectValuesMatch(selectedValues, expectedValues, metadata.multiple)) {
|
|
3564
|
+
if (options.optional) return false;
|
|
3565
|
+
throw new Error(
|
|
3566
|
+
`Select value did not stick. Expected ${expectedValues.join(", ") || "(empty)"} but got ${selectedValues.join(", ") || "(empty)"}.`
|
|
3567
|
+
);
|
|
3568
|
+
}
|
|
3569
|
+
return true;
|
|
2903
3570
|
});
|
|
2904
|
-
return true;
|
|
2905
3571
|
}
|
|
2906
3572
|
/**
|
|
2907
3573
|
* Handle custom (non-native) select/dropdown components
|
|
2908
3574
|
*/
|
|
2909
3575
|
async selectCustom(config, options = {}) {
|
|
2910
3576
|
const { trigger, option, value, match = "text" } = config;
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
3577
|
+
return this.withStaleNodeRetry(async () => {
|
|
3578
|
+
await this.click(trigger, options);
|
|
3579
|
+
await sleep4(100);
|
|
3580
|
+
const optionSelectors = Array.isArray(option) ? option : [option];
|
|
3581
|
+
const optionHandle = await this.evaluateInFrame(
|
|
3582
|
+
`(() => {
|
|
3583
|
+
const selectors = ${JSON.stringify(optionSelectors)};
|
|
3584
|
+
const wanted = ${JSON.stringify(value)};
|
|
3585
|
+
const mode = ${JSON.stringify(match)};
|
|
3586
|
+
|
|
3587
|
+
for (const selector of selectors) {
|
|
3588
|
+
const candidates = document.querySelectorAll(selector);
|
|
3589
|
+
for (const candidate of candidates) {
|
|
3590
|
+
const text = candidate.textContent?.trim() || '';
|
|
3591
|
+
const candidateValue =
|
|
3592
|
+
candidate.getAttribute?.('data-value') ??
|
|
3593
|
+
candidate.getAttribute?.('value') ??
|
|
3594
|
+
candidate.value ??
|
|
3595
|
+
'';
|
|
3596
|
+
const matches =
|
|
3597
|
+
mode === 'value'
|
|
3598
|
+
? candidateValue === wanted
|
|
3599
|
+
: mode === 'contains'
|
|
3600
|
+
? text.includes(wanted)
|
|
3601
|
+
: text === wanted;
|
|
3602
|
+
|
|
3603
|
+
if (matches) {
|
|
3604
|
+
return candidate;
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
2930
3607
|
}
|
|
3608
|
+
|
|
3609
|
+
return null;
|
|
3610
|
+
})()`,
|
|
3611
|
+
{ returnByValue: false }
|
|
3612
|
+
);
|
|
3613
|
+
if (!optionHandle.result.objectId) {
|
|
3614
|
+
if (options.optional) return false;
|
|
3615
|
+
throw new ElementNotFoundError(`Option with ${match} "${value}"`);
|
|
3616
|
+
}
|
|
3617
|
+
const nodeResult = await this.cdp.send("DOM.requestNode", {
|
|
3618
|
+
objectId: optionHandle.result.objectId
|
|
3619
|
+
});
|
|
3620
|
+
if (!nodeResult.nodeId) {
|
|
3621
|
+
if (options.optional) return false;
|
|
3622
|
+
throw new ElementNotFoundError(`Option with ${match} "${value}"`);
|
|
3623
|
+
}
|
|
3624
|
+
await this.scrollIntoView(nodeResult.nodeId);
|
|
3625
|
+
await ensureActionable(
|
|
3626
|
+
this.cdp,
|
|
3627
|
+
optionHandle.result.objectId,
|
|
3628
|
+
["visible", "enabled", "stable"],
|
|
3629
|
+
{
|
|
3630
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
2931
3631
|
}
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
3632
|
+
);
|
|
3633
|
+
await this.clickElement(nodeResult.nodeId);
|
|
3634
|
+
return true;
|
|
2935
3635
|
});
|
|
2936
|
-
if (!result.result.value) {
|
|
2937
|
-
if (options.optional) return false;
|
|
2938
|
-
throw new ElementNotFoundError(`Option with ${match} "${value}"`);
|
|
2939
|
-
}
|
|
2940
|
-
return true;
|
|
2941
3636
|
}
|
|
2942
3637
|
/**
|
|
2943
|
-
* Check a checkbox or radio button
|
|
3638
|
+
* Check a checkbox or radio button using real mouse click.
|
|
3639
|
+
* No-op if already checked. Verifies state changed after click.
|
|
2944
3640
|
*/
|
|
2945
3641
|
async check(selector, options = {}) {
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
if (
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
3642
|
+
return this.withStaleNodeRetry(async () => {
|
|
3643
|
+
const element = await this.findElement(selector, options);
|
|
3644
|
+
if (!element) {
|
|
3645
|
+
if (options.optional) return false;
|
|
3646
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
3647
|
+
const hints = await generateHints(this, selectorList, "check");
|
|
3648
|
+
throw new ElementNotFoundError(selector, hints);
|
|
3649
|
+
}
|
|
3650
|
+
const { object } = await this.cdp.send("DOM.resolveNode", {
|
|
3651
|
+
nodeId: element.nodeId
|
|
3652
|
+
});
|
|
3653
|
+
try {
|
|
3654
|
+
await ensureActionable(this.cdp, object.objectId, ["visible", "enabled"], {
|
|
3655
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
3656
|
+
});
|
|
3657
|
+
} catch (e) {
|
|
3658
|
+
if (options.optional) return false;
|
|
3659
|
+
throw e;
|
|
3660
|
+
}
|
|
3661
|
+
const before = await this.cdp.send("Runtime.callFunctionOn", {
|
|
3662
|
+
objectId: object.objectId,
|
|
3663
|
+
functionDeclaration: "function() { return !!this.checked; }",
|
|
3664
|
+
returnByValue: true
|
|
3665
|
+
});
|
|
3666
|
+
if (before.result.value) return true;
|
|
3667
|
+
await this.scrollIntoView(element.nodeId);
|
|
3668
|
+
await this.clickElement(element.nodeId);
|
|
3669
|
+
const after = await this.cdp.send("Runtime.callFunctionOn", {
|
|
3670
|
+
objectId: object.objectId,
|
|
3671
|
+
functionDeclaration: "function() { return !!this.checked; }",
|
|
3672
|
+
returnByValue: true
|
|
3673
|
+
});
|
|
3674
|
+
if (!after.result.value) {
|
|
3675
|
+
throw new Error("Clicking the checkbox did not change its state");
|
|
3676
|
+
}
|
|
3677
|
+
return true;
|
|
2961
3678
|
});
|
|
2962
|
-
return result.result.value;
|
|
2963
3679
|
}
|
|
2964
3680
|
/**
|
|
2965
|
-
* Uncheck a checkbox
|
|
3681
|
+
* Uncheck a checkbox using real mouse click.
|
|
3682
|
+
* No-op if already unchecked. Radio buttons can't be unchecked (returns true).
|
|
2966
3683
|
*/
|
|
2967
3684
|
async uncheck(selector, options = {}) {
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
if (
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
3685
|
+
return this.withStaleNodeRetry(async () => {
|
|
3686
|
+
const element = await this.findElement(selector, options);
|
|
3687
|
+
if (!element) {
|
|
3688
|
+
if (options.optional) return false;
|
|
3689
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
3690
|
+
const hints = await generateHints(this, selectorList, "uncheck");
|
|
3691
|
+
throw new ElementNotFoundError(selector, hints);
|
|
3692
|
+
}
|
|
3693
|
+
const { object } = await this.cdp.send("DOM.resolveNode", {
|
|
3694
|
+
nodeId: element.nodeId
|
|
3695
|
+
});
|
|
3696
|
+
try {
|
|
3697
|
+
await ensureActionable(this.cdp, object.objectId, ["visible", "enabled"], {
|
|
3698
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
3699
|
+
});
|
|
3700
|
+
} catch (e) {
|
|
3701
|
+
if (options.optional) return false;
|
|
3702
|
+
throw e;
|
|
3703
|
+
}
|
|
3704
|
+
const isRadio = await this.cdp.send(
|
|
3705
|
+
"Runtime.callFunctionOn",
|
|
3706
|
+
{
|
|
3707
|
+
objectId: object.objectId,
|
|
3708
|
+
functionDeclaration: 'function() { return this.type === "radio"; }',
|
|
3709
|
+
returnByValue: true
|
|
3710
|
+
}
|
|
3711
|
+
);
|
|
3712
|
+
if (isRadio.result.value) return true;
|
|
3713
|
+
const before = await this.cdp.send("Runtime.callFunctionOn", {
|
|
3714
|
+
objectId: object.objectId,
|
|
3715
|
+
functionDeclaration: "function() { return !!this.checked; }",
|
|
3716
|
+
returnByValue: true
|
|
3717
|
+
});
|
|
3718
|
+
if (!before.result.value) return true;
|
|
3719
|
+
await this.scrollIntoView(element.nodeId);
|
|
3720
|
+
await this.clickElement(element.nodeId);
|
|
3721
|
+
const after = await this.cdp.send("Runtime.callFunctionOn", {
|
|
3722
|
+
objectId: object.objectId,
|
|
3723
|
+
functionDeclaration: "function() { return !!this.checked; }",
|
|
3724
|
+
returnByValue: true
|
|
3725
|
+
});
|
|
3726
|
+
if (after.result.value) {
|
|
3727
|
+
throw new Error("Clicking the checkbox did not change its state");
|
|
3728
|
+
}
|
|
3729
|
+
return true;
|
|
2983
3730
|
});
|
|
2984
|
-
return result.result.value;
|
|
2985
3731
|
}
|
|
2986
3732
|
/**
|
|
2987
3733
|
* Submit a form (tries Enter key first, then click)
|
|
@@ -2995,97 +3741,84 @@ var Page = class {
|
|
|
2995
3741
|
* the submit event and triggers HTML5 validation.
|
|
2996
3742
|
*/
|
|
2997
3743
|
async submit(selector, options = {}) {
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
if (
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
const isFormElement = await this.evaluateInFrame(
|
|
3007
|
-
`(() => {
|
|
3008
|
-
const el = document.querySelector(${JSON.stringify(element.selector)});
|
|
3009
|
-
return el instanceof HTMLFormElement;
|
|
3010
|
-
})()`
|
|
3011
|
-
);
|
|
3012
|
-
if (isFormElement.result.value) {
|
|
3013
|
-
await this.evaluateInFrame(
|
|
3014
|
-
`(() => {
|
|
3015
|
-
const form = document.querySelector(${JSON.stringify(element.selector)});
|
|
3016
|
-
if (form && form instanceof HTMLFormElement) {
|
|
3017
|
-
form.requestSubmit();
|
|
3018
|
-
}
|
|
3019
|
-
})()`
|
|
3020
|
-
);
|
|
3021
|
-
if (shouldWait === true) {
|
|
3022
|
-
await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
|
|
3023
|
-
} else if (shouldWait === "auto") {
|
|
3024
|
-
await Promise.race([this.waitForNavigation({ timeout: 1e3, optional: true }), sleep3(500)]);
|
|
3744
|
+
return this.withStaleNodeRetry(async () => {
|
|
3745
|
+
const { method = "enter+click", waitForNavigation: shouldWait = "auto" } = options;
|
|
3746
|
+
const element = await this.findElement(selector, options);
|
|
3747
|
+
if (!element) {
|
|
3748
|
+
if (options.optional) return false;
|
|
3749
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
3750
|
+
const hints = await generateHints(this, selectorList, "submit");
|
|
3751
|
+
throw new ElementNotFoundError(selector, hints);
|
|
3025
3752
|
}
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3753
|
+
const objectId = await this.resolveObjectId(element.nodeId);
|
|
3754
|
+
const isFormElement = await this.cdp.send(
|
|
3755
|
+
"Runtime.callFunctionOn",
|
|
3756
|
+
{
|
|
3757
|
+
objectId,
|
|
3758
|
+
functionDeclaration: "function() { return this instanceof HTMLFormElement; }",
|
|
3759
|
+
returnByValue: true
|
|
3760
|
+
}
|
|
3761
|
+
);
|
|
3762
|
+
if (isFormElement.result.value) {
|
|
3763
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
3764
|
+
objectId,
|
|
3765
|
+
functionDeclaration: `function() {
|
|
3766
|
+
if (typeof this.requestSubmit === 'function') {
|
|
3767
|
+
this.requestSubmit();
|
|
3768
|
+
} else {
|
|
3769
|
+
this.submit();
|
|
3770
|
+
}
|
|
3771
|
+
}`
|
|
3772
|
+
});
|
|
3773
|
+
if (shouldWait === true) {
|
|
3033
3774
|
await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
|
|
3034
|
-
|
|
3035
|
-
|
|
3775
|
+
} else if (shouldWait === "auto") {
|
|
3776
|
+
await Promise.race([
|
|
3777
|
+
this.waitForNavigation({ timeout: 1e3, optional: true }),
|
|
3778
|
+
sleep4(500)
|
|
3779
|
+
]);
|
|
3036
3780
|
}
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3781
|
+
return true;
|
|
3782
|
+
}
|
|
3783
|
+
await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
|
|
3784
|
+
if (method.includes("enter")) {
|
|
3785
|
+
await this.press("Enter");
|
|
3786
|
+
if (shouldWait === true) {
|
|
3787
|
+
try {
|
|
3788
|
+
await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
|
|
3789
|
+
return true;
|
|
3790
|
+
} catch {
|
|
3791
|
+
}
|
|
3792
|
+
} else if (shouldWait === "auto") {
|
|
3793
|
+
const navigationDetected = await Promise.race([
|
|
3794
|
+
this.waitForNavigation({ timeout: 1e3, optional: true }).then(
|
|
3795
|
+
(success) => success ? "nav" : null
|
|
3796
|
+
),
|
|
3797
|
+
sleep4(500).then(() => "timeout")
|
|
3798
|
+
]);
|
|
3799
|
+
if (navigationDetected === "nav") {
|
|
3800
|
+
return true;
|
|
3801
|
+
}
|
|
3802
|
+
} else if (method === "enter") {
|
|
3045
3803
|
return true;
|
|
3046
3804
|
}
|
|
3047
|
-
} else {
|
|
3048
|
-
if (method === "enter") return true;
|
|
3049
3805
|
}
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3806
|
+
if (method.includes("click")) {
|
|
3807
|
+
await this.click(element.selector, { ...options, optional: false });
|
|
3808
|
+
if (shouldWait === true) {
|
|
3809
|
+
await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
|
|
3810
|
+
} else if (shouldWait === "auto") {
|
|
3811
|
+
await sleep4(100);
|
|
3812
|
+
}
|
|
3057
3813
|
}
|
|
3058
|
-
|
|
3059
|
-
|
|
3814
|
+
return true;
|
|
3815
|
+
});
|
|
3060
3816
|
}
|
|
3061
3817
|
/**
|
|
3062
3818
|
* Press a key
|
|
3063
3819
|
*/
|
|
3064
3820
|
async press(key) {
|
|
3065
|
-
|
|
3066
|
-
Enter: { key: "Enter", code: "Enter", keyCode: 13 },
|
|
3067
|
-
Tab: { key: "Tab", code: "Tab", keyCode: 9 },
|
|
3068
|
-
Escape: { key: "Escape", code: "Escape", keyCode: 27 },
|
|
3069
|
-
Backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
|
|
3070
|
-
Delete: { key: "Delete", code: "Delete", keyCode: 46 },
|
|
3071
|
-
ArrowUp: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
|
|
3072
|
-
ArrowDown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
|
|
3073
|
-
ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
|
|
3074
|
-
ArrowRight: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 }
|
|
3075
|
-
};
|
|
3076
|
-
const keyInfo = keyMap[key] ?? { key, code: key, keyCode: 0 };
|
|
3077
|
-
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
3078
|
-
type: "keyDown",
|
|
3079
|
-
key: keyInfo.key,
|
|
3080
|
-
code: keyInfo.code,
|
|
3081
|
-
windowsVirtualKeyCode: keyInfo.keyCode
|
|
3082
|
-
});
|
|
3083
|
-
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
3084
|
-
type: "keyUp",
|
|
3085
|
-
key: keyInfo.key,
|
|
3086
|
-
code: keyInfo.code,
|
|
3087
|
-
windowsVirtualKeyCode: keyInfo.keyCode
|
|
3088
|
-
});
|
|
3821
|
+
await this.dispatchKey(key);
|
|
3089
3822
|
}
|
|
3090
3823
|
/**
|
|
3091
3824
|
* Focus an element
|
|
@@ -3114,6 +3847,15 @@ var Page = class {
|
|
|
3114
3847
|
throw new ElementNotFoundError(selector, hints);
|
|
3115
3848
|
}
|
|
3116
3849
|
await this.scrollIntoView(element.nodeId);
|
|
3850
|
+
try {
|
|
3851
|
+
const objectId = await this.resolveObjectId(element.nodeId);
|
|
3852
|
+
await ensureActionable(this.cdp, objectId, ["visible", "stable"], {
|
|
3853
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
3854
|
+
});
|
|
3855
|
+
} catch (e) {
|
|
3856
|
+
if (options.optional) return false;
|
|
3857
|
+
throw e;
|
|
3858
|
+
}
|
|
3117
3859
|
const box = await this.getBoxModel(element.nodeId);
|
|
3118
3860
|
if (!box) {
|
|
3119
3861
|
if (options.optional) return false;
|
|
@@ -3304,53 +4046,424 @@ var Page = class {
|
|
|
3304
4046
|
* Get text content from the page or a specific element
|
|
3305
4047
|
*/
|
|
3306
4048
|
async text(selector) {
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
4049
|
+
if (!selector) {
|
|
4050
|
+
const result = await this.evaluateInFrame(
|
|
4051
|
+
"document.body.innerText"
|
|
4052
|
+
);
|
|
4053
|
+
return result.result.value ?? "";
|
|
4054
|
+
}
|
|
4055
|
+
return this.withStaleNodeRetry(async () => {
|
|
4056
|
+
const element = await this.findElement(selector, { timeout: DEFAULT_TIMEOUT2 });
|
|
4057
|
+
if (!element) return "";
|
|
4058
|
+
const objectId = await this.resolveObjectId(element.nodeId);
|
|
4059
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4060
|
+
objectId,
|
|
4061
|
+
functionDeclaration: 'function() { return this.innerText || this.textContent || ""; }',
|
|
4062
|
+
returnByValue: true
|
|
4063
|
+
});
|
|
4064
|
+
return result.result.value ?? "";
|
|
4065
|
+
});
|
|
3310
4066
|
}
|
|
3311
4067
|
// ============ File Handling ============
|
|
3312
4068
|
/**
|
|
3313
4069
|
* Set files on a file input
|
|
3314
4070
|
*/
|
|
3315
4071
|
async setInputFiles(selector, files, options = {}) {
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
if (
|
|
3319
|
-
|
|
4072
|
+
return this.withStaleNodeRetry(async () => {
|
|
4073
|
+
const element = await this.findElement(selector, options);
|
|
4074
|
+
if (!element) {
|
|
4075
|
+
if (options.optional) return false;
|
|
4076
|
+
throw new ElementNotFoundError(selector);
|
|
4077
|
+
}
|
|
4078
|
+
const fileData = await Promise.all(
|
|
4079
|
+
files.map(async (f) => {
|
|
4080
|
+
let base64;
|
|
4081
|
+
if (typeof f.buffer === "string") {
|
|
4082
|
+
base64 = f.buffer;
|
|
4083
|
+
} else {
|
|
4084
|
+
const bytes = new Uint8Array(f.buffer);
|
|
4085
|
+
base64 = btoa(String.fromCharCode(...bytes));
|
|
4086
|
+
}
|
|
4087
|
+
return { name: f.name, mimeType: f.mimeType, data: base64 };
|
|
4088
|
+
})
|
|
4089
|
+
);
|
|
4090
|
+
const objectId = await this.resolveObjectId(element.nodeId);
|
|
4091
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4092
|
+
objectId,
|
|
4093
|
+
functionDeclaration: `function(files) {
|
|
4094
|
+
if (!(this instanceof HTMLInputElement) || this.type !== 'file') {
|
|
4095
|
+
return { ok: false, fileCount: 0 };
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4098
|
+
const dt = new DataTransfer();
|
|
4099
|
+
for (const f of files) {
|
|
4100
|
+
const bytes = Uint8Array.from(atob(f.data), function(c) { return c.charCodeAt(0); });
|
|
4101
|
+
const file = new File([bytes], f.name, { type: f.mimeType });
|
|
4102
|
+
dt.items.add(file);
|
|
4103
|
+
}
|
|
4104
|
+
|
|
4105
|
+
var descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'files');
|
|
4106
|
+
if (descriptor && descriptor.set) {
|
|
4107
|
+
descriptor.set.call(this, dt.files);
|
|
4108
|
+
} else {
|
|
4109
|
+
this.files = dt.files;
|
|
4110
|
+
}
|
|
4111
|
+
|
|
4112
|
+
this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
|
4113
|
+
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
|
4114
|
+
return {
|
|
4115
|
+
ok: (this.files && this.files.length === files.length) || files.length === 0,
|
|
4116
|
+
fileCount: this.files ? this.files.length : 0
|
|
4117
|
+
};
|
|
4118
|
+
}`,
|
|
4119
|
+
arguments: [{ value: fileData }],
|
|
4120
|
+
returnByValue: true
|
|
4121
|
+
});
|
|
4122
|
+
if (!result.result.value.ok) {
|
|
4123
|
+
if (options.optional) return false;
|
|
4124
|
+
throw new Error("Failed to set files on input");
|
|
4125
|
+
}
|
|
4126
|
+
return true;
|
|
4127
|
+
});
|
|
4128
|
+
}
|
|
4129
|
+
async getNativeSelectMetadata(objectId, targets) {
|
|
4130
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4131
|
+
objectId,
|
|
4132
|
+
functionDeclaration: `function(targetValues) {
|
|
4133
|
+
if (!(this instanceof HTMLSelectElement)) {
|
|
4134
|
+
return {
|
|
4135
|
+
currentIndex: -1,
|
|
4136
|
+
currentValue: '',
|
|
4137
|
+
disabled: [],
|
|
4138
|
+
isSelect: false,
|
|
4139
|
+
missing: Array.isArray(targetValues) ? targetValues.map(String) : [],
|
|
4140
|
+
multiple: false,
|
|
4141
|
+
options: [],
|
|
4142
|
+
selectedValues: [],
|
|
4143
|
+
targetIndexes: []
|
|
4144
|
+
};
|
|
4145
|
+
}
|
|
4146
|
+
|
|
4147
|
+
var allOptions = Array.from(this.options).map(function(opt, index) {
|
|
4148
|
+
return { index: index, label: opt.label || opt.text || '', value: opt.value || '' };
|
|
4149
|
+
});
|
|
4150
|
+
var targetIndexes = [];
|
|
4151
|
+
var missing = [];
|
|
4152
|
+
var disabled = [];
|
|
4153
|
+
|
|
4154
|
+
for (var i = 0; i < targetValues.length; i++) {
|
|
4155
|
+
var target = String(targetValues[i]);
|
|
4156
|
+
var idx = -1;
|
|
4157
|
+
|
|
4158
|
+
for (var j = 0; j < this.options.length; j++) {
|
|
4159
|
+
var opt = this.options[j];
|
|
4160
|
+
if (opt.value === target || opt.text === target || opt.label === target) {
|
|
4161
|
+
idx = j;
|
|
4162
|
+
break;
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
|
|
4166
|
+
if (idx === -1 && /^\\d+$/.test(target)) {
|
|
4167
|
+
var numericIndex = parseInt(target, 10);
|
|
4168
|
+
if (numericIndex >= 0 && numericIndex < this.options.length) {
|
|
4169
|
+
idx = numericIndex;
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
|
|
4173
|
+
if (idx === -1) {
|
|
4174
|
+
missing.push(target);
|
|
4175
|
+
continue;
|
|
4176
|
+
}
|
|
4177
|
+
|
|
4178
|
+
if (this.options[idx] && this.options[idx].disabled) {
|
|
4179
|
+
disabled.push(target);
|
|
4180
|
+
continue;
|
|
4181
|
+
}
|
|
4182
|
+
|
|
4183
|
+
if (targetIndexes.indexOf(idx) === -1) {
|
|
4184
|
+
targetIndexes.push(idx);
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
|
|
4188
|
+
return {
|
|
4189
|
+
currentIndex: this.selectedIndex,
|
|
4190
|
+
currentValue: this.value || '',
|
|
4191
|
+
disabled: disabled,
|
|
4192
|
+
isSelect: true,
|
|
4193
|
+
missing: missing,
|
|
4194
|
+
multiple: !!this.multiple,
|
|
4195
|
+
options: allOptions,
|
|
4196
|
+
selectedValues: Array.from(this.selectedOptions).map(function(opt) { return opt.value || ''; }),
|
|
4197
|
+
targetIndexes: targetIndexes
|
|
4198
|
+
};
|
|
4199
|
+
}`,
|
|
4200
|
+
arguments: [{ value: targets }],
|
|
4201
|
+
returnByValue: true
|
|
4202
|
+
});
|
|
4203
|
+
return result.result.value;
|
|
4204
|
+
}
|
|
4205
|
+
async readNativeSelectValues(objectId) {
|
|
4206
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4207
|
+
objectId,
|
|
4208
|
+
functionDeclaration: 'function() { return this instanceof HTMLSelectElement ? Array.from(this.selectedOptions).map(function(opt) { return opt.value || ""; }) : []; }',
|
|
4209
|
+
returnByValue: true
|
|
4210
|
+
});
|
|
4211
|
+
return result.result.value ?? [];
|
|
4212
|
+
}
|
|
4213
|
+
selectValuesMatch(actual, expected, multiple) {
|
|
4214
|
+
if (!multiple) {
|
|
4215
|
+
return (actual[0] ?? "") === (expected[0] ?? "");
|
|
3320
4216
|
}
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
4217
|
+
if (actual.length !== expected.length) {
|
|
4218
|
+
return false;
|
|
4219
|
+
}
|
|
4220
|
+
const actualSorted = [...actual].sort();
|
|
4221
|
+
const expectedSorted = [...expected].sort();
|
|
4222
|
+
return actualSorted.every((value, index) => value === expectedSorted[index]);
|
|
4223
|
+
}
|
|
4224
|
+
async applyNativeSelectByKeyboard(nodeId, objectId, currentIndex, targetIndex) {
|
|
4225
|
+
await this.cdp.send("DOM.focus", { nodeId });
|
|
4226
|
+
if (targetIndex !== currentIndex) {
|
|
4227
|
+
let effectiveIndex = currentIndex;
|
|
4228
|
+
if (effectiveIndex < 0 || targetIndex < effectiveIndex) {
|
|
4229
|
+
await this.dispatchKey("Home");
|
|
4230
|
+
effectiveIndex = 0;
|
|
4231
|
+
}
|
|
4232
|
+
const steps = targetIndex - effectiveIndex;
|
|
4233
|
+
const direction = steps >= 0 ? "ArrowDown" : "ArrowUp";
|
|
4234
|
+
for (let i = 0; i < Math.abs(steps); i++) {
|
|
4235
|
+
await this.dispatchKey(direction);
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
const selectedValues = await this.readNativeSelectValues(objectId);
|
|
4239
|
+
return selectedValues[0] !== void 0;
|
|
4240
|
+
}
|
|
4241
|
+
async applyNativeSelectFallback(objectId, targetIndexes) {
|
|
4242
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
4243
|
+
objectId,
|
|
4244
|
+
functionDeclaration: `function(indexes) {
|
|
4245
|
+
if (!(this instanceof HTMLSelectElement)) return false;
|
|
4246
|
+
|
|
4247
|
+
var wanted = new Set(indexes.map(function(index) { return Number(index); }));
|
|
4248
|
+
for (var i = 0; i < this.options.length; i++) {
|
|
4249
|
+
this.options[i].selected = wanted.has(i);
|
|
4250
|
+
}
|
|
4251
|
+
if (!this.multiple && indexes.length === 1) {
|
|
4252
|
+
this.selectedIndex = indexes[0];
|
|
4253
|
+
}
|
|
4254
|
+
this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
|
4255
|
+
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
|
4256
|
+
return true;
|
|
4257
|
+
}`,
|
|
4258
|
+
arguments: [{ value: targetIndexes }],
|
|
4259
|
+
returnByValue: true
|
|
4260
|
+
});
|
|
4261
|
+
}
|
|
4262
|
+
async selectEditableContent(objectId) {
|
|
4263
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
4264
|
+
objectId,
|
|
4265
|
+
functionDeclaration: `function() {
|
|
4266
|
+
if (this.isContentEditable) {
|
|
4267
|
+
this.focus();
|
|
4268
|
+
const range = document.createRange();
|
|
4269
|
+
range.selectNodeContents(this);
|
|
4270
|
+
const selection = window.getSelection();
|
|
4271
|
+
if (selection) {
|
|
4272
|
+
selection.removeAllRanges();
|
|
4273
|
+
selection.addRange(range);
|
|
4274
|
+
}
|
|
4275
|
+
return;
|
|
3329
4276
|
}
|
|
3330
|
-
return { name: f.name, mimeType: f.mimeType, data: base64 };
|
|
3331
|
-
})
|
|
3332
|
-
);
|
|
3333
|
-
await this.cdp.send("Runtime.evaluate", {
|
|
3334
|
-
expression: `(() => {
|
|
3335
|
-
const input = document.querySelector(${JSON.stringify(element.selector)});
|
|
3336
|
-
if (!input) return false;
|
|
3337
4277
|
|
|
3338
|
-
|
|
3339
|
-
|
|
4278
|
+
if (this.tagName === 'TEXTAREA') {
|
|
4279
|
+
this.selectionStart = 0;
|
|
4280
|
+
this.selectionEnd = this.value.length;
|
|
4281
|
+
this.focus();
|
|
4282
|
+
return;
|
|
4283
|
+
}
|
|
3340
4284
|
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
4285
|
+
if (typeof this.select === 'function') {
|
|
4286
|
+
this.select();
|
|
4287
|
+
}
|
|
4288
|
+
this.focus();
|
|
4289
|
+
}`
|
|
4290
|
+
});
|
|
4291
|
+
}
|
|
4292
|
+
async clearEditableSelection(objectId, key) {
|
|
4293
|
+
await this.selectEditableContent(objectId);
|
|
4294
|
+
await this.dispatchKey(key);
|
|
4295
|
+
}
|
|
4296
|
+
async readEditableValue(objectId) {
|
|
4297
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4298
|
+
objectId,
|
|
4299
|
+
functionDeclaration: `function() {
|
|
4300
|
+
if (this.isContentEditable) {
|
|
4301
|
+
return this.textContent || '';
|
|
3345
4302
|
}
|
|
4303
|
+
return this.value || '';
|
|
4304
|
+
}`,
|
|
4305
|
+
returnByValue: true
|
|
4306
|
+
});
|
|
4307
|
+
return result.result.value ?? "";
|
|
4308
|
+
}
|
|
4309
|
+
async typeEditableFallback(nodeId, objectId, value) {
|
|
4310
|
+
await this.selectEditableContent(objectId);
|
|
4311
|
+
await this.cdp.send("DOM.focus", { nodeId });
|
|
4312
|
+
for (const char of value) {
|
|
4313
|
+
await this.dispatchKey(char);
|
|
4314
|
+
}
|
|
4315
|
+
}
|
|
4316
|
+
async applyRecordedSelectFallback(objectId, targetIndexes) {
|
|
4317
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
4318
|
+
objectId,
|
|
4319
|
+
functionDeclaration: `function(indexes) {
|
|
4320
|
+
if (!(this instanceof HTMLSelectElement)) return false;
|
|
3346
4321
|
|
|
3347
|
-
|
|
3348
|
-
|
|
4322
|
+
var wanted = new Set(indexes.map(function(index) { return Number(index); }));
|
|
4323
|
+
for (var i = 0; i < this.options.length; i++) {
|
|
4324
|
+
this.options[i].selected = wanted.has(i);
|
|
4325
|
+
}
|
|
4326
|
+
if (!this.multiple && indexes.length === 1) {
|
|
4327
|
+
this.selectedIndex = indexes[0];
|
|
4328
|
+
}
|
|
3349
4329
|
return true;
|
|
3350
|
-
}
|
|
4330
|
+
}`,
|
|
4331
|
+
arguments: [{ value: targetIndexes }],
|
|
3351
4332
|
returnByValue: true
|
|
3352
4333
|
});
|
|
3353
|
-
return
|
|
4334
|
+
return this.invokeRecordedEventListeners(objectId, ["input", "change"]);
|
|
4335
|
+
}
|
|
4336
|
+
async invokeRecordedEventListeners(objectId, eventTypes) {
|
|
4337
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4338
|
+
objectId,
|
|
4339
|
+
functionDeclaration: `function(types) {
|
|
4340
|
+
function buildPath(target) {
|
|
4341
|
+
var path = [];
|
|
4342
|
+
var node = target;
|
|
4343
|
+
|
|
4344
|
+
while (node) {
|
|
4345
|
+
path.push(node);
|
|
4346
|
+
|
|
4347
|
+
if (node.parentElement) {
|
|
4348
|
+
node = node.parentElement;
|
|
4349
|
+
continue;
|
|
4350
|
+
}
|
|
4351
|
+
|
|
4352
|
+
if (node === document) {
|
|
4353
|
+
node = window;
|
|
4354
|
+
continue;
|
|
4355
|
+
}
|
|
4356
|
+
|
|
4357
|
+
if (node.defaultView && node !== node.defaultView) {
|
|
4358
|
+
node = node.defaultView;
|
|
4359
|
+
continue;
|
|
4360
|
+
}
|
|
4361
|
+
|
|
4362
|
+
if (node.ownerDocument && node !== node.ownerDocument) {
|
|
4363
|
+
node = node.ownerDocument;
|
|
4364
|
+
continue;
|
|
4365
|
+
}
|
|
4366
|
+
|
|
4367
|
+
var root = node.getRootNode && node.getRootNode();
|
|
4368
|
+
if (root && root !== node && root.host) {
|
|
4369
|
+
node = root.host;
|
|
4370
|
+
continue;
|
|
4371
|
+
}
|
|
4372
|
+
|
|
4373
|
+
node = null;
|
|
4374
|
+
}
|
|
4375
|
+
|
|
4376
|
+
return path;
|
|
4377
|
+
}
|
|
4378
|
+
|
|
4379
|
+
function createEvent(type, target, currentTarget, path, phase) {
|
|
4380
|
+
return {
|
|
4381
|
+
type: type,
|
|
4382
|
+
target: target,
|
|
4383
|
+
currentTarget: currentTarget,
|
|
4384
|
+
srcElement: target,
|
|
4385
|
+
isTrusted: true,
|
|
4386
|
+
bubbles: true,
|
|
4387
|
+
cancelable: true,
|
|
4388
|
+
composed: true,
|
|
4389
|
+
defaultPrevented: false,
|
|
4390
|
+
eventPhase: phase,
|
|
4391
|
+
timeStamp: Date.now(),
|
|
4392
|
+
preventDefault: function() {
|
|
4393
|
+
this.defaultPrevented = true;
|
|
4394
|
+
},
|
|
4395
|
+
stopPropagation: function() {
|
|
4396
|
+
this.__stopped = true;
|
|
4397
|
+
},
|
|
4398
|
+
stopImmediatePropagation: function() {
|
|
4399
|
+
this.__stopped = true;
|
|
4400
|
+
this.__immediateStopped = true;
|
|
4401
|
+
},
|
|
4402
|
+
composedPath: function() {
|
|
4403
|
+
return path.slice();
|
|
4404
|
+
}
|
|
4405
|
+
};
|
|
4406
|
+
}
|
|
4407
|
+
|
|
4408
|
+
function invokePhase(type, nodes, capture, target, path) {
|
|
4409
|
+
var invoked = false;
|
|
4410
|
+
|
|
4411
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
4412
|
+
var currentTarget = nodes[i];
|
|
4413
|
+
var store = currentTarget && currentTarget.__bpEventListeners;
|
|
4414
|
+
var entries = store && store[type];
|
|
4415
|
+
if (!Array.isArray(entries) || entries.length === 0) continue;
|
|
4416
|
+
|
|
4417
|
+
var phase = currentTarget === target ? 2 : capture ? 1 : 3;
|
|
4418
|
+
var event = createEvent(type, target, currentTarget, path, phase);
|
|
4419
|
+
|
|
4420
|
+
for (var j = 0; j < entries.length; j++) {
|
|
4421
|
+
var entry = entries[j];
|
|
4422
|
+
if (!!entry.capture !== capture) continue;
|
|
4423
|
+
|
|
4424
|
+
var listener = entry.listener;
|
|
4425
|
+
if (typeof listener === 'function') {
|
|
4426
|
+
listener.call(currentTarget, event);
|
|
4427
|
+
invoked = true;
|
|
4428
|
+
} else if (listener && typeof listener.handleEvent === 'function') {
|
|
4429
|
+
listener.handleEvent(event);
|
|
4430
|
+
invoked = true;
|
|
4431
|
+
}
|
|
4432
|
+
|
|
4433
|
+
if (event.__immediateStopped) {
|
|
4434
|
+
break;
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4437
|
+
|
|
4438
|
+
if (event.__stopped) {
|
|
4439
|
+
break;
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
|
|
4443
|
+
return invoked;
|
|
4444
|
+
}
|
|
4445
|
+
|
|
4446
|
+
var path = buildPath(this);
|
|
4447
|
+
var capturePath = path.slice().reverse();
|
|
4448
|
+
var bubblePath = path.slice();
|
|
4449
|
+
var invokedAny = false;
|
|
4450
|
+
|
|
4451
|
+
for (var i = 0; i < types.length; i++) {
|
|
4452
|
+
var type = String(types[i]);
|
|
4453
|
+
if (invokePhase(type, capturePath, true, this, path)) {
|
|
4454
|
+
invokedAny = true;
|
|
4455
|
+
}
|
|
4456
|
+
if (invokePhase(type, bubblePath, false, this, path)) {
|
|
4457
|
+
invokedAny = true;
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
|
|
4461
|
+
return invokedAny;
|
|
4462
|
+
}`,
|
|
4463
|
+
arguments: [{ value: eventTypes }],
|
|
4464
|
+
returnByValue: true
|
|
4465
|
+
});
|
|
4466
|
+
return result.result.value ?? false;
|
|
3354
4467
|
}
|
|
3355
4468
|
/**
|
|
3356
4469
|
* Wait for a download to complete, triggered by an action
|
|
@@ -3510,7 +4623,7 @@ var Page = class {
|
|
|
3510
4623
|
return lines.join("\n");
|
|
3511
4624
|
};
|
|
3512
4625
|
const text = formatTree(accessibilityTree);
|
|
3513
|
-
|
|
4626
|
+
const result = {
|
|
3514
4627
|
url,
|
|
3515
4628
|
title,
|
|
3516
4629
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -3518,6 +4631,8 @@ var Page = class {
|
|
|
3518
4631
|
interactiveElements,
|
|
3519
4632
|
text
|
|
3520
4633
|
};
|
|
4634
|
+
this.lastSnapshot = result;
|
|
4635
|
+
return result;
|
|
3521
4636
|
}
|
|
3522
4637
|
/**
|
|
3523
4638
|
* Export the current ref map for cross-exec reuse (CLI).
|
|
@@ -4046,11 +5161,13 @@ var Page = class {
|
|
|
4046
5161
|
try {
|
|
4047
5162
|
return await fn();
|
|
4048
5163
|
} catch (e) {
|
|
4049
|
-
|
|
5164
|
+
const message = e instanceof Error ? e.message : "";
|
|
5165
|
+
if (e instanceof Error && (message.includes("Could not find node with given id") || message.includes("Node with given id does not belong to the document") || message.includes("No node with given id found") || message.includes("Could not find object with given id") || message.includes("Cannot find context with specified id") || message.includes("Cannot find context with given id") || message.includes("Execution context was destroyed") || message.includes("No execution context with given id") || message.includes("Argument should belong to the same JavaScript world"))) {
|
|
4050
5166
|
lastError = e;
|
|
4051
5167
|
if (attempt < retries) {
|
|
4052
5168
|
this.rootNodeId = null;
|
|
4053
|
-
|
|
5169
|
+
this.currentFrameContextId = null;
|
|
5170
|
+
await sleep4(delay);
|
|
4054
5171
|
continue;
|
|
4055
5172
|
}
|
|
4056
5173
|
}
|
|
@@ -4095,6 +5212,39 @@ var Page = class {
|
|
|
4095
5212
|
}
|
|
4096
5213
|
}
|
|
4097
5214
|
}
|
|
5215
|
+
if (selectorList.every((s) => s.startsWith("ref:")) && this.lastSnapshot) {
|
|
5216
|
+
for (const selector of selectorList) {
|
|
5217
|
+
const ref = selector.slice(4);
|
|
5218
|
+
const originalElement = this.lastSnapshot.interactiveElements.find((e) => e.ref === ref);
|
|
5219
|
+
if (!originalElement) continue;
|
|
5220
|
+
const freshSnapshot = await this.snapshot();
|
|
5221
|
+
const match = freshSnapshot.interactiveElements.find(
|
|
5222
|
+
(e) => e.role === originalElement.role && e.name === originalElement.name
|
|
5223
|
+
);
|
|
5224
|
+
if (match) {
|
|
5225
|
+
const newBackendNodeId = this.refMap.get(match.ref);
|
|
5226
|
+
if (newBackendNodeId) {
|
|
5227
|
+
try {
|
|
5228
|
+
await this.ensureRootNode();
|
|
5229
|
+
const pushResult = await this.cdp.send(
|
|
5230
|
+
"DOM.pushNodesByBackendIdsToFrontend",
|
|
5231
|
+
{ backendNodeIds: [newBackendNodeId] }
|
|
5232
|
+
);
|
|
5233
|
+
if (pushResult.nodeIds?.[0]) {
|
|
5234
|
+
this._lastMatchedSelector = `ref:${match.ref}`;
|
|
5235
|
+
return {
|
|
5236
|
+
nodeId: pushResult.nodeIds[0],
|
|
5237
|
+
backendNodeId: newBackendNodeId,
|
|
5238
|
+
selector: `ref:${match.ref}`,
|
|
5239
|
+
waitedMs: 0
|
|
5240
|
+
};
|
|
5241
|
+
}
|
|
5242
|
+
} catch {
|
|
5243
|
+
}
|
|
5244
|
+
}
|
|
5245
|
+
}
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
4098
5248
|
const cssSelectors = selectorList.filter((s) => !s.startsWith("ref:"));
|
|
4099
5249
|
if (cssSelectors.length === 0) {
|
|
4100
5250
|
return null;
|
|
@@ -4158,6 +5308,38 @@ var Page = class {
|
|
|
4158
5308
|
*/
|
|
4159
5309
|
async ensureRootNode() {
|
|
4160
5310
|
if (this.rootNodeId) return;
|
|
5311
|
+
if (this.currentFrame) {
|
|
5312
|
+
const mainDocument = await this.cdp.send("DOM.getDocument", {
|
|
5313
|
+
depth: 0
|
|
5314
|
+
});
|
|
5315
|
+
const iframeNode = await this.cdp.send("DOM.querySelector", {
|
|
5316
|
+
nodeId: mainDocument.root.nodeId,
|
|
5317
|
+
selector: this.currentFrame
|
|
5318
|
+
});
|
|
5319
|
+
if (iframeNode.nodeId) {
|
|
5320
|
+
const frameResult = await this.cdp.send("DOM.describeNode", {
|
|
5321
|
+
nodeId: iframeNode.nodeId,
|
|
5322
|
+
depth: 1
|
|
5323
|
+
});
|
|
5324
|
+
if (frameResult.node.contentDocument?.nodeId) {
|
|
5325
|
+
this.rootNodeId = frameResult.node.contentDocument.nodeId;
|
|
5326
|
+
if (frameResult.node.frameId) {
|
|
5327
|
+
let contextId = this.frameExecutionContexts.get(frameResult.node.frameId);
|
|
5328
|
+
if (!contextId) {
|
|
5329
|
+
for (let i = 0; i < 10; i++) {
|
|
5330
|
+
await sleep4(100);
|
|
5331
|
+
contextId = this.frameExecutionContexts.get(frameResult.node.frameId);
|
|
5332
|
+
if (contextId) break;
|
|
5333
|
+
}
|
|
5334
|
+
}
|
|
5335
|
+
this.currentFrameContextId = contextId ?? null;
|
|
5336
|
+
}
|
|
5337
|
+
return;
|
|
5338
|
+
}
|
|
5339
|
+
}
|
|
5340
|
+
this.currentFrame = null;
|
|
5341
|
+
this.currentFrameContextId = null;
|
|
5342
|
+
}
|
|
4161
5343
|
const doc = await this.cdp.send("DOM.getDocument", {
|
|
4162
5344
|
depth: 0
|
|
4163
5345
|
});
|
|
@@ -4198,30 +5380,115 @@ var Page = class {
|
|
|
4198
5380
|
}
|
|
4199
5381
|
}
|
|
4200
5382
|
/**
|
|
4201
|
-
* Click an element by node ID
|
|
5383
|
+
* Click an element by node ID using Playwright's 3-event sequence:
|
|
5384
|
+
* mouseMoved → mousePressed → mouseReleased (sequential).
|
|
5385
|
+
* Uses DOM.getContentQuads for accurate coordinates (handles CSS transforms).
|
|
5386
|
+
* Falls back to JS this.click() if CDP mouse dispatch fails.
|
|
4202
5387
|
*/
|
|
4203
5388
|
async clickElement(nodeId) {
|
|
4204
|
-
const
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
5389
|
+
const { object } = await this.cdp.send("DOM.resolveNode", {
|
|
5390
|
+
nodeId
|
|
5391
|
+
});
|
|
5392
|
+
let x;
|
|
5393
|
+
let y;
|
|
5394
|
+
try {
|
|
5395
|
+
const { quads } = await this.cdp.send("DOM.getContentQuads", {
|
|
5396
|
+
objectId: object.objectId
|
|
5397
|
+
});
|
|
5398
|
+
if (quads && quads.length > 0) {
|
|
5399
|
+
const quad = quads[0];
|
|
5400
|
+
x = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
|
|
5401
|
+
y = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
|
|
5402
|
+
} else {
|
|
5403
|
+
throw new Error("No quads");
|
|
5404
|
+
}
|
|
5405
|
+
} catch {
|
|
5406
|
+
const box = await this.getBoxModel(nodeId);
|
|
5407
|
+
if (!box) throw new Error("Could not get element position for click");
|
|
5408
|
+
x = box.content[0] + box.width / 2;
|
|
5409
|
+
y = box.content[1] + box.height / 2;
|
|
5410
|
+
}
|
|
5411
|
+
try {
|
|
5412
|
+
await this.cdp.send("Input.dispatchMouseEvent", {
|
|
5413
|
+
type: "mouseMoved",
|
|
5414
|
+
x,
|
|
5415
|
+
y,
|
|
5416
|
+
button: "none",
|
|
5417
|
+
buttons: 0,
|
|
5418
|
+
modifiers: 0
|
|
5419
|
+
});
|
|
5420
|
+
await this.cdp.send("Input.dispatchMouseEvent", {
|
|
5421
|
+
type: "mousePressed",
|
|
5422
|
+
x,
|
|
5423
|
+
y,
|
|
5424
|
+
button: "left",
|
|
5425
|
+
buttons: 1,
|
|
5426
|
+
clickCount: 1,
|
|
5427
|
+
modifiers: 0
|
|
5428
|
+
});
|
|
5429
|
+
await this.cdp.send("Input.dispatchMouseEvent", {
|
|
5430
|
+
type: "mouseReleased",
|
|
5431
|
+
x,
|
|
5432
|
+
y,
|
|
5433
|
+
button: "left",
|
|
5434
|
+
buttons: 0,
|
|
5435
|
+
clickCount: 1,
|
|
5436
|
+
modifiers: 0
|
|
5437
|
+
});
|
|
5438
|
+
} catch {
|
|
5439
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
5440
|
+
objectId: object.objectId,
|
|
5441
|
+
functionDeclaration: "function() { this.click(); }"
|
|
5442
|
+
});
|
|
5443
|
+
}
|
|
5444
|
+
await this.cdp.send("Runtime.evaluate", { expression: "0" });
|
|
5445
|
+
}
|
|
5446
|
+
/**
|
|
5447
|
+
* Resolve a nodeId to a Remote Object ID for use with Runtime.callFunctionOn
|
|
5448
|
+
*/
|
|
5449
|
+
async resolveObjectId(nodeId) {
|
|
5450
|
+
const { object } = await this.cdp.send("DOM.resolveNode", {
|
|
5451
|
+
nodeId
|
|
4216
5452
|
});
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
5453
|
+
return object.objectId;
|
|
5454
|
+
}
|
|
5455
|
+
async dispatchKeyDefinition(def) {
|
|
5456
|
+
const downParams = {
|
|
5457
|
+
type: def.text !== void 0 ? "keyDown" : "rawKeyDown",
|
|
5458
|
+
key: def.key,
|
|
5459
|
+
code: def.code,
|
|
5460
|
+
windowsVirtualKeyCode: def.keyCode,
|
|
5461
|
+
modifiers: 0,
|
|
5462
|
+
autoRepeat: false,
|
|
5463
|
+
location: def.location ?? 0,
|
|
5464
|
+
isKeypad: false
|
|
5465
|
+
};
|
|
5466
|
+
if (def.text !== void 0) {
|
|
5467
|
+
downParams["text"] = def.text;
|
|
5468
|
+
downParams["unmodifiedText"] = def.text;
|
|
5469
|
+
}
|
|
5470
|
+
await this.cdp.send("Input.dispatchKeyEvent", downParams);
|
|
5471
|
+
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
5472
|
+
type: "keyUp",
|
|
5473
|
+
key: def.key,
|
|
5474
|
+
code: def.code,
|
|
5475
|
+
windowsVirtualKeyCode: def.keyCode,
|
|
5476
|
+
modifiers: 0,
|
|
5477
|
+
location: def.location ?? 0
|
|
4223
5478
|
});
|
|
4224
5479
|
}
|
|
5480
|
+
async dispatchKey(key) {
|
|
5481
|
+
const def = US_KEYBOARD[key];
|
|
5482
|
+
if (def) {
|
|
5483
|
+
await this.dispatchKeyDefinition(def);
|
|
5484
|
+
return;
|
|
5485
|
+
}
|
|
5486
|
+
if ([...key].length === 1) {
|
|
5487
|
+
await this.cdp.send("Input.insertText", { text: key });
|
|
5488
|
+
return;
|
|
5489
|
+
}
|
|
5490
|
+
await this.dispatchKeyDefinition({ key, code: key, keyCode: 0 });
|
|
5491
|
+
}
|
|
4225
5492
|
// ============ Audio I/O ============
|
|
4226
5493
|
/**
|
|
4227
5494
|
* Audio input controller (fake microphone).
|
|
@@ -4294,7 +5561,7 @@ var Page = class {
|
|
|
4294
5561
|
const start = Date.now();
|
|
4295
5562
|
await this.audioOutput.start();
|
|
4296
5563
|
if (options.preDelay && options.preDelay > 0) {
|
|
4297
|
-
await
|
|
5564
|
+
await sleep4(options.preDelay);
|
|
4298
5565
|
}
|
|
4299
5566
|
const inputDone = this.audioInput.play(options.input, {
|
|
4300
5567
|
waitForEnd: !!options.sendSelector
|
|
@@ -4322,11 +5589,27 @@ var Page = class {
|
|
|
4322
5589
|
};
|
|
4323
5590
|
}
|
|
4324
5591
|
};
|
|
4325
|
-
function
|
|
5592
|
+
function sleep4(ms) {
|
|
4326
5593
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4327
5594
|
}
|
|
4328
5595
|
|
|
4329
5596
|
// src/browser/browser.ts
|
|
5597
|
+
function scoreTarget(t) {
|
|
5598
|
+
let score = 0;
|
|
5599
|
+
if (t.url.startsWith("http://") || t.url.startsWith("https://")) score += 10;
|
|
5600
|
+
if (t.url.startsWith("chrome://")) score -= 20;
|
|
5601
|
+
if (t.url.startsWith("chrome-extension://")) score -= 15;
|
|
5602
|
+
if (t.url.startsWith("devtools://")) score -= 25;
|
|
5603
|
+
if (t.url === "about:blank") score -= 5;
|
|
5604
|
+
if (!t.attached) score += 3;
|
|
5605
|
+
if (t.title && t.title.length > 0) score += 2;
|
|
5606
|
+
return score;
|
|
5607
|
+
}
|
|
5608
|
+
function pickBestTarget(targets) {
|
|
5609
|
+
if (targets.length === 0) return void 0;
|
|
5610
|
+
const sorted = [...targets].sort((a, b) => scoreTarget(b) - scoreTarget(a));
|
|
5611
|
+
return sorted[0].targetId;
|
|
5612
|
+
}
|
|
4330
5613
|
var Browser = class _Browser {
|
|
4331
5614
|
cdp;
|
|
4332
5615
|
providerSession;
|
|
@@ -4348,28 +5631,46 @@ var Browser = class _Browser {
|
|
|
4348
5631
|
return new _Browser(cdp, provider, session, options);
|
|
4349
5632
|
}
|
|
4350
5633
|
/**
|
|
4351
|
-
* Get or create a page by name
|
|
4352
|
-
* If no name is provided, returns the first available page or creates a new one
|
|
5634
|
+
* Get or create a page by name.
|
|
5635
|
+
* If no name is provided, returns the first available page or creates a new one.
|
|
5636
|
+
*
|
|
5637
|
+
* Target selection heuristics (when no targetId is specified):
|
|
5638
|
+
* - Prefer http/https URLs over chrome://, devtools://, about:blank
|
|
5639
|
+
* - Prefer unattached targets (not already controlled by another client)
|
|
5640
|
+
* - Filter by targetUrl if provided
|
|
4353
5641
|
*/
|
|
4354
5642
|
async page(name, options) {
|
|
4355
5643
|
const pageName = name ?? "default";
|
|
4356
5644
|
const cached = this.pages.get(pageName);
|
|
4357
5645
|
if (cached) return cached;
|
|
4358
5646
|
const targets = await this.cdp.send("Target.getTargets");
|
|
4359
|
-
|
|
5647
|
+
let pageTargets = targets.targetInfos.filter((t) => t.type === "page");
|
|
5648
|
+
if (options?.targetUrl) {
|
|
5649
|
+
const urlFilter = options.targetUrl;
|
|
5650
|
+
const filtered = pageTargets.filter((t) => t.url.includes(urlFilter));
|
|
5651
|
+
if (filtered.length > 0) {
|
|
5652
|
+
pageTargets = filtered;
|
|
5653
|
+
} else {
|
|
5654
|
+
console.warn(
|
|
5655
|
+
`[browser-pilot] No targets match URL filter "${urlFilter}", falling back to all page targets`
|
|
5656
|
+
);
|
|
5657
|
+
}
|
|
5658
|
+
}
|
|
4360
5659
|
let targetId;
|
|
4361
5660
|
if (options?.targetId) {
|
|
4362
|
-
const targetExists =
|
|
5661
|
+
const targetExists = targets.targetInfos.some(
|
|
5662
|
+
(t) => t.type === "page" && t.targetId === options.targetId
|
|
5663
|
+
);
|
|
4363
5664
|
if (targetExists) {
|
|
4364
5665
|
targetId = options.targetId;
|
|
4365
5666
|
} else {
|
|
4366
5667
|
console.warn(`[browser-pilot] Target ${options.targetId} no longer exists, falling back`);
|
|
4367
|
-
targetId = pageTargets
|
|
5668
|
+
targetId = pickBestTarget(pageTargets) ?? (await this.cdp.send("Target.createTarget", {
|
|
4368
5669
|
url: "about:blank"
|
|
4369
5670
|
})).targetId;
|
|
4370
5671
|
}
|
|
4371
5672
|
} else if (pageTargets.length > 0) {
|
|
4372
|
-
targetId = pageTargets
|
|
5673
|
+
targetId = pickBestTarget(pageTargets);
|
|
4373
5674
|
} else {
|
|
4374
5675
|
const result = await this.cdp.send("Target.createTarget", {
|
|
4375
5676
|
url: "about:blank"
|
|
@@ -4379,6 +5680,21 @@ var Browser = class _Browser {
|
|
|
4379
5680
|
await this.cdp.attachToTarget(targetId);
|
|
4380
5681
|
const page = new Page(this.cdp, targetId);
|
|
4381
5682
|
await page.init();
|
|
5683
|
+
const minViewport = options?.minViewport !== void 0 ? options.minViewport : { width: 200, height: 200 };
|
|
5684
|
+
if (minViewport !== false) {
|
|
5685
|
+
try {
|
|
5686
|
+
const viewport = await page.evaluate(
|
|
5687
|
+
"({ w: window.innerWidth, h: window.innerHeight })"
|
|
5688
|
+
);
|
|
5689
|
+
if (viewport.w < minViewport.width || viewport.h < minViewport.height) {
|
|
5690
|
+
console.warn(
|
|
5691
|
+
`[browser-pilot] Attached target has small viewport (${viewport.w}x${viewport.h}). Applying default viewport override (1280x720). Use { minViewport: false } to disable this check.`
|
|
5692
|
+
);
|
|
5693
|
+
await page.setViewport({ width: 1280, height: 720 });
|
|
5694
|
+
}
|
|
5695
|
+
} catch {
|
|
5696
|
+
}
|
|
5697
|
+
}
|
|
4382
5698
|
this.pages.set(pageName, page);
|
|
4383
5699
|
return page;
|
|
4384
5700
|
}
|