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