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.
Files changed (47) hide show
  1. package/README.md +80 -35
  2. package/SKILL.md +151 -239
  3. package/install.js +1 -0
  4. package/package.json +1 -1
  5. package/src/aria/index.js +8 -0
  6. package/src/aria/output-processor.js +173 -0
  7. package/src/aria/role-query.js +1229 -0
  8. package/src/aria/snapshot.js +459 -0
  9. package/src/aria.js +237 -43
  10. package/src/cdp/browser.js +22 -4
  11. package/src/cdp-skill.js +245 -69
  12. package/src/dom/click-executor.js +240 -76
  13. package/src/dom/element-locator.js +34 -25
  14. package/src/dom/fill-executor.js +55 -27
  15. package/src/page/dialog-handler.js +119 -0
  16. package/src/page/page-controller.js +190 -3
  17. package/src/runner/context-helpers.js +33 -55
  18. package/src/runner/execute-dynamic.js +8 -7
  19. package/src/runner/execute-form.js +11 -11
  20. package/src/runner/execute-input.js +2 -2
  21. package/src/runner/execute-interaction.js +99 -120
  22. package/src/runner/execute-navigation.js +11 -26
  23. package/src/runner/execute-query.js +8 -5
  24. package/src/runner/step-executors.js +225 -84
  25. package/src/runner/step-registry.js +1064 -0
  26. package/src/runner/step-validator.js +16 -754
  27. package/src/tests/Aria.test.js +1025 -0
  28. package/src/tests/ContextHelpers.test.js +39 -28
  29. package/src/tests/ExecuteBrowser.test.js +572 -0
  30. package/src/tests/ExecuteDynamic.test.js +2 -457
  31. package/src/tests/ExecuteForm.test.js +700 -0
  32. package/src/tests/ExecuteInput.test.js +540 -0
  33. package/src/tests/ExecuteInteraction.test.js +319 -0
  34. package/src/tests/ExecuteQuery.test.js +820 -0
  35. package/src/tests/FillExecutor.test.js +2 -2
  36. package/src/tests/StepValidator.test.js +222 -76
  37. package/src/tests/TestRunner.test.js +36 -25
  38. package/src/tests/integration.test.js +2 -1
  39. package/src/types.js +9 -9
  40. package/src/utils/backoff.js +118 -0
  41. package/src/utils/cdp-helpers.js +130 -0
  42. package/src/utils/devices.js +140 -0
  43. package/src/utils/errors.js +242 -0
  44. package/src/utils/index.js +65 -0
  45. package/src/utils/temp.js +75 -0
  46. package/src/utils/validators.js +433 -0
  47. 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 result = await session.send('Runtime.evaluate', {
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
- returnByValue: true,
147
- awaitPromise: true
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
- const el = window.__ariaRefs && window.__ariaRefs.get(${JSON.stringify(ref)});
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
- this.__clickHandler = () => { this.__clickReceived = true; };
338
- this.addEventListener('click', this.__clickHandler, { once: true });
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
- await inputEmulator.click(x, y);
343
- await sleep(50);
373
+ try {
374
+ await inputEmulator.click(x, y);
375
+ await sleep(50);
344
376
 
345
- const verifyResult = await session.send('Runtime.callFunctionOn', {
346
- objectId: targetObjectId,
347
- functionDeclaration: `function() {
348
- this.removeEventListener('click', this.__clickHandler);
349
- const received = this.__clickReceived;
350
- delete this.__clickReceived;
351
- delete this.__clickHandler;
352
- return received;
353
- }`,
354
- returnByValue: true
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
- const targetReceived = verifyResult.result.value === true;
358
- const result = { targetReceived };
401
+ const targetReceived = verifyResult.result.value === true;
402
+ const result = { targetReceived };
359
403
 
360
- // If click didn't reach target, get interceptor info
361
- if (!targetReceived) {
362
- const interceptor = await getInterceptorInfo(x, y, targetObjectId);
363
- if (interceptor) {
364
- result.interceptedBy = interceptor;
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
- return result;
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
- // Set up click listener on the ref element
440
- await session.send('Runtime.evaluate', {
441
- expression: `
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
- const el = window.__ariaRefs && window.__ariaRefs.get(${JSON.stringify(ref)});
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.__clickHandler = () => { el.__clickReceived = true; };
447
- el.addEventListener('click', el.__clickHandler, { once: true });
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 click was received
457
- const verifyResult = await session.send('Runtime.evaluate', {
458
- expression: `
459
- (function() {
460
- const el = window.__ariaRefs && window.__ariaRefs.get(${JSON.stringify(ref)});
461
- if (!el) return { targetReceived: false, reason: 'element not found' };
462
- el.removeEventListener('click', el.__clickHandler);
463
- const received = el.__clickReceived;
464
- delete el.__clickReceived;
465
- delete el.__clickHandler;
466
- return { targetReceived: received };
467
- })()
468
- `,
469
- returnByValue: true
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
- expression: `document.querySelector(${JSON.stringify(selector)})`,
64
- returnByValue: false
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
- expression: `Array.from(document.querySelectorAll(${JSON.stringify(selector)}))`,
94
- returnByValue: false
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
- expression: checkExpr,
180
- returnByValue: true
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
- returnByValue: false
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
- returnByValue: false
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
- returnByValue: false
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
  }