browser-pilot 0.0.11 → 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,14 +1,17 @@
1
1
  import {
2
2
  createCDPClient
3
- } from "./chunk-BCOZUKWS.mjs";
3
+ } from "./chunk-HP6R3W32.mjs";
4
4
  import {
5
5
  createProvider
6
6
  } from "./chunk-BRAFQUMG.mjs";
7
7
  import {
8
+ ActionabilityError,
8
9
  BatchExecutor,
9
10
  ElementNotFoundError,
10
- TimeoutError
11
- } from "./chunk-FAUNIZR7.mjs";
11
+ TimeoutError,
12
+ ensureActionable,
13
+ generateHints
14
+ } from "./chunk-A2ZRAEO3.mjs";
12
15
 
13
16
  // src/audio/encoding.ts
14
17
  function bufferToBase64(data) {
@@ -1122,35 +1125,37 @@ var AudioOutput = class {
1122
1125
  let heardAudio = false;
1123
1126
  let lastSoundTime = 0;
1124
1127
  const startTime = Date.now();
1125
- const checkInterval = setInterval(async () => {
1126
- const elapsed = Date.now() - startTime;
1127
- if (elapsed > maxDuration) {
1128
- clearInterval(checkInterval);
1129
- this.onDiagHandler?.(`max duration reached (${maxDuration}ms), stopping`);
1130
- resolve(await this.stop());
1131
- return;
1132
- }
1133
- const latest = this.chunks[this.chunks.length - 1];
1134
- if (latest) {
1135
- const rms = calculateRMS(latest.left);
1136
- if (rms > silenceThreshold) {
1137
- if (!heardAudio) {
1138
- heardAudio = true;
1139
- this.onDiagHandler?.("first audio detected \u2014 silence countdown begins");
1128
+ const checkInterval = setInterval(() => {
1129
+ void (async () => {
1130
+ const elapsed = Date.now() - startTime;
1131
+ if (elapsed > maxDuration) {
1132
+ clearInterval(checkInterval);
1133
+ this.onDiagHandler?.(`max duration reached (${maxDuration}ms), stopping`);
1134
+ resolve(await this.stop());
1135
+ return;
1136
+ }
1137
+ const latest = this.chunks[this.chunks.length - 1];
1138
+ if (latest) {
1139
+ const rms = calculateRMS(latest.left);
1140
+ if (rms > silenceThreshold) {
1141
+ if (!heardAudio) {
1142
+ heardAudio = true;
1143
+ this.onDiagHandler?.("first audio detected \u2014 silence countdown begins");
1144
+ }
1145
+ lastSoundTime = Date.now();
1140
1146
  }
1141
- lastSoundTime = Date.now();
1142
1147
  }
1143
- }
1144
- if (!heardAudio && elapsed > noAudioTimeout) {
1145
- clearInterval(checkInterval);
1146
- this.onDiagHandler?.(`no audio detected after ${noAudioTimeout}ms, stopping early`);
1147
- resolve(await this.stop());
1148
- return;
1149
- }
1150
- if (heardAudio && Date.now() - lastSoundTime > silenceTimeout) {
1151
- clearInterval(checkInterval);
1152
- resolve(await this.stop());
1153
- }
1148
+ if (!heardAudio && elapsed > noAudioTimeout) {
1149
+ clearInterval(checkInterval);
1150
+ this.onDiagHandler?.(`no audio detected after ${noAudioTimeout}ms, stopping early`);
1151
+ resolve(await this.stop());
1152
+ return;
1153
+ }
1154
+ if (heardAudio && Date.now() - lastSoundTime > silenceTimeout) {
1155
+ clearInterval(checkInterval);
1156
+ resolve(await this.stop());
1157
+ }
1158
+ })();
1154
1159
  }, 200);
1155
1160
  });
1156
1161
  }
@@ -1325,8 +1330,12 @@ var RequestInterceptor = class {
1325
1330
  boundHandleAuthRequired;
1326
1331
  constructor(cdp) {
1327
1332
  this.cdp = cdp;
1328
- this.boundHandleRequestPaused = this.handleRequestPaused.bind(this);
1329
- this.boundHandleAuthRequired = this.handleAuthRequired.bind(this);
1333
+ this.boundHandleRequestPaused = (params) => {
1334
+ void this.handleRequestPaused(params);
1335
+ };
1336
+ this.boundHandleAuthRequired = (params) => {
1337
+ void this.handleAuthRequired(params);
1338
+ };
1330
1339
  }
1331
1340
  /**
1332
1341
  * Enable request interception with optional patterns
@@ -1498,6 +1507,285 @@ var RequestInterceptor = class {
1498
1507
  }
1499
1508
  };
1500
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
+
1501
1789
  // src/wait/strategies.ts
1502
1790
  var DEEP_QUERY_SCRIPT = `
1503
1791
  function deepQuery(selector, root = document) {
@@ -1531,18 +1819,19 @@ function deepQuery(selector, root = document) {
1531
1819
  }
1532
1820
  `;
1533
1821
  async function isElementVisible(cdp, selector, contextId) {
1822
+ const specialExpression = buildSpecialSelectorPredicateExpression(selector);
1534
1823
  const params = {
1535
- expression: `(() => {
1536
- ${DEEP_QUERY_SCRIPT}
1537
- const el = deepQuery(${JSON.stringify(selector)});
1538
- if (!el) return false;
1539
- const style = getComputedStyle(el);
1540
- if (style.display === 'none') return false;
1541
- if (style.visibility === 'hidden') return false;
1542
- if (parseFloat(style.opacity) === 0) return false;
1543
- const rect = el.getBoundingClientRect();
1544
- return rect.width > 0 && rect.height > 0;
1545
- })()`,
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
+ })()`,
1546
1835
  returnByValue: true
1547
1836
  };
1548
1837
  if (contextId !== void 0) {
@@ -1552,11 +1841,14 @@ async function isElementVisible(cdp, selector, contextId) {
1552
1841
  return result.result.value === true;
1553
1842
  }
1554
1843
  async function isElementAttached(cdp, selector, contextId) {
1844
+ const specialExpression = buildSpecialSelectorPredicateExpression(selector, {
1845
+ includeHidden: true
1846
+ });
1555
1847
  const params = {
1556
- expression: `(() => {
1557
- ${DEEP_QUERY_SCRIPT}
1558
- return deepQuery(${JSON.stringify(selector)}) !== null;
1559
- })()`,
1848
+ expression: specialExpression ?? `(() => {
1849
+ ${DEEP_QUERY_SCRIPT}
1850
+ return deepQuery(${JSON.stringify(selector)}) !== null;
1851
+ })()`,
1560
1852
  returnByValue: true
1561
1853
  };
1562
1854
  if (contextId !== void 0) {
@@ -1568,30 +1860,71 @@ async function isElementAttached(cdp, selector, contextId) {
1568
1860
  function sleep2(ms) {
1569
1861
  return new Promise((resolve) => setTimeout(resolve, ms));
1570
1862
  }
1863
+ async function isPageStatic(cdp, windowMs = 200, contextId) {
1864
+ const params = {
1865
+ expression: `new Promise(resolve => {
1866
+ // If page is still loading, it's not static
1867
+ if (document.readyState !== 'complete') { resolve(false); return; }
1868
+ // Check for recent page load (navigationStart within last 1s = page just loaded)
1869
+ try {
1870
+ var nav = performance.getEntriesByType('navigation')[0];
1871
+ if (nav && (performance.now() - nav.loadEventEnd) < 500) { resolve(false); return; }
1872
+ } catch(e) {}
1873
+ // Observe for DOM mutations
1874
+ var seen = false;
1875
+ var obs = new MutationObserver(function() { seen = true; });
1876
+ obs.observe(document.documentElement, { childList: true, subtree: true, attributes: true });
1877
+ setTimeout(function() { obs.disconnect(); resolve(!seen); }, ${windowMs});
1878
+ })`,
1879
+ returnByValue: true,
1880
+ awaitPromise: true
1881
+ };
1882
+ if (contextId !== void 0) params["contextId"] = contextId;
1883
+ try {
1884
+ const result = await cdp.send("Runtime.evaluate", params);
1885
+ return result.result.value === true;
1886
+ } catch {
1887
+ return false;
1888
+ }
1889
+ }
1571
1890
  async function waitForElement(cdp, selector, options = {}) {
1572
1891
  const { state = "visible", timeout = 3e4, pollInterval = 100, contextId } = options;
1573
1892
  const startTime = Date.now();
1574
1893
  const deadline = startTime + timeout;
1575
- while (Date.now() < deadline) {
1576
- let conditionMet = false;
1894
+ const checkCondition = async () => {
1577
1895
  switch (state) {
1578
1896
  case "visible":
1579
- conditionMet = await isElementVisible(cdp, selector, contextId);
1580
- break;
1897
+ return isElementVisible(cdp, selector, contextId);
1581
1898
  case "hidden":
1582
- conditionMet = !await isElementVisible(cdp, selector, contextId);
1583
- break;
1899
+ return !await isElementVisible(cdp, selector, contextId);
1584
1900
  case "attached":
1585
- conditionMet = await isElementAttached(cdp, selector, contextId);
1586
- break;
1901
+ return isElementAttached(cdp, selector, contextId);
1587
1902
  case "detached":
1588
- conditionMet = !await isElementAttached(cdp, selector, contextId);
1589
- break;
1903
+ return !await isElementAttached(cdp, selector, contextId);
1904
+ default: {
1905
+ const _exhaustive = state;
1906
+ throw new Error(`Unhandled wait state: ${_exhaustive}`);
1907
+ }
1590
1908
  }
1591
- if (conditionMet) {
1592
- return { success: true, waitedMs: Date.now() - startTime };
1909
+ };
1910
+ if (await checkCondition()) {
1911
+ return { success: true, waitedMs: Date.now() - startTime };
1912
+ }
1913
+ const waitingForPresence = state === "visible" || state === "attached";
1914
+ if (waitingForPresence && timeout >= 300) {
1915
+ const pageStatic = await isPageStatic(cdp, 200, contextId);
1916
+ if (pageStatic) {
1917
+ if (await checkCondition()) {
1918
+ return { success: true, waitedMs: Date.now() - startTime };
1919
+ }
1920
+ return { success: false, waitedMs: Date.now() - startTime };
1593
1921
  }
1922
+ }
1923
+ while (Date.now() < deadline) {
1594
1924
  await sleep2(pollInterval);
1925
+ if (await checkCondition()) {
1926
+ return { success: true, waitedMs: Date.now() - startTime };
1927
+ }
1595
1928
  }
1596
1929
  return { success: false, waitedMs: Date.now() - startTime };
1597
1930
  }
@@ -1599,28 +1932,46 @@ async function waitForAnyElement(cdp, selectors, options = {}) {
1599
1932
  const { state = "visible", timeout = 3e4, pollInterval = 100, contextId } = options;
1600
1933
  const startTime = Date.now();
1601
1934
  const deadline = startTime + timeout;
1935
+ const checkSelector = async (selector) => {
1936
+ switch (state) {
1937
+ case "visible":
1938
+ return isElementVisible(cdp, selector, contextId);
1939
+ case "hidden":
1940
+ return !await isElementVisible(cdp, selector, contextId);
1941
+ case "attached":
1942
+ return isElementAttached(cdp, selector, contextId);
1943
+ case "detached":
1944
+ return !await isElementAttached(cdp, selector, contextId);
1945
+ default: {
1946
+ const _exhaustive = state;
1947
+ throw new Error(`Unhandled wait state: ${_exhaustive}`);
1948
+ }
1949
+ }
1950
+ };
1951
+ for (const selector of selectors) {
1952
+ if (await checkSelector(selector)) {
1953
+ return { success: true, selector, waitedMs: Date.now() - startTime };
1954
+ }
1955
+ }
1956
+ const waitingForPresence = state === "visible" || state === "attached";
1957
+ if (waitingForPresence && timeout >= 300) {
1958
+ const pageStatic = await isPageStatic(cdp, 200, contextId);
1959
+ if (pageStatic) {
1960
+ for (const selector of selectors) {
1961
+ if (await checkSelector(selector)) {
1962
+ return { success: true, selector, waitedMs: Date.now() - startTime };
1963
+ }
1964
+ }
1965
+ return { success: false, waitedMs: Date.now() - startTime };
1966
+ }
1967
+ }
1602
1968
  while (Date.now() < deadline) {
1969
+ await sleep2(pollInterval);
1603
1970
  for (const selector of selectors) {
1604
- let conditionMet = false;
1605
- switch (state) {
1606
- case "visible":
1607
- conditionMet = await isElementVisible(cdp, selector, contextId);
1608
- break;
1609
- case "hidden":
1610
- conditionMet = !await isElementVisible(cdp, selector, contextId);
1611
- break;
1612
- case "attached":
1613
- conditionMet = await isElementAttached(cdp, selector, contextId);
1614
- break;
1615
- case "detached":
1616
- conditionMet = !await isElementAttached(cdp, selector, contextId);
1617
- break;
1618
- }
1619
- if (conditionMet) {
1971
+ if (await checkSelector(selector)) {
1620
1972
  return { success: true, selector, waitedMs: Date.now() - startTime };
1621
1973
  }
1622
1974
  }
1623
- await sleep2(pollInterval);
1624
1975
  }
1625
1976
  return { success: false, waitedMs: Date.now() - startTime };
1626
1977
  }
@@ -1667,6 +2018,13 @@ async function waitForNavigation(cdp, options = {}) {
1667
2018
  cdp.on("Page.navigatedWithinDocument", onSameDoc);
1668
2019
  cleanup.push(() => cdp.off("Page.navigatedWithinDocument", onSameDoc));
1669
2020
  }
2021
+ const onLifecycle = (params) => {
2022
+ if (params["name"] === "networkIdle") {
2023
+ done(true);
2024
+ }
2025
+ };
2026
+ cdp.on("Page.lifecycleEvent", onLifecycle);
2027
+ cleanup.push(() => cdp.off("Page.lifecycleEvent", onLifecycle));
1670
2028
  const pollUrl = async () => {
1671
2029
  while (!resolved && Date.now() < startTime + timeout) {
1672
2030
  await sleep2(100);
@@ -1681,7 +2039,7 @@ async function waitForNavigation(cdp, options = {}) {
1681
2039
  }
1682
2040
  }
1683
2041
  };
1684
- pollUrl();
2042
+ void pollUrl();
1685
2043
  });
1686
2044
  }
1687
2045
  async function waitForNetworkIdle(cdp, options = {}) {
@@ -1729,580 +2087,6 @@ async function waitForNetworkIdle(cdp, options = {}) {
1729
2087
  });
1730
2088
  }
1731
2089
 
1732
- // src/browser/actionability.ts
1733
- var ActionabilityError = class extends Error {
1734
- failureType;
1735
- coveringElement;
1736
- constructor(message, failureType, coveringElement) {
1737
- super(message);
1738
- this.name = "ActionabilityError";
1739
- this.failureType = failureType;
1740
- this.coveringElement = coveringElement;
1741
- }
1742
- };
1743
- var CHECK_VISIBLE = `function() {
1744
- // checkVisibility handles display:none, visibility:hidden, content-visibility up the tree
1745
- if (typeof this.checkVisibility === 'function' && !this.checkVisibility()) {
1746
- return { actionable: false, reason: 'Element is not visible (checkVisibility failed). Try scrolling or check if a prior action is needed to reveal it.' };
1747
- }
1748
-
1749
- var style = getComputedStyle(this);
1750
-
1751
- if (style.visibility !== 'visible') {
1752
- return { actionable: false, reason: 'Element has visibility: ' + style.visibility + '. Try scrolling or check if a prior action is needed to reveal it.' };
1753
- }
1754
-
1755
- // display:contents elements have no box themselves \u2014 check children
1756
- if (style.display === 'contents') {
1757
- var children = this.children;
1758
- if (children.length === 0) {
1759
- return { actionable: false, reason: 'Element has display:contents with no children. Try scrolling or check if a prior action is needed to reveal it.' };
1760
- }
1761
- for (var i = 0; i < children.length; i++) {
1762
- var childRect = children[i].getBoundingClientRect();
1763
- if (childRect.width > 0 && childRect.height > 0) {
1764
- return { actionable: true };
1765
- }
1766
- }
1767
- return { actionable: false, reason: 'Element has display:contents but no visible children. Try scrolling or check if a prior action is needed to reveal it.' };
1768
- }
1769
-
1770
- var rect = this.getBoundingClientRect();
1771
- if (rect.width <= 0 || rect.height <= 0) {
1772
- return { actionable: false, reason: 'Element has zero size (' + rect.width + 'x' + rect.height + '). Try scrolling or check if a prior action is needed to reveal it.' };
1773
- }
1774
-
1775
- return { actionable: true };
1776
- }`;
1777
- var CHECK_ENABLED = `function() {
1778
- // Native disabled property
1779
- var disableable = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTION', 'OPTGROUP'];
1780
- if (disableable.indexOf(this.tagName) !== -1 && this.disabled) {
1781
- return { actionable: false, reason: 'Element is disabled. Check if a prerequisite field needs to be filled first.' };
1782
- }
1783
-
1784
- // Check ancestor FIELDSET[disabled]
1785
- var parent = this.parentElement;
1786
- while (parent) {
1787
- if (parent.tagName === 'FIELDSET' && parent.disabled) {
1788
- // Exception: elements inside the first <legend> of a disabled fieldset are NOT disabled
1789
- var legend = parent.querySelector(':scope > legend');
1790
- if (!legend || !legend.contains(this)) {
1791
- return { actionable: false, reason: 'Element is inside a disabled fieldset. Check if a prerequisite field needs to be filled first.' };
1792
- }
1793
- }
1794
- parent = parent.parentElement;
1795
- }
1796
-
1797
- // aria-disabled="true" walking up ancestor chain (crosses shadow DOM)
1798
- var node = this;
1799
- while (node) {
1800
- if (node.nodeType === 1 && node.getAttribute && node.getAttribute('aria-disabled') === 'true') {
1801
- return { actionable: false, reason: 'Element or ancestor has aria-disabled="true". Check if a prerequisite field needs to be filled first.' };
1802
- }
1803
- if (node.parentElement) {
1804
- node = node.parentElement;
1805
- } else if (node.getRootNode && node.getRootNode() !== node) {
1806
- // Cross shadow DOM boundary
1807
- var root = node.getRootNode();
1808
- node = root.host || null;
1809
- } else {
1810
- break;
1811
- }
1812
- }
1813
-
1814
- return { actionable: true };
1815
- }`;
1816
- var CHECK_STABLE = `function() {
1817
- var self = this;
1818
- return new Promise(function(resolve) {
1819
- var maxFrames = 30;
1820
- var prev = null;
1821
- var frame = 0;
1822
- var resolved = false;
1823
-
1824
- var fallbackTimer = setTimeout(function() {
1825
- if (!resolved) {
1826
- resolved = true;
1827
- resolve({ actionable: false, reason: 'Element stability check timed out (tab may be backgrounded)' });
1828
- }
1829
- }, 2000);
1830
-
1831
- function check() {
1832
- if (resolved) return;
1833
- frame++;
1834
- if (frame > maxFrames) {
1835
- resolved = true;
1836
- clearTimeout(fallbackTimer);
1837
- resolve({ actionable: false, reason: 'Element position not stable after ' + maxFrames + ' frames' });
1838
- return;
1839
- }
1840
-
1841
- var rect = self.getBoundingClientRect();
1842
- var cur = { x: rect.x, y: rect.y, w: rect.width, h: rect.height };
1843
-
1844
- if (prev !== null &&
1845
- prev.x === cur.x && prev.y === cur.y &&
1846
- prev.w === cur.w && prev.h === cur.h) {
1847
- resolved = true;
1848
- clearTimeout(fallbackTimer);
1849
- resolve({ actionable: true });
1850
- return;
1851
- }
1852
-
1853
- prev = cur;
1854
- requestAnimationFrame(check);
1855
- }
1856
-
1857
- requestAnimationFrame(check);
1858
- });
1859
- }`;
1860
- var CHECK_HIT_TARGET = `function(x, y) {
1861
- // Compute click center if coordinates not provided
1862
- if (x === undefined || y === undefined) {
1863
- var rect = this.getBoundingClientRect();
1864
- x = rect.x + rect.width / 2;
1865
- y = rect.y + rect.height / 2;
1866
- }
1867
-
1868
- function checkPoint(root, px, py) {
1869
- var method = root.elementsFromPoint || root.msElementsFromPoint;
1870
- if (!method) return [];
1871
- return method.call(root, px, py) || [];
1872
- }
1873
-
1874
- // Follow only the top-most hit through nested shadow roots.
1875
- // Accepting any hit in the stack creates false positives for covered elements.
1876
- var root = document;
1877
- var topHits = [];
1878
- var seenRoots = [];
1879
- while (root && seenRoots.indexOf(root) === -1) {
1880
- seenRoots.push(root);
1881
- var hits = checkPoint(root, x, y);
1882
- if (!hits.length) break;
1883
- var top = hits[0];
1884
- topHits.push(top);
1885
- if (top && top.shadowRoot) {
1886
- root = top.shadowRoot;
1887
- continue;
1888
- }
1889
- break;
1890
- }
1891
-
1892
- // Target must be the top-most hit element or an ancestor/descendant
1893
- for (var j = 0; j < topHits.length; j++) {
1894
- var hit = topHits[j];
1895
- if (hit === this || this.contains(hit) || hit.contains(this)) {
1896
- return { actionable: true };
1897
- }
1898
- }
1899
-
1900
- // Report the covering element
1901
- var top = topHits.length > 0 ? topHits[topHits.length - 1] : null;
1902
- if (top) {
1903
- return {
1904
- actionable: false,
1905
- reason: 'Element is covered by <' + top.tagName.toLowerCase() + '>' +
1906
- (top.id ? '#' + top.id : '') +
1907
- (top.className && typeof top.className === 'string' ? '.' + top.className.split(' ').join('.') : '') +
1908
- '. Try dismissing overlays first.',
1909
- coveringElement: {
1910
- tag: top.tagName.toLowerCase(),
1911
- id: top.id || undefined,
1912
- className: (typeof top.className === 'string' && top.className) || undefined
1913
- }
1914
- };
1915
- }
1916
-
1917
- return { actionable: false, reason: 'No element found at click point (' + x + ', ' + y + '). Try scrolling the element into view first.' };
1918
- }`;
1919
- var CHECK_EDITABLE = `function() {
1920
- // Must be an editable element type
1921
- var tag = this.tagName;
1922
- var isEditable = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' ||
1923
- this.isContentEditable;
1924
- if (!isEditable) {
1925
- return { actionable: false, reason: 'Element is not an editable type (<' + tag.toLowerCase() + '>). Target an <input>, <textarea>, <select>, or [contenteditable] element instead.' };
1926
- }
1927
-
1928
- // Check disabled
1929
- var disableable = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTION', 'OPTGROUP'];
1930
- if (disableable.indexOf(tag) !== -1 && this.disabled) {
1931
- return { actionable: false, reason: 'Element is disabled. Check if a prerequisite field needs to be filled first.' };
1932
- }
1933
-
1934
- // Check ancestor FIELDSET[disabled]
1935
- var parent = this.parentElement;
1936
- while (parent) {
1937
- if (parent.tagName === 'FIELDSET' && parent.disabled) {
1938
- var legend = parent.querySelector(':scope > legend');
1939
- if (!legend || !legend.contains(this)) {
1940
- return { actionable: false, reason: 'Element is inside a disabled fieldset. Check if a prerequisite field needs to be filled first.' };
1941
- }
1942
- }
1943
- parent = parent.parentElement;
1944
- }
1945
-
1946
- // aria-disabled walking up (crosses shadow DOM)
1947
- var node = this;
1948
- while (node) {
1949
- if (node.nodeType === 1 && node.getAttribute && node.getAttribute('aria-disabled') === 'true') {
1950
- return { actionable: false, reason: 'Element or ancestor has aria-disabled="true". Check if a prerequisite field needs to be filled first.' };
1951
- }
1952
- if (node.parentElement) {
1953
- node = node.parentElement;
1954
- } else if (node.getRootNode && node.getRootNode() !== node) {
1955
- var root = node.getRootNode();
1956
- node = root.host || null;
1957
- } else {
1958
- break;
1959
- }
1960
- }
1961
-
1962
- // Check readonly
1963
- if (this.hasAttribute && this.hasAttribute('readonly')) {
1964
- return { actionable: false, reason: 'Cannot fill a readonly input. Remove the readonly attribute or target a different element.' };
1965
- }
1966
- if (this.getAttribute && this.getAttribute('aria-readonly') === 'true') {
1967
- return { actionable: false, reason: 'Cannot fill a readonly input (aria-readonly="true"). Remove the attribute or target a different element.' };
1968
- }
1969
-
1970
- return { actionable: true };
1971
- }`;
1972
- function sleep3(ms) {
1973
- return new Promise((resolve) => setTimeout(resolve, ms));
1974
- }
1975
- var BACKOFF = [0, 20, 100, 100];
1976
- async function runCheck(cdp, objectId, check, options) {
1977
- let script;
1978
- let awaitPromise = false;
1979
- const args = [];
1980
- switch (check) {
1981
- case "visible":
1982
- script = CHECK_VISIBLE;
1983
- break;
1984
- case "enabled":
1985
- script = CHECK_ENABLED;
1986
- break;
1987
- case "stable":
1988
- script = CHECK_STABLE;
1989
- awaitPromise = true;
1990
- break;
1991
- case "hitTarget":
1992
- script = CHECK_HIT_TARGET;
1993
- if (options?.coordinates) {
1994
- args.push({ value: options.coordinates.x });
1995
- args.push({ value: options.coordinates.y });
1996
- } else {
1997
- args.push({ value: void 0 });
1998
- args.push({ value: void 0 });
1999
- }
2000
- break;
2001
- case "editable":
2002
- script = CHECK_EDITABLE;
2003
- break;
2004
- default: {
2005
- const _exhaustive = check;
2006
- throw new Error(`Unknown actionability check: ${_exhaustive}`);
2007
- }
2008
- }
2009
- const params = {
2010
- functionDeclaration: script,
2011
- objectId,
2012
- returnByValue: true,
2013
- arguments: args
2014
- };
2015
- if (awaitPromise) {
2016
- params["awaitPromise"] = true;
2017
- }
2018
- const response = await cdp.send("Runtime.callFunctionOn", params);
2019
- if (response.exceptionDetails) {
2020
- return {
2021
- actionable: false,
2022
- reason: `Check "${check}" threw: ${response.exceptionDetails.text}`,
2023
- failureType: check
2024
- };
2025
- }
2026
- const result = response.result.value;
2027
- if (!result.actionable) {
2028
- result.failureType = check;
2029
- }
2030
- return result;
2031
- }
2032
- async function runChecks(cdp, objectId, checks, options) {
2033
- for (const check of checks) {
2034
- const result = await runCheck(cdp, objectId, check, options);
2035
- if (!result.actionable) {
2036
- return result;
2037
- }
2038
- }
2039
- return { actionable: true };
2040
- }
2041
- async function ensureActionable(cdp, objectId, checks, options) {
2042
- const timeout = options?.timeout ?? 3e4;
2043
- const start = Date.now();
2044
- let attempt = 0;
2045
- while (true) {
2046
- const result = await runChecks(cdp, objectId, checks, options);
2047
- if (result.actionable) return;
2048
- if (Date.now() - start >= timeout) {
2049
- throw new ActionabilityError(
2050
- `Element not actionable: ${result.reason}`,
2051
- result.failureType,
2052
- result.coveringElement
2053
- );
2054
- }
2055
- const delay = attempt < BACKOFF.length ? BACKOFF[attempt] ?? 0 : 500;
2056
- if (delay > 0) await sleep3(delay);
2057
- attempt++;
2058
- }
2059
- }
2060
-
2061
- // src/browser/fuzzy-match.ts
2062
- function jaroWinkler(a, b) {
2063
- if (a.length === 0 && b.length === 0) return 0;
2064
- if (a.length === 0 || b.length === 0) return 0;
2065
- if (a === b) return 1;
2066
- const s1 = a.toLowerCase();
2067
- const s2 = b.toLowerCase();
2068
- const matchWindow = Math.max(0, Math.floor(Math.max(s1.length, s2.length) / 2) - 1);
2069
- const s1Matches = new Array(s1.length).fill(false);
2070
- const s2Matches = new Array(s2.length).fill(false);
2071
- let matches = 0;
2072
- let transpositions = 0;
2073
- for (let i = 0; i < s1.length; i++) {
2074
- const start = Math.max(0, i - matchWindow);
2075
- const end = Math.min(i + matchWindow + 1, s2.length);
2076
- for (let j = start; j < end; j++) {
2077
- if (s2Matches[j] || s1[i] !== s2[j]) continue;
2078
- s1Matches[i] = true;
2079
- s2Matches[j] = true;
2080
- matches++;
2081
- break;
2082
- }
2083
- }
2084
- if (matches === 0) return 0;
2085
- let k = 0;
2086
- for (let i = 0; i < s1.length; i++) {
2087
- if (!s1Matches[i]) continue;
2088
- while (!s2Matches[k]) k++;
2089
- if (s1[i] !== s2[k]) transpositions++;
2090
- k++;
2091
- }
2092
- const jaro = (matches / s1.length + matches / s2.length + (matches - transpositions / 2) / matches) / 3;
2093
- let prefix = 0;
2094
- for (let i = 0; i < Math.min(4, Math.min(s1.length, s2.length)); i++) {
2095
- if (s1[i] === s2[i]) {
2096
- prefix++;
2097
- } else {
2098
- break;
2099
- }
2100
- }
2101
- const WINKLER_SCALING = 0.1;
2102
- return jaro + prefix * WINKLER_SCALING * (1 - jaro);
2103
- }
2104
- function stringSimilarity(a, b) {
2105
- if (a.length === 0 || b.length === 0) return 0;
2106
- const lowerA = a.toLowerCase();
2107
- const lowerB = b.toLowerCase();
2108
- if (lowerA === lowerB) return 1;
2109
- const jw = jaroWinkler(a, b);
2110
- let containsBonus = 0;
2111
- if (lowerB.includes(lowerA)) {
2112
- containsBonus = 0.2;
2113
- } else if (lowerA.includes(lowerB)) {
2114
- containsBonus = 0.1;
2115
- }
2116
- return Math.min(1, jw + containsBonus);
2117
- }
2118
- function scoreElement(query, element) {
2119
- const lowerQuery = query.toLowerCase();
2120
- const words = lowerQuery.split(/\s+/).filter((w) => w.length > 0);
2121
- let nameScore = 0;
2122
- if (element.name) {
2123
- const lowerName = element.name.toLowerCase();
2124
- if (lowerName === lowerQuery) {
2125
- nameScore = 1;
2126
- } else if (lowerName.includes(lowerQuery)) {
2127
- nameScore = 0.8;
2128
- } else if (words.length > 0) {
2129
- const matchedWords = words.filter((w) => lowerName.includes(w));
2130
- nameScore = matchedWords.length / words.length * 0.7;
2131
- } else {
2132
- nameScore = stringSimilarity(query, element.name) * 0.6;
2133
- }
2134
- }
2135
- let roleScore = 0;
2136
- const lowerRole = element.role.toLowerCase();
2137
- if (lowerRole === lowerQuery || lowerQuery.includes(lowerRole)) {
2138
- roleScore = 0.3;
2139
- } else if (words.some((w) => lowerRole.includes(w))) {
2140
- roleScore = 0.2;
2141
- }
2142
- let selectorScore = 0;
2143
- const lowerSelector = element.selector.toLowerCase();
2144
- if (words.some((w) => lowerSelector.includes(w))) {
2145
- selectorScore = 0.2;
2146
- }
2147
- const totalScore = nameScore * 0.6 + roleScore * 0.25 + selectorScore * 0.15;
2148
- return totalScore;
2149
- }
2150
- function explainMatch(query, element, score) {
2151
- const reasons = [];
2152
- const lowerQuery = query.toLowerCase();
2153
- const words = lowerQuery.split(/\s+/).filter((w) => w.length > 0);
2154
- if (element.name) {
2155
- const lowerName = element.name.toLowerCase();
2156
- if (lowerName === lowerQuery) {
2157
- reasons.push("exact name match");
2158
- } else if (lowerName.includes(lowerQuery)) {
2159
- reasons.push("name contains query");
2160
- } else if (words.some((w) => lowerName.includes(w))) {
2161
- const matchedWords = words.filter((w) => lowerName.includes(w));
2162
- reasons.push(`name contains: ${matchedWords.join(", ")}`);
2163
- } else if (stringSimilarity(query, element.name) > 0.5) {
2164
- reasons.push("similar name");
2165
- }
2166
- }
2167
- const lowerRole = element.role.toLowerCase();
2168
- if (lowerRole === lowerQuery || words.some((w) => w === lowerRole)) {
2169
- reasons.push(`role: ${element.role}`);
2170
- }
2171
- if (words.some((w) => element.selector.toLowerCase().includes(w))) {
2172
- reasons.push("selector match");
2173
- }
2174
- if (reasons.length === 0) {
2175
- reasons.push(`fuzzy match (score: ${score.toFixed(2)})`);
2176
- }
2177
- return reasons.join(", ");
2178
- }
2179
- function fuzzyMatchElements(query, elements, maxResults = 5) {
2180
- if (!query || query.length === 0) {
2181
- return [];
2182
- }
2183
- const THRESHOLD = 0.3;
2184
- const scored = elements.map((element) => ({
2185
- element,
2186
- score: scoreElement(query, element)
2187
- }));
2188
- return scored.filter((s) => s.score >= THRESHOLD).sort((a, b) => b.score - a.score).slice(0, maxResults).map((s) => ({
2189
- element: s.element,
2190
- score: s.score,
2191
- matchReason: explainMatch(query, s.element, s.score)
2192
- }));
2193
- }
2194
-
2195
- // src/browser/hint-generator.ts
2196
- var ACTION_ROLE_MAP = {
2197
- click: ["button", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "tab", "option"],
2198
- fill: ["textbox", "searchbox", "textarea"],
2199
- type: ["textbox", "searchbox", "textarea"],
2200
- submit: ["button", "form"],
2201
- select: ["combobox", "listbox", "option"],
2202
- check: ["checkbox", "radio", "switch"],
2203
- uncheck: ["checkbox", "switch"],
2204
- focus: [],
2205
- // Any focusable element
2206
- hover: [],
2207
- // Any element
2208
- clear: ["textbox", "searchbox", "textarea"]
2209
- };
2210
- function extractIntent(selectors) {
2211
- const patterns = [];
2212
- let text = "";
2213
- for (const selector of selectors) {
2214
- if (selector.startsWith("ref:")) {
2215
- continue;
2216
- }
2217
- const idMatch = selector.match(/#([a-zA-Z0-9_-]+)/);
2218
- if (idMatch) {
2219
- patterns.push(idMatch[1]);
2220
- }
2221
- const ariaMatch = selector.match(/\[aria-label=["']([^"']+)["']\]/);
2222
- if (ariaMatch) {
2223
- patterns.push(ariaMatch[1]);
2224
- }
2225
- const testidMatch = selector.match(/\[data-testid=["']([^"']+)["']\]/);
2226
- if (testidMatch) {
2227
- patterns.push(testidMatch[1]);
2228
- }
2229
- const classMatch = selector.match(/\.([a-zA-Z0-9_-]+)/);
2230
- if (classMatch) {
2231
- patterns.push(classMatch[1]);
2232
- }
2233
- }
2234
- patterns.sort((a, b) => b.length - a.length);
2235
- text = patterns[0] ?? selectors[0] ?? "";
2236
- return { text, patterns };
2237
- }
2238
- function getHintType(selector) {
2239
- if (selector.startsWith("ref:")) return "ref";
2240
- if (selector.includes("data-testid")) return "testid";
2241
- if (selector.includes("aria-label")) return "aria";
2242
- if (selector.startsWith("#")) return "id";
2243
- return "css";
2244
- }
2245
- function getConfidence(score) {
2246
- if (score >= 0.8) return "high";
2247
- if (score >= 0.5) return "medium";
2248
- return "low";
2249
- }
2250
- function diversifyHints(candidates, maxHints) {
2251
- const hints = [];
2252
- const usedTypes = /* @__PURE__ */ new Set();
2253
- for (const candidate of candidates) {
2254
- if (hints.length >= maxHints) break;
2255
- const refSelector = `ref:${candidate.element.ref}`;
2256
- const hintType = getHintType(refSelector);
2257
- if (!usedTypes.has(hintType)) {
2258
- hints.push({
2259
- selector: refSelector,
2260
- reason: candidate.matchReason,
2261
- confidence: getConfidence(candidate.score),
2262
- element: {
2263
- ref: candidate.element.ref,
2264
- role: candidate.element.role,
2265
- name: candidate.element.name,
2266
- disabled: candidate.element.disabled
2267
- }
2268
- });
2269
- usedTypes.add(hintType);
2270
- } else if (hints.length < maxHints) {
2271
- hints.push({
2272
- selector: refSelector,
2273
- reason: candidate.matchReason,
2274
- confidence: getConfidence(candidate.score),
2275
- element: {
2276
- ref: candidate.element.ref,
2277
- role: candidate.element.role,
2278
- name: candidate.element.name,
2279
- disabled: candidate.element.disabled
2280
- }
2281
- });
2282
- }
2283
- }
2284
- return hints;
2285
- }
2286
- async function generateHints(page, failedSelectors, actionType, maxHints = 3) {
2287
- let snapshot;
2288
- try {
2289
- snapshot = await page.snapshot();
2290
- } catch {
2291
- return [];
2292
- }
2293
- const intent = extractIntent(failedSelectors);
2294
- const roleFilter = ACTION_ROLE_MAP[actionType] ?? [];
2295
- let candidates = snapshot.interactiveElements;
2296
- if (roleFilter.length > 0) {
2297
- candidates = candidates.filter((el) => roleFilter.includes(el.role));
2298
- }
2299
- const matches = fuzzyMatchElements(intent.text, candidates, maxHints * 2);
2300
- if (matches.length === 0) {
2301
- return [];
2302
- }
2303
- return diversifyHints(matches, maxHints);
2304
- }
2305
-
2306
2090
  // src/browser/keyboard.ts
2307
2091
  var US_KEYBOARD = {
2308
2092
  // Letters (lowercase)
@@ -2420,6 +2204,47 @@ var US_KEYBOARD = {
2420
2204
  PageUp: { key: "PageUp", code: "PageUp", keyCode: 33 },
2421
2205
  PageDown: { key: "PageDown", code: "PageDown", keyCode: 34 }
2422
2206
  };
2207
+ var MODIFIER_CODES = {
2208
+ Control: "ControlLeft",
2209
+ Shift: "ShiftLeft",
2210
+ Alt: "AltLeft",
2211
+ Meta: "MetaLeft"
2212
+ };
2213
+ var MODIFIER_KEY_CODES = {
2214
+ Control: 17,
2215
+ Shift: 16,
2216
+ Alt: 18,
2217
+ Meta: 91
2218
+ };
2219
+ function computeModifierBitmask(modifiers) {
2220
+ let mask = 0;
2221
+ if (modifiers.includes("Alt")) mask |= 1;
2222
+ if (modifiers.includes("Control")) mask |= 2;
2223
+ if (modifiers.includes("Meta")) mask |= 4;
2224
+ if (modifiers.includes("Shift")) mask |= 8;
2225
+ return mask;
2226
+ }
2227
+ function parseShortcut(combo) {
2228
+ const parts = combo.split("+");
2229
+ if (parts.length < 2) {
2230
+ throw new Error(
2231
+ `Invalid shortcut "${combo}": must contain at least one modifier and a key (e.g. "Control+a").`
2232
+ );
2233
+ }
2234
+ const key = parts[parts.length - 1];
2235
+ const modifiers = [];
2236
+ const validModifiers = new Set(Object.keys(MODIFIER_CODES));
2237
+ for (let i = 0; i < parts.length - 1; i++) {
2238
+ const mod = parts[i];
2239
+ if (!validModifiers.has(mod)) {
2240
+ throw new Error(
2241
+ `Invalid modifier "${mod}" in shortcut "${combo}". Valid modifiers: ${[...validModifiers].join(", ")}`
2242
+ );
2243
+ }
2244
+ modifiers.push(mod);
2245
+ }
2246
+ return { modifiers, key };
2247
+ }
2423
2248
 
2424
2249
  // src/browser/page.ts
2425
2250
  var DEFAULT_TIMEOUT = 3e4;
@@ -2498,6 +2323,8 @@ var Page = class {
2498
2323
  frameExecutionContexts = /* @__PURE__ */ new Map();
2499
2324
  /** Current frame's execution context ID (null = main frame default) */
2500
2325
  currentFrameContextId = null;
2326
+ /** Frame selector if context acquisition failed (cross-origin/sandboxed) */
2327
+ brokenFrame = null;
2501
2328
  /** Last matched selector from findElement (for selectorUsed tracking) */
2502
2329
  _lastMatchedSelector;
2503
2330
  /** Last snapshot for stale ref recovery */
@@ -2553,7 +2380,9 @@ var Page = class {
2553
2380
  }
2554
2381
  }
2555
2382
  });
2556
- this.cdp.on("Page.javascriptDialogOpening", this.handleDialogOpening.bind(this));
2383
+ this.cdp.on("Page.javascriptDialogOpening", (params) => {
2384
+ void this.handleDialogOpening(params);
2385
+ });
2557
2386
  await Promise.all([
2558
2387
  this.cdp.send("Page.enable"),
2559
2388
  this.cdp.send("DOM.enable"),
@@ -2680,6 +2509,9 @@ var Page = class {
2680
2509
  timeout: options.timeout ?? DEFAULT_TIMEOUT
2681
2510
  });
2682
2511
  } catch (e) {
2512
+ if (e instanceof ActionabilityError && e.failureType === "hitTarget" && await this.tryClickAssociatedLabel(objectId)) {
2513
+ return true;
2514
+ }
2683
2515
  if (options.optional) return false;
2684
2516
  throw e;
2685
2517
  }
@@ -2703,14 +2535,24 @@ var Page = class {
2703
2535
  clickY = box.content[1] + box.height / 2;
2704
2536
  }
2705
2537
  const hitTargetCoordinates = this.currentFrame ? void 0 : { x: clickX, y: clickY };
2706
- try {
2707
- await ensureActionable(this.cdp, objectId, ["hitTarget"], {
2708
- timeout: options.timeout ?? DEFAULT_TIMEOUT,
2709
- coordinates: hitTargetCoordinates
2710
- });
2711
- } catch (e) {
2712
- if (options.optional) return false;
2713
- throw e;
2538
+ const HIT_TARGET_RETRIES = 3;
2539
+ const HIT_TARGET_DELAY = 100;
2540
+ for (let attempt = 0; attempt < HIT_TARGET_RETRIES; attempt++) {
2541
+ try {
2542
+ await ensureActionable(this.cdp, objectId, ["hitTarget"], {
2543
+ timeout: options.timeout ?? DEFAULT_TIMEOUT,
2544
+ coordinates: hitTargetCoordinates
2545
+ });
2546
+ break;
2547
+ } catch (e) {
2548
+ if (options.optional) return false;
2549
+ if (e instanceof ActionabilityError && e.failureType === "hitTarget" && attempt < HIT_TARGET_RETRIES - 1) {
2550
+ await sleep3(HIT_TARGET_DELAY);
2551
+ await this.cdp.send("DOM.scrollIntoViewIfNeeded", { nodeId: element.nodeId });
2552
+ continue;
2553
+ }
2554
+ throw e;
2555
+ }
2714
2556
  }
2715
2557
  await this.clickElement(element.nodeId);
2716
2558
  return true;
@@ -2820,8 +2662,8 @@ var Page = class {
2820
2662
  if (options.optional) return false;
2821
2663
  throw new ElementNotFoundError(selector);
2822
2664
  }
2665
+ const objectId = await this.resolveObjectId(element.nodeId);
2823
2666
  try {
2824
- const objectId = await this.resolveObjectId(element.nodeId);
2825
2667
  await ensureActionable(this.cdp, objectId, ["visible", "enabled"], {
2826
2668
  timeout: options.timeout ?? DEFAULT_TIMEOUT
2827
2669
  });
@@ -2870,9 +2712,15 @@ var Page = class {
2870
2712
  await this.cdp.send("Input.insertText", { text: char });
2871
2713
  }
2872
2714
  if (delay > 0) {
2873
- await sleep4(delay);
2715
+ await sleep3(delay);
2874
2716
  }
2875
2717
  }
2718
+ if (options.blur) {
2719
+ await this.cdp.send("Runtime.callFunctionOn", {
2720
+ objectId,
2721
+ functionDeclaration: "function() { this.blur(); }"
2722
+ });
2723
+ }
2876
2724
  return true;
2877
2725
  });
2878
2726
  }
