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.
- package/README.md +72 -10
- package/dist/actions.cjs +481 -24
- package/dist/actions.d.cts +13 -3
- package/dist/actions.d.ts +13 -3
- package/dist/actions.mjs +1 -1
- package/dist/browser-LZTEHUDI.mjs +9 -0
- package/dist/browser.cjs +1227 -75
- package/dist/browser.d.cts +18 -3
- package/dist/browser.d.ts +18 -3
- package/dist/browser.mjs +3 -3
- package/dist/cdp.cjs +32 -3
- package/dist/cdp.d.cts +1 -1
- package/dist/cdp.d.ts +1 -1
- package/dist/cdp.mjs +3 -1
- package/dist/chunk-7NDR6V7S.mjs +7788 -0
- package/dist/{chunk-RUWAXHDX.mjs → chunk-IN5HPAPB.mjs} +749 -60
- package/dist/{chunk-4MBSALQL.mjs → chunk-KIFB526Y.mjs} +45 -3
- package/dist/chunk-LUGLEMVR.mjs +11 -0
- package/dist/chunk-SPSZZH22.mjs +308 -0
- package/dist/{chunk-NLIARNEE.mjs → chunk-XMJABKCF.mjs} +471 -24
- package/dist/cli.mjs +1427 -7092
- package/dist/client-3AFV2IAF.mjs +10 -0
- package/dist/{client-7Nqka5MV.d.ts → client-Ck2nQksT.d.cts} +9 -7
- package/dist/{client-7Nqka5MV.d.cts → client-Ck2nQksT.d.ts} +9 -7
- package/dist/index.cjs +1266 -84
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.mjs +3 -3
- package/dist/transport-WHEBAZUP.mjs +83 -0
- package/dist/{types-j23Iqo2L.d.ts → types-BSoh5v1Y.d.cts} +106 -5
- package/dist/{types-BOPu0OQZ.d.cts → types-CjT0vClo.d.ts} +106 -5
- package/package.json +2 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
createCDPClient
|
|
3
|
-
|
|
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-
|
|
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
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
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
|
-
|
|
1567
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
4246
|
-
if (
|
|
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,
|
|
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(
|
|
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(
|
|
4772
|
-
|
|
4773
|
-
|
|
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(
|
|
4779
|
-
|
|
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(
|
|
4809
|
-
|
|
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.
|
|
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
|
|
4825
|
-
|
|
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
|