browserclaw 0.10.6 → 0.11.1
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 +132 -60
- package/dist/index.cjs +550 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +113 -8
- package/dist/index.d.ts +113 -8
- package/dist/index.js +550 -58
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -37,9 +37,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
37
37
|
mod
|
|
38
38
|
));
|
|
39
39
|
|
|
40
|
-
//
|
|
40
|
+
// node_modules/ipaddr.js/lib/ipaddr.js
|
|
41
41
|
var require_ipaddr = __commonJS({
|
|
42
|
-
"
|
|
42
|
+
"node_modules/ipaddr.js/lib/ipaddr.js"(exports$1, module) {
|
|
43
43
|
(function(root) {
|
|
44
44
|
const ipv4Part = "(0?\\d+|0x[a-f0-9]+)";
|
|
45
45
|
const ipv4Regexes = {
|
|
@@ -835,6 +835,10 @@ var CHROMIUM_BUNDLE_IDS = /* @__PURE__ */ new Set([
|
|
|
835
835
|
"com.microsoft.EdgeBeta",
|
|
836
836
|
"com.microsoft.EdgeDev",
|
|
837
837
|
"com.microsoft.EdgeCanary",
|
|
838
|
+
"com.microsoft.edgemac",
|
|
839
|
+
"com.microsoft.edgemac.beta",
|
|
840
|
+
"com.microsoft.edgemac.dev",
|
|
841
|
+
"com.microsoft.edgemac.canary",
|
|
838
842
|
"org.chromium.Chromium",
|
|
839
843
|
"com.vivaldi.Vivaldi",
|
|
840
844
|
"com.operasoftware.Opera",
|
|
@@ -903,12 +907,12 @@ function fileExists(filePath) {
|
|
|
903
907
|
return false;
|
|
904
908
|
}
|
|
905
909
|
}
|
|
906
|
-
function execText(command, args, timeoutMs = 1200) {
|
|
910
|
+
function execText(command, args, timeoutMs = 1200, maxBuffer = 1024 * 1024) {
|
|
907
911
|
try {
|
|
908
912
|
const output = execFileSync(command, args, {
|
|
909
913
|
timeout: timeoutMs,
|
|
910
914
|
encoding: "utf8",
|
|
911
|
-
maxBuffer
|
|
915
|
+
maxBuffer
|
|
912
916
|
});
|
|
913
917
|
return output.trim() || null;
|
|
914
918
|
} catch {
|
|
@@ -944,7 +948,12 @@ function detectDefaultBrowserBundleIdMac() {
|
|
|
944
948
|
"Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist"
|
|
945
949
|
);
|
|
946
950
|
if (!fileExists(plistPath)) return null;
|
|
947
|
-
const handlersRaw = execText(
|
|
951
|
+
const handlersRaw = execText(
|
|
952
|
+
"/usr/bin/plutil",
|
|
953
|
+
["-extract", "LSHandlers", "json", "-o", "-", "--", plistPath],
|
|
954
|
+
2e3,
|
|
955
|
+
5 * 1024 * 1024
|
|
956
|
+
);
|
|
948
957
|
if (handlersRaw === null) return null;
|
|
949
958
|
let handlers;
|
|
950
959
|
try {
|
|
@@ -996,6 +1005,34 @@ function findChromeMac() {
|
|
|
996
1005
|
}
|
|
997
1006
|
]);
|
|
998
1007
|
}
|
|
1008
|
+
function splitExecLine(line) {
|
|
1009
|
+
const tokens = [];
|
|
1010
|
+
let current = "";
|
|
1011
|
+
let inQuotes = false;
|
|
1012
|
+
let quoteChar = "";
|
|
1013
|
+
for (const ch of line) {
|
|
1014
|
+
if ((ch === '"' || ch === "'") && (!inQuotes || ch === quoteChar)) {
|
|
1015
|
+
if (inQuotes) {
|
|
1016
|
+
inQuotes = false;
|
|
1017
|
+
quoteChar = "";
|
|
1018
|
+
} else {
|
|
1019
|
+
inQuotes = true;
|
|
1020
|
+
quoteChar = ch;
|
|
1021
|
+
}
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
if (!inQuotes && /\s/.test(ch)) {
|
|
1025
|
+
if (current) {
|
|
1026
|
+
tokens.push(current);
|
|
1027
|
+
current = "";
|
|
1028
|
+
}
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
current += ch;
|
|
1032
|
+
}
|
|
1033
|
+
if (current) tokens.push(current);
|
|
1034
|
+
return tokens;
|
|
1035
|
+
}
|
|
999
1036
|
function detectDefaultChromiumLinux() {
|
|
1000
1037
|
const desktopId = execText("xdg-settings", ["get", "default-web-browser"]) ?? execText("xdg-mime", ["query", "default", "x-scheme-handler/http"]);
|
|
1001
1038
|
if (desktopId === null) return null;
|
|
@@ -1027,10 +1064,10 @@ function detectDefaultChromiumLinux() {
|
|
|
1027
1064
|
} catch {
|
|
1028
1065
|
}
|
|
1029
1066
|
if (execLine === null) return null;
|
|
1030
|
-
const tokens = execLine
|
|
1067
|
+
const tokens = splitExecLine(execLine);
|
|
1031
1068
|
let command = null;
|
|
1032
1069
|
for (const token of tokens) {
|
|
1033
|
-
if (!token || token === "env" || token.includes("=") && !token.startsWith("/")) continue;
|
|
1070
|
+
if (!token || token === "env" || token.includes("=") && !token.startsWith("/") && !token.includes("\\")) continue;
|
|
1034
1071
|
command = token.replace(/^["']|["']$/g, "");
|
|
1035
1072
|
break;
|
|
1036
1073
|
}
|
|
@@ -1087,6 +1124,49 @@ function findChromeWindows() {
|
|
|
1087
1124
|
candidates.push({ kind: "edge", path: j(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe") });
|
|
1088
1125
|
return findFirstExe(candidates);
|
|
1089
1126
|
}
|
|
1127
|
+
function readWindowsProgId() {
|
|
1128
|
+
const output = execText("reg", [
|
|
1129
|
+
"query",
|
|
1130
|
+
"HKCU\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice",
|
|
1131
|
+
"/v",
|
|
1132
|
+
"ProgId"
|
|
1133
|
+
]);
|
|
1134
|
+
if (output === null) return null;
|
|
1135
|
+
return /ProgId\s+REG_\w+\s+(.+)$/im.exec(output)?.[1]?.trim() ?? null;
|
|
1136
|
+
}
|
|
1137
|
+
function readWindowsCommandForProgId(progId) {
|
|
1138
|
+
const output = execText("reg", [
|
|
1139
|
+
"query",
|
|
1140
|
+
progId === "http" ? "HKCR\\http\\shell\\open\\command" : `HKCR\\${progId}\\shell\\open\\command`,
|
|
1141
|
+
"/ve"
|
|
1142
|
+
]);
|
|
1143
|
+
if (output === null) return null;
|
|
1144
|
+
return /REG_\w+\s+(.+)$/im.exec(output)?.[1]?.trim() ?? null;
|
|
1145
|
+
}
|
|
1146
|
+
function expandWindowsEnvVars(value) {
|
|
1147
|
+
return value.replace(/%([^%]+)%/g, (_match, name) => {
|
|
1148
|
+
const key = name.trim();
|
|
1149
|
+
return key !== "" ? process.env[key] ?? `%${key}%` : _match;
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
function extractWindowsExecutablePath(command) {
|
|
1153
|
+
const quoted = /"([^"]+\.exe)"/i.exec(command);
|
|
1154
|
+
if (quoted?.[1] !== void 0) return quoted[1];
|
|
1155
|
+
const unquoted = /([^\s]+\.exe)/i.exec(command);
|
|
1156
|
+
if (unquoted?.[1] !== void 0) return unquoted[1];
|
|
1157
|
+
return null;
|
|
1158
|
+
}
|
|
1159
|
+
function detectDefaultChromiumWindows() {
|
|
1160
|
+
const progId = readWindowsProgId();
|
|
1161
|
+
const command = (progId !== null ? readWindowsCommandForProgId(progId) : null) ?? readWindowsCommandForProgId("http");
|
|
1162
|
+
if (command === null) return null;
|
|
1163
|
+
const exePath = extractWindowsExecutablePath(expandWindowsEnvVars(command));
|
|
1164
|
+
if (exePath === null) return null;
|
|
1165
|
+
if (!fileExists(exePath)) return null;
|
|
1166
|
+
const exeName = path.win32.basename(exePath).toLowerCase();
|
|
1167
|
+
if (!CHROMIUM_EXE_NAMES.has(exeName)) return null;
|
|
1168
|
+
return { kind: inferKindFromExeName(exeName), path: exePath };
|
|
1169
|
+
}
|
|
1090
1170
|
function resolveBrowserExecutable(opts) {
|
|
1091
1171
|
if (opts?.executablePath !== void 0 && opts.executablePath !== "") {
|
|
1092
1172
|
if (!fileExists(opts.executablePath)) throw new Error(`executablePath not found: ${opts.executablePath}`);
|
|
@@ -1095,7 +1175,7 @@ function resolveBrowserExecutable(opts) {
|
|
|
1095
1175
|
const platform = process.platform;
|
|
1096
1176
|
if (platform === "darwin") return detectDefaultChromiumMac() ?? findChromeMac();
|
|
1097
1177
|
if (platform === "linux") return detectDefaultChromiumLinux() ?? findChromeLinux();
|
|
1098
|
-
if (platform === "win32") return findChromeWindows();
|
|
1178
|
+
if (platform === "win32") return detectDefaultChromiumWindows() ?? findChromeWindows();
|
|
1099
1179
|
return null;
|
|
1100
1180
|
}
|
|
1101
1181
|
async function ensurePortAvailable(port) {
|
|
@@ -1185,7 +1265,8 @@ function isWebSocketUrl(url) {
|
|
|
1185
1265
|
}
|
|
1186
1266
|
}
|
|
1187
1267
|
function isLoopbackHost(hostname) {
|
|
1188
|
-
|
|
1268
|
+
const h = hostname.replace(/\.+$/, "");
|
|
1269
|
+
return h === "localhost" || h === "127.0.0.1" || h === "::1" || h === "[::1]";
|
|
1189
1270
|
}
|
|
1190
1271
|
var PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"];
|
|
1191
1272
|
function hasProxyEnvConfigured(env = process.env) {
|
|
@@ -1377,6 +1458,7 @@ async function launchChrome(opts = {}) {
|
|
|
1377
1458
|
const spawnChrome = () => {
|
|
1378
1459
|
const args = [
|
|
1379
1460
|
`--remote-debugging-port=${String(cdpPort)}`,
|
|
1461
|
+
"--remote-debugging-address=127.0.0.1",
|
|
1380
1462
|
`--user-data-dir=${userDataDir}`,
|
|
1381
1463
|
"--no-first-run",
|
|
1382
1464
|
"--no-default-browser-check",
|
|
@@ -1395,6 +1477,9 @@ async function launchChrome(opts = {}) {
|
|
|
1395
1477
|
if (opts.noSandbox === true) {
|
|
1396
1478
|
args.push("--no-sandbox", "--disable-setuid-sandbox");
|
|
1397
1479
|
}
|
|
1480
|
+
if (opts.ignoreHTTPSErrors === true) {
|
|
1481
|
+
args.push("--ignore-certificate-errors");
|
|
1482
|
+
}
|
|
1398
1483
|
if (process.platform === "linux") args.push("--disable-dev-shm-usage");
|
|
1399
1484
|
const extraArgs = Array.isArray(opts.chromeArgs) ? opts.chromeArgs.filter((a) => typeof a === "string" && a.trim().length > 0) : [];
|
|
1400
1485
|
if (extraArgs.length) args.push(...extraArgs);
|
|
@@ -1715,7 +1800,15 @@ function appendCdpPath2(cdpUrl, cdpPath) {
|
|
|
1715
1800
|
}
|
|
1716
1801
|
}
|
|
1717
1802
|
async function withPlaywrightPageCdpSession(page, fn) {
|
|
1718
|
-
const
|
|
1803
|
+
const CDP_SESSION_TIMEOUT_MS = 1e4;
|
|
1804
|
+
const session = await Promise.race([
|
|
1805
|
+
page.context().newCDPSession(page),
|
|
1806
|
+
new Promise((_, reject) => {
|
|
1807
|
+
setTimeout(() => {
|
|
1808
|
+
reject(new Error("newCDPSession timed out after 10s"));
|
|
1809
|
+
}, CDP_SESSION_TIMEOUT_MS);
|
|
1810
|
+
})
|
|
1811
|
+
]);
|
|
1719
1812
|
try {
|
|
1720
1813
|
return await fn(session);
|
|
1721
1814
|
} finally {
|
|
@@ -1825,6 +1918,56 @@ function bumpDownloadArmId() {
|
|
|
1825
1918
|
nextDownloadArmId += 1;
|
|
1826
1919
|
return nextDownloadArmId;
|
|
1827
1920
|
}
|
|
1921
|
+
var BlockedBrowserTargetError = class extends Error {
|
|
1922
|
+
constructor() {
|
|
1923
|
+
super("Browser target is unavailable after SSRF policy blocked its navigation.");
|
|
1924
|
+
this.name = "BlockedBrowserTargetError";
|
|
1925
|
+
}
|
|
1926
|
+
};
|
|
1927
|
+
var blockedTargetsByCdpUrl = /* @__PURE__ */ new Set();
|
|
1928
|
+
var blockedPageRefsByCdpUrl = /* @__PURE__ */ new Map();
|
|
1929
|
+
function blockedTargetKey(cdpUrl, targetId) {
|
|
1930
|
+
return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
|
|
1931
|
+
}
|
|
1932
|
+
function isBlockedTarget(cdpUrl, targetId) {
|
|
1933
|
+
const normalized = targetId?.trim() ?? "";
|
|
1934
|
+
if (normalized === "") return false;
|
|
1935
|
+
return blockedTargetsByCdpUrl.has(blockedTargetKey(cdpUrl, normalized));
|
|
1936
|
+
}
|
|
1937
|
+
function markTargetBlocked(cdpUrl, targetId) {
|
|
1938
|
+
const normalized = targetId?.trim() ?? "";
|
|
1939
|
+
if (normalized === "") return;
|
|
1940
|
+
blockedTargetsByCdpUrl.add(blockedTargetKey(cdpUrl, normalized));
|
|
1941
|
+
}
|
|
1942
|
+
function clearBlockedTarget(cdpUrl, targetId) {
|
|
1943
|
+
const normalized = targetId?.trim() ?? "";
|
|
1944
|
+
if (normalized === "") return;
|
|
1945
|
+
blockedTargetsByCdpUrl.delete(blockedTargetKey(cdpUrl, normalized));
|
|
1946
|
+
}
|
|
1947
|
+
function hasBlockedTargetsForCdpUrl(cdpUrl) {
|
|
1948
|
+
const prefix = `${normalizeCdpUrl(cdpUrl)}::`;
|
|
1949
|
+
for (const key of blockedTargetsByCdpUrl) {
|
|
1950
|
+
if (key.startsWith(prefix)) return true;
|
|
1951
|
+
}
|
|
1952
|
+
return false;
|
|
1953
|
+
}
|
|
1954
|
+
function blockedPageRefsForCdpUrl(cdpUrl) {
|
|
1955
|
+
const normalized = normalizeCdpUrl(cdpUrl);
|
|
1956
|
+
const existing = blockedPageRefsByCdpUrl.get(normalized);
|
|
1957
|
+
if (existing) return existing;
|
|
1958
|
+
const created = /* @__PURE__ */ new WeakSet();
|
|
1959
|
+
blockedPageRefsByCdpUrl.set(normalized, created);
|
|
1960
|
+
return created;
|
|
1961
|
+
}
|
|
1962
|
+
function isBlockedPageRef(cdpUrl, page) {
|
|
1963
|
+
return blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.has(page) ?? false;
|
|
1964
|
+
}
|
|
1965
|
+
function markPageRefBlocked(cdpUrl, page) {
|
|
1966
|
+
blockedPageRefsForCdpUrl(cdpUrl).add(page);
|
|
1967
|
+
}
|
|
1968
|
+
function clearBlockedPageRef(cdpUrl, page) {
|
|
1969
|
+
blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.delete(page);
|
|
1970
|
+
}
|
|
1828
1971
|
function ensureContextState(context) {
|
|
1829
1972
|
const existing = contextStates.get(context);
|
|
1830
1973
|
if (existing) return existing;
|
|
@@ -2265,11 +2408,43 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
|
2265
2408
|
if (!resolvedViaCdp && pages.length === 1) return pages[0] ?? null;
|
|
2266
2409
|
return null;
|
|
2267
2410
|
}
|
|
2411
|
+
async function partitionAccessiblePages(opts) {
|
|
2412
|
+
const accessible = [];
|
|
2413
|
+
let blockedCount = 0;
|
|
2414
|
+
for (const page of opts.pages) {
|
|
2415
|
+
if (isBlockedPageRef(opts.cdpUrl, page)) {
|
|
2416
|
+
blockedCount += 1;
|
|
2417
|
+
continue;
|
|
2418
|
+
}
|
|
2419
|
+
const targetId = await pageTargetId(page).catch(() => null);
|
|
2420
|
+
if (targetId === null || targetId === "") {
|
|
2421
|
+
if (hasBlockedTargetsForCdpUrl(opts.cdpUrl)) {
|
|
2422
|
+
blockedCount += 1;
|
|
2423
|
+
continue;
|
|
2424
|
+
}
|
|
2425
|
+
accessible.push(page);
|
|
2426
|
+
continue;
|
|
2427
|
+
}
|
|
2428
|
+
if (isBlockedTarget(opts.cdpUrl, targetId)) {
|
|
2429
|
+
blockedCount += 1;
|
|
2430
|
+
continue;
|
|
2431
|
+
}
|
|
2432
|
+
accessible.push(page);
|
|
2433
|
+
}
|
|
2434
|
+
return { accessible, blockedCount };
|
|
2435
|
+
}
|
|
2268
2436
|
async function getPageForTargetId(opts) {
|
|
2437
|
+
if (opts.targetId !== void 0 && opts.targetId !== "" && isBlockedTarget(opts.cdpUrl, opts.targetId))
|
|
2438
|
+
throw new BlockedBrowserTargetError();
|
|
2269
2439
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
2270
2440
|
const pages = getAllPages(browser);
|
|
2271
2441
|
if (!pages.length) throw new Error("No pages available in the connected browser.");
|
|
2272
|
-
const
|
|
2442
|
+
const { accessible, blockedCount } = await partitionAccessiblePages({ cdpUrl: opts.cdpUrl, pages });
|
|
2443
|
+
if (!accessible.length) {
|
|
2444
|
+
if (blockedCount > 0) throw new BlockedBrowserTargetError();
|
|
2445
|
+
throw new Error("No pages available in the connected browser.");
|
|
2446
|
+
}
|
|
2447
|
+
const first = accessible[0];
|
|
2273
2448
|
if (opts.targetId === void 0 || opts.targetId === "") return first;
|
|
2274
2449
|
const found = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
|
|
2275
2450
|
if (!found) {
|
|
@@ -2278,6 +2453,10 @@ async function getPageForTargetId(opts) {
|
|
|
2278
2453
|
`Tab not found (targetId: ${opts.targetId}). Call browser.tabs() to list open tabs.`
|
|
2279
2454
|
);
|
|
2280
2455
|
}
|
|
2456
|
+
if (isBlockedPageRef(opts.cdpUrl, found)) throw new BlockedBrowserTargetError();
|
|
2457
|
+
const foundTargetId = await pageTargetId(found).catch(() => null);
|
|
2458
|
+
if (foundTargetId !== null && foundTargetId !== "" && isBlockedTarget(opts.cdpUrl, foundTargetId))
|
|
2459
|
+
throw new BlockedBrowserTargetError();
|
|
2281
2460
|
return found;
|
|
2282
2461
|
}
|
|
2283
2462
|
async function resolvePageByTargetIdOrThrow(opts) {
|
|
@@ -2355,7 +2534,14 @@ function toAIFriendlyError(error, selector) {
|
|
|
2355
2534
|
`Element "${selector}" is not interactable (hidden or covered). Try scrolling it into view, closing overlays, or re-snapshotting.`
|
|
2356
2535
|
);
|
|
2357
2536
|
}
|
|
2358
|
-
|
|
2537
|
+
const timeoutMatch = /Timeout (\d+)ms exceeded/.exec(message);
|
|
2538
|
+
if (timeoutMatch) {
|
|
2539
|
+
return new Error(
|
|
2540
|
+
`Element "${selector}" timed out after ${timeoutMatch[1]}ms \u2014 element may be hidden or not interactable. Run a new snapshot to see current page elements.`
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
const cleaned = message.replace(/locator\([^)]*\)\./g, "").replace(/waiting for locator\([^)]*\)/g, "").trim();
|
|
2544
|
+
return new Error(cleaned || message);
|
|
2359
2545
|
}
|
|
2360
2546
|
function normalizeTimeoutMs(timeoutMs, fallback, maxMs = 12e4) {
|
|
2361
2547
|
return Math.max(500, Math.min(maxMs, timeoutMs ?? fallback));
|
|
@@ -3053,7 +3239,19 @@ function requiresInspectableBrowserNavigationRedirects(ssrfPolicy) {
|
|
|
3053
3239
|
|
|
3054
3240
|
// src/actions/interaction.ts
|
|
3055
3241
|
var MAX_CLICK_DELAY_MS = 5e3;
|
|
3056
|
-
var
|
|
3242
|
+
var DEFAULT_SCROLL_TIMEOUT_MS = 2e4;
|
|
3243
|
+
var CHECKABLE_ROLES = /* @__PURE__ */ new Set(["menuitemcheckbox", "menuitemradio", "checkbox", "radio", "switch"]);
|
|
3244
|
+
async function setCheckedViaEvaluate(locator, checked) {
|
|
3245
|
+
await locator.evaluate((el, desired) => {
|
|
3246
|
+
const input = el;
|
|
3247
|
+
const desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "checked");
|
|
3248
|
+
if (desc?.set) desc.set.call(input, desired);
|
|
3249
|
+
else input.checked = desired;
|
|
3250
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
3251
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
3252
|
+
input.click();
|
|
3253
|
+
}, checked);
|
|
3254
|
+
}
|
|
3057
3255
|
function resolveLocator(page, resolved) {
|
|
3058
3256
|
if (resolved.ref !== void 0 && resolved.ref !== "") return refLocator(page, resolved.ref);
|
|
3059
3257
|
const sel = resolved.selector ?? "";
|
|
@@ -3067,11 +3265,29 @@ async function mouseClickViaPlaywright(opts) {
|
|
|
3067
3265
|
delay: opts.delayMs
|
|
3068
3266
|
});
|
|
3069
3267
|
}
|
|
3268
|
+
async function pressAndHoldViaCdp(opts) {
|
|
3269
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3270
|
+
ensurePageState(page);
|
|
3271
|
+
const { x, y } = opts;
|
|
3272
|
+
await withPageScopedCdpClient({
|
|
3273
|
+
cdpUrl: opts.cdpUrl,
|
|
3274
|
+
page,
|
|
3275
|
+
targetId: opts.targetId,
|
|
3276
|
+
fn: async (send) => {
|
|
3277
|
+
await send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y, button: "none" });
|
|
3278
|
+
if (opts.delay !== void 0 && opts.delay !== 0) await new Promise((r) => setTimeout(r, opts.delay));
|
|
3279
|
+
await send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 });
|
|
3280
|
+
if (opts.holdMs !== void 0 && opts.holdMs !== 0) await new Promise((r) => setTimeout(r, opts.holdMs));
|
|
3281
|
+
await send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 });
|
|
3282
|
+
}
|
|
3283
|
+
});
|
|
3284
|
+
}
|
|
3070
3285
|
async function clickByTextViaPlaywright(opts) {
|
|
3071
3286
|
const page = await getRestoredPageForTarget(opts);
|
|
3072
3287
|
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
3288
|
+
const locator = page.getByText(opts.text, { exact: opts.exact }).and(page.locator(":visible")).or(page.getByTitle(opts.text, { exact: opts.exact })).first();
|
|
3073
3289
|
try {
|
|
3074
|
-
await
|
|
3290
|
+
await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
3075
3291
|
} catch (err) {
|
|
3076
3292
|
throw toAIFriendlyError(err, `text="${opts.text}"`);
|
|
3077
3293
|
}
|
|
@@ -3079,13 +3295,12 @@ async function clickByTextViaPlaywright(opts) {
|
|
|
3079
3295
|
async function clickByRoleViaPlaywright(opts) {
|
|
3080
3296
|
const page = await getRestoredPageForTarget(opts);
|
|
3081
3297
|
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
3298
|
+
const label = `role=${opts.role}${opts.name !== void 0 && opts.name !== "" ? ` name="${opts.name}"` : ""}`;
|
|
3299
|
+
const locator = page.getByRole(opts.role, { name: opts.name }).nth(opts.index ?? 0);
|
|
3082
3300
|
try {
|
|
3083
|
-
await
|
|
3301
|
+
await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
3084
3302
|
} catch (err) {
|
|
3085
|
-
throw toAIFriendlyError(
|
|
3086
|
-
err,
|
|
3087
|
-
`role=${opts.role}${opts.name !== void 0 && opts.name !== "" ? ` name="${opts.name}"` : ""}`
|
|
3088
|
-
);
|
|
3303
|
+
throw toAIFriendlyError(err, label);
|
|
3089
3304
|
}
|
|
3090
3305
|
}
|
|
3091
3306
|
async function clickViaPlaywright(opts) {
|
|
@@ -3106,7 +3321,7 @@ async function clickViaPlaywright(opts) {
|
|
|
3106
3321
|
try {
|
|
3107
3322
|
const delayMs = resolveBoundedDelayMs(opts.delayMs, "click delayMs", MAX_CLICK_DELAY_MS);
|
|
3108
3323
|
if (delayMs > 0) {
|
|
3109
|
-
await locator.hover({ timeout });
|
|
3324
|
+
await locator.hover({ timeout, force: opts.force });
|
|
3110
3325
|
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
3111
3326
|
}
|
|
3112
3327
|
let ariaCheckedBefore;
|
|
@@ -3114,9 +3329,9 @@ async function clickViaPlaywright(opts) {
|
|
|
3114
3329
|
ariaCheckedBefore = await locator.getAttribute("aria-checked", { timeout }).catch(() => void 0);
|
|
3115
3330
|
}
|
|
3116
3331
|
if (opts.doubleClick === true) {
|
|
3117
|
-
await locator.dblclick({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
3332
|
+
await locator.dblclick({ timeout, button: opts.button, modifiers: opts.modifiers, force: opts.force });
|
|
3118
3333
|
} else {
|
|
3119
|
-
await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
3334
|
+
await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers, force: opts.force });
|
|
3120
3335
|
}
|
|
3121
3336
|
if (checkableRole && opts.doubleClick !== true && ariaCheckedBefore !== void 0) {
|
|
3122
3337
|
const POLL_INTERVAL_MS = 50;
|
|
@@ -3210,9 +3425,13 @@ async function fillFormViaPlaywright(opts) {
|
|
|
3210
3425
|
if (type === "checkbox" || type === "radio") {
|
|
3211
3426
|
const checked = rawValue === true || rawValue === 1 || rawValue === "1" || rawValue === "true";
|
|
3212
3427
|
try {
|
|
3213
|
-
await locator.setChecked(checked, { timeout });
|
|
3214
|
-
} catch
|
|
3215
|
-
|
|
3428
|
+
await locator.setChecked(checked, { timeout, force: true });
|
|
3429
|
+
} catch {
|
|
3430
|
+
try {
|
|
3431
|
+
await setCheckedViaEvaluate(locator, checked);
|
|
3432
|
+
} catch (err) {
|
|
3433
|
+
throw toAIFriendlyError(err, ref);
|
|
3434
|
+
}
|
|
3216
3435
|
}
|
|
3217
3436
|
continue;
|
|
3218
3437
|
}
|
|
@@ -3229,7 +3448,13 @@ async function scrollIntoViewViaPlaywright(opts) {
|
|
|
3229
3448
|
const label = resolved.ref ?? resolved.selector ?? "";
|
|
3230
3449
|
const locator = resolveLocator(page, resolved);
|
|
3231
3450
|
try {
|
|
3232
|
-
await locator.
|
|
3451
|
+
await locator.waitFor({
|
|
3452
|
+
state: "attached",
|
|
3453
|
+
timeout: normalizeTimeoutMs(opts.timeoutMs, DEFAULT_SCROLL_TIMEOUT_MS)
|
|
3454
|
+
});
|
|
3455
|
+
await locator.evaluate((el) => {
|
|
3456
|
+
el.scrollIntoView({ block: "center", behavior: "instant" });
|
|
3457
|
+
});
|
|
3233
3458
|
} catch (err) {
|
|
3234
3459
|
throw toAIFriendlyError(err, label);
|
|
3235
3460
|
}
|
|
@@ -3359,6 +3584,82 @@ function isRetryableNavigateError(err) {
|
|
|
3359
3584
|
const msg = typeof err === "string" ? err.toLowerCase() : err instanceof Error ? err.message.toLowerCase() : "";
|
|
3360
3585
|
return msg.includes("frame has been detached") || msg.includes("target page, context or browser has been closed");
|
|
3361
3586
|
}
|
|
3587
|
+
function isPolicyDenyNavigationError(err) {
|
|
3588
|
+
return err instanceof InvalidBrowserNavigationUrlError;
|
|
3589
|
+
}
|
|
3590
|
+
function isTopLevelNavigationRequest(page, request) {
|
|
3591
|
+
if (!request.isNavigationRequest()) return false;
|
|
3592
|
+
try {
|
|
3593
|
+
return request.frame() === page.mainFrame();
|
|
3594
|
+
} catch {
|
|
3595
|
+
return true;
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
async function closeBlockedNavigationTarget(opts) {
|
|
3599
|
+
markPageRefBlocked(opts.cdpUrl, opts.page);
|
|
3600
|
+
const resolvedTargetId = await pageTargetId(opts.page).catch(() => null);
|
|
3601
|
+
const fallbackTargetId = opts.targetId?.trim() ?? "";
|
|
3602
|
+
const targetIdToBlock = resolvedTargetId ?? fallbackTargetId;
|
|
3603
|
+
if (targetIdToBlock) markTargetBlocked(opts.cdpUrl, targetIdToBlock);
|
|
3604
|
+
await opts.page.close().catch((e) => {
|
|
3605
|
+
console.warn("[browserclaw] failed to close blocked page", e);
|
|
3606
|
+
});
|
|
3607
|
+
}
|
|
3608
|
+
async function assertPageNavigationCompletedSafely(opts) {
|
|
3609
|
+
const navigationPolicy = withBrowserNavigationPolicy(opts.ssrfPolicy);
|
|
3610
|
+
try {
|
|
3611
|
+
await assertBrowserNavigationRedirectChainAllowed({ request: opts.response?.request(), ...navigationPolicy });
|
|
3612
|
+
await assertBrowserNavigationResultAllowed({ url: opts.page.url(), ...navigationPolicy });
|
|
3613
|
+
} catch (err) {
|
|
3614
|
+
if (isPolicyDenyNavigationError(err))
|
|
3615
|
+
await closeBlockedNavigationTarget({ cdpUrl: opts.cdpUrl, page: opts.page, targetId: opts.targetId });
|
|
3616
|
+
throw err;
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
async function gotoPageWithNavigationGuard(opts) {
|
|
3620
|
+
const navigationPolicy = withBrowserNavigationPolicy(opts.ssrfPolicy);
|
|
3621
|
+
const state = { blocked: null };
|
|
3622
|
+
const handler = async (route, request) => {
|
|
3623
|
+
if (state.blocked !== null) {
|
|
3624
|
+
await route.abort().catch((e) => {
|
|
3625
|
+
console.warn("[browserclaw] route abort failed", e);
|
|
3626
|
+
});
|
|
3627
|
+
return;
|
|
3628
|
+
}
|
|
3629
|
+
if (!isTopLevelNavigationRequest(opts.page, request)) {
|
|
3630
|
+
await route.continue();
|
|
3631
|
+
return;
|
|
3632
|
+
}
|
|
3633
|
+
try {
|
|
3634
|
+
await assertBrowserNavigationAllowed({ url: request.url(), ...navigationPolicy });
|
|
3635
|
+
} catch (err) {
|
|
3636
|
+
if (isPolicyDenyNavigationError(err)) {
|
|
3637
|
+
state.blocked = err;
|
|
3638
|
+
await route.abort().catch((e) => {
|
|
3639
|
+
console.warn("[browserclaw] route abort failed", e);
|
|
3640
|
+
});
|
|
3641
|
+
return;
|
|
3642
|
+
}
|
|
3643
|
+
throw err;
|
|
3644
|
+
}
|
|
3645
|
+
await route.continue();
|
|
3646
|
+
};
|
|
3647
|
+
await opts.page.route("**", handler);
|
|
3648
|
+
try {
|
|
3649
|
+
const response = await opts.page.goto(opts.url, { timeout: opts.timeoutMs });
|
|
3650
|
+
if (state.blocked !== null) throw state.blocked;
|
|
3651
|
+
return response;
|
|
3652
|
+
} catch (err) {
|
|
3653
|
+
if (state.blocked !== null) throw state.blocked;
|
|
3654
|
+
throw err;
|
|
3655
|
+
} finally {
|
|
3656
|
+
await opts.page.unroute("**", handler).catch((e) => {
|
|
3657
|
+
console.warn("[browserclaw] route unroute failed", e);
|
|
3658
|
+
});
|
|
3659
|
+
if (state.blocked !== null)
|
|
3660
|
+
await closeBlockedNavigationTarget({ cdpUrl: opts.cdpUrl, page: opts.page, targetId: opts.targetId });
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3362
3663
|
async function navigateViaPlaywright(opts) {
|
|
3363
3664
|
const url = opts.url.trim();
|
|
3364
3665
|
if (!url) throw new Error("url is required");
|
|
@@ -3367,7 +3668,14 @@ async function navigateViaPlaywright(opts) {
|
|
|
3367
3668
|
const timeout = Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 2e4));
|
|
3368
3669
|
let page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3369
3670
|
ensurePageState(page);
|
|
3370
|
-
const navigate = async () => await
|
|
3671
|
+
const navigate = async () => await gotoPageWithNavigationGuard({
|
|
3672
|
+
cdpUrl: opts.cdpUrl,
|
|
3673
|
+
page,
|
|
3674
|
+
url,
|
|
3675
|
+
timeoutMs: timeout,
|
|
3676
|
+
ssrfPolicy: policy,
|
|
3677
|
+
targetId: opts.targetId
|
|
3678
|
+
});
|
|
3371
3679
|
let response;
|
|
3372
3680
|
try {
|
|
3373
3681
|
response = await navigate();
|
|
@@ -3381,21 +3689,23 @@ async function navigateViaPlaywright(opts) {
|
|
|
3381
3689
|
ensurePageState(page);
|
|
3382
3690
|
response = await navigate();
|
|
3383
3691
|
}
|
|
3384
|
-
await
|
|
3385
|
-
|
|
3386
|
-
|
|
3692
|
+
await assertPageNavigationCompletedSafely({
|
|
3693
|
+
cdpUrl: opts.cdpUrl,
|
|
3694
|
+
page,
|
|
3695
|
+
response,
|
|
3696
|
+
ssrfPolicy: policy,
|
|
3697
|
+
targetId: opts.targetId
|
|
3387
3698
|
});
|
|
3388
|
-
|
|
3389
|
-
await assertBrowserNavigationResultAllowed({ url: finalUrl, ...withBrowserNavigationPolicy(policy) });
|
|
3390
|
-
return { url: finalUrl };
|
|
3699
|
+
return { url: page.url() };
|
|
3391
3700
|
}
|
|
3392
3701
|
async function listPagesViaPlaywright(opts) {
|
|
3393
3702
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
3394
3703
|
const pages = getAllPages(browser);
|
|
3395
3704
|
const results = [];
|
|
3396
3705
|
for (const page of pages) {
|
|
3706
|
+
if (isBlockedPageRef(opts.cdpUrl, page)) continue;
|
|
3397
3707
|
const tid = await pageTargetId(page).catch(() => null);
|
|
3398
|
-
if (tid !== null && tid !== "")
|
|
3708
|
+
if (tid !== null && tid !== "" && !isBlockedTarget(opts.cdpUrl, tid))
|
|
3399
3709
|
results.push({
|
|
3400
3710
|
targetId: tid,
|
|
3401
3711
|
title: await page.title().catch(() => ""),
|
|
@@ -3411,18 +3721,35 @@ async function createPageViaPlaywright(opts) {
|
|
|
3411
3721
|
ensureContextState(context);
|
|
3412
3722
|
const page = await context.newPage();
|
|
3413
3723
|
ensurePageState(page);
|
|
3724
|
+
clearBlockedPageRef(opts.cdpUrl, page);
|
|
3725
|
+
const createdTargetId = await pageTargetId(page).catch(() => null);
|
|
3726
|
+
clearBlockedTarget(opts.cdpUrl, createdTargetId ?? void 0);
|
|
3414
3727
|
const targetUrl = (opts.url ?? "").trim() || "about:blank";
|
|
3415
3728
|
const policy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3416
3729
|
if (targetUrl !== "about:blank") {
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3730
|
+
await assertBrowserNavigationAllowed({ url: targetUrl, ...withBrowserNavigationPolicy(policy) });
|
|
3731
|
+
let response = null;
|
|
3732
|
+
try {
|
|
3733
|
+
response = await gotoPageWithNavigationGuard({
|
|
3734
|
+
cdpUrl: opts.cdpUrl,
|
|
3735
|
+
page,
|
|
3736
|
+
url: targetUrl,
|
|
3737
|
+
timeoutMs: 3e4,
|
|
3738
|
+
ssrfPolicy: policy,
|
|
3739
|
+
targetId: createdTargetId ?? void 0
|
|
3740
|
+
});
|
|
3741
|
+
} catch (err) {
|
|
3742
|
+
if (isPolicyDenyNavigationError(err) || err instanceof BlockedBrowserTargetError) throw err;
|
|
3743
|
+
}
|
|
3744
|
+
await assertPageNavigationCompletedSafely({
|
|
3745
|
+
cdpUrl: opts.cdpUrl,
|
|
3746
|
+
page,
|
|
3747
|
+
response,
|
|
3748
|
+
ssrfPolicy: policy,
|
|
3749
|
+
targetId: createdTargetId ?? void 0
|
|
3422
3750
|
});
|
|
3423
|
-
await assertBrowserNavigationResultAllowed({ url: page.url(), ...navigationPolicy });
|
|
3424
3751
|
}
|
|
3425
|
-
const tid = await pageTargetId(page).catch(() => null);
|
|
3752
|
+
const tid = createdTargetId ?? await pageTargetId(page).catch(() => null);
|
|
3426
3753
|
if (tid === null || tid === "") throw new Error("Failed to get targetId for new page");
|
|
3427
3754
|
return {
|
|
3428
3755
|
targetId: tid,
|
|
@@ -3437,7 +3764,12 @@ async function closePageViaPlaywright(opts) {
|
|
|
3437
3764
|
await page.close();
|
|
3438
3765
|
}
|
|
3439
3766
|
async function closePageByTargetIdViaPlaywright(opts) {
|
|
3440
|
-
|
|
3767
|
+
try {
|
|
3768
|
+
await (await resolvePageByTargetIdOrThrow(opts)).close();
|
|
3769
|
+
} catch (err) {
|
|
3770
|
+
if (err instanceof BrowserTabNotFoundError) return;
|
|
3771
|
+
throw err;
|
|
3772
|
+
}
|
|
3441
3773
|
}
|
|
3442
3774
|
async function focusPageByTargetIdViaPlaywright(opts) {
|
|
3443
3775
|
const page = await resolvePageByTargetIdOrThrow(opts);
|
|
@@ -3459,6 +3791,27 @@ async function focusPageByTargetIdViaPlaywright(opts) {
|
|
|
3459
3791
|
}
|
|
3460
3792
|
}
|
|
3461
3793
|
}
|
|
3794
|
+
async function waitForTabViaPlaywright(opts) {
|
|
3795
|
+
if (opts.urlContains === void 0 && opts.titleContains === void 0)
|
|
3796
|
+
throw new Error("urlContains or titleContains is required");
|
|
3797
|
+
const timeout = Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 3e4));
|
|
3798
|
+
const start = Date.now();
|
|
3799
|
+
const POLL_INTERVAL_MS = 250;
|
|
3800
|
+
while (Date.now() - start < timeout) {
|
|
3801
|
+
const tabs = await listPagesViaPlaywright({ cdpUrl: opts.cdpUrl });
|
|
3802
|
+
const match = tabs.find((t) => {
|
|
3803
|
+
if (opts.urlContains !== void 0 && !t.url.includes(opts.urlContains)) return false;
|
|
3804
|
+
if (opts.titleContains !== void 0 && !t.title.includes(opts.titleContains)) return false;
|
|
3805
|
+
return true;
|
|
3806
|
+
});
|
|
3807
|
+
if (match) return match;
|
|
3808
|
+
await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
|
|
3809
|
+
}
|
|
3810
|
+
const criteria = [];
|
|
3811
|
+
if (opts.urlContains !== void 0) criteria.push(`url contains "${opts.urlContains}"`);
|
|
3812
|
+
if (opts.titleContains !== void 0) criteria.push(`title contains "${opts.titleContains}"`);
|
|
3813
|
+
throw new Error(`Timed out waiting for tab: ${criteria.join(", ")}`);
|
|
3814
|
+
}
|
|
3462
3815
|
async function resizeViewportViaPlaywright(opts) {
|
|
3463
3816
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3464
3817
|
ensurePageState(page);
|
|
@@ -3500,9 +3853,13 @@ async function waitForViaPlaywright(opts) {
|
|
|
3500
3853
|
if (opts.loadState !== void 0) {
|
|
3501
3854
|
await page.waitForLoadState(opts.loadState, { timeout });
|
|
3502
3855
|
}
|
|
3503
|
-
if (opts.fn !== void 0
|
|
3504
|
-
|
|
3505
|
-
|
|
3856
|
+
if (opts.fn !== void 0) {
|
|
3857
|
+
if (typeof opts.fn === "function") {
|
|
3858
|
+
await page.waitForFunction(opts.fn, opts.arg, { timeout });
|
|
3859
|
+
} else {
|
|
3860
|
+
const fn = opts.fn.trim();
|
|
3861
|
+
if (fn !== "") await page.waitForFunction(fn, opts.arg, { timeout });
|
|
3862
|
+
}
|
|
3506
3863
|
}
|
|
3507
3864
|
}
|
|
3508
3865
|
|
|
@@ -3614,6 +3971,7 @@ async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, de
|
|
|
3614
3971
|
url: action.url,
|
|
3615
3972
|
loadState: action.loadState,
|
|
3616
3973
|
fn: action.fn,
|
|
3974
|
+
arg: action.arg,
|
|
3617
3975
|
timeoutMs: action.timeoutMs
|
|
3618
3976
|
});
|
|
3619
3977
|
break;
|
|
@@ -4089,6 +4447,40 @@ async function responseBodyViaPlaywright(opts) {
|
|
|
4089
4447
|
truncated
|
|
4090
4448
|
};
|
|
4091
4449
|
}
|
|
4450
|
+
async function waitForRequestViaPlaywright(opts) {
|
|
4451
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
4452
|
+
ensurePageState(page);
|
|
4453
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
|
|
4454
|
+
const pattern = opts.url.trim();
|
|
4455
|
+
if (!pattern) throw new Error("url is required");
|
|
4456
|
+
const upperMethod = opts.method !== void 0 ? opts.method.toUpperCase() : void 0;
|
|
4457
|
+
const response = await page.waitForResponse(
|
|
4458
|
+
(resp) => matchUrlPattern(pattern, resp.url()) && (upperMethod === void 0 || resp.request().method() === upperMethod),
|
|
4459
|
+
{ timeout }
|
|
4460
|
+
);
|
|
4461
|
+
const request = response.request();
|
|
4462
|
+
let responseBody;
|
|
4463
|
+
let truncated = false;
|
|
4464
|
+
try {
|
|
4465
|
+
responseBody = await response.text();
|
|
4466
|
+
const maxChars = typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars) ? Math.max(1, Math.min(5e6, Math.floor(opts.maxChars))) : 2e5;
|
|
4467
|
+
if (responseBody.length > maxChars) {
|
|
4468
|
+
responseBody = responseBody.slice(0, maxChars);
|
|
4469
|
+
truncated = true;
|
|
4470
|
+
}
|
|
4471
|
+
} catch (err) {
|
|
4472
|
+
console.warn("[browserclaw] response body unavailable:", err instanceof Error ? err.message : String(err));
|
|
4473
|
+
}
|
|
4474
|
+
return {
|
|
4475
|
+
url: response.url(),
|
|
4476
|
+
method: request.method(),
|
|
4477
|
+
postData: request.postData() ?? void 0,
|
|
4478
|
+
status: response.status(),
|
|
4479
|
+
ok: response.ok(),
|
|
4480
|
+
responseBody,
|
|
4481
|
+
truncated
|
|
4482
|
+
};
|
|
4483
|
+
}
|
|
4092
4484
|
|
|
4093
4485
|
// src/capture/screenshot.ts
|
|
4094
4486
|
async function takeScreenshotViaPlaywright(opts) {
|
|
@@ -4212,6 +4604,13 @@ async function traceStopViaPlaywright(opts) {
|
|
|
4212
4604
|
}
|
|
4213
4605
|
|
|
4214
4606
|
// src/snapshot/ref-map.ts
|
|
4607
|
+
function parseStateFromSuffix(suffix) {
|
|
4608
|
+
const state = {};
|
|
4609
|
+
if (/\[disabled\]/i.test(suffix)) state.disabled = true;
|
|
4610
|
+
if (/\[checked\s*=\s*"?mixed"?\]/i.test(suffix)) state.checked = "mixed";
|
|
4611
|
+
else if (/\[checked\]/i.test(suffix)) state.checked = true;
|
|
4612
|
+
return state;
|
|
4613
|
+
}
|
|
4215
4614
|
var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
4216
4615
|
"button",
|
|
4217
4616
|
"link",
|
|
@@ -4370,7 +4769,8 @@ function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
|
|
|
4370
4769
|
const ref = nextRef();
|
|
4371
4770
|
const nth = tracker.getNextIndex(role, name);
|
|
4372
4771
|
tracker.trackRef(role, name, ref);
|
|
4373
|
-
|
|
4772
|
+
const state = parseStateFromSuffix(suffix);
|
|
4773
|
+
refs[ref] = { role, name, nth, ...state };
|
|
4374
4774
|
let enhanced = `${prefix}${roleRaw}`;
|
|
4375
4775
|
if (name !== void 0 && name !== "") enhanced += ` "${name}"`;
|
|
4376
4776
|
enhanced += ` [ref=${ref}]`;
|
|
@@ -4407,7 +4807,8 @@ function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
|
|
|
4407
4807
|
const ref = nextRef();
|
|
4408
4808
|
const nth = tracker.getNextIndex(role, name);
|
|
4409
4809
|
tracker.trackRef(role, name, ref);
|
|
4410
|
-
|
|
4810
|
+
const state = parseStateFromSuffix(suffix);
|
|
4811
|
+
refs[ref] = { role, name, nth, ...state };
|
|
4411
4812
|
let enhanced = `${prefix}${roleRaw}`;
|
|
4412
4813
|
if (name !== "") enhanced += ` "${name}"`;
|
|
4413
4814
|
enhanced += ` [ref=${ref}]`;
|
|
@@ -4445,12 +4846,13 @@ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
|
|
|
4445
4846
|
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
4446
4847
|
const ref = parseAiSnapshotRef(suffix);
|
|
4447
4848
|
const prefix = /^(\s*-\s*)/.exec(line)?.[1] ?? "";
|
|
4849
|
+
const state = parseStateFromSuffix(suffix);
|
|
4448
4850
|
if (ref !== null) {
|
|
4449
|
-
refs[ref] = { role, ...name !== void 0 && name !== "" ? { name } : {} };
|
|
4851
|
+
refs[ref] = { role, ...name !== void 0 && name !== "" ? { name } : {}, ...state };
|
|
4450
4852
|
out2.push(`${prefix}${roleRaw}${name !== void 0 && name !== "" ? ` "${name}"` : ""}${suffix}`);
|
|
4451
4853
|
} else {
|
|
4452
4854
|
const generatedRef = nextInteractiveRef();
|
|
4453
|
-
refs[generatedRef] = { role, ...name !== void 0 && name !== "" ? { name } : {} };
|
|
4855
|
+
refs[generatedRef] = { role, ...name !== void 0 && name !== "" ? { name } : {}, ...state };
|
|
4454
4856
|
let enhanced = `${prefix}${roleRaw}`;
|
|
4455
4857
|
if (name !== void 0 && name !== "") enhanced += ` "${name}"`;
|
|
4456
4858
|
enhanced += ` [ref=${generatedRef}]`;
|
|
@@ -4488,12 +4890,13 @@ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
|
|
|
4488
4890
|
const isStructural = STRUCTURAL_ROLES.has(role);
|
|
4489
4891
|
if (options.compact === true && isStructural && name === "") continue;
|
|
4490
4892
|
const ref = parseAiSnapshotRef(suffix);
|
|
4893
|
+
const state = parseStateFromSuffix(suffix);
|
|
4491
4894
|
if (ref !== null) {
|
|
4492
|
-
refs[ref] = { role, ...name !== "" ? { name } : {} };
|
|
4895
|
+
refs[ref] = { role, ...name !== "" ? { name } : {}, ...state };
|
|
4493
4896
|
out.push(line);
|
|
4494
4897
|
} else if (INTERACTIVE_ROLES.has(role)) {
|
|
4495
4898
|
const generatedRef = nextGeneratedRef();
|
|
4496
|
-
refs[generatedRef] = { role, ...name !== "" ? { name } : {} };
|
|
4899
|
+
refs[generatedRef] = { role, ...name !== "" ? { name } : {}, ...state };
|
|
4497
4900
|
let enhanced = `${prefix}${roleRaw}`;
|
|
4498
4901
|
if (name !== "") enhanced += ` "${name}"`;
|
|
4499
4902
|
enhanced += ` [ref=${generatedRef}]`;
|
|
@@ -4859,7 +5262,8 @@ var CrawlPage = class {
|
|
|
4859
5262
|
button: opts?.button,
|
|
4860
5263
|
modifiers: opts?.modifiers,
|
|
4861
5264
|
delayMs: opts?.delayMs,
|
|
4862
|
-
timeoutMs: opts?.timeoutMs
|
|
5265
|
+
timeoutMs: opts?.timeoutMs,
|
|
5266
|
+
force: opts?.force
|
|
4863
5267
|
});
|
|
4864
5268
|
}
|
|
4865
5269
|
/**
|
|
@@ -4885,7 +5289,8 @@ var CrawlPage = class {
|
|
|
4885
5289
|
button: opts?.button,
|
|
4886
5290
|
modifiers: opts?.modifiers,
|
|
4887
5291
|
delayMs: opts?.delayMs,
|
|
4888
|
-
timeoutMs: opts?.timeoutMs
|
|
5292
|
+
timeoutMs: opts?.timeoutMs,
|
|
5293
|
+
force: opts?.force
|
|
4889
5294
|
});
|
|
4890
5295
|
}
|
|
4891
5296
|
/**
|
|
@@ -4915,6 +5320,32 @@ var CrawlPage = class {
|
|
|
4915
5320
|
delayMs: opts?.delayMs
|
|
4916
5321
|
});
|
|
4917
5322
|
}
|
|
5323
|
+
/**
|
|
5324
|
+
* Press and hold at page coordinates using raw CDP events.
|
|
5325
|
+
*
|
|
5326
|
+
* Bypasses Playwright's automation layer by dispatching CDP
|
|
5327
|
+
* `Input.dispatchMouseEvent` directly — useful for anti-bot challenges
|
|
5328
|
+
* that detect automated clicks.
|
|
5329
|
+
*
|
|
5330
|
+
* @param x - X coordinate in CSS pixels
|
|
5331
|
+
* @param y - Y coordinate in CSS pixels
|
|
5332
|
+
* @param opts - Options (delay: ms before press, holdMs: hold duration)
|
|
5333
|
+
*
|
|
5334
|
+
* @example
|
|
5335
|
+
* ```ts
|
|
5336
|
+
* await page.pressAndHold(400, 300, { delay: 150, holdMs: 5000 });
|
|
5337
|
+
* ```
|
|
5338
|
+
*/
|
|
5339
|
+
async pressAndHold(x, y, opts) {
|
|
5340
|
+
return pressAndHoldViaCdp({
|
|
5341
|
+
cdpUrl: this.cdpUrl,
|
|
5342
|
+
targetId: this.targetId,
|
|
5343
|
+
x,
|
|
5344
|
+
y,
|
|
5345
|
+
delay: opts?.delay,
|
|
5346
|
+
holdMs: opts?.holdMs
|
|
5347
|
+
});
|
|
5348
|
+
}
|
|
4918
5349
|
/**
|
|
4919
5350
|
* Click an element by its visible text content (no snapshot/ref needed).
|
|
4920
5351
|
*
|
|
@@ -4962,6 +5393,7 @@ var CrawlPage = class {
|
|
|
4962
5393
|
targetId: this.targetId,
|
|
4963
5394
|
role,
|
|
4964
5395
|
name,
|
|
5396
|
+
index: opts?.index,
|
|
4965
5397
|
button: opts?.button,
|
|
4966
5398
|
modifiers: opts?.modifiers,
|
|
4967
5399
|
timeoutMs: opts?.timeoutMs
|
|
@@ -5485,6 +5917,35 @@ var CrawlPage = class {
|
|
|
5485
5917
|
maxChars: opts?.maxChars
|
|
5486
5918
|
});
|
|
5487
5919
|
}
|
|
5920
|
+
/**
|
|
5921
|
+
* Wait for a network request matching a URL pattern and return request + response details.
|
|
5922
|
+
*
|
|
5923
|
+
* Unlike `networkRequests()` which only captures metadata, this method captures
|
|
5924
|
+
* the full request body (POST data) and response body.
|
|
5925
|
+
*
|
|
5926
|
+
* @param url - URL string or pattern to match (supports `*` wildcards and substring matching)
|
|
5927
|
+
* @param opts - Options (method filter, timeoutMs, maxChars for response body)
|
|
5928
|
+
* @returns Request method, postData, response status, and response body
|
|
5929
|
+
*
|
|
5930
|
+
* @example
|
|
5931
|
+
* ```ts
|
|
5932
|
+
* const reqPromise = page.waitForRequest('/api/submit', { method: 'POST' });
|
|
5933
|
+
* await page.click('e5'); // submit a form
|
|
5934
|
+
* const req = await reqPromise;
|
|
5935
|
+
* console.log(req.postData); // form body
|
|
5936
|
+
* console.log(req.status, req.responseBody); // response
|
|
5937
|
+
* ```
|
|
5938
|
+
*/
|
|
5939
|
+
async waitForRequest(url, opts) {
|
|
5940
|
+
return waitForRequestViaPlaywright({
|
|
5941
|
+
cdpUrl: this.cdpUrl,
|
|
5942
|
+
targetId: this.targetId,
|
|
5943
|
+
url,
|
|
5944
|
+
method: opts?.method,
|
|
5945
|
+
timeoutMs: opts?.timeoutMs,
|
|
5946
|
+
maxChars: opts?.maxChars
|
|
5947
|
+
});
|
|
5948
|
+
}
|
|
5488
5949
|
/**
|
|
5489
5950
|
* Get console messages captured from the page.
|
|
5490
5951
|
*
|
|
@@ -5915,14 +6376,15 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
5915
6376
|
*
|
|
5916
6377
|
* @example
|
|
5917
6378
|
* ```ts
|
|
5918
|
-
* //
|
|
5919
|
-
* const browser = await BrowserClaw.launch();
|
|
6379
|
+
* // Launch and navigate to a URL
|
|
6380
|
+
* const browser = await BrowserClaw.launch({ url: 'https://example.com' });
|
|
5920
6381
|
*
|
|
5921
6382
|
* // Headless mode
|
|
5922
|
-
* const browser = await BrowserClaw.launch({ headless: true });
|
|
6383
|
+
* const browser = await BrowserClaw.launch({ url: 'https://example.com', headless: true });
|
|
5923
6384
|
*
|
|
5924
6385
|
* // Specific browser
|
|
5925
6386
|
* const browser = await BrowserClaw.launch({
|
|
6387
|
+
* url: 'https://example.com',
|
|
5926
6388
|
* executablePath: '/usr/bin/google-chrome',
|
|
5927
6389
|
* });
|
|
5928
6390
|
* ```
|
|
@@ -5931,7 +6393,12 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
5931
6393
|
const chrome = await launchChrome(opts);
|
|
5932
6394
|
const cdpUrl = `http://127.0.0.1:${String(chrome.cdpPort)}`;
|
|
5933
6395
|
const ssrfPolicy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
5934
|
-
|
|
6396
|
+
const browser = new _BrowserClaw(cdpUrl, chrome, ssrfPolicy, opts.recordVideo);
|
|
6397
|
+
if (opts.url !== void 0 && opts.url !== "") {
|
|
6398
|
+
const page = await browser.currentPage();
|
|
6399
|
+
await page.goto(opts.url);
|
|
6400
|
+
}
|
|
6401
|
+
return browser;
|
|
5935
6402
|
}
|
|
5936
6403
|
/**
|
|
5937
6404
|
* Connect to an already-running Chrome instance via its CDP endpoint.
|
|
@@ -6007,6 +6474,31 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
6007
6474
|
async tabs() {
|
|
6008
6475
|
return listPagesViaPlaywright({ cdpUrl: this.cdpUrl });
|
|
6009
6476
|
}
|
|
6477
|
+
/**
|
|
6478
|
+
* Wait for a tab matching the given criteria and return a page handle.
|
|
6479
|
+
*
|
|
6480
|
+
* Polls open tabs until one matches, then focuses it and returns a CrawlPage.
|
|
6481
|
+
*
|
|
6482
|
+
* @param opts - Match criteria (urlContains, titleContains) and timeout
|
|
6483
|
+
* @returns A CrawlPage for the matched tab
|
|
6484
|
+
*
|
|
6485
|
+
* @example
|
|
6486
|
+
* ```ts
|
|
6487
|
+
* await page.click('e5'); // opens a new tab
|
|
6488
|
+
* const appPage = await browser.waitForTab({ urlContains: 'app-web' });
|
|
6489
|
+
* const { snapshot } = await appPage.snapshot();
|
|
6490
|
+
* ```
|
|
6491
|
+
*/
|
|
6492
|
+
async waitForTab(opts) {
|
|
6493
|
+
const tab = await waitForTabViaPlaywright({
|
|
6494
|
+
cdpUrl: this.cdpUrl,
|
|
6495
|
+
urlContains: opts.urlContains,
|
|
6496
|
+
titleContains: opts.titleContains,
|
|
6497
|
+
timeoutMs: opts.timeoutMs
|
|
6498
|
+
});
|
|
6499
|
+
await focusPageByTargetIdViaPlaywright({ cdpUrl: this.cdpUrl, targetId: tab.targetId });
|
|
6500
|
+
return new CrawlPage(this.cdpUrl, tab.targetId, this.ssrfPolicy);
|
|
6501
|
+
}
|
|
6010
6502
|
/**
|
|
6011
6503
|
* Bring a tab to the foreground.
|
|
6012
6504
|
*
|
|
@@ -6055,6 +6547,6 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
6055
6547
|
}
|
|
6056
6548
|
};
|
|
6057
6549
|
|
|
6058
|
-
export { BrowserClaw, BrowserTabNotFoundError, CrawlPage, InvalidBrowserNavigationUrlError, STEALTH_SCRIPT, assertBrowserNavigationAllowed, assertBrowserNavigationRedirectChainAllowed, assertBrowserNavigationResultAllowed, assertSafeUploadPaths, batchViaPlaywright, createPinnedLookup, detectChallengeViaPlaywright, ensureContextState, executeSingleAction, forceDisconnectPlaywrightForTarget, getChromeWebSocketUrl, getRestoredPageForTarget, isChromeCdpReady, isChromeReachable, normalizeCdpHttpBaseForJsonEndpoints, parseRoleRef, requireRef, requireRefOrSelector, requiresInspectableBrowserNavigationRedirects, resolveBoundedDelayMs, resolveInteractionTimeoutMs, resolvePageByTargetIdOrThrow, resolvePinnedHostnameWithPolicy, resolveStrictExistingUploadPaths, sanitizeUntrustedFileName, setDialogHandler, waitForChallengeViaPlaywright, withBrowserNavigationPolicy, withPageScopedCdpClient, withPlaywrightPageCdpSession, writeViaSiblingTempPath };
|
|
6550
|
+
export { BrowserClaw, BrowserTabNotFoundError, CrawlPage, InvalidBrowserNavigationUrlError, STEALTH_SCRIPT, assertBrowserNavigationAllowed, assertBrowserNavigationRedirectChainAllowed, assertBrowserNavigationResultAllowed, assertSafeUploadPaths, batchViaPlaywright, createPinnedLookup, detectChallengeViaPlaywright, ensureContextState, executeSingleAction, forceDisconnectPlaywrightForTarget, getChromeWebSocketUrl, getRestoredPageForTarget, isChromeCdpReady, isChromeReachable, normalizeCdpHttpBaseForJsonEndpoints, parseRoleRef, pressAndHoldViaCdp, requireRef, requireRefOrSelector, requiresInspectableBrowserNavigationRedirects, resolveBoundedDelayMs, resolveInteractionTimeoutMs, resolvePageByTargetIdOrThrow, resolvePinnedHostnameWithPolicy, resolveStrictExistingUploadPaths, sanitizeUntrustedFileName, setDialogHandler, waitForChallengeViaPlaywright, withBrowserNavigationPolicy, withPageScopedCdpClient, withPlaywrightPageCdpSession, writeViaSiblingTempPath };
|
|
6059
6551
|
//# sourceMappingURL=index.js.map
|
|
6060
6552
|
//# sourceMappingURL=index.js.map
|