cdp-skill 1.0.14 → 1.0.16

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.
@@ -0,0 +1,634 @@
1
+ /**
2
+ * LazyResolver
3
+ * Stateless element resolution - always re-resolves refs from metadata instead of caching DOM elements.
4
+ * This eliminates stale element errors entirely.
5
+ *
6
+ * EXPORTS:
7
+ * - createLazyResolver(session, options?) → LazyResolver
8
+ * Methods: resolveRef, resolveSelector, resolveText
9
+ *
10
+ * DEPENDENCIES:
11
+ * - ../utils.js: releaseObject
12
+ */
13
+
14
+ import { releaseObject } from '../utils.js';
15
+
16
+ /**
17
+ * Create a lazy resolver for stateless element resolution
18
+ * @param {Object} session - CDP session
19
+ * @param {Object} [options] - Configuration options
20
+ * @param {Function} [options.getFrameContext] - Returns contextId when in a non-main frame
21
+ * @returns {Object} Lazy resolver interface
22
+ */
23
+ export function createLazyResolver(session, options = {}) {
24
+ if (!session) throw new Error('CDP session is required');
25
+
26
+ const getFrameContext = options.getFrameContext || null;
27
+
28
+ /**
29
+ * Build Runtime.evaluate params with frame context when in an iframe.
30
+ */
31
+ function evalParams(expression, returnByValue = false) {
32
+ const params = { expression, returnByValue };
33
+ if (getFrameContext) {
34
+ const contextId = getFrameContext();
35
+ if (contextId) params.contextId = contextId;
36
+ }
37
+ return params;
38
+ }
39
+
40
+ /**
41
+ * Resolve an element by CSS selector - always fresh resolution
42
+ * @param {string} selector - CSS selector
43
+ * @returns {Promise<{objectId: string, box: Object}|null>} Element with objectId and bounding box, or null
44
+ */
45
+ async function resolveSelector(selector) {
46
+ if (!selector || typeof selector !== 'string') return null;
47
+
48
+ const expression = `
49
+ (function() {
50
+ const el = document.querySelector(${JSON.stringify(selector)});
51
+ if (!el) return null;
52
+ const rect = el.getBoundingClientRect();
53
+ return {
54
+ found: true,
55
+ box: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
56
+ };
57
+ })()
58
+ `;
59
+
60
+ try {
61
+ // First check if element exists and get box
62
+ const checkResult = await session.send('Runtime.evaluate', evalParams(expression, true));
63
+ if (!checkResult.result.value?.found) return null;
64
+
65
+ // Now get the actual objectId
66
+ const objResult = await session.send('Runtime.evaluate',
67
+ evalParams(`document.querySelector(${JSON.stringify(selector)})`, false)
68
+ );
69
+
70
+ if (objResult.result.subtype === 'null' || !objResult.result.objectId) return null;
71
+
72
+ return {
73
+ objectId: objResult.result.objectId,
74
+ box: checkResult.result.value.box,
75
+ resolvedBy: 'selector',
76
+ selector
77
+ };
78
+ } catch (err) {
79
+ return null;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Resolve an element by role and name - always fresh resolution
85
+ * @param {string} role - ARIA role
86
+ * @param {string} name - Accessible name
87
+ * @returns {Promise<{objectId: string, box: Object}|null>} Element with objectId and bounding box, or null
88
+ */
89
+ async function resolveByRoleAndName(role, name) {
90
+ if (!role) return null;
91
+
92
+ const expression = `
93
+ (function() {
94
+ const role = ${JSON.stringify(role)};
95
+ const name = ${JSON.stringify(name || '')};
96
+
97
+ // Role to selector mappings
98
+ const ROLE_SELECTORS = {
99
+ button: ['button', 'input[type="button"]', 'input[type="submit"]', 'input[type="reset"]', '[role="button"]'],
100
+ textbox: ['input:not([type])', 'input[type="text"]', 'input[type="email"]', 'input[type="password"]', 'input[type="search"]', 'input[type="tel"]', 'input[type="url"]', 'textarea', '[role="textbox"]'],
101
+ checkbox: ['input[type="checkbox"]', '[role="checkbox"]'],
102
+ link: ['a[href]', '[role="link"]'],
103
+ heading: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', '[role="heading"]'],
104
+ listitem: ['li', '[role="listitem"]'],
105
+ option: ['option', '[role="option"]'],
106
+ combobox: ['select', '[role="combobox"]'],
107
+ radio: ['input[type="radio"]', '[role="radio"]'],
108
+ img: ['img[alt]', '[role="img"]'],
109
+ tab: ['[role="tab"]'],
110
+ menuitem: ['[role="menuitem"]'],
111
+ slider: ['input[type="range"]', '[role="slider"]'],
112
+ spinbutton: ['input[type="number"]', '[role="spinbutton"]'],
113
+ searchbox: ['input[type="search"]', '[role="searchbox"]'],
114
+ switch: ['[role="switch"]']
115
+ };
116
+
117
+ const selectors = ROLE_SELECTORS[role] || ['[role="' + role + '"]'];
118
+ const selectorString = selectors.join(', ');
119
+ const elements = document.querySelectorAll(selectorString);
120
+
121
+ function getAccessibleName(el) {
122
+ return (
123
+ el.getAttribute('aria-label') ||
124
+ el.textContent?.trim() ||
125
+ el.getAttribute('title') ||
126
+ el.getAttribute('placeholder') ||
127
+ el.value ||
128
+ ''
129
+ );
130
+ }
131
+
132
+ function isVisible(el) {
133
+ if (!el.isConnected) return false;
134
+ const style = window.getComputedStyle(el);
135
+ if (style.display === 'none' || style.visibility === 'hidden') return false;
136
+ const rect = el.getBoundingClientRect();
137
+ return rect.width > 0 && rect.height > 0;
138
+ }
139
+
140
+ // Find element matching role and name
141
+ for (const el of elements) {
142
+ if (!isVisible(el)) continue;
143
+ const elName = getAccessibleName(el);
144
+ // Match by name (case-insensitive contains)
145
+ if (name && !elName.toLowerCase().includes(name.toLowerCase())) continue;
146
+
147
+ const rect = el.getBoundingClientRect();
148
+ return {
149
+ found: true,
150
+ box: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
151
+ index: Array.from(elements).indexOf(el)
152
+ };
153
+ }
154
+
155
+ return null;
156
+ })()
157
+ `;
158
+
159
+ try {
160
+ const checkResult = await session.send('Runtime.evaluate', evalParams(expression, true));
161
+ if (!checkResult.result.value?.found) return null;
162
+
163
+ const index = checkResult.result.value.index;
164
+
165
+ // Get the actual objectId
166
+ const objExpression = `
167
+ (function() {
168
+ const role = ${JSON.stringify(role)};
169
+ const ROLE_SELECTORS = {
170
+ button: ['button', 'input[type="button"]', 'input[type="submit"]', 'input[type="reset"]', '[role="button"]'],
171
+ textbox: ['input:not([type])', 'input[type="text"]', 'input[type="email"]', 'input[type="password"]', 'input[type="search"]', 'input[type="tel"]', 'input[type="url"]', 'textarea', '[role="textbox"]'],
172
+ checkbox: ['input[type="checkbox"]', '[role="checkbox"]'],
173
+ link: ['a[href]', '[role="link"]'],
174
+ heading: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', '[role="heading"]'],
175
+ listitem: ['li', '[role="listitem"]'],
176
+ option: ['option', '[role="option"]'],
177
+ combobox: ['select', '[role="combobox"]'],
178
+ radio: ['input[type="radio"]', '[role="radio"]'],
179
+ img: ['img[alt]', '[role="img"]'],
180
+ tab: ['[role="tab"]'],
181
+ menuitem: ['[role="menuitem"]'],
182
+ slider: ['input[type="range"]', '[role="slider"]'],
183
+ spinbutton: ['input[type="number"]', '[role="spinbutton"]'],
184
+ searchbox: ['input[type="search"]', '[role="searchbox"]'],
185
+ switch: ['[role="switch"]']
186
+ };
187
+ const selectors = ROLE_SELECTORS[role] || ['[role="' + role + '"]'];
188
+ const elements = document.querySelectorAll(selectors.join(', '));
189
+ return elements[${index}] || null;
190
+ })()
191
+ `;
192
+
193
+ const objResult = await session.send('Runtime.evaluate', evalParams(objExpression, false));
194
+ if (objResult.result.subtype === 'null' || !objResult.result.objectId) return null;
195
+
196
+ return {
197
+ objectId: objResult.result.objectId,
198
+ box: checkResult.result.value.box,
199
+ resolvedBy: 'role+name',
200
+ role,
201
+ name
202
+ };
203
+ } catch (err) {
204
+ return null;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Resolve an element through shadow DOM using the host path
210
+ * @param {string[]} shadowHostPath - Array of selectors for shadow hosts
211
+ * @param {string} selector - Final selector within the shadow root
212
+ * @returns {Promise<{objectId: string, box: Object}|null>} Element with objectId and bounding box, or null
213
+ */
214
+ async function resolveThroughShadowDOM(shadowHostPath, selector) {
215
+ if (!shadowHostPath || shadowHostPath.length === 0) return null;
216
+
217
+ const expression = `
218
+ (function() {
219
+ const hostPath = ${JSON.stringify(shadowHostPath)};
220
+ const selector = ${JSON.stringify(selector)};
221
+
222
+ let root = document;
223
+ for (const hostSelector of hostPath) {
224
+ const host = root.querySelector(hostSelector);
225
+ if (!host || !host.shadowRoot) return null;
226
+ root = host.shadowRoot;
227
+ }
228
+
229
+ const el = root.querySelector(selector);
230
+ if (!el) return null;
231
+
232
+ const rect = el.getBoundingClientRect();
233
+ return {
234
+ found: true,
235
+ box: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
236
+ };
237
+ })()
238
+ `;
239
+
240
+ try {
241
+ const checkResult = await session.send('Runtime.evaluate', evalParams(expression, true));
242
+ if (!checkResult.result.value?.found) return null;
243
+
244
+ // Get objectId
245
+ const objExpression = `
246
+ (function() {
247
+ const hostPath = ${JSON.stringify(shadowHostPath)};
248
+ const selector = ${JSON.stringify(selector)};
249
+ let root = document;
250
+ for (const hostSelector of hostPath) {
251
+ const host = root.querySelector(hostSelector);
252
+ if (!host || !host.shadowRoot) return null;
253
+ root = host.shadowRoot;
254
+ }
255
+ return root.querySelector(selector);
256
+ })()
257
+ `;
258
+
259
+ const objResult = await session.send('Runtime.evaluate', evalParams(objExpression, false));
260
+ if (objResult.result.subtype === 'null' || !objResult.result.objectId) return null;
261
+
262
+ return {
263
+ objectId: objResult.result.objectId,
264
+ box: checkResult.result.value.box,
265
+ resolvedBy: 'shadow-dom',
266
+ shadowHostPath,
267
+ selector
268
+ };
269
+ } catch (err) {
270
+ return null;
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Resolve an element ref using stored metadata - ALWAYS fresh resolution
276
+ * This is the core of lazy resolution - never uses cached element references
277
+ *
278
+ * Resolution order:
279
+ * 1. Try selector from metadata
280
+ * 2. Try role+name search if selector fails
281
+ * 3. Try shadow DOM traversal if shadowHostPath exists
282
+ *
283
+ * @param {string} ref - Element ref (e.g., "s1e5")
284
+ * @returns {Promise<{objectId: string, box: Object, resolvedBy: string}|null>} Resolved element or null
285
+ */
286
+ async function resolveRef(ref) {
287
+ if (!ref || typeof ref !== 'string') return null;
288
+
289
+ // Get metadata from browser
290
+ const metaExpression = `
291
+ (function() {
292
+ const meta = window.__ariaRefMeta && window.__ariaRefMeta.get(${JSON.stringify(ref)});
293
+ if (!meta) return null;
294
+ return {
295
+ selector: meta.selector || null,
296
+ role: meta.role || null,
297
+ name: meta.name || null,
298
+ shadowHostPath: meta.shadowHostPath || null
299
+ };
300
+ })()
301
+ `;
302
+
303
+ let metadata;
304
+ try {
305
+ const metaResult = await session.send('Runtime.evaluate', evalParams(metaExpression, true));
306
+ metadata = metaResult.result.value;
307
+ } catch (err) {
308
+ return null;
309
+ }
310
+
311
+ if (!metadata) {
312
+ // No metadata stored - ref doesn't exist
313
+ return null;
314
+ }
315
+
316
+ // Strategy 1: Try selector first (most specific)
317
+ if (metadata.selector) {
318
+ // If there's a shadow host path, use shadow DOM resolution
319
+ if (metadata.shadowHostPath && metadata.shadowHostPath.length > 0) {
320
+ const shadowResult = await resolveThroughShadowDOM(metadata.shadowHostPath, metadata.selector);
321
+ if (shadowResult) {
322
+ shadowResult.ref = ref;
323
+ return shadowResult;
324
+ }
325
+ }
326
+
327
+ // Try regular selector
328
+ const selectorResult = await resolveSelector(metadata.selector);
329
+ if (selectorResult) {
330
+ selectorResult.ref = ref;
331
+ return selectorResult;
332
+ }
333
+ }
334
+
335
+ // Strategy 2: Try role+name search (works even if selector changed)
336
+ if (metadata.role) {
337
+ const roleResult = await resolveByRoleAndName(metadata.role, metadata.name);
338
+ if (roleResult) {
339
+ roleResult.ref = ref;
340
+ return roleResult;
341
+ }
342
+ }
343
+
344
+ // Strategy 3: Last resort - scan all shadow roots for role+name
345
+ if (metadata.role) {
346
+ const shadowScanResult = await scanShadowRootsForRoleAndName(metadata.role, metadata.name);
347
+ if (shadowScanResult) {
348
+ shadowScanResult.ref = ref;
349
+ return shadowScanResult;
350
+ }
351
+ }
352
+
353
+ return null;
354
+ }
355
+
356
+ /**
357
+ * Scan all shadow roots for an element matching role and name
358
+ * @param {string} role - ARIA role
359
+ * @param {string} name - Accessible name
360
+ * @returns {Promise<{objectId: string, box: Object}|null>}
361
+ */
362
+ async function scanShadowRootsForRoleAndName(role, name) {
363
+ const expression = `
364
+ (function() {
365
+ const targetRole = ${JSON.stringify(role)};
366
+ const targetName = ${JSON.stringify(name || '')};
367
+
368
+ const ROLE_SELECTORS = {
369
+ button: ['button', 'input[type="button"]', 'input[type="submit"]', 'input[type="reset"]', '[role="button"]'],
370
+ textbox: ['input:not([type])', 'input[type="text"]', 'input[type="email"]', 'input[type="password"]', 'input[type="search"]', 'input[type="tel"]', 'input[type="url"]', 'textarea', '[role="textbox"]'],
371
+ checkbox: ['input[type="checkbox"]', '[role="checkbox"]'],
372
+ link: ['a[href]', '[role="link"]'],
373
+ heading: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', '[role="heading"]'],
374
+ listitem: ['li', '[role="listitem"]'],
375
+ option: ['option', '[role="option"]'],
376
+ combobox: ['select', '[role="combobox"]'],
377
+ radio: ['input[type="radio"]', '[role="radio"]'],
378
+ tab: ['[role="tab"]'],
379
+ menuitem: ['[role="menuitem"]']
380
+ };
381
+
382
+ function getAccessibleName(el) {
383
+ return (
384
+ el.getAttribute('aria-label') ||
385
+ el.textContent?.trim() ||
386
+ el.getAttribute('title') ||
387
+ el.getAttribute('placeholder') ||
388
+ el.value ||
389
+ ''
390
+ );
391
+ }
392
+
393
+ function isVisible(el) {
394
+ if (!el.isConnected) return false;
395
+ const style = window.getComputedStyle(el);
396
+ if (style.display === 'none' || style.visibility === 'hidden') return false;
397
+ const rect = el.getBoundingClientRect();
398
+ return rect.width > 0 && rect.height > 0;
399
+ }
400
+
401
+ function searchInRoot(root, path) {
402
+ const selectors = ROLE_SELECTORS[targetRole] || ['[role="' + targetRole + '"]'];
403
+ const elements = root.querySelectorAll(selectors.join(', '));
404
+
405
+ for (const el of elements) {
406
+ if (!isVisible(el)) continue;
407
+ const elName = getAccessibleName(el);
408
+ if (targetName && !elName.toLowerCase().includes(targetName.toLowerCase())) continue;
409
+
410
+ const rect = el.getBoundingClientRect();
411
+ return {
412
+ found: true,
413
+ box: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
414
+ path: path
415
+ };
416
+ }
417
+ return null;
418
+ }
419
+
420
+ // Collect all shadow roots
421
+ const shadowHosts = document.querySelectorAll('*');
422
+ for (const host of shadowHosts) {
423
+ if (host.shadowRoot) {
424
+ const result = searchInRoot(host.shadowRoot, []);
425
+ if (result) return result;
426
+ }
427
+ }
428
+
429
+ return null;
430
+ })()
431
+ `;
432
+
433
+ try {
434
+ const checkResult = await session.send('Runtime.evaluate', evalParams(expression, true));
435
+ if (!checkResult.result.value?.found) return null;
436
+
437
+ // For simplicity, we'll re-run to get the objectId
438
+ // This is acceptable because lazy resolution is already making fresh queries
439
+ const objExpression = `
440
+ (function() {
441
+ const targetRole = ${JSON.stringify(role)};
442
+ const targetName = ${JSON.stringify(name || '')};
443
+
444
+ const ROLE_SELECTORS = {
445
+ button: ['button', 'input[type="button"]', 'input[type="submit"]', 'input[type="reset"]', '[role="button"]'],
446
+ textbox: ['input:not([type])', 'input[type="text"]', 'input[type="email"]', 'input[type="password"]', 'input[type="search"]', 'input[type="tel"]', 'input[type="url"]', 'textarea', '[role="textbox"]'],
447
+ checkbox: ['input[type="checkbox"]', '[role="checkbox"]'],
448
+ link: ['a[href]', '[role="link"]'],
449
+ heading: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', '[role="heading"]'],
450
+ listitem: ['li', '[role="listitem"]'],
451
+ option: ['option', '[role="option"]'],
452
+ combobox: ['select', '[role="combobox"]'],
453
+ radio: ['input[type="radio"]', '[role="radio"]'],
454
+ tab: ['[role="tab"]'],
455
+ menuitem: ['[role="menuitem"]']
456
+ };
457
+
458
+ function getAccessibleName(el) {
459
+ return (
460
+ el.getAttribute('aria-label') ||
461
+ el.textContent?.trim() ||
462
+ el.getAttribute('title') ||
463
+ el.getAttribute('placeholder') ||
464
+ el.value ||
465
+ ''
466
+ );
467
+ }
468
+
469
+ function isVisible(el) {
470
+ if (!el.isConnected) return false;
471
+ const style = window.getComputedStyle(el);
472
+ if (style.display === 'none' || style.visibility === 'hidden') return false;
473
+ const rect = el.getBoundingClientRect();
474
+ return rect.width > 0 && rect.height > 0;
475
+ }
476
+
477
+ const shadowHosts = document.querySelectorAll('*');
478
+ for (const host of shadowHosts) {
479
+ if (host.shadowRoot) {
480
+ const selectors = ROLE_SELECTORS[targetRole] || ['[role="' + targetRole + '"]'];
481
+ const elements = host.shadowRoot.querySelectorAll(selectors.join(', '));
482
+ for (const el of elements) {
483
+ if (!isVisible(el)) continue;
484
+ const elName = getAccessibleName(el);
485
+ if (targetName && !elName.toLowerCase().includes(targetName.toLowerCase())) continue;
486
+ return el;
487
+ }
488
+ }
489
+ }
490
+ return null;
491
+ })()
492
+ `;
493
+
494
+ const objResult = await session.send('Runtime.evaluate', evalParams(objExpression, false));
495
+ if (objResult.result.subtype === 'null' || !objResult.result.objectId) return null;
496
+
497
+ return {
498
+ objectId: objResult.result.objectId,
499
+ box: checkResult.result.value.box,
500
+ resolvedBy: 'shadow-scan',
501
+ role,
502
+ name
503
+ };
504
+ } catch (err) {
505
+ return null;
506
+ }
507
+ }
508
+
509
+ /**
510
+ * Resolve an element by text content - always fresh resolution
511
+ * @param {string} text - Text to search for
512
+ * @param {Object} [opts] - Options
513
+ * @param {boolean} [opts.exact=false] - Require exact match
514
+ * @returns {Promise<{objectId: string, box: Object}|null>} Element with objectId and bounding box, or null
515
+ */
516
+ async function resolveText(text, opts = {}) {
517
+ if (!text || typeof text !== 'string') return null;
518
+
519
+ const { exact = false } = opts;
520
+ const expression = `
521
+ (function() {
522
+ const text = ${JSON.stringify(text)};
523
+ const exact = ${exact};
524
+
525
+ function getElementText(el) {
526
+ const ariaLabel = el.getAttribute('aria-label');
527
+ if (ariaLabel) return ariaLabel;
528
+ if (el.tagName === 'INPUT') return el.value || el.placeholder || '';
529
+ return el.textContent || '';
530
+ }
531
+
532
+ function matchesText(elText) {
533
+ if (exact) return elText.trim() === text;
534
+ return elText.toLowerCase().includes(text.toLowerCase());
535
+ }
536
+
537
+ function isVisible(el) {
538
+ if (!el.isConnected) return false;
539
+ const style = window.getComputedStyle(el);
540
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
541
+ const rect = el.getBoundingClientRect();
542
+ return rect.width > 0 && rect.height > 0;
543
+ }
544
+
545
+ // Priority: buttons → links → role buttons → other clickable
546
+ const selectorGroups = [
547
+ ['button', 'input[type="button"]', 'input[type="submit"]', 'input[type="reset"]'],
548
+ ['a[href]'],
549
+ ['[role="button"]'],
550
+ ['[onclick]', '[tabindex]', 'label', 'summary']
551
+ ];
552
+
553
+ for (const selectors of selectorGroups) {
554
+ const elements = document.querySelectorAll(selectors.join(', '));
555
+ for (const el of elements) {
556
+ if (!isVisible(el)) continue;
557
+ if (matchesText(getElementText(el))) {
558
+ const rect = el.getBoundingClientRect();
559
+ return {
560
+ found: true,
561
+ box: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
562
+ selectors: selectors.join(', ')
563
+ };
564
+ }
565
+ }
566
+ }
567
+ return null;
568
+ })()
569
+ `;
570
+
571
+ try {
572
+ const checkResult = await session.send('Runtime.evaluate', evalParams(expression, true));
573
+ if (!checkResult.result.value?.found) return null;
574
+
575
+ const matchedSelectors = checkResult.result.value.selectors;
576
+
577
+ // Get objectId
578
+ const objExpression = `
579
+ (function() {
580
+ const text = ${JSON.stringify(text)};
581
+ const exact = ${exact};
582
+ const selectors = ${JSON.stringify(matchedSelectors)};
583
+
584
+ function getElementText(el) {
585
+ const ariaLabel = el.getAttribute('aria-label');
586
+ if (ariaLabel) return ariaLabel;
587
+ if (el.tagName === 'INPUT') return el.value || el.placeholder || '';
588
+ return el.textContent || '';
589
+ }
590
+
591
+ function matchesText(elText) {
592
+ if (exact) return elText.trim() === text;
593
+ return elText.toLowerCase().includes(text.toLowerCase());
594
+ }
595
+
596
+ function isVisible(el) {
597
+ if (!el.isConnected) return false;
598
+ const style = window.getComputedStyle(el);
599
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
600
+ const rect = el.getBoundingClientRect();
601
+ return rect.width > 0 && rect.height > 0;
602
+ }
603
+
604
+ const elements = document.querySelectorAll(selectors);
605
+ for (const el of elements) {
606
+ if (!isVisible(el)) continue;
607
+ if (matchesText(getElementText(el))) return el;
608
+ }
609
+ return null;
610
+ })()
611
+ `;
612
+
613
+ const objResult = await session.send('Runtime.evaluate', evalParams(objExpression, false));
614
+ if (objResult.result.subtype === 'null' || !objResult.result.objectId) return null;
615
+
616
+ return {
617
+ objectId: objResult.result.objectId,
618
+ box: checkResult.result.value.box,
619
+ resolvedBy: 'text',
620
+ text
621
+ };
622
+ } catch (err) {
623
+ return null;
624
+ }
625
+ }
626
+
627
+ return {
628
+ resolveRef,
629
+ resolveSelector,
630
+ resolveText,
631
+ resolveByRoleAndName,
632
+ resolveThroughShadowDOM
633
+ };
634
+ }