browser-pilot 0.0.12 → 0.0.13

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createCDPClient
3
- } from "./chunk-4MBSALQL.mjs";
3
+ } from "./chunk-HP6R3W32.mjs";
4
4
  import {
5
5
  createProvider
6
6
  } from "./chunk-BRAFQUMG.mjs";
@@ -11,7 +11,7 @@ import {
11
11
  TimeoutError,
12
12
  ensureActionable,
13
13
  generateHints
14
- } from "./chunk-NLIARNEE.mjs";
14
+ } from "./chunk-A2ZRAEO3.mjs";
15
15
 
16
16
  // src/audio/encoding.ts
17
17
  function bufferToBase64(data) {
@@ -1507,6 +1507,285 @@ var RequestInterceptor = class {
1507
1507
  }
1508
1508
  };
1509
1509
 
1510
+ // src/browser/special-selectors.ts
1511
+ function stripQuotes(value) {
1512
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1513
+ return value.slice(1, -1);
1514
+ }
1515
+ return value;
1516
+ }
1517
+ function parseTextSelector(selector) {
1518
+ if (!selector.startsWith("text:")) return null;
1519
+ let raw = selector.slice(5).trim();
1520
+ let exact = false;
1521
+ if (raw.startsWith("=")) {
1522
+ exact = true;
1523
+ raw = raw.slice(1).trim();
1524
+ }
1525
+ const query = stripQuotes(raw);
1526
+ if (!query) return null;
1527
+ return { query, exact };
1528
+ }
1529
+ function parseRoleSelector(selector) {
1530
+ if (!selector.startsWith("role:")) return null;
1531
+ const body = selector.slice(5);
1532
+ const separator = body.indexOf(":");
1533
+ const role = (separator === -1 ? body : body.slice(0, separator)).trim().toLowerCase();
1534
+ const name = separator === -1 ? void 0 : stripQuotes(body.slice(separator + 1).trim());
1535
+ if (!role) return null;
1536
+ return { role, name: name || void 0 };
1537
+ }
1538
+ var SPECIAL_SELECTOR_SCRIPT = `
1539
+ function bpNormalizeSpace(value) {
1540
+ return String(value == null ? '' : value).replace(/\\s+/g, ' ').trim();
1541
+ }
1542
+
1543
+ function bpCollectElements(root) {
1544
+ var elements = [];
1545
+
1546
+ function visit(node) {
1547
+ if (!node || typeof node.querySelectorAll !== 'function') return;
1548
+ var matches = node.querySelectorAll('*');
1549
+ for (var i = 0; i < matches.length; i++) {
1550
+ var el = matches[i];
1551
+ elements.push(el);
1552
+ if (el.shadowRoot) {
1553
+ visit(el.shadowRoot);
1554
+ }
1555
+ }
1556
+ }
1557
+
1558
+ if (root && root.documentElement) {
1559
+ elements.push(root.documentElement);
1560
+ }
1561
+
1562
+ visit(root);
1563
+ return elements;
1564
+ }
1565
+
1566
+ function bpIsVisible(el) {
1567
+ if (!el) return false;
1568
+ var style = getComputedStyle(el);
1569
+ if (style.display === 'none') return false;
1570
+ if (style.visibility === 'hidden') return false;
1571
+ if (parseFloat(style.opacity || '1') === 0) return false;
1572
+ var rect = el.getBoundingClientRect();
1573
+ return rect.width > 0 && rect.height > 0;
1574
+ }
1575
+
1576
+ function bpInferRole(el) {
1577
+ if (!el || !el.tagName) return '';
1578
+
1579
+ var explicitRole = bpNormalizeSpace(el.getAttribute && el.getAttribute('role'));
1580
+ if (explicitRole) return explicitRole.toLowerCase();
1581
+
1582
+ var tag = el.tagName.toLowerCase();
1583
+ if (tag === 'button') return 'button';
1584
+ if (tag === 'a' && el.hasAttribute('href')) return 'link';
1585
+ if (tag === 'textarea') return 'textbox';
1586
+ if (tag === 'select') return el.multiple ? 'listbox' : 'combobox';
1587
+ if (tag === 'option') return 'option';
1588
+ if (tag === 'summary') return 'button';
1589
+
1590
+ if (tag === 'input') {
1591
+ var type = (el.type || 'text').toLowerCase();
1592
+ if (type === 'checkbox') return 'checkbox';
1593
+ if (type === 'radio') return 'radio';
1594
+ if (type === 'search') return 'searchbox';
1595
+ if (type === 'number') return 'spinbutton';
1596
+ if (type === 'button' || type === 'submit' || type === 'reset' || type === 'image') {
1597
+ return 'button';
1598
+ }
1599
+ return 'textbox';
1600
+ }
1601
+
1602
+ return '';
1603
+ }
1604
+
1605
+ function bpTextFromIdRefs(refs) {
1606
+ if (!refs) return '';
1607
+ var ids = refs.split(/\\s+/).filter(Boolean);
1608
+ var parts = [];
1609
+ for (var i = 0; i < ids.length; i++) {
1610
+ var node = document.getElementById(ids[i]);
1611
+ if (!node) continue;
1612
+ var text = bpNormalizeSpace(node.innerText || node.textContent || '');
1613
+ if (text) parts.push(text);
1614
+ }
1615
+ return bpNormalizeSpace(parts.join(' '));
1616
+ }
1617
+
1618
+ function bpAccessibleName(el) {
1619
+ if (!el) return '';
1620
+
1621
+ var labelledBy = bpTextFromIdRefs(el.getAttribute && el.getAttribute('aria-labelledby'));
1622
+ if (labelledBy) return labelledBy;
1623
+
1624
+ var ariaLabel = bpNormalizeSpace(el.getAttribute && el.getAttribute('aria-label'));
1625
+ if (ariaLabel) return ariaLabel;
1626
+
1627
+ if (el.labels && el.labels.length) {
1628
+ var labels = [];
1629
+ for (var i = 0; i < el.labels.length; i++) {
1630
+ var labelText = bpNormalizeSpace(el.labels[i].innerText || el.labels[i].textContent || '');
1631
+ if (labelText) labels.push(labelText);
1632
+ }
1633
+ if (labels.length) return bpNormalizeSpace(labels.join(' '));
1634
+ }
1635
+
1636
+ if (el.id) {
1637
+ var fallbackLabel = document.querySelector('label[for="' + el.id.replace(/"/g, '\\\\"') + '"]');
1638
+ if (fallbackLabel) {
1639
+ var fallbackText = bpNormalizeSpace(
1640
+ fallbackLabel.innerText || fallbackLabel.textContent || ''
1641
+ );
1642
+ if (fallbackText) return fallbackText;
1643
+ }
1644
+ }
1645
+
1646
+ var type = (el.type || '').toLowerCase();
1647
+ if (
1648
+ el.tagName === 'INPUT' &&
1649
+ (type === 'submit' || type === 'button' || type === 'reset' || type === 'image')
1650
+ ) {
1651
+ var inputValue = bpNormalizeSpace(el.value || el.getAttribute('value'));
1652
+ if (inputValue) return inputValue;
1653
+ }
1654
+
1655
+ var alt = bpNormalizeSpace(el.getAttribute && el.getAttribute('alt'));
1656
+ if (alt) return alt;
1657
+
1658
+ var text = bpNormalizeSpace(el.innerText || el.textContent || '');
1659
+ if (text) return text;
1660
+
1661
+ var placeholder = bpNormalizeSpace(el.getAttribute && el.getAttribute('placeholder'));
1662
+ if (placeholder) return placeholder;
1663
+
1664
+ var title = bpNormalizeSpace(el.getAttribute && el.getAttribute('title'));
1665
+ if (title) return title;
1666
+
1667
+ var value = bpNormalizeSpace(el.value);
1668
+ if (value) return value;
1669
+
1670
+ return bpNormalizeSpace(el.name || el.id || '');
1671
+ }
1672
+
1673
+ function bpIsInteractive(role, el) {
1674
+ if (
1675
+ role === 'button' ||
1676
+ role === 'link' ||
1677
+ role === 'textbox' ||
1678
+ role === 'checkbox' ||
1679
+ role === 'radio' ||
1680
+ role === 'combobox' ||
1681
+ role === 'listbox' ||
1682
+ role === 'option' ||
1683
+ role === 'searchbox' ||
1684
+ role === 'spinbutton' ||
1685
+ role === 'switch' ||
1686
+ role === 'tab'
1687
+ ) {
1688
+ return true;
1689
+ }
1690
+
1691
+ if (!el || !el.tagName) return false;
1692
+ var tag = el.tagName.toLowerCase();
1693
+ return tag === 'button' || tag === 'a' || tag === 'input' || tag === 'select' || tag === 'textarea';
1694
+ }
1695
+
1696
+ function bpFindByText(query, exact, includeHidden) {
1697
+ var needle = bpNormalizeSpace(query).toLowerCase();
1698
+ if (!needle) return null;
1699
+
1700
+ var best = null;
1701
+ var bestScore = -1;
1702
+ var elements = bpCollectElements(document);
1703
+
1704
+ for (var i = 0; i < elements.length; i++) {
1705
+ var el = elements[i];
1706
+ if (!includeHidden && !bpIsVisible(el)) continue;
1707
+
1708
+ var text = bpAccessibleName(el);
1709
+ if (!text) continue;
1710
+
1711
+ var haystack = text.toLowerCase();
1712
+ var matched = exact ? haystack === needle : haystack.includes(needle);
1713
+ if (!matched) continue;
1714
+
1715
+ var role = bpInferRole(el);
1716
+ var score = 0;
1717
+ if (bpIsInteractive(role, el)) score += 100;
1718
+ if (haystack === needle) score += 50;
1719
+ if (role === 'button' || role === 'link') score += 10;
1720
+
1721
+ if (score > bestScore) {
1722
+ best = el;
1723
+ bestScore = score;
1724
+ }
1725
+ }
1726
+
1727
+ return best;
1728
+ }
1729
+
1730
+ function bpFindByRole(role, name, includeHidden) {
1731
+ var targetRole = bpNormalizeSpace(role).toLowerCase();
1732
+ if (!targetRole) return null;
1733
+
1734
+ var nameNeedle = bpNormalizeSpace(name).toLowerCase();
1735
+ var best = null;
1736
+ var bestScore = -1;
1737
+ var elements = bpCollectElements(document);
1738
+
1739
+ for (var i = 0; i < elements.length; i++) {
1740
+ var el = elements[i];
1741
+ if (!includeHidden && !bpIsVisible(el)) continue;
1742
+
1743
+ var actualRole = bpInferRole(el);
1744
+ if (actualRole !== targetRole) continue;
1745
+
1746
+ var accessibleName = bpAccessibleName(el);
1747
+ if (nameNeedle) {
1748
+ var loweredName = accessibleName.toLowerCase();
1749
+ if (!loweredName.includes(nameNeedle)) continue;
1750
+ }
1751
+
1752
+ var score = 0;
1753
+ if (accessibleName) score += 10;
1754
+ if (nameNeedle && accessibleName.toLowerCase() === nameNeedle) score += 20;
1755
+
1756
+ if (score > bestScore) {
1757
+ best = el;
1758
+ bestScore = score;
1759
+ }
1760
+ }
1761
+
1762
+ return best;
1763
+ }
1764
+ `;
1765
+ function buildSpecialSelectorLookupExpression(selector, options = {}) {
1766
+ const includeHidden = options.includeHidden === true;
1767
+ const text = parseTextSelector(selector);
1768
+ if (text) {
1769
+ return `(() => {
1770
+ ${SPECIAL_SELECTOR_SCRIPT}
1771
+ return bpFindByText(${JSON.stringify(text.query)}, ${text.exact}, ${includeHidden});
1772
+ })()`;
1773
+ }
1774
+ const role = parseRoleSelector(selector);
1775
+ if (role) {
1776
+ return `(() => {
1777
+ ${SPECIAL_SELECTOR_SCRIPT}
1778
+ return bpFindByRole(${JSON.stringify(role.role)}, ${JSON.stringify(role.name ?? "")}, ${includeHidden});
1779
+ })()`;
1780
+ }
1781
+ return null;
1782
+ }
1783
+ function buildSpecialSelectorPredicateExpression(selector, options = {}) {
1784
+ const lookup = buildSpecialSelectorLookupExpression(selector, options);
1785
+ if (!lookup) return null;
1786
+ return `(() => !!(${lookup}))()`;
1787
+ }
1788
+
1510
1789
  // src/wait/strategies.ts
