browser-pilot 0.0.10 → 0.0.12

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-4MBSALQL.mjs";
4
4
  import {
5
5
  createProvider
6
- } from "./chunk-R3PS4PCM.mjs";
6
+ } from "./chunk-BRAFQUMG.mjs";
7
7
  import {
8
+ ActionabilityError,
8
9
  BatchExecutor,
9
10
  ElementNotFoundError,
10
- TimeoutError
11
- } from "./chunk-KKW2SZLV.mjs";
11
+ TimeoutError,
12
+ ensureActionable,
13
+ generateHints
14
+ } from "./chunk-NLIARNEE.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
@@ -1568,30 +1577,71 @@ async function isElementAttached(cdp, selector, contextId) {
1568
1577
  function sleep2(ms) {
1569
1578
  return new Promise((resolve) => setTimeout(resolve, ms));
1570
1579
  }
1580
+ async function isPageStatic(cdp, windowMs = 200, contextId) {
1581
+ const params = {
1582
+ expression: `new Promise(resolve => {
1583
+ // If page is still loading, it's not static
1584
+ if (document.readyState !== 'complete') { resolve(false); return; }
1585
+ // Check for recent page load (navigationStart within last 1s = page just loaded)
1586
+ try {
1587
+ var nav = performance.getEntriesByType('navigation')[0];
1588
+ if (nav && (performance.now() - nav.loadEventEnd) < 500) { resolve(false); return; }
1589
+ } catch(e) {}
1590
+ // Observe for DOM mutations
1591
+ var seen = false;
1592
+ var obs = new MutationObserver(function() { seen = true; });
1593
+ obs.observe(document.documentElement, { childList: true, subtree: true, attributes: true });
1594
+ setTimeout(function() { obs.disconnect(); resolve(!seen); }, ${windowMs});
1595
+ })`,
1596
+ returnByValue: true,
1597
+ awaitPromise: true
1598
+ };
1599
+ if (contextId !== void 0) params["contextId"] = contextId;
1600
+ try {
1601
+ const result = await cdp.send("Runtime.evaluate", params);
1602
+ return result.result.value === true;
1603
+ } catch {
1604
+ return false;
1605
+ }
1606
+ }
1571
1607
  async function waitForElement(cdp, selector, options = {}) {
1572
1608
  const { state = "visible", timeout = 3e4, pollInterval = 100, contextId } = options;
1573
1609
  const startTime = Date.now();
1574
1610
  const deadline = startTime + timeout;
1575
- while (Date.now() < deadline) {
1576
- let conditionMet = false;
1611
+ const checkCondition = async () => {
1577
1612
  switch (state) {
1578
1613
  case "visible":
1579
- conditionMet = await isElementVisible(cdp, selector, contextId);
1580
- break;
1614
+ return isElementVisible(cdp, selector, contextId);
1581
1615
  case "hidden":
1582
- conditionMet = !await isElementVisible(cdp, selector, contextId);
1583
- break;
1616
+ return !await isElementVisible(cdp, selector, contextId);
1584
1617
  case "attached":
1585
- conditionMet = await isElementAttached(cdp, selector, contextId);
1586
- break;
1618
+ return isElementAttached(cdp, selector, contextId);
1587
1619
  case "detached":
1588
- conditionMet = !await isElementAttached(cdp, selector, contextId);
1589
- break;
1620
+ return !await isElementAttached(cdp, selector, contextId);
1621
+ default: {
1622
+ const _exhaustive = state;
1623
+ throw new Error(`Unhandled wait state: ${_exhaustive}`);
1624
+ }
1590
1625
  }
1591
- if (conditionMet) {
1592
- return { success: true, waitedMs: Date.now() - startTime };
1626
+ };
1627
+ if (await checkCondition()) {
1628
+ return { success: true, waitedMs: Date.now() - startTime };
1629
+ }
1630
+ const waitingForPresence = state === "visible" || state === "attached";
1631
+ if (waitingForPresence && timeout >= 300) {
1632
+ const pageStatic = await isPageStatic(cdp, 200, contextId);
1633
+ if (pageStatic) {
1634
+ if (await checkCondition()) {
1635
+ return { success: true, waitedMs: Date.now() - startTime };
1636
+ }
1637
+ return { success: false, waitedMs: Date.now() - startTime };
1593
1638
  }
1639
+ }
1640
+ while (Date.now() < deadline) {
1594
1641
  await sleep2(pollInterval);
1642
+ if (await checkCondition()) {
1643
+ return { success: true, waitedMs: Date.now() - startTime };
1644
+ }
1595
1645
  }
1596
1646
  return { success: false, waitedMs: Date.now() - startTime };
1597
1647
  }
@@ -1599,28 +1649,46 @@ async function waitForAnyElement(cdp, selectors, options = {}) {
1599
1649
  const { state = "visible", timeout = 3e4, pollInterval = 100, contextId } = options;
1600
1650
  const startTime = Date.now();
1601
1651
  const deadline = startTime + timeout;
1652
+ const checkSelector = async (selector) => {
1653
+ switch (state) {
1654
+ case "visible":
1655
+ return isElementVisible(cdp, selector, contextId);
1656
+ case "hidden":
1657
+ return !await isElementVisible(cdp, selector, contextId);
1658
+ case "attached":
1659
+ return isElementAttached(cdp, selector, contextId);
1660
+ case "detached":
1661
+ return !await isElementAttached(cdp, selector, contextId);
1662
+ default: {
1663
+ const _exhaustive = state;
1664
+ throw new Error(`Unhandled wait state: ${_exhaustive}`);
1665
+ }
1666
+ }
1667
+ };
1668
+ for (const selector of selectors) {
1669
+ if (await checkSelector(selector)) {
1670
+ return { success: true, selector, waitedMs: Date.now() - startTime };
1671
+ }
1672
+ }
1673
+ const waitingForPresence = state === "visible" || state === "attached";
1674
+ if (waitingForPresence && timeout >= 300) {
1675
+ const pageStatic = await isPageStatic(cdp, 200, contextId);
1676
+ if (pageStatic) {
1677
+ for (const selector of selectors) {
1678
+ if (await checkSelector(selector)) {
1679
+ return { success: true, selector, waitedMs: Date.now() - startTime };
1680
+ }
1681
+ }
1682
+ return { success: false, waitedMs: Date.now() - startTime };
1683
+ }
1684
+ }
1602
1685
  while (Date.now() < deadline) {
1686
+ await sleep2(pollInterval);
1603
1687
  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) {
1688
+ if (await checkSelector(selector)) {
1620
1689
  return { success: true, selector, waitedMs: Date.now() - startTime };
1621
1690
  }
1622
1691
  }
1623
- await sleep2(pollInterval);
1624
1692
  }
1625
1693
  return { success: false, waitedMs: Date.now() - startTime };
1626
1694
  }
@@ -1667,6 +1735,13 @@ async function waitForNavigation(cdp, options = {}) {
1667
1735
  cdp.on("Page.navigatedWithinDocument", onSameDoc);
1668
1736
  cleanup.push(() => cdp.off("Page.navigatedWithinDocument", onSameDoc));
1669
1737
  }
1738
+ const onLifecycle = (params) => {
1739
+ if (params["name"] === "networkIdle") {
1740
+ done(true);
1741
+ }
1742
+ };
1743
+ cdp.on("Page.lifecycleEvent", onLifecycle);
1744
+ cleanup.push(() => cdp.off("Page.lifecycleEvent", onLifecycle));
1670
1745
  const pollUrl = async () => {
1671
1746
  while (!resolved && Date.now() < startTime + timeout) {
1672
1747
  await sleep2(100);
@@ -1681,7 +1756,7 @@ async function waitForNavigation(cdp, options = {}) {
1681
1756
  }
1682
1757
  }
1683
1758
  };
1684
- pollUrl();
1759
+ void pollUrl();
1685
1760
  });
1686
1761
  }
1687
1762
  async function waitForNetworkIdle(cdp, options = {}) {
@@ -1729,253 +1804,221 @@ async function waitForNetworkIdle(cdp, options = {}) {
1729
1804
  });
1730
1805
  }
1731
1806
 
1732
- // src/browser/fuzzy-match.ts
1733
- function jaroWinkler(a, b) {
1734
- if (a.length === 0 && b.length === 0) return 0;
1735
- if (a.length === 0 || b.length === 0) return 0;
1736
- if (a === b) return 1;
1737
- const s1 = a.toLowerCase();
1738
- const s2 = b.toLowerCase();
1739
- const matchWindow = Math.max(0, Math.floor(Math.max(s1.length, s2.length) / 2) - 1);
1740
- const s1Matches = new Array(s1.length).fill(false);
1741
- const s2Matches = new Array(s2.length).fill(false);
1742
- let matches = 0;
1743
- let transpositions = 0;
1744
- for (let i = 0; i < s1.length; i++) {
1745
- const start = Math.max(0, i - matchWindow);
1746
- const end = Math.min(i + matchWindow + 1, s2.length);
1747
- for (let j = start; j < end; j++) {
1748
- if (s2Matches[j] || s1[i] !== s2[j]) continue;
1749
- s1Matches[i] = true;
1750
- s2Matches[j] = true;
1751
- matches++;
1752
- break;
1753
- }
1754
- }
1755
- if (matches === 0) return 0;
1756
- let k = 0;
1757
- for (let i = 0; i < s1.length; i++) {
1758
- if (!s1Matches[i]) continue;
1759
- while (!s2Matches[k]) k++;
1760
- if (s1[i] !== s2[k]) transpositions++;
1761
- k++;
1762
- }
1763
- const jaro = (matches / s1.length + matches / s2.length + (matches - transpositions / 2) / matches) / 3;
1764
- let prefix = 0;
1765
- for (let i = 0; i < Math.min(4, Math.min(s1.length, s2.length)); i++) {
1766
- if (s1[i] === s2[i]) {
1767
- prefix++;
1768
- } else {
1769
- break;
1770
- }
1771
- }
1772
- const WINKLER_SCALING = 0.1;
1773
- return jaro + prefix * WINKLER_SCALING * (1 - jaro);
1774
- }
1775
- function stringSimilarity(a, b) {
1776
- if (a.length === 0 || b.length === 0) return 0;
1777
- const lowerA = a.toLowerCase();
1778
- const lowerB = b.toLowerCase();
1779
- if (lowerA === lowerB) return 1;
1780
- const jw = jaroWinkler(a, b);
1781
- let containsBonus = 0;
1782
- if (lowerB.includes(lowerA)) {
1783
- containsBonus = 0.2;
1784
- } else if (lowerA.includes(lowerB)) {
1785
- containsBonus = 0.1;
1786
- }
1787
- return Math.min(1, jw + containsBonus);
1807
+ // src/browser/keyboard.ts
1808
+ var US_KEYBOARD = {
1809
+ // Letters (lowercase)
1810
+ a: { key: "a", code: "KeyA", keyCode: 65, text: "a" },
1811
+ b: { key: "b", code: "KeyB", keyCode: 66, text: "b" },
1812
+ c: { key: "c", code: "KeyC", keyCode: 67, text: "c" },
1813
+ d: { key: "d", code: "KeyD", keyCode: 68, text: "d" },
1814
+ e: { key: "e", code: "KeyE", keyCode: 69, text: "e" },
1815
+ f: { key: "f", code: "KeyF", keyCode: 70, text: "f" },
1816
+ g: { key: "g", code: "KeyG", keyCode: 71, text: "g" },
1817
+ h: { key: "h", code: "KeyH", keyCode: 72, text: "h" },
1818
+ i: { key: "i", code: "KeyI", keyCode: 73, text: "i" },
1819
+ j: { key: "j", code: "KeyJ", keyCode: 74, text: "j" },
1820
+ k: { key: "k", code: "KeyK", keyCode: 75, text: "k" },
1821
+ l: { key: "l", code: "KeyL", keyCode: 76, text: "l" },
1822
+ m: { key: "m", code: "KeyM", keyCode: 77, text: "m" },
1823
+ n: { key: "n", code: "KeyN", keyCode: 78, text: "n" },
1824
+ o: { key: "o", code: "KeyO", keyCode: 79, text: "o" },
1825
+ p: { key: "p", code: "KeyP", keyCode: 80, text: "p" },
1826
+ q: { key: "q", code: "KeyQ", keyCode: 81, text: "q" },
1827
+ r: { key: "r", code: "KeyR", keyCode: 82, text: "r" },
1828
+ s: { key: "s", code: "KeyS", keyCode: 83, text: "s" },
1829
+ t: { key: "t", code: "KeyT", keyCode: 84, text: "t" },
1830
+ u: { key: "u", code: "KeyU", keyCode: 85, text: "u" },
1831
+ v: { key: "v", code: "KeyV", keyCode: 86, text: "v" },
1832
+ w: { key: "w", code: "KeyW", keyCode: 87, text: "w" },
1833
+ x: { key: "x", code: "KeyX", keyCode: 88, text: "x" },
1834
+ y: { key: "y", code: "KeyY", keyCode: 89, text: "y" },
1835
+ z: { key: "z", code: "KeyZ", keyCode: 90, text: "z" },
1836
+ // Letters (uppercase)
1837
+ A: { key: "A", code: "KeyA", keyCode: 65, text: "A" },
1838
+ B: { key: "B", code: "KeyB", keyCode: 66, text: "B" },
1839
+ C: { key: "C", code: "KeyC", keyCode: 67, text: "C" },
1840
+ D: { key: "D", code: "KeyD", keyCode: 68, text: "D" },
1841
+ E: { key: "E", code: "KeyE", keyCode: 69, text: "E" },
1842
+ F: { key: "F", code: "KeyF", keyCode: 70, text: "F" },
1843
+ G: { key: "G", code: "KeyG", keyCode: 71, text: "G" },
1844
+ H: { key: "H", code: "KeyH", keyCode: 72, text: "H" },
1845
+ I: { key: "I", code: "KeyI", keyCode: 73, text: "I" },
1846
+ J: { key: "J", code: "KeyJ", keyCode: 74, text: "J" },
1847
+ K: { key: "K", code: "KeyK", keyCode: 75, text: "K" },
1848
+ L: { key: "L", code: "KeyL", keyCode: 76, text: "L" },
1849
+ M: { key: "M", code: "KeyM", keyCode: 77, text: "M" },
1850
+ N: { key: "N", code: "KeyN", keyCode: 78, text: "N" },
1851
+ O: { key: "O", code: "KeyO", keyCode: 79, text: "O" },
1852
+ P: { key: "P", code: "KeyP", keyCode: 80, text: "P" },
1853
+ Q: { key: "Q", code: "KeyQ", keyCode: 81, text: "Q" },
1854
+ R: { key: "R", code: "KeyR", keyCode: 82, text: "R" },
1855
+ S: { key: "S", code: "KeyS", keyCode: 83, text: "S" },
1856
+ T: { key: "T", code: "KeyT", keyCode: 84, text: "T" },
1857
+ U: { key: "U", code: "KeyU", keyCode: 85, text: "U" },
1858
+ V: { key: "V", code: "KeyV", keyCode: 86, text: "V" },
1859
+ W: { key: "W", code: "KeyW", keyCode: 87, text: "W" },
1860
+ X: { key: "X", code: "KeyX", keyCode: 88, text: "X" },
1861
+ Y: { key: "Y", code: "KeyY", keyCode: 89, text: "Y" },
1862
+ Z: { key: "Z", code: "KeyZ", keyCode: 90, text: "Z" },
1863
+ // Numbers
1864
+ "0": { key: "0", code: "Digit0", keyCode: 48, text: "0" },
1865
+ "1": { key: "1", code: "Digit1", keyCode: 49, text: "1" },
1866
+ "2": { key: "2", code: "Digit2", keyCode: 50, text: "2" },
1867
+ "3": { key: "3", code: "Digit3", keyCode: 51, text: "3" },
1868
+ "4": { key: "4", code: "Digit4", keyCode: 52, text: "4" },
1869
+ "5": { key: "5", code: "Digit5", keyCode: 53, text: "5" },
1870
+ "6": { key: "6", code: "Digit6", keyCode: 54, text: "6" },
1871
+ "7": { key: "7", code: "Digit7", keyCode: 55, text: "7" },
1872
+ "8": { key: "8", code: "Digit8", keyCode: 56, text: "8" },
1873
+ "9": { key: "9", code: "Digit9", keyCode: 57, text: "9" },
1874
+ // Punctuation
1875
+ " ": { key: " ", code: "Space", keyCode: 32, text: " " },
1876
+ ".": { key: ".", code: "Period", keyCode: 190, text: "." },
1877
+ ",": { key: ",", code: "Comma", keyCode: 188, text: "," },
1878
+ "/": { key: "/", code: "Slash", keyCode: 191, text: "/" },
1879
+ ";": { key: ";", code: "Semicolon", keyCode: 186, text: ";" },
1880
+ "'": { key: "'", code: "Quote", keyCode: 222, text: "'" },
1881
+ "[": { key: "[", code: "BracketLeft", keyCode: 219, text: "[" },
1882
+ "]": { key: "]", code: "BracketRight", keyCode: 221, text: "]" },
1883
+ "\\": { key: "\\", code: "Backslash", keyCode: 220, text: "\\" },
1884
+ "-": { key: "-", code: "Minus", keyCode: 189, text: "-" },
1885
+ "=": { key: "=", code: "Equal", keyCode: 187, text: "=" },
1886
+ "`": { key: "`", code: "Backquote", keyCode: 192, text: "`" },
1887
+ // Shifted punctuation
1888
+ "!": { key: "!", code: "Digit1", keyCode: 49, text: "!" },
1889
+ "@": { key: "@", code: "Digit2", keyCode: 50, text: "@" },
1890
+ "#": { key: "#", code: "Digit3", keyCode: 51, text: "#" },
1891
+ $: { key: "$", code: "Digit4", keyCode: 52, text: "$" },
1892
+ "%": { key: "%", code: "Digit5", keyCode: 53, text: "%" },
1893
+ "^": { key: "^", code: "Digit6", keyCode: 54, text: "^" },
1894
+ "&": { key: "&", code: "Digit7", keyCode: 55, text: "&" },
1895
+ "*": { key: "*", code: "Digit8", keyCode: 56, text: "*" },
1896
+ "(": { key: "(", code: "Digit9", keyCode: 57, text: "(" },
1897
+ ")": { key: ")", code: "Digit0", keyCode: 48, text: ")" },
1898
+ _: { key: "_", code: "Minus", keyCode: 189, text: "_" },
1899
+ "+": { key: "+", code: "Equal", keyCode: 187, text: "+" },
1900
+ "{": { key: "{", code: "BracketLeft", keyCode: 219, text: "{" },
1901
+ "}": { key: "}", code: "BracketRight", keyCode: 221, text: "}" },
1902
+ "|": { key: "|", code: "Backslash", keyCode: 220, text: "|" },
1903
+ ":": { key: ":", code: "Semicolon", keyCode: 186, text: ":" },
1904
+ '"': { key: '"', code: "Quote", keyCode: 222, text: '"' },
1905
+ "<": { key: "<", code: "Comma", keyCode: 188, text: "<" },
1906
+ ">": { key: ">", code: "Period", keyCode: 190, text: ">" },
1907
+ "?": { key: "?", code: "Slash", keyCode: 191, text: "?" },
1908
+ "~": { key: "~", code: "Backquote", keyCode: 192, text: "~" },
1909
+ // Special keys (non-text: use rawKeyDown, no text field)
1910
+ Enter: { key: "Enter", code: "Enter", keyCode: 13 },
1911
+ Tab: { key: "Tab", code: "Tab", keyCode: 9 },
1912
+ Backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
1913
+ Delete: { key: "Delete", code: "Delete", keyCode: 46 },
1914
+ Escape: { key: "Escape", code: "Escape", keyCode: 27 },
1915
+ ArrowUp: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
1916
+ ArrowDown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
1917
+ ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
1918
+ ArrowRight: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 },
1919
+ Home: { key: "Home", code: "Home", keyCode: 36 },
1920
+ End: { key: "End", code: "End", keyCode: 35 },
1921
+ PageUp: { key: "PageUp", code: "PageUp", keyCode: 33 },
1922
+ PageDown: { key: "PageDown", code: "PageDown", keyCode: 34 }
1923
+ };
1924
+ var MODIFIER_CODES = {
1925
+ Control: "ControlLeft",
1926
+ Shift: "ShiftLeft",
1927
+ Alt: "AltLeft",
1928
+ Meta: "MetaLeft"
1929
+ };
1930
+ var MODIFIER_KEY_CODES = {
1931
+ Control: 17,
1932
+ Shift: 16,
1933
+ Alt: 18,
1934
+ Meta: 91
1935
+ };
1936
+ function computeModifierBitmask(modifiers) {
1937
+ let mask = 0;
1938
+ if (modifiers.includes("Alt")) mask |= 1;
1939
+ if (modifiers.includes("Control")) mask |= 2;
1940
+ if (modifiers.includes("Meta")) mask |= 4;
1941
+ if (modifiers.includes("Shift")) mask |= 8;
1942
+ return mask;
1788
1943
  }
1789
- function scoreElement(query, element) {
1790
- const lowerQuery = query.toLowerCase();
1791
- const words = lowerQuery.split(/\s+/).filter((w) => w.length > 0);
1792
- let nameScore = 0;
1793
- if (element.name) {
1794
- const lowerName = element.name.toLowerCase();
1795
- if (lowerName === lowerQuery) {
1796
- nameScore = 1;
1797
- } else if (lowerName.includes(lowerQuery)) {
1798
- nameScore = 0.8;
1799
- } else if (words.length > 0) {
1800
- const matchedWords = words.filter((w) => lowerName.includes(w));
1801
- nameScore = matchedWords.length / words.length * 0.7;
1802
- } else {
1803
- nameScore = stringSimilarity(query, element.name) * 0.6;
1804
- }
1805
- }
1806
- let roleScore = 0;
1807
- const lowerRole = element.role.toLowerCase();
1808
- if (lowerRole === lowerQuery || lowerQuery.includes(lowerRole)) {
1809
- roleScore = 0.3;
1810
- } else if (words.some((w) => lowerRole.includes(w))) {
1811
- roleScore = 0.2;
1812
- }
1813
- let selectorScore = 0;
1814
- const lowerSelector = element.selector.toLowerCase();
1815
- if (words.some((w) => lowerSelector.includes(w))) {
1816
- selectorScore = 0.2;
1944
+ function parseShortcut(combo) {
1945
+ const parts = combo.split("+");
1946
+ if (parts.length < 2) {
1947
+ throw new Error(
1948
+ `Invalid shortcut "${combo}": must contain at least one modifier and a key (e.g. "Control+a").`
1949
+ );
1817
1950
  }
1818
- const totalScore = nameScore * 0.6 + roleScore * 0.25 + selectorScore * 0.15;
1819
- return totalScore;
1820
- }
1821
- function explainMatch(query, element, score) {
1822
- const reasons = [];
1823
- const lowerQuery = query.toLowerCase();
1824
- const words = lowerQuery.split(/\s+/).filter((w) => w.length > 0);
1825
- if (element.name) {
1826
- const lowerName = element.name.toLowerCase();
1827
- if (lowerName === lowerQuery) {
1828
- reasons.push("exact name match");
1829
- } else if (lowerName.includes(lowerQuery)) {
1830
- reasons.push("name contains query");
1831
- } else if (words.some((w) => lowerName.includes(w))) {
1832
- const matchedWords = words.filter((w) => lowerName.includes(w));
1833
- reasons.push(`name contains: ${matchedWords.join(", ")}`);
1834
- } else if (stringSimilarity(query, element.name) > 0.5) {
1835
- reasons.push("similar name");
1836
- }
1837
- }
1838
- const lowerRole = element.role.toLowerCase();
1839
- if (lowerRole === lowerQuery || words.some((w) => w === lowerRole)) {
1840
- reasons.push(`role: ${element.role}`);
1841
- }
1842
- if (words.some((w) => element.selector.toLowerCase().includes(w))) {
1843
- reasons.push("selector match");
1844
- }
1845
- if (reasons.length === 0) {
1846
- reasons.push(`fuzzy match (score: ${score.toFixed(2)})`);
1847
- }
1848
- return reasons.join(", ");
1849
- }
1850
- function fuzzyMatchElements(query, elements, maxResults = 5) {
1851
- if (!query || query.length === 0) {
1852
- return [];
1853
- }
1854
- const THRESHOLD = 0.3;
1855
- const scored = elements.map((element) => ({
1856
- element,
1857
- score: scoreElement(query, element)
1858
- }));
1859
- return scored.filter((s) => s.score >= THRESHOLD).sort((a, b) => b.score - a.score).slice(0, maxResults).map((s) => ({
1860
- element: s.element,
1861
- score: s.score,
1862
- matchReason: explainMatch(query, s.element, s.score)
1863
- }));
1864
- }
1865
-
1866
- // src/browser/hint-generator.ts
1867
- var ACTION_ROLE_MAP = {
1868
- click: ["button", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "tab", "option"],
1869
- fill: ["textbox", "searchbox", "textarea"],
1870
- type: ["textbox", "searchbox", "textarea"],
1871
- submit: ["button", "form"],
1872
- select: ["combobox", "listbox", "option"],
1873
- check: ["checkbox", "radio", "switch"],
1874
- uncheck: ["checkbox", "switch"],
1875
- focus: [],
1876
- // Any focusable element
1877
- hover: [],
1878
- // Any element
1879
- clear: ["textbox", "searchbox", "textarea"]
1880
- };
1881
- function extractIntent(selectors) {
1882
- const patterns = [];
1883
- let text = "";
1884
- for (const selector of selectors) {
1885
- if (selector.startsWith("ref:")) {
1886
- continue;
1887
- }
1888
- const idMatch = selector.match(/#([a-zA-Z0-9_-]+)/);
1889
- if (idMatch) {
1890
- patterns.push(idMatch[1]);
1891
- }
1892
- const ariaMatch = selector.match(/\[aria-label=["']([^"']+)["']\]/);
1893
- if (ariaMatch) {
1894
- patterns.push(ariaMatch[1]);
1895
- }
1896
- const testidMatch = selector.match(/\[data-testid=["']([^"']+)["']\]/);
1897
- if (testidMatch) {
1898
- patterns.push(testidMatch[1]);
1899
- }
1900
- const classMatch = selector.match(/\.([a-zA-Z0-9_-]+)/);
1901
- if (classMatch) {
1902
- patterns.push(classMatch[1]);
1951
+ const key = parts[parts.length - 1];
1952
+ const modifiers = [];
1953
+ const validModifiers = new Set(Object.keys(MODIFIER_CODES));
1954
+ for (let i = 0; i < parts.length - 1; i++) {
1955
+ const mod = parts[i];
1956
+ if (!validModifiers.has(mod)) {
1957
+ throw new Error(
1958
+ `Invalid modifier "${mod}" in shortcut "${combo}". Valid modifiers: ${[...validModifiers].join(", ")}`
1959
+ );
1903
1960
  }
1961
+ modifiers.push(mod);
1904
1962
  }
1905
- patterns.sort((a, b) => b.length - a.length);
1906
- text = patterns[0] ?? selectors[0] ?? "";
1907
- return { text, patterns };
1963
+ return { modifiers, key };
1908
1964
  }
1909
- function getHintType(selector) {
1910
- if (selector.startsWith("ref:")) return "ref";
1911
- if (selector.includes("data-testid")) return "testid";
1912
- if (selector.includes("aria-label")) return "aria";
1913
- if (selector.startsWith("#")) return "id";
1914
- return "css";
1915
- }
1916
- function getConfidence(score) {
1917
- if (score >= 0.8) return "high";
1918
- if (score >= 0.5) return "medium";
1919
- return "low";
1920
- }
1921
- function diversifyHints(candidates, maxHints) {
1922
- const hints = [];
1923
- const usedTypes = /* @__PURE__ */ new Set();
1924
- for (const candidate of candidates) {
1925
- if (hints.length >= maxHints) break;
1926
- const refSelector = `ref:${candidate.element.ref}`;
1927
- const hintType = getHintType(refSelector);
1928
- if (!usedTypes.has(hintType)) {
1929
- hints.push({
1930
- selector: refSelector,
1931
- reason: candidate.matchReason,
1932
- confidence: getConfidence(candidate.score),
1933
- element: {
1934
- ref: candidate.element.ref,
1935
- role: candidate.element.role,
1936
- name: candidate.element.name,
1937
- disabled: candidate.element.disabled
1938
- }
1939
- });
1940
- usedTypes.add(hintType);
1941
- } else if (hints.length < maxHints) {
1942
- hints.push({
1943
- selector: refSelector,
1944
- reason: candidate.matchReason,
1945
- confidence: getConfidence(candidate.score),
1946
- element: {
1947
- ref: candidate.element.ref,
1948
- role: candidate.element.role,
1949
- name: candidate.element.name,
1950
- disabled: candidate.element.disabled
1951
- }
1965
+
1966
+ // src/browser/page.ts
1967
+ var DEFAULT_TIMEOUT = 3e4;
1968
+ var EVENT_LISTENER_TRACKER_SCRIPT = `(() => {
1969
+ if (globalThis.__bpEventListenerTrackerInstalled) return;
1970
+ Object.defineProperty(globalThis, '__bpEventListenerTrackerInstalled', {
1971
+ value: true,
1972
+ configurable: true,
1973
+ });
1974
+
1975
+ const storeKey = '__bpEventListeners';
1976
+ const originalAddEventListener = EventTarget.prototype.addEventListener;
1977
+ const originalRemoveEventListener = EventTarget.prototype.removeEventListener;
1978
+
1979
+ function ensureStore(target) {
1980
+ if (!Object.prototype.hasOwnProperty.call(target, storeKey)) {
1981
+ Object.defineProperty(target, storeKey, {
1982
+ value: Object.create(null),
1983
+ configurable: true,
1952
1984
  });
1953
1985
  }
1986
+ return target[storeKey];
1954
1987
  }
1955
- return hints;
1956
- }
1957
- async function generateHints(page, failedSelectors, actionType, maxHints = 3) {
1958
- let snapshot;
1959
- try {
1960
- snapshot = await page.snapshot();
1961
- } catch {
1962
- return [];
1963
- }
1964
- const intent = extractIntent(failedSelectors);
1965
- const roleFilter = ACTION_ROLE_MAP[actionType] ?? [];
1966
- let candidates = snapshot.interactiveElements;
1967
- if (roleFilter.length > 0) {
1968
- candidates = candidates.filter((el) => roleFilter.includes(el.role));
1969
- }
1970
- const matches = fuzzyMatchElements(intent.text, candidates, maxHints * 2);
1971
- if (matches.length === 0) {
1972
- return [];
1973
- }
1974
- return diversifyHints(matches, maxHints);
1975
- }
1976
1988
 
1977
- // src/browser/page.ts
1978
- var DEFAULT_TIMEOUT = 3e4;
1989
+ EventTarget.prototype.addEventListener = function(type, listener, options) {
1990
+ try {
1991
+ if (listener) {
1992
+ const store = ensureStore(this);
1993
+ const bucket = store[type] || (store[type] = []);
1994
+ const capture =
1995
+ typeof options === 'boolean' ? options : !!(options && options.capture);
1996
+ const exists = bucket.some((entry) => entry.listener === listener && entry.capture === capture);
1997
+ if (!exists) {
1998
+ bucket.push({ listener, capture });
1999
+ }
2000
+ }
2001
+ } catch {}
2002
+
2003
+ return originalAddEventListener.call(this, type, listener, options);
2004
+ };
2005
+
2006
+ EventTarget.prototype.removeEventListener = function(type, listener, options) {
2007
+ try {
2008
+ const store = this[storeKey];
2009
+ const bucket = store && store[type];
2010
+ const capture =
2011
+ typeof options === 'boolean' ? options : !!(options && options.capture);
2012
+ if (Array.isArray(bucket)) {
2013
+ store[type] = bucket.filter((entry) => {
2014
+ return !(entry.listener === listener && entry.capture === capture);
2015
+ });
2016
+ }
2017
+ } catch {}
2018
+
2019
+ return originalRemoveEventListener.call(this, type, listener, options);
2020
+ };
2021
+ })();`;
1979
2022
  var Page = class {
1980
2023
  cdp;
1981
2024
  _targetId;
@@ -1997,8 +2040,12 @@ var Page = class {
1997
2040
  frameExecutionContexts = /* @__PURE__ */ new Map();
1998
2041
  /** Current frame's execution context ID (null = main frame default) */
1999
2042
  currentFrameContextId = null;
2043
+ /** Frame selector if context acquisition failed (cross-origin/sandboxed) */
2044
+ brokenFrame = null;
2000
2045
  /** Last matched selector from findElement (for selectorUsed tracking) */
2001
2046
  _lastMatchedSelector;
2047
+ /** Last snapshot for stale ref recovery */
2048
+ lastSnapshot;
2002
2049
  /** Audio input controller (lazy-initialized) */
2003
2050
  _audioInput;
2004
2051
  /** Audio output controller (lazy-initialized) */
@@ -2043,17 +2090,34 @@ var Page = class {
2043
2090
  for (const [frameId, ctxId] of this.frameExecutionContexts.entries()) {
2044
2091
  if (ctxId === contextId) {
2045
2092
  this.frameExecutionContexts.delete(frameId);
2093
+ if (this.currentFrameContextId === contextId) {
2094
+ this.currentFrameContextId = null;
2095
+ }
2046
2096
  break;
2047
2097
  }
2048
2098
  }
2049
2099
  });
2050
- this.cdp.on("Page.javascriptDialogOpening", this.handleDialogOpening.bind(this));
2100
+ this.cdp.on("Page.javascriptDialogOpening", (params) => {
2101
+ void this.handleDialogOpening(params);
2102
+ });
2051
2103
  await Promise.all([
2052
2104
  this.cdp.send("Page.enable"),
2053
2105
  this.cdp.send("DOM.enable"),
2054
2106
  this.cdp.send("Runtime.enable"),
2055
2107
  this.cdp.send("Network.enable")
2056
2108
  ]);
2109
+ await this.installEventListenerTracker();
2110
+ }
2111
+ async installEventListenerTracker() {
2112
+ await this.cdp.send("Page.addScriptToEvaluateOnNewDocument", {
2113
+ source: EVENT_LISTENER_TRACKER_SCRIPT
2114
+ });
2115
+ try {
2116
+ await this.cdp.send("Runtime.evaluate", {
2117
+ expression: EVENT_LISTENER_TRACKER_SCRIPT
2118
+ });
2119
+ } catch {
2120
+ }
2057
2121
  }
2058
2122
  // ============ Navigation ============
2059
2123
  /**
@@ -2069,6 +2133,9 @@ var Page = class {
2069
2133
  }
2070
2134
  this.rootNodeId = null;
2071
2135
  this.refMap.clear();
2136
+ this.currentFrame = null;
2137
+ this.currentFrameContextId = null;
2138
+ this.frameContexts.clear();
2072
2139
  }
2073
2140
  /**
2074
2141
  * Get the current URL
@@ -2139,8 +2206,9 @@ var Page = class {
2139
2206
  /**
2140
2207
  * Click an element (supports multi-selector)
2141
2208
  *
2142
- * Uses CDP mouse events for regular elements. For form submit buttons,
2143
- * uses dispatchEvent to reliably trigger form submission in headless Chrome.
2209
+ * Uses CDP mouse events (mouseMoved + mousePressed + mouseReleased) to
2210
+ * simulate a real click. Real mouse events on submit buttons naturally
2211
+ * trigger native form submission — no JS dispatch needed.
2144
2212
  */
2145
2213
  async click(selector, options = {}) {
2146
2214
  return this.withStaleNodeRetry(async () => {
@@ -2152,27 +2220,55 @@ var Page = class {
2152
2220
  throw new ElementNotFoundError(selector, hints);
2153
2221
  }
2154
2222
  await this.scrollIntoView(element.nodeId);
2155
- const submitResult = await this.evaluateInFrame(
2156
- `(() => {
2157
- const el = document.querySelector(${JSON.stringify(element.selector)});
2158
- if (!el) return { isSubmit: false };
2159
-
2160
- // Check if this is a form submit button
2161
- const isSubmitButton = (el instanceof HTMLButtonElement && (el.type === 'submit' || (el.form && el.type !== 'button'))) ||
2162
- (el instanceof HTMLInputElement && el.type === 'submit');
2163
-
2164
- if (isSubmitButton && el.form) {
2165
- // Dispatch submit event directly - works reliably in headless Chrome
2166
- el.form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
2167
- return { isSubmit: true };
2223
+ const objectId = await this.resolveObjectId(element.nodeId);
2224
+ try {
2225
+ await ensureActionable(this.cdp, objectId, ["visible", "enabled", "stable"], {
2226
+ timeout: options.timeout ?? DEFAULT_TIMEOUT
2227
+ });
2228
+ } catch (e) {
2229
+ if (options.optional) return false;
2230
+ throw e;
2231
+ }
2232
+ let clickX;
2233
+ let clickY;
2234
+ try {
2235
+ const { quads } = await this.cdp.send("DOM.getContentQuads", {
2236
+ objectId
2237
+ });
2238
+ if (quads?.length > 0) {
2239
+ const quad = quads[0];
2240
+ clickX = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
2241
+ clickY = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
2242
+ } else {
2243
+ throw new Error("No quads");
2244
+ }
2245
+ } catch {
2246
+ const box = await this.getBoxModel(element.nodeId);
2247
+ if (!box) throw new Error("Could not get element position");
2248
+ clickX = box.content[0] + box.width / 2;
2249
+ clickY = box.content[1] + box.height / 2;
2250
+ }
2251
+ const hitTargetCoordinates = this.currentFrame ? void 0 : { x: clickX, y: clickY };
2252
+ const HIT_TARGET_RETRIES = 3;
2253
+ const HIT_TARGET_DELAY = 100;
2254
+ for (let attempt = 0; attempt < HIT_TARGET_RETRIES; attempt++) {
2255
+ try {
2256
+ await ensureActionable(this.cdp, objectId, ["hitTarget"], {
2257
+ timeout: options.timeout ?? DEFAULT_TIMEOUT,
2258
+ coordinates: hitTargetCoordinates
2259
+ });
2260
+ break;
2261
+ } catch (e) {
2262
+ if (options.optional) return false;
2263
+ if (e instanceof ActionabilityError && e.failureType === "hitTarget" && attempt < HIT_TARGET_RETRIES - 1) {
2264
+ await sleep3(HIT_TARGET_DELAY);
2265
+ await this.cdp.send("DOM.scrollIntoViewIfNeeded", { nodeId: element.nodeId });
2266
+ continue;
2168
2267
  }
2169
- return { isSubmit: false };
2170
- })()`
2171
- );
2172
- const isSubmit = submitResult.result.value?.isSubmit;
2173
- if (!isSubmit) {
2174
- await this.clickElement(element.nodeId);
2268
+ throw e;
2269
+ }
2175
2270
  }
2271
+ await this.clickElement(element.nodeId);
2176
2272
  return true;
2177
2273
  });
2178
2274
  }
@@ -2180,7 +2276,7 @@ var Page = class {
2180
2276
  * Fill an input field (clears first by default)
2181
2277
  */
2182
2278
  async fill(selector, value, options = {}) {
2183
- const { clear = true, blur = false } = options;
2279
+ const { blur = false } = options;
2184
2280
  return this.withStaleNodeRetry(async () => {
2185
2281
  const element = await this.findElement(selector, options);
2186
2282
  if (!element) {
@@ -2189,71 +2285,158 @@ var Page = class {
2189
2285
  const hints = await generateHints(this, selectorList, "fill");
2190
2286
  throw new ElementNotFoundError(selector, hints);
2191
2287
  }
2192
- await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
2193
- if (clear) {
2194
- await this.evaluateInFrame(
2195
- `(() => {
2196
- const el = document.querySelector(${JSON.stringify(element.selector)});
2197
- if (el) {
2198
- el.value = '';
2199
- el.dispatchEvent(new InputEvent('input', {
2200
- bubbles: true,
2201
- cancelable: true,
2202
- inputType: 'deleteContent'
2203
- }));
2204
- }
2205
- })()`
2206
- );
2288
+ const { object } = await this.cdp.send("DOM.resolveNode", {
2289
+ nodeId: element.nodeId
2290
+ });
2291
+ const objectId = object.objectId;
2292
+ try {
2293
+ await ensureActionable(this.cdp, objectId, ["visible", "enabled", "editable"], {
2294
+ timeout: options.timeout ?? DEFAULT_TIMEOUT
2295
+ });
2296
+ } catch (e) {
2297
+ if (options.optional) return false;
2298
+ throw e;
2207
2299
  }
2208
- await this.cdp.send("Input.insertText", { text: value });
2209
- await this.evaluateInFrame(
2210
- `(() => {
2211
- const el = document.querySelector(${JSON.stringify(element.selector)});
2212
- if (el) {
2213
- el.dispatchEvent(new InputEvent('input', {
2214
- bubbles: true,
2215
- cancelable: true,
2216
- inputType: 'insertText',
2217
- data: ${JSON.stringify(value)}
2218
- }));
2219
- el.dispatchEvent(new Event('change', { bubbles: true }));
2300
+ const tagInfo = await this.cdp.send("Runtime.callFunctionOn", {
2301
+ objectId,
2302
+ functionDeclaration: `function() {
2303
+ return { tagName: this.tagName?.toLowerCase() || '', inputType: (this.type || '').toLowerCase() };
2304
+ }`,
2305
+ returnByValue: true
2306
+ });
2307
+ const { tagName, inputType } = tagInfo.result.value;
2308
+ const specialInputTypes = /* @__PURE__ */ new Set([
2309
+ "date",
2310
+ "datetime-local",
2311
+ "month",
2312
+ "week",
2313
+ "time",
2314
+ "color",
2315
+ "range",
2316
+ "file"
2317
+ ]);
2318
+ const isSpecialInput = tagName === "input" && specialInputTypes.has(inputType);
2319
+ if (isSpecialInput) {
2320
+ await this.cdp.send("Runtime.callFunctionOn", {
2321
+ objectId,
2322
+ functionDeclaration: `function(val) {
2323
+ this.value = val;
2324
+ this.dispatchEvent(new Event('input', { bubbles: true }));
2325
+ this.dispatchEvent(new Event('change', { bubbles: true }));
2326
+ }`,
2327
+ arguments: [{ value }],
2328
+ returnByValue: true
2329
+ });
2330
+ } else {
2331
+ await this.selectEditableContent(objectId);
2332
+ if (value === "") {
2333
+ await this.dispatchKey("Delete");
2334
+ } else {
2335
+ await this.cdp.send("Input.insertText", { text: value });
2336
+ }
2337
+ }
2338
+ if (options.verify !== false) {
2339
+ let actualValue = await this.readEditableValue(objectId);
2340
+ if (actualValue !== value && !isSpecialInput) {
2341
+ if (value === "") {
2342
+ await this.clearEditableSelection(objectId, "Backspace");
2343
+ } else {
2344
+ await this.typeEditableFallback(element.nodeId, objectId, value);
2220
2345
  }
2221
- })()`
2222
- );
2346
+ actualValue = await this.readEditableValue(objectId);
2347
+ }
2348
+ if (actualValue !== value) {
2349
+ if (options.optional) return false;
2350
+ throw new Error(
2351
+ `Fill value did not stick. Expected ${JSON.stringify(value)} but got ${JSON.stringify(actualValue)}.`
2352
+ );
2353
+ }
2354
+ }
2223
2355
  if (blur) {
2224
- await this.evaluateInFrame(
2225
- `document.querySelector(${JSON.stringify(element.selector)})?.blur()`
2226
- );
2356
+ await this.cdp.send("Runtime.callFunctionOn", {
2357
+ objectId,
2358
+ functionDeclaration: "function() { this.blur(); }"
2359
+ });
2227
2360
  }
2228
2361
  return true;
2229
2362
  });
2230
2363
  }
2231
2364
  /**
2232
2365
  * Type text character by character (for autocomplete fields, etc.)
2366
+ *
2367
+ * Uses proper keyDown/rawKeyDown distinction with US keyboard layout.
2368
+ * Printable chars use 'keyDown' with text, non-text keys use 'rawKeyDown',
2369
+ * and non-layout chars (emoji, CJK) fall back to Input.insertText.
2233
2370
  */
2234
2371
  async type(selector, text, options = {}) {
2235
- const { delay = 50 } = options;
2236
- const element = await this.findElement(selector, options);
2237
- if (!element) {
2238
- if (options.optional) return false;
2239
- throw new ElementNotFoundError(selector);
2240
- }
2241
- await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
2242
- for (const char of text) {
2243
- await this.cdp.send("Input.dispatchKeyEvent", {
2244
- type: "keyDown",
2245
- key: char,
2246
- text: char
2247
- });
2248
- await this.cdp.send("Input.dispatchKeyEvent", {
2249
- type: "keyUp",
2250
- key: char
2251
- });
2252
- if (delay > 0) {
2253
- await sleep3(delay);
2372
+ return this.withStaleNodeRetry(async () => {
2373
+ const { delay = 50 } = options;
2374
+ const element = await this.findElement(selector, options);
2375
+ if (!element) {
2376
+ if (options.optional) return false;
2377
+ throw new ElementNotFoundError(selector);
2254
2378
  }
2255
- }
2256
- return true;
2379
+ const objectId = await this.resolveObjectId(element.nodeId);
2380
+ try {
2381
+ await ensureActionable(this.cdp, objectId, ["visible", "enabled"], {
2382
+ timeout: options.timeout ?? DEFAULT_TIMEOUT
2383
+ });
2384
+ } catch (e) {
2385
+ if (options.optional) return false;
2386
+ throw e;
2387
+ }
2388
+ await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
2389
+ for (const char of text) {
2390
+ const def = US_KEYBOARD[char];
2391
+ if (def) {
2392
+ if (def.text !== void 0) {
2393
+ await this.cdp.send("Input.dispatchKeyEvent", {
2394
+ type: "keyDown",
2395
+ key: def.key,
2396
+ code: def.code,
2397
+ text: def.text,
2398
+ unmodifiedText: def.text,
2399
+ windowsVirtualKeyCode: def.keyCode,
2400
+ modifiers: 0,
2401
+ autoRepeat: false,
2402
+ location: def.location ?? 0,
2403
+ isKeypad: false
2404
+ });
2405
+ } else {
2406
+ await this.cdp.send("Input.dispatchKeyEvent", {
2407
+ type: "rawKeyDown",
2408
+ key: def.key,
2409
+ code: def.code,
2410
+ windowsVirtualKeyCode: def.keyCode,
2411
+ modifiers: 0,
2412
+ autoRepeat: false,
2413
+ location: def.location ?? 0,
2414
+ isKeypad: false
2415
+ });
2416
+ }
2417
+ await this.cdp.send("Input.dispatchKeyEvent", {
2418
+ type: "keyUp",
2419
+ key: def.key,
2420
+ code: def.code,
2421
+ windowsVirtualKeyCode: def.keyCode,
2422
+ modifiers: 0,
2423
+ location: def.location ?? 0
2424
+ });
2425
+ } else {
2426
+ await this.cdp.send("Input.insertText", { text: char });
2427
+ }
2428
+ if (delay > 0) {
2429
+ await sleep3(delay);
2430
+ }
2431
+ }
2432
+ if (options.blur) {
2433
+ await this.cdp.send("Runtime.callFunctionOn", {
2434
+ objectId,
2435
+ functionDeclaration: "function() { this.blur(); }"
2436
+ });
2437
+ }
2438
+ return true;
2439
+ });
2257
2440
  }
2258
2441
  async select(selectorOrConfig, valueOrOptions, maybeOptions) {
2259
2442
  if (typeof selectorOrConfig === "object" && !Array.isArray(selectorOrConfig) && "trigger" in selectorOrConfig) {
@@ -2262,108 +2445,231 @@ var Page = class {
2262
2445
  const selector = selectorOrConfig;
2263
2446
  const value = valueOrOptions;
2264
2447
  const options = maybeOptions ?? {};
2265
- const element = await this.findElement(selector, options);
2266
- if (!element) {
2267
- if (options.optional) return false;
2268
- const selectorList = Array.isArray(selector) ? selector : [selector];
2269
- const hints = await generateHints(this, selectorList, "select");
2270
- throw new ElementNotFoundError(selector, hints);
2271
- }
2272
- const values = Array.isArray(value) ? value : [value];
2273
- await this.cdp.send("Runtime.evaluate", {
2274
- expression: `(() => {
2275
- const el = document.querySelector(${JSON.stringify(element.selector)});
2276
- if (!el || el.tagName !== 'SELECT') return false;
2277
- const values = ${JSON.stringify(values)};
2278
- for (const opt of el.options) {
2279
- opt.selected = values.includes(opt.value) || values.includes(opt.text);
2280
- }
2281
- el.dispatchEvent(new Event('change', { bubbles: true }));
2448
+ return this.withStaleNodeRetry(async () => {
2449
+ const element = await this.findElement(selector, options);
2450
+ if (!element) {
2451
+ if (options.optional) return false;
2452
+ const selectorList = Array.isArray(selector) ? selector : [selector];
2453
+ const hints = await generateHints(this, selectorList, "select");
2454
+ throw new ElementNotFoundError(selector, hints);
2455
+ }
2456
+ const values = Array.isArray(value) ? value : [value];
2457
+ const objectId = await this.resolveObjectId(element.nodeId);
2458
+ try {
2459
+ await this.scrollIntoView(element.nodeId);
2460
+ await ensureActionable(this.cdp, objectId, ["visible", "enabled"], {
2461
+ timeout: options.timeout ?? DEFAULT_TIMEOUT
2462
+ });
2463
+ } catch (e) {
2464
+ if (options.optional) return false;
2465
+ throw e;
2466
+ }
2467
+ const metadata = await this.getNativeSelectMetadata(objectId, values);
2468
+ if (!metadata.isSelect) {
2469
+ throw new Error("select() target must be a native <select> element");
2470
+ }
2471
+ if (metadata.missing.length > 0) {
2472
+ throw new Error(`No option found for: ${metadata.missing.join(", ")}`);
2473
+ }
2474
+ if (metadata.disabled.length > 0) {
2475
+ throw new Error(`Cannot select disabled option(s): ${metadata.disabled.join(", ")}`);
2476
+ }
2477
+ if (!metadata.multiple && metadata.targetIndexes.length > 1) {
2478
+ throw new Error("Cannot select multiple values on a single-select element");
2479
+ }
2480
+ const expectedValues = metadata.targetIndexes.map((idx) => metadata.options[idx].value);
2481
+ if (this.selectValuesMatch(metadata.selectedValues, expectedValues, metadata.multiple)) {
2282
2482
  return true;
2283
- })()`,
2284
- returnByValue: true
2483
+ }
2484
+ if (!metadata.multiple && metadata.targetIndexes.length === 1) {
2485
+ await this.applyNativeSelectByKeyboard(
2486
+ element.nodeId,
2487
+ objectId,
2488
+ metadata.currentIndex,
2489
+ metadata.targetIndexes[0]
2490
+ );
2491
+ }
2492
+ let selectedValues = await this.readNativeSelectValues(objectId);
2493
+ if (!this.selectValuesMatch(selectedValues, expectedValues, metadata.multiple)) {
2494
+ await this.applyNativeSelectFallback(objectId, metadata.targetIndexes);
2495
+ selectedValues = await this.readNativeSelectValues(objectId);
2496
+ }
2497
+ if (!this.selectValuesMatch(selectedValues, expectedValues, metadata.multiple)) {
2498
+ await this.applyRecordedSelectFallback(objectId, metadata.targetIndexes);
2499
+ selectedValues = await this.readNativeSelectValues(objectId);
2500
+ }
2501
+ if (!this.selectValuesMatch(selectedValues, expectedValues, metadata.multiple)) {
2502
+ if (options.optional) return false;
2503
+ throw new Error(
2504
+ `Select value did not stick. Expected ${expectedValues.join(", ") || "(empty)"} but got ${selectedValues.join(", ") || "(empty)"}.`
2505
+ );
2506
+ }
2507
+ return true;
2285
2508
  });
2286
- return true;
2287
2509
  }
2288
2510
  /**
2289
2511
  * Handle custom (non-native) select/dropdown components
2290
2512
  */
2291
2513
  async selectCustom(config, options = {}) {
2292
2514
  const { trigger, option, value, match = "text" } = config;
2293
- await this.click(trigger, options);
2294
- await sleep3(100);
2295
- let optionSelector;
2296
- const optionSelectors = Array.isArray(option) ? option : [option];
2297
- if (match === "contains") {
2298
- optionSelector = optionSelectors.map((s) => `${s}:has-text("${value}")`).join(", ");
2299
- } else if (match === "value") {
2300
- optionSelector = optionSelectors.map((s) => `${s}[data-value="${value}"], ${s}[value="${value}"]`).join(", ");
2301
- } else {
2302
- optionSelector = optionSelectors.map((s) => `${s}`).join(", ");
2303
- }
2304
- const result = await this.cdp.send("Runtime.evaluate", {
2305
- expression: `(() => {
2306
- const options = document.querySelectorAll(${JSON.stringify(optionSelector)});
2307
- for (const opt of options) {
2308
- const text = opt.textContent?.trim();
2309
- if (${match === "text" ? `text === ${JSON.stringify(value)}` : match === "contains" ? `text?.includes(${JSON.stringify(value)})` : "true"}) {
2310
- opt.click();
2311
- return true;
2312
- }
2313
- }
2314
- return false;
2315
- })()`,
2316
- returnByValue: true
2515
+ return this.withStaleNodeRetry(async () => {
2516
+ await this.click(trigger, options);
2517
+ const optionSelectors = Array.isArray(option) ? option : [option];
2518
+ await waitForAnyElement(this.cdp, optionSelectors, {
2519
+ state: "visible",
2520
+ timeout: 500,
2521
+ contextId: this.currentFrameContextId ?? void 0
2522
+ }).catch(() => sleep3(100));
2523
+ const optionHandle = await this.evaluateInFrame(
2524
+ `(() => {
2525
+ const selectors = ${JSON.stringify(optionSelectors)};
2526
+ const wanted = ${JSON.stringify(value)};
2527
+ const mode = ${JSON.stringify(match)};
2528
+
2529
+ for (const selector of selectors) {
2530
+ const candidates = document.querySelectorAll(selector);
2531
+ for (const candidate of candidates) {
2532
+ const text = candidate.textContent?.trim() || '';
2533
+ const candidateValue =
2534
+ candidate.getAttribute?.('data-value') ??
2535
+ candidate.getAttribute?.('value') ??
2536
+ candidate.value ??
2537
+ '';
2538
+ const matches =
2539
+ mode === 'value'
2540
+ ? candidateValue === wanted
2541
+ : mode === 'contains'
2542
+ ? text.includes(wanted)
2543
+ : text === wanted;
2544
+
2545
+ if (matches) {
2546
+ return candidate;
2547
+ }
2548
+ }
2549
+ }
2550
+
2551
+ return null;
2552
+ })()`,
2553
+ { returnByValue: false }
2554
+ );
2555
+ if (!optionHandle.result.objectId) {
2556
+ if (options.optional) return false;
2557
+ throw new ElementNotFoundError(`Option with ${match} "${value}"`);
2558
+ }
2559
+ const nodeResult = await this.cdp.send("DOM.requestNode", {
2560
+ objectId: optionHandle.result.objectId
2561
+ });
2562
+ if (!nodeResult.nodeId) {
2563
+ if (options.optional) return false;
2564
+ throw new ElementNotFoundError(`Option with ${match} "${value}"`);
2565
+ }
2566
+ await this.scrollIntoView(nodeResult.nodeId);
2567
+ await ensureActionable(
2568
+ this.cdp,
2569
+ optionHandle.result.objectId,
2570
+ ["visible", "enabled", "stable"],
2571
+ {
2572
+ timeout: options.timeout ?? DEFAULT_TIMEOUT
2573
+ }
2574
+ );
2575
+ await this.clickElement(nodeResult.nodeId);
2576
+ return true;
2317
2577
  });
2318
- if (!result.result.value) {
2319
- if (options.optional) return false;
2320
- throw new ElementNotFoundError(`Option with ${match} "${value}"`);
2321
- }
2322
- return true;
2323
2578
  }
2324
2579
  /**
2325
- * Check a checkbox or radio button
2580
+ * Check a checkbox or radio button using real mouse click.
2581
+ * No-op if already checked. Verifies state changed after click.
2326
2582
  */
2327
2583
  async check(selector, options = {}) {
2328
- const element = await this.findElement(selector, options);
2329
- if (!element) {
2330
- if (options.optional) return false;
2331
- const selectorList = Array.isArray(selector) ? selector : [selector];
2332
- const hints = await generateHints(this, selectorList, "check");
2333
- throw new ElementNotFoundError(selector, hints);
2334
- }
2335
- const result = await this.cdp.send("Runtime.evaluate", {
2336
- expression: `(() => {
2337
- const el = document.querySelector(${JSON.stringify(element.selector)});
2338
- if (!el) return false;
2339
- if (!el.checked) el.click();
2340
- return true;
2341
- })()`,
2342
- returnByValue: true
2584
+ return this.withStaleNodeRetry(async () => {
2585
+ const element = await this.findElement(selector, options);
2586
+ if (!element) {
2587
+ if (options.optional) return false;
2588
+ const selectorList = Array.isArray(selector) ? selector : [selector];
2589
+ const hints = await generateHints(this, selectorList, "check");
2590
+ throw new ElementNotFoundError(selector, hints);
2591
+ }
2592
+ const { object } = await this.cdp.send("DOM.resolveNode", {
2593
+ nodeId: element.nodeId
2594
+ });
2595
+ try {
2596
+ await ensureActionable(this.cdp, object.objectId, ["visible", "enabled"], {
2597
+ timeout: options.timeout ?? DEFAULT_TIMEOUT
2598
+ });
2599
+ } catch (e) {
2600
+ if (options.optional) return false;
2601
+ throw e;
2602
+ }
2603
+ const before = await this.cdp.send("Runtime.callFunctionOn", {
2604
+ objectId: object.objectId,
2605
+ functionDeclaration: "function() { return !!this.checked; }",
2606
+ returnByValue: true
2607
+ });
2608
+ if (before.result.value) return true;
2609
+ await this.scrollIntoView(element.nodeId);
2610
+ await this.clickElement(element.nodeId);
2611
+ const after = await this.cdp.send("Runtime.callFunctionOn", {
2612
+ objectId: object.objectId,
2613
+ functionDeclaration: "function() { return !!this.checked; }",
2614
+ returnByValue: true
2615
+ });
2616
+ if (!after.result.value) {
2617
+ throw new Error("Clicking the checkbox did not change its state");
2618
+ }
2619
+ return true;
2343
2620
  });
2344
- return result.result.value;
2345
2621
  }
2346
2622
  /**
2347
- * Uncheck a checkbox
2623
+ * Uncheck a checkbox using real mouse click.
2624
+ * No-op if already unchecked. Radio buttons can't be unchecked (returns true).
2348
2625
  */
2349
2626
  async uncheck(selector, options = {}) {
2350
- const element = await this.findElement(selector, options);
2351
- if (!element) {
2352
- if (options.optional) return false;
2353
- const selectorList = Array.isArray(selector) ? selector : [selector];
2354
- const hints = await generateHints(this, selectorList, "uncheck");
2355
- throw new ElementNotFoundError(selector, hints);
2356
- }
2357
- const result = await this.cdp.send("Runtime.evaluate", {
2358
- expression: `(() => {
2359
- const el = document.querySelector(${JSON.stringify(element.selector)});
2360
- if (!el) return false;
2361
- if (el.checked) el.click();
2362
- return true;
2363
- })()`,
2364
- returnByValue: true
2627
+ return this.withStaleNodeRetry(async () => {
2628
+ const element = await this.findElement(selector, options);
2629
+ if (!element) {
2630
+ if (options.optional) return false;
2631
+ const selectorList = Array.isArray(selector) ? selector : [selector];
2632
+ const hints = await generateHints(this, selectorList, "uncheck");
2633
+ throw new ElementNotFoundError(selector, hints);
2634
+ }
2635
+ const { object } = await this.cdp.send("DOM.resolveNode", {
2636
+ nodeId: element.nodeId
2637
+ });
2638
+ try {
2639
+ await ensureActionable(this.cdp, object.objectId, ["visible", "enabled"], {
2640
+ timeout: options.timeout ?? DEFAULT_TIMEOUT
2641
+ });
2642
+ } catch (e) {
2643
+ if (options.optional) return false;
2644
+ throw e;
2645
+ }
2646
+ const isRadio = await this.cdp.send(
2647
+ "Runtime.callFunctionOn",
2648
+ {
2649
+ objectId: object.objectId,
2650
+ functionDeclaration: 'function() { return this.type === "radio"; }',
2651
+ returnByValue: true
2652
+ }
2653
+ );
2654
+ if (isRadio.result.value) return true;
2655
+ const before = await this.cdp.send("Runtime.callFunctionOn", {
2656
+ objectId: object.objectId,
2657
+ functionDeclaration: "function() { return !!this.checked; }",
2658
+ returnByValue: true
2659
+ });
2660
+ if (!before.result.value) return true;
2661
+ await this.scrollIntoView(element.nodeId);
2662
+ await this.clickElement(element.nodeId);
2663
+ const after = await this.cdp.send("Runtime.callFunctionOn", {
2664
+ objectId: object.objectId,
2665
+ functionDeclaration: "function() { return !!this.checked; }",
2666
+ returnByValue: true
2667
+ });
2668
+ if (after.result.value) {
2669
+ throw new Error("Clicking the checkbox did not change its state");
2670
+ }
2671
+ return true;
2365
2672
  });
2366
- return result.result.value;
2367
2673
  }
2368
2674
  /**
2369
2675
  * Submit a form (tries Enter key first, then click)
@@ -2377,97 +2683,100 @@ var Page = class {
2377
2683
  * the submit event and triggers HTML5 validation.
2378
2684
  */
2379
2685
  async submit(selector, options = {}) {
2380
- const { method = "enter+click", waitForNavigation: shouldWait = "auto" } = options;
2381
- const element = await this.findElement(selector, options);
2382
- if (!element) {
2383
- if (options.optional) return false;
2384
- const selectorList = Array.isArray(selector) ? selector : [selector];
2385
- const hints = await generateHints(this, selectorList, "submit");
2386
- throw new ElementNotFoundError(selector, hints);
2387
- }
2388
- const isFormElement = await this.evaluateInFrame(
2389
- `(() => {
2390
- const el = document.querySelector(${JSON.stringify(element.selector)});
2391
- return el instanceof HTMLFormElement;
2392
- })()`
2393
- );
2394
- if (isFormElement.result.value) {
2395
- await this.evaluateInFrame(
2396
- `(() => {
2397
- const form = document.querySelector(${JSON.stringify(element.selector)});
2398
- if (form && form instanceof HTMLFormElement) {
2399
- form.requestSubmit();
2400
- }
2401
- })()`
2402
- );
2403
- if (shouldWait === true) {
2404
- await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT });
2405
- } else if (shouldWait === "auto") {
2406
- await Promise.race([this.waitForNavigation({ timeout: 1e3, optional: true }), sleep3(500)]);
2686
+ return this.withStaleNodeRetry(async () => {
2687
+ const { method = "enter+click", waitForNavigation: shouldWait = "auto" } = options;
2688
+ const element = await this.findElement(selector, options);
2689
+ if (!element) {
2690
+ if (options.optional) return false;
2691
+ const selectorList = Array.isArray(selector) ? selector : [selector];
2692
+ const hints = await generateHints(this, selectorList, "submit");
2693
+ throw new ElementNotFoundError(selector, hints);
2407
2694
  }
2408
- return true;
2409
- }
2410
- await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
2411
- if (method.includes("enter")) {
2412
- await this.press("Enter");
2413
- if (shouldWait === true) {
2414
- try {
2695
+ const objectId = await this.resolveObjectId(element.nodeId);
2696
+ const isFormElement = await this.cdp.send(
2697
+ "Runtime.callFunctionOn",
2698
+ {
2699
+ objectId,
2700
+ functionDeclaration: "function() { return this instanceof HTMLFormElement; }",
2701
+ returnByValue: true
2702
+ }
2703
+ );
2704
+ if (isFormElement.result.value) {
2705
+ await this.cdp.send("Runtime.callFunctionOn", {
2706
+ objectId,
2707
+ functionDeclaration: `function() {
2708
+ if (typeof this.requestSubmit === 'function') {
2709
+ this.requestSubmit();
2710
+ } else {
2711
+ this.submit();
2712
+ }
2713
+ }`
2714
+ });
2715
+ if (shouldWait === true) {
2415
2716
  await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT });
2416
- return true;
2417
- } catch {
2717
+ } else if (shouldWait === "auto") {
2718
+ await Promise.race([
2719
+ this.waitForNavigation({ timeout: 2e3, optional: true }).then(
2720
+ () => "navigation"
2721
+ ),
2722
+ this.waitForDOMMutation({ timeout: 1e3 }).then(() => "mutation"),
2723
+ sleep3(1500).then(() => "timeout")
2724
+ ]);
2418
2725
  }
2419
- } else if (shouldWait === "auto") {
2420
- const navigationDetected = await Promise.race([
2421
- this.waitForNavigation({ timeout: 1e3, optional: true }).then(
2422
- (success) => success ? "nav" : null
2423
- ),
2424
- sleep3(500).then(() => "timeout")
2425
- ]);
2426
- if (navigationDetected === "nav") {
2726
+ return true;
2727
+ }
2728
+ await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
2729
+ if (method.includes("enter")) {
2730
+ await this.press("Enter");
2731
+ if (shouldWait === true) {
2732
+ try {
2733
+ await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT });
2734
+ return true;
2735
+ } catch {
2736
+ }
2737
+ } else if (shouldWait === "auto") {
2738
+ const navigationDetected = await Promise.race([
2739
+ this.waitForNavigation({ timeout: 2e3, optional: true }).then(
2740
+ (success) => success ? "nav" : null
2741
+ ),
2742
+ this.waitForDOMMutation({ timeout: 1e3 }).then(() => "mutation"),
2743
+ sleep3(1500).then(() => "timeout")
2744
+ ]);
2745
+ if (navigationDetected === "nav") {
2746
+ return true;
2747
+ }
2748
+ } else if (method === "enter") {
2427
2749
  return true;
2428
2750
  }
2429
- } else {
2430
- if (method === "enter") return true;
2431
2751
  }
2432
- }
2433
- if (method.includes("click")) {
2434
- await this.click(element.selector, { ...options, optional: false });
2435
- if (shouldWait === true) {
2436
- await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT });
2437
- } else if (shouldWait === "auto") {
2438
- await sleep3(100);
2752
+ if (method.includes("click")) {
2753
+ await this.click(element.selector, { ...options, optional: false });
2754
+ if (shouldWait === true) {
2755
+ await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT });
2756
+ } else if (shouldWait === "auto") {
2757
+ await sleep3(100);
2758
+ }
2439
2759
  }
2760
+ return true;
2761
+ });
2762
+ }
2763
+ /**
2764
+ * Press a key, optionally with modifier keys held down
2765
+ */
2766
+ async press(key, options) {
2767
+ const modifiers = options?.modifiers;
2768
+ if (modifiers && modifiers.length > 0) {
2769
+ await this.dispatchKeyWithModifiers(key, modifiers);
2770
+ } else {
2771
+ await this.dispatchKey(key);
2440
2772
  }
2441
- return true;
2442
2773
  }
2443
2774
  /**
2444
- * Press a key
2775
+ * Execute a keyboard shortcut (e.g. "Control+a", "Meta+Shift+z")
2445
2776
  */
2446
- async press(key) {
2447
- const keyMap = {
2448
- Enter: { key: "Enter", code: "Enter", keyCode: 13 },
2449
- Tab: { key: "Tab", code: "Tab", keyCode: 9 },
2450
- Escape: { key: "Escape", code: "Escape", keyCode: 27 },
2451
- Backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
2452
- Delete: { key: "Delete", code: "Delete", keyCode: 46 },
2453
- ArrowUp: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
2454
- ArrowDown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
2455
- ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
2456
- ArrowRight: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 }
2457
- };
2458
- const keyInfo = keyMap[key] ?? { key, code: key, keyCode: 0 };
2459
- await this.cdp.send("Input.dispatchKeyEvent", {
2460
- type: "keyDown",
2461
- key: keyInfo.key,
2462
- code: keyInfo.code,
2463
- windowsVirtualKeyCode: keyInfo.keyCode
2464
- });
2465
- await this.cdp.send("Input.dispatchKeyEvent", {
2466
- type: "keyUp",
2467
- key: keyInfo.key,
2468
- code: keyInfo.code,
2469
- windowsVirtualKeyCode: keyInfo.keyCode
2470
- });
2777
+ async shortcut(combo) {
2778
+ const { modifiers, key } = parseShortcut(combo);
2779
+ await this.dispatchKeyWithModifiers(key, modifiers);
2471
2780
  }
2472
2781
  /**
2473
2782
  * Focus an element
@@ -2496,13 +2805,37 @@ var Page = class {
2496
2805
  throw new ElementNotFoundError(selector, hints);
2497
2806
  }
2498
2807
  await this.scrollIntoView(element.nodeId);
2499
- const box = await this.getBoxModel(element.nodeId);
2500
- if (!box) {
2808
+ const objectId = await this.resolveObjectId(element.nodeId);
2809
+ try {
2810
+ await ensureActionable(this.cdp, objectId, ["visible", "stable"], {
2811
+ timeout: options.timeout ?? DEFAULT_TIMEOUT
2812
+ });
2813
+ } catch (e) {
2501
2814
  if (options.optional) return false;
2502
- throw new Error("Could not get element box model");
2815
+ throw e;
2816
+ }
2817
+ let x;
2818
+ let y;
2819
+ try {
2820
+ const { quads } = await this.cdp.send("DOM.getContentQuads", {
2821
+ objectId
2822
+ });
2823
+ if (quads?.length > 0) {
2824
+ const quad = quads[0];
2825
+ x = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
2826
+ y = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
2827
+ } else {
2828
+ throw new Error("No quads");
2829
+ }
2830
+ } catch {
2831
+ const box = await this.getBoxModel(element.nodeId);
2832
+ if (!box) {
2833
+ if (options.optional) return false;
2834
+ throw new Error("Could not get element position");
2835
+ }
2836
+ x = box.content[0] + box.width / 2;
2837
+ y = box.content[1] + box.height / 2;
2503
2838
  }
2504
- const x = box.content[0] + box.width / 2;
2505
- const y = box.content[1] + box.height / 2;
2506
2839
  await this.cdp.send("Input.dispatchMouseEvent", {
2507
2840
  type: "mouseMoved",
2508
2841
  x,
@@ -2560,15 +2893,19 @@ var Page = class {
2560
2893
  if (descResult.node.frameId) {
2561
2894
  const frameId = descResult.node.frameId;
2562
2895
  const { timeout = DEFAULT_TIMEOUT } = options;
2563
- const pollInterval = 50;
2564
- const deadline = Date.now() + timeout;
2565
2896
  let contextId = this.frameExecutionContexts.get(frameId);
2566
- while (!contextId && Date.now() < deadline) {
2567
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
2568
- contextId = this.frameExecutionContexts.get(frameId);
2897
+ if (!contextId) {
2898
+ contextId = await this.waitForFrameContext(frameId, Math.min(timeout, 2e3));
2569
2899
  }
2570
2900
  if (contextId) {
2571
2901
  this.currentFrameContextId = contextId;
2902
+ this.brokenFrame = null;
2903
+ } else {
2904
+ const frameKey2 = Array.isArray(selector) ? selector[0] : selector;
2905
+ this.brokenFrame = frameKey2;
2906
+ console.warn(
2907
+ `[browser-pilot] Frame "${frameKey2}" execution context unavailable. JS evaluation will fail in this frame. DOM operations may still work.`
2908
+ );
2572
2909
  }
2573
2910
  }
2574
2911
  this.refMap.clear();
@@ -2581,6 +2918,7 @@ var Page = class {
2581
2918
  this.currentFrame = null;
2582
2919
  this.rootNodeId = null;
2583
2920
  this.currentFrameContextId = null;
2921
+ this.brokenFrame = null;
2584
2922
  this.refMap.clear();
2585
2923
  }
2586
2924
  /**
@@ -2630,109 +2968,491 @@ var Page = class {
2630
2968
  }
2631
2969
  return result.success;
2632
2970
  }
2633
- // ============ JavaScript Execution ============
2634
- /**
2635
- * Evaluate JavaScript in the page context (or current frame context if in iframe)
2636
- */
2637
- async evaluate(expression, ...args) {
2638
- let script;
2639
- if (typeof expression === "function") {
2640
- const argString = args.map((a) => JSON.stringify(a)).join(", ");
2641
- script = `(${expression.toString()})(${argString})`;
2642
- } else {
2643
- script = expression;
2971
+ // ============ JavaScript Execution ============
2972
+ /**
2973
+ * Evaluate JavaScript in the page context (or current frame context if in iframe)
2974
+ */
2975
+ async evaluate(expression, ...args) {
2976
+ let script;
2977
+ if (typeof expression === "function") {
2978
+ const argString = args.map((a) => JSON.stringify(a)).join(", ");
2979
+ script = `(${expression.toString()})(${argString})`;
2980
+ } else {
2981
+ script = expression;
2982
+ }
2983
+ const params = {
2984
+ expression: script,
2985
+ returnByValue: true,
2986
+ awaitPromise: true
2987
+ };
2988
+ if (this.currentFrameContextId !== null) {
2989
+ params["contextId"] = this.currentFrameContextId;
2990
+ }
2991
+ const result = await this.cdp.send("Runtime.evaluate", params);
2992
+ if (result.exceptionDetails) {
2993
+ throw new Error(`Evaluation failed: ${result.exceptionDetails.text}`);
2994
+ }
2995
+ return result.result.value;
2996
+ }
2997
+ // ============ Screenshots ============
2998
+ /**
2999
+ * Take a screenshot
3000
+ */
3001
+ async screenshot(options = {}) {
3002
+ const { format = "png", quality, fullPage = false } = options;
3003
+ let clip;
3004
+ if (fullPage) {
3005
+ const metrics = await this.cdp.send("Page.getLayoutMetrics");
3006
+ clip = {
3007
+ x: 0,
3008
+ y: 0,
3009
+ width: metrics.contentSize.width,
3010
+ height: metrics.contentSize.height,
3011
+ scale: 1
3012
+ };
3013
+ }
3014
+ const result = await this.cdp.send("Page.captureScreenshot", {
3015
+ format,
3016
+ quality: format === "png" ? void 0 : quality,
3017
+ clip,
3018
+ captureBeyondViewport: fullPage
3019
+ });
3020
+ return result.data;
3021
+ }
3022
+ // ============ Text Extraction ============
3023
+ /**
3024
+ * Get text content from the page or a specific element
3025
+ */
3026
+ async text(selector) {
3027
+ if (!selector) {
3028
+ const result = await this.evaluateInFrame(
3029
+ "document.body.innerText"
3030
+ );
3031
+ return result.result.value ?? "";
3032
+ }
3033
+ return this.withStaleNodeRetry(async () => {
3034
+ const element = await this.findElement(selector, { timeout: DEFAULT_TIMEOUT });
3035
+ if (!element) return "";
3036
+ const objectId = await this.resolveObjectId(element.nodeId);
3037
+ const result = await this.cdp.send("Runtime.callFunctionOn", {
3038
+ objectId,
3039
+ functionDeclaration: 'function() { return this.innerText || this.textContent || ""; }',
3040
+ returnByValue: true
3041
+ });
3042
+ return result.result.value ?? "";
3043
+ });
3044
+ }
3045
+ // ============ File Handling ============
3046
+ /**
3047
+ * Set files on a file input
3048
+ */
3049
+ async setInputFiles(selector, files, options = {}) {
3050
+ return this.withStaleNodeRetry(async () => {
3051
+ const element = await this.findElement(selector, options);
3052
+ if (!element) {
3053
+ if (options.optional) return false;
3054
+ throw new ElementNotFoundError(selector);
3055
+ }
3056
+ const fileData = await Promise.all(
3057
+ files.map(async (f) => {
3058
+ let base64;
3059
+ if (typeof f.buffer === "string") {
3060
+ base64 = f.buffer;
3061
+ } else {
3062
+ const bytes = new Uint8Array(f.buffer);
3063
+ base64 = btoa(String.fromCharCode(...bytes));
3064
+ }
3065
+ return { name: f.name, mimeType: f.mimeType, data: base64 };
3066
+ })
3067
+ );
3068
+ const objectId = await this.resolveObjectId(element.nodeId);
3069
+ const result = await this.cdp.send("Runtime.callFunctionOn", {
3070
+ objectId,
3071
+ functionDeclaration: `function(files) {
3072
+ if (!(this instanceof HTMLInputElement) || this.type !== 'file') {
3073
+ return { ok: false, fileCount: 0 };
3074
+ }
3075
+
3076
+ const dt = new DataTransfer();
3077
+ for (const f of files) {
3078
+ const bytes = Uint8Array.from(atob(f.data), function(c) { return c.charCodeAt(0); });
3079
+ const file = new File([bytes], f.name, { type: f.mimeType });
3080
+ dt.items.add(file);
3081
+ }
3082
+
3083
+ var descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'files');
3084
+ if (descriptor && descriptor.set) {
3085
+ descriptor.set.call(this, dt.files);
3086
+ } else {
3087
+ this.files = dt.files;
3088
+ }
3089
+
3090
+ this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
3091
+ this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
3092
+ return {
3093
+ ok: (this.files && this.files.length === files.length) || files.length === 0,
3094
+ fileCount: this.files ? this.files.length : 0
3095
+ };
3096
+ }`,
3097
+ arguments: [{ value: fileData }],
3098
+ returnByValue: true
3099
+ });
3100
+ if (!result.result.value.ok) {
3101
+ if (options.optional) return false;
3102
+ throw new Error("Failed to set files on input");
3103
+ }
3104
+ return true;
3105
+ });
3106
+ }
3107
+ async getNativeSelectMetadata(objectId, targets) {
3108
+ const result = await this.cdp.send("Runtime.callFunctionOn", {
3109
+ objectId,
3110
+ functionDeclaration: `function(targetValues) {
3111
+ if (!(this instanceof HTMLSelectElement)) {
3112
+ return {
3113
+ currentIndex: -1,
3114
+ currentValue: '',
3115
+ disabled: [],
3116
+ isSelect: false,
3117
+ missing: Array.isArray(targetValues) ? targetValues.map(String) : [],
3118
+ multiple: false,
3119
+ options: [],
3120
+ selectedValues: [],
3121
+ targetIndexes: []
3122
+ };
3123
+ }
3124
+
3125
+ var allOptions = Array.from(this.options).map(function(opt, index) {
3126
+ return { index: index, label: opt.label || opt.text || '', value: opt.value || '' };
3127
+ });
3128
+ var targetIndexes = [];
3129
+ var missing = [];
3130
+ var disabled = [];
3131
+
3132
+ for (var i = 0; i < targetValues.length; i++) {
3133
+ var target = String(targetValues[i]);
3134
+ var idx = -1;
3135
+
3136
+ for (var j = 0; j < this.options.length; j++) {
3137
+ var opt = this.options[j];
3138
+ if (opt.value === target || opt.text === target || opt.label === target) {
3139
+ idx = j;
3140
+ break;
3141
+ }
3142
+ }
3143
+
3144
+ if (idx === -1 && /^\\d+$/.test(target)) {
3145
+ var numericIndex = parseInt(target, 10);
3146
+ if (numericIndex >= 0 && numericIndex < this.options.length) {
3147
+ idx = numericIndex;
3148
+ }
3149
+ }
3150
+
3151
+ if (idx === -1) {
3152
+ missing.push(target);
3153
+ continue;
3154
+ }
3155
+
3156
+ if (this.options[idx] && this.options[idx].disabled) {
3157
+ disabled.push(target);
3158
+ continue;
3159
+ }
3160
+
3161
+ if (targetIndexes.indexOf(idx) === -1) {
3162
+ targetIndexes.push(idx);
3163
+ }
3164
+ }
3165
+
3166
+ return {
3167
+ currentIndex: this.selectedIndex,
3168
+ currentValue: this.value || '',
3169
+ disabled: disabled,
3170
+ isSelect: true,
3171
+ missing: missing,
3172
+ multiple: !!this.multiple,
3173
+ options: allOptions,
3174
+ selectedValues: Array.from(this.selectedOptions).map(function(opt) { return opt.value || ''; }),
3175
+ targetIndexes: targetIndexes
3176
+ };
3177
+ }`,
3178
+ arguments: [{ value: targets }],
3179
+ returnByValue: true
3180
+ });
3181
+ return result.result.value;
3182
+ }
3183
+ async readNativeSelectValues(objectId) {
3184
+ const result = await this.cdp.send("Runtime.callFunctionOn", {
3185
+ objectId,
3186
+ functionDeclaration: 'function() { return this instanceof HTMLSelectElement ? Array.from(this.selectedOptions).map(function(opt) { return opt.value || ""; }) : []; }',
3187
+ returnByValue: true
3188
+ });
3189
+ return result.result.value ?? [];
3190
+ }
3191
+ selectValuesMatch(actual, expected, multiple) {
3192
+ if (!multiple) {
3193
+ return (actual[0] ?? "") === (expected[0] ?? "");
2644
3194
  }
2645
- const params = {
2646
- expression: script,
2647
- returnByValue: true,
2648
- awaitPromise: true
2649
- };
2650
- if (this.currentFrameContextId !== null) {
2651
- params["contextId"] = this.currentFrameContextId;
3195
+ if (actual.length !== expected.length) {
3196
+ return false;
2652
3197
  }
2653
- const result = await this.cdp.send("Runtime.evaluate", params);
2654
- if (result.exceptionDetails) {
2655
- throw new Error(`Evaluation failed: ${result.exceptionDetails.text}`);
3198
+ const actualSorted = [...actual].sort();
3199
+ const expectedSorted = [...expected].sort();
3200
+ return actualSorted.every((value, index) => value === expectedSorted[index]);
3201
+ }
3202
+ async applyNativeSelectByKeyboard(nodeId, objectId, currentIndex, targetIndex) {
3203
+ await this.cdp.send("DOM.focus", { nodeId });
3204
+ if (targetIndex !== currentIndex) {
3205
+ let effectiveIndex = currentIndex;
3206
+ if (effectiveIndex < 0 || targetIndex < effectiveIndex) {
3207
+ await this.dispatchKey("Home");
3208
+ effectiveIndex = 0;
3209
+ }
3210
+ const steps = targetIndex - effectiveIndex;
3211
+ const direction = steps >= 0 ? "ArrowDown" : "ArrowUp";
3212
+ for (let i = 0; i < Math.abs(steps); i++) {
3213
+ await this.dispatchKey(direction);
3214
+ }
2656
3215
  }
2657
- return result.result.value;
3216
+ const selectedValues = await this.readNativeSelectValues(objectId);
3217
+ return selectedValues[0] !== void 0;
3218
+ }
3219
+ async applyNativeSelectFallback(objectId, targetIndexes) {
3220
+ await this.cdp.send("Runtime.callFunctionOn", {
3221
+ objectId,
3222
+ functionDeclaration: `function(indexes) {
3223
+ if (!(this instanceof HTMLSelectElement)) return false;
3224
+
3225
+ var wanted = new Set(indexes.map(function(index) { return Number(index); }));
3226
+ for (var i = 0; i < this.options.length; i++) {
3227
+ this.options[i].selected = wanted.has(i);
3228
+ }
3229
+ if (!this.multiple && indexes.length === 1) {
3230
+ this.selectedIndex = indexes[0];
3231
+ }
3232
+ this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
3233
+ this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
3234
+ return true;
3235
+ }`,
3236
+ arguments: [{ value: targetIndexes }],
3237
+ returnByValue: true
3238
+ });
2658
3239
  }
2659
- // ============ Screenshots ============
2660
- /**
2661
- * Take a screenshot
2662
- */
2663
- async screenshot(options = {}) {
2664
- const { format = "png", quality, fullPage = false } = options;
2665
- let clip;
2666
- if (fullPage) {
2667
- const metrics = await this.cdp.send("Page.getLayoutMetrics");
2668
- clip = {
2669
- x: 0,
2670
- y: 0,
2671
- width: metrics.contentSize.width,
2672
- height: metrics.contentSize.height,
2673
- scale: 1
2674
- };
2675
- }
2676
- const result = await this.cdp.send("Page.captureScreenshot", {
2677
- format,
2678
- quality: format === "png" ? void 0 : quality,
2679
- clip,
2680
- captureBeyondViewport: fullPage
3240
+ async selectEditableContent(objectId) {
3241
+ await this.cdp.send("Runtime.callFunctionOn", {
3242
+ objectId,
3243
+ functionDeclaration: `function() {
3244
+ if (this.isContentEditable) {
3245
+ this.focus();
3246
+ const range = document.createRange();
3247
+ range.selectNodeContents(this);
3248
+ const selection = window.getSelection();
3249
+ if (selection) {
3250
+ selection.removeAllRanges();
3251
+ selection.addRange(range);
3252
+ }
3253
+ return;
3254
+ }
3255
+
3256
+ if (this.tagName === 'TEXTAREA') {
3257
+ this.selectionStart = 0;
3258
+ this.selectionEnd = this.value.length;
3259
+ this.focus();
3260
+ return;
3261
+ }
3262
+
3263
+ if (typeof this.select === 'function') {
3264
+ this.select();
3265
+ }
3266
+ this.focus();
3267
+ }`
2681
3268
  });
2682
- return result.data;
2683
3269
  }
2684
- // ============ Text Extraction ============
2685
- /**
2686
- * Get text content from the page or a specific element
2687
- */
2688
- async text(selector) {
2689
- const expression = selector ? `document.querySelector(${JSON.stringify(selector)})?.innerText ?? ''` : "document.body.innerText";
2690
- const result = await this.evaluateInFrame(expression);
3270
+ async clearEditableSelection(objectId, key) {
3271
+ await this.selectEditableContent(objectId);
3272
+ await this.dispatchKey(key);
3273
+ }
3274
+ async readEditableValue(objectId) {
3275
+ const result = await this.cdp.send("Runtime.callFunctionOn", {
3276
+ objectId,
3277
+ functionDeclaration: `function() {
3278
+ if (this.isContentEditable) {
3279
+ return this.textContent || '';
3280
+ }
3281
+ return this.value || '';
3282
+ }`,
3283
+ returnByValue: true
3284
+ });
2691
3285
  return result.result.value ?? "";
2692
3286
  }
2693
- // ============ File Handling ============
2694
- /**
2695
- * Set files on a file input
2696
- */
2697
- async setInputFiles(selector, files, options = {}) {
2698
- const element = await this.findElement(selector, options);
2699
- if (!element) {
2700
- if (options.optional) return false;
2701
- throw new ElementNotFoundError(selector);
3287
+ async typeEditableFallback(nodeId, objectId, value) {
3288
+ await this.selectEditableContent(objectId);
3289
+ await this.cdp.send("DOM.focus", { nodeId });
3290
+ for (const char of value) {
3291
+ await this.dispatchKey(char);
2702
3292
  }
2703
- const fileData = await Promise.all(
2704
- files.map(async (f) => {
2705
- let base64;
2706
- if (typeof f.buffer === "string") {
2707
- base64 = f.buffer;
2708
- } else {
2709
- const bytes = new Uint8Array(f.buffer);
2710
- base64 = btoa(String.fromCharCode(...bytes));
3293
+ }
3294
+ async applyRecordedSelectFallback(objectId, targetIndexes) {
3295
+ await this.cdp.send("Runtime.callFunctionOn", {
3296
+ objectId,
3297
+ functionDeclaration: `function(indexes) {
3298
+ if (!(this instanceof HTMLSelectElement)) return false;
3299
+
3300
+ var wanted = new Set(indexes.map(function(index) { return Number(index); }));
3301
+ for (var i = 0; i < this.options.length; i++) {
3302
+ this.options[i].selected = wanted.has(i);
2711
3303
  }
2712
- return { name: f.name, mimeType: f.mimeType, data: base64 };
2713
- })
2714
- );
2715
- await this.cdp.send("Runtime.evaluate", {
2716
- expression: `(() => {
2717
- const input = document.querySelector(${JSON.stringify(element.selector)});
2718
- if (!input) return false;
3304
+ if (!this.multiple && indexes.length === 1) {
3305
+ this.selectedIndex = indexes[0];
3306
+ }
3307
+ return true;
3308
+ }`,
3309
+ arguments: [{ value: targetIndexes }],
3310
+ returnByValue: true
3311
+ });
3312
+ return this.invokeRecordedEventListeners(objectId, ["input", "change"]);
3313
+ }
3314
+ async invokeRecordedEventListeners(objectId, eventTypes) {
3315
+ const result = await this.cdp.send("Runtime.callFunctionOn", {
3316
+ objectId,
3317
+ functionDeclaration: `function(types) {
3318
+ function buildPath(target) {
3319
+ var path = [];
3320
+ var node = target;
2719
3321
 
2720
- const files = ${JSON.stringify(fileData)};
2721
- const dt = new DataTransfer();
3322
+ while (node) {
3323
+ path.push(node);
2722
3324
 
2723
- for (const f of files) {
2724
- const bytes = Uint8Array.from(atob(f.data), c => c.charCodeAt(0));
2725
- const file = new File([bytes], f.name, { type: f.mimeType });
2726
- dt.items.add(file);
3325
+ if (node.parentElement) {
3326
+ node = node.parentElement;
3327
+ continue;
3328
+ }
3329
+
3330
+ if (node === document) {
3331
+ node = window;
3332
+ continue;
3333
+ }
3334
+
3335
+ if (node.defaultView && node !== node.defaultView) {
3336
+ node = node.defaultView;
3337
+ continue;
3338
+ }
3339
+
3340
+ if (node.ownerDocument && node !== node.ownerDocument) {
3341
+ node = node.ownerDocument;
3342
+ continue;
3343
+ }
3344
+
3345
+ var root = node.getRootNode && node.getRootNode();
3346
+ if (root && root !== node && root.host) {
3347
+ node = root.host;
3348
+ continue;
3349
+ }
3350
+
3351
+ node = null;
3352
+ }
3353
+
3354
+ return path;
2727
3355
  }
2728
3356
 
2729
- input.files = dt.files;
2730
- input.dispatchEvent(new Event('change', { bubbles: true }));
2731
- return true;
2732
- })()`,
3357
+ function createEvent(type, target, currentTarget, path, phase) {
3358
+ return {
3359
+ type: type,
3360
+ target: target,
3361
+ currentTarget: currentTarget,
3362
+ srcElement: target,
3363
+ isTrusted: true,
3364
+ bubbles: true,
3365
+ cancelable: true,
3366
+ composed: true,
3367
+ defaultPrevented: false,
3368
+ eventPhase: phase,
3369
+ timeStamp: Date.now(),
3370
+ preventDefault: function() {
3371
+ this.defaultPrevented = true;
3372
+ },
3373
+ stopPropagation: function() {
3374
+ this.__stopped = true;
3375
+ },
3376
+ stopImmediatePropagation: function() {
3377
+ this.__stopped = true;
3378
+ this.__immediateStopped = true;
3379
+ },
3380
+ composedPath: function() {
3381
+ return path.slice();
3382
+ }
3383
+ };
3384
+ }
3385
+
3386
+ function invokePhase(type, nodes, capture, target, path) {
3387
+ var invoked = false;
3388
+
3389
+ for (var i = 0; i < nodes.length; i++) {
3390
+ var currentTarget = nodes[i];
3391
+
3392
+ var phase = currentTarget === target ? 2 : capture ? 1 : 3;
3393
+
3394
+ // Invoke inline handler if present (e.g. onclick, oninput)
3395
+ var inlineHandler = currentTarget['on' + type];
3396
+ if (typeof inlineHandler === 'function') {
3397
+ var inlineEvent = createEvent(type, target, currentTarget, path, phase);
3398
+ inlineHandler.call(currentTarget, inlineEvent);
3399
+ invoked = true;
3400
+ if (inlineEvent.__stopped) break;
3401
+ }
3402
+
3403
+ var store = currentTarget && currentTarget.__bpEventListeners;
3404
+ var entries = store && store[type];
3405
+ if (!Array.isArray(entries) || entries.length === 0) continue;
3406
+
3407
+ var event = createEvent(type, target, currentTarget, path, phase);
3408
+
3409
+ for (var j = 0; j < entries.length; j++) {
3410
+ var entry = entries[j];
3411
+ if (!!entry.capture !== capture) continue;
3412
+
3413
+ var listener = entry.listener;
3414
+ if (typeof listener === 'function') {
3415
+ listener.call(currentTarget, event);
3416
+ invoked = true;
3417
+ } else if (listener && typeof listener.handleEvent === 'function') {
3418
+ listener.handleEvent(event);
3419
+ invoked = true;
3420
+ }
3421
+
3422
+ if (event.__immediateStopped) {
3423
+ break;
3424
+ }
3425
+ }
3426
+
3427
+ if (event.__stopped) {
3428
+ break;
3429
+ }
3430
+ }
3431
+
3432
+ return invoked;
3433
+ }
3434
+
3435
+ var path = buildPath(this);
3436
+ var capturePath = path.slice().reverse();
3437
+ var bubblePath = path.slice();
3438
+ var invokedAny = false;
3439
+
3440
+ for (var i = 0; i < types.length; i++) {
3441
+ var type = String(types[i]);
3442
+ if (invokePhase(type, capturePath, true, this, path)) {
3443
+ invokedAny = true;
3444
+ }
3445
+ if (invokePhase(type, bubblePath, false, this, path)) {
3446
+ invokedAny = true;
3447
+ }
3448
+ }
3449
+
3450
+ return invokedAny;
3451
+ }`,
3452
+ arguments: [{ value: eventTypes }],
2733
3453
  returnByValue: true
2734
3454
  });
2735
- return true;
3455
+ return result.result.value ?? false;
2736
3456
  }
2737
3457
  /**
2738
3458
  * Wait for a download to complete, triggered by an action
@@ -2892,7 +3612,7 @@ var Page = class {
2892
3612
  return lines.join("\n");
2893
3613
  };
2894
3614
  const text = formatTree(accessibilityTree);
2895
- return {
3615
+ const result = {
2896
3616
  url,
2897
3617
  title,
2898
3618
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2900,6 +3620,8 @@ var Page = class {
2900
3620
  interactiveElements,
2901
3621
  text
2902
3622
  };
3623
+ this.lastSnapshot = result;
3624
+ return result;
2903
3625
  }
2904
3626
  /**
2905
3627
  * Export the current ref map for cross-exec reuse (CLI).
@@ -3313,8 +4035,15 @@ var Page = class {
3313
4035
  }
3314
4036
  };
3315
4037
  if (this.dialogHandler) {
4038
+ const DIALOG_TIMEOUT = 5e3;
3316
4039
  try {
3317
- await this.dialogHandler(dialog);
4040
+ await Promise.race([
4041
+ this.dialogHandler(dialog),
4042
+ sleep3(DIALOG_TIMEOUT).then(() => {
4043
+ console.warn("[browser-pilot] Dialog handler timed out after 5s, auto-dismissing");
4044
+ return dialog.dismiss();
4045
+ })
4046
+ ]);
3318
4047
  } catch (e) {
3319
4048
  console.error("[Dialog handler error]", e);
3320
4049
  await dialog.dismiss();
@@ -3394,6 +4123,7 @@ var Page = class {
3394
4123
  this.refMap.clear();
3395
4124
  this.currentFrame = null;
3396
4125
  this.currentFrameContextId = null;
4126
+ this.brokenFrame = null;
3397
4127
  this.frameContexts.clear();
3398
4128
  this.dialogHandler = null;
3399
4129
  try {
@@ -3428,10 +4158,12 @@ var Page = class {
3428
4158
  try {
3429
4159
  return await fn();
3430
4160
  } catch (e) {
3431
- if (e instanceof Error && (e.message.includes("Could not find node with given id") || e.message.includes("Node with given id does not belong to the document") || e.message.includes("No node with given id found"))) {
4161
+ const message = e instanceof Error ? e.message : "";
4162
+ if (e instanceof Error && (message.includes("Could not find node with given id") || message.includes("Node with given id does not belong to the document") || message.includes("No node with given id found") || message.includes("Could not find object with given id") || message.includes("Cannot find context with specified id") || message.includes("Cannot find context with given id") || message.includes("Execution context was destroyed") || message.includes("No execution context with given id") || message.includes("Argument should belong to the same JavaScript world"))) {
3432
4163
  lastError = e;
3433
4164
  if (attempt < retries) {
3434
4165
  this.rootNodeId = null;
4166
+ this.currentFrameContextId = null;
3435
4167
  await sleep3(delay);
3436
4168
  continue;
3437
4169
  }
@@ -3477,6 +4209,39 @@ var Page = class {
3477
4209
  }
3478
4210
  }
3479
4211
  }
4212
+ if (selectorList.every((s) => s.startsWith("ref:")) && this.lastSnapshot) {
4213
+ for (const selector of selectorList) {
4214
+ const ref = selector.slice(4);
4215
+ const originalElement = this.lastSnapshot.interactiveElements.find((e) => e.ref === ref);
4216
+ if (!originalElement) continue;
4217
+ const freshSnapshot = await this.snapshot();
4218
+ const match = freshSnapshot.interactiveElements.find(
4219
+ (e) => e.role === originalElement.role && e.name === originalElement.name
4220
+ );
4221
+ if (match) {
4222
+ const newBackendNodeId = this.refMap.get(match.ref);
4223
+ if (newBackendNodeId) {
4224
+ try {
4225
+ await this.ensureRootNode();
4226
+ const pushResult = await this.cdp.send(
4227
+ "DOM.pushNodesByBackendIdsToFrontend",
4228
+ { backendNodeIds: [newBackendNodeId] }
4229
+ );
4230
+ if (pushResult.nodeIds?.[0]) {
4231
+ this._lastMatchedSelector = `ref:${match.ref}`;
4232
+ return {
4233
+ nodeId: pushResult.nodeIds[0],
4234
+ backendNodeId: newBackendNodeId,
4235
+ selector: `ref:${match.ref}`,
4236
+ waitedMs: 0
4237
+ };
4238
+ }
4239
+ } catch {
4240
+ }
4241
+ }
4242
+ }
4243
+ }
4244
+ }
3480
4245
  const cssSelectors = selectorList.filter((s) => !s.startsWith("ref:"));
3481
4246
  if (cssSelectors.length === 0) {
3482
4247
  return null;
@@ -3540,6 +4305,34 @@ var Page = class {
3540
4305
  */
3541
4306
  async ensureRootNode() {
3542
4307
  if (this.rootNodeId) return;
4308
+ if (this.currentFrame) {
4309
+ const mainDocument = await this.cdp.send("DOM.getDocument", {
4310
+ depth: 0
4311
+ });
4312
+ const iframeNode = await this.cdp.send("DOM.querySelector", {
4313
+ nodeId: mainDocument.root.nodeId,
4314
+ selector: this.currentFrame
4315
+ });
4316
+ if (iframeNode.nodeId) {
4317
+ const frameResult = await this.cdp.send("DOM.describeNode", {
4318
+ nodeId: iframeNode.nodeId,
4319
+ depth: 1
4320
+ });
4321
+ if (frameResult.node.contentDocument?.nodeId) {
4322
+ this.rootNodeId = frameResult.node.contentDocument.nodeId;
4323
+ if (frameResult.node.frameId) {
4324
+ let contextId = this.frameExecutionContexts.get(frameResult.node.frameId);
4325
+ if (!contextId) {
4326
+ contextId = await this.waitForFrameContext(frameResult.node.frameId, 1e3);
4327
+ }
4328
+ this.currentFrameContextId = contextId ?? null;
4329
+ }
4330
+ return;
4331
+ }
4332
+ }
4333
+ this.currentFrame = null;
4334
+ this.currentFrameContextId = null;
4335
+ }
3543
4336
  const doc = await this.cdp.send("DOM.getDocument", {
3544
4337
  depth: 0
3545
4338
  });
@@ -3550,6 +4343,11 @@ var Page = class {
3550
4343
  * Automatically injects contextId when in an iframe
3551
4344
  */
3552
4345
  async evaluateInFrame(expression, options = {}) {
4346
+ if (this.brokenFrame && this.currentFrame) {
4347
+ throw new Error(
4348
+ `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.`
4349
+ );
4350
+ }
3553
4351
  const params = {
3554
4352
  expression,
3555
4353
  returnByValue: options.returnByValue ?? true,
@@ -3561,10 +4359,43 @@ var Page = class {
3561
4359
  return this.cdp.send("Runtime.evaluate", params);
3562
4360
  }
3563
4361
  /**
3564
- * Scroll an element into view
4362
+ * Scroll an element into view, with fallback to center-scroll if clipped by fixed headers
3565
4363
  */
3566
4364
  async scrollIntoView(nodeId) {
3567
4365
  await this.cdp.send("DOM.scrollIntoViewIfNeeded", { nodeId });
4366
+ if (!await this.isInViewport(nodeId)) {
4367
+ const objectId = await this.resolveObjectId(nodeId);
4368
+ await this.cdp.send("Runtime.callFunctionOn", {
4369
+ objectId,
4370
+ functionDeclaration: `function() { this.scrollIntoView({ block: 'center', inline: 'center' }); }`
4371
+ });
4372
+ }
4373
+ }
4374
+ /**
4375
+ * Check if element is within the visible viewport
4376
+ */
4377
+ async isInViewport(nodeId) {
4378
+ try {
4379
+ const objectId = await this.resolveObjectId(nodeId);
4380
+ const result = await this.cdp.send("Runtime.callFunctionOn", {
4381
+ objectId,
4382
+ functionDeclaration: `function() {
4383
+ var rect = this.getBoundingClientRect();
4384
+ return (
4385
+ rect.top >= 0 &&
4386
+ rect.left >= 0 &&
4387
+ rect.bottom <= window.innerHeight &&
4388
+ rect.right <= window.innerWidth &&
4389
+ rect.width > 0 &&
4390
+ rect.height > 0
4391
+ );
4392
+ }`,
4393
+ returnByValue: true
4394
+ });
4395
+ return result?.result?.value === true;
4396
+ } catch {
4397
+ return true;
4398
+ }
3568
4399
  }
3569
4400
  /**
3570
4401
  * Get element box model (position and dimensions)
@@ -3580,30 +4411,147 @@ var Page = class {
3580
4411
  }
3581
4412
  }
3582
4413
  /**
3583
- * Click an element by node ID
4414
+ * Click an element by node ID using Playwright's 3-event sequence:
4415
+ * mouseMoved → mousePressed → mouseReleased (sequential).
4416
+ * Uses DOM.getContentQuads for accurate coordinates (handles CSS transforms).
4417
+ * Falls back to JS this.click() if CDP mouse dispatch fails.
3584
4418
  */
3585
4419
  async clickElement(nodeId) {
3586
- const box = await this.getBoxModel(nodeId);
3587
- if (!box) {
3588
- throw new Error("Could not get element box model for click");
3589
- }
3590
- const x = box.content[0] + box.width / 2;
3591
- const y = box.content[1] + box.height / 2;
3592
- await this.cdp.send("Input.dispatchMouseEvent", {
3593
- type: "mousePressed",
3594
- x,
3595
- y,
3596
- button: "left",
3597
- clickCount: 1
3598
- });
3599
- await this.cdp.send("Input.dispatchMouseEvent", {
3600
- type: "mouseReleased",
3601
- x,
3602
- y,
3603
- button: "left",
3604
- clickCount: 1
4420
+ const { object } = await this.cdp.send("DOM.resolveNode", {
4421
+ nodeId
4422
+ });
4423
+ let x;
4424
+ let y;
4425
+ try {
4426
+ const { quads } = await this.cdp.send("DOM.getContentQuads", {
4427
+ objectId: object.objectId
4428
+ });
4429
+ if (quads && quads.length > 0) {
4430
+ const quad = quads[0];
4431
+ x = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
4432
+ y = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
4433
+ } else {
4434
+ throw new Error("No quads");
4435
+ }
4436
+ } catch {
4437
+ const box = await this.getBoxModel(nodeId);
4438
+ if (!box) throw new Error("Could not get element position for click");
4439
+ x = box.content[0] + box.width / 2;
4440
+ y = box.content[1] + box.height / 2;
4441
+ }
4442
+ try {
4443
+ await this.cdp.send("Input.dispatchMouseEvent", {
4444
+ type: "mouseMoved",
4445
+ x,
4446
+ y,
4447
+ button: "none",
4448
+ buttons: 0,
4449
+ modifiers: 0
4450
+ });
4451
+ await this.cdp.send("Input.dispatchMouseEvent", {
4452
+ type: "mousePressed",
4453
+ x,
4454
+ y,
4455
+ button: "left",
4456
+ buttons: 1,
4457
+ clickCount: 1,
4458
+ modifiers: 0
4459
+ });
4460
+ await this.cdp.send("Input.dispatchMouseEvent", {
4461
+ type: "mouseReleased",
4462
+ x,
4463
+ y,
4464
+ button: "left",
4465
+ buttons: 0,
4466
+ clickCount: 1,
4467
+ modifiers: 0
4468
+ });
4469
+ } catch {
4470
+ await this.cdp.send("Runtime.callFunctionOn", {
4471
+ objectId: object.objectId,
4472
+ functionDeclaration: "function() { this.click(); }"
4473
+ });
4474
+ }
4475
+ await this.cdp.send("Runtime.evaluate", { expression: "0" });
4476
+ }
4477
+ /**
4478
+ * Resolve a nodeId to a Remote Object ID for use with Runtime.callFunctionOn
4479
+ */
4480
+ async resolveObjectId(nodeId) {
4481
+ const { object } = await this.cdp.send("DOM.resolveNode", {
4482
+ nodeId
4483
+ });
4484
+ return object.objectId;
4485
+ }
4486
+ async dispatchKeyDefinition(def, modifierBitmask = 0) {
4487
+ const downParams = {
4488
+ type: def.text !== void 0 ? "keyDown" : "rawKeyDown",
4489
+ key: def.key,
4490
+ code: def.code,
4491
+ windowsVirtualKeyCode: def.keyCode,
4492
+ modifiers: modifierBitmask,
4493
+ autoRepeat: false,
4494
+ location: def.location ?? 0,
4495
+ isKeypad: false
4496
+ };
4497
+ if (def.text !== void 0) {
4498
+ downParams["text"] = def.text;
4499
+ downParams["unmodifiedText"] = def.text;
4500
+ }
4501
+ await this.cdp.send("Input.dispatchKeyEvent", downParams);
4502
+ await this.cdp.send("Input.dispatchKeyEvent", {
4503
+ type: "keyUp",
4504
+ key: def.key,
4505
+ code: def.code,
4506
+ windowsVirtualKeyCode: def.keyCode,
4507
+ modifiers: modifierBitmask,
4508
+ location: def.location ?? 0
3605
4509
  });
3606
4510
  }
4511
+ async dispatchKey(key) {
4512
+ const def = US_KEYBOARD[key];
4513
+ if (def) {
4514
+ await this.dispatchKeyDefinition(def);
4515
+ return;
4516
+ }
4517
+ if (key.length === 1) {
4518
+ await this.cdp.send("Input.insertText", { text: key });
4519
+ return;
4520
+ }
4521
+ await this.dispatchKeyDefinition({ key, code: key, keyCode: 0 });
4522
+ }
4523
+ async dispatchKeyWithModifiers(key, modifiers) {
4524
+ const mask = computeModifierBitmask(modifiers);
4525
+ for (const mod of modifiers) {
4526
+ await this.cdp.send("Input.dispatchKeyEvent", {
4527
+ type: "rawKeyDown",
4528
+ key: mod,
4529
+ code: MODIFIER_CODES[mod],
4530
+ windowsVirtualKeyCode: MODIFIER_KEY_CODES[mod],
4531
+ modifiers: mask,
4532
+ location: 1
4533
+ });
4534
+ }
4535
+ const def = US_KEYBOARD[key];
4536
+ if (def) {
4537
+ await this.dispatchKeyDefinition(def, mask);
4538
+ } else if (key.length === 1) {
4539
+ await this.dispatchKeyDefinition({ key, code: key, keyCode: 0, text: key }, mask);
4540
+ } else {
4541
+ await this.dispatchKeyDefinition({ key, code: key, keyCode: 0 }, mask);
4542
+ }
4543
+ for (let i = modifiers.length - 1; i >= 0; i--) {
4544
+ const mod = modifiers[i];
4545
+ await this.cdp.send("Input.dispatchKeyEvent", {
4546
+ type: "keyUp",
4547
+ key: mod,
4548
+ code: MODIFIER_CODES[mod],
4549
+ windowsVirtualKeyCode: MODIFIER_KEY_CODES[mod],
4550
+ modifiers: 0,
4551
+ location: 1
4552
+ });
4553
+ }
4554
+ }
3607
4555
  // ============ Audio I/O ============
3608
4556
  /**
3609
4557
  * Audio input controller (fake microphone).
@@ -3703,12 +4651,68 @@ var Page = class {
3703
4651
  totalMs: Date.now() - start
3704
4652
  };
3705
4653
  }
4654
+ /**
4655
+ * Wait for a DOM mutation in the current frame (used for detecting client-side form handling)
4656
+ */
4657
+ async waitForDOMMutation(options) {
4658
+ await this.evaluateInFrame(
4659
+ `new Promise((resolve) => {
4660
+ var observer = new MutationObserver(function() {
4661
+ observer.disconnect();
4662
+ resolve();
4663
+ });
4664
+ observer.observe(document.body, { childList: true, subtree: true });
4665
+ setTimeout(function() { observer.disconnect(); resolve(); }, ${options.timeout});
4666
+ })`
4667
+ );
4668
+ }
4669
+ /**
4670
+ * Wait for a frame execution context via Runtime.executionContextCreated event
4671
+ */
4672
+ async waitForFrameContext(frameId, timeout) {
4673
+ const existing = this.frameExecutionContexts.get(frameId);
4674
+ if (existing) return existing;
4675
+ return new Promise((resolve) => {
4676
+ const timer = setTimeout(() => {
4677
+ cleanup();
4678
+ resolve(void 0);
4679
+ }, timeout);
4680
+ const handler = (params) => {
4681
+ const context = params["context"];
4682
+ if (context.auxData?.frameId === frameId && context.auxData?.isDefault !== false) {
4683
+ cleanup();
4684
+ resolve(context.id);
4685
+ }
4686
+ };
4687
+ const cleanup = () => {
4688
+ clearTimeout(timer);
4689
+ this.cdp.off("Runtime.executionContextCreated", handler);
4690
+ };
4691
+ this.cdp.on("Runtime.executionContextCreated", handler);
4692
+ });
4693
+ }
3706
4694
  };
3707
4695
  function sleep3(ms) {
3708
4696
  return new Promise((resolve) => setTimeout(resolve, ms));
3709
4697
  }
3710
4698
 
3711
4699
  // src/browser/browser.ts
4700
+ function scoreTarget(t) {
4701
+ let score = 0;
4702
+ if (t.url.startsWith("http://") || t.url.startsWith("https://")) score += 10;
4703
+ if (t.url.startsWith("chrome://")) score -= 20;
4704
+ if (t.url.startsWith("chrome-extension://")) score -= 15;
4705
+ if (t.url.startsWith("devtools://")) score -= 25;
4706
+ if (t.url === "about:blank") score -= 5;
4707
+ if (!t.attached) score += 3;
4708
+ if (t.title && t.title.length > 0) score += 2;
4709
+ return score;
4710
+ }
4711
+ function pickBestTarget(targets) {
4712
+ if (targets.length === 0) return void 0;
4713
+ const sorted = [...targets].sort((a, b) => scoreTarget(b) - scoreTarget(a));
4714
+ return sorted[0].targetId;
4715
+ }
3712
4716
  var Browser = class _Browser {
3713
4717
  cdp;
3714
4718
  providerSession;
@@ -3730,28 +4734,46 @@ var Browser = class _Browser {
3730
4734
  return new _Browser(cdp, provider, session, options);
3731
4735
  }
3732
4736
  /**
3733
- * Get or create a page by name
3734
- * If no name is provided, returns the first available page or creates a new one
4737
+ * Get or create a page by name.
4738
+ * If no name is provided, returns the first available page or creates a new one.
4739
+ *
4740
+ * Target selection heuristics (when no targetId is specified):
4741
+ * - Prefer http/https URLs over chrome://, devtools://, about:blank
4742
+ * - Prefer unattached targets (not already controlled by another client)
4743
+ * - Filter by targetUrl if provided
3735
4744
  */
3736
4745
  async page(name, options) {
3737
4746
  const pageName = name ?? "default";
3738
4747
  const cached = this.pages.get(pageName);
3739
4748
  if (cached) return cached;
3740
4749
  const targets = await this.cdp.send("Target.getTargets");
3741
- const pageTargets = targets.targetInfos.filter((t) => t.type === "page");
4750
+ let pageTargets = targets.targetInfos.filter((t) => t.type === "page");
4751
+ if (options?.targetUrl) {
4752
+ const urlFilter = options.targetUrl;
4753
+ const filtered = pageTargets.filter((t) => t.url.includes(urlFilter));
4754
+ if (filtered.length > 0) {
4755
+ pageTargets = filtered;
4756
+ } else {
4757
+ console.warn(
4758
+ `[browser-pilot] No targets match URL filter "${urlFilter}", falling back to all page targets`
4759
+ );
4760
+ }
4761
+ }
3742
4762
  let targetId;
3743
4763
  if (options?.targetId) {
3744
- const targetExists = pageTargets.some((t) => t.targetId === options.targetId);
4764
+ const targetExists = targets.targetInfos.some(
4765
+ (t) => t.type === "page" && t.targetId === options.targetId
4766
+ );
3745
4767
  if (targetExists) {
3746
4768
  targetId = options.targetId;
3747
4769
  } else {
3748
4770
  console.warn(`[browser-pilot] Target ${options.targetId} no longer exists, falling back`);
3749
- targetId = pageTargets.length > 0 ? pageTargets[0].targetId : (await this.cdp.send("Target.createTarget", {
4771
+ targetId = pickBestTarget(pageTargets) ?? (await this.cdp.send("Target.createTarget", {
3750
4772
  url: "about:blank"
3751
4773
  })).targetId;
3752
4774
  }
3753
4775
  } else if (pageTargets.length > 0) {
3754
- targetId = pageTargets[0].targetId;
4776
+ targetId = pickBestTarget(pageTargets);
3755
4777
  } else {
3756
4778
  const result = await this.cdp.send("Target.createTarget", {
3757
4779
  url: "about:blank"
@@ -3761,6 +4783,21 @@ var Browser = class _Browser {
3761
4783
  await this.cdp.attachToTarget(targetId);
3762
4784
  const page = new Page(this.cdp, targetId);
3763
4785
  await page.init();
4786
+ const minViewport = options?.minViewport !== void 0 ? options.minViewport : { width: 200, height: 200 };
4787
+ if (minViewport !== false) {
4788
+ try {
4789
+ const viewport = await page.evaluate(
4790
+ "({ w: window.innerWidth, h: window.innerHeight })"
4791
+ );
4792
+ if (viewport.w < minViewport.width || viewport.h < minViewport.height) {
4793
+ console.warn(
4794
+ `[browser-pilot] Attached target has small viewport (${viewport.w}x${viewport.h}). Applying default viewport override (1280x720). Use { minViewport: false } to disable this check.`
4795
+ );
4796
+ await page.setViewport({ width: 1280, height: 720 });
4797
+ }
4798
+ } catch {
4799
+ }
4800
+ }
3764
4801
  this.pages.set(pageName, page);
3765
4802
  return page;
3766
4803
  }