cdp-skill 1.0.15 → 1.0.17
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 +4 -4
- package/SKILL.md +276 -170
- package/package.json +9 -8
- package/{src → scripts}/aria/index.js +1 -1
- package/scripts/aria/role-query.js +295 -0
- package/{src → scripts}/aria.js +11 -5
- package/{src → scripts}/capture/console-capture.js +11 -9
- package/{src → scripts}/capture/screenshot-capture.js +8 -9
- package/{src → scripts}/cdp/connection.js +30 -6
- package/{src → scripts}/cdp-skill.js +7 -6
- package/{src → scripts}/diff.js +7 -6
- package/{src → scripts}/dom/LazyResolver.js +23 -12
- package/{src → scripts}/dom/actionability.js +39 -22
- package/{src → scripts}/dom/click-executor.js +90 -53
- package/{src → scripts}/dom/element-locator.js +4 -4
- package/{src → scripts}/dom/fill-executor.js +8 -4
- package/{src → scripts}/dom/input-emulator.js +47 -9
- package/{src → scripts}/dom/react-filler.js +11 -3
- package/{src → scripts}/dom/wait-executor.js +10 -2
- package/{src → scripts}/page/dialog-handler.js +7 -3
- package/{src → scripts}/page/dom-stability.js +17 -10
- package/{src → scripts}/page/page-controller.js +41 -34
- package/{src → scripts}/runner/context-helpers.js +7 -0
- package/{src → scripts}/runner/execute-browser.js +3 -118
- package/{src → scripts}/runner/execute-dynamic.js +46 -11
- package/{src → scripts}/runner/execute-form.js +6 -4
- package/{src → scripts}/runner/execute-input.js +127 -100
- package/{src → scripts}/runner/execute-interaction.js +31 -46
- package/{src → scripts}/runner/execute-navigation.js +14 -12
- package/{src → scripts}/runner/step-executors.js +28 -9
- package/{src → scripts}/runner/step-registry.js +57 -8
- package/{src → scripts}/runner/step-validator.js +13 -3
- package/{src → scripts}/tests/ExecuteInput.test.js +58 -188
- package/src/aria/role-query.js +0 -1229
- package/src/aria/snapshot.js +0 -459
- /package/{src → scripts}/aria/output-processor.js +0 -0
- /package/{src → scripts}/capture/debug-capture.js +0 -0
- /package/{src → scripts}/capture/error-aggregator.js +0 -0
- /package/{src → scripts}/capture/eval-serializer.js +0 -0
- /package/{src → scripts}/capture/index.js +0 -0
- /package/{src → scripts}/capture/network-capture.js +0 -0
- /package/{src → scripts}/capture/pdf-capture.js +0 -0
- /package/{src → scripts}/cdp/browser.js +0 -0
- /package/{src → scripts}/cdp/discovery.js +0 -0
- /package/{src → scripts}/cdp/index.js +0 -0
- /package/{src → scripts}/cdp/target-and-session.js +0 -0
- /package/{src → scripts}/constants.js +0 -0
- /package/{src → scripts}/dom/element-handle.js +0 -0
- /package/{src → scripts}/dom/element-validator.js +0 -0
- /package/{src → scripts}/dom/index.js +0 -0
- /package/{src → scripts}/dom/keyboard-executor.js +0 -0
- /package/{src → scripts}/dom/quad-helpers.js +0 -0
- /package/{src → scripts}/index.js +0 -0
- /package/{src → scripts}/page/cookie-manager.js +0 -0
- /package/{src → scripts}/page/index.js +0 -0
- /package/{src → scripts}/page/wait-utilities.js +0 -0
- /package/{src → scripts}/page/web-storage-manager.js +0 -0
- /package/{src → scripts}/runner/execute-query.js +0 -0
- /package/{src → scripts}/runner/index.js +0 -0
- /package/{src → scripts}/tests/Actionability.test.js +0 -0
- /package/{src → scripts}/tests/Aria.test.js +0 -0
- /package/{src → scripts}/tests/BrowserClient.test.js +0 -0
- /package/{src → scripts}/tests/CDPConnection.test.js +0 -0
- /package/{src → scripts}/tests/ChromeDiscovery.test.js +0 -0
- /package/{src → scripts}/tests/ClickExecutor.test.js +0 -0
- /package/{src → scripts}/tests/ConsoleCapture.test.js +0 -0
- /package/{src → scripts}/tests/ContextHelpers.test.js +0 -0
- /package/{src → scripts}/tests/CookieManager.test.js +0 -0
- /package/{src → scripts}/tests/DebugCapture.test.js +0 -0
- /package/{src → scripts}/tests/ElementHandle.test.js +0 -0
- /package/{src → scripts}/tests/ElementLocator.test.js +0 -0
- /package/{src → scripts}/tests/ErrorAggregator.test.js +0 -0
- /package/{src → scripts}/tests/EvalSerializer.test.js +0 -0
- /package/{src → scripts}/tests/ExecuteBrowser.test.js +0 -0
- /package/{src → scripts}/tests/ExecuteDynamic.test.js +0 -0
- /package/{src → scripts}/tests/ExecuteForm.test.js +0 -0
- /package/{src → scripts}/tests/ExecuteInteraction.test.js +0 -0
- /package/{src → scripts}/tests/ExecuteQuery.test.js +0 -0
- /package/{src → scripts}/tests/FillExecutor.test.js +0 -0
- /package/{src → scripts}/tests/InputEmulator.test.js +0 -0
- /package/{src → scripts}/tests/KeyboardExecutor.test.js +0 -0
- /package/{src → scripts}/tests/LazyResolver.test.js +0 -0
- /package/{src → scripts}/tests/NetworkErrorCapture.test.js +0 -0
- /package/{src → scripts}/tests/PageController.test.js +0 -0
- /package/{src → scripts}/tests/PdfCapture.test.js +0 -0
- /package/{src → scripts}/tests/ScreenshotCapture.test.js +0 -0
- /package/{src → scripts}/tests/SessionRegistry.test.js +0 -0
- /package/{src → scripts}/tests/StepValidator.test.js +0 -0
- /package/{src → scripts}/tests/TargetManager.test.js +0 -0
- /package/{src → scripts}/tests/TestRunner.test.js +0 -0
- /package/{src → scripts}/tests/WaitStrategy.test.js +0 -0
- /package/{src → scripts}/tests/WaitUtilities.test.js +0 -0
- /package/{src → scripts}/tests/WebStorageManager.test.js +0 -0
- /package/{src → scripts}/tests/integration.test.js +0 -0
- /package/{src → scripts}/types.js +0 -0
- /package/{src → scripts}/utils/backoff.js +0 -0
- /package/{src → scripts}/utils/cdp-helpers.js +0 -0
- /package/{src → scripts}/utils/devices.js +0 -0
- /package/{src → scripts}/utils/errors.js +0 -0
- /package/{src → scripts}/utils/index.js +0 -0
- /package/{src → scripts}/utils/temp.js +0 -0
- /package/{src → scripts}/utils/validators.js +0 -0
- /package/{src → scripts}/utils.js +0 -0
|
@@ -22,9 +22,22 @@ const stableFrameCount = 3;
|
|
|
22
22
|
/**
|
|
23
23
|
* Create an actionability checker for Playwright-style auto-waiting
|
|
24
24
|
* @param {Object} session - CDP session
|
|
25
|
+
* @param {Object} [options] - Options
|
|
26
|
+
* @param {function(): number|null} [options.getFrameContext] - Returns contextId for current frame
|
|
25
27
|
* @returns {Object} Actionability checker interface
|
|
26
28
|
*/
|
|
27
|
-
export function createActionabilityChecker(session) {
|
|
29
|
+
export function createActionabilityChecker(session, options = {}) {
|
|
30
|
+
const { getFrameContext } = options;
|
|
31
|
+
|
|
32
|
+
/** Build Runtime.evaluate params with frame context when in an iframe. */
|
|
33
|
+
function evalParams(expression, returnByValue = true) {
|
|
34
|
+
const params = { expression, returnByValue };
|
|
35
|
+
if (getFrameContext) {
|
|
36
|
+
const contextId = getFrameContext();
|
|
37
|
+
if (contextId) params.contextId = contextId;
|
|
38
|
+
}
|
|
39
|
+
return params;
|
|
40
|
+
}
|
|
28
41
|
// Simplified: removed stability check, shorter retry delays
|
|
29
42
|
const retryDelays = [0, 50, 100, 200];
|
|
30
43
|
|
|
@@ -48,10 +61,18 @@ export function createActionabilityChecker(session) {
|
|
|
48
61
|
|
|
49
62
|
async function findElementInternal(selector) {
|
|
50
63
|
try {
|
|
51
|
-
const result = await session.send('Runtime.evaluate',
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
const result = await session.send('Runtime.evaluate',
|
|
65
|
+
evalParams(`document.querySelector(${JSON.stringify(selector)})`, false)
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Check for selector syntax errors (e.g., invalid CSS selectors)
|
|
69
|
+
if (result.exceptionDetails) {
|
|
70
|
+
const msg = result.exceptionDetails.exception?.description ||
|
|
71
|
+
result.exceptionDetails.exception?.value ||
|
|
72
|
+
result.exceptionDetails.text ||
|
|
73
|
+
'Unknown selector error';
|
|
74
|
+
return { success: false, error: `Selector error: ${msg}`, immediate: true };
|
|
75
|
+
}
|
|
55
76
|
|
|
56
77
|
if (result.result.subtype === 'null' || !result.result.objectId) {
|
|
57
78
|
return { success: false, error: `Element not found: ${selector}` };
|
|
@@ -302,6 +323,10 @@ export function createActionabilityChecker(session) {
|
|
|
302
323
|
const element = await findElementInternal(selector);
|
|
303
324
|
if (!element.success) {
|
|
304
325
|
lastError = element.error;
|
|
326
|
+
// Immediate failures (syntax errors) should not retry
|
|
327
|
+
if (element.immediate) {
|
|
328
|
+
return { success: false, error: element.error };
|
|
329
|
+
}
|
|
305
330
|
retry++;
|
|
306
331
|
continue;
|
|
307
332
|
}
|
|
@@ -456,14 +481,9 @@ export function createActionabilityChecker(session) {
|
|
|
456
481
|
// Check if the hit element is a child of our target (also valid)
|
|
457
482
|
const isChild = await session.send('Runtime.callFunctionOn', {
|
|
458
483
|
objectId,
|
|
459
|
-
functionDeclaration: `function(
|
|
460
|
-
//
|
|
461
|
-
|
|
462
|
-
// Use elementFromPoint as a fallback check
|
|
463
|
-
const rect = this.getBoundingClientRect();
|
|
464
|
-
const centerX = rect.left + rect.width / 2;
|
|
465
|
-
const centerY = rect.top + rect.height / 2;
|
|
466
|
-
const hitEl = document.elementFromPoint(centerX, centerY);
|
|
484
|
+
functionDeclaration: `function(pt) {
|
|
485
|
+
// Use the provided point coordinates for elementFromPoint check
|
|
486
|
+
const hitEl = document.elementFromPoint(pt.x, pt.y);
|
|
467
487
|
|
|
468
488
|
if (!hitEl) return { isChild: false, coverInfo: 'no-element' };
|
|
469
489
|
|
|
@@ -481,6 +501,7 @@ export function createActionabilityChecker(session) {
|
|
|
481
501
|
|
|
482
502
|
return { isChild: false, coverInfo: desc };
|
|
483
503
|
}`,
|
|
504
|
+
arguments: [{ value: point }],
|
|
484
505
|
returnByValue: true
|
|
485
506
|
});
|
|
486
507
|
|
|
@@ -499,19 +520,17 @@ export function createActionabilityChecker(session) {
|
|
|
499
520
|
try {
|
|
500
521
|
const fallbackResult = await session.send('Runtime.callFunctionOn', {
|
|
501
522
|
objectId,
|
|
502
|
-
functionDeclaration: `function() {
|
|
503
|
-
const
|
|
504
|
-
const centerX = rect.left + rect.width / 2;
|
|
505
|
-
const centerY = rect.top + rect.height / 2;
|
|
506
|
-
const hitEl = document.elementFromPoint(centerX, centerY);
|
|
523
|
+
functionDeclaration: `function(pt) {
|
|
524
|
+
const hitEl = document.elementFromPoint(pt.x, pt.y);
|
|
507
525
|
|
|
508
|
-
if (!hitEl) return { covered: true, coverInfo: 'no-element-at-
|
|
526
|
+
if (!hitEl) return { covered: true, coverInfo: 'no-element-at-point' };
|
|
509
527
|
if (hitEl === this || this.contains(hitEl)) return { covered: false };
|
|
510
528
|
|
|
511
529
|
let desc = hitEl.tagName.toLowerCase();
|
|
512
530
|
if (hitEl.id) desc += '#' + hitEl.id;
|
|
513
531
|
return { covered: true, coverInfo: desc };
|
|
514
532
|
}`,
|
|
533
|
+
arguments: [{ value: point }],
|
|
515
534
|
returnByValue: true
|
|
516
535
|
});
|
|
517
536
|
return {
|
|
@@ -592,9 +611,7 @@ export function createActionabilityChecker(session) {
|
|
|
592
611
|
|
|
593
612
|
// Scroll the page
|
|
594
613
|
const scrollDir = direction === 'up' ? -scrollAmount : scrollAmount;
|
|
595
|
-
await session.send('Runtime.evaluate', {
|
|
596
|
-
expression: `window.scrollBy(0, ${scrollDir})`
|
|
597
|
-
});
|
|
614
|
+
await session.send('Runtime.evaluate', evalParams(`window.scrollBy(0, ${scrollDir})`));
|
|
598
615
|
|
|
599
616
|
scrollCount++;
|
|
600
617
|
await sleep(200); // Wait for content to load/render
|
|
@@ -8,12 +8,10 @@
|
|
|
8
8
|
*
|
|
9
9
|
* DEPENDENCIES:
|
|
10
10
|
* - ./actionability.js: createActionabilityChecker
|
|
11
|
-
* - ./element-validator.js: createElementValidator
|
|
12
11
|
* - ../utils.js: sleep, elementNotFoundError, getCurrentUrl, getElementAtPoint, detectNavigation, releaseObject
|
|
13
12
|
*/
|
|
14
13
|
|
|
15
14
|
import { createActionabilityChecker } from './actionability.js';
|
|
16
|
-
import { createElementValidator } from './element-validator.js';
|
|
17
15
|
import { createLazyResolver } from './LazyResolver.js';
|
|
18
16
|
import {
|
|
19
17
|
sleep,
|
|
@@ -39,8 +37,7 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
39
37
|
if (!inputEmulator) throw new Error('Input emulator is required');
|
|
40
38
|
|
|
41
39
|
const getFrameContext = elementLocator.getFrameContext || null;
|
|
42
|
-
const actionabilityChecker = createActionabilityChecker(session);
|
|
43
|
-
const elementValidator = createElementValidator(session);
|
|
40
|
+
const actionabilityChecker = createActionabilityChecker(session, { getFrameContext });
|
|
44
41
|
const lazyResolver = createLazyResolver(session, { getFrameContext });
|
|
45
42
|
|
|
46
43
|
/** Build Runtime.evaluate params with frame context when in an iframe. */
|
|
@@ -61,8 +58,8 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
61
58
|
visibleBox.y = Math.max(box.y, 0);
|
|
62
59
|
const right = Math.min(box.x + box.width, viewport.width);
|
|
63
60
|
const bottom = Math.min(box.y + box.height, viewport.height);
|
|
64
|
-
visibleBox.width = right - visibleBox.x;
|
|
65
|
-
visibleBox.height = bottom - visibleBox.y;
|
|
61
|
+
visibleBox.width = Math.max(0, right - visibleBox.x);
|
|
62
|
+
visibleBox.height = Math.max(0, bottom - visibleBox.y);
|
|
66
63
|
}
|
|
67
64
|
|
|
68
65
|
return {
|
|
@@ -402,16 +399,25 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
402
399
|
if (matchesRoleAndName(el)) return el;
|
|
403
400
|
}
|
|
404
401
|
|
|
405
|
-
// Strategy 3: Search in shadow roots
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const els = host.shadowRoot.querySelectorAll(selectorString);
|
|
402
|
+
// Strategy 3: Search in shadow roots via tree walk (avoids querySelectorAll('*'))
|
|
403
|
+
function searchShadowRoots(node) {
|
|
404
|
+
if (node.shadowRoot) {
|
|
405
|
+
const els = node.shadowRoot.querySelectorAll(selectorString);
|
|
410
406
|
for (const el of els) {
|
|
411
407
|
if (matchesRoleAndName(el)) return el;
|
|
412
408
|
}
|
|
409
|
+
const found = searchShadowRoots(node.shadowRoot);
|
|
410
|
+
if (found) return found;
|
|
413
411
|
}
|
|
412
|
+
const children = node.children || [];
|
|
413
|
+
for (const child of children) {
|
|
414
|
+
const found = searchShadowRoots(child);
|
|
415
|
+
if (found) return found;
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
414
418
|
}
|
|
419
|
+
const shadowResult = searchShadowRoots(document.body);
|
|
420
|
+
if (shadowResult) return shadowResult;
|
|
415
421
|
}
|
|
416
422
|
|
|
417
423
|
return null;
|
|
@@ -562,7 +568,7 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
562
568
|
}
|
|
563
569
|
|
|
564
570
|
async function clickAtCoordinates(x, y, opts = {}) {
|
|
565
|
-
const { debug = false, waitForNavigation = false, navigationTimeout = 100 } = opts;
|
|
571
|
+
const { debug = false, waitForNavigation = false, navigationTimeout = 100, waitAfter = false, waitAfterOptions = {} } = opts;
|
|
566
572
|
|
|
567
573
|
const urlBeforeClick = await getCurrentUrl(session);
|
|
568
574
|
|
|
@@ -579,38 +585,27 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
579
585
|
coordinates: { x, y }
|
|
580
586
|
};
|
|
581
587
|
|
|
582
|
-
|
|
583
|
-
const navResult = await detectNavigation(session, urlBeforeClick, navigationTimeout);
|
|
584
|
-
result.navigated = navResult.navigated;
|
|
585
|
-
if (navResult.newUrl) {
|
|
586
|
-
result.newUrl = navResult.newUrl;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
if (debug) {
|
|
591
|
-
result.debug = {
|
|
592
|
-
clickedAt: { x, y },
|
|
593
|
-
elementHit: elementAtPoint
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
return result;
|
|
588
|
+
return addNavigationAndDebugInfo(result, urlBeforeClick, { point: { x, y }, elementAtPoint }, { waitForNavigation, navigationTimeout, debug, waitAfter, waitAfterOptions });
|
|
598
589
|
}
|
|
599
590
|
|
|
591
|
+
let clickVerifyCounter = 0;
|
|
592
|
+
|
|
600
593
|
async function clickWithVerificationByRef(ref, x, y) {
|
|
601
594
|
// Use pointerdown for verification instead of click.
|
|
602
595
|
// React re-renders between mousedown and click, destroying the original DOM node.
|
|
603
596
|
// pointerdown fires synchronously before any re-render.
|
|
604
597
|
// Also uses document-level capture as fallback for descendant hits.
|
|
605
598
|
// LAZY RESOLUTION: Always resolve ref from metadata, never rely on cached element.
|
|
599
|
+
const verifyKey = `__clickVerify_${++clickVerifyCounter}`;
|
|
600
|
+
|
|
606
601
|
await session.send('Runtime.evaluate', frameEvalParams(`
|
|
607
602
|
(function() {
|
|
608
603
|
${LAZY_RESOLVE_SCRIPT}
|
|
609
604
|
|
|
610
605
|
const el = lazyResolveRef(${JSON.stringify(ref)});
|
|
611
606
|
if (el && el.isConnected) {
|
|
612
|
-
// Store resolved element for verification phase
|
|
613
|
-
window.
|
|
607
|
+
// Store resolved element for verification phase (unique key per click)
|
|
608
|
+
window[${JSON.stringify(verifyKey)}] = el;
|
|
614
609
|
el.__clickReceived = false;
|
|
615
610
|
el.__ptrHandler = () => { el.__clickReceived = true; };
|
|
616
611
|
el.addEventListener('pointerdown', el.__ptrHandler, { once: true });
|
|
@@ -624,16 +619,35 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
624
619
|
})()
|
|
625
620
|
`, false));
|
|
626
621
|
|
|
627
|
-
|
|
628
|
-
|
|
622
|
+
try {
|
|
623
|
+
await inputEmulator.click(x, y);
|
|
624
|
+
await sleep(50);
|
|
625
|
+
} catch (clickError) {
|
|
626
|
+
// Cleanup listeners on click failure
|
|
627
|
+
try {
|
|
628
|
+
await session.send('Runtime.evaluate', frameEvalParams(`
|
|
629
|
+
(function() {
|
|
630
|
+
const el = window[${JSON.stringify(verifyKey)}];
|
|
631
|
+
delete window[${JSON.stringify(verifyKey)}];
|
|
632
|
+
if (!el) return;
|
|
633
|
+
if (el.__ptrHandler) el.removeEventListener('pointerdown', el.__ptrHandler);
|
|
634
|
+
if (el.__docHandler) document.removeEventListener('pointerdown', el.__docHandler, { capture: true });
|
|
635
|
+
delete el.__clickReceived;
|
|
636
|
+
delete el.__ptrHandler;
|
|
637
|
+
delete el.__docHandler;
|
|
638
|
+
})()
|
|
639
|
+
`, true));
|
|
640
|
+
} catch { /* ignore cleanup errors */ }
|
|
641
|
+
throw clickError;
|
|
642
|
+
}
|
|
629
643
|
|
|
630
644
|
// Check if pointerdown was received
|
|
631
645
|
let verifyResult;
|
|
632
646
|
try {
|
|
633
647
|
verifyResult = await session.send('Runtime.evaluate', frameEvalParams(`
|
|
634
648
|
(function() {
|
|
635
|
-
const el = window.
|
|
636
|
-
delete window.
|
|
649
|
+
const el = window[${JSON.stringify(verifyKey)}];
|
|
650
|
+
delete window[${JSON.stringify(verifyKey)}];
|
|
637
651
|
if (!el) return { targetReceived: false, reason: 'element not found' };
|
|
638
652
|
if (el.__ptrHandler) el.removeEventListener('pointerdown', el.__ptrHandler);
|
|
639
653
|
if (el.__docHandler) document.removeEventListener('pointerdown', el.__docHandler, { capture: true });
|
|
@@ -657,7 +671,7 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
657
671
|
}
|
|
658
672
|
|
|
659
673
|
async function clickByRef(ref, jsClick = false, opts = {}) {
|
|
660
|
-
const { force = false, debug = false, nativeOnly = false, waitForNavigation, navigationTimeout = 100 } = opts;
|
|
674
|
+
const { force = false, debug = false, nativeOnly = false, waitForNavigation, navigationTimeout = 100, waitAfter = false, waitAfterOptions = {} } = opts;
|
|
661
675
|
|
|
662
676
|
// LAZY RESOLUTION: Always resolve ref from metadata, never rely on cached element
|
|
663
677
|
// This eliminates stale element errors entirely
|
|
@@ -666,19 +680,24 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
666
680
|
throw elementNotFoundError(`ref:${ref}`, 0);
|
|
667
681
|
}
|
|
668
682
|
|
|
669
|
-
// Get visibility info using the resolved element
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
683
|
+
// Get visibility info using the resolved element, then release the objectId
|
|
684
|
+
let visibilityResult;
|
|
685
|
+
try {
|
|
686
|
+
visibilityResult = await session.send('Runtime.callFunctionOn', {
|
|
687
|
+
objectId: resolved.objectId,
|
|
688
|
+
functionDeclaration: `function() {
|
|
689
|
+
const style = window.getComputedStyle(this);
|
|
690
|
+
const rect = this.getBoundingClientRect();
|
|
691
|
+
return {
|
|
692
|
+
isVisible: style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && rect.width > 0 && rect.height > 0,
|
|
693
|
+
box: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
|
|
694
|
+
};
|
|
695
|
+
}`,
|
|
696
|
+
returnByValue: true
|
|
697
|
+
});
|
|
698
|
+
} finally {
|
|
699
|
+
await releaseObject(session, resolved.objectId);
|
|
700
|
+
}
|
|
682
701
|
|
|
683
702
|
const refInfo = {
|
|
684
703
|
box: visibilityResult.result?.value?.box || resolved.box,
|
|
@@ -757,7 +776,8 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
757
776
|
// If element is outside viewport (e.g., inside an unscrolled container), scroll it into view first
|
|
758
777
|
// LAZY RESOLUTION: Always resolve ref from metadata for scroll
|
|
759
778
|
const box = refInfo.box;
|
|
760
|
-
|
|
779
|
+
const vp = await getViewportBounds().catch(() => null) || { width: 1280, height: 720 };
|
|
780
|
+
if (box && (box.x < 0 || box.y < 0 || box.x + box.width > vp.width || box.y + box.height > vp.height)) {
|
|
761
781
|
try {
|
|
762
782
|
await session.send('Runtime.evaluate', frameEvalParams(`
|
|
763
783
|
(function() {
|
|
@@ -769,8 +789,13 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
769
789
|
await sleep(100);
|
|
770
790
|
// Re-fetch element info after scroll using lazy resolution
|
|
771
791
|
const updatedResult = await lazyResolver.resolveRef(ref);
|
|
772
|
-
if (updatedResult
|
|
773
|
-
|
|
792
|
+
if (updatedResult) {
|
|
793
|
+
if (updatedResult.box) {
|
|
794
|
+
refInfo.box = updatedResult.box;
|
|
795
|
+
}
|
|
796
|
+
if (updatedResult.objectId) {
|
|
797
|
+
await releaseObject(session, updatedResult.objectId);
|
|
798
|
+
}
|
|
774
799
|
}
|
|
775
800
|
} catch {
|
|
776
801
|
// Scroll failed — proceed with original coordinates
|
|
@@ -877,6 +902,16 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
877
902
|
}
|
|
878
903
|
}
|
|
879
904
|
|
|
905
|
+
// Auto-wait after click
|
|
906
|
+
if (waitAfter) {
|
|
907
|
+
const changeResult = await detectContentChange({
|
|
908
|
+
timeout: waitAfterOptions.timeout || 5000,
|
|
909
|
+
stableTime: waitAfterOptions.stableTime || 500,
|
|
910
|
+
checkNavigation: true
|
|
911
|
+
});
|
|
912
|
+
result.waitResult = changeResult;
|
|
913
|
+
}
|
|
914
|
+
|
|
880
915
|
if (debug) {
|
|
881
916
|
result.debug = {
|
|
882
917
|
clickedAt: point,
|
|
@@ -1194,8 +1229,10 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
1194
1229
|
if (!scrollResult.found) {
|
|
1195
1230
|
throw elementNotFoundError(selector, scrollOptions.timeout || 30000);
|
|
1196
1231
|
}
|
|
1197
|
-
//
|
|
1198
|
-
|
|
1232
|
+
// Release the objectId from scrollUntilVisible — clickBySelector will re-find the element
|
|
1233
|
+
if (scrollResult.objectId) {
|
|
1234
|
+
await releaseObject(session, scrollResult.objectId);
|
|
1235
|
+
}
|
|
1199
1236
|
}
|
|
1200
1237
|
|
|
1201
1238
|
return clickBySelector(selector, { jsClick, nativeOnly, force, debug, waitForNavigation, navigationTimeout, waitAfter, waitAfterOptions });
|
|
@@ -205,16 +205,16 @@ export function createElementLocator(session, options = {}) {
|
|
|
205
205
|
async function findElement(selector) {
|
|
206
206
|
const element = await querySelector(selector);
|
|
207
207
|
if (!element) return null;
|
|
208
|
-
return {
|
|
208
|
+
return { objectId: element.objectId, _handle: element };
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
async function getBoundingBox(
|
|
212
|
-
if (!
|
|
211
|
+
async function getBoundingBox(objectId) {
|
|
212
|
+
if (!objectId) return null;
|
|
213
213
|
|
|
214
214
|
let result;
|
|
215
215
|
try {
|
|
216
216
|
result = await session.send('Runtime.callFunctionOn', {
|
|
217
|
-
objectId
|
|
217
|
+
objectId,
|
|
218
218
|
functionDeclaration: `function() {
|
|
219
219
|
const rect = this.getBoundingClientRect();
|
|
220
220
|
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
@@ -134,17 +134,21 @@ export function createFillExecutor(session, elementLocator, inputEmulator, ariaS
|
|
|
134
134
|
return { filled: true, ref, method: 'react' };
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
await session.send('Runtime.callFunctionOn', {
|
|
137
|
+
const boxAfterScroll = await session.send('Runtime.callFunctionOn', {
|
|
138
138
|
objectId,
|
|
139
139
|
functionDeclaration: `function() {
|
|
140
140
|
this.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
141
|
-
|
|
141
|
+
const rect = this.getBoundingClientRect();
|
|
142
|
+
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
143
|
+
}`,
|
|
144
|
+
returnByValue: true
|
|
142
145
|
});
|
|
143
146
|
|
|
144
147
|
await sleep(100);
|
|
145
148
|
|
|
146
|
-
const
|
|
147
|
-
const
|
|
149
|
+
const box = boxAfterScroll.result?.value || refInfo.box;
|
|
150
|
+
const x = box.x + box.width / 2;
|
|
151
|
+
const y = box.y + box.height / 2;
|
|
148
152
|
await inputEmulator.click(x, y);
|
|
149
153
|
|
|
150
154
|
await session.send('Runtime.callFunctionOn', {
|
|
@@ -21,9 +21,11 @@ import { sleep } from '../utils.js';
|
|
|
21
21
|
* @param {Object} session - CDP session
|
|
22
22
|
* @returns {Object} Input emulator interface
|
|
23
23
|
*/
|
|
24
|
-
export function createInputEmulator(session) {
|
|
24
|
+
export function createInputEmulator(session, options = {}) {
|
|
25
25
|
if (!session) throw new Error('CDP session is required');
|
|
26
26
|
|
|
27
|
+
const { getFrameContext } = options;
|
|
28
|
+
|
|
27
29
|
// Transaction-based mouse state
|
|
28
30
|
// Inspired by Puppeteer's CdpMouse
|
|
29
31
|
const mouseState = {
|
|
@@ -251,7 +253,7 @@ export function createInputEmulator(session) {
|
|
|
251
253
|
|
|
252
254
|
// Trigger synthetic input event for framework bindings (React, Vue, etc.)
|
|
253
255
|
if (dispatchEvents) {
|
|
254
|
-
|
|
256
|
+
const evalParams = {
|
|
255
257
|
expression: `
|
|
256
258
|
(function() {
|
|
257
259
|
const el = document.activeElement;
|
|
@@ -261,7 +263,12 @@ export function createInputEmulator(session) {
|
|
|
261
263
|
}
|
|
262
264
|
})()
|
|
263
265
|
`
|
|
264
|
-
}
|
|
266
|
+
};
|
|
267
|
+
if (getFrameContext) {
|
|
268
|
+
const contextId = getFrameContext();
|
|
269
|
+
if (contextId) evalParams.contextId = contextId;
|
|
270
|
+
}
|
|
271
|
+
await session.send('Runtime.evaluate', evalParams);
|
|
265
272
|
}
|
|
266
273
|
}
|
|
267
274
|
|
|
@@ -277,20 +284,42 @@ export function createInputEmulator(session) {
|
|
|
277
284
|
await type(text, opts);
|
|
278
285
|
}
|
|
279
286
|
|
|
287
|
+
// Mapping of Meta+key combos to macOS editing commands (sent via CDP commands param)
|
|
288
|
+
const MAC_COMMANDS = {
|
|
289
|
+
'a': ['selectAll'], 'c': ['copy'], 'v': ['paste'], 'x': ['cut'],
|
|
290
|
+
'z': ['undo'],
|
|
291
|
+
};
|
|
292
|
+
const MAC_SHIFT_COMMANDS = {
|
|
293
|
+
'z': ['redo'],
|
|
294
|
+
};
|
|
295
|
+
|
|
280
296
|
async function press(key, opts = {}) {
|
|
281
|
-
const { modifiers = {}, delay = 0 } = opts;
|
|
297
|
+
const { modifiers = {}, delay = 0, commands } = opts;
|
|
282
298
|
const keyDef = KEY_DEFINITIONS[key] || getKeyDefinition(key);
|
|
283
299
|
const modifierFlags = calculateModifiers(modifiers);
|
|
284
300
|
|
|
285
|
-
|
|
301
|
+
// Resolve commands: explicit > auto-detect for Meta combos on macOS
|
|
302
|
+
const resolvedCommands = commands
|
|
303
|
+
|| (modifiers.meta && modifiers.shift && MAC_SHIFT_COMMANDS[keyDef.key])
|
|
304
|
+
|| (modifiers.meta && MAC_COMMANDS[keyDef.key])
|
|
305
|
+
|| undefined;
|
|
306
|
+
|
|
307
|
+
const keyDown = {
|
|
286
308
|
type: 'rawKeyDown',
|
|
287
309
|
key: keyDef.key,
|
|
288
310
|
code: keyDef.code,
|
|
289
311
|
windowsVirtualKeyCode: keyDef.keyCode,
|
|
312
|
+
nativeVirtualKeyCode: keyDef.keyCode,
|
|
290
313
|
modifiers: modifierFlags
|
|
291
|
-
}
|
|
314
|
+
};
|
|
315
|
+
if (resolvedCommands) keyDown.commands = resolvedCommands;
|
|
316
|
+
|
|
317
|
+
await session.send('Input.dispatchKeyEvent', keyDown);
|
|
292
318
|
|
|
293
|
-
|
|
319
|
+
// Skip char event when command modifiers are held (shortcuts shouldn't produce text)
|
|
320
|
+
// Shift alone still produces text (e.g., Shift+a → "A")
|
|
321
|
+
const hasCommandModifier = modifiers.ctrl || modifiers.meta || modifiers.alt;
|
|
322
|
+
if (keyDef.text && !hasCommandModifier) {
|
|
294
323
|
await session.send('Input.dispatchKeyEvent', {
|
|
295
324
|
type: 'char',
|
|
296
325
|
text: keyDef.text,
|
|
@@ -306,12 +335,13 @@ export function createInputEmulator(session) {
|
|
|
306
335
|
key: keyDef.key,
|
|
307
336
|
code: keyDef.code,
|
|
308
337
|
windowsVirtualKeyCode: keyDef.keyCode,
|
|
338
|
+
nativeVirtualKeyCode: keyDef.keyCode,
|
|
309
339
|
modifiers: modifierFlags
|
|
310
340
|
});
|
|
311
341
|
}
|
|
312
342
|
|
|
313
343
|
async function selectAll() {
|
|
314
|
-
|
|
344
|
+
const evalParams = {
|
|
315
345
|
expression: `
|
|
316
346
|
(function() {
|
|
317
347
|
const el = document.activeElement;
|
|
@@ -322,7 +352,12 @@ export function createInputEmulator(session) {
|
|
|
322
352
|
}
|
|
323
353
|
})()
|
|
324
354
|
`
|
|
325
|
-
}
|
|
355
|
+
};
|
|
356
|
+
if (getFrameContext) {
|
|
357
|
+
const contextId = getFrameContext();
|
|
358
|
+
if (contextId) evalParams.contextId = contextId;
|
|
359
|
+
}
|
|
360
|
+
await session.send('Runtime.evaluate', evalParams);
|
|
326
361
|
}
|
|
327
362
|
|
|
328
363
|
async function moveMouse(x, y) {
|
|
@@ -342,6 +377,9 @@ export function createInputEmulator(session) {
|
|
|
342
377
|
y
|
|
343
378
|
});
|
|
344
379
|
|
|
380
|
+
mouseState.x = x;
|
|
381
|
+
mouseState.y = y;
|
|
382
|
+
|
|
345
383
|
if (duration > 0) {
|
|
346
384
|
await sleep(duration);
|
|
347
385
|
}
|
|
@@ -27,8 +27,11 @@ export function createReactInputFiller(session) {
|
|
|
27
27
|
const prototype = el.tagName === 'TEXTAREA'
|
|
28
28
|
? window.HTMLTextAreaElement.prototype
|
|
29
29
|
: window.HTMLInputElement.prototype;
|
|
30
|
-
const
|
|
31
|
-
|
|
30
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value');
|
|
31
|
+
if (!descriptor || !descriptor.set) {
|
|
32
|
+
return { success: false, error: 'Cannot get native value setter for ' + el.tagName };
|
|
33
|
+
}
|
|
34
|
+
descriptor.set.call(el, newValue);
|
|
32
35
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
33
36
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
34
37
|
return { success: true, value: el.value };
|
|
@@ -44,7 +47,12 @@ export function createReactInputFiller(session) {
|
|
|
44
47
|
throw new Error(`React fill failed: ${errorText}`);
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
|
|
50
|
+
const fillResult = result.result.value;
|
|
51
|
+
if (fillResult && fillResult.success === false) {
|
|
52
|
+
throw new Error(`React fill failed: ${fillResult.error}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return fillResult;
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
async function fillBySelector(selector, value) {
|
|
@@ -197,6 +197,7 @@ export function createWaitExecutor(session, elementLocator) {
|
|
|
197
197
|
async function waitForText(text, opts = {}) {
|
|
198
198
|
const { timeout = DEFAULT_TIMEOUT, caseSensitive = false } = opts;
|
|
199
199
|
const validatedTimeout = validateTimeout(timeout);
|
|
200
|
+
const waitStartTime = Date.now();
|
|
200
201
|
|
|
201
202
|
try {
|
|
202
203
|
// Use browser-side polling with MutationObserver
|
|
@@ -270,13 +271,20 @@ export function createWaitExecutor(session, elementLocator) {
|
|
|
270
271
|
|
|
271
272
|
return result.result.value;
|
|
272
273
|
} catch (error) {
|
|
273
|
-
// Fall back to original Node.js polling
|
|
274
|
+
// Fall back to original Node.js polling using remaining time from overall timeout
|
|
275
|
+
const elapsed = Date.now() - waitStartTime;
|
|
276
|
+
const remaining = validatedTimeout - elapsed;
|
|
277
|
+
if (remaining <= 0) {
|
|
278
|
+
throw timeoutError(
|
|
279
|
+
`Timeout (${validatedTimeout}ms) waiting for text: "${text}"${caseSensitive ? ' (case-sensitive)' : ''}`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
274
282
|
const startTime = Date.now();
|
|
275
283
|
const checkExpr = caseSensitive
|
|
276
284
|
? `document.body.innerText.includes(${JSON.stringify(text)})`
|
|
277
285
|
: `document.body.innerText.toLowerCase().includes(${JSON.stringify(text.toLowerCase())})`;
|
|
278
286
|
|
|
279
|
-
while (Date.now() - startTime <
|
|
287
|
+
while (Date.now() - startTime < remaining) {
|
|
280
288
|
try {
|
|
281
289
|
const result = await session.send('Runtime.evaluate', {
|
|
282
290
|
expression: checkExpr,
|
|
@@ -32,9 +32,13 @@ export function createDialogHandler(session) {
|
|
|
32
32
|
promptText = queued.promptText;
|
|
33
33
|
} else if (dialogCallback) {
|
|
34
34
|
// If custom callback is set, use it
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
try {
|
|
36
|
+
const result = dialogCallback({ type, message, defaultPrompt });
|
|
37
|
+
accept = result.accept !== false;
|
|
38
|
+
promptText = result.promptText;
|
|
39
|
+
} catch {
|
|
40
|
+
// Callback threw — fall through to default accept behavior
|
|
41
|
+
}
|
|
38
42
|
} else {
|
|
39
43
|
// Auto-accept with reasonable defaults for prompts
|
|
40
44
|
if (type === 'prompt') {
|
|
@@ -24,16 +24,23 @@ export function lcsLength(a, b) {
|
|
|
24
24
|
const m = a.length;
|
|
25
25
|
const n = b.length;
|
|
26
26
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
27
|
+
// For large arrays, sample evenly to keep DP tractable while preserving order
|
|
28
|
+
const MAX_DP = 1000;
|
|
29
|
+
if (m > MAX_DP || n > MAX_DP) {
|
|
30
|
+
const sampleEvenly = (arr, size) => {
|
|
31
|
+
if (arr.length <= size) return arr;
|
|
32
|
+
const step = arr.length / size;
|
|
33
|
+
const sampled = [];
|
|
34
|
+
for (let i = 0; i < size; i++) {
|
|
35
|
+
sampled.push(arr[Math.floor(i * step)]);
|
|
36
|
+
}
|
|
37
|
+
return sampled;
|
|
38
|
+
};
|
|
39
|
+
const sampledA = sampleEvenly(a, MAX_DP);
|
|
40
|
+
const sampledB = sampleEvenly(b, MAX_DP);
|
|
41
|
+
const lcs = lcsLength(sampledA, sampledB);
|
|
42
|
+
// Scale result back to original array size
|
|
43
|
+
return Math.round(lcs * Math.max(m, n) / MAX_DP);
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
// Standard DP solution
|