@@ -2952,8 +2800,12 @@ var Page = class {
2952
2800
  const { trigger, option, value, match = "text" } = config;
2953
2801
  return this.withStaleNodeRetry(async () => {
2954
2802
  await this.click(trigger, options);
2955
- await sleep4(100);
2956
2803
  const optionSelectors = Array.isArray(option) ? option : [option];
2804
+ await waitForAnyElement(this.cdp, optionSelectors, {
2805
+ state: "visible",
2806
+ timeout: 500,
2807
+ contextId: this.currentFrameContextId ?? void 0
2808
+ }).catch(() => sleep3(100));
2957
2809
  const optionHandle = await this.evaluateInFrame(
2958
2810
  `(() => {
2959
2811
  const selectors = ${JSON.stringify(optionSelectors)};
@@ -3048,7 +2900,12 @@ var Page = class {
3048
2900
  returnByValue: true
3049
2901
  });
3050
2902
  if (!after.result.value) {
3051
- 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
+ );
3052
2909
  }
3053
2910
  return true;
3054
2911
  });
@@ -3100,7 +2957,12 @@ var Page = class {
3100
2957
  returnByValue: true
3101
2958
  });
3102
2959
  if (after.result.value) {
3103
- 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
+ );
3104
2966
  }
3105
2967
  return true;
3106
2968
  });
@@ -3150,8 +3012,11 @@ var Page = class {
3150
3012
  await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT });
3151
3013
  } else if (shouldWait === "auto") {
3152
3014
  await Promise.race([
3153
- this.waitForNavigation({ timeout: 1e3, optional: true }),
3154
- sleep4(500)
3015
+ this.waitForNavigation({ timeout: 2e3, optional: true }).then(
3016
+ () => "navigation"
3017
+ ),
3018
+ this.waitForDOMMutation({ timeout: 1e3 }).then(() => "mutation"),
3019
+ sleep3(1500).then(() => "timeout")
3155
3020
  ]);
3156
3021
  }
3157
3022
  return true;
@@ -3167,10 +3032,11 @@ var Page = class {
3167
3032
  }
3168
3033
  } else if (shouldWait === "auto") {
3169
3034
  const navigationDetected = await Promise.race([
3170
- this.waitForNavigation({ timeout: 1e3, optional: true }).then(
3035
+ this.waitForNavigation({ timeout: 2e3, optional: true }).then(
3171
3036
  (success) => success ? "nav" : null
3172
3037
  ),
3173
- sleep4(500).then(() => "timeout")
3038
+ this.waitForDOMMutation({ timeout: 1e3 }).then(() => "mutation"),
3039
+ sleep3(1500).then(() => "timeout")
3174
3040
  ]);
3175
3041
  if (navigationDetected === "nav") {
3176
3042
  return true;
@@ -3184,17 +3050,29 @@ var Page = class {
3184
3050
  if (shouldWait === true) {
3185
3051
  await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT });
3186
3052
  } else if (shouldWait === "auto") {
3187
- await sleep4(100);
3053
+ await sleep3(100);
3188
3054
  }
3189
3055
  }
3190
3056
  return true;
3191
3057
  });
3192
3058
  }
3193
3059
  /**
3194
- * Press a key
3060
+ * Press a key, optionally with modifier keys held down
3195
3061
  */
3196
- async press(key) {
3197
- await this.dispatchKey(key);
3062
+ async press(key, options) {
3063
+ const modifiers = options?.modifiers;
3064
+ if (modifiers && modifiers.length > 0) {
3065
+ await this.dispatchKeyWithModifiers(key, modifiers);
3066
+ } else {
3067
+ await this.dispatchKey(key);
3068
+ }
3069
+ }
3070
+ /**
3071
+ * Execute a keyboard shortcut (e.g. "Control+a", "Meta+Shift+z")
3072
+ */
3073
+ async shortcut(combo) {
3074
+ const { modifiers, key } = parseShortcut(combo);
3075
+ await this.dispatchKeyWithModifiers(key, modifiers);
3198
3076
  }
3199
3077
  /**
3200
3078
  * Focus an element
@@ -3223,8 +3101,8 @@ var Page = class {
3223
3101
  throw new ElementNotFoundError(selector, hints);
3224
3102
  }
3225
3103
  await this.scrollIntoView(element.nodeId);
3104
+ const objectId = await this.resolveObjectId(element.nodeId);
3226
3105
  try {
3227
- const objectId = await this.resolveObjectId(element.nodeId);
3228
3106
  await ensureActionable(this.cdp, objectId, ["visible", "stable"], {
3229
3107
  timeout: options.timeout ?? DEFAULT_TIMEOUT
3230
3108
  });
@@ -3232,13 +3110,28 @@ var Page = class {
3232
3110
  if (options.optional) return false;
3233
3111
  throw e;
3234
3112
  }
3235
- const box = await this.getBoxModel(element.nodeId);
3236
- if (!box) {
3237
- if (options.optional) return false;
3238
- throw new Error("Could not get element box model");
3113
+ let x;
3114
+ let y;
3115
+ try {
3116
+ const { quads } = await this.cdp.send("DOM.getContentQuads", {
3117
+ objectId
3118
+ });
3119
+ if (quads?.length > 0) {
3120
+ const quad = quads[0];
3121
+ x = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
3122
+ y = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
3123
+ } else {
3124
+ throw new Error("No quads");
3125
+ }
3126
+ } catch {
3127
+ const box = await this.getBoxModel(element.nodeId);
3128
+ if (!box) {
3129
+ if (options.optional) return false;
3130
+ throw new Error("Could not get element position");
3131
+ }
3132
+ x = box.content[0] + box.width / 2;
3133
+ y = box.content[1] + box.height / 2;
3239
3134
  }
3240
- const x = box.content[0] + box.width / 2;
3241
- const y = box.content[1] + box.height / 2;
3242
3135
  await this.cdp.send("Input.dispatchMouseEvent", {
3243
3136
  type: "mouseMoved",
3244
3137
  x,
@@ -3296,15 +3189,19 @@ var Page = class {
3296
3189
  if (descResult.node.frameId) {
3297
3190
  const frameId = descResult.node.frameId;
3298
3191
  const { timeout = DEFAULT_TIMEOUT } = options;
3299
- const pollInterval = 50;
3300
- const deadline = Date.now() + timeout;
3301
3192
  let contextId = this.frameExecutionContexts.get(frameId);
3302
- while (!contextId && Date.now() < deadline) {
3303
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
3304
- contextId = this.frameExecutionContexts.get(frameId);
3193
+ if (!contextId) {
3194
+ contextId = await this.waitForFrameContext(frameId, Math.min(timeout, 2e3));
3305
3195
  }
3306
3196
  if (contextId) {
3307
3197
  this.currentFrameContextId = contextId;
3198
+ this.brokenFrame = null;
3199
+ } else {
3200
+ const frameKey2 = Array.isArray(selector) ? selector[0] : selector;
3201
+ this.brokenFrame = frameKey2;
3202
+ console.warn(
3203
+ `[browser-pilot] Frame "${frameKey2}" execution context unavailable. JS evaluation will fail in this frame. DOM operations may still work.`
3204
+ );
3308
3205
  }
3309
3206
  }
3310
3207
  this.refMap.clear();
@@ -3317,6 +3214,7 @@ var Page = class {
3317
3214
  this.currentFrame = null;
3318
3215
  this.rootNodeId = null;
3319
3216
  this.currentFrameContextId = null;
3217
+ this.brokenFrame = null;
3320
3218
  this.refMap.clear();
3321
3219
  }
3322
3220
  /**
@@ -3388,7 +3286,7 @@ var Page = class {
3388
3286
  }
3389
3287
  const result = await this.cdp.send("Runtime.evaluate", params);
3390
3288
  if (result.exceptionDetails) {
3391
- throw new Error(`Evaluation failed: ${result.exceptionDetails.text}`);
3289
+ throw new Error(this.formatEvaluationError(result.exceptionDetails));
3392
3290
  }
3393
3291
  return result.result.value;
3394
3292
  }
@@ -3440,6 +3338,75 @@ var Page = class {
3440
3338
  return result.result.value ?? "";
3441
3339
  });
3442
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
+ }
3443
3410
  // ============ File Handling ============
3444
3411
  /**
3445
3412
  * Set files on a file input
@@ -3786,11 +3753,22 @@ var Page = class {
3786
3753
 
3787
3754
  for (var i = 0; i < nodes.length; i++) {
3788
3755
  var currentTarget = nodes[i];
3756
+
3757
+ var phase = currentTarget === target ? 2 : capture ? 1 : 3;
3758
+
3759
+ // Invoke inline handler if present (e.g. onclick, oninput)
3760
+ var inlineHandler = currentTarget['on' + type];
3761
+ if (typeof inlineHandler === 'function') {
3762
+ var inlineEvent = createEvent(type, target, currentTarget, path, phase);
3763
+ inlineHandler.call(currentTarget, inlineEvent);
3764
+ invoked = true;
3765
+ if (inlineEvent.__stopped) break;
3766
+ }
3767
+
3789
3768
  var store = currentTarget && currentTarget.__bpEventListeners;
3790
3769
  var entries = store && store[type];
3791
3770
  if (!Array.isArray(entries) || entries.length === 0) continue;
3792
3771
 
3793
- var phase = currentTarget === target ? 2 : capture ? 1 : 3;
3794
3772
  var event = createEvent(type, target, currentTarget, path, phase);
3795
3773
 
3796
3774
  for (var j = 0; j < entries.length; j++) {
@@ -3901,7 +3879,8 @@ var Page = class {
3901
3879
  /**
3902
3880
  * Get an accessibility tree snapshot of the page
3903
3881
  */
3904
- async snapshot() {
3882
+ async snapshot(options = {}) {
3883
+ const roleFilter = new Set((options.roles ?? []).map((role) => role.trim().toLowerCase()));
3905
3884
  const [url, title, axTree] = await Promise.all([
3906
3885
  this.url(),
3907
3886
  this.title(),
@@ -3922,7 +3901,7 @@ var Page = class {
3922
3901
  const buildNode = (nodeId) => {
3923
3902
  const node = nodeMap.get(nodeId);
3924
3903
  if (!node) return null;
3925
- const role = node.role?.value ?? "generic";
3904
+ const role = (node.role?.value ?? "generic").toLowerCase();
3926
3905
  const name = node.name?.value;
3927
3906
  const value = node.value?.value;
3928
3907
  const ref = nodeRefs.get(nodeId);
@@ -3938,7 +3917,7 @@ var Page = class {
3938
3917
  return {
3939
3918
  role,
3940
3919
  name,
3941
- value,
3920
+ value: value !== void 0 ? String(value) : void 0,
3942
3921
  ref,
3943
3922
  children: children.length > 0 ? children : void 0,
3944
3923
  disabled,
@@ -3946,7 +3925,24 @@ var Page = class {
3946
3925
  };
3947
3926
  };
3948
3927
  const rootNodes = nodes.filter((n) => !n.parentId || !nodeMap.has(n.parentId));
3949
- 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
+ }
3950
3946
  const interactiveRoles = /* @__PURE__ */ new Set([
3951
3947
  "button",
3952
3948
  "link",
@@ -3968,37 +3964,44 @@ var Page = class {
3968
3964
  ]);
3969
3965
  const interactiveElements = [];
3970
3966
  for (const node of nodes) {
3971
- const role = node.role?.value;
3972
- if (role && interactiveRoles.has(role)) {
3967
+ const role = (node.role?.value ?? "").toLowerCase();
3968
+ if (role && interactiveRoles.has(role) && (roleFilter.size === 0 || roleFilter.has(role))) {
3973
3969
  const ref = nodeRefs.get(node.nodeId);
3974
3970
  const name = node.name?.value ?? "";
3975
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;
3976
3974
  const selector = node.backendDOMNodeId ? `[data-backend-node-id="${node.backendDOMNodeId}"]` : `[aria-label="${name}"]`;
3977
3975
  interactiveElements.push({
3978
3976
  ref,
3979
3977
  role,
3980
3978
  name,
3981
3979
  selector,
3982
- disabled
3980
+ disabled,
3981
+ checked,
3982
+ value: value !== void 0 ? String(value) : void 0
3983
3983
  });
3984
3984
  }
3985
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
+ };
3986
3994
  const formatTree = (nodes2, depth = 0) => {
3987
3995
  const lines = [];
3988
3996
  for (const node of nodes2) {
3989
- let line = `${" ".repeat(depth)}- ${node.role}`;
3990
- if (node.name) line += ` "${node.name}"`;
3991
- line += ` [ref=${node.ref}]`;
3992
- if (node.disabled) line += " (disabled)";
3993
- if (node.checked !== void 0) line += node.checked ? " (checked)" : " (unchecked)";
3994
- lines.push(line);
3997
+ lines.push(formatNode(node, depth));
3995
3998
  if (node.children) {
3996
3999
  lines.push(formatTree(node.children, depth + 1));
3997
4000
  }
3998
4001
  }
3999
4002
  return lines.join("\n");
4000
4003
  };
4001
- const text = formatTree(accessibilityTree);
4004
+ const text = roleFilter.size > 0 ? accessibilityTree.map((node) => formatNode(node)).join("\n") : formatTree(accessibilityTree);
4002
4005
  const result = {
4003
4006
  url,
4004
4007
  title,
@@ -4007,7 +4010,9 @@ var Page = class {
4007
4010
  interactiveElements,
4008
4011
  text
4009
4012
  };
4010
- this.lastSnapshot = result;
4013
+ if (roleFilter.size === 0) {
4014
+ this.lastSnapshot = result;
4015
+ }
4011
4016
  return result;
4012
4017
  }
4013
4018
  /**
@@ -4422,8 +4427,15 @@ var Page = class {
4422
4427
  }
4423
4428
  };
4424
4429
  if (this.dialogHandler) {
4430
+ const DIALOG_TIMEOUT = 5e3;
4425
4431
  try {
4426
- await this.dialogHandler(dialog);
4432
+ await Promise.race([
4433
+ this.dialogHandler(dialog),
4434
+ sleep3(DIALOG_TIMEOUT).then(() => {
4435
+ console.warn("[browser-pilot] Dialog handler timed out after 5s, auto-dismissing");
4436
+ return dialog.dismiss();
4437
+ })
4438
+ ]);
4427
4439
  } catch (e) {
4428
4440
  console.error("[Dialog handler error]", e);
4429
4441
  await dialog.dismiss();
@@ -4503,6 +4515,7 @@ var Page = class {
4503
4515
  this.refMap.clear();
4504
4516
  this.currentFrame = null;
4505
4517
  this.currentFrameContextId = null;
4518
+ this.brokenFrame = null;
4506
4519
  this.frameContexts.clear();
4507
4520
  this.dialogHandler = null;
4508
4521
  try {
@@ -4543,7 +4556,7 @@ var Page = class {
4543
4556
  if (attempt < retries) {
4544
4557
  this.rootNodeId = null;
4545
4558
  this.currentFrameContextId = null;
4546
- await sleep4(delay);
4559
+ await sleep3(delay);
4547
4560
  continue;
4548
4561
  }
4549
4562
  }
@@ -4554,7 +4567,7 @@ var Page = class {
4554
4567
  }
4555
4568
  /**
4556
4569
  * Find an element using single or multiple selectors
4557
- * Supports ref: prefix for ref-based selectors (e.g., "ref:e4")
4570
+ * Supports ref:, text:, and role: selectors.
4558
4571
  */
4559
4572
  async findElement(selectors, options = {}) {
4560
4573
  const { timeout = DEFAULT_TIMEOUT } = options;
@@ -4621,11 +4634,11 @@ var Page = class {
4621
4634
  }
4622
4635
  }
4623
4636
  }
4624
- const cssSelectors = selectorList.filter((s) => !s.startsWith("ref:"));
4625
- if (cssSelectors.length === 0) {
4637
+ const runtimeSelectors = selectorList.filter((s) => !s.startsWith("ref:"));
4638
+ if (runtimeSelectors.length === 0) {
4626
4639
  return null;
4627
4640
  }
4628
- const result = await waitForAnyElement(this.cdp, cssSelectors, {
4641
+ const result = await waitForAnyElement(this.cdp, runtimeSelectors, {
4629
4642
  state: "visible",
4630
4643
  timeout,
4631
4644
  contextId: this.currentFrameContextId ?? void 0
@@ -4633,6 +4646,14 @@ var Page = class {
4633
4646
  if (!result.success || !result.selector) {
4634
4647
  return null;
4635
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
+ }
4636
4657
  await this.ensureRootNode();
4637
4658
  const queryResult = await this.cdp.send("DOM.querySelector", {
4638
4659
  nodeId: this.rootNodeId,
@@ -4679,6 +4700,122 @@ var Page = class {
4679
4700
  waitedMs: result.waitedMs
4680
4701
  };
4681
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
+ }
4682
4819
  /**
4683
4820
  * Ensure we have a valid root node ID
4684
4821
  */
@@ -4702,11 +4839,7 @@ var Page = class {
4702
4839
  if (frameResult.node.frameId) {
4703
4840
  let contextId = this.frameExecutionContexts.get(frameResult.node.frameId);
4704
4841
  if (!contextId) {
4705
- for (let i = 0; i < 10; i++) {
4706
- await sleep4(100);
4707
- contextId = this.frameExecutionContexts.get(frameResult.node.frameId);
4708
- if (contextId) break;
4709
- }
4842
+ contextId = await this.waitForFrameContext(frameResult.node.frameId, 1e3);
4710
4843
  }
4711
4844
  this.currentFrameContextId = contextId ?? null;
4712
4845
  }
@@ -4726,6 +4859,11 @@ var Page = class {
4726
4859
  * Automatically injects contextId when in an iframe
4727
4860
  */
4728
4861
  async evaluateInFrame(expression, options = {}) {
4862
+ if (this.brokenFrame && this.currentFrame) {
4863
+ throw new Error(
4864
+ `Cannot evaluate JavaScript in frame "${this.brokenFrame}": execution context is unavailable (cross-origin or sandboxed iframe). DOM operations (click, fill, etc.) may still work via CDP.`
4865
+ );
4866
+ }
4729
4867
  const params = {
4730
4868
  expression,
4731
4869
  returnByValue: options.returnByValue ?? true,
@@ -4737,10 +4875,43 @@ var Page = class {
4737
4875
  return this.cdp.send("Runtime.evaluate", params);
4738
4876
  }
4739
4877
  /**
4740
- * Scroll an element into view
4878
+ * Scroll an element into view, with fallback to center-scroll if clipped by fixed headers
4741
4879
  */
4742
4880
  async scrollIntoView(nodeId) {
4743
4881
  await this.cdp.send("DOM.scrollIntoViewIfNeeded", { nodeId });
4882
+ if (!await this.isInViewport(nodeId)) {
4883
+ const objectId = await this.resolveObjectId(nodeId);
4884
+ await this.cdp.send("Runtime.callFunctionOn", {
4885
+ objectId,
4886
+ functionDeclaration: `function() { this.scrollIntoView({ block: 'center', inline: 'center' }); }`
4887
+ });
4888
+ }
4889
+ }
4890
+ /**
4891
+ * Check if element is within the visible viewport
4892
+ */
4893
+ async isInViewport(nodeId) {
4894
+ try {
4895
+ const objectId = await this.resolveObjectId(nodeId);
4896
+ const result = await this.cdp.send("Runtime.callFunctionOn", {
4897
+ objectId,
4898
+ functionDeclaration: `function() {
4899
+ var rect = this.getBoundingClientRect();
4900
+ return (
4901
+ rect.top >= 0 &&
4902
+ rect.left >= 0 &&
4903
+ rect.bottom <= window.innerHeight &&
4904
+ rect.right <= window.innerWidth &&
4905
+ rect.width > 0 &&
4906
+ rect.height > 0
4907
+ );
4908
+ }`,
4909
+ returnByValue: true
4910
+ });
4911
+ return result?.result?.value === true;
4912
+ } catch {
4913
+ return true;
4914
+ }
4744
4915
  }
4745
4916
  /**
4746
4917
  * Get element box model (position and dimensions)
@@ -4828,13 +4999,13 @@ var Page = class {
4828
4999
  });
4829
5000
  return object.objectId;
4830
5001
  }
4831
- async dispatchKeyDefinition(def) {
5002
+ async dispatchKeyDefinition(def, modifierBitmask = 0) {
4832
5003
  const downParams = {
4833
5004
  type: def.text !== void 0 ? "keyDown" : "rawKeyDown",
4834
5005
  key: def.key,
4835
5006
  code: def.code,
4836
5007
  windowsVirtualKeyCode: def.keyCode,
4837
- modifiers: 0,
5008
+ modifiers: modifierBitmask,
4838
5009
  autoRepeat: false,
4839
5010
  location: def.location ?? 0,
4840
5011
  isKeypad: false
@@ -4849,7 +5020,7 @@ var Page = class {
4849
5020
  key: def.key,
4850
5021
  code: def.code,
4851
5022
  windowsVirtualKeyCode: def.keyCode,
4852
- modifiers: 0,
5023
+ modifiers: modifierBitmask,
4853
5024
  location: def.location ?? 0
4854
5025
  });
4855
5026
  }
@@ -4859,12 +5030,44 @@ var Page = class {
4859
5030
  await this.dispatchKeyDefinition(def);
4860
5031
  return;
4861
5032
  }
4862
- if ([...key].length === 1) {
5033
+ if (key.length === 1) {
4863
5034
  await this.cdp.send("Input.insertText", { text: key });
4864
5035
  return;
4865
5036
  }
4866
5037
  await this.dispatchKeyDefinition({ key, code: key, keyCode: 0 });
4867
5038
  }
5039
+ async dispatchKeyWithModifiers(key, modifiers) {
5040
+ const mask = computeModifierBitmask(modifiers);
5041
+ for (const mod of modifiers) {
5042
+ await this.cdp.send("Input.dispatchKeyEvent", {
5043
+ type: "rawKeyDown",
5044
+ key: mod,
5045
+ code: MODIFIER_CODES[mod],
5046
+ windowsVirtualKeyCode: MODIFIER_KEY_CODES[mod],
5047
+ modifiers: mask,
5048
+ location: 1
5049
+ });
5050
+ }
5051
+ const def = US_KEYBOARD[key];
5052
+ if (def) {
5053
+ await this.dispatchKeyDefinition(def, mask);
5054
+ } else if (key.length === 1) {
5055
+ await this.dispatchKeyDefinition({ key, code: key, keyCode: 0, text: key }, mask);
5056
+ } else {
5057
+ await this.dispatchKeyDefinition({ key, code: key, keyCode: 0 }, mask);
5058
+ }
5059
+ for (let i = modifiers.length - 1; i >= 0; i--) {
5060
+ const mod = modifiers[i];
5061
+ await this.cdp.send("Input.dispatchKeyEvent", {
5062
+ type: "keyUp",
5063
+ key: mod,
5064
+ code: MODIFIER_CODES[mod],
5065
+ windowsVirtualKeyCode: MODIFIER_KEY_CODES[mod],
5066
+ modifiers: 0,
5067
+ location: 1
5068
+ });
5069
+ }
5070
+ }
4868
5071
  // ============ Audio I/O ============
4869
5072
  /**
4870
5073
  * Audio input controller (fake microphone).
@@ -4937,7 +5140,7 @@ var Page = class {
4937
5140
  const start = Date.now();
4938
5141
  await this.audioOutput.start();
4939
5142
  if (options.preDelay && options.preDelay > 0) {
4940
- await sleep4(options.preDelay);
5143
+ await sleep3(options.preDelay);
4941
5144
  }
4942
5145
  const inputDone = this.audioInput.play(options.input, {
4943
5146
  waitForEnd: !!options.sendSelector
@@ -4964,8 +5167,48 @@ var Page = class {
4964
5167
  totalMs: Date.now() - start
4965
5168
  };
4966
5169
  }
5170
+ /**
5171
+ * Wait for a DOM mutation in the current frame (used for detecting client-side form handling)
5172
+ */
5173
+ async waitForDOMMutation(options) {
5174
+ await this.evaluateInFrame(
5175
+ `new Promise((resolve) => {
5176
+ var observer = new MutationObserver(function() {
5177
+ observer.disconnect();
5178
+ resolve();
5179
+ });
5180
+ observer.observe(document.body, { childList: true, subtree: true });
5181
+ setTimeout(function() { observer.disconnect(); resolve(); }, ${options.timeout});
5182
+ })`
5183
+ );
5184
+ }
5185
+ /**
5186
+ * Wait for a frame execution context via Runtime.executionContextCreated event
5187
+ */
5188
+ async waitForFrameContext(frameId, timeout) {
5189
+ const existing = this.frameExecutionContexts.get(frameId);
5190
+ if (existing) return existing;
5191
+ return new Promise((resolve) => {
5192
+ const timer = setTimeout(() => {
5193
+ cleanup();
5194
+ resolve(void 0);
5195
+ }, timeout);
5196
+ const handler = (params) => {
5197
+ const context = params["context"];
5198
+ if (context.auxData?.frameId === frameId && context.auxData?.isDefault !== false) {
5199
+ cleanup();
5200
+ resolve(context.id);
5201
+ }
5202
+ };
5203
+ const cleanup = () => {
5204
+ clearTimeout(timer);
5205
+ this.cdp.off("Runtime.executionContextCreated", handler);
5206
+ };
5207
+ this.cdp.on("Runtime.executionContextCreated", handler);
5208
+ });
5209
+ }
4967
5210
  };
4968
- function sleep4(ms) {
5211
+ function sleep3(ms) {
4969
5212
  return new Promise((resolve) => setTimeout(resolve, ms));
4970
5213
  }
4971
5214
 
@@ -4990,6 +5233,7 @@ var Browser = class _Browser {
4990
5233
  cdp;
4991
5234
  providerSession;
4992
5235
  pages = /* @__PURE__ */ new Map();
5236
+ pageCounter = 0;
4993
5237
  constructor(cdp, _provider, providerSession, _options) {
4994
5238
  this.cdp = cdp;
4995
5239
  this.providerSession = providerSession;
@@ -5019,7 +5263,11 @@ var Browser = class _Browser {
5019
5263
  const pageName = name ?? "default";
5020
5264
  const cached = this.pages.get(pageName);
5021
5265
  if (cached) return cached;
5022
- const targets = await this.cdp.send("Target.getTargets");
5266
+ const targets = await this.cdp.send(
5267
+ "Target.getTargets",
5268
+ void 0,
5269
+ null
5270
+ );
5023
5271
  let pageTargets = targets.targetInfos.filter((t) => t.type === "page");
5024
5272
  if (options?.targetUrl) {
5025
5273
  const urlFilter = options.targetUrl;
@@ -5041,16 +5289,24 @@ var Browser = class _Browser {
5041
5289
  targetId = options.targetId;
5042
5290
  } else {
5043
5291
  console.warn(`[browser-pilot] Target ${options.targetId} no longer exists, falling back`);
5044
- targetId = pickBestTarget(pageTargets) ?? (await this.cdp.send("Target.createTarget", {
5045
- url: "about:blank"
5046
- })).targetId;
5292
+ targetId = pickBestTarget(pageTargets) ?? (await this.cdp.send(
5293
+ "Target.createTarget",
5294
+ {
5295
+ url: "about:blank"
5296
+ },
5297
+ null
5298
+ )).targetId;
5047
5299
  }
5048
5300
  } else if (pageTargets.length > 0) {
5049
5301
  targetId = pickBestTarget(pageTargets);
5050
5302
  } else {
5051
- const result = await this.cdp.send("Target.createTarget", {
5052
- url: "about:blank"
5053
- });
5303
+ const result = await this.cdp.send(
5304
+ "Target.createTarget",
5305
+ {
5306
+ url: "about:blank"
5307
+ },
5308
+ null
5309
+ );
5054
5310
  targetId = result.targetId;
5055
5311
  }
5056
5312
  await this.cdp.attachToTarget(targetId);
@@ -5078,13 +5334,17 @@ var Browser = class _Browser {
5078
5334
  * Create a new page (tab)
5079
5335
  */
5080
5336
  async newPage(url = "about:blank") {
5081
- const result = await this.cdp.send("Target.createTarget", {
5082
- url
5083
- });
5337
+ const result = await this.cdp.send(
5338
+ "Target.createTarget",
5339
+ {
5340
+ url
5341
+ },
5342
+ null
5343
+ );
5084
5344
  await this.cdp.attachToTarget(result.targetId);
5085
5345
  const page = new Page(this.cdp, result.targetId);
5086
5346
  await page.init();
5087
- const name = `page-${this.pages.size + 1}`;
5347
+ const name = `page-${++this.pageCounter}`;
5088
5348
  this.pages.set(name, page);
5089
5349
  return page;
5090
5350
  }
@@ -5094,14 +5354,30 @@ var Browser = class _Browser {
5094
5354
  async closePage(name) {
5095
5355
  const page = this.pages.get(name);
5096
5356
  if (!page) return;
5097
- const targets = await this.cdp.send("Target.getTargets");
5098
- const pageTargets = targets.targetInfos.filter((t) => t.type === "page");
5099
- if (pageTargets.length > 0) {
5100
- await this.cdp.send("Target.closeTarget", {
5101
- targetId: pageTargets[0].targetId
5102
- });
5103
- }
5357
+ const targetId = page.targetId;
5358
+ await this.cdp.send("Target.closeTarget", { targetId }, null);
5104
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");
5105
5381
  }
5106
5382
  /**
5107
5383
  * Get the WebSocket URL for this browser connection