cdp-skill 1.0.8 → 1.0.14
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 +80 -35
- package/SKILL.md +151 -239
- package/install.js +1 -0
- package/package.json +1 -1
- package/src/aria/index.js +8 -0
- package/src/aria/output-processor.js +173 -0
- package/src/aria/role-query.js +1229 -0
- package/src/aria/snapshot.js +459 -0
- package/src/aria.js +237 -43
- package/src/cdp/browser.js +22 -4
- package/src/cdp-skill.js +245 -69
- package/src/dom/click-executor.js +240 -76
- package/src/dom/element-locator.js +34 -25
- package/src/dom/fill-executor.js +55 -27
- package/src/page/dialog-handler.js +119 -0
- package/src/page/page-controller.js +190 -3
- package/src/runner/context-helpers.js +33 -55
- package/src/runner/execute-dynamic.js +8 -7
- package/src/runner/execute-form.js +11 -11
- package/src/runner/execute-input.js +2 -2
- package/src/runner/execute-interaction.js +99 -120
- package/src/runner/execute-navigation.js +11 -26
- package/src/runner/execute-query.js +8 -5
- package/src/runner/step-executors.js +225 -84
- package/src/runner/step-registry.js +1064 -0
- package/src/runner/step-validator.js +16 -754
- package/src/tests/Aria.test.js +1025 -0
- package/src/tests/ContextHelpers.test.js +39 -28
- package/src/tests/ExecuteBrowser.test.js +572 -0
- package/src/tests/ExecuteDynamic.test.js +2 -457
- package/src/tests/ExecuteForm.test.js +700 -0
- package/src/tests/ExecuteInput.test.js +540 -0
- package/src/tests/ExecuteInteraction.test.js +319 -0
- package/src/tests/ExecuteQuery.test.js +820 -0
- package/src/tests/FillExecutor.test.js +2 -2
- package/src/tests/StepValidator.test.js +222 -76
- package/src/tests/TestRunner.test.js +36 -25
- package/src/tests/integration.test.js +2 -1
- package/src/types.js +9 -9
- package/src/utils/backoff.js +118 -0
- package/src/utils/cdp-helpers.js +130 -0
- package/src/utils/devices.js +140 -0
- package/src/utils/errors.js +242 -0
- package/src/utils/index.js +65 -0
- package/src/utils/temp.js +75 -0
- package/src/utils/validators.js +433 -0
- package/src/utils.js +14 -1142
|
@@ -20,7 +20,8 @@ import {
|
|
|
20
20
|
getCurrentUrl,
|
|
21
21
|
getElementAtPoint,
|
|
22
22
|
detectNavigation,
|
|
23
|
-
releaseObject
|
|
23
|
+
releaseObject,
|
|
24
|
+
isContextDestroyed
|
|
24
25
|
} from '../utils.js';
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -36,9 +37,20 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
36
37
|
if (!elementLocator) throw new Error('Element locator is required');
|
|
37
38
|
if (!inputEmulator) throw new Error('Input emulator is required');
|
|
38
39
|
|
|
40
|
+
const getFrameContext = elementLocator.getFrameContext || null;
|
|
39
41
|
const actionabilityChecker = createActionabilityChecker(session);
|
|
40
42
|
const elementValidator = createElementValidator(session);
|
|
41
43
|
|
|
44
|
+
/** Build Runtime.evaluate params with frame context when in an iframe. */
|
|
45
|
+
function frameEvalParams(expression, returnByValue = true) {
|
|
46
|
+
const params = { expression, returnByValue };
|
|
47
|
+
if (getFrameContext) {
|
|
48
|
+
const contextId = getFrameContext();
|
|
49
|
+
if (contextId) params.contextId = contextId;
|
|
50
|
+
}
|
|
51
|
+
return params;
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
function calculateVisibleCenter(box, viewport = null) {
|
|
43
55
|
let visibleBox = { ...box };
|
|
44
56
|
|
|
@@ -58,13 +70,10 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
58
70
|
}
|
|
59
71
|
|
|
60
72
|
async function getViewportBounds() {
|
|
61
|
-
const result = await session.send('Runtime.evaluate', {
|
|
62
|
-
expression: `({
|
|
73
|
+
const result = await session.send('Runtime.evaluate', frameEvalParams(`({
|
|
63
74
|
width: window.innerWidth || document.documentElement.clientWidth,
|
|
64
75
|
height: window.innerHeight || document.documentElement.clientHeight
|
|
65
|
-
})`,
|
|
66
|
-
returnByValue: true
|
|
67
|
-
});
|
|
76
|
+
})`, true));
|
|
68
77
|
return result.result.value;
|
|
69
78
|
}
|
|
70
79
|
|
|
@@ -85,8 +94,7 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
85
94
|
|
|
86
95
|
const urlBefore = checkNavigation ? await getCurrentUrl(session) : null;
|
|
87
96
|
|
|
88
|
-
const
|
|
89
|
-
expression: `
|
|
97
|
+
const detectExpr = `
|
|
90
98
|
(function() {
|
|
91
99
|
return new Promise((resolve) => {
|
|
92
100
|
const timeout = ${timeout};
|
|
@@ -142,10 +150,10 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
142
150
|
}, stableTime);
|
|
143
151
|
});
|
|
144
152
|
})()
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
153
|
+
`;
|
|
154
|
+
const detectParams = frameEvalParams(detectExpr, true);
|
|
155
|
+
detectParams.awaitPromise = true;
|
|
156
|
+
const result = await session.send('Runtime.evaluate', detectParams);
|
|
149
157
|
|
|
150
158
|
const changeResult = result.result.value || { type: 'none', changeCount: 0 };
|
|
151
159
|
|
|
@@ -245,10 +253,7 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
245
253
|
})()
|
|
246
254
|
`;
|
|
247
255
|
|
|
248
|
-
const result = await session.send('Runtime.evaluate',
|
|
249
|
-
expression,
|
|
250
|
-
returnByValue: true
|
|
251
|
-
});
|
|
256
|
+
const result = await session.send('Runtime.evaluate', frameEvalParams(expression, true));
|
|
252
257
|
|
|
253
258
|
if (result.exceptionDetails || !result.result.value) {
|
|
254
259
|
return null;
|
|
@@ -302,10 +307,24 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
302
307
|
}
|
|
303
308
|
|
|
304
309
|
async function executeJsClickOnRef(ref) {
|
|
305
|
-
const result = await session.send('Runtime.evaluate',
|
|
306
|
-
expression: `
|
|
310
|
+
const result = await session.send('Runtime.evaluate', frameEvalParams(`
|
|
307
311
|
(function() {
|
|
308
|
-
|
|
312
|
+
let el = window.__ariaRefs && window.__ariaRefs.get(${JSON.stringify(ref)});
|
|
313
|
+
|
|
314
|
+
// Re-resolve if element is missing or stale
|
|
315
|
+
if (!el || !el.isConnected) {
|
|
316
|
+
const meta = window.__ariaRefMeta && window.__ariaRefMeta.get(${JSON.stringify(ref)});
|
|
317
|
+
if (meta && meta.selector) {
|
|
318
|
+
try {
|
|
319
|
+
const candidate = document.querySelector(meta.selector);
|
|
320
|
+
if (candidate && candidate.isConnected) {
|
|
321
|
+
el = candidate;
|
|
322
|
+
if (window.__ariaRefs) window.__ariaRefs.set(${JSON.stringify(ref)}, el);
|
|
323
|
+
}
|
|
324
|
+
} catch (e) {}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
309
328
|
if (!el) {
|
|
310
329
|
return { success: false, reason: 'ref not found in __ariaRefs' };
|
|
311
330
|
}
|
|
@@ -319,9 +338,7 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
319
338
|
el.click();
|
|
320
339
|
return { success: true };
|
|
321
340
|
})()
|
|
322
|
-
`,
|
|
323
|
-
returnByValue: true
|
|
324
|
-
});
|
|
341
|
+
`, true));
|
|
325
342
|
|
|
326
343
|
const value = result.result.value || {};
|
|
327
344
|
if (!value.success) {
|
|
@@ -330,42 +347,87 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
330
347
|
}
|
|
331
348
|
|
|
332
349
|
async function clickWithVerification(x, y, targetObjectId) {
|
|
350
|
+
// Use pointerdown for verification instead of click.
|
|
351
|
+
// React (and similar frameworks) re-render elements between mousedown and click,
|
|
352
|
+
// destroying the original DOM node and its event listeners. pointerdown fires
|
|
353
|
+
// synchronously at the start of the interaction, before any re-render.
|
|
354
|
+
// Also listen on document (capture phase) as a fallback — if the click target
|
|
355
|
+
// is the element or a descendant, count it as received.
|
|
333
356
|
await session.send('Runtime.callFunctionOn', {
|
|
334
357
|
objectId: targetObjectId,
|
|
335
358
|
functionDeclaration: `function() {
|
|
336
359
|
this.__clickReceived = false;
|
|
337
|
-
|
|
338
|
-
this.
|
|
360
|
+
const self = this;
|
|
361
|
+
this.__ptrHandler = (e) => { self.__clickReceived = true; };
|
|
362
|
+
this.addEventListener('pointerdown', this.__ptrHandler, { once: true });
|
|
363
|
+
// Document-level capture fallback: catch clicks that bubble from descendants
|
|
364
|
+
this.__docHandler = (e) => {
|
|
365
|
+
if (self.contains(e.target) || e.target === self) {
|
|
366
|
+
self.__clickReceived = true;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
document.addEventListener('pointerdown', this.__docHandler, { capture: true, once: true });
|
|
339
370
|
}`
|
|
340
371
|
});
|
|
341
372
|
|
|
342
|
-
|
|
343
|
-
|
|
373
|
+
try {
|
|
374
|
+
await inputEmulator.click(x, y);
|
|
375
|
+
await sleep(50);
|
|
344
376
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
377
|
+
let verifyResult;
|
|
378
|
+
try {
|
|
379
|
+
verifyResult = await session.send('Runtime.callFunctionOn', {
|
|
380
|
+
objectId: targetObjectId,
|
|
381
|
+
functionDeclaration: `function() {
|
|
382
|
+
this.removeEventListener('pointerdown', this.__ptrHandler);
|
|
383
|
+
document.removeEventListener('pointerdown', this.__docHandler, { capture: true });
|
|
384
|
+
const received = this.__clickReceived;
|
|
385
|
+
delete this.__clickReceived;
|
|
386
|
+
delete this.__ptrHandler;
|
|
387
|
+
delete this.__docHandler;
|
|
388
|
+
return received;
|
|
389
|
+
}`,
|
|
390
|
+
returnByValue: true
|
|
391
|
+
});
|
|
392
|
+
} catch (verifyError) {
|
|
393
|
+
// Context destroyed during verification means click likely triggered navigation
|
|
394
|
+
// Treat as successful click with navigation
|
|
395
|
+
if (isContextDestroyed(null, verifyError)) {
|
|
396
|
+
return { targetReceived: true, contextDestroyed: true };
|
|
397
|
+
}
|
|
398
|
+
throw verifyError;
|
|
399
|
+
}
|
|
356
400
|
|
|
357
|
-
|
|
358
|
-
|
|
401
|
+
const targetReceived = verifyResult.result.value === true;
|
|
402
|
+
const result = { targetReceived };
|
|
359
403
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
404
|
+
// If click didn't reach target, get interceptor info
|
|
405
|
+
if (!targetReceived) {
|
|
406
|
+
const interceptor = await getInterceptorInfo(x, y, targetObjectId);
|
|
407
|
+
if (interceptor) {
|
|
408
|
+
result.interceptedBy = interceptor;
|
|
409
|
+
}
|
|
365
410
|
}
|
|
366
|
-
}
|
|
367
411
|
|
|
368
|
-
|
|
412
|
+
return result;
|
|
413
|
+
} finally {
|
|
414
|
+
// Always cleanup event listeners, even if click fails
|
|
415
|
+
try {
|
|
416
|
+
await session.send('Runtime.callFunctionOn', {
|
|
417
|
+
objectId: targetObjectId,
|
|
418
|
+
functionDeclaration: `function() {
|
|
419
|
+
this.removeEventListener('pointerdown', this.__ptrHandler);
|
|
420
|
+
document.removeEventListener('pointerdown', this.__docHandler, { capture: true });
|
|
421
|
+
delete this.__clickReceived;
|
|
422
|
+
delete this.__ptrHandler;
|
|
423
|
+
delete this.__docHandler;
|
|
424
|
+
}`,
|
|
425
|
+
returnByValue: true
|
|
426
|
+
});
|
|
427
|
+
} catch (cleanupError) {
|
|
428
|
+
// Ignore cleanup errors (element may be gone)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
369
431
|
}
|
|
370
432
|
|
|
371
433
|
async function addNavigationAndDebugInfo(result, urlBeforeClick, debugData, opts) {
|
|
@@ -436,38 +498,66 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
436
498
|
}
|
|
437
499
|
|
|
438
500
|
async function clickWithVerificationByRef(ref, x, y) {
|
|
439
|
-
//
|
|
440
|
-
|
|
441
|
-
|
|
501
|
+
// Use pointerdown for verification instead of click.
|
|
502
|
+
// React re-renders between mousedown and click, destroying the original DOM node.
|
|
503
|
+
// pointerdown fires synchronously before any re-render.
|
|
504
|
+
// Also uses document-level capture as fallback for descendant hits.
|
|
505
|
+
await session.send('Runtime.evaluate', frameEvalParams(`
|
|
442
506
|
(function() {
|
|
443
|
-
|
|
444
|
-
if (el) {
|
|
507
|
+
let el = window.__ariaRefs && window.__ariaRefs.get(${JSON.stringify(ref)});
|
|
508
|
+
if ((!el || !el.isConnected) && window.__ariaRefMeta) {
|
|
509
|
+
const meta = window.__ariaRefMeta.get(${JSON.stringify(ref)});
|
|
510
|
+
if (meta && meta.selector) {
|
|
511
|
+
try {
|
|
512
|
+
const candidate = document.querySelector(meta.selector);
|
|
513
|
+
if (candidate && candidate.isConnected) {
|
|
514
|
+
el = candidate;
|
|
515
|
+
if (window.__ariaRefs) window.__ariaRefs.set(${JSON.stringify(ref)}, el);
|
|
516
|
+
}
|
|
517
|
+
} catch (e) {}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (el && el.isConnected) {
|
|
445
521
|
el.__clickReceived = false;
|
|
446
|
-
el.
|
|
447
|
-
el.addEventListener('
|
|
522
|
+
el.__ptrHandler = () => { el.__clickReceived = true; };
|
|
523
|
+
el.addEventListener('pointerdown', el.__ptrHandler, { once: true });
|
|
524
|
+
el.__docHandler = (e) => {
|
|
525
|
+
if (el.contains(e.target) || e.target === el) {
|
|
526
|
+
el.__clickReceived = true;
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
document.addEventListener('pointerdown', el.__docHandler, { capture: true, once: true });
|
|
448
530
|
}
|
|
449
531
|
})()
|
|
450
|
-
|
|
451
|
-
});
|
|
532
|
+
`, false));
|
|
452
533
|
|
|
453
534
|
await inputEmulator.click(x, y);
|
|
454
535
|
await sleep(50);
|
|
455
536
|
|
|
456
|
-
// Check if
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
537
|
+
// Check if pointerdown was received
|
|
538
|
+
let verifyResult;
|
|
539
|
+
try {
|
|
540
|
+
verifyResult = await session.send('Runtime.evaluate', frameEvalParams(`
|
|
541
|
+
(function() {
|
|
542
|
+
const el = window.__ariaRefs && window.__ariaRefs.get(${JSON.stringify(ref)});
|
|
543
|
+
if (!el) return { targetReceived: false, reason: 'element not found' };
|
|
544
|
+
if (el.__ptrHandler) el.removeEventListener('pointerdown', el.__ptrHandler);
|
|
545
|
+
if (el.__docHandler) document.removeEventListener('pointerdown', el.__docHandler, { capture: true });
|
|
546
|
+
const received = el.__clickReceived;
|
|
547
|
+
delete el.__clickReceived;
|
|
548
|
+
delete el.__ptrHandler;
|
|
549
|
+
delete el.__docHandler;
|
|
550
|
+
return { targetReceived: received };
|
|
551
|
+
})()
|
|
552
|
+
`, true));
|
|
553
|
+
} catch (verifyError) {
|
|
554
|
+
// Context destroyed during verification means click likely triggered navigation
|
|
555
|
+
// Treat as successful click with navigation
|
|
556
|
+
if (isContextDestroyed(null, verifyError)) {
|
|
557
|
+
return { targetReceived: true, contextDestroyed: true };
|
|
558
|
+
}
|
|
559
|
+
throw verifyError;
|
|
560
|
+
}
|
|
471
561
|
|
|
472
562
|
return verifyResult.result.value || { targetReceived: false };
|
|
473
563
|
}
|
|
@@ -493,12 +583,92 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
493
583
|
}
|
|
494
584
|
|
|
495
585
|
if (!force && refInfo.isVisible === false) {
|
|
586
|
+
// Special case: hidden radio/checkbox inputs — try to click associated label
|
|
587
|
+
const labelResult = await session.send('Runtime.evaluate', frameEvalParams(`
|
|
588
|
+
(function() {
|
|
589
|
+
const ref = ${JSON.stringify(ref)};
|
|
590
|
+
const el = window.__ariaRefs && window.__ariaRefs.get(ref);
|
|
591
|
+
if (!el) return { found: false };
|
|
592
|
+
|
|
593
|
+
const tag = el.tagName.toUpperCase();
|
|
594
|
+
const type = (el.type || '').toLowerCase();
|
|
595
|
+
if (tag === 'INPUT' && (type === 'radio' || type === 'checkbox')) {
|
|
596
|
+
// Look for associated label
|
|
597
|
+
let label = null;
|
|
598
|
+
if (el.id) {
|
|
599
|
+
label = document.querySelector('label[for="' + el.id + '"]');
|
|
600
|
+
}
|
|
601
|
+
if (!label) {
|
|
602
|
+
label = el.closest('label');
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (label) {
|
|
606
|
+
const rect = label.getBoundingClientRect();
|
|
607
|
+
const style = window.getComputedStyle(label);
|
|
608
|
+
const isVisible = style.display !== 'none' &&
|
|
609
|
+
style.visibility !== 'hidden' &&
|
|
610
|
+
style.opacity !== '0' &&
|
|
611
|
+
rect.width > 0 && rect.height > 0;
|
|
612
|
+
|
|
613
|
+
if (isVisible) {
|
|
614
|
+
return {
|
|
615
|
+
found: true,
|
|
616
|
+
clickedLabel: true,
|
|
617
|
+
box: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return { found: false };
|
|
623
|
+
})()
|
|
624
|
+
`, true));
|
|
625
|
+
|
|
626
|
+
const labelInfo = labelResult.result?.value || { found: false };
|
|
627
|
+
if (labelInfo.found && labelInfo.clickedLabel) {
|
|
628
|
+
// Click the label instead
|
|
629
|
+
const labelCenter = calculateVisibleCenter(labelInfo.box);
|
|
630
|
+
const urlBefore = await getCurrentUrl(session);
|
|
631
|
+
await inputEmulator.click(labelCenter.x, labelCenter.y);
|
|
632
|
+
const urlAfter = await getCurrentUrl(session);
|
|
633
|
+
const navigated = urlAfter !== urlBefore;
|
|
634
|
+
|
|
635
|
+
return {
|
|
636
|
+
clicked: true,
|
|
637
|
+
method: 'label-proxy',
|
|
638
|
+
ref,
|
|
639
|
+
warning: `Element ref:${ref} is a hidden radio/checkbox input. Clicked associated label instead.`,
|
|
640
|
+
navigated
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// No label found or element isn't radio/checkbox — return original error
|
|
496
645
|
return {
|
|
497
646
|
clicked: false,
|
|
498
647
|
warning: `Element ref:${ref} exists but is not visible. It may be hidden or have zero dimensions.`
|
|
499
648
|
};
|
|
500
649
|
}
|
|
501
650
|
|
|
651
|
+
// If element is outside viewport (e.g., inside an unscrolled container), scroll it into view first
|
|
652
|
+
const box = refInfo.box;
|
|
653
|
+
if (box && (box.x < 0 || box.y < 0 || box.x + box.width > 1920 || box.y + box.height > 1080)) {
|
|
654
|
+
try {
|
|
655
|
+
await session.send('Runtime.evaluate', frameEvalParams(`
|
|
656
|
+
(function() {
|
|
657
|
+
const el = window.__ariaRefs && window.__ariaRefs.get(${JSON.stringify(ref)});
|
|
658
|
+
if (el) el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
|
|
659
|
+
})()
|
|
660
|
+
`, true));
|
|
661
|
+
await sleep(100);
|
|
662
|
+
// Re-fetch element info after scroll
|
|
663
|
+
const updatedInfo = await ariaSnapshot.getElementByRef(ref);
|
|
664
|
+
if (updatedInfo && updatedInfo.box) {
|
|
665
|
+
refInfo.box = updatedInfo.box;
|
|
666
|
+
}
|
|
667
|
+
} catch {
|
|
668
|
+
// Scroll failed — proceed with original coordinates
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
502
672
|
const urlBeforeClick = await getCurrentUrl(session);
|
|
503
673
|
|
|
504
674
|
const point = calculateVisibleCenter(refInfo.box);
|
|
@@ -916,12 +1086,6 @@ export function createClickExecutor(session, elementLocator, inputEmulator, aria
|
|
|
916
1086
|
if (!scrollResult.found) {
|
|
917
1087
|
throw elementNotFoundError(selector, scrollOptions.timeout || 30000);
|
|
918
1088
|
}
|
|
919
|
-
// Release the objectId from scroll search since clickBySelector will find it again
|
|
920
|
-
if (scrollResult.objectId) {
|
|
921
|
-
try {
|
|
922
|
-
await releaseObject(session, scrollResult.objectId);
|
|
923
|
-
} catch { /* ignore cleanup errors */ }
|
|
924
|
-
}
|
|
925
1089
|
// Element found, now proceed with normal click
|
|
926
1090
|
// The scrollUntilVisible already scrolled it into view, so the actionability check should pass
|
|
927
1091
|
}
|
|
@@ -30,13 +30,27 @@ const MAX_TIMEOUT = TIMEOUTS.MAX;
|
|
|
30
30
|
* @param {Object} session - CDP session
|
|
31
31
|
* @param {Object} [options] - Configuration options
|
|
32
32
|
* @param {number} [options.timeout=30000] - Default timeout in ms
|
|
33
|
+
* @param {Function} [options.getFrameContext] - Returns contextId when in a non-main frame
|
|
33
34
|
* @returns {Object} Element locator interface
|
|
34
35
|
*/
|
|
35
36
|
export function createElementLocator(session, options = {}) {
|
|
36
37
|
if (!session) throw new Error('CDP session is required');
|
|
37
38
|
|
|
39
|
+
const getFrameContext = options.getFrameContext || null;
|
|
38
40
|
let defaultTimeout = options.timeout || 30000;
|
|
39
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Build Runtime.evaluate params, injecting contextId when in an iframe.
|
|
44
|
+
*/
|
|
45
|
+
function evalParams(expression, returnByValue = false) {
|
|
46
|
+
const params = { expression, returnByValue };
|
|
47
|
+
if (getFrameContext) {
|
|
48
|
+
const contextId = getFrameContext();
|
|
49
|
+
if (contextId) params.contextId = contextId;
|
|
50
|
+
}
|
|
51
|
+
return params;
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
function validateTimeout(timeout) {
|
|
41
55
|
if (typeof timeout !== 'number' || !Number.isFinite(timeout)) return defaultTimeout;
|
|
42
56
|
if (timeout < 0) return 0;
|
|
@@ -59,10 +73,9 @@ export function createElementLocator(session, options = {}) {
|
|
|
59
73
|
|
|
60
74
|
let result;
|
|
61
75
|
try {
|
|
62
|
-
result = await session.send('Runtime.evaluate',
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
});
|
|
76
|
+
result = await session.send('Runtime.evaluate',
|
|
77
|
+
evalParams(`document.querySelector(${JSON.stringify(selector)})`, false)
|
|
78
|
+
);
|
|
66
79
|
} catch (error) {
|
|
67
80
|
throw connectionError(error.message, 'Runtime.evaluate (querySelector)');
|
|
68
81
|
}
|
|
@@ -89,10 +102,9 @@ export function createElementLocator(session, options = {}) {
|
|
|
89
102
|
|
|
90
103
|
let result;
|
|
91
104
|
try {
|
|
92
|
-
result = await session.send('Runtime.evaluate',
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
});
|
|
105
|
+
result = await session.send('Runtime.evaluate',
|
|
106
|
+
evalParams(`Array.from(document.querySelectorAll(${JSON.stringify(selector)}))`, false)
|
|
107
|
+
);
|
|
96
108
|
} catch (error) {
|
|
97
109
|
throw connectionError(error.message, 'Runtime.evaluate (querySelectorAll)');
|
|
98
110
|
}
|
|
@@ -175,10 +187,9 @@ export function createElementLocator(session, options = {}) {
|
|
|
175
187
|
while (Date.now() - startTime < validatedTimeout) {
|
|
176
188
|
let result;
|
|
177
189
|
try {
|
|
178
|
-
result = await session.send('Runtime.evaluate',
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
});
|
|
190
|
+
result = await session.send('Runtime.evaluate',
|
|
191
|
+
evalParams(checkExpr, true)
|
|
192
|
+
);
|
|
182
193
|
} catch (error) {
|
|
183
194
|
throw connectionError(error.message, 'Runtime.evaluate (waitForText)');
|
|
184
195
|
}
|
|
@@ -278,10 +289,9 @@ export function createElementLocator(session, options = {}) {
|
|
|
278
289
|
|
|
279
290
|
let result;
|
|
280
291
|
try {
|
|
281
|
-
result = await session.send('Runtime.evaluate',
|
|
282
|
-
expression,
|
|
283
|
-
|
|
284
|
-
});
|
|
292
|
+
result = await session.send('Runtime.evaluate',
|
|
293
|
+
evalParams(expression, false)
|
|
294
|
+
);
|
|
285
295
|
} catch (error) {
|
|
286
296
|
throw connectionError(error.message, 'Runtime.evaluate (queryByRole)');
|
|
287
297
|
}
|
|
@@ -403,10 +413,9 @@ export function createElementLocator(session, options = {}) {
|
|
|
403
413
|
|
|
404
414
|
let result;
|
|
405
415
|
try {
|
|
406
|
-
result = await session.send('Runtime.evaluate',
|
|
407
|
-
expression,
|
|
408
|
-
|
|
409
|
-
});
|
|
416
|
+
result = await session.send('Runtime.evaluate',
|
|
417
|
+
evalParams(expression, false)
|
|
418
|
+
);
|
|
410
419
|
} catch (error) {
|
|
411
420
|
throw connectionError(error.message, 'Runtime.evaluate (findElementByText)');
|
|
412
421
|
}
|
|
@@ -514,10 +523,9 @@ export function createElementLocator(session, options = {}) {
|
|
|
514
523
|
|
|
515
524
|
let result;
|
|
516
525
|
try {
|
|
517
|
-
result = await session.send('Runtime.evaluate',
|
|
518
|
-
expression,
|
|
519
|
-
|
|
520
|
-
});
|
|
526
|
+
result = await session.send('Runtime.evaluate',
|
|
527
|
+
evalParams(expression, false)
|
|
528
|
+
);
|
|
521
529
|
} catch (error) {
|
|
522
530
|
throw connectionError(error.message, 'Runtime.evaluate (findElementByTextWithinSelector)');
|
|
523
531
|
}
|
|
@@ -582,6 +590,7 @@ export function createElementLocator(session, options = {}) {
|
|
|
582
590
|
waitForElementByText,
|
|
583
591
|
getBoundingBox,
|
|
584
592
|
getDefaultTimeout: () => defaultTimeout,
|
|
585
|
-
setDefaultTimeout: (timeout) => { defaultTimeout = validateTimeout(timeout); }
|
|
593
|
+
setDefaultTimeout: (timeout) => { defaultTimeout = validateTimeout(timeout); },
|
|
594
|
+
get getFrameContext() { return getFrameContext; }
|
|
586
595
|
};
|
|
587
596
|
}
|