1511
1790
  var DEEP_QUERY_SCRIPT = `
1512
1791
  function deepQuery(selector, root = document) {
@@ -1540,18 +1819,19 @@ function deepQuery(selector, root = document) {
1540
1819
  }
1541
1820
  `;
1542
1821
  async function isElementVisible(cdp, selector, contextId) {
1822
+ const specialExpression = buildSpecialSelectorPredicateExpression(selector);
1543
1823
  const params = {
1544
- expression: `(() => {
1545
- ${DEEP_QUERY_SCRIPT}
1546
- const el = deepQuery(${JSON.stringify(selector)});
1547
- if (!el) return false;
1548
- const style = getComputedStyle(el);
1549
- if (style.display === 'none') return false;
1550
- if (style.visibility === 'hidden') return false;
1551
- if (parseFloat(style.opacity) === 0) return false;
1552
- const rect = el.getBoundingClientRect();
1553
- return rect.width > 0 && rect.height > 0;
1554
- })()`,
1824
+ expression: specialExpression ?? `(() => {
1825
+ ${DEEP_QUERY_SCRIPT}
1826
+ const el = deepQuery(${JSON.stringify(selector)});
1827
+ if (!el) return false;
1828
+ const style = getComputedStyle(el);
1829
+ if (style.display === 'none') return false;
1830
+ if (style.visibility === 'hidden') return false;
1831
+ if (parseFloat(style.opacity) === 0) return false;
1832
+ const rect = el.getBoundingClientRect();
1833
+ return rect.width > 0 && rect.height > 0;
1834
+ })()`,
1555
1835
  returnByValue: true
1556
1836
  };
1557
1837
  if (contextId !== void 0) {
@@ -1561,11 +1841,14 @@ async function isElementVisible(cdp, selector, contextId) {
1561
1841
  return result.result.value === true;
1562
1842
  }
1563
1843
  async function isElementAttached(cdp, selector, contextId) {
1844
+ const specialExpression = buildSpecialSelectorPredicateExpression(selector, {
1845
+ includeHidden: true
1846
+ });
1564
1847
  const params = {
1565
- expression: `(() => {
1566
- ${DEEP_QUERY_SCRIPT}
1567
- return deepQuery(${JSON.stringify(selector)}) !== null;
1568
- })()`,
1848
+ expression: specialExpression ?? `(() => {
1849
+ ${DEEP_QUERY_SCRIPT}
1850
+ return deepQuery(${JSON.stringify(selector)}) !== null;
1851
+ })()`,
1569
1852
  returnByValue: true
1570
1853
  };
1571
1854
  if (contextId !== void 0) {
@@ -2226,6 +2509,9 @@ var Page = class {
2226
2509
  timeout: options.timeout ?? DEFAULT_TIMEOUT
2227
2510
  });
2228
2511
  } catch (e) {
2512
+ if (e instanceof ActionabilityError && e.failureType === "hitTarget" && await this.tryClickAssociatedLabel(objectId)) {
2513
+ return true;
2514
+ }
2229
2515
  if (options.optional) return false;
2230
2516
  throw e;
2231
2517
  }
@@ -2614,7 +2900,12 @@ var Page = class {
2614
2900
  returnByValue: true
2615
2901
  });
2616
2902
  if (!after.result.value) {
2617
- throw new Error("Clicking the checkbox did not change its state");
2903
+ if (await this.tryToggleViaLabel(object.objectId, true)) {
2904
+ return true;
2905
+ }
2906
+ throw new Error(
2907
+ "Clicking the checkbox did not change its state. Tried the associated label too."
2908
+ );
2618
2909
  }
2619
2910
  return true;
2620
2911
  });
