browserclaw 0.5.2 → 0.5.5
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/dist/index.cjs +343 -209
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +343 -209
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -829,15 +829,16 @@ async function connectBrowser(cdpUrl, authToken) {
|
|
|
829
829
|
const headers = {};
|
|
830
830
|
if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
|
|
831
831
|
const browser = await playwrightCore.chromium.connectOverCDP(endpoint, { timeout, headers });
|
|
832
|
-
const
|
|
833
|
-
cached = connected;
|
|
834
|
-
observeBrowser(browser);
|
|
835
|
-
browser.on("disconnected", () => {
|
|
832
|
+
const onDisconnected = () => {
|
|
836
833
|
if (cached?.browser === browser) cached = null;
|
|
837
834
|
for (const key of roleRefsByTarget.keys()) {
|
|
838
835
|
if (key.startsWith(normalized + "::")) roleRefsByTarget.delete(key);
|
|
839
836
|
}
|
|
840
|
-
}
|
|
837
|
+
};
|
|
838
|
+
const connected = { browser, cdpUrl: normalized, authToken, onDisconnected };
|
|
839
|
+
cached = connected;
|
|
840
|
+
observeBrowser(browser);
|
|
841
|
+
browser.on("disconnected", onDisconnected);
|
|
841
842
|
return connected;
|
|
842
843
|
} catch (err) {
|
|
843
844
|
lastErr = err;
|
|
@@ -867,6 +868,14 @@ async function disconnectBrowser() {
|
|
|
867
868
|
if (cur) await cur.browser.close().catch(() => {
|
|
868
869
|
});
|
|
869
870
|
}
|
|
871
|
+
function cdpSocketNeedsAttach(wsUrl) {
|
|
872
|
+
try {
|
|
873
|
+
const pathname = new URL(wsUrl).pathname;
|
|
874
|
+
return pathname === "/cdp" || pathname.endsWith("/cdp") || pathname.includes("/devtools/browser/") || pathname === "/";
|
|
875
|
+
} catch {
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
870
879
|
async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
871
880
|
const httpBase = normalizeCdpHttpBaseForJsonEndpoints(cdpUrl);
|
|
872
881
|
const ctrl = new AbortController();
|
|
@@ -883,8 +892,10 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
|
883
892
|
}
|
|
884
893
|
if (!Array.isArray(targets)) return;
|
|
885
894
|
const target = targets.find((entry) => String(entry?.id ?? "").trim() === targetId);
|
|
886
|
-
const
|
|
887
|
-
if (!
|
|
895
|
+
const wsUrlRaw = String(target?.webSocketDebuggerUrl ?? "").trim();
|
|
896
|
+
if (!wsUrlRaw) return;
|
|
897
|
+
const wsUrl = normalizeCdpWsUrl(wsUrlRaw, httpBase);
|
|
898
|
+
const needsAttach = cdpSocketNeedsAttach(wsUrl);
|
|
888
899
|
await new Promise((resolve2) => {
|
|
889
900
|
let done = false;
|
|
890
901
|
const finish = () => {
|
|
@@ -897,8 +908,9 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
|
897
908
|
}
|
|
898
909
|
resolve2();
|
|
899
910
|
};
|
|
900
|
-
const timer = setTimeout(finish,
|
|
911
|
+
const timer = setTimeout(finish, 3e3);
|
|
901
912
|
let ws;
|
|
913
|
+
let nextId = 1;
|
|
902
914
|
try {
|
|
903
915
|
ws = new WebSocket(wsUrl);
|
|
904
916
|
} catch {
|
|
@@ -906,11 +918,27 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
|
906
918
|
return;
|
|
907
919
|
}
|
|
908
920
|
ws.onopen = () => {
|
|
921
|
+
if (needsAttach) {
|
|
922
|
+
ws.send(JSON.stringify({ id: nextId++, method: "Target.attachToTarget", params: { targetId, flatten: true } }));
|
|
923
|
+
} else {
|
|
924
|
+
ws.send(JSON.stringify({ id: nextId++, method: "Runtime.terminateExecution" }));
|
|
925
|
+
setTimeout(finish, 300);
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
ws.onmessage = (event) => {
|
|
929
|
+
if (!needsAttach) return;
|
|
909
930
|
try {
|
|
910
|
-
|
|
931
|
+
const msg = JSON.parse(String(event.data));
|
|
932
|
+
if (msg.id && msg.result?.sessionId) {
|
|
933
|
+
ws.send(JSON.stringify({ id: nextId++, sessionId: msg.result.sessionId, method: "Runtime.terminateExecution" }));
|
|
934
|
+
try {
|
|
935
|
+
ws.send(JSON.stringify({ id: nextId++, method: "Target.detachFromTarget", params: { sessionId: msg.result.sessionId } }));
|
|
936
|
+
} catch {
|
|
937
|
+
}
|
|
938
|
+
setTimeout(finish, 300);
|
|
939
|
+
}
|
|
911
940
|
} catch {
|
|
912
941
|
}
|
|
913
|
-
setTimeout(finish, 300);
|
|
914
942
|
};
|
|
915
943
|
ws.onerror = () => finish();
|
|
916
944
|
ws.onclose = () => finish();
|
|
@@ -922,6 +950,9 @@ async function forceDisconnectPlaywrightForTarget(opts) {
|
|
|
922
950
|
if (!cur || cur.cdpUrl !== normalized) return;
|
|
923
951
|
cached = null;
|
|
924
952
|
connectingByUrl.delete(normalized);
|
|
953
|
+
if (cur.onDisconnected && typeof cur.browser.off === "function") {
|
|
954
|
+
cur.browser.off("disconnected", cur.onDisconnected);
|
|
955
|
+
}
|
|
925
956
|
const targetId = opts.targetId?.trim() || "";
|
|
926
957
|
if (targetId) {
|
|
927
958
|
await tryTerminateExecutionViaCdp(normalized, targetId).catch(() => {
|
|
@@ -957,33 +988,37 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
|
957
988
|
}
|
|
958
989
|
if (tid && tid === targetId) return page;
|
|
959
990
|
}
|
|
960
|
-
if (!resolvedViaCdp && pages.length === 1) {
|
|
961
|
-
return pages[0];
|
|
962
|
-
}
|
|
963
991
|
if (cdpUrl) {
|
|
964
992
|
try {
|
|
965
993
|
const listUrl = `${normalizeCdpHttpBaseForJsonEndpoints(cdpUrl)}/json/list`;
|
|
966
994
|
const headers = {};
|
|
967
995
|
if (cached?.authToken) headers["Authorization"] = `Bearer ${cached.authToken}`;
|
|
968
|
-
const
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
const
|
|
972
|
-
if (
|
|
973
|
-
const
|
|
974
|
-
|
|
975
|
-
if (
|
|
976
|
-
const
|
|
977
|
-
if (
|
|
978
|
-
|
|
979
|
-
|
|
996
|
+
const ctrl = new AbortController();
|
|
997
|
+
const t = setTimeout(() => ctrl.abort(), 2e3);
|
|
998
|
+
try {
|
|
999
|
+
const response = await fetch(listUrl, { headers, signal: ctrl.signal });
|
|
1000
|
+
if (response.ok) {
|
|
1001
|
+
const targets = await response.json();
|
|
1002
|
+
const target = targets.find((entry) => entry.id === targetId);
|
|
1003
|
+
if (target) {
|
|
1004
|
+
const urlMatch = pages.filter((p) => p.url() === target.url);
|
|
1005
|
+
if (urlMatch.length === 1) return urlMatch[0];
|
|
1006
|
+
if (urlMatch.length > 1) {
|
|
1007
|
+
const sameUrlTargets = targets.filter((entry) => entry.url === target.url);
|
|
1008
|
+
if (sameUrlTargets.length === urlMatch.length) {
|
|
1009
|
+
const idx = sameUrlTargets.findIndex((entry) => entry.id === targetId);
|
|
1010
|
+
if (idx >= 0 && idx < urlMatch.length) return urlMatch[idx];
|
|
1011
|
+
}
|
|
980
1012
|
}
|
|
981
1013
|
}
|
|
982
1014
|
}
|
|
1015
|
+
} finally {
|
|
1016
|
+
clearTimeout(t);
|
|
983
1017
|
}
|
|
984
1018
|
} catch {
|
|
985
1019
|
}
|
|
986
1020
|
}
|
|
1021
|
+
if (!resolvedViaCdp && pages.length === 1) return pages[0];
|
|
987
1022
|
return null;
|
|
988
1023
|
}
|
|
989
1024
|
async function getPageForTargetId(opts) {
|
|
@@ -1508,20 +1543,6 @@ function isBlockedHostnameNormalized(normalized) {
|
|
|
1508
1543
|
if (BLOCKED_HOSTNAMES.has(normalized)) return true;
|
|
1509
1544
|
return normalized.endsWith(".localhost") || normalized.endsWith(".local") || normalized.endsWith(".internal");
|
|
1510
1545
|
}
|
|
1511
|
-
function isStrictDecimalOctet(part) {
|
|
1512
|
-
if (!/^[0-9]+$/.test(part)) return false;
|
|
1513
|
-
const n = parseInt(part, 10);
|
|
1514
|
-
if (n < 0 || n > 255) return false;
|
|
1515
|
-
if (String(n) !== part) return false;
|
|
1516
|
-
return true;
|
|
1517
|
-
}
|
|
1518
|
-
function isUnsupportedIPv4Literal(ip) {
|
|
1519
|
-
if (/^[0-9]+$/.test(ip)) return true;
|
|
1520
|
-
const parts = ip.split(".");
|
|
1521
|
-
if (parts.length !== 4) return true;
|
|
1522
|
-
if (!parts.every(isStrictDecimalOctet)) return true;
|
|
1523
|
-
return false;
|
|
1524
|
-
}
|
|
1525
1546
|
var BLOCKED_IPV4_RANGES = /* @__PURE__ */ new Set([
|
|
1526
1547
|
"unspecified",
|
|
1527
1548
|
"broadcast",
|
|
@@ -1540,6 +1561,97 @@ var BLOCKED_IPV6_RANGES = /* @__PURE__ */ new Set([
|
|
|
1540
1561
|
"multicast"
|
|
1541
1562
|
]);
|
|
1542
1563
|
var RFC2544_BENCHMARK_PREFIX = [ipaddr__namespace.IPv4.parse("198.18.0.0"), 15];
|
|
1564
|
+
var EMBEDDED_IPV4_SENTINEL_RULES = [
|
|
1565
|
+
// IPv4-compatible (::a.b.c.d)
|
|
1566
|
+
{
|
|
1567
|
+
matches: (parts) => parts[0] === 0 && parts[1] === 0 && parts[2] === 0 && parts[3] === 0 && parts[4] === 0 && parts[5] === 0,
|
|
1568
|
+
toHextets: (parts) => [parts[6], parts[7]]
|
|
1569
|
+
},
|
|
1570
|
+
// NAT64 local-use (64:ff9b:1::/48)
|
|
1571
|
+
{
|
|
1572
|
+
matches: (parts) => parts[0] === 100 && parts[1] === 65435 && parts[2] === 1 && parts[3] === 0 && parts[4] === 0 && parts[5] === 0,
|
|
1573
|
+
toHextets: (parts) => [parts[6], parts[7]]
|
|
1574
|
+
},
|
|
1575
|
+
// 6to4 (2002::/16)
|
|
1576
|
+
{
|
|
1577
|
+
matches: (parts) => parts[0] === 8194,
|
|
1578
|
+
toHextets: (parts) => [parts[1], parts[2]]
|
|
1579
|
+
},
|
|
1580
|
+
// Teredo (2001:0000::/32) — IPv4 XOR'd
|
|
1581
|
+
{
|
|
1582
|
+
matches: (parts) => parts[0] === 8193 && parts[1] === 0,
|
|
1583
|
+
toHextets: (parts) => [parts[6] ^ 65535, parts[7] ^ 65535]
|
|
1584
|
+
},
|
|
1585
|
+
// ISATAP — sentinel in parts[4-5]: 0x0000:0x5efe or 0x0200:0x5efe
|
|
1586
|
+
{
|
|
1587
|
+
matches: (parts) => (parts[4] & 64767) === 0 && parts[5] === 24318,
|
|
1588
|
+
toHextets: (parts) => [parts[6], parts[7]]
|
|
1589
|
+
}
|
|
1590
|
+
];
|
|
1591
|
+
function stripIpv6Brackets(value) {
|
|
1592
|
+
if (value.startsWith("[") && value.endsWith("]")) return value.slice(1, -1);
|
|
1593
|
+
return value;
|
|
1594
|
+
}
|
|
1595
|
+
function isNumericIpv4LiteralPart(value) {
|
|
1596
|
+
return /^[0-9]+$/.test(value) || /^0x[0-9a-f]+$/i.test(value);
|
|
1597
|
+
}
|
|
1598
|
+
function parseIpv6WithEmbeddedIpv4(raw) {
|
|
1599
|
+
if (!raw.includes(":") || !raw.includes(".")) return;
|
|
1600
|
+
const match = /^(.*:)([^:%]+(?:\.[^:%]+){3})(%[0-9A-Za-z]+)?$/i.exec(raw);
|
|
1601
|
+
if (!match) return;
|
|
1602
|
+
const [, prefix, embeddedIpv4, zoneSuffix = ""] = match;
|
|
1603
|
+
if (!ipaddr__namespace.IPv4.isValidFourPartDecimal(embeddedIpv4)) return;
|
|
1604
|
+
const octets = embeddedIpv4.split(".").map((part) => Number.parseInt(part, 10));
|
|
1605
|
+
const normalizedIpv6 = `${prefix}${(octets[0] << 8 | octets[1]).toString(16)}:${(octets[2] << 8 | octets[3]).toString(16)}${zoneSuffix}`;
|
|
1606
|
+
if (!ipaddr__namespace.IPv6.isValid(normalizedIpv6)) return;
|
|
1607
|
+
return ipaddr__namespace.IPv6.parse(normalizedIpv6);
|
|
1608
|
+
}
|
|
1609
|
+
function normalizeIpParseInput(raw) {
|
|
1610
|
+
const trimmed = raw?.trim();
|
|
1611
|
+
if (!trimmed) return;
|
|
1612
|
+
return stripIpv6Brackets(trimmed);
|
|
1613
|
+
}
|
|
1614
|
+
function parseCanonicalIpAddress(raw) {
|
|
1615
|
+
const normalized = normalizeIpParseInput(raw);
|
|
1616
|
+
if (!normalized) return;
|
|
1617
|
+
if (ipaddr__namespace.IPv4.isValid(normalized)) {
|
|
1618
|
+
if (!ipaddr__namespace.IPv4.isValidFourPartDecimal(normalized)) return;
|
|
1619
|
+
return ipaddr__namespace.IPv4.parse(normalized);
|
|
1620
|
+
}
|
|
1621
|
+
if (ipaddr__namespace.IPv6.isValid(normalized)) return ipaddr__namespace.IPv6.parse(normalized);
|
|
1622
|
+
return parseIpv6WithEmbeddedIpv4(normalized);
|
|
1623
|
+
}
|
|
1624
|
+
function parseLooseIpAddress(raw) {
|
|
1625
|
+
const normalized = normalizeIpParseInput(raw);
|
|
1626
|
+
if (!normalized) return;
|
|
1627
|
+
if (ipaddr__namespace.isValid(normalized)) return ipaddr__namespace.parse(normalized);
|
|
1628
|
+
return parseIpv6WithEmbeddedIpv4(normalized);
|
|
1629
|
+
}
|
|
1630
|
+
function isCanonicalDottedDecimalIPv4(raw) {
|
|
1631
|
+
const trimmed = raw?.trim();
|
|
1632
|
+
if (!trimmed) return false;
|
|
1633
|
+
const normalized = stripIpv6Brackets(trimmed);
|
|
1634
|
+
if (!normalized) return false;
|
|
1635
|
+
return ipaddr__namespace.IPv4.isValidFourPartDecimal(normalized);
|
|
1636
|
+
}
|
|
1637
|
+
function isLegacyIpv4Literal(raw) {
|
|
1638
|
+
const trimmed = raw?.trim();
|
|
1639
|
+
if (!trimmed) return false;
|
|
1640
|
+
const normalized = stripIpv6Brackets(trimmed);
|
|
1641
|
+
if (!normalized || normalized.includes(":")) return false;
|
|
1642
|
+
if (isCanonicalDottedDecimalIPv4(normalized)) return false;
|
|
1643
|
+
const parts = normalized.split(".");
|
|
1644
|
+
if (parts.length === 0 || parts.length > 4) return false;
|
|
1645
|
+
if (parts.some((part) => part.length === 0)) return false;
|
|
1646
|
+
if (!parts.every((part) => isNumericIpv4LiteralPart(part))) return false;
|
|
1647
|
+
return true;
|
|
1648
|
+
}
|
|
1649
|
+
function looksLikeUnsupportedIpv4Literal(address) {
|
|
1650
|
+
const parts = address.split(".");
|
|
1651
|
+
if (parts.length === 0 || parts.length > 4) return false;
|
|
1652
|
+
if (parts.some((part) => part.length === 0)) return true;
|
|
1653
|
+
return parts.every((part) => /^[0-9]+$/.test(part) || /^0x/i.test(part));
|
|
1654
|
+
}
|
|
1543
1655
|
function isBlockedSpecialUseIpv4Address(address, opts) {
|
|
1544
1656
|
const inRfc2544 = address.match(RFC2544_BENCHMARK_PREFIX);
|
|
1545
1657
|
if (inRfc2544 && opts?.allowRfc2544BenchmarkRange === true) return false;
|
|
@@ -1549,89 +1661,50 @@ function isBlockedSpecialUseIpv6Address(address) {
|
|
|
1549
1661
|
if (BLOCKED_IPV6_RANGES.has(address.range())) return true;
|
|
1550
1662
|
return (address.parts[0] & 65472) === 65216;
|
|
1551
1663
|
}
|
|
1552
|
-
function
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
} catch {
|
|
1570
|
-
return true;
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
if (parts[0] === 8194) {
|
|
1574
|
-
const ip4str = `${parts[1] >> 8 & 255}.${parts[1] & 255}.${parts[2] >> 8 & 255}.${parts[2] & 255}`;
|
|
1575
|
-
try {
|
|
1576
|
-
return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
|
|
1577
|
-
} catch {
|
|
1578
|
-
return true;
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
if (parts[0] === 8193 && parts[1] === 0) {
|
|
1582
|
-
const hiXored = parts[6] ^ 65535;
|
|
1583
|
-
const loXored = parts[7] ^ 65535;
|
|
1584
|
-
const ip4str = `${hiXored >> 8 & 255}.${hiXored & 255}.${loXored >> 8 & 255}.${loXored & 255}`;
|
|
1585
|
-
try {
|
|
1586
|
-
return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
|
|
1587
|
-
} catch {
|
|
1588
|
-
return true;
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
|
-
if (parts[0] === 0 && parts[1] === 0 && parts[2] === 0 && parts[3] === 0 && parts[4] === 65535 && parts[5] === 0) {
|
|
1592
|
-
const ip4str = `${parts[6] >> 8 & 255}.${parts[6] & 255}.${parts[7] >> 8 & 255}.${parts[7] & 255}`;
|
|
1593
|
-
try {
|
|
1594
|
-
return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
|
|
1595
|
-
} catch {
|
|
1596
|
-
return true;
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
if (parts[0] === 0 && parts[1] === 0 && parts[2] === 0 && parts[3] === 0 && parts[4] === 0 && parts[5] === 0) {
|
|
1600
|
-
const ip4str = `${parts[6] >> 8 & 255}.${parts[6] & 255}.${parts[7] >> 8 & 255}.${parts[7] & 255}`;
|
|
1601
|
-
try {
|
|
1602
|
-
return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
|
|
1603
|
-
} catch {
|
|
1604
|
-
return true;
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
if ((parts[4] & 65023) === 0 && parts[5] === 24318) {
|
|
1608
|
-
const ip4str = `${parts[6] >> 8 & 255}.${parts[6] & 255}.${parts[7] >> 8 & 255}.${parts[7] & 255}`;
|
|
1609
|
-
try {
|
|
1610
|
-
return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
|
|
1611
|
-
} catch {
|
|
1612
|
-
return true;
|
|
1613
|
-
}
|
|
1664
|
+
function decodeIpv4FromHextets(high, low) {
|
|
1665
|
+
const octets = [
|
|
1666
|
+
high >>> 8 & 255,
|
|
1667
|
+
high & 255,
|
|
1668
|
+
low >>> 8 & 255,
|
|
1669
|
+
low & 255
|
|
1670
|
+
];
|
|
1671
|
+
return ipaddr__namespace.IPv4.parse(octets.join("."));
|
|
1672
|
+
}
|
|
1673
|
+
function extractEmbeddedIpv4FromIpv6(address) {
|
|
1674
|
+
if (address.isIPv4MappedAddress()) return address.toIPv4Address();
|
|
1675
|
+
if (address.range() === "rfc6145") return decodeIpv4FromHextets(address.parts[6], address.parts[7]);
|
|
1676
|
+
if (address.range() === "rfc6052") return decodeIpv4FromHextets(address.parts[6], address.parts[7]);
|
|
1677
|
+
for (const rule of EMBEDDED_IPV4_SENTINEL_RULES) {
|
|
1678
|
+
if (!rule.matches(address.parts)) continue;
|
|
1679
|
+
const [high, low] = rule.toHextets(address.parts);
|
|
1680
|
+
return decodeIpv4FromHextets(high, low);
|
|
1614
1681
|
}
|
|
1615
|
-
return null;
|
|
1616
1682
|
}
|
|
1617
|
-
function
|
|
1683
|
+
function resolveIpv4SpecialUseBlockOptions(policy) {
|
|
1684
|
+
return { allowRfc2544BenchmarkRange: policy?.allowRfc2544BenchmarkRange === true };
|
|
1685
|
+
}
|
|
1686
|
+
function isBlockedHostnameOrIp(hostname, policy) {
|
|
1687
|
+
const normalized = normalizeHostname(hostname);
|
|
1688
|
+
if (!normalized) return false;
|
|
1689
|
+
return isBlockedHostnameNormalized(normalized) || isPrivateIpAddress(normalized, policy);
|
|
1690
|
+
}
|
|
1691
|
+
function isPrivateIpAddress(address, policy) {
|
|
1618
1692
|
let normalized = address.trim().toLowerCase();
|
|
1619
1693
|
if (normalized.startsWith("[") && normalized.endsWith("]")) normalized = normalized.slice(1, -1);
|
|
1620
1694
|
if (!normalized) return false;
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
const v6 = parsed;
|
|
1695
|
+
const blockOptions = resolveIpv4SpecialUseBlockOptions(policy);
|
|
1696
|
+
const strictIp = parseCanonicalIpAddress(normalized);
|
|
1697
|
+
if (strictIp) {
|
|
1698
|
+
if (strictIp.kind() === "ipv4") return isBlockedSpecialUseIpv4Address(strictIp, blockOptions);
|
|
1699
|
+
const v6 = strictIp;
|
|
1627
1700
|
if (isBlockedSpecialUseIpv6Address(v6)) return true;
|
|
1628
|
-
const
|
|
1629
|
-
if (
|
|
1701
|
+
const embeddedIpv4 = extractEmbeddedIpv4FromIpv6(v6);
|
|
1702
|
+
if (embeddedIpv4) return isBlockedSpecialUseIpv4Address(embeddedIpv4, blockOptions);
|
|
1630
1703
|
return false;
|
|
1631
|
-
} catch {
|
|
1632
1704
|
}
|
|
1633
|
-
if (
|
|
1634
|
-
if (normalized
|
|
1705
|
+
if (normalized.includes(":") && !parseLooseIpAddress(normalized)) return true;
|
|
1706
|
+
if (!isCanonicalDottedDecimalIPv4(normalized) && isLegacyIpv4Literal(normalized)) return true;
|
|
1707
|
+
if (looksLikeUnsupportedIpv4Literal(normalized)) return true;
|
|
1635
1708
|
return false;
|
|
1636
1709
|
}
|
|
1637
1710
|
function normalizeHostnameSet(values) {
|
|
@@ -1713,13 +1786,7 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
1713
1786
|
);
|
|
1714
1787
|
}
|
|
1715
1788
|
if (!skipPrivateNetworkChecks) {
|
|
1716
|
-
if (
|
|
1717
|
-
throw new InvalidBrowserNavigationUrlError(
|
|
1718
|
-
`Navigation to internal/loopback address blocked: "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1719
|
-
);
|
|
1720
|
-
}
|
|
1721
|
-
const ipOpts = { allowRfc2544BenchmarkRange: params.policy?.allowRfc2544BenchmarkRange };
|
|
1722
|
-
if (isPrivateIpAddress(normalized, ipOpts)) {
|
|
1789
|
+
if (isBlockedHostnameOrIp(normalized, params.policy)) {
|
|
1723
1790
|
throw new InvalidBrowserNavigationUrlError(
|
|
1724
1791
|
`Navigation to internal/loopback address blocked: "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1725
1792
|
);
|
|
@@ -1740,9 +1807,8 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
1740
1807
|
);
|
|
1741
1808
|
}
|
|
1742
1809
|
if (!skipPrivateNetworkChecks) {
|
|
1743
|
-
const ipOpts = { allowRfc2544BenchmarkRange: params.policy?.allowRfc2544BenchmarkRange };
|
|
1744
1810
|
for (const r of results) {
|
|
1745
|
-
if (
|
|
1811
|
+
if (isBlockedHostnameOrIp(r.address, params.policy)) {
|
|
1746
1812
|
throw new InvalidBrowserNavigationUrlError(
|
|
1747
1813
|
`Navigation to internal/loopback address blocked: "${hostname}" resolves to "${r.address}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1748
1814
|
);
|
|
@@ -1763,6 +1829,7 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
1763
1829
|
}
|
|
1764
1830
|
async function assertBrowserNavigationAllowed(opts) {
|
|
1765
1831
|
const rawUrl = String(opts.url ?? "").trim();
|
|
1832
|
+
if (!rawUrl) throw new InvalidBrowserNavigationUrlError("url is required");
|
|
1766
1833
|
let parsed;
|
|
1767
1834
|
try {
|
|
1768
1835
|
parsed = new URL(rawUrl);
|
|
@@ -1919,12 +1986,30 @@ function resolveBoundedDelayMs(value, label, maxMs) {
|
|
|
1919
1986
|
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${maxMs}ms`);
|
|
1920
1987
|
return normalized;
|
|
1921
1988
|
}
|
|
1922
|
-
|
|
1989
|
+
function resolveInteractionTimeoutMs(timeoutMs) {
|
|
1990
|
+
return Math.max(500, Math.min(6e4, Math.floor(timeoutMs ?? 8e3)));
|
|
1991
|
+
}
|
|
1992
|
+
function requireRefOrSelector(ref, selector) {
|
|
1993
|
+
const trimmedRef = typeof ref === "string" ? ref.trim() : "";
|
|
1994
|
+
const trimmedSelector = typeof selector === "string" ? selector.trim() : "";
|
|
1995
|
+
if (!trimmedRef && !trimmedSelector) throw new Error("ref or selector is required");
|
|
1996
|
+
return { ref: trimmedRef || void 0, selector: trimmedSelector || void 0 };
|
|
1997
|
+
}
|
|
1998
|
+
function resolveLocator(page, resolved) {
|
|
1999
|
+
return resolved.ref ? refLocator(page, resolved.ref) : page.locator(resolved.selector);
|
|
2000
|
+
}
|
|
2001
|
+
async function getRestoredPageForTarget(opts) {
|
|
1923
2002
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1924
2003
|
ensurePageState(page);
|
|
1925
2004
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1926
|
-
|
|
1927
|
-
|
|
2005
|
+
return page;
|
|
2006
|
+
}
|
|
2007
|
+
async function clickViaPlaywright(opts) {
|
|
2008
|
+
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
2009
|
+
const page = await getRestoredPageForTarget(opts);
|
|
2010
|
+
const label = resolved.ref ?? resolved.selector;
|
|
2011
|
+
const locator = resolveLocator(page, resolved);
|
|
2012
|
+
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
1928
2013
|
try {
|
|
1929
2014
|
const delayMs = resolveBoundedDelayMs(opts.delayMs, "click delayMs", MAX_CLICK_DELAY_MS);
|
|
1930
2015
|
if (delayMs > 0) {
|
|
@@ -1937,28 +2022,27 @@ async function clickViaPlaywright(opts) {
|
|
|
1937
2022
|
await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
1938
2023
|
}
|
|
1939
2024
|
} catch (err) {
|
|
1940
|
-
throw toAIFriendlyError(err,
|
|
2025
|
+
throw toAIFriendlyError(err, label);
|
|
1941
2026
|
}
|
|
1942
2027
|
}
|
|
1943
2028
|
async function hoverViaPlaywright(opts) {
|
|
1944
|
-
const
|
|
1945
|
-
|
|
1946
|
-
|
|
2029
|
+
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
2030
|
+
const page = await getRestoredPageForTarget(opts);
|
|
2031
|
+
const label = resolved.ref ?? resolved.selector;
|
|
2032
|
+
const locator = resolveLocator(page, resolved);
|
|
1947
2033
|
try {
|
|
1948
|
-
await
|
|
1949
|
-
timeout: normalizeTimeoutMs(opts.timeoutMs, 8e3, 6e4)
|
|
1950
|
-
});
|
|
2034
|
+
await locator.hover({ timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
|
|
1951
2035
|
} catch (err) {
|
|
1952
|
-
throw toAIFriendlyError(err,
|
|
2036
|
+
throw toAIFriendlyError(err, label);
|
|
1953
2037
|
}
|
|
1954
2038
|
}
|
|
1955
2039
|
async function typeViaPlaywright(opts) {
|
|
2040
|
+
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
1956
2041
|
const text = String(opts.text ?? "");
|
|
1957
|
-
const page = await
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
const
|
|
1961
|
-
const timeout = normalizeTimeoutMs(opts.timeoutMs, 8e3, 6e4);
|
|
2042
|
+
const page = await getRestoredPageForTarget(opts);
|
|
2043
|
+
const label = resolved.ref ?? resolved.selector;
|
|
2044
|
+
const locator = resolveLocator(page, resolved);
|
|
2045
|
+
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
1962
2046
|
try {
|
|
1963
2047
|
if (opts.slowly) {
|
|
1964
2048
|
await locator.click({ timeout });
|
|
@@ -1968,39 +2052,38 @@ async function typeViaPlaywright(opts) {
|
|
|
1968
2052
|
}
|
|
1969
2053
|
if (opts.submit) await locator.press("Enter", { timeout });
|
|
1970
2054
|
} catch (err) {
|
|
1971
|
-
throw toAIFriendlyError(err,
|
|
2055
|
+
throw toAIFriendlyError(err, label);
|
|
1972
2056
|
}
|
|
1973
2057
|
}
|
|
1974
2058
|
async function selectOptionViaPlaywright(opts) {
|
|
2059
|
+
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
1975
2060
|
if (!opts.values?.length) throw new Error("values are required");
|
|
1976
|
-
const page = await
|
|
1977
|
-
|
|
1978
|
-
|
|
2061
|
+
const page = await getRestoredPageForTarget(opts);
|
|
2062
|
+
const label = resolved.ref ?? resolved.selector;
|
|
2063
|
+
const locator = resolveLocator(page, resolved);
|
|
1979
2064
|
try {
|
|
1980
|
-
await
|
|
1981
|
-
timeout: normalizeTimeoutMs(opts.timeoutMs, 8e3, 6e4)
|
|
1982
|
-
});
|
|
2065
|
+
await locator.selectOption(opts.values, { timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
|
|
1983
2066
|
} catch (err) {
|
|
1984
|
-
throw toAIFriendlyError(err,
|
|
2067
|
+
throw toAIFriendlyError(err, label);
|
|
1985
2068
|
}
|
|
1986
2069
|
}
|
|
1987
2070
|
async function dragViaPlaywright(opts) {
|
|
1988
|
-
const
|
|
1989
|
-
|
|
1990
|
-
|
|
2071
|
+
const resolvedStart = requireRefOrSelector(opts.startRef, opts.startSelector);
|
|
2072
|
+
const resolvedEnd = requireRefOrSelector(opts.endRef, opts.endSelector);
|
|
2073
|
+
const page = await getRestoredPageForTarget(opts);
|
|
2074
|
+
const startLocator = resolveLocator(page, resolvedStart);
|
|
2075
|
+
const endLocator = resolveLocator(page, resolvedEnd);
|
|
2076
|
+
const startLabel = resolvedStart.ref ?? resolvedStart.selector;
|
|
2077
|
+
const endLabel = resolvedEnd.ref ?? resolvedEnd.selector;
|
|
1991
2078
|
try {
|
|
1992
|
-
await
|
|
1993
|
-
timeout: normalizeTimeoutMs(opts.timeoutMs, 8e3, 6e4)
|
|
1994
|
-
});
|
|
2079
|
+
await startLocator.dragTo(endLocator, { timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
|
|
1995
2080
|
} catch (err) {
|
|
1996
|
-
throw toAIFriendlyError(err, `${
|
|
2081
|
+
throw toAIFriendlyError(err, `${startLabel} -> ${endLabel}`);
|
|
1997
2082
|
}
|
|
1998
2083
|
}
|
|
1999
2084
|
async function fillFormViaPlaywright(opts) {
|
|
2000
|
-
const page = await
|
|
2001
|
-
|
|
2002
|
-
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
2003
|
-
const timeout = normalizeTimeoutMs(opts.timeoutMs, 8e3, 6e4);
|
|
2085
|
+
const page = await getRestoredPageForTarget(opts);
|
|
2086
|
+
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
2004
2087
|
for (const field of opts.fields) {
|
|
2005
2088
|
const ref = field.ref.trim();
|
|
2006
2089
|
const type = (typeof field.type === "string" ? field.type.trim() : "") || "text";
|
|
@@ -2025,21 +2108,18 @@ async function fillFormViaPlaywright(opts) {
|
|
|
2025
2108
|
}
|
|
2026
2109
|
}
|
|
2027
2110
|
async function scrollIntoViewViaPlaywright(opts) {
|
|
2028
|
-
const
|
|
2029
|
-
|
|
2030
|
-
|
|
2111
|
+
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
2112
|
+
const page = await getRestoredPageForTarget(opts);
|
|
2113
|
+
const label = resolved.ref ?? resolved.selector;
|
|
2114
|
+
const locator = resolveLocator(page, resolved);
|
|
2031
2115
|
try {
|
|
2032
|
-
await
|
|
2033
|
-
timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4)
|
|
2034
|
-
});
|
|
2116
|
+
await locator.scrollIntoViewIfNeeded({ timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
|
|
2035
2117
|
} catch (err) {
|
|
2036
|
-
throw toAIFriendlyError(err,
|
|
2118
|
+
throw toAIFriendlyError(err, label);
|
|
2037
2119
|
}
|
|
2038
2120
|
}
|
|
2039
2121
|
async function highlightViaPlaywright(opts) {
|
|
2040
|
-
const page = await
|
|
2041
|
-
ensurePageState(page);
|
|
2042
|
-
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
2122
|
+
const page = await getRestoredPageForTarget(opts);
|
|
2043
2123
|
try {
|
|
2044
2124
|
await refLocator(page, opts.ref).highlight();
|
|
2045
2125
|
} catch (err) {
|
|
@@ -2047,9 +2127,7 @@ async function highlightViaPlaywright(opts) {
|
|
|
2047
2127
|
}
|
|
2048
2128
|
}
|
|
2049
2129
|
async function setInputFilesViaPlaywright(opts) {
|
|
2050
|
-
const page = await
|
|
2051
|
-
ensurePageState(page);
|
|
2052
|
-
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
2130
|
+
const page = await getRestoredPageForTarget(opts);
|
|
2053
2131
|
if (!opts.paths.length) throw new Error("paths are required");
|
|
2054
2132
|
const inputRef = typeof opts.ref === "string" ? opts.ref.trim() : "";
|
|
2055
2133
|
const element = typeof opts.element === "string" ? opts.element.trim() : "";
|
|
@@ -2242,13 +2320,18 @@ async function resizeViewportViaPlaywright(opts) {
|
|
|
2242
2320
|
|
|
2243
2321
|
// src/actions/wait.ts
|
|
2244
2322
|
var MAX_WAIT_TIME_MS = 3e4;
|
|
2323
|
+
function resolveBoundedDelayMs2(value, label, maxMs) {
|
|
2324
|
+
const normalized = Math.floor(value ?? 0);
|
|
2325
|
+
if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
|
|
2326
|
+
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${maxMs}ms`);
|
|
2327
|
+
return normalized;
|
|
2328
|
+
}
|
|
2245
2329
|
async function waitForViaPlaywright(opts) {
|
|
2246
2330
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2247
2331
|
ensurePageState(page);
|
|
2248
2332
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
|
|
2249
2333
|
if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
|
|
2250
|
-
|
|
2251
|
-
await page.waitForTimeout(bounded);
|
|
2334
|
+
await page.waitForTimeout(resolveBoundedDelayMs2(opts.timeMs, "wait timeMs", MAX_WAIT_TIME_MS));
|
|
2252
2335
|
}
|
|
2253
2336
|
if (opts.text) {
|
|
2254
2337
|
await page.getByText(opts.text).first().waitFor({ state: "visible", timeout });
|
|
@@ -2410,55 +2493,106 @@ async function evaluateViaPlaywright(opts) {
|
|
|
2410
2493
|
if (signal && abortListener) signal.removeEventListener("abort", abortListener);
|
|
2411
2494
|
}
|
|
2412
2495
|
}
|
|
2496
|
+
function createPageDownloadWaiter(page, timeoutMs) {
|
|
2497
|
+
let done = false;
|
|
2498
|
+
let timer;
|
|
2499
|
+
let handler;
|
|
2500
|
+
const cleanup = () => {
|
|
2501
|
+
if (timer) clearTimeout(timer);
|
|
2502
|
+
timer = void 0;
|
|
2503
|
+
if (handler) {
|
|
2504
|
+
page.off("download", handler);
|
|
2505
|
+
handler = void 0;
|
|
2506
|
+
}
|
|
2507
|
+
};
|
|
2508
|
+
return {
|
|
2509
|
+
promise: new Promise((resolve2, reject) => {
|
|
2510
|
+
handler = (download) => {
|
|
2511
|
+
if (done) return;
|
|
2512
|
+
done = true;
|
|
2513
|
+
cleanup();
|
|
2514
|
+
resolve2(download);
|
|
2515
|
+
};
|
|
2516
|
+
page.on("download", handler);
|
|
2517
|
+
timer = setTimeout(() => {
|
|
2518
|
+
if (done) return;
|
|
2519
|
+
done = true;
|
|
2520
|
+
cleanup();
|
|
2521
|
+
reject(new Error("Timeout waiting for download"));
|
|
2522
|
+
}, timeoutMs);
|
|
2523
|
+
}),
|
|
2524
|
+
cancel: () => {
|
|
2525
|
+
if (done) return;
|
|
2526
|
+
done = true;
|
|
2527
|
+
cleanup();
|
|
2528
|
+
}
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
async function saveDownloadPayload(download, outPath) {
|
|
2532
|
+
await writeViaSiblingTempPath({
|
|
2533
|
+
rootDir: path.dirname(outPath),
|
|
2534
|
+
targetPath: outPath,
|
|
2535
|
+
writeTemp: async (tempPath) => {
|
|
2536
|
+
await download.saveAs(tempPath);
|
|
2537
|
+
}
|
|
2538
|
+
});
|
|
2539
|
+
return {
|
|
2540
|
+
url: download.url(),
|
|
2541
|
+
suggestedFilename: download.suggestedFilename(),
|
|
2542
|
+
path: outPath
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
async function awaitDownloadPayload(params) {
|
|
2546
|
+
try {
|
|
2547
|
+
const download = await params.waiter.promise;
|
|
2548
|
+
if (params.state.armIdDownload !== params.armId) throw new Error("Download was superseded by another waiter");
|
|
2549
|
+
return await saveDownloadPayload(download, params.outPath);
|
|
2550
|
+
} catch (err) {
|
|
2551
|
+
params.waiter.cancel();
|
|
2552
|
+
throw err;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2413
2555
|
async function downloadViaPlaywright(opts) {
|
|
2414
2556
|
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
2415
2557
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2416
2558
|
const state = ensurePageState(page);
|
|
2417
2559
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
2418
2560
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
2419
|
-
const
|
|
2561
|
+
const outPath = String(opts.path ?? "").trim();
|
|
2562
|
+
if (!outPath) throw new Error("path is required");
|
|
2420
2563
|
state.armIdDownload = bumpDownloadArmId();
|
|
2564
|
+
const armId = state.armIdDownload;
|
|
2565
|
+
const waiter = createPageDownloadWaiter(page, timeout);
|
|
2421
2566
|
try {
|
|
2422
|
-
const
|
|
2423
|
-
|
|
2424
|
-
locator.click({ timeout })
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
targetPath: outPath,
|
|
2430
|
-
writeTemp: async (tempPath) => {
|
|
2431
|
-
await download.saveAs(tempPath);
|
|
2432
|
-
}
|
|
2433
|
-
});
|
|
2434
|
-
return {
|
|
2435
|
-
url: download.url(),
|
|
2436
|
-
suggestedFilename: download.suggestedFilename(),
|
|
2437
|
-
path: outPath
|
|
2438
|
-
};
|
|
2567
|
+
const locator = refLocator(page, opts.ref);
|
|
2568
|
+
try {
|
|
2569
|
+
await locator.click({ timeout });
|
|
2570
|
+
} catch (err) {
|
|
2571
|
+
throw toAIFriendlyError(err, opts.ref);
|
|
2572
|
+
}
|
|
2573
|
+
return await awaitDownloadPayload({ waiter, state, armId, outPath });
|
|
2439
2574
|
} catch (err) {
|
|
2440
|
-
|
|
2575
|
+
waiter.cancel();
|
|
2576
|
+
throw err;
|
|
2441
2577
|
}
|
|
2442
2578
|
}
|
|
2443
2579
|
async function waitForDownloadViaPlaywright(opts) {
|
|
2444
2580
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2445
|
-
ensurePageState(page);
|
|
2581
|
+
const state = ensurePageState(page);
|
|
2446
2582
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
2447
|
-
|
|
2448
|
-
const
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
})
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
path: savePath
|
|
2461
|
-
};
|
|
2583
|
+
state.armIdDownload = bumpDownloadArmId();
|
|
2584
|
+
const armId = state.armIdDownload;
|
|
2585
|
+
const waiter = createPageDownloadWaiter(page, timeout);
|
|
2586
|
+
try {
|
|
2587
|
+
const download = await waiter.promise;
|
|
2588
|
+
if (state.armIdDownload !== armId) throw new Error("Download was superseded by another waiter");
|
|
2589
|
+
const savePath = opts.path ?? download.suggestedFilename();
|
|
2590
|
+
await assertSafeOutputPath(savePath, opts.allowedOutputRoots);
|
|
2591
|
+
return await saveDownloadPayload(download, savePath);
|
|
2592
|
+
} catch (err) {
|
|
2593
|
+
waiter.cancel();
|
|
2594
|
+
throw err;
|
|
2595
|
+
}
|
|
2462
2596
|
}
|
|
2463
2597
|
async function emulateMediaViaPlaywright(opts) {
|
|
2464
2598
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
@@ -2573,7 +2707,7 @@ async function setLocaleViaPlaywright(opts) {
|
|
|
2573
2707
|
async function setOfflineViaPlaywright(opts) {
|
|
2574
2708
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2575
2709
|
ensurePageState(page);
|
|
2576
|
-
await page.context().setOffline(opts.offline);
|
|
2710
|
+
await page.context().setOffline(Boolean(opts.offline));
|
|
2577
2711
|
}
|
|
2578
2712
|
async function setTimezoneViaPlaywright(opts) {
|
|
2579
2713
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|