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
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
export function createAriaSnapshot(session, options = {}) {
|
|
2
|
+
const getFrameContext = options.getFrameContext || null;
|
|
3
|
+
/**
|
|
4
|
+
* Generate accessibility snapshot of the page
|
|
5
|
+
* @param {Object} options - Snapshot options
|
|
6
|
+
* @param {string} options.root - CSS selector or role selector (e.g., "role=main") for root element
|
|
7
|
+
* @param {string} options.mode - 'ai' for agent-friendly output, 'full' for complete tree
|
|
8
|
+
* @param {string} options.detail - Detail level: 'summary', 'interactive', or 'full' (default: 'full')
|
|
9
|
+
* @param {number} options.maxDepth - Maximum tree depth (default: 50)
|
|
10
|
+
* @param {number} options.maxElements - Maximum elements to include (default: unlimited)
|
|
11
|
+
* @param {boolean} options.includeText - Include static text nodes in output (default: false for ai mode)
|
|
12
|
+
* @param {boolean} options.includeFrames - Include same-origin iframe content (default: false)
|
|
13
|
+
* @param {boolean} options.viewportOnly - Only include elements visible in viewport (default: false)
|
|
14
|
+
* @param {boolean} options.pierceShadow - Traverse into open shadow DOM trees (default: false)
|
|
15
|
+
* @param {boolean} options.preserveRefs - Merge new refs into existing instead of overwriting (default: false)
|
|
16
|
+
* @param {string} options.since - Snapshot ID to check against (e.g., "s1") - returns {unchanged: true} if page hasn't changed
|
|
17
|
+
* @returns {Promise<Object>} Snapshot result with tree, yaml, refs, and snapshotId
|
|
18
|
+
*/
|
|
19
|
+
async function generate(options = {}) {
|
|
20
|
+
const { root = null, mode = 'ai', detail = 'full', maxDepth = 50, maxElements = 0, includeText = false, includeFrames = false, viewportOnly = false, pierceShadow = false, preserveRefs = false, since = null, internal = false } = options;
|
|
21
|
+
|
|
22
|
+
const evalArgs = {
|
|
23
|
+
expression: `(${SNAPSHOT_SCRIPT})(${JSON.stringify(root)}, ${JSON.stringify({ mode, detail, maxDepth, maxElements, includeText, includeFrames, viewportOnly, pierceShadow, preserveRefs, since, internal })})`,
|
|
24
|
+
returnByValue: true,
|
|
25
|
+
awaitPromise: false
|
|
26
|
+
};
|
|
27
|
+
if (getFrameContext) {
|
|
28
|
+
const contextId = getFrameContext();
|
|
29
|
+
if (contextId) evalArgs.contextId = contextId;
|
|
30
|
+
}
|
|
31
|
+
const result = await session.send('Runtime.evaluate', evalArgs);
|
|
32
|
+
|
|
33
|
+
if (result.exceptionDetails) {
|
|
34
|
+
throw new Error(`Snapshot generation failed: ${result.exceptionDetails.text}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const snapshotResult = result.result.value;
|
|
38
|
+
|
|
39
|
+
// If page unchanged (HTTP 304-like response), return early
|
|
40
|
+
if (snapshotResult.unchanged) {
|
|
41
|
+
return {
|
|
42
|
+
unchanged: true,
|
|
43
|
+
snapshotId: snapshotResult.snapshotId,
|
|
44
|
+
message: `Page unchanged since ${since}`
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Handle detail levels post-processing
|
|
49
|
+
if (detail === 'summary') {
|
|
50
|
+
const summaryResult = generateSummaryView(snapshotResult);
|
|
51
|
+
summaryResult.snapshotId = snapshotResult.snapshotId;
|
|
52
|
+
return summaryResult;
|
|
53
|
+
} else if (detail === 'interactive') {
|
|
54
|
+
const interactiveResult = generateInteractiveView(snapshotResult);
|
|
55
|
+
interactiveResult.snapshotId = snapshotResult.snapshotId;
|
|
56
|
+
return interactiveResult;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return snapshotResult;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generate a summary view of the snapshot
|
|
64
|
+
* Shows landmarks and interactive element counts
|
|
65
|
+
*/
|
|
66
|
+
function generateSummaryView(snapshot) {
|
|
67
|
+
if (!snapshot || !snapshot.tree) {
|
|
68
|
+
return { ...snapshot, detail: 'summary' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const landmarks = [];
|
|
72
|
+
let totalElements = 0;
|
|
73
|
+
let interactiveElements = 0;
|
|
74
|
+
let viewportElements = 0;
|
|
75
|
+
|
|
76
|
+
const LANDMARK_ROLES = ['main', 'navigation', 'banner', 'contentinfo', 'complementary', 'search', 'form', 'region'];
|
|
77
|
+
const INTERACTIVE_ROLES = ['button', 'checkbox', 'combobox', 'link', 'listbox', 'menuitem', 'option', 'radio', 'searchbox', 'slider', 'spinbutton', 'switch', 'tab', 'textbox', 'treeitem'];
|
|
78
|
+
|
|
79
|
+
function walkTree(node, depth = 0) {
|
|
80
|
+
if (!node) return;
|
|
81
|
+
totalElements++;
|
|
82
|
+
|
|
83
|
+
const role = node.role || '';
|
|
84
|
+
const isInteractive = INTERACTIVE_ROLES.includes(role);
|
|
85
|
+
const isLandmark = LANDMARK_ROLES.includes(role);
|
|
86
|
+
|
|
87
|
+
if (isInteractive) {
|
|
88
|
+
interactiveElements++;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Count all semantic (non-generic, non-staticText) nodes as viewport elements
|
|
92
|
+
// since they passed isVisible() checks during tree construction
|
|
93
|
+
if (role && role !== 'generic' && role !== 'staticText') {
|
|
94
|
+
viewportElements++;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (isLandmark) {
|
|
98
|
+
const landmark = {
|
|
99
|
+
role,
|
|
100
|
+
name: node.name || null,
|
|
101
|
+
interactiveCount: countInteractive(node),
|
|
102
|
+
children: getChildRoles(node)
|
|
103
|
+
};
|
|
104
|
+
landmarks.push(landmark);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (node.children && Array.isArray(node.children)) {
|
|
108
|
+
for (const child of node.children) {
|
|
109
|
+
walkTree(child, depth + 1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function countInteractive(node) {
|
|
115
|
+
let count = 0;
|
|
116
|
+
function walk(n) {
|
|
117
|
+
if (!n) return;
|
|
118
|
+
if (INTERACTIVE_ROLES.includes(n.role)) count++;
|
|
119
|
+
if (n.children) n.children.forEach(walk);
|
|
120
|
+
}
|
|
121
|
+
if (node.children) node.children.forEach(walk);
|
|
122
|
+
return count;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getChildRoles(node) {
|
|
126
|
+
const roles = [];
|
|
127
|
+
if (node.children) {
|
|
128
|
+
for (const child of node.children) {
|
|
129
|
+
if (child.role && !['staticText', 'generic'].includes(child.role)) {
|
|
130
|
+
roles.push(child.role);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return roles.slice(0, 5); // Limit to 5
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
walkTree(snapshot.tree);
|
|
138
|
+
|
|
139
|
+
// Generate summary YAML
|
|
140
|
+
const yamlLines = [];
|
|
141
|
+
yamlLines.push('# Snapshot Summary');
|
|
142
|
+
yamlLines.push(`# Total elements: ${totalElements}`);
|
|
143
|
+
yamlLines.push(`# Interactive elements: ${interactiveElements}`);
|
|
144
|
+
yamlLines.push(`# Viewport elements: ${viewportElements}`);
|
|
145
|
+
yamlLines.push('');
|
|
146
|
+
yamlLines.push('landmarks:');
|
|
147
|
+
for (const lm of landmarks) {
|
|
148
|
+
yamlLines.push(` - role: ${lm.role}`);
|
|
149
|
+
if (lm.name) yamlLines.push(` name: "${lm.name}"`);
|
|
150
|
+
yamlLines.push(` interactiveCount: ${lm.interactiveCount}`);
|
|
151
|
+
if (lm.children.length > 0) {
|
|
152
|
+
yamlLines.push(` children: [${lm.children.join(', ')}]`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
yaml: yamlLines.join('\n'),
|
|
158
|
+
refs: snapshot.refs,
|
|
159
|
+
detail: 'summary',
|
|
160
|
+
stats: {
|
|
161
|
+
totalElements,
|
|
162
|
+
interactiveElements,
|
|
163
|
+
viewportElements,
|
|
164
|
+
landmarkCount: landmarks.length
|
|
165
|
+
},
|
|
166
|
+
landmarks
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Generate an interactive-only view of the snapshot
|
|
172
|
+
* Shows only actionable elements with their paths
|
|
173
|
+
*/
|
|
174
|
+
function generateInteractiveView(snapshot) {
|
|
175
|
+
if (!snapshot || !snapshot.tree) {
|
|
176
|
+
return { ...snapshot, detail: 'interactive' };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const INTERACTIVE_ROLES = ['button', 'checkbox', 'combobox', 'link', 'listbox', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'searchbox', 'slider', 'spinbutton', 'switch', 'tab', 'textbox', 'treeitem'];
|
|
180
|
+
const elements = [];
|
|
181
|
+
|
|
182
|
+
function walkTree(node, path = []) {
|
|
183
|
+
if (!node) return;
|
|
184
|
+
|
|
185
|
+
const role = node.role || '';
|
|
186
|
+
const isInteractive = INTERACTIVE_ROLES.includes(role);
|
|
187
|
+
|
|
188
|
+
if (isInteractive) {
|
|
189
|
+
const el = {
|
|
190
|
+
role,
|
|
191
|
+
name: node.name || '',
|
|
192
|
+
ref: node.ref || null,
|
|
193
|
+
path: path.join(' > ')
|
|
194
|
+
};
|
|
195
|
+
if (node.checked !== undefined) el.checked = node.checked;
|
|
196
|
+
if (node.disabled) el.disabled = true;
|
|
197
|
+
if (node.expanded !== undefined) el.expanded = node.expanded;
|
|
198
|
+
if (node.value) el.value = node.value;
|
|
199
|
+
elements.push(el);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (node.children && Array.isArray(node.children)) {
|
|
203
|
+
const newPath = role && !['staticText', 'generic'].includes(role) ? [...path, role] : path;
|
|
204
|
+
for (const child of node.children) {
|
|
205
|
+
walkTree(child, newPath);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
walkTree(snapshot.tree);
|
|
211
|
+
|
|
212
|
+
// Generate compact YAML for interactive elements
|
|
213
|
+
const yamlLines = elements.map(el => {
|
|
214
|
+
let line = `- ${el.role} "${el.name}"`;
|
|
215
|
+
if (el.ref) line += ` [ref=${el.ref}]`;
|
|
216
|
+
if (el.checked !== undefined) line += el.checked ? ' [checked]' : '';
|
|
217
|
+
if (el.disabled) line += ' [disabled]';
|
|
218
|
+
if (el.expanded !== undefined) line += el.expanded ? ' [expanded]' : ' [collapsed]';
|
|
219
|
+
line += `: path=${el.path}`;
|
|
220
|
+
return line;
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
yaml: yamlLines.join('\n'),
|
|
225
|
+
refs: snapshot.refs,
|
|
226
|
+
detail: 'interactive',
|
|
227
|
+
stats: {
|
|
228
|
+
interactiveCount: elements.length
|
|
229
|
+
},
|
|
230
|
+
elements
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get element by ref with automatic re-resolution fallback.
|
|
236
|
+
* When the original element is stale (removed from DOM), attempts to find
|
|
237
|
+
* a replacement element using stored metadata (CSS selector + role/name verification).
|
|
238
|
+
* @param {string} ref - Element reference (e.g., 's1e1')
|
|
239
|
+
* @returns {Promise<Object>} Element info with selector, box, and connection status
|
|
240
|
+
*/
|
|
241
|
+
async function getElementByRef(ref) {
|
|
242
|
+
const evalArgs = {
|
|
243
|
+
expression: `(function() {
|
|
244
|
+
const ref = ${JSON.stringify(ref)};
|
|
245
|
+
const refsMap = window.__ariaRefs;
|
|
246
|
+
const metaMap = window.__ariaRefMeta;
|
|
247
|
+
let el = refsMap && refsMap.get(ref);
|
|
248
|
+
|
|
249
|
+
// Helper to build result from a live element
|
|
250
|
+
function buildResult(element, reResolved) {
|
|
251
|
+
const style = window.getComputedStyle(element);
|
|
252
|
+
const isVisible = style.display !== 'none' &&
|
|
253
|
+
style.visibility !== 'hidden' &&
|
|
254
|
+
style.opacity !== '0';
|
|
255
|
+
const rect = element.getBoundingClientRect();
|
|
256
|
+
const info = {
|
|
257
|
+
selector: element.id ? '#' + element.id : null,
|
|
258
|
+
box: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
259
|
+
isConnected: true,
|
|
260
|
+
isVisible: isVisible && rect.width > 0 && rect.height > 0
|
|
261
|
+
};
|
|
262
|
+
if (reResolved) info.reResolved = true;
|
|
263
|
+
return info;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Helper to compute accessible name for verification
|
|
267
|
+
function getAccessibleName(element) {
|
|
268
|
+
return (
|
|
269
|
+
element.getAttribute('aria-label') ||
|
|
270
|
+
element.getAttribute('title') ||
|
|
271
|
+
element.getAttribute('placeholder') ||
|
|
272
|
+
(element.textContent ? element.textContent.replace(/\\s+/g, ' ').trim().substring(0, 200) : '') ||
|
|
273
|
+
''
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Helper to get ARIA role
|
|
278
|
+
function getRole(element) {
|
|
279
|
+
const explicit = element.getAttribute('role');
|
|
280
|
+
if (explicit) return explicit.split(/\\s+/)[0];
|
|
281
|
+
const tag = element.tagName.toUpperCase();
|
|
282
|
+
if (tag === 'INPUT') {
|
|
283
|
+
const type = (element.type || 'text').toLowerCase();
|
|
284
|
+
const inputTypeMap = {
|
|
285
|
+
'checkbox': 'checkbox', 'radio': 'radio',
|
|
286
|
+
'range': 'slider', 'number': 'spinbutton',
|
|
287
|
+
'search': 'searchbox'
|
|
288
|
+
};
|
|
289
|
+
return inputTypeMap[type] || 'textbox';
|
|
290
|
+
}
|
|
291
|
+
const implicitMap = {
|
|
292
|
+
'A': 'link', 'BUTTON': 'button',
|
|
293
|
+
'SELECT': 'combobox', 'TEXTAREA': 'textbox',
|
|
294
|
+
'H1': 'heading', 'H2': 'heading', 'H3': 'heading',
|
|
295
|
+
'H4': 'heading', 'H5': 'heading', 'H6': 'heading',
|
|
296
|
+
'NAV': 'navigation', 'MAIN': 'main', 'LI': 'listitem',
|
|
297
|
+
'OPTION': 'option', 'IMG': 'img', 'DIALOG': 'dialog'
|
|
298
|
+
};
|
|
299
|
+
return implicitMap[tag] || null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 1. Element exists and is connected - return as-is (fast path)
|
|
303
|
+
if (el && el.isConnected) {
|
|
304
|
+
return buildResult(el, false);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Helper to check if candidate matches role+name
|
|
308
|
+
function matchesRoleAndName(candidate, meta) {
|
|
309
|
+
if (!candidate || !candidate.isConnected) return false;
|
|
310
|
+
const candidateRole = getRole(candidate);
|
|
311
|
+
const roleMatch = !meta.role || candidateRole === meta.role;
|
|
312
|
+
if (!roleMatch) return false;
|
|
313
|
+
if (!meta.name) return true;
|
|
314
|
+
const candidateName = getAccessibleName(candidate);
|
|
315
|
+
return candidateName.toLowerCase().includes(meta.name.toLowerCase().substring(0, 100));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Helper to resolve a CSS selector through a chain of shadow hosts
|
|
319
|
+
function queryShadow(shadowHostPath, selector) {
|
|
320
|
+
let root = document;
|
|
321
|
+
for (const hostSel of shadowHostPath) {
|
|
322
|
+
try {
|
|
323
|
+
const host = root.querySelector(hostSel);
|
|
324
|
+
if (!host || !host.shadowRoot) return null;
|
|
325
|
+
root = host.shadowRoot;
|
|
326
|
+
} catch (e) { return null; }
|
|
327
|
+
}
|
|
328
|
+
try { return root.querySelector(selector); } catch (e) { return null; }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Helper to querySelectorAll through shadow hosts
|
|
332
|
+
function queryShadowAll(shadowHostPath, selector) {
|
|
333
|
+
let root = document;
|
|
334
|
+
for (const hostSel of shadowHostPath) {
|
|
335
|
+
try {
|
|
336
|
+
const host = root.querySelector(hostSel);
|
|
337
|
+
if (!host || !host.shadowRoot) return [];
|
|
338
|
+
root = host.shadowRoot;
|
|
339
|
+
} catch (e) { return []; }
|
|
340
|
+
}
|
|
341
|
+
try { return Array.from(root.querySelectorAll(selector)); } catch (e) { return []; }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Helper to collect all shadow roots in the document for broad search
|
|
345
|
+
function collectShadowRoots(node, roots) {
|
|
346
|
+
if (node.shadowRoot) {
|
|
347
|
+
roots.push(node.shadowRoot);
|
|
348
|
+
collectShadowRoots(node.shadowRoot, roots);
|
|
349
|
+
}
|
|
350
|
+
const children = node.children || node.childNodes || [];
|
|
351
|
+
for (const child of children) {
|
|
352
|
+
if (child.nodeType === 1) collectShadowRoots(child, roots);
|
|
353
|
+
}
|
|
354
|
+
return roots;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// 2. Element is null or stale - attempt re-resolution via metadata
|
|
358
|
+
if (metaMap) {
|
|
359
|
+
const meta = metaMap.get(ref);
|
|
360
|
+
if (meta) {
|
|
361
|
+
const hasShadowPath = meta.shadowHostPath && meta.shadowHostPath.length > 0;
|
|
362
|
+
|
|
363
|
+
// 2a. Try stored CSS selector first (fastest)
|
|
364
|
+
if (meta.selector) {
|
|
365
|
+
try {
|
|
366
|
+
const candidate = hasShadowPath
|
|
367
|
+
? queryShadow(meta.shadowHostPath, meta.selector)
|
|
368
|
+
: document.querySelector(meta.selector);
|
|
369
|
+
if (matchesRoleAndName(candidate, meta)) {
|
|
370
|
+
if (refsMap) refsMap.set(ref, candidate);
|
|
371
|
+
return buildResult(candidate, true);
|
|
372
|
+
}
|
|
373
|
+
} catch (e) {
|
|
374
|
+
// querySelector can throw on invalid selectors - fall through
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 2b. Broader search: find by role + name
|
|
379
|
+
if (meta.role) {
|
|
380
|
+
const roleSelectors = {
|
|
381
|
+
'link': 'a[href]',
|
|
382
|
+
'button': 'button,[role="button"]',
|
|
383
|
+
'heading': 'h1,h2,h3,h4,h5,h6,[role="heading"]',
|
|
384
|
+
'textbox': 'input:not([type]),input[type="text"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],textarea,[role="textbox"]',
|
|
385
|
+
'checkbox': 'input[type="checkbox"],[role="checkbox"]',
|
|
386
|
+
'radio': 'input[type="radio"],[role="radio"]',
|
|
387
|
+
'combobox': 'select,[role="combobox"],[role="listbox"]',
|
|
388
|
+
'img': 'img,[role="img"]',
|
|
389
|
+
'listitem': 'li,[role="listitem"]',
|
|
390
|
+
'tab': '[role="tab"]',
|
|
391
|
+
'menuitem': '[role="menuitem"]'
|
|
392
|
+
};
|
|
393
|
+
const sel = roleSelectors[meta.role] || '[role="' + meta.role + '"]';
|
|
394
|
+
|
|
395
|
+
// Search in known shadow path first, then light DOM
|
|
396
|
+
if (hasShadowPath) {
|
|
397
|
+
try {
|
|
398
|
+
const candidates = queryShadowAll(meta.shadowHostPath, sel);
|
|
399
|
+
for (const candidate of candidates) {
|
|
400
|
+
if (matchesRoleAndName(candidate, meta)) {
|
|
401
|
+
if (refsMap) refsMap.set(ref, candidate);
|
|
402
|
+
return buildResult(candidate, true);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
} catch (e) {}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Light DOM search
|
|
409
|
+
try {
|
|
410
|
+
const candidates = document.querySelectorAll(sel);
|
|
411
|
+
for (const candidate of candidates) {
|
|
412
|
+
if (matchesRoleAndName(candidate, meta)) {
|
|
413
|
+
if (refsMap) refsMap.set(ref, candidate);
|
|
414
|
+
return buildResult(candidate, true);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
} catch (e) {}
|
|
418
|
+
|
|
419
|
+
// 2c. Last resort: search ALL shadow roots in the document
|
|
420
|
+
if (!hasShadowPath) {
|
|
421
|
+
try {
|
|
422
|
+
const shadowRoots = collectShadowRoots(document.body, []);
|
|
423
|
+
for (const sr of shadowRoots) {
|
|
424
|
+
const candidates = sr.querySelectorAll(sel);
|
|
425
|
+
for (const candidate of candidates) {
|
|
426
|
+
if (matchesRoleAndName(candidate, meta)) {
|
|
427
|
+
if (refsMap) refsMap.set(ref, candidate);
|
|
428
|
+
return buildResult(candidate, true);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
} catch (e) {}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// 3. All fallbacks failed
|
|
439
|
+
if (el && !el.isConnected) {
|
|
440
|
+
return { stale: true, ref: ref };
|
|
441
|
+
}
|
|
442
|
+
return null;
|
|
443
|
+
})()`,
|
|
444
|
+
returnByValue: true
|
|
445
|
+
};
|
|
446
|
+
if (getFrameContext) {
|
|
447
|
+
const contextId = getFrameContext();
|
|
448
|
+
if (contextId) evalArgs.contextId = contextId;
|
|
449
|
+
}
|
|
450
|
+
const result = await session.send('Runtime.evaluate', evalArgs);
|
|
451
|
+
|
|
452
|
+
return result.result.value;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
generate,
|
|
457
|
+
getElementByRef
|
|
458
|
+
};
|
|
459
|
+
}
|