@@ -2666,7 +2957,12 @@ var Page = class {
2666
2957
  returnByValue: true
2667
2958
  });
2668
2959
  if (after.result.value) {
2669
- throw new Error("Clicking the checkbox did not change its state");
2960
+ if (await this.tryToggleViaLabel(object.objectId, false)) {
2961
+ return true;
2962
+ }
2963
+ throw new Error(
2964
+ "Clicking the checkbox did not change its state. Tried the associated label too."
2965
+ );
2670
2966
  }
2671
2967
  return true;
2672
2968
  });
@@ -2990,7 +3286,7 @@ var Page = class {
2990
3286
  }
2991
3287
  const result = await this.cdp.send("Runtime.evaluate", params);
2992
3288
  if (result.exceptionDetails) {
2993
- throw new Error(`Evaluation failed: ${result.exceptionDetails.text}`);
3289
+ throw new Error(this.formatEvaluationError(result.exceptionDetails));
2994
3290
  }
2995
3291
  return result.result.value;
2996
3292
  }
@@ -3042,6 +3338,75 @@ var Page = class {
3042
3338
  return result.result.value ?? "";
3043
3339
  });
3044
3340
  }
3341
+ /**
3342
+ * Enumerate form controls on the page with labels and current state.
3343
+ */
3344
+ async forms() {
3345
+ const result = await this.evaluateInFrame(
3346
+ `(() => {
3347
+ function normalize(value) {
3348
+ return String(value == null ? '' : value).replace(/\\s+/g, ' ').trim();
3349
+ }
3350
+
3351
+ function labelFor(el) {
3352
+ if (!el) return '';
3353
+ if (el.labels && el.labels.length) {
3354
+ return normalize(
3355
+ Array.from(el.labels)
3356
+ .map((label) => label.innerText || label.textContent || '')
3357
+ .join(' ')
3358
+ );
3359
+ }
3360
+ var ariaLabel = normalize(el.getAttribute && el.getAttribute('aria-label'));
3361
+ if (ariaLabel) return ariaLabel;
3362
+ if (el.id) {
3363
+ var byFor = document.querySelector('label[for="' + el.id.replace(/"/g, '\\\\"') + '"]');
3364
+ if (byFor) return normalize(byFor.innerText || byFor.textContent || '');
3365
+ }
3366
+ var closest = el.closest && el.closest('label');
3367
+ if (closest) return normalize(closest.innerText || closest.textContent || '');
3368
+ return '';
3369
+ }
3370
+
3371
+ return Array.from(document.querySelectorAll('input, select, textarea')).map((el) => {
3372
+ var tag = el.tagName.toLowerCase();
3373
+ var type = tag === 'input' ? (el.type || 'text').toLowerCase() : tag;
3374
+ var value = null;
3375
+
3376
+ if (tag === 'select') {
3377
+ value = el.multiple
3378
+ ? Array.from(el.selectedOptions).map((opt) => opt.value)
3379
+ : el.value || null;
3380
+ } else if (tag === 'textarea' || tag === 'input') {
3381
+ value = typeof el.value === 'string' ? el.value : null;
3382
+ }
3383
+
3384
+ return {
3385
+ tag: tag,
3386
+ type: type,
3387
+ id: el.id || undefined,
3388
+ name: el.getAttribute('name') || undefined,
3389
+ value: value,
3390
+ checked: 'checked' in el ? !!el.checked : undefined,
3391
+ required: !!el.required,
3392
+ disabled: !!el.disabled,
3393
+ label: labelFor(el) || undefined,
3394
+ placeholder: normalize(el.getAttribute && el.getAttribute('placeholder')) || undefined,
3395
+ options:
3396
+ tag === 'select'
3397
+ ? Array.from(el.options).map((opt) => ({
3398
+ value: opt.value || '',
3399
+ text: normalize(opt.text || opt.label || ''),
3400
+ selected: !!opt.selected,
3401
+ disabled: !!opt.disabled,
3402
+ }))
3403
+ : undefined,
3404
+ };
3405
+ });
3406
+ })()`
3407
+ );
3408
+ return result.result.value ?? [];
3409
+ }
3045
3410
  // ============ File Handling ============
