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