browser-pilot 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import {
2
- createCDPClient
3
- } from "./chunk-4MBSALQL.mjs";
2
+ createCDPClient,
3
+ stringifyUnknown
4
+ } from "./chunk-KIFB526Y.mjs";
4
5
  import {
5
6
  createProvider
6
7
  } from "./chunk-BRAFQUMG.mjs";
@@ -11,7 +12,7 @@ import {
11
12
  TimeoutError,
12
13
  ensureActionable,
13
14
  generateHints
14
- } from "./chunk-NLIARNEE.mjs";
15
+ } from "./chunk-XMJABKCF.mjs";
15
16
 
16
17
  // src/audio/encoding.ts
17
18
  function bufferToBase64(data) {
@@ -1507,6 +1508,285 @@ var RequestInterceptor = class {
1507
1508
  }
1508
1509
  };
1509
1510
 
1511
+ // src/browser/special-selectors.ts
1512
+ function stripQuotes(value) {
1513
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1514
+ return value.slice(1, -1);
1515
+ }
1516
+ return value;
1517
+ }
1518
+ function parseTextSelector(selector) {
1519
+ if (!selector.startsWith("text:")) return null;
1520
+ let raw = selector.slice(5).trim();
1521
+ let exact = false;
1522
+ if (raw.startsWith("=")) {
1523
+ exact = true;
1524
+ raw = raw.slice(1).trim();
1525
+ }
1526
+ const query = stripQuotes(raw);
1527
+ if (!query) return null;
1528
+ return { query, exact };
1529
+ }
1530
+ function parseRoleSelector(selector) {
1531
+ if (!selector.startsWith("role:")) return null;
1532
+ const body = selector.slice(5);
1533
+ const separator = body.indexOf(":");
1534
+ const role = (separator === -1 ? body : body.slice(0, separator)).trim().toLowerCase();
1535
+ const name = separator === -1 ? void 0 : stripQuotes(body.slice(separator + 1).trim());
1536
+ if (!role) return null;
1537
+ return { role, name: name || void 0 };
1538
+ }
1539
+ var SPECIAL_SELECTOR_SCRIPT = `
1540
+ function bpNormalizeSpace(value) {
1541
+ return String(value == null ? '' : value).replace(/\\s+/g, ' ').trim();
1542
+ }
1543
+
1544
+ function bpCollectElements(root) {
1545
+ var elements = [];
1546
+
1547
+ function visit(node) {
1548
+ if (!node || typeof node.querySelectorAll !== 'function') return;
1549
+ var matches = node.querySelectorAll('*');
1550
+ for (var i = 0; i < matches.length; i++) {
1551
+ var el = matches[i];
1552
+ elements.push(el);
1553
+ if (el.shadowRoot) {
1554
+ visit(el.shadowRoot);
1555
+ }
1556
+ }
1557
+ }
1558
+
1559
+ if (root && root.documentElement) {
1560
+ elements.push(root.documentElement);
1561
+ }
1562
+
1563
+ visit(root);
1564
+ return elements;
1565
+ }
1566
+
1567
+ function bpIsVisible(el) {
1568
+ if (!el) return false;
1569
+ var style = getComputedStyle(el);
1570
+ if (style.display === 'none') return false;
1571
+ if (style.visibility === 'hidden') return false;
1572
+ if (parseFloat(style.opacity || '1') === 0) return false;
1573
+ var rect = el.getBoundingClientRect();
1574
+ return rect.width > 0 && rect.height > 0;
1575
+ }
1576
+
1577
+ function bpInferRole(el) {
1578
+ if (!el || !el.tagName) return '';
1579
+
1580
+ var explicitRole = bpNormalizeSpace(el.getAttribute && el.getAttribute('role'));
1581
+ if (explicitRole) return explicitRole.toLowerCase();
1582
+
1583
+ var tag = el.tagName.toLowerCase();
1584
+ if (tag === 'button') return 'button';
1585
+ if (tag === 'a' && el.hasAttribute('href')) return 'link';
1586
+ if (tag === 'textarea') return 'textbox';
1587
+ if (tag === 'select') return el.multiple ? 'listbox' : 'combobox';
1588
+ if (tag === 'option') return 'option';
1589
+ if (tag === 'summary') return 'button';
1590
+
1591
+ if (tag === 'input') {
1592
+ var type = (el.type || 'text').toLowerCase();
1593
+ if (type === 'checkbox') return 'checkbox';
1594
+ if (type === 'radio') return 'radio';
1595
+ if (type === 'search') return 'searchbox';
1596
+ if (type === 'number') return 'spinbutton';
1597
+ if (type === 'button' || type === 'submit' || type === 'reset' || type === 'image') {
1598
+ return 'button';
1599
+ }
1600
+ return 'textbox';
1601
+ }
1602
+
1603
+ return '';
1604
+ }
1605
+
1606
+ function bpTextFromIdRefs(refs) {
1607
+ if (!refs) return '';
1608
+ var ids = refs.split(/\\s+/).filter(Boolean);
1609
+ var parts = [];
1610
+ for (var i = 0; i < ids.length; i++) {
1611
+ var node = document.getElementById(ids[i]);
1612
+ if (!node) continue;
1613
+ var text = bpNormalizeSpace(node.innerText || node.textContent || '');
1614
+ if (text) parts.push(text);
1615
+ }
1616
+ return bpNormalizeSpace(parts.join(' '));
1617
+ }
1618
+
1619
+ function bpAccessibleName(el) {
1620
+ if (!el) return '';
1621
+
1622
+ var labelledBy = bpTextFromIdRefs(el.getAttribute && el.getAttribute('aria-labelledby'));
1623
+ if (labelledBy) return labelledBy;
1624
+
1625
+ var ariaLabel = bpNormalizeSpace(el.getAttribute && el.getAttribute('aria-label'));
1626
+ if (ariaLabel) return ariaLabel;
1627
+
1628
+ if (el.labels && el.labels.length) {
1629
+ var labels = [];
1630
+ for (var i = 0; i < el.labels.length; i++) {
1631
+ var labelText = bpNormalizeSpace(el.labels[i].innerText || el.labels[i].textContent || '');
1632
+ if (labelText) labels.push(labelText);
1633
+ }
1634
+ if (labels.length) return bpNormalizeSpace(labels.join(' '));
1635
+ }
1636
+
1637
+ if (el.id) {
1638
+ var fallbackLabel = document.querySelector('label[for="' + el.id.replace(/"/g, '\\\\"') + '"]');
1639
+ if (fallbackLabel) {
1640
+ var fallbackText = bpNormalizeSpace(
1641
+ fallbackLabel.innerText || fallbackLabel.textContent || ''
1642
+ );
1643
+ if (fallbackText) return fallbackText;
1644
+ }
1645
+ }
1646
+
1647
+ var type = (el.type || '').toLowerCase();
1648
+ if (
1649
+ el.tagName === 'INPUT' &&
1650
+ (type === 'submit' || type === 'button' || type === 'reset' || type === 'image')
1651
+ ) {
1652
+ var inputValue = bpNormalizeSpace(el.value || el.getAttribute('value'));
1653
+ if (inputValue) return inputValue;
1654
+ }
1655
+
1656
+ var alt = bpNormalizeSpace(el.getAttribute && el.getAttribute('alt'));
1657
+ if (alt) return alt;
1658
+
1659
+ var text = bpNormalizeSpace(el.innerText || el.textContent || '');
1660
+ if (text) return text;
1661
+
1662
+ var placeholder = bpNormalizeSpace(el.getAttribute && el.getAttribute('placeholder'));
1663
+ if (placeholder) return placeholder;
1664
+
1665
+ var title = bpNormalizeSpace(el.getAttribute && el.getAttribute('title'));
1666
+ if (title) return title;
1667
+
1668
+ var value = bpNormalizeSpace(el.value);
1669
+ if (value) return value;
1670
+
1671
+ return bpNormalizeSpace(el.name || el.id || '');
1672
+ }
1673
+
1674
+ function bpIsInteractive(role, el) {
1675
+ if (
1676
+ role === 'button' ||
1677
+ role === 'link' ||
1678
+ role === 'textbox' ||
1679
+ role === 'checkbox' ||
1680
+ role === 'radio' ||
1681
+ role === 'combobox' ||
1682
+ role === 'listbox' ||
1683
+ role === 'option' ||
1684
+ role === 'searchbox' ||
1685
+ role === 'spinbutton' ||
1686
+ role === 'switch' ||
1687
+ role === 'tab'
1688
+ ) {
1689
+ return true;
1690
+ }
1691
+
1692
+ if (!el || !el.tagName) return false;
1693
+ var tag = el.tagName.toLowerCase();
1694
+ return tag === 'button' || tag === 'a' || tag === 'input' || tag === 'select' || tag === 'textarea';
1695
+ }
1696
+
1697
+ function bpFindByText(query, exact, includeHidden) {
1698
+ var needle = bpNormalizeSpace(query).toLowerCase();
1699
+ if (!needle) return null;
1700
+
1701
+ var best = null;
1702
+ var bestScore = -1;
1703
+ var elements = bpCollectElements(document);
1704
+
1705
+ for (var i = 0; i < elements.length; i++) {
1706
+ var el = elements[i];
1707
+ if (!includeHidden && !bpIsVisible(el)) continue;
1708
+
1709
+ var text = bpAccessibleName(el);
1710
+ if (!text) continue;
1711
+
1712
+ var haystack = text.toLowerCase();
1713
+ var matched = exact ? haystack === needle : haystack.includes(needle);
1714
+ if (!matched) continue;
1715
+
1716
+ var role = bpInferRole(el);
1717
+ var score = 0;
1718
+ if (bpIsInteractive(role, el)) score += 100;
1719
+ if (haystack === needle) score += 50;
1720
+ if (role === 'button' || role === 'link') score += 10;
1721
+
1722
+ if (score > bestScore) {
1723
+ best = el;
1724
+ bestScore = score;
1725
+ }
1726
+ }
1727
+
1728
+ return best;
1729
+ }
1730
+
1731
+ function bpFindByRole(role, name, includeHidden) {
1732
+ var targetRole = bpNormalizeSpace(role).toLowerCase();
1733
+ if (!targetRole) return null;
1734
+
1735
+ var nameNeedle = bpNormalizeSpace(name).toLowerCase();
1736
+ var best = null;
1737
+ var bestScore = -1;
1738
+ var elements = bpCollectElements(document);
1739
+
1740
+ for (var i = 0; i < elements.length; i++) {
1741
+ var el = elements[i];
1742
+ if (!includeHidden && !bpIsVisible(el)) continue;
1743
+
1744
+ var actualRole = bpInferRole(el);
1745
+ if (actualRole !== targetRole) continue;
1746
+
1747
+ var accessibleName = bpAccessibleName(el);
1748
+ if (nameNeedle) {
1749
+ var loweredName = accessibleName.toLowerCase();
1750
+ if (!loweredName.includes(nameNeedle)) continue;
1751
+ }
1752
+
1753
+ var score = 0;
1754
+ if (accessibleName) score += 10;
1755
+ if (nameNeedle && accessibleName.toLowerCase() === nameNeedle) score += 20;
1756
+
1757
+ if (score > bestScore) {
1758
+ best = el;
1759
+ bestScore = score;
1760
+ }
1761
+ }
1762
+
1763
+ return best;
1764
+ }
1765
+ `;
1766
+ function buildSpecialSelectorLookupExpression(selector, options = {}) {
1767
+ const includeHidden = options.includeHidden === true;
1768
+ const text = parseTextSelector(selector);
1769
+ if (text) {
1770
+ return `(() => {
1771
+ ${SPECIAL_SELECTOR_SCRIPT}
1772
+ return bpFindByText(${JSON.stringify(text.query)}, ${text.exact}, ${includeHidden});
1773
+ })()`;
1774
+ }
1775
+ const role = parseRoleSelector(selector);
1776
+ if (role) {
1777
+ return `(() => {
1778
+ ${SPECIAL_SELECTOR_SCRIPT}
1779
+ return bpFindByRole(${JSON.stringify(role.role)}, ${JSON.stringify(role.name ?? "")}, ${includeHidden});
1780
+ })()`;
1781
+ }
1782
+ return null;
1783
+ }
1784
+ function buildSpecialSelectorPredicateExpression(selector, options = {}) {
1785
+ const lookup = buildSpecialSelectorLookupExpression(selector, options);
1786
+ if (!lookup) return null;
1787
+ return `(() => !!(${lookup}))()`;
1788
+ }
1789
+
1510
1790
  // src/wait/strategies.ts
1511
1791
  var DEEP_QUERY_SCRIPT = `
1512
1792
  function deepQuery(selector, root = document) {
@@ -1540,18 +1820,19 @@ function deepQuery(selector, root = document) {
1540
1820
  }
1541
1821
  `;
1542
1822
  async function isElementVisible(cdp, selector, contextId) {
1823
+ const specialExpression = buildSpecialSelectorPredicateExpression(selector);
1543
1824
  const params = {
1544
- expression: `(() => {
1545
- ${DEEP_QUERY_SCRIPT}
1546
- const el = deepQuery(${JSON.stringify(selector)});
1547
- if (!el) return false;
1548
- const style = getComputedStyle(el);
1549
- if (style.display === 'none') return false;
1550
- if (style.visibility === 'hidden') return false;
1551
- if (parseFloat(style.opacity) === 0) return false;
1552
- const rect = el.getBoundingClientRect();
1553
- return rect.width > 0 && rect.height > 0;
1554
- })()`,
1825
+ expression: specialExpression ?? `(() => {
1826
+ ${DEEP_QUERY_SCRIPT}
1827
+ const el = deepQuery(${JSON.stringify(selector)});
1828
+ if (!el) return false;
1829
+ const style = getComputedStyle(el);
1830
+ if (style.display === 'none') return false;
1831
+ if (style.visibility === 'hidden') return false;
1832
+ if (parseFloat(style.opacity) === 0) return false;
1833
+ const rect = el.getBoundingClientRect();
1834
+ return rect.width > 0 && rect.height > 0;
1835
+ })()`,
1555
1836
  returnByValue: true
1556
1837
  };
1557
1838
  if (contextId !== void 0) {
@@ -1561,11 +1842,14 @@ async function isElementVisible(cdp, selector, contextId) {
1561
1842
  return result.result.value === true;
1562
1843
  }
1563
1844
  async function isElementAttached(cdp, selector, contextId) {
1845
+ const specialExpression = buildSpecialSelectorPredicateExpression(selector, {
1846
+ includeHidden: true
1847
+ });
1564
1848
  const params = {
1565
- expression: `(() => {
1566
- ${DEEP_QUERY_SCRIPT}
1567
- return deepQuery(${JSON.stringify(selector)}) !== null;
1568
- })()`,
1849
+ expression: specialExpression ?? `(() => {
1850
+ ${DEEP_QUERY_SCRIPT}
1851
+ return deepQuery(${JSON.stringify(selector)}) !== null;
1852
+ })()`,
1569
1853
  returnByValue: true
1570
1854
  };
1571
1855
  if (contextId !== void 0) {
@@ -2044,6 +2328,9 @@ var Page = class {
2044
2328
  brokenFrame = null;
2045
2329
  /** Last matched selector from findElement (for selectorUsed tracking) */
2046
2330
  _lastMatchedSelector;
2331
+ _lastActionCoordinates = null;
2332
+ _lastActionBoundingBox = null;
2333
+ _lastActionTargetMetadata = null;
2047
2334
  /** Last snapshot for stale ref recovery */
2048
2335
  lastSnapshot;
2049
2336
  /** Audio input controller (lazy-initialized) */
@@ -2075,6 +2362,76 @@ var Page = class {
2075
2362
  getLastMatchedSelector() {
2076
2363
  return this._lastMatchedSelector;
2077
2364
  }
2365
+ async getActionTargetMetadata(identifiers) {
2366
+ try {
2367
+ const objectId = identifiers.objectId ?? (identifiers.nodeId ? await this.resolveObjectId(identifiers.nodeId) : void 0);
2368
+ if (!objectId) return null;
2369
+ const response = await this.cdp.send("Runtime.callFunctionOn", {
2370
+ objectId,
2371
+ functionDeclaration: `function() {
2372
+ const tagName = this.tagName?.toLowerCase?.() || '';
2373
+ const inputType =
2374
+ tagName === 'input' && typeof this.type === 'string' ? this.type.toLowerCase() : '';
2375
+ const autocomplete =
2376
+ typeof this.autocomplete === 'string' ? this.autocomplete.toLowerCase() : '';
2377
+ return { tagName, inputType, autocomplete };
2378
+ }`,
2379
+ returnByValue: true
2380
+ });
2381
+ return response.result.value ?? null;
2382
+ } catch {
2383
+ return null;
2384
+ }
2385
+ }
2386
+ async getElementPosition(identifiers) {
2387
+ try {
2388
+ const { quads } = await this.cdp.send(
2389
+ "DOM.getContentQuads",
2390
+ identifiers
2391
+ );
2392
+ if (quads?.length > 0) {
2393
+ const q = quads[0];
2394
+ const minX = Math.min(q[0], q[2], q[4], q[6]);
2395
+ const maxX = Math.max(q[0], q[2], q[4], q[6]);
2396
+ const minY = Math.min(q[1], q[3], q[5], q[7]);
2397
+ const maxY = Math.max(q[1], q[3], q[5], q[7]);
2398
+ return {
2399
+ center: { x: (minX + maxX) / 2, y: (minY + maxY) / 2 },
2400
+ bbox: { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
2401
+ };
2402
+ }
2403
+ } catch {
2404
+ }
2405
+ if (identifiers.nodeId) {
2406
+ const box = await this.getBoxModel(identifiers.nodeId);
2407
+ if (box) {
2408
+ return {
2409
+ center: { x: box.content[0] + box.width / 2, y: box.content[1] + box.height / 2 },
2410
+ bbox: { x: box.content[0], y: box.content[1], width: box.width, height: box.height }
2411
+ };
2412
+ }
2413
+ }
2414
+ return null;
2415
+ }
2416
+ setLastActionPosition(coords, bbox) {
2417
+ this._lastActionCoordinates = coords;
2418
+ this._lastActionBoundingBox = bbox;
2419
+ }
2420
+ getLastActionCoordinates() {
2421
+ return this._lastActionCoordinates;
2422
+ }
2423
+ getLastActionBoundingBox() {
2424
+ return this._lastActionBoundingBox;
2425
+ }
2426
+ getLastActionTargetMetadata() {
2427
+ return this._lastActionTargetMetadata;
2428
+ }
2429
+ /** Reset position tracking (call before each executor step) */
2430
+ resetLastActionPosition() {
2431
+ this._lastActionCoordinates = null;
2432
+ this._lastActionBoundingBox = null;
2433
+ this._lastActionTargetMetadata = null;
2434
+ }
2078
2435
  /**
2079
2436
  * Initialize the page (enable required CDP domains)
2080
2437
  */
@@ -2226,6 +2583,9 @@ var Page = class {
2226
2583
  timeout: options.timeout ?? DEFAULT_TIMEOUT
2227
2584
  });
2228
2585
  } catch (e) {
2586
+ if (e instanceof ActionabilityError && e.failureType === "hitTarget" && await this.tryClickAssociatedLabel(objectId)) {
2587
+ return true;
2588
+ }
2229
2589
  if (options.optional) return false;
2230
2590
  throw e;
2231
2591
  }
@@ -2239,6 +2599,14 @@ var Page = class {
2239
2599
  const quad = quads[0];
2240
2600
  clickX = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
2241
2601
  clickY = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
2602
+ const minX = Math.min(quad[0], quad[2], quad[4], quad[6]);
2603
+ const maxX = Math.max(quad[0], quad[2], quad[4], quad[6]);
2604
+ const minY = Math.min(quad[1], quad[3], quad[5], quad[7]);
2605
+ const maxY = Math.max(quad[1], quad[3], quad[5], quad[7]);
2606
+ this.setLastActionPosition(
2607
+ { x: clickX, y: clickY },
2608
+ { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
2609
+ );
2242
2610
  } else {
2243
2611
  throw new Error("No quads");
2244
2612
  }
@@ -2247,6 +2615,10 @@ var Page = class {
2247
2615
  if (!box) throw new Error("Could not get element position");
2248
2616
  clickX = box.content[0] + box.width / 2;
2249
2617
  clickY = box.content[1] + box.height / 2;
2618
+ this.setLastActionPosition(
2619
+ { x: clickX, y: clickY },
2620
+ { x: box.content[0], y: box.content[1], width: box.width, height: box.height }
2621
+ );
2250
2622
  }
2251
2623
  const hitTargetCoordinates = this.currentFrame ? void 0 : { x: clickX, y: clickY };
2252
2624
  const HIT_TARGET_RETRIES = 3;
@@ -2297,13 +2669,20 @@ var Page = class {
2297
2669
  if (options.optional) return false;
2298
2670
  throw e;
2299
2671
  }
2672
+ const fillPos = await this.getElementPosition({ nodeId: element.nodeId });
2673
+ if (fillPos) this.setLastActionPosition(fillPos.center, fillPos.bbox);
2300
2674
  const tagInfo = await this.cdp.send("Runtime.callFunctionOn", {
2301
2675
  objectId,
2302
2676
  functionDeclaration: `function() {
2303
- return { tagName: this.tagName?.toLowerCase() || '', inputType: (this.type || '').toLowerCase() };
2677
+ return {
2678
+ tagName: this.tagName?.toLowerCase() || '',
2679
+ inputType: (this.type || '').toLowerCase(),
2680
+ autocomplete: typeof this.autocomplete === 'string' ? this.autocomplete.toLowerCase() : '',
2681
+ };
2304
2682
  }`,
2305
2683
  returnByValue: true
2306
2684
  });
2685
+ this._lastActionTargetMetadata = tagInfo.result.value;
2307
2686
  const { tagName, inputType } = tagInfo.result.value;
2308
2687
  const specialInputTypes = /* @__PURE__ */ new Set([
2309
2688
  "date",
@@ -2385,6 +2764,9 @@ var Page = class {
2385
2764
  if (options.optional) return false;
2386
2765
  throw e;
2387
2766
  }
2767
+ const typePos = await this.getElementPosition({ nodeId: element.nodeId });
2768
+ if (typePos) this.setLastActionPosition(typePos.center, typePos.bbox);
2769
+ this._lastActionTargetMetadata = await this.getActionTargetMetadata({ objectId });
2388
2770
  await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
2389
2771
  for (const char of text) {
2390
2772
  const def = US_KEYBOARD[char];
@@ -2464,6 +2846,9 @@ var Page = class {
2464
2846
  if (options.optional) return false;
2465
2847
  throw e;
2466
2848
  }
2849
+ const selectPos = await this.getElementPosition({ nodeId: element.nodeId });
2850
+ if (selectPos) this.setLastActionPosition(selectPos.center, selectPos.bbox);
2851
+ this._lastActionTargetMetadata = await this.getActionTargetMetadata({ objectId });
2467
2852
  const metadata = await this.getNativeSelectMetadata(objectId, values);
2468
2853
  if (!metadata.isSelect) {
2469
2854
  throw new Error("select() target must be a native <select> element");
@@ -2600,6 +2985,8 @@ var Page = class {
2600
2985
  if (options.optional) return false;
2601
2986
  throw e;
2602
2987
  }
2988
+ const checkPos = await this.getElementPosition({ nodeId: element.nodeId });
2989
+ if (checkPos) this.setLastActionPosition(checkPos.center, checkPos.bbox);
2603
2990
  const before = await this.cdp.send("Runtime.callFunctionOn", {
2604
2991
  objectId: object.objectId,
2605
2992
  functionDeclaration: "function() { return !!this.checked; }",
@@ -2614,7 +3001,12 @@ var Page = class {
2614
3001
  returnByValue: true
2615
3002
  });
2616
3003
  if (!after.result.value) {
2617
- throw new Error("Clicking the checkbox did not change its state");
3004
+ if (await this.tryToggleViaLabel(object.objectId, true)) {
3005
+ return true;
3006
+ }
3007
+ throw new Error(
3008
+ "Clicking the checkbox did not change its state. Tried the associated label too."
3009
+ );
2618
3010
  }
2619
3011
  return true;
2620
3012
  });
@@ -2643,6 +3035,8 @@ var Page = class {
2643
3035
  if (options.optional) return false;
2644
3036
  throw e;
2645
3037
  }
3038
+ const uncheckPos = await this.getElementPosition({ nodeId: element.nodeId });
3039
+ if (uncheckPos) this.setLastActionPosition(uncheckPos.center, uncheckPos.bbox);
2646
3040
  const isRadio = await this.cdp.send(
2647
3041
  "Runtime.callFunctionOn",
2648
3042
  {
@@ -2666,7 +3060,12 @@ var Page = class {
2666
3060
  returnByValue: true
2667
3061
  });
2668
3062
  if (after.result.value) {
2669
- throw new Error("Clicking the checkbox did not change its state");
3063
+ if (await this.tryToggleViaLabel(object.objectId, false)) {
3064
+ return true;
3065
+ }
3066
+ throw new Error(
3067
+ "Clicking the checkbox did not change its state. Tried the associated label too."
3068
+ );
2670
3069
  }
2671
3070
  return true;
2672
3071
  });
@@ -2693,6 +3092,8 @@ var Page = class {
2693
3092
  throw new ElementNotFoundError(selector, hints);
2694
3093
  }
2695
3094
  const objectId = await this.resolveObjectId(element.nodeId);
3095
+ const submitPos = await this.getElementPosition({ nodeId: element.nodeId });
3096
+ if (submitPos) this.setLastActionPosition(submitPos.center, submitPos.bbox);
2696
3097
  const isFormElement = await this.cdp.send(
2697
3098
  "Runtime.callFunctionOn",
2698
3099
  {
@@ -2789,6 +3190,8 @@ var Page = class {
2789
3190
  const hints = await generateHints(this, selectorList, "focus");
2790
3191
  throw new ElementNotFoundError(selector, hints);
2791
3192
  }
3193
+ const focusPos = await this.getElementPosition({ nodeId: element.nodeId });
3194
+ if (focusPos) this.setLastActionPosition(focusPos.center, focusPos.bbox);
2792
3195
  await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
2793
3196
  return true;
2794
3197
  }
@@ -2824,6 +3227,14 @@ var Page = class {
2824
3227
  const quad = quads[0];
2825
3228
  x = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
2826
3229
  y = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
3230
+ const minX = Math.min(quad[0], quad[2], quad[4], quad[6]);
3231
+ const maxX = Math.max(quad[0], quad[2], quad[4], quad[6]);
3232
+ const minY = Math.min(quad[1], quad[3], quad[5], quad[7]);
3233
+ const maxY = Math.max(quad[1], quad[3], quad[5], quad[7]);
3234
+ this.setLastActionPosition(
3235
+ { x, y },
3236
+ { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
3237
+ );
2827
3238
  } else {
2828
3239
  throw new Error("No quads");
2829
3240
  }
@@ -2835,6 +3246,10 @@ var Page = class {
2835
3246
  }
2836
3247
  x = box.content[0] + box.width / 2;
2837
3248
  y = box.content[1] + box.height / 2;
3249
+ this.setLastActionPosition(
3250
+ { x, y },
3251
+ { x: box.content[0], y: box.content[1], width: box.width, height: box.height }
3252
+ );
2838
3253
  }
2839
3254
  await this.cdp.send("Input.dispatchMouseEvent", {
2840
3255
  type: "mouseMoved",
@@ -2860,6 +3275,8 @@ var Page = class {
2860
3275
  if (options.optional) return false;
2861
3276
  throw new ElementNotFoundError(selector);
2862
3277
  }
3278
+ const scrollPos = await this.getElementPosition({ nodeId: element.nodeId });
3279
+ if (scrollPos) this.setLastActionPosition(scrollPos.center, scrollPos.bbox);
2863
3280
  await this.scrollIntoView(element.nodeId);
2864
3281
  return true;
2865
3282
  }
@@ -2990,7 +3407,7 @@ var Page = class {
2990
3407
  }
2991
3408
  const result = await this.cdp.send("Runtime.evaluate", params);
2992
3409
  if (result.exceptionDetails) {
2993
- throw new Error(`Evaluation failed: ${result.exceptionDetails.text}`);
3410
+ throw new Error(this.formatEvaluationError(result.exceptionDetails));
2994
3411
  }
2995
3412
  return result.result.value;
2996
3413
  }
@@ -3042,6 +3459,75 @@ var Page = class {
3042
3459
  return result.result.value ?? "";
3043
3460
  });
3044
3461
  }
3462
+ /**
3463
+ * Enumerate form controls on the page with labels and current state.
3464
+ */
3465
+ async forms() {
3466
+ const result = await this.evaluateInFrame(
3467
+ `(() => {
3468
+ function normalize(value) {
3469
+ return String(value == null ? '' : value).replace(/\\s+/g, ' ').trim();
3470
+ }
3471
+
3472
+ function labelFor(el) {
3473
+ if (!el) return '';
3474
+ if (el.labels && el.labels.length) {
3475
+ return normalize(
3476
+ Array.from(el.labels)
3477
+ .map((label) => label.innerText || label.textContent || '')
3478
+ .join(' ')
3479
+ );
3480
+ }
3481
+ var ariaLabel = normalize(el.getAttribute && el.getAttribute('aria-label'));
3482
+ if (ariaLabel) return ariaLabel;
3483
+ if (el.id) {
3484
+ var byFor = document.querySelector('label[for="' + el.id.replace(/"/g, '\\\\"') + '"]');
3485
+ if (byFor) return normalize(byFor.innerText || byFor.textContent || '');
3486
+ }
3487
+ var closest = el.closest && el.closest('label');
3488
+ if (closest) return normalize(closest.innerText || closest.textContent || '');
3489
+ return '';
3490
+ }
3491
+
3492
+ return Array.from(document.querySelectorAll('input, select, textarea')).map((el) => {
3493
+ var tag = el.tagName.toLowerCase();
3494
+ var type = tag === 'input' ? (el.type || 'text').toLowerCase() : tag;
3495
+ var value = null;
3496
+
3497
+ if (tag === 'select') {
3498
+ value = el.multiple
3499
+ ? Array.from(el.selectedOptions).map((opt) => opt.value)
3500
+ : el.value || null;
3501
+ } else if (tag === 'textarea' || tag === 'input') {
3502
+ value = typeof el.value === 'string' ? el.value : null;
3503
+ }
3504
+
3505
+ return {
3506
+ tag: tag,
3507
+ type: type,
3508
+ id: el.id || undefined,
3509
+ name: el.getAttribute('name') || undefined,
3510
+ value: value,
3511
+ checked: 'checked' in el ? !!el.checked : undefined,
3512
+ required: !!el.required,
3513
+ disabled: !!el.disabled,
3514
+ label: labelFor(el) || undefined,
3515
+ placeholder: normalize(el.getAttribute && el.getAttribute('placeholder')) || undefined,
3516
+ options:
3517
+ tag === 'select'
3518
+ ? Array.from(el.options).map((opt) => ({
3519
+ value: opt.value || '',
3520
+ text: normalize(opt.text || opt.label || ''),
3521
+ selected: !!opt.selected,
3522
+ disabled: !!opt.disabled,
3523
+ }))
3524
+ : undefined,
3525
+ };
3526
+ });
3527
+ })()`
3528
+ );
3529
+ return result.result.value ?? [];
3530
+ }
3045
3531
  // ============ File Handling ============
3046
3532
  /**
3047
3533
  * Set files on a file input
@@ -3514,7 +4000,8 @@ var Page = class {
3514
4000
  /**
3515
4001
  * Get an accessibility tree snapshot of the page
3516
4002
  */
3517
- async snapshot() {
4003
+ async snapshot(options = {}) {
4004
+ const roleFilter = new Set((options.roles ?? []).map((role) => role.trim().toLowerCase()));
3518
4005
  const [url, title, axTree] = await Promise.all([
3519
4006
  this.url(),
3520
4007
  this.title(),
@@ -3535,7 +4022,7 @@ var Page = class {
3535
4022
  const buildNode = (nodeId) => {
3536
4023
  const node = nodeMap.get(nodeId);
3537
4024
  if (!node) return null;
3538
- const role = node.role?.value ?? "generic";
4025
+ const role = (node.role?.value ?? "generic").toLowerCase();
3539
4026
  const name = node.name?.value;
3540
4027
  const value = node.value?.value;
3541
4028
  const ref = nodeRefs.get(nodeId);
@@ -3551,7 +4038,7 @@ var Page = class {
3551
4038
  return {
3552
4039
  role,
3553
4040
  name,
3554
- value,
4041
+ value: value !== void 0 ? stringifyUnknown(value) : void 0,
3555
4042
  ref,
3556
4043
  children: children.length > 0 ? children : void 0,
3557
4044
  disabled,
@@ -3559,7 +4046,24 @@ var Page = class {
3559
4046
  };
3560
4047
  };
3561
4048
  const rootNodes = nodes.filter((n) => !n.parentId || !nodeMap.has(n.parentId));
3562
- const accessibilityTree = rootNodes.map((n) => buildNode(n.nodeId)).filter((n) => n !== null);
4049
+ let accessibilityTree = rootNodes.map((n) => buildNode(n.nodeId)).filter((n) => n !== null);
4050
+ if (roleFilter.size > 0) {
4051
+ const filteredAccessibilityTree = [];
4052
+ for (const node of nodes) {
4053
+ if (!roleFilter.has((node.role?.value ?? "generic").toLowerCase())) {
4054
+ continue;
4055
+ }
4056
+ const snapshotNode = buildNode(node.nodeId);
4057
+ if (!snapshotNode) {
4058
+ continue;
4059
+ }
4060
+ filteredAccessibilityTree.push({
4061
+ ...snapshotNode,
4062
+ children: void 0
4063
+ });
4064
+ }
4065
+ accessibilityTree = filteredAccessibilityTree;
4066
+ }
3563
4067
  const interactiveRoles = /* @__PURE__ */ new Set([
3564
4068
  "button",
3565
4069
  "link",
@@ -3581,37 +4085,44 @@ var Page = class {
3581
4085
  ]);
3582
4086
  const interactiveElements = [];
3583
4087
  for (const node of nodes) {
3584
- const role = node.role?.value;
3585
- if (role && interactiveRoles.has(role)) {
4088
+ const role = (node.role?.value ?? "").toLowerCase();
4089
+ if (role && interactiveRoles.has(role) && (roleFilter.size === 0 || roleFilter.has(role))) {
3586
4090
  const ref = nodeRefs.get(node.nodeId);
3587
4091
  const name = node.name?.value ?? "";
3588
4092
  const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
4093
+ const checked = node.properties?.find((p) => p.name === "checked")?.value.value;
4094
+ const value = node.value?.value;
3589
4095
  const selector = node.backendDOMNodeId ? `[data-backend-node-id="${node.backendDOMNodeId}"]` : `[aria-label="${name}"]`;
3590
4096
  interactiveElements.push({
3591
4097
  ref,
3592
4098
  role,
3593
4099
  name,
3594
4100
  selector,
3595
- disabled
4101
+ disabled,
4102
+ checked,
4103
+ value: value !== void 0 ? stringifyUnknown(value) : void 0
3596
4104
  });
3597
4105
  }
3598
4106
  }
4107
+ const formatNode = (node, depth = 0) => {
4108
+ let line = `${" ".repeat(depth)}- ${node.role}`;
4109
+ if (node.name) line += ` "${node.name}"`;
4110
+ line += ` ref:${node.ref}`;
4111
+ if (node.disabled) line += " (disabled)";
4112
+ if (node.checked !== void 0) line += node.checked ? " (checked)" : " (unchecked)";
4113
+ return line;
4114
+ };
3599
4115
  const formatTree = (nodes2, depth = 0) => {
3600
4116
  const lines = [];
3601
4117
  for (const node of nodes2) {
3602
- let line = `${" ".repeat(depth)}- ${node.role}`;
3603
- if (node.name) line += ` "${node.name}"`;
3604
- line += ` [ref=${node.ref}]`;
3605
- if (node.disabled) line += " (disabled)";
3606
- if (node.checked !== void 0) line += node.checked ? " (checked)" : " (unchecked)";
3607
- lines.push(line);
4118
+ lines.push(formatNode(node, depth));
3608
4119
  if (node.children) {
3609
4120
  lines.push(formatTree(node.children, depth + 1));
3610
4121
  }
3611
4122
  }
3612
4123
  return lines.join("\n");
3613
4124
  };
3614
- const text = formatTree(accessibilityTree);
4125
+ const text = roleFilter.size > 0 ? accessibilityTree.map((node) => formatNode(node)).join("\n") : formatTree(accessibilityTree);
3615
4126
  const result = {
3616
4127
  url,
3617
4128
  title,
@@ -3620,7 +4131,9 @@ var Page = class {
3620
4131
  interactiveElements,
3621
4132
  text
3622
4133
  };
3623
- this.lastSnapshot = result;
4134
+ if (roleFilter.size === 0) {
4135
+ this.lastSnapshot = result;
4136
+ }
3624
4137
  return result;
3625
4138
  }
3626
4139
  /**
@@ -4057,7 +4570,7 @@ var Page = class {
4057
4570
  */
4058
4571
  formatConsoleArgs(args) {
4059
4572
  return args.map((arg) => {
4060
- if (arg.value !== void 0) return String(arg.value);
4573
+ if (arg.value !== void 0) return stringifyUnknown(arg.value);
4061
4574
  if (arg.description) return arg.description;
4062
4575
  return "[object]";
4063
4576
  }).join(" ");
@@ -4175,7 +4688,7 @@ var Page = class {
4175
4688
  }
4176
4689
  /**
4177
4690
  * Find an element using single or multiple selectors
4178
- * Supports ref: prefix for ref-based selectors (e.g., "ref:e4")
4691
+ * Supports ref:, text:, and role: selectors.
4179
4692
  */
4180
4693
  async findElement(selectors, options = {}) {
4181
4694
  const { timeout = DEFAULT_TIMEOUT } = options;
@@ -4242,11 +4755,11 @@ var Page = class {
4242
4755
  }
4243
4756
  }
4244
4757
  }
4245
- const cssSelectors = selectorList.filter((s) => !s.startsWith("ref:"));
4246
- if (cssSelectors.length === 0) {
4758
+ const runtimeSelectors = selectorList.filter((s) => !s.startsWith("ref:"));
4759
+ if (runtimeSelectors.length === 0) {
4247
4760
  return null;
4248
4761
  }
4249
- const result = await waitForAnyElement(this.cdp, cssSelectors, {
4762
+ const result = await waitForAnyElement(this.cdp, runtimeSelectors, {
4250
4763
  state: "visible",
4251
4764
  timeout,
4252
4765
  contextId: this.currentFrameContextId ?? void 0
@@ -4254,6 +4767,14 @@ var Page = class {
4254
4767
  if (!result.success || !result.selector) {
4255
4768
  return null;
4256
4769
  }
4770
+ const specialSelectorMatch = await this.resolveSpecialSelector(result.selector);
4771
+ if (specialSelectorMatch) {
4772
+ this._lastMatchedSelector = result.selector;
4773
+ return {
4774
+ ...specialSelectorMatch,
4775
+ waitedMs: result.waitedMs
4776
+ };
4777
+ }
4257
4778
  await this.ensureRootNode();
4258
4779
  const queryResult = await this.cdp.send("DOM.querySelector", {
4259
4780
  nodeId: this.rootNodeId,
@@ -4300,6 +4821,122 @@ var Page = class {
4300
4821
  waitedMs: result.waitedMs
4301
4822
  };
4302
4823
  }
4824
+ formatEvaluationError(details) {
4825
+ const description = typeof details.exception?.description === "string" && details.exception.description || typeof details.exception?.value === "string" && details.exception.value || details.text || "Uncaught";
4826
+ return `Evaluation failed: ${description}`;
4827
+ }
4828
+ async resolveSpecialSelector(selector, options = {}) {
4829
+ const expression = buildSpecialSelectorLookupExpression(selector, options);
4830
+ if (!expression) return null;
4831
+ const result = await this.evaluateInFrame(expression, {
4832
+ returnByValue: false
4833
+ });
4834
+ if (!result.result.objectId) {
4835
+ return null;
4836
+ }
4837
+ const resolved = await this.objectIdToNode(result.result.objectId);
4838
+ if (!resolved) {
4839
+ return null;
4840
+ }
4841
+ return {
4842
+ nodeId: resolved.nodeId,
4843
+ backendNodeId: resolved.backendNodeId,
4844
+ selector,
4845
+ waitedMs: 0
4846
+ };
4847
+ }
4848
+ async readCheckedState(objectId) {
4849
+ const result = await this.cdp.send("Runtime.callFunctionOn", {
4850
+ objectId,
4851
+ functionDeclaration: "function() { return !!this.checked; }",
4852
+ returnByValue: true
4853
+ });
4854
+ return result.result.value === true;
4855
+ }
4856
+ async readInputType(objectId) {
4857
+ const result = await this.cdp.send(
4858
+ "Runtime.callFunctionOn",
4859
+ {
4860
+ objectId,
4861
+ functionDeclaration: 'function() { return this instanceof HTMLInputElement ? String(this.type || "").toLowerCase() : null; }',
4862
+ returnByValue: true
4863
+ }
4864
+ );
4865
+ return result.result.value ?? null;
4866
+ }
4867
+ async getAssociatedLabelNodeId(objectId) {
4868
+ const result = await this.cdp.send("Runtime.callFunctionOn", {
4869
+ objectId,
4870
+ functionDeclaration: `function() {
4871
+ if (!(this instanceof HTMLInputElement)) return null;
4872
+
4873
+ if (this.id) {
4874
+ var labels = Array.from(document.querySelectorAll('label'));
4875
+ for (var i = 0; i < labels.length; i++) {
4876
+ if (labels[i].htmlFor === this.id) return labels[i];
4877
+ }
4878
+ }
4879
+
4880
+ return this.closest('label');
4881
+ }`,
4882
+ returnByValue: false
4883
+ });
4884
+ if (!result.result.objectId) {
4885
+ return null;
4886
+ }
4887
+ return (await this.objectIdToNode(result.result.objectId))?.nodeId ?? null;
4888
+ }
4889
+ async objectIdToNode(objectId) {
4890
+ const describeResult = await this.cdp.send("DOM.describeNode", {
4891
+ objectId,
4892
+ depth: 0
4893
+ });
4894
+ const backendNodeId = describeResult.node.backendNodeId;
4895
+ if (!backendNodeId) {
4896
+ return null;
4897
+ }
4898
+ if (describeResult.node.nodeId) {
4899
+ return {
4900
+ nodeId: describeResult.node.nodeId,
4901
+ backendNodeId
4902
+ };
4903
+ }
4904
+ await this.ensureRootNode();
4905
+ const pushResult = await this.cdp.send(
4906
+ "DOM.pushNodesByBackendIdsToFrontend",
4907
+ {
4908
+ backendNodeIds: [backendNodeId]
4909
+ }
4910
+ );
4911
+ const nodeId = pushResult.nodeIds?.[0];
4912
+ if (!nodeId) {
4913
+ return null;
4914
+ }
4915
+ return { nodeId, backendNodeId };
4916
+ }
4917
+ async tryClickAssociatedLabel(objectId) {
4918
+ const inputType = await this.readInputType(objectId);
4919
+ if (inputType !== "checkbox" && inputType !== "radio") {
4920
+ return false;
4921
+ }
4922
+ const labelNodeId = await this.getAssociatedLabelNodeId(objectId);
4923
+ if (!labelNodeId) {
4924
+ return false;
4925
+ }
4926
+ try {
4927
+ await this.scrollIntoView(labelNodeId);
4928
+ await this.clickElement(labelNodeId);
4929
+ return true;
4930
+ } catch {
4931
+ return false;
4932
+ }
4933
+ }
4934
+ async tryToggleViaLabel(objectId, desiredChecked) {
4935
+ if (!await this.tryClickAssociatedLabel(objectId)) {
4936
+ return false;
4937
+ }
4938
+ return await this.readCheckedState(objectId) === desiredChecked;
4939
+ }
4303
4940
  /**
4304
4941
  * Ensure we have a valid root node ID
4305
4942
  */
@@ -4717,10 +5354,30 @@ var Browser = class _Browser {
4717
5354
  cdp;
4718
5355
  providerSession;
4719
5356
  pages = /* @__PURE__ */ new Map();
5357
+ pageCounter = 0;
4720
5358
  constructor(cdp, _provider, providerSession, _options) {
4721
5359
  this.cdp = cdp;
4722
5360
  this.providerSession = providerSession;
4723
5361
  }
5362
+ /**
5363
+ * Create a Browser from an existing CDPClient (used by daemon fast-path).
5364
+ * The caller is responsible for the CDP connection lifecycle.
5365
+ */
5366
+ static fromCDP(cdp, sessionInfo) {
5367
+ const providerSession = {
5368
+ wsUrl: sessionInfo.wsUrl,
5369
+ sessionId: sessionInfo.sessionId,
5370
+ async close() {
5371
+ }
5372
+ };
5373
+ const provider = {
5374
+ name: sessionInfo.provider ?? "daemon",
5375
+ async createSession() {
5376
+ return providerSession;
5377
+ }
5378
+ };
5379
+ return new _Browser(cdp, provider, providerSession, { provider: "generic" });
5380
+ }
4724
5381
  /**
4725
5382
  * Connect to a browser instance
4726
5383
  */
@@ -4746,7 +5403,11 @@ var Browser = class _Browser {
4746
5403
  const pageName = name ?? "default";
4747
5404
  const cached = this.pages.get(pageName);
4748
5405
  if (cached) return cached;
4749
- const targets = await this.cdp.send("Target.getTargets");
5406
+ const targets = await this.cdp.send(
5407
+ "Target.getTargets",
5408
+ void 0,
5409
+ null
5410
+ );
4750
5411
  let pageTargets = targets.targetInfos.filter((t) => t.type === "page");
4751
5412
  if (options?.targetUrl) {
4752
5413
  const urlFilter = options.targetUrl;
@@ -4768,16 +5429,24 @@ var Browser = class _Browser {
4768
5429
  targetId = options.targetId;
4769
5430
  } else {
4770
5431
  console.warn(`[browser-pilot] Target ${options.targetId} no longer exists, falling back`);
4771
- targetId = pickBestTarget(pageTargets) ?? (await this.cdp.send("Target.createTarget", {
4772
- url: "about:blank"
4773
- })).targetId;
5432
+ targetId = pickBestTarget(pageTargets) ?? (await this.cdp.send(
5433
+ "Target.createTarget",
5434
+ {
5435
+ url: "about:blank"
5436
+ },
5437
+ null
5438
+ )).targetId;
4774
5439
  }
4775
5440
  } else if (pageTargets.length > 0) {
4776
5441
  targetId = pickBestTarget(pageTargets);
4777
5442
  } else {
4778
- const result = await this.cdp.send("Target.createTarget", {
4779
- url: "about:blank"
4780
- });
5443
+ const result = await this.cdp.send(
5444
+ "Target.createTarget",
5445
+ {
5446
+ url: "about:blank"
5447
+ },
5448
+ null
5449
+ );
4781
5450
  targetId = result.targetId;
4782
5451
  }
4783
5452
  await this.cdp.attachToTarget(targetId);
@@ -4805,13 +5474,17 @@ var Browser = class _Browser {
4805
5474
  * Create a new page (tab)
4806
5475
  */
4807
5476
  async newPage(url = "about:blank") {
4808
- const result = await this.cdp.send("Target.createTarget", {
4809
- url
4810
- });
5477
+ const result = await this.cdp.send(
5478
+ "Target.createTarget",
5479
+ {
5480
+ url
5481
+ },
5482
+ null
5483
+ );
4811
5484
  await this.cdp.attachToTarget(result.targetId);
4812
5485
  const page = new Page(this.cdp, result.targetId);
4813
5486
  await page.init();
4814
- const name = `page-${this.pages.size + 1}`;
5487
+ const name = `page-${++this.pageCounter}`;
4815
5488
  this.pages.set(name, page);
4816
5489
  return page;
4817
5490
  }
@@ -4821,14 +5494,30 @@ var Browser = class _Browser {
4821
5494
  async closePage(name) {
4822
5495
  const page = this.pages.get(name);
4823
5496
  if (!page) return;
4824
- const targets = await this.cdp.send("Target.getTargets");
4825
- const pageTargets = targets.targetInfos.filter((t) => t.type === "page");
4826
- if (pageTargets.length > 0) {
4827
- await this.cdp.send("Target.closeTarget", {
4828
- targetId: pageTargets[0].targetId
4829
- });
4830
- }
5497
+ const targetId = page.targetId;
5498
+ await this.cdp.send("Target.closeTarget", { targetId }, null);
4831
5499
  this.pages.delete(name);
5500
+ const deadline = Date.now() + 5e3;
5501
+ while (Date.now() < deadline) {
5502
+ const { targetInfos } = await this.cdp.send(
5503
+ "Target.getTargets",
5504
+ void 0,
5505
+ null
5506
+ );
5507
+ if (!targetInfos.some((t) => t.targetId === targetId)) return;
5508
+ await new Promise((r) => setTimeout(r, 50));
5509
+ }
5510
+ }
5511
+ /**
5512
+ * List all page targets in the connected browser.
5513
+ */
5514
+ async listTargets() {
5515
+ const { targetInfos } = await this.cdp.send(
5516
+ "Target.getTargets",
5517
+ void 0,
5518
+ null
5519
+ );
5520
+ return targetInfos.filter((target) => target.type === "page");
4832
5521
  }
4833
5522
  /**
4834
5523
  * Get the WebSocket URL for this browser connection