3046
3411
  /**
3047
3412
  * Set files on a file input
@@ -3514,7 +3879,8 @@ var Page = class {
3514
3879
  /**
3515
3880
  * Get an accessibility tree snapshot of the page
3516
3881
  */
3517
- async snapshot() {
3882
+ async snapshot(options = {}) {
3883
+ const roleFilter = new Set((options.roles ?? []).map((role) => role.trim().toLowerCase()));
3518
3884
  const [url, title, axTree] = await Promise.all([
3519
3885
  this.url(),
3520
3886
  this.title(),
@@ -3535,7 +3901,7 @@ var Page = class {
3535
3901
  const buildNode = (nodeId) => {
3536
3902
  const node = nodeMap.get(nodeId);
3537
3903
  if (!node) return null;
3538
- const role = node.role?.value ?? "generic";
3904
+ const role = (node.role?.value ?? "generic").toLowerCase();
3539
3905
  const name = node.name?.value;
3540
3906
  const value = node.value?.value;
3541
3907
  const ref = nodeRefs.get(nodeId);
@@ -3551,7 +3917,7 @@ var Page = class {
3551
3917
  return {
3552
3918
  role,
3553
3919
  name,
3554
- value,
3920
+ value: value !== void 0 ? String(value) : void 0,
3555
3921
  ref,
3556
3922
  children: children.length > 0 ? children : void 0,
3557
3923
  disabled,
@@ -3559,7 +3925,24 @@ var Page = class {
3559
3925
  };
3560
3926
  };
3561
3927
  const rootNodes = nodes.filter((n) => !n.parentId || !nodeMap.has(n.parentId));
3562
- const accessibilityTree = rootNodes.map((n) => buildNode(n.nodeId)).filter((n) => n !== null);
3928
+ let accessibilityTree = rootNodes.map((n) => buildNode(n.nodeId)).filter((n) => n !== null);
3929
+ if (roleFilter.size > 0) {
3930
+ const filteredAccessibilityTree = [];
3931
+ for (const node of nodes) {
3932
+ if (!roleFilter.has((node.role?.value ?? "generic").toLowerCase())) {
3933
+ continue;
3934
+ }
3935
+ const snapshotNode = buildNode(node.nodeId);
3936
+ if (!snapshotNode) {
3937
+ continue;
3938
+ }
3939
+ filteredAccessibilityTree.push({
3940
+ ...snapshotNode,
3941
+ children: void 0
3942
+ });
3943
+ }
3944
+ accessibilityTree = filteredAccessibilityTree;
3945
+ }
3563
3946
  const interactiveRoles = /* @__PURE__ */ new Set([
3564
3947
  "button",
3565
3948
  "link",
@@ -3581,37 +3964,44 @@ var Page = class {
3581
3964
  ]);
3582
3965
  const interactiveElements = [];
3583
3966
  for (const node of nodes) {
3584
- const role = node.role?.value;
3585
- if (role && interactiveRoles.has(role)) {
3967
+ const role = (node.role?.value ?? "").toLowerCase();
3968
+ if (role && interactiveRoles.has(role) && (roleFilter.size === 0 || roleFilter.has(role))) {
3586
3969
  const ref = nodeRefs.get(node.nodeId);
3587
3970
  const name = node.name?.value ?? "";
3588
3971
  const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
3972
+ const checked = node.properties?.find((p) => p.name === "checked")?.value.value;
3973
+ const value = node.value?.value;
3589
3974
  const selector = node.backendDOMNodeId ? `[data-backend-node-id="${node.backendDOMNodeId}"]` : `[aria-label="${name}"]`;
3590
3975
  interactiveElements.push({
3591
3976
  ref,
3592
3977
  role,
3593
3978
  name,
3594
3979
  selector,
3595
- disabled
3980
+ disabled,
3981
+ checked,
3982
+ value: value !== void 0 ? String(value) : void 0
3596
3983
  });
3597
3984
  }
3598
3985
  }
3986
+ const formatNode = (node, depth = 0) => {
3987
+ let line = `${" ".repeat(depth)}- ${node.role}`;
3988
+ if (node.name) line += ` "${node.name}"`;
3989
+ line += ` ref:${node.ref}`;
3990
+ if (node.disabled) line += " (disabled)";
3991
+ if (node.checked !== void 0) line += node.checked ? " (checked)" : " (unchecked)";
3992
+ return line;
3993
+ };
3599
3994
  const formatTree = (nodes2, depth = 0) => {
3600
3995
  const lines = [];
3601
3996
  for (const node of nodes2) {
3602
- let line = `${" ".repeat(depth)}- ${node.role}`;
3603
- if (node.name) line += ` "${node.name}"`;
3604
- line += ` [ref=${node.ref}]`;
3605
- if (node.disabled) line += " (disabled)";
3606
- if (node.checked !== void 0) line += node.checked ? " (checked)" : " (unchecked)";
3607
- lines.push(line);
3997
+ lines.push(formatNode(node, depth));
3608
3998
  if (node.children) {
3609
3999
  lines.push(formatTree(node.children, depth + 1));
3610
4000
  }
3611
4001
  }
3612
4002
  return lines.join("\n");
3613
4003
  };
3614
- const text = formatTree(accessibilityTree);
4004
+ const text = roleFilter.size > 0 ? accessibilityTree.map((node) => formatNode(node)).join("\n") : formatTree(accessibilityTree);
3615
4005
  const result = {
3616
4006
  url,
3617
4007
  title,
@@ -3620,7 +4010,9 @@ var Page = class {
3620
4010
  interactiveElements,
3621
4011
  text
3622
4012
  };
3623
- this.lastSnapshot = result;
4013
+ if (roleFilter.size === 0) {
4014
+ this.lastSnapshot = result;
4015
+ }
3624
4016
  return result;
3625
4017
  }
3626
4018
  /**
@@ -4175,7 +4567,7 @@ var Page = class {
4175
4567
  }
4176
4568
  /**
4177
4569
  * Find an element using single or multiple selectors
4178
- * Supports ref: prefix for ref-based selectors (e.g., "ref:e4")
4570
+ * Supports ref:, text:, and role: selectors.
4179
4571
  */
4180
4572
  async findElement(selectors, options = {}) {
4181
4573
  const { timeout = DEFAULT_TIMEOUT } = options;
@@ -4242,11 +4634,11 @@ var Page = class {
4242
4634
  }
4243
4635
  }
4244
4636
  }
4245
- const cssSelectors = selectorList.filter((s) => !s.startsWith("ref:"));
4246
- if (cssSelectors.length === 0) {
4637
+ const runtimeSelectors = selectorList.filter((s) => !s.startsWith("ref:"));
4638
+ if (runtimeSelectors.length === 0) {
4247
4639
  return null;
4248
4640
  }
4249
- const result = await waitForAnyElement(this.cdp, cssSelectors, {
4641
+ const result = await waitForAnyElement(this.cdp, runtimeSelectors, {
4250
4642
  state: "visible",
4251
4643
  timeout,
4252
4644
  contextId: this.currentFrameContextId ?? void 0
@@ -4254,6 +4646,14 @@ var Page = class {
4254
4646
  if (!result.success || !result.selector) {
4255
4647
  return null;
4256
4648
  }
4649
+ const specialSelectorMatch = await this.resolveSpecialSelector(result.selector);
4650
+ if (specialSelectorMatch) {
4651
+ this._lastMatchedSelector = result.selector;
4652
+ return {
4653
+ ...specialSelectorMatch,
4654
+ waitedMs: result.waitedMs
4655
+ };
4656
+ }
4257
4657
  await this.ensureRootNode();
4258
4658
  const queryResult = await this.cdp.send("DOM.querySelector", {
4259
4659
  nodeId: this.rootNodeId,
@@ -4300,6 +4700,122 @@ var Page = class {
4300
4700
  waitedMs: result.waitedMs
4301
4701
  };
4302
4702
  }
4703
+ formatEvaluationError(details) {
4704
+ const description = typeof details.exception?.description === "string" && details.exception.description || typeof details.exception?.value === "string" && details.exception.value || details.text || "Uncaught";
4705
+ return `Evaluation failed: ${description}`;
4706
+ }
4707
+ async resolveSpecialSelector(selector, options = {}) {
4708
+ const expression = buildSpecialSelectorLookupExpression(selector, options);
4709
+ if (!expression) return null;
4710
+ const result = await this.evaluateInFrame(expression, {
4711
+ returnByValue: false
4712
+ });
4713
+ if (!result.result.objectId) {
4714
+ return null;
4715
+ }
4716
+ const resolved = await this.objectIdToNode(result.result.objectId);
4717
+ if (!resolved) {
4718
+ return null;
4719
+ }
4720
+ return {
4721
+ nodeId: resolved.nodeId,
4722
+ backendNodeId: resolved.backendNodeId,
4723
+ selector,
4724
+ waitedMs: 0
4725
+ };
4726
+ }
4727
+ async readCheckedState(objectId) {
4728
+ const result = await this.cdp.send("Runtime.callFunctionOn", {
4729
+ objectId,
4730
+ functionDeclaration: "function() { return !!this.checked; }",
4731
+ returnByValue: true
4732
+ });
4733
+ return result.result.value === true;
4734
+ }
4735
+ async readInputType(objectId) {
4736
+ const result = await this.cdp.send(
4737
+ "Runtime.callFunctionOn",
4738
+ {
4739
+ objectId,
4740
+ functionDeclaration: 'function() { return this instanceof HTMLInputElement ? String(this.type || "").toLowerCase() : null; }',
4741
+ returnByValue: true
4742
+ }
4743
+ );
4744
+ return result.result.value ?? null;
4745
+ }
4746
+ async getAssociatedLabelNodeId(objectId) {
4747
+ const result = await this.cdp.send("Runtime.callFunctionOn", {
4748
+ objectId,
4749
+ functionDeclaration: `function() {
4750
+ if (!(this instanceof HTMLInputElement)) return null;
4751
+
4752
+ if (this.id) {
4753
+ var labels = Array.from(document.querySelectorAll('label'));
4754
+ for (var i = 0; i < labels.length; i++) {
4755
+ if (labels[i].htmlFor === this.id) return labels[i];
4756
+ }
4757
+ }
4758
+
4759
+ return this.closest('label');
4760
+ }`,
4761
+ returnByValue: false
4762
+ });
4763
+ if (!result.result.objectId) {
4764
+ return null;
4765
+ }
4766
+ return (await this.objectIdToNode(result.result.objectId))?.nodeId ?? null;
4767
+ }
4768
+ async objectIdToNode(objectId) {
4769
+ const describeResult = await this.cdp.send("DOM.describeNode", {
4770
+ objectId,
4771
+ depth: 0
4772
+ });
4773
+ const backendNodeId = describeResult.node.backendNodeId;
4774
+ if (!backendNodeId) {
4775
+ return null;
4776
+ }
4777
+ if (describeResult.node.nodeId) {
4778
+ return {
4779
+ nodeId: describeResult.node.nodeId,
4780
+ backendNodeId
4781
+ };
4782
+ }
4783
+ await this.ensureRootNode();
4784
+ const pushResult = await this.cdp.send(
4785
+ "DOM.pushNodesByBackendIdsToFrontend",
4786
+ {
4787
+ backendNodeIds: [backendNodeId]
4788
+ }
4789
+ );
4790
+ const nodeId = pushResult.nodeIds?.[0];
4791
+ if (!nodeId) {
4792
+ return null;
4793
+ }
4794
+ return { nodeId, backendNodeId };
4795
+ }
4796
+ async tryClickAssociatedLabel(objectId) {
4797
+ const inputType = await this.readInputType(objectId);
4798
+ if (inputType !== "checkbox" && inputType !== "radio") {
4799
+ return false;
4800
+ }
4801
+ const labelNodeId = await this.getAssociatedLabelNodeId(objectId);
4802
+ if (!labelNodeId) {
4803
+ return false;
4804
+ }
4805
+ try {
4806
+ await this.scrollIntoView(labelNodeId);
4807
+ await this.clickElement(labelNodeId);
4808
+ return true;
4809
+ } catch {
4810
+ return false;
4811
+ }
4812
+ }
4813
+ async tryToggleViaLabel(objectId, desiredChecked) {
4814
+ if (!await this.tryClickAssociatedLabel(objectId)) {
4815
+ return false;
4816
+ }
4817
+ return await this.readCheckedState(objectId) === desiredChecked;
4818
+ }
4303
4819
  /**
4304
4820
  * Ensure we have a valid root node ID
4305
4821
  */
@@ -4717,6 +5233,7 @@ var Browser = class _Browser {
4717
5233
  cdp;
4718
5234
  providerSession;
4719
5235
  pages = /* @__PURE__ */ new Map();
5236
+ pageCounter = 0;
4720
5237
  constructor(cdp, _provider, providerSession, _options) {
4721
5238
  this.cdp = cdp;
4722
5239
  this.providerSession = providerSession;
@@ -4746,7 +5263,11 @@ var Browser = class _Browser {
4746
5263
  const pageName = name ?? "default";
4747
5264
  const cached = this.pages.get(pageName);
4748
5265
  if (cached) return cached;
4749
- const targets = await this.cdp.send("Target.getTargets");
5266
+ const targets = await this.cdp.send(
5267
+ "Target.getTargets",
5268
+ void 0,
5269
+ null
5270
+ );
4750
5271
  let pageTargets = targets.targetInfos.filter((t) => t.type === "page");
4751
5272
  if (options?.targetUrl) {
4752
5273
  const urlFilter = options.targetUrl;
@@ -4768,16 +5289,24 @@ var Browser = class _Browser {
4768
5289
  targetId = options.targetId;
4769
5290
  } else {
4770
5291
  console.warn(`[browser-pilot] Target ${options.targetId} no longer exists, falling back`);
4771
- targetId = pickBestTarget(pageTargets) ?? (await this.cdp.send("Target.createTarget", {
4772
- url: "about:blank"
4773
- })).targetId;
5292
+ targetId = pickBestTarget(pageTargets) ?? (await this.cdp.send(
5293
+ "Target.createTarget",
5294
+ {
5295
+ url: "about:blank"
5296
+ },
5297
+ null
5298
+ )).targetId;
4774
5299
  }
4775
5300
  } else if (pageTargets.length > 0) {
4776
5301
  targetId = pickBestTarget(pageTargets);
4777
5302
  } else {
4778
- const result = await this.cdp.send("Target.createTarget", {
4779
- url: "about:blank"
4780
- });
5303
+ const result = await this.cdp.send(
5304
+ "Target.createTarget",
5305
+ {
5306
+ url: "about:blank"
5307
+ },
5308
+ null
5309
+ );
4781
5310
  targetId = result.targetId;
4782
5311
  }
4783
5312
  await this.cdp.attachToTarget(targetId);
@@ -4805,13 +5334,17 @@ var Browser = class _Browser {
4805
5334
  * Create a new page (tab)
4806
5335
  */
4807
5336
  async newPage(url = "about:blank") {
4808
- const result = await this.cdp.send("Target.createTarget", {
4809
- url
4810
- });
5337
+ const result = await this.cdp.send(
5338
+ "Target.createTarget",
5339
+ {
5340
+ url
5341
+ },
5342
+ null
5343
+ );
4811
5344
  await this.cdp.attachToTarget(result.targetId);
4812
5345
  const page = new Page(this.cdp, result.targetId);
4813
5346
  await page.init();
4814
- const name = `page-${this.pages.size + 1}`;
5347
+ const name = `page-${++this.pageCounter}`;
4815
5348
  this.pages.set(name, page);
4816
5349
  return page;
4817
5350
  }
@@ -4821,14 +5354,30 @@ var Browser = class _Browser {
4821
5354
  async closePage(name) {
4822
5355
  const page = this.pages.get(name);
4823
5356
  if (!page) return;
4824
- const targets = await this.cdp.send("Target.getTargets");
4825
- const pageTargets = targets.targetInfos.filter((t) => t.type === "page");
4826
- if (pageTargets.length > 0) {
4827
- await this.cdp.send("Target.closeTarget", {
4828
- targetId: pageTargets[0].targetId
4829
- });
4830
- }
5357
+ const targetId = page.targetId;
5358
+ await this.cdp.send("Target.closeTarget", { targetId }, null);
4831
5359
  this.pages.delete(name);
5360
+ const deadline = Date.now() + 5e3;
5361
+ while (Date.now() < deadline) {
5362
+ const { targetInfos } = await this.cdp.send(
5363
+ "Target.getTargets",
5364
+ void 0,
5365
+ null
5366
+ );
5367
+ if (!targetInfos.some((t) => t.targetId === targetId)) return;
5368
+ await new Promise((r) => setTimeout(r, 50));
5369
+ }
5370
+ }
5371
+ /**
5372
+ * List all page targets in the connected browser.
5373
+ */
5374
+ async listTargets() {
5375
+ const { targetInfos } = await this.cdp.send(
5376
+ "Target.getTargets",
5377
+ void 0,
5378
+ null
5379
+ );
5380
+ return targetInfos.filter((target) => target.type === "page");
4832
5381
  }
4833
5382
  /**
4834
5383
  * Get the WebSocket URL for this browser connection