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
package/src/aria.js
CHANGED
|
@@ -184,7 +184,8 @@ export function createQueryOutputProcessor(session) {
|
|
|
184
184
|
* @param {Object} elementLocator - Element locator instance
|
|
185
185
|
* @returns {Object} Role query executor interface
|
|
186
186
|
*/
|
|
187
|
-
export function createRoleQueryExecutor(session, elementLocator) {
|
|
187
|
+
export function createRoleQueryExecutor(session, elementLocator, options = {}) {
|
|
188
|
+
const getFrameContext = options.getFrameContext || null;
|
|
188
189
|
const outputProcessor = createQueryOutputProcessor(session);
|
|
189
190
|
|
|
190
191
|
async function releaseObject(objectId) {
|
|
@@ -327,10 +328,12 @@ export function createRoleQueryExecutor(session, elementLocator) {
|
|
|
327
328
|
|
|
328
329
|
let result;
|
|
329
330
|
try {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
331
|
+
const evalArgs = { expression, returnByValue: false };
|
|
332
|
+
if (getFrameContext) {
|
|
333
|
+
const contextId = getFrameContext();
|
|
334
|
+
if (contextId) evalArgs.contextId = contextId;
|
|
335
|
+
}
|
|
336
|
+
result = await session.send('Runtime.evaluate', evalArgs);
|
|
334
337
|
} catch (error) {
|
|
335
338
|
throw new Error(`Role query error: ${error.message}`);
|
|
336
339
|
}
|
|
@@ -353,7 +356,7 @@ export function createRoleQueryExecutor(session, elementLocator) {
|
|
|
353
356
|
throw new Error(`Role query error: ${error.message}`);
|
|
354
357
|
}
|
|
355
358
|
|
|
356
|
-
const { createElementHandle } = await import('./dom.js');
|
|
359
|
+
const { createElementHandle } = await import('./dom/element-handle.js');
|
|
357
360
|
const elements = props.result
|
|
358
361
|
.filter(p => /^\d+$/.test(p.name) && p.value && p.value.objectId)
|
|
359
362
|
.map(p => createElementHandle(session, p.value.objectId, {
|
|
@@ -740,10 +743,18 @@ const SNAPSHOT_SCRIPT = `
|
|
|
740
743
|
|
|
741
744
|
// Text content for buttons, links, etc.
|
|
742
745
|
const role = getAriaRole(el);
|
|
743
|
-
if (['button', 'link', 'menuitem', '
|
|
746
|
+
if (['button', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option',
|
|
747
|
+
'tab', 'treeitem', 'heading', 'gridcell', 'listitem', 'columnheader',
|
|
748
|
+
'rowheader', 'cell', 'switch'].includes(role)) {
|
|
744
749
|
return normalizeWhitespace(el.textContent);
|
|
745
750
|
}
|
|
746
751
|
|
|
752
|
+
// Short-text fallback for any remaining role with empty name
|
|
753
|
+
if (role) {
|
|
754
|
+
const fallbackText = normalizeWhitespace(el.textContent);
|
|
755
|
+
if (fallbackText && fallbackText.length <= 80) return fallbackText;
|
|
756
|
+
}
|
|
757
|
+
|
|
747
758
|
return '';
|
|
748
759
|
}
|
|
749
760
|
|
|
@@ -918,6 +929,14 @@ const SNAPSHOT_SCRIPT = `
|
|
|
918
929
|
// Check if element already has a ref in current snapshot
|
|
919
930
|
if (elementRefs.has(el)) return elementRefs.get(el);
|
|
920
931
|
|
|
932
|
+
// Build metadata with shadow host path for shadow DOM elements
|
|
933
|
+
function buildMeta(element, r, n) {
|
|
934
|
+
const meta = { selector: generateSelector(element), role: r || '', name: n || '' };
|
|
935
|
+
const shadowPath = getShadowHostPath(element);
|
|
936
|
+
if (shadowPath.length > 0) meta.shadowHostPath = shadowPath;
|
|
937
|
+
return meta;
|
|
938
|
+
}
|
|
939
|
+
|
|
921
940
|
// Check if element already has a ref from a previous snapshot
|
|
922
941
|
// This ensures the same element always gets the same ref
|
|
923
942
|
if (window.__ariaRefs) {
|
|
@@ -926,7 +945,7 @@ const SNAPSHOT_SCRIPT = `
|
|
|
926
945
|
elementRefs.set(el, existingRef);
|
|
927
946
|
refElements.set(existingRef, el);
|
|
928
947
|
// Update metadata in case it changed
|
|
929
|
-
refMeta.set(existingRef,
|
|
948
|
+
refMeta.set(existingRef, buildMeta(el, role, name));
|
|
930
949
|
return existingRef;
|
|
931
950
|
}
|
|
932
951
|
}
|
|
@@ -938,7 +957,7 @@ const SNAPSHOT_SCRIPT = `
|
|
|
938
957
|
elementRefs.set(el, ref);
|
|
939
958
|
refElements.set(ref, el);
|
|
940
959
|
// Store metadata for re-resolution fallback
|
|
941
|
-
refMeta.set(ref,
|
|
960
|
+
refMeta.set(ref, buildMeta(el, role, name));
|
|
942
961
|
return ref;
|
|
943
962
|
}
|
|
944
963
|
|
|
@@ -1220,10 +1239,21 @@ const SNAPSHOT_SCRIPT = `
|
|
|
1220
1239
|
return document.querySelector(selector);
|
|
1221
1240
|
}
|
|
1222
1241
|
|
|
1223
|
-
// Main execution
|
|
1224
|
-
|
|
1242
|
+
// Main execution - auto-scope to <main> when no root specified (reduces footer/boilerplate noise)
|
|
1243
|
+
let autoScoped = false;
|
|
1244
|
+
let root;
|
|
1245
|
+
if (!rootSelector) {
|
|
1246
|
+
const mainEl = document.querySelector('main, [role="main"]');
|
|
1247
|
+
if (mainEl) {
|
|
1248
|
+
root = mainEl;
|
|
1249
|
+
autoScoped = true;
|
|
1250
|
+
} else {
|
|
1251
|
+
root = document.body;
|
|
1252
|
+
}
|
|
1253
|
+
} else {
|
|
1254
|
+
root = resolveRoot(rootSelector);
|
|
1255
|
+
}
|
|
1225
1256
|
if (!root) {
|
|
1226
|
-
// Provide helpful error message based on selector type
|
|
1227
1257
|
const roleMatch = rootSelector && rootSelector.match(/^role=(.+)$/i);
|
|
1228
1258
|
if (roleMatch) {
|
|
1229
1259
|
return { error: 'Root element not found for role: ' + roleMatch[1] + '. Use CSS selector (e.g., "main", "#container") or check that an element with this role exists.' };
|
|
@@ -1242,22 +1272,39 @@ const SNAPSHOT_SCRIPT = `
|
|
|
1242
1272
|
refs[ref] = generateSelector(el);
|
|
1243
1273
|
}
|
|
1244
1274
|
|
|
1245
|
-
|
|
1275
|
+
// Build the shadow host path for an element (empty array if not in shadow DOM)
|
|
1276
|
+
function getShadowHostPath(el) {
|
|
1277
|
+
const hosts = [];
|
|
1278
|
+
let node = el;
|
|
1279
|
+
while (node) {
|
|
1280
|
+
const root = node.getRootNode();
|
|
1281
|
+
if (root instanceof ShadowRoot) {
|
|
1282
|
+
hosts.unshift(generateSelectorForElement(root.host));
|
|
1283
|
+
node = root.host;
|
|
1284
|
+
} else {
|
|
1285
|
+
break;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
return hosts;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// Generate a CSS selector for a single element (used by both generateSelector and shadow path)
|
|
1292
|
+
function generateSelectorForElement(el) {
|
|
1246
1293
|
if (el.id) return '#' + CSS.escape(el.id);
|
|
1247
1294
|
|
|
1248
|
-
// Try unique attributes
|
|
1249
1295
|
for (const attr of ['data-testid', 'data-test-id', 'data-cy', 'name']) {
|
|
1250
1296
|
if (el.hasAttribute(attr)) {
|
|
1251
1297
|
const value = el.getAttribute(attr);
|
|
1252
1298
|
const selector = '[' + attr + '=' + JSON.stringify(value) + ']';
|
|
1253
|
-
if (document.querySelectorAll(selector).length === 1) return selector;
|
|
1299
|
+
try { if (document.querySelectorAll(selector).length === 1) return selector; } catch(e) {}
|
|
1254
1300
|
}
|
|
1255
1301
|
}
|
|
1256
1302
|
|
|
1257
|
-
// Build path
|
|
1303
|
+
// Build path from element up to its root (document or shadow root)
|
|
1258
1304
|
const path = [];
|
|
1259
1305
|
let current = el;
|
|
1260
|
-
|
|
1306
|
+
const rootNode = el.getRootNode();
|
|
1307
|
+
while (current && current !== document.body && current !== rootNode) {
|
|
1261
1308
|
let selector = current.tagName.toLowerCase();
|
|
1262
1309
|
if (current.id) {
|
|
1263
1310
|
selector = '#' + CSS.escape(current.id);
|
|
@@ -1279,7 +1326,38 @@ const SNAPSHOT_SCRIPT = `
|
|
|
1279
1326
|
return path.join(' > ');
|
|
1280
1327
|
}
|
|
1281
1328
|
|
|
1282
|
-
|
|
1329
|
+
function generateSelector(el) {
|
|
1330
|
+
return generateSelectorForElement(el);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// Build landmark header when auto-scoped to main (shows what else is on the page)
|
|
1334
|
+
let landmarkHeader = '';
|
|
1335
|
+
if (autoScoped) {
|
|
1336
|
+
const LM_QUERIES = [
|
|
1337
|
+
{ sel: 'nav, [role="navigation"]', role: 'navigation' },
|
|
1338
|
+
{ sel: 'header, [role="banner"]', role: 'banner' },
|
|
1339
|
+
{ sel: 'footer, [role="contentinfo"]', role: 'contentinfo' },
|
|
1340
|
+
{ sel: 'aside, [role="complementary"]', role: 'complementary' },
|
|
1341
|
+
{ sel: '[role="search"]', role: 'search' }
|
|
1342
|
+
];
|
|
1343
|
+
const found = [];
|
|
1344
|
+
for (const { sel, role } of LM_QUERIES) {
|
|
1345
|
+
try {
|
|
1346
|
+
const count = document.querySelectorAll(sel).length;
|
|
1347
|
+
if (count > 0) {
|
|
1348
|
+
const label = document.querySelector(sel).getAttribute('aria-label');
|
|
1349
|
+
found.push(label ? role + ' "' + label + '"' : role);
|
|
1350
|
+
}
|
|
1351
|
+
} catch (e) {}
|
|
1352
|
+
}
|
|
1353
|
+
if (found.length > 0) {
|
|
1354
|
+
landmarkHeader = '# Auto-scoped to main content. Other landmarks: ' + found.join(', ') + '\\n# Use {root: "body"} for full page\\n';
|
|
1355
|
+
} else {
|
|
1356
|
+
landmarkHeader = '# Auto-scoped to main content. Use {root: "body"} for full page\\n';
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
const yaml = landmarkHeader + (tree.children ? tree.children.map(c => renderYaml(c, '')).join('\\n') : renderYaml(tree, ''));
|
|
1283
1361
|
|
|
1284
1362
|
// Store refs globally for later use (e.g., click by ref)
|
|
1285
1363
|
// When preserveRefs is true, merge new refs into existing map instead of overwriting
|
|
@@ -1306,13 +1384,15 @@ const SNAPSHOT_SCRIPT = `
|
|
|
1306
1384
|
// Store page hash for change detection
|
|
1307
1385
|
window.__ariaSnapshotHash = computePageHash();
|
|
1308
1386
|
|
|
1309
|
-
|
|
1387
|
+
const snapshotResult = {
|
|
1310
1388
|
tree,
|
|
1311
1389
|
yaml,
|
|
1312
1390
|
refs,
|
|
1313
1391
|
truncated: limitReached,
|
|
1314
1392
|
snapshotId: 's' + currentSnapshotId
|
|
1315
1393
|
};
|
|
1394
|
+
if (autoScoped) snapshotResult.autoScoped = true;
|
|
1395
|
+
return snapshotResult;
|
|
1316
1396
|
})
|
|
1317
1397
|
`;
|
|
1318
1398
|
|
|
@@ -1321,7 +1401,8 @@ const SNAPSHOT_SCRIPT = `
|
|
|
1321
1401
|
* @param {Object} session - CDP session
|
|
1322
1402
|
* @returns {Object} ARIA snapshot interface
|
|
1323
1403
|
*/
|
|
1324
|
-
export function createAriaSnapshot(session) {
|
|
1404
|
+
export function createAriaSnapshot(session, options = {}) {
|
|
1405
|
+
const getFrameContext = options.getFrameContext || null;
|
|
1325
1406
|
/**
|
|
1326
1407
|
* Generate accessibility snapshot of the page
|
|
1327
1408
|
* @param {Object} options - Snapshot options
|
|
@@ -1341,11 +1422,16 @@ export function createAriaSnapshot(session) {
|
|
|
1341
1422
|
async function generate(options = {}) {
|
|
1342
1423
|
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;
|
|
1343
1424
|
|
|
1344
|
-
const
|
|
1425
|
+
const evalArgs = {
|
|
1345
1426
|
expression: `(${SNAPSHOT_SCRIPT})(${JSON.stringify(root)}, ${JSON.stringify({ mode, detail, maxDepth, maxElements, includeText, includeFrames, viewportOnly, pierceShadow, preserveRefs, since, internal })})`,
|
|
1346
1427
|
returnByValue: true,
|
|
1347
1428
|
awaitPromise: false
|
|
1348
|
-
}
|
|
1429
|
+
};
|
|
1430
|
+
if (getFrameContext) {
|
|
1431
|
+
const contextId = getFrameContext();
|
|
1432
|
+
if (contextId) evalArgs.contextId = contextId;
|
|
1433
|
+
}
|
|
1434
|
+
const result = await session.send('Runtime.evaluate', evalArgs);
|
|
1349
1435
|
|
|
1350
1436
|
if (result.exceptionDetails) {
|
|
1351
1437
|
throw new Error(`Snapshot generation failed: ${result.exceptionDetails.text}`);
|
|
@@ -1405,7 +1491,9 @@ export function createAriaSnapshot(session) {
|
|
|
1405
1491
|
interactiveElements++;
|
|
1406
1492
|
}
|
|
1407
1493
|
|
|
1408
|
-
|
|
1494
|
+
// Count all semantic (non-generic, non-staticText) nodes as viewport elements
|
|
1495
|
+
// since they passed isVisible() checks during tree construction
|
|
1496
|
+
if (role && role !== 'generic' && role !== 'staticText') {
|
|
1409
1497
|
viewportElements++;
|
|
1410
1498
|
}
|
|
1411
1499
|
|
|
@@ -1554,7 +1642,7 @@ export function createAriaSnapshot(session) {
|
|
|
1554
1642
|
* @returns {Promise<Object>} Element info with selector, box, and connection status
|
|
1555
1643
|
*/
|
|
1556
1644
|
async function getElementByRef(ref) {
|
|
1557
|
-
const
|
|
1645
|
+
const evalArgs = {
|
|
1558
1646
|
expression: `(function() {
|
|
1559
1647
|
const ref = ${JSON.stringify(ref)};
|
|
1560
1648
|
const refsMap = window.__ariaRefs;
|
|
@@ -1619,32 +1707,133 @@ export function createAriaSnapshot(session) {
|
|
|
1619
1707
|
return buildResult(el, false);
|
|
1620
1708
|
}
|
|
1621
1709
|
|
|
1710
|
+
// Helper to check if candidate matches role+name
|
|
1711
|
+
function matchesRoleAndName(candidate, meta) {
|
|
1712
|
+
if (!candidate || !candidate.isConnected) return false;
|
|
1713
|
+
const candidateRole = getRole(candidate);
|
|
1714
|
+
const roleMatch = !meta.role || candidateRole === meta.role;
|
|
1715
|
+
if (!roleMatch) return false;
|
|
1716
|
+
if (!meta.name) return true;
|
|
1717
|
+
const candidateName = getAccessibleName(candidate);
|
|
1718
|
+
return candidateName.toLowerCase().includes(meta.name.toLowerCase().substring(0, 100));
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// Helper to resolve a CSS selector through a chain of shadow hosts
|
|
1722
|
+
function queryShadow(shadowHostPath, selector) {
|
|
1723
|
+
let root = document;
|
|
1724
|
+
for (const hostSel of shadowHostPath) {
|
|
1725
|
+
try {
|
|
1726
|
+
const host = root.querySelector(hostSel);
|
|
1727
|
+
if (!host || !host.shadowRoot) return null;
|
|
1728
|
+
root = host.shadowRoot;
|
|
1729
|
+
} catch (e) { return null; }
|
|
1730
|
+
}
|
|
1731
|
+
try { return root.querySelector(selector); } catch (e) { return null; }
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// Helper to querySelectorAll through shadow hosts
|
|
1735
|
+
function queryShadowAll(shadowHostPath, selector) {
|
|
1736
|
+
let root = document;
|
|
1737
|
+
for (const hostSel of shadowHostPath) {
|
|
1738
|
+
try {
|
|
1739
|
+
const host = root.querySelector(hostSel);
|
|
1740
|
+
if (!host || !host.shadowRoot) return [];
|
|
1741
|
+
root = host.shadowRoot;
|
|
1742
|
+
} catch (e) { return []; }
|
|
1743
|
+
}
|
|
1744
|
+
try { return Array.from(root.querySelectorAll(selector)); } catch (e) { return []; }
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// Helper to collect all shadow roots in the document for broad search
|
|
1748
|
+
function collectShadowRoots(node, roots) {
|
|
1749
|
+
if (node.shadowRoot) {
|
|
1750
|
+
roots.push(node.shadowRoot);
|
|
1751
|
+
collectShadowRoots(node.shadowRoot, roots);
|
|
1752
|
+
}
|
|
1753
|
+
const children = node.children || node.childNodes || [];
|
|
1754
|
+
for (const child of children) {
|
|
1755
|
+
if (child.nodeType === 1) collectShadowRoots(child, roots);
|
|
1756
|
+
}
|
|
1757
|
+
return roots;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1622
1760
|
// 2. Element is null or stale - attempt re-resolution via metadata
|
|
1623
1761
|
if (metaMap) {
|
|
1624
1762
|
const meta = metaMap.get(ref);
|
|
1625
|
-
if (meta
|
|
1626
|
-
|
|
1627
|
-
const candidate = document.querySelector(meta.selector);
|
|
1628
|
-
if (candidate && candidate.isConnected) {
|
|
1629
|
-
// Verify role matches
|
|
1630
|
-
const candidateRole = getRole(candidate);
|
|
1631
|
-
const roleMatch = !meta.role || candidateRole === meta.role;
|
|
1632
|
-
|
|
1633
|
-
// Verify name matches (loose: contains check, case-insensitive)
|
|
1634
|
-
let nameMatch = true;
|
|
1635
|
-
if (meta.name) {
|
|
1636
|
-
const candidateName = getAccessibleName(candidate);
|
|
1637
|
-
nameMatch = candidateName.toLowerCase().includes(meta.name.toLowerCase().substring(0, 100));
|
|
1638
|
-
}
|
|
1763
|
+
if (meta) {
|
|
1764
|
+
const hasShadowPath = meta.shadowHostPath && meta.shadowHostPath.length > 0;
|
|
1639
1765
|
|
|
1640
|
-
|
|
1641
|
-
|
|
1766
|
+
// 2a. Try stored CSS selector first (fastest)
|
|
1767
|
+
if (meta.selector) {
|
|
1768
|
+
try {
|
|
1769
|
+
const candidate = hasShadowPath
|
|
1770
|
+
? queryShadow(meta.shadowHostPath, meta.selector)
|
|
1771
|
+
: document.querySelector(meta.selector);
|
|
1772
|
+
if (matchesRoleAndName(candidate, meta)) {
|
|
1642
1773
|
if (refsMap) refsMap.set(ref, candidate);
|
|
1643
1774
|
return buildResult(candidate, true);
|
|
1644
1775
|
}
|
|
1776
|
+
} catch (e) {
|
|
1777
|
+
// querySelector can throw on invalid selectors - fall through
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
// 2b. Broader search: find by role + name
|
|
1782
|
+
if (meta.role) {
|
|
1783
|
+
const roleSelectors = {
|
|
1784
|
+
'link': 'a[href]',
|
|
1785
|
+
'button': 'button,[role="button"]',
|
|
1786
|
+
'heading': 'h1,h2,h3,h4,h5,h6,[role="heading"]',
|
|
1787
|
+
'textbox': 'input:not([type]),input[type="text"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],textarea,[role="textbox"]',
|
|
1788
|
+
'checkbox': 'input[type="checkbox"],[role="checkbox"]',
|
|
1789
|
+
'radio': 'input[type="radio"],[role="radio"]',
|
|
1790
|
+
'combobox': 'select,[role="combobox"],[role="listbox"]',
|
|
1791
|
+
'img': 'img,[role="img"]',
|
|
1792
|
+
'listitem': 'li,[role="listitem"]',
|
|
1793
|
+
'tab': '[role="tab"]',
|
|
1794
|
+
'menuitem': '[role="menuitem"]'
|
|
1795
|
+
};
|
|
1796
|
+
const sel = roleSelectors[meta.role] || '[role="' + meta.role + '"]';
|
|
1797
|
+
|
|
1798
|
+
// Search in known shadow path first, then light DOM
|
|
1799
|
+
if (hasShadowPath) {
|
|
1800
|
+
try {
|
|
1801
|
+
const candidates = queryShadowAll(meta.shadowHostPath, sel);
|
|
1802
|
+
for (const candidate of candidates) {
|
|
1803
|
+
if (matchesRoleAndName(candidate, meta)) {
|
|
1804
|
+
if (refsMap) refsMap.set(ref, candidate);
|
|
1805
|
+
return buildResult(candidate, true);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
} catch (e) {}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// Light DOM search
|
|
1812
|
+
try {
|
|
1813
|
+
const candidates = document.querySelectorAll(sel);
|
|
1814
|
+
for (const candidate of candidates) {
|
|
1815
|
+
if (matchesRoleAndName(candidate, meta)) {
|
|
1816
|
+
if (refsMap) refsMap.set(ref, candidate);
|
|
1817
|
+
return buildResult(candidate, true);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
} catch (e) {}
|
|
1821
|
+
|
|
1822
|
+
// 2c. Last resort: search ALL shadow roots in the document
|
|
1823
|
+
if (!hasShadowPath) {
|
|
1824
|
+
try {
|
|
1825
|
+
const shadowRoots = collectShadowRoots(document.body, []);
|
|
1826
|
+
for (const sr of shadowRoots) {
|
|
1827
|
+
const candidates = sr.querySelectorAll(sel);
|
|
1828
|
+
for (const candidate of candidates) {
|
|
1829
|
+
if (matchesRoleAndName(candidate, meta)) {
|
|
1830
|
+
if (refsMap) refsMap.set(ref, candidate);
|
|
1831
|
+
return buildResult(candidate, true);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
} catch (e) {}
|
|
1645
1836
|
}
|
|
1646
|
-
} catch (e) {
|
|
1647
|
-
// querySelector can throw on invalid selectors - fall through
|
|
1648
1837
|
}
|
|
1649
1838
|
}
|
|
1650
1839
|
}
|
|
@@ -1656,7 +1845,12 @@ export function createAriaSnapshot(session) {
|
|
|
1656
1845
|
return null;
|
|
1657
1846
|
})()`,
|
|
1658
1847
|
returnByValue: true
|
|
1659
|
-
}
|
|
1848
|
+
};
|
|
1849
|
+
if (getFrameContext) {
|
|
1850
|
+
const contextId = getFrameContext();
|
|
1851
|
+
if (contextId) evalArgs.contextId = contextId;
|
|
1852
|
+
}
|
|
1853
|
+
const result = await session.send('Runtime.evaluate', evalArgs);
|
|
1660
1854
|
|
|
1661
1855
|
return result.result.value;
|
|
1662
1856
|
}
|
package/src/cdp/browser.js
CHANGED
|
@@ -196,6 +196,8 @@ export async function launchChrome(options = {}) {
|
|
|
196
196
|
|
|
197
197
|
if (headless) {
|
|
198
198
|
args.push('--headless=new');
|
|
199
|
+
args.push('--disable-dev-shm-usage');
|
|
200
|
+
args.push('--disable-software-rasterizer');
|
|
199
201
|
}
|
|
200
202
|
|
|
201
203
|
// Chrome requires --user-data-dir for remote debugging (as of Chrome 129+)
|
|
@@ -218,10 +220,13 @@ export async function launchChrome(options = {}) {
|
|
|
218
220
|
stdio: ['ignore', 'ignore', 'pipe'] // capture stderr
|
|
219
221
|
});
|
|
220
222
|
|
|
221
|
-
// Collect stderr output for error reporting
|
|
223
|
+
// Collect stderr output for error reporting (capped at 50KB)
|
|
224
|
+
const MAX_STDERR_BYTES = 50 * 1024;
|
|
222
225
|
let stderrOutput = '';
|
|
223
226
|
chromeProcess.stderr.on('data', (data) => {
|
|
224
|
-
stderrOutput
|
|
227
|
+
if (stderrOutput.length < MAX_STDERR_BYTES) {
|
|
228
|
+
stderrOutput += data.toString().substring(0, MAX_STDERR_BYTES - stderrOutput.length);
|
|
229
|
+
}
|
|
225
230
|
});
|
|
226
231
|
|
|
227
232
|
// Don't let this process keep Node alive
|
|
@@ -418,6 +423,11 @@ export function createBrowser(options = {}) {
|
|
|
418
423
|
while (targetLocks.has(targetId)) {
|
|
419
424
|
await targetLocks.get(targetId);
|
|
420
425
|
}
|
|
426
|
+
// Atomic check-and-set: verify lock is still free after await
|
|
427
|
+
if (targetLocks.has(targetId)) {
|
|
428
|
+
// Another acquirer won the race, recursively retry
|
|
429
|
+
return acquireLock(targetId);
|
|
430
|
+
}
|
|
421
431
|
// Create a new lock
|
|
422
432
|
let releaseFn;
|
|
423
433
|
const lockPromise = new Promise(resolve => {
|
|
@@ -440,9 +450,15 @@ export function createBrowser(options = {}) {
|
|
|
440
450
|
}
|
|
441
451
|
}
|
|
442
452
|
|
|
443
|
-
async function doConnect() {
|
|
453
|
+
async function doConnect(abortSignal) {
|
|
444
454
|
const version = await discovery.getVersion();
|
|
445
455
|
connection = createConnection(version.webSocketDebuggerUrl);
|
|
456
|
+
|
|
457
|
+
// Check if aborted before connecting
|
|
458
|
+
if (abortSignal?.aborted) {
|
|
459
|
+
throw new Error('Connection aborted');
|
|
460
|
+
}
|
|
461
|
+
|
|
446
462
|
await connection.connect();
|
|
447
463
|
|
|
448
464
|
targetManager = createTargetManager(connection);
|
|
@@ -459,9 +475,11 @@ export function createBrowser(options = {}) {
|
|
|
459
475
|
async function connect() {
|
|
460
476
|
if (connected) return;
|
|
461
477
|
|
|
462
|
-
const
|
|
478
|
+
const controller = new AbortController();
|
|
479
|
+
const connectPromise = doConnect(controller.signal);
|
|
463
480
|
const timeoutPromise = new Promise((_, reject) => {
|
|
464
481
|
setTimeout(() => {
|
|
482
|
+
controller.abort();
|
|
465
483
|
reject(timeoutError(`Connection to Chrome timed out after ${connectTimeout}ms`));
|
|
466
484
|
}, connectTimeout);
|
|
467
485
|
});
|