browserclaw 0.5.8 → 0.6.0
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 +1261 -1186
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -20
- package/dist/index.d.ts +16 -20
- package/dist/index.js +1255 -1180
- package/dist/index.js.map +1 -1
- package/package.json +13 -2
package/dist/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import os from 'os';
|
|
2
|
-
import path, { posix, win32, resolve, dirname, join, basename, relative, sep, normalize } from 'path';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import net from 'net';
|
|
5
|
-
import { spawn, execFileSync } from 'child_process';
|
|
6
|
-
import { devices, chromium } from 'playwright-core';
|
|
7
1
|
import http from 'http';
|
|
8
2
|
import https from 'https';
|
|
9
|
-
import {
|
|
3
|
+
import { devices, chromium } from 'playwright-core';
|
|
4
|
+
import { spawn, execFileSync } from 'child_process';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import net from 'net';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import path, { posix, win32, resolve, dirname, join, basename, relative, sep, normalize } from 'path';
|
|
9
|
+
import { randomUUID } from 'crypto';
|
|
10
10
|
import { lookup } from 'dns';
|
|
11
|
+
import { lookup as lookup$1 } from 'dns/promises';
|
|
11
12
|
import { lstat, realpath, rename, rm } from 'fs/promises';
|
|
12
|
-
import { randomUUID } from 'crypto';
|
|
13
13
|
|
|
14
14
|
var __create = Object.create;
|
|
15
15
|
var __defProp = Object.defineProperty;
|
|
@@ -910,7 +910,7 @@ function execText(command, args, timeoutMs = 1200) {
|
|
|
910
910
|
encoding: "utf8",
|
|
911
911
|
maxBuffer: 1024 * 1024
|
|
912
912
|
});
|
|
913
|
-
return
|
|
913
|
+
return output.trim() || null;
|
|
914
914
|
} catch {
|
|
915
915
|
return null;
|
|
916
916
|
}
|
|
@@ -921,7 +921,8 @@ function inferKindFromIdentifier(identifier) {
|
|
|
921
921
|
if (id.includes("edge")) return "edge";
|
|
922
922
|
if (id.includes("chromium")) return "chromium";
|
|
923
923
|
if (id.includes("canary")) return "canary";
|
|
924
|
-
if (id.includes("opera") || id.includes("vivaldi") || id.includes("yandex") || id.includes("thebrowser"))
|
|
924
|
+
if (id.includes("opera") || id.includes("vivaldi") || id.includes("yandex") || id.includes("thebrowser"))
|
|
925
|
+
return "chromium";
|
|
925
926
|
return "chrome";
|
|
926
927
|
}
|
|
927
928
|
function inferKindFromExeName(name) {
|
|
@@ -938,24 +939,29 @@ function findFirstExe(candidates) {
|
|
|
938
939
|
return null;
|
|
939
940
|
}
|
|
940
941
|
function detectDefaultBrowserBundleIdMac() {
|
|
941
|
-
const plistPath = path.join(
|
|
942
|
+
const plistPath = path.join(
|
|
943
|
+
os.homedir(),
|
|
944
|
+
"Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist"
|
|
945
|
+
);
|
|
942
946
|
if (!fileExists(plistPath)) return null;
|
|
943
947
|
const handlersRaw = execText("/usr/bin/plutil", ["-extract", "LSHandlers", "json", "-o", "-", "--", plistPath], 2e3);
|
|
944
|
-
if (
|
|
948
|
+
if (handlersRaw === null) return null;
|
|
945
949
|
let handlers;
|
|
946
950
|
try {
|
|
947
|
-
|
|
951
|
+
const parsed = JSON.parse(handlersRaw);
|
|
952
|
+
if (!Array.isArray(parsed)) return null;
|
|
953
|
+
handlers = parsed;
|
|
948
954
|
} catch {
|
|
949
955
|
return null;
|
|
950
956
|
}
|
|
951
|
-
if (!Array.isArray(handlers)) return null;
|
|
952
957
|
const resolveScheme = (scheme) => {
|
|
953
958
|
let candidate = null;
|
|
954
959
|
for (const entry of handlers) {
|
|
955
|
-
if (
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
960
|
+
if (entry === null || entry === void 0 || typeof entry !== "object") continue;
|
|
961
|
+
const rec = entry;
|
|
962
|
+
if (rec.LSHandlerURLScheme !== scheme) continue;
|
|
963
|
+
const role = (typeof rec.LSHandlerRoleAll === "string" ? rec.LSHandlerRoleAll : null) ?? (typeof rec.LSHandlerRoleViewer === "string" ? rec.LSHandlerRoleViewer : null) ?? null;
|
|
964
|
+
if (role !== null) candidate = role;
|
|
959
965
|
}
|
|
960
966
|
return candidate;
|
|
961
967
|
};
|
|
@@ -963,12 +969,12 @@ function detectDefaultBrowserBundleIdMac() {
|
|
|
963
969
|
}
|
|
964
970
|
function detectDefaultChromiumMac() {
|
|
965
971
|
const bundleId = detectDefaultBrowserBundleIdMac();
|
|
966
|
-
if (
|
|
972
|
+
if (bundleId === null || !CHROMIUM_BUNDLE_IDS.has(bundleId)) return null;
|
|
967
973
|
const appPathRaw = execText("/usr/bin/osascript", ["-e", `POSIX path of (path to application id "${bundleId}")`]);
|
|
968
|
-
if (
|
|
974
|
+
if (appPathRaw === null) return null;
|
|
969
975
|
const appPath = appPathRaw.trim().replace(/\/$/, "");
|
|
970
976
|
const exeName = execText("/usr/bin/defaults", ["read", path.join(appPath, "Contents", "Info"), "CFBundleExecutable"]);
|
|
971
|
-
if (
|
|
977
|
+
if (exeName === null) return null;
|
|
972
978
|
const exePath = path.join(appPath, "Contents", "MacOS", exeName.trim());
|
|
973
979
|
if (!fileExists(exePath)) return null;
|
|
974
980
|
return { kind: inferKindFromIdentifier(bundleId), path: exePath };
|
|
@@ -984,12 +990,15 @@ function findChromeMac() {
|
|
|
984
990
|
{ kind: "chromium", path: "/Applications/Chromium.app/Contents/MacOS/Chromium" },
|
|
985
991
|
{ kind: "chromium", path: path.join(os.homedir(), "Applications/Chromium.app/Contents/MacOS/Chromium") },
|
|
986
992
|
{ kind: "canary", path: "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary" },
|
|
987
|
-
{
|
|
993
|
+
{
|
|
994
|
+
kind: "canary",
|
|
995
|
+
path: path.join(os.homedir(), "Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary")
|
|
996
|
+
}
|
|
988
997
|
]);
|
|
989
998
|
}
|
|
990
999
|
function detectDefaultChromiumLinux() {
|
|
991
|
-
const desktopId = execText("xdg-settings", ["get", "default-web-browser"])
|
|
992
|
-
if (
|
|
1000
|
+
const desktopId = execText("xdg-settings", ["get", "default-web-browser"]) ?? execText("xdg-mime", ["query", "default", "x-scheme-handler/http"]);
|
|
1001
|
+
if (desktopId === null) return null;
|
|
993
1002
|
const trimmed = desktopId.trim();
|
|
994
1003
|
if (!CHROMIUM_DESKTOP_IDS.has(trimmed)) return null;
|
|
995
1004
|
const searchDirs = [
|
|
@@ -1006,17 +1015,18 @@ function detectDefaultChromiumLinux() {
|
|
|
1006
1015
|
break;
|
|
1007
1016
|
}
|
|
1008
1017
|
}
|
|
1009
|
-
if (
|
|
1018
|
+
if (desktopPath === null) return null;
|
|
1010
1019
|
let execLine = null;
|
|
1011
1020
|
try {
|
|
1012
1021
|
const lines = fs.readFileSync(desktopPath, "utf8").split(/\r?\n/);
|
|
1013
|
-
for (const line of lines)
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1022
|
+
for (const line of lines)
|
|
1023
|
+
if (line.startsWith("Exec=")) {
|
|
1024
|
+
execLine = line.slice(5).trim();
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1017
1027
|
} catch {
|
|
1018
1028
|
}
|
|
1019
|
-
if (
|
|
1029
|
+
if (execLine === null) return null;
|
|
1020
1030
|
const tokens = execLine.split(/\s+/);
|
|
1021
1031
|
let command = null;
|
|
1022
1032
|
for (const token of tokens) {
|
|
@@ -1024,9 +1034,9 @@ function detectDefaultChromiumLinux() {
|
|
|
1024
1034
|
command = token.replace(/^["']|["']$/g, "");
|
|
1025
1035
|
break;
|
|
1026
1036
|
}
|
|
1027
|
-
if (
|
|
1037
|
+
if (command === null) return null;
|
|
1028
1038
|
const resolved = command.startsWith("/") ? command : execText("which", [command], 800)?.trim() ?? null;
|
|
1029
|
-
if (
|
|
1039
|
+
if (resolved === null || resolved === "") return null;
|
|
1030
1040
|
const exeName = path.posix.basename(resolved).toLowerCase();
|
|
1031
1041
|
if (!CHROMIUM_EXE_NAMES.has(exeName)) return null;
|
|
1032
1042
|
return { kind: inferKindFromExeName(exeName), path: resolved };
|
|
@@ -1055,21 +1065,30 @@ function findChromeWindows() {
|
|
|
1055
1065
|
const candidates = [];
|
|
1056
1066
|
if (localAppData) {
|
|
1057
1067
|
candidates.push({ kind: "chrome", path: j(localAppData, "Google", "Chrome", "Application", "chrome.exe") });
|
|
1058
|
-
candidates.push({
|
|
1068
|
+
candidates.push({
|
|
1069
|
+
kind: "brave",
|
|
1070
|
+
path: j(localAppData, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
|
|
1071
|
+
});
|
|
1059
1072
|
candidates.push({ kind: "edge", path: j(localAppData, "Microsoft", "Edge", "Application", "msedge.exe") });
|
|
1060
1073
|
candidates.push({ kind: "chromium", path: j(localAppData, "Chromium", "Application", "chrome.exe") });
|
|
1061
1074
|
candidates.push({ kind: "canary", path: j(localAppData, "Google", "Chrome SxS", "Application", "chrome.exe") });
|
|
1062
1075
|
}
|
|
1063
1076
|
candidates.push({ kind: "chrome", path: j(programFiles, "Google", "Chrome", "Application", "chrome.exe") });
|
|
1064
1077
|
candidates.push({ kind: "chrome", path: j(programFilesX86, "Google", "Chrome", "Application", "chrome.exe") });
|
|
1065
|
-
candidates.push({
|
|
1066
|
-
|
|
1078
|
+
candidates.push({
|
|
1079
|
+
kind: "brave",
|
|
1080
|
+
path: j(programFiles, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
|
|
1081
|
+
});
|
|
1082
|
+
candidates.push({
|
|
1083
|
+
kind: "brave",
|
|
1084
|
+
path: j(programFilesX86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
|
|
1085
|
+
});
|
|
1067
1086
|
candidates.push({ kind: "edge", path: j(programFiles, "Microsoft", "Edge", "Application", "msedge.exe") });
|
|
1068
1087
|
candidates.push({ kind: "edge", path: j(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe") });
|
|
1069
1088
|
return findFirstExe(candidates);
|
|
1070
1089
|
}
|
|
1071
1090
|
function resolveBrowserExecutable(opts) {
|
|
1072
|
-
if (opts?.executablePath) {
|
|
1091
|
+
if (opts?.executablePath !== void 0 && opts.executablePath !== "") {
|
|
1073
1092
|
if (!fileExists(opts.executablePath)) throw new Error(`executablePath not found: ${opts.executablePath}`);
|
|
1074
1093
|
return { kind: "custom", path: opts.executablePath };
|
|
1075
1094
|
}
|
|
@@ -1082,10 +1101,12 @@ function resolveBrowserExecutable(opts) {
|
|
|
1082
1101
|
async function ensurePortAvailable(port) {
|
|
1083
1102
|
await new Promise((resolve2, reject) => {
|
|
1084
1103
|
const tester = net.createServer().once("error", (err) => {
|
|
1085
|
-
if (err.code === "EADDRINUSE") reject(new Error(`Port ${port} is already in use`));
|
|
1104
|
+
if (err.code === "EADDRINUSE") reject(new Error(`Port ${String(port)} is already in use`));
|
|
1086
1105
|
else reject(err);
|
|
1087
1106
|
}).once("listening", () => {
|
|
1088
|
-
tester.close(() =>
|
|
1107
|
+
tester.close(() => {
|
|
1108
|
+
resolve2();
|
|
1109
|
+
});
|
|
1089
1110
|
}).listen(port);
|
|
1090
1111
|
});
|
|
1091
1112
|
}
|
|
@@ -1167,7 +1188,7 @@ function isLoopbackHost(hostname) {
|
|
|
1167
1188
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
1168
1189
|
}
|
|
1169
1190
|
function hasProxyEnvConfigured() {
|
|
1170
|
-
return
|
|
1191
|
+
return (process.env.HTTP_PROXY ?? process.env.HTTPS_PROXY ?? process.env.http_proxy ?? process.env.https_proxy ?? "") !== "";
|
|
1171
1192
|
}
|
|
1172
1193
|
function normalizeCdpWsUrl(wsUrl, cdpUrl) {
|
|
1173
1194
|
const ws = new URL(wsUrl);
|
|
@@ -1219,7 +1240,12 @@ async function canOpenWebSocket(url, timeoutMs) {
|
|
|
1219
1240
|
}
|
|
1220
1241
|
resolve2(value);
|
|
1221
1242
|
};
|
|
1222
|
-
const timer = setTimeout(
|
|
1243
|
+
const timer = setTimeout(
|
|
1244
|
+
() => {
|
|
1245
|
+
finish(false);
|
|
1246
|
+
},
|
|
1247
|
+
Math.max(50, timeoutMs + 25)
|
|
1248
|
+
);
|
|
1223
1249
|
let ws;
|
|
1224
1250
|
try {
|
|
1225
1251
|
ws = new WebSocket(url);
|
|
@@ -1227,21 +1253,27 @@ async function canOpenWebSocket(url, timeoutMs) {
|
|
|
1227
1253
|
finish(false);
|
|
1228
1254
|
return;
|
|
1229
1255
|
}
|
|
1230
|
-
ws.onopen = () =>
|
|
1231
|
-
|
|
1256
|
+
ws.onopen = () => {
|
|
1257
|
+
finish(true);
|
|
1258
|
+
};
|
|
1259
|
+
ws.onerror = () => {
|
|
1260
|
+
finish(false);
|
|
1261
|
+
};
|
|
1232
1262
|
});
|
|
1233
1263
|
}
|
|
1234
1264
|
async function fetchChromeVersion(cdpUrl, timeoutMs = 500, authToken) {
|
|
1235
1265
|
const ctrl = new AbortController();
|
|
1236
|
-
const t = setTimeout(() =>
|
|
1266
|
+
const t = setTimeout(() => {
|
|
1267
|
+
ctrl.abort();
|
|
1268
|
+
}, timeoutMs);
|
|
1237
1269
|
try {
|
|
1238
1270
|
const httpBase = isWebSocketUrl(cdpUrl) ? normalizeCdpHttpBaseForJsonEndpoints(cdpUrl) : cdpUrl;
|
|
1239
1271
|
const headers = {};
|
|
1240
|
-
if (authToken) headers
|
|
1272
|
+
if (authToken !== void 0 && authToken !== "") headers.Authorization = `Bearer ${authToken}`;
|
|
1241
1273
|
const res = await fetch(appendCdpPath(httpBase, "/json/version"), { signal: ctrl.signal, headers });
|
|
1242
1274
|
if (!res.ok) return null;
|
|
1243
1275
|
const data = await res.json();
|
|
1244
|
-
if (
|
|
1276
|
+
if (data === null || data === void 0 || typeof data !== "object") return null;
|
|
1245
1277
|
return data;
|
|
1246
1278
|
} catch {
|
|
1247
1279
|
return null;
|
|
@@ -1257,13 +1289,14 @@ async function isChromeReachable(cdpUrl, timeoutMs = 500, authToken) {
|
|
|
1257
1289
|
async function getChromeWebSocketUrl(cdpUrl, timeoutMs = 500, authToken) {
|
|
1258
1290
|
if (isWebSocketUrl(cdpUrl)) return cdpUrl;
|
|
1259
1291
|
const version = await fetchChromeVersion(cdpUrl, timeoutMs, authToken);
|
|
1260
|
-
const
|
|
1261
|
-
|
|
1292
|
+
const rawWsUrl = version?.webSocketDebuggerUrl;
|
|
1293
|
+
const wsUrl = typeof rawWsUrl === "string" ? rawWsUrl.trim() : "";
|
|
1294
|
+
if (wsUrl === "") return null;
|
|
1262
1295
|
return normalizeCdpWsUrl(wsUrl, cdpUrl);
|
|
1263
1296
|
}
|
|
1264
1297
|
async function isChromeCdpReady(cdpUrl, timeoutMs = 500, handshakeTimeoutMs = 800) {
|
|
1265
1298
|
const wsUrl = await getChromeWebSocketUrl(cdpUrl, timeoutMs);
|
|
1266
|
-
if (
|
|
1299
|
+
if (wsUrl === null) return false;
|
|
1267
1300
|
return await canRunCdpHealthCommand(wsUrl, handshakeTimeoutMs);
|
|
1268
1301
|
}
|
|
1269
1302
|
async function canRunCdpHealthCommand(wsUrl, timeoutMs = 800) {
|
|
@@ -1279,7 +1312,12 @@ async function canRunCdpHealthCommand(wsUrl, timeoutMs = 800) {
|
|
|
1279
1312
|
}
|
|
1280
1313
|
resolve2(value);
|
|
1281
1314
|
};
|
|
1282
|
-
const timer = setTimeout(
|
|
1315
|
+
const timer = setTimeout(
|
|
1316
|
+
() => {
|
|
1317
|
+
finish(false);
|
|
1318
|
+
},
|
|
1319
|
+
Math.max(50, timeoutMs + 25)
|
|
1320
|
+
);
|
|
1283
1321
|
let ws;
|
|
1284
1322
|
try {
|
|
1285
1323
|
ws = new WebSocket(wsUrl);
|
|
@@ -1297,26 +1335,33 @@ async function canRunCdpHealthCommand(wsUrl, timeoutMs = 800) {
|
|
|
1297
1335
|
ws.onmessage = (event) => {
|
|
1298
1336
|
try {
|
|
1299
1337
|
const parsed = JSON.parse(String(event.data));
|
|
1300
|
-
if (parsed
|
|
1301
|
-
|
|
1338
|
+
if (typeof parsed !== "object" || parsed === null) return;
|
|
1339
|
+
const msg = parsed;
|
|
1340
|
+
if (msg.id !== 1) return;
|
|
1341
|
+
finish(typeof msg.result === "object" && msg.result !== null);
|
|
1302
1342
|
} catch {
|
|
1303
1343
|
}
|
|
1304
1344
|
};
|
|
1305
|
-
ws.onerror = () =>
|
|
1306
|
-
|
|
1345
|
+
ws.onerror = () => {
|
|
1346
|
+
finish(false);
|
|
1347
|
+
};
|
|
1348
|
+
ws.onclose = () => {
|
|
1349
|
+
finish(false);
|
|
1350
|
+
};
|
|
1307
1351
|
});
|
|
1308
1352
|
}
|
|
1309
1353
|
async function launchChrome(opts = {}) {
|
|
1310
1354
|
const cdpPort = opts.cdpPort ?? DEFAULT_CDP_PORT;
|
|
1311
1355
|
await ensurePortAvailable(cdpPort);
|
|
1312
1356
|
const exe = resolveBrowserExecutable({ executablePath: opts.executablePath });
|
|
1313
|
-
if (!exe)
|
|
1357
|
+
if (!exe)
|
|
1358
|
+
throw new Error("No supported browser found (Chrome/Brave/Edge/Chromium). Install one or provide executablePath.");
|
|
1314
1359
|
const profileName = opts.profileName ?? DEFAULT_PROFILE_NAME;
|
|
1315
1360
|
const userDataDir = opts.userDataDir ?? resolveUserDataDir(profileName);
|
|
1316
1361
|
fs.mkdirSync(userDataDir, { recursive: true });
|
|
1317
1362
|
const spawnChrome = () => {
|
|
1318
1363
|
const args = [
|
|
1319
|
-
`--remote-debugging-port=${cdpPort}`,
|
|
1364
|
+
`--remote-debugging-port=${String(cdpPort)}`,
|
|
1320
1365
|
`--user-data-dir=${userDataDir}`,
|
|
1321
1366
|
"--no-first-run",
|
|
1322
1367
|
"--no-default-browser-check",
|
|
@@ -1329,10 +1374,10 @@ async function launchChrome(opts = {}) {
|
|
|
1329
1374
|
"--hide-crash-restore-bubble",
|
|
1330
1375
|
"--password-store=basic"
|
|
1331
1376
|
];
|
|
1332
|
-
if (opts.headless) {
|
|
1377
|
+
if (opts.headless === true) {
|
|
1333
1378
|
args.push("--headless=new", "--disable-gpu");
|
|
1334
1379
|
}
|
|
1335
|
-
if (opts.noSandbox) {
|
|
1380
|
+
if (opts.noSandbox === true) {
|
|
1336
1381
|
args.push("--no-sandbox", "--disable-setuid-sandbox");
|
|
1337
1382
|
}
|
|
1338
1383
|
if (process.platform === "linux") args.push("--disable-dev-shm-usage");
|
|
@@ -1379,12 +1424,12 @@ async function launchChrome(opts = {}) {
|
|
|
1379
1424
|
} catch {
|
|
1380
1425
|
}
|
|
1381
1426
|
const proc = spawnChrome();
|
|
1382
|
-
const cdpUrl = `http://127.0.0.1:${cdpPort}`;
|
|
1427
|
+
const cdpUrl = `http://127.0.0.1:${String(cdpPort)}`;
|
|
1383
1428
|
const stderrChunks = [];
|
|
1384
1429
|
const onStderr = (chunk) => {
|
|
1385
1430
|
stderrChunks.push(chunk);
|
|
1386
1431
|
};
|
|
1387
|
-
proc.stderr
|
|
1432
|
+
proc.stderr.on("data", onStderr);
|
|
1388
1433
|
const readyDeadline = Date.now() + 15e3;
|
|
1389
1434
|
while (Date.now() < readyDeadline) {
|
|
1390
1435
|
if (await isChromeReachable(cdpUrl, 500)) break;
|
|
@@ -1395,14 +1440,14 @@ async function launchChrome(opts = {}) {
|
|
|
1395
1440
|
const stderrHint = stderrOutput ? `
|
|
1396
1441
|
Chrome stderr:
|
|
1397
1442
|
${stderrOutput.slice(0, 2e3)}` : "";
|
|
1398
|
-
const sandboxHint = process.platform === "linux" &&
|
|
1443
|
+
const sandboxHint = process.platform === "linux" && opts.noSandbox !== true ? "\nHint: If running in a container or as root, try setting noSandbox: true." : "";
|
|
1399
1444
|
try {
|
|
1400
1445
|
proc.kill("SIGKILL");
|
|
1401
1446
|
} catch {
|
|
1402
1447
|
}
|
|
1403
|
-
throw new Error(`Failed to start Chrome CDP on port ${cdpPort}.${sandboxHint}${stderrHint}`);
|
|
1448
|
+
throw new Error(`Failed to start Chrome CDP on port ${String(cdpPort)}.${sandboxHint}${stderrHint}`);
|
|
1404
1449
|
}
|
|
1405
|
-
proc.stderr
|
|
1450
|
+
proc.stderr.off("data", onStderr);
|
|
1406
1451
|
stderrChunks.length = 0;
|
|
1407
1452
|
return {
|
|
1408
1453
|
pid: proc.pid ?? -1,
|
|
@@ -1415,14 +1460,14 @@ ${stderrOutput.slice(0, 2e3)}` : "";
|
|
|
1415
1460
|
}
|
|
1416
1461
|
async function stopChrome(running, timeoutMs = 2500) {
|
|
1417
1462
|
const proc = running.proc;
|
|
1418
|
-
if (proc.exitCode
|
|
1463
|
+
if (proc.exitCode !== null) return;
|
|
1419
1464
|
try {
|
|
1420
1465
|
proc.kill("SIGTERM");
|
|
1421
1466
|
} catch {
|
|
1422
1467
|
}
|
|
1423
1468
|
const start = Date.now();
|
|
1424
1469
|
while (Date.now() - start < timeoutMs) {
|
|
1425
|
-
if (proc.exitCode
|
|
1470
|
+
if (proc.exitCode !== null) return;
|
|
1426
1471
|
await new Promise((r) => setTimeout(r, 100));
|
|
1427
1472
|
}
|
|
1428
1473
|
try {
|
|
@@ -1430,17 +1475,19 @@ async function stopChrome(running, timeoutMs = 2500) {
|
|
|
1430
1475
|
} catch {
|
|
1431
1476
|
}
|
|
1432
1477
|
}
|
|
1478
|
+
|
|
1479
|
+
// src/connection.ts
|
|
1433
1480
|
var BrowserTabNotFoundError = class extends Error {
|
|
1434
1481
|
constructor(message = "Tab not found") {
|
|
1435
1482
|
super(message);
|
|
1436
1483
|
this.name = "BrowserTabNotFoundError";
|
|
1437
1484
|
}
|
|
1438
1485
|
};
|
|
1439
|
-
var OPENCLAW_EXTENSION_RELAY_BROWSER = "OpenClaw/extension-relay";
|
|
1440
|
-
var extensionRelayByCdpUrl = /* @__PURE__ */ new Map();
|
|
1441
1486
|
async function fetchJsonForCdp(url, timeoutMs) {
|
|
1442
1487
|
const ctrl = new AbortController();
|
|
1443
|
-
const t = setTimeout(() =>
|
|
1488
|
+
const t = setTimeout(() => {
|
|
1489
|
+
ctrl.abort();
|
|
1490
|
+
}, timeoutMs);
|
|
1444
1491
|
try {
|
|
1445
1492
|
const res = await fetch(url, { signal: ctrl.signal });
|
|
1446
1493
|
if (!res.ok) return null;
|
|
@@ -1460,20 +1507,6 @@ function appendCdpPath2(cdpUrl, cdpPath) {
|
|
|
1460
1507
|
return `${cdpUrl.replace(/\/$/, "")}${cdpPath}`;
|
|
1461
1508
|
}
|
|
1462
1509
|
}
|
|
1463
|
-
async function isExtensionRelayCdpEndpoint(cdpUrl) {
|
|
1464
|
-
const normalized = normalizeCdpUrl(cdpUrl);
|
|
1465
|
-
const cached = extensionRelayByCdpUrl.get(normalized);
|
|
1466
|
-
if (cached !== void 0) return cached;
|
|
1467
|
-
try {
|
|
1468
|
-
const version = await fetchJsonForCdp(appendCdpPath2(normalizeCdpHttpBaseForJsonEndpoints(normalized), "/json/version"), 2e3);
|
|
1469
|
-
const isRelay = String(version?.Browser ?? "").trim() === OPENCLAW_EXTENSION_RELAY_BROWSER;
|
|
1470
|
-
extensionRelayByCdpUrl.set(normalized, isRelay);
|
|
1471
|
-
return isRelay;
|
|
1472
|
-
} catch {
|
|
1473
|
-
extensionRelayByCdpUrl.set(normalized, false);
|
|
1474
|
-
return false;
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
1510
|
async function withPlaywrightPageCdpSession(page, fn) {
|
|
1478
1511
|
const session = await page.context().newCDPSession(page);
|
|
1479
1512
|
try {
|
|
@@ -1490,7 +1523,7 @@ async function withPageScopedCdpClient(opts) {
|
|
|
1490
1523
|
}
|
|
1491
1524
|
var LOOPBACK_ENTRIES = "localhost,127.0.0.1,[::1]";
|
|
1492
1525
|
function noProxyAlreadyCoversLocalhost() {
|
|
1493
|
-
const current = process.env.NO_PROXY
|
|
1526
|
+
const current = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
|
|
1494
1527
|
return current.includes("localhost") && current.includes("127.0.0.1") && current.includes("[::1]");
|
|
1495
1528
|
}
|
|
1496
1529
|
function isLoopbackCdpUrl(url) {
|
|
@@ -1508,7 +1541,7 @@ var NoProxyLeaseManager = class {
|
|
|
1508
1541
|
if (this.leaseCount === 0 && !noProxyAlreadyCoversLocalhost()) {
|
|
1509
1542
|
const noProxy = process.env.NO_PROXY;
|
|
1510
1543
|
const noProxyLower = process.env.no_proxy;
|
|
1511
|
-
const current = noProxy
|
|
1544
|
+
const current = noProxy ?? noProxyLower ?? "";
|
|
1512
1545
|
const applied = current ? `${current},${LOOPBACK_ENTRIES}` : LOOPBACK_ENTRIES;
|
|
1513
1546
|
process.env.NO_PROXY = applied;
|
|
1514
1547
|
process.env.no_proxy = applied;
|
|
@@ -1553,9 +1586,12 @@ function getHeadersWithAuth(endpoint, baseHeaders = {}) {
|
|
|
1553
1586
|
const headers = { ...baseHeaders };
|
|
1554
1587
|
try {
|
|
1555
1588
|
const parsed = new URL(endpoint);
|
|
1556
|
-
if (
|
|
1557
|
-
|
|
1558
|
-
|
|
1589
|
+
if (Object.keys(headers).some((k) => k.toLowerCase() === "authorization")) return headers;
|
|
1590
|
+
if (parsed.username || parsed.password) {
|
|
1591
|
+
const credentials = Buffer.from(
|
|
1592
|
+
`${decodeURIComponent(parsed.username)}:${decodeURIComponent(parsed.password)}`
|
|
1593
|
+
).toString("base64");
|
|
1594
|
+
headers.Authorization = `Basic ${credentials}`;
|
|
1559
1595
|
}
|
|
1560
1596
|
} catch {
|
|
1561
1597
|
}
|
|
@@ -1603,7 +1639,7 @@ function roleRefsKey(cdpUrl, targetId) {
|
|
|
1603
1639
|
function findNetworkRequestById(state, id) {
|
|
1604
1640
|
for (let i = state.requests.length - 1; i >= 0; i--) {
|
|
1605
1641
|
const candidate = state.requests[i];
|
|
1606
|
-
if (candidate
|
|
1642
|
+
if (candidate.id === id) return candidate;
|
|
1607
1643
|
}
|
|
1608
1644
|
return void 0;
|
|
1609
1645
|
}
|
|
@@ -1634,16 +1670,16 @@ function ensurePageState(page) {
|
|
|
1634
1670
|
});
|
|
1635
1671
|
page.on("pageerror", (err) => {
|
|
1636
1672
|
state.errors.push({
|
|
1637
|
-
message: err
|
|
1638
|
-
name: err
|
|
1639
|
-
stack: err
|
|
1673
|
+
message: err.message !== "" ? err.message : String(err),
|
|
1674
|
+
name: err.name !== "" ? err.name : void 0,
|
|
1675
|
+
stack: err.stack !== void 0 && err.stack !== "" ? err.stack : void 0,
|
|
1640
1676
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1641
1677
|
});
|
|
1642
1678
|
if (state.errors.length > MAX_PAGE_ERRORS) state.errors.shift();
|
|
1643
1679
|
});
|
|
1644
1680
|
page.on("request", (req) => {
|
|
1645
1681
|
state.nextRequestId += 1;
|
|
1646
|
-
const id = `r${state.nextRequestId}`;
|
|
1682
|
+
const id = `r${String(state.nextRequestId)}`;
|
|
1647
1683
|
state.requestIds.set(req, id);
|
|
1648
1684
|
state.requests.push({
|
|
1649
1685
|
id,
|
|
@@ -1657,7 +1693,7 @@ function ensurePageState(page) {
|
|
|
1657
1693
|
page.on("response", (resp) => {
|
|
1658
1694
|
const req = resp.request();
|
|
1659
1695
|
const id = state.requestIds.get(req);
|
|
1660
|
-
if (
|
|
1696
|
+
if (id === void 0) return;
|
|
1661
1697
|
const rec = findNetworkRequestById(state, id);
|
|
1662
1698
|
if (rec) {
|
|
1663
1699
|
rec.status = resp.status();
|
|
@@ -1666,7 +1702,7 @@ function ensurePageState(page) {
|
|
|
1666
1702
|
});
|
|
1667
1703
|
page.on("requestfailed", (req) => {
|
|
1668
1704
|
const id = state.requestIds.get(req);
|
|
1669
|
-
if (
|
|
1705
|
+
if (id === void 0) return;
|
|
1670
1706
|
const rec = findNetworkRequestById(state, id);
|
|
1671
1707
|
if (rec) {
|
|
1672
1708
|
rec.failureText = req.failure()?.errorText;
|
|
@@ -1683,7 +1719,8 @@ function ensurePageState(page) {
|
|
|
1683
1719
|
var STEALTH_SCRIPT = `Object.defineProperty(navigator, 'webdriver', { get: () => undefined })`;
|
|
1684
1720
|
function applyStealthToPage(page) {
|
|
1685
1721
|
page.evaluate(STEALTH_SCRIPT).catch((e) => {
|
|
1686
|
-
if (process.env.DEBUG
|
|
1722
|
+
if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
|
|
1723
|
+
console.warn("[browserclaw] stealth evaluate failed:", e instanceof Error ? e.message : String(e));
|
|
1687
1724
|
});
|
|
1688
1725
|
}
|
|
1689
1726
|
function observeContext(context) {
|
|
@@ -1691,7 +1728,8 @@ function observeContext(context) {
|
|
|
1691
1728
|
observedContexts.add(context);
|
|
1692
1729
|
ensureContextState(context);
|
|
1693
1730
|
context.addInitScript(STEALTH_SCRIPT).catch((e) => {
|
|
1694
|
-
if (process.env.DEBUG
|
|
1731
|
+
if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
|
|
1732
|
+
console.warn("[browserclaw] stealth initScript failed:", e instanceof Error ? e.message : String(e));
|
|
1695
1733
|
});
|
|
1696
1734
|
for (const page of context.pages()) {
|
|
1697
1735
|
ensurePageState(page);
|
|
@@ -1707,15 +1745,15 @@ function observeBrowser(browser) {
|
|
|
1707
1745
|
}
|
|
1708
1746
|
function rememberRoleRefsForTarget(opts) {
|
|
1709
1747
|
const targetId = opts.targetId.trim();
|
|
1710
|
-
if (
|
|
1748
|
+
if (targetId === "") return;
|
|
1711
1749
|
roleRefsByTarget.set(roleRefsKey(opts.cdpUrl, targetId), {
|
|
1712
1750
|
refs: opts.refs,
|
|
1713
|
-
...opts.frameSelector ? { frameSelector: opts.frameSelector } : {},
|
|
1714
|
-
...opts.mode ? { mode: opts.mode } : {}
|
|
1751
|
+
...opts.frameSelector !== void 0 && opts.frameSelector !== "" ? { frameSelector: opts.frameSelector } : {},
|
|
1752
|
+
...opts.mode !== void 0 ? { mode: opts.mode } : {}
|
|
1715
1753
|
});
|
|
1716
1754
|
while (roleRefsByTarget.size > MAX_ROLE_REFS_CACHE) {
|
|
1717
1755
|
const first = roleRefsByTarget.keys().next();
|
|
1718
|
-
if (first.done) break;
|
|
1756
|
+
if (first.done === true) break;
|
|
1719
1757
|
roleRefsByTarget.delete(first.value);
|
|
1720
1758
|
}
|
|
1721
1759
|
}
|
|
@@ -1724,7 +1762,7 @@ function storeRoleRefsForTarget(opts) {
|
|
|
1724
1762
|
state.roleRefs = opts.refs;
|
|
1725
1763
|
state.roleRefsFrameSelector = opts.frameSelector;
|
|
1726
1764
|
state.roleRefsMode = opts.mode;
|
|
1727
|
-
if (
|
|
1765
|
+
if (opts.targetId === void 0 || opts.targetId.trim() === "") return;
|
|
1728
1766
|
rememberRoleRefsForTarget({
|
|
1729
1767
|
cdpUrl: opts.cdpUrl,
|
|
1730
1768
|
targetId: opts.targetId,
|
|
@@ -1734,8 +1772,8 @@ function storeRoleRefsForTarget(opts) {
|
|
|
1734
1772
|
});
|
|
1735
1773
|
}
|
|
1736
1774
|
function restoreRoleRefsForTarget(opts) {
|
|
1737
|
-
const targetId = opts.targetId?.trim()
|
|
1738
|
-
if (
|
|
1775
|
+
const targetId = opts.targetId?.trim() ?? "";
|
|
1776
|
+
if (targetId === "") return;
|
|
1739
1777
|
const entry = roleRefsByTarget.get(roleRefsKey(opts.cdpUrl, targetId));
|
|
1740
1778
|
if (!entry) return;
|
|
1741
1779
|
const state = ensurePageState(opts.page);
|
|
@@ -1757,8 +1795,12 @@ async function connectBrowser(cdpUrl, authToken) {
|
|
|
1757
1795
|
const timeout = 5e3 + attempt * 2e3;
|
|
1758
1796
|
const endpoint = await getChromeWebSocketUrl(normalized, timeout, authToken).catch(() => null) ?? normalized;
|
|
1759
1797
|
const headers = getHeadersWithAuth(endpoint);
|
|
1760
|
-
if (authToken &&
|
|
1761
|
-
|
|
1798
|
+
if (authToken !== void 0 && authToken !== "" && !headers.Authorization)
|
|
1799
|
+
headers.Authorization = `Bearer ${authToken}`;
|
|
1800
|
+
const browser = await withNoProxyForCdpUrl(
|
|
1801
|
+
endpoint,
|
|
1802
|
+
() => chromium.connectOverCDP(endpoint, { timeout, headers })
|
|
1803
|
+
);
|
|
1762
1804
|
const onDisconnected = () => {
|
|
1763
1805
|
if (cachedByCdpUrl.get(normalized)?.browser === browser) {
|
|
1764
1806
|
cachedByCdpUrl.delete(normalized);
|
|
@@ -1812,7 +1854,9 @@ function cdpSocketNeedsAttach(wsUrl) {
|
|
|
1812
1854
|
async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
1813
1855
|
const httpBase = normalizeCdpHttpBaseForJsonEndpoints(cdpUrl);
|
|
1814
1856
|
const ctrl = new AbortController();
|
|
1815
|
-
const t = setTimeout(() =>
|
|
1857
|
+
const t = setTimeout(() => {
|
|
1858
|
+
ctrl.abort();
|
|
1859
|
+
}, 2e3);
|
|
1816
1860
|
let targets;
|
|
1817
1861
|
try {
|
|
1818
1862
|
const res = await fetch(`${httpBase}/json/list`, { signal: ctrl.signal });
|
|
@@ -1824,9 +1868,12 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
|
1824
1868
|
clearTimeout(t);
|
|
1825
1869
|
}
|
|
1826
1870
|
if (!Array.isArray(targets)) return;
|
|
1827
|
-
const target = targets.find((entry) =>
|
|
1828
|
-
|
|
1829
|
-
|
|
1871
|
+
const target = targets.find((entry) => {
|
|
1872
|
+
const e = entry;
|
|
1873
|
+
return (e.id ?? "").trim() === targetId;
|
|
1874
|
+
});
|
|
1875
|
+
const wsUrlRaw = (target?.webSocketDebuggerUrl ?? "").trim();
|
|
1876
|
+
if (wsUrlRaw === "") return;
|
|
1830
1877
|
const wsUrl = normalizeCdpWsUrl(wsUrlRaw, httpBase);
|
|
1831
1878
|
const needsAttach = cdpSocketNeedsAttach(wsUrl);
|
|
1832
1879
|
await new Promise((resolve2) => {
|
|
@@ -1862,10 +1909,18 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
|
1862
1909
|
if (!needsAttach) return;
|
|
1863
1910
|
try {
|
|
1864
1911
|
const msg = JSON.parse(String(event.data));
|
|
1865
|
-
|
|
1866
|
-
|
|
1912
|
+
const result = msg.result;
|
|
1913
|
+
if (msg.id !== void 0 && result?.sessionId !== void 0) {
|
|
1914
|
+
const sessionId = result.sessionId;
|
|
1915
|
+
ws.send(JSON.stringify({ id: nextId++, sessionId, method: "Runtime.terminateExecution" }));
|
|
1867
1916
|
try {
|
|
1868
|
-
ws.send(
|
|
1917
|
+
ws.send(
|
|
1918
|
+
JSON.stringify({
|
|
1919
|
+
id: nextId++,
|
|
1920
|
+
method: "Target.detachFromTarget",
|
|
1921
|
+
params: { sessionId }
|
|
1922
|
+
})
|
|
1923
|
+
);
|
|
1869
1924
|
} catch {
|
|
1870
1925
|
}
|
|
1871
1926
|
setTimeout(finish, 300);
|
|
@@ -1873,8 +1928,12 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
|
1873
1928
|
} catch {
|
|
1874
1929
|
}
|
|
1875
1930
|
};
|
|
1876
|
-
ws.onerror = () =>
|
|
1877
|
-
|
|
1931
|
+
ws.onerror = () => {
|
|
1932
|
+
finish();
|
|
1933
|
+
};
|
|
1934
|
+
ws.onclose = () => {
|
|
1935
|
+
finish();
|
|
1936
|
+
};
|
|
1878
1937
|
});
|
|
1879
1938
|
}
|
|
1880
1939
|
async function forceDisconnectPlaywrightForTarget(opts) {
|
|
@@ -1886,15 +1945,15 @@ async function forceDisconnectPlaywrightForTarget(opts) {
|
|
|
1886
1945
|
if (cur.onDisconnected && typeof cur.browser.off === "function") {
|
|
1887
1946
|
cur.browser.off("disconnected", cur.onDisconnected);
|
|
1888
1947
|
}
|
|
1889
|
-
const targetId = opts.targetId?.trim()
|
|
1890
|
-
if (targetId) {
|
|
1948
|
+
const targetId = opts.targetId?.trim() ?? "";
|
|
1949
|
+
if (targetId !== "") {
|
|
1891
1950
|
await tryTerminateExecutionViaCdp(normalized, targetId).catch(() => {
|
|
1892
1951
|
});
|
|
1893
1952
|
}
|
|
1894
1953
|
cur.browser.close().catch(() => {
|
|
1895
1954
|
});
|
|
1896
1955
|
}
|
|
1897
|
-
|
|
1956
|
+
function getAllPages(browser) {
|
|
1898
1957
|
return browser.contexts().flatMap((c) => c.pages());
|
|
1899
1958
|
}
|
|
1900
1959
|
async function pageTargetId(page) {
|
|
@@ -1902,7 +1961,7 @@ async function pageTargetId(page) {
|
|
|
1902
1961
|
try {
|
|
1903
1962
|
const info = await session.send("Target.getTargetInfo");
|
|
1904
1963
|
const targetInfo = info.targetInfo;
|
|
1905
|
-
return
|
|
1964
|
+
return (targetInfo?.targetId ?? "").trim() || null;
|
|
1906
1965
|
} finally {
|
|
1907
1966
|
await session.detach().catch(() => {
|
|
1908
1967
|
});
|
|
@@ -1923,21 +1982,15 @@ function matchPageByTargetList(pages, targets, targetId) {
|
|
|
1923
1982
|
return null;
|
|
1924
1983
|
}
|
|
1925
1984
|
async function findPageByTargetIdViaTargetList(pages, targetId, cdpUrl) {
|
|
1926
|
-
const targets = await fetchJsonForCdp(
|
|
1985
|
+
const targets = await fetchJsonForCdp(
|
|
1986
|
+
appendCdpPath2(normalizeCdpHttpBaseForJsonEndpoints(cdpUrl), "/json/list"),
|
|
1987
|
+
2e3
|
|
1988
|
+
);
|
|
1927
1989
|
if (!Array.isArray(targets)) return null;
|
|
1928
1990
|
return matchPageByTargetList(pages, targets, targetId);
|
|
1929
1991
|
}
|
|
1930
1992
|
async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
1931
|
-
const pages =
|
|
1932
|
-
const isExtensionRelay = cdpUrl ? await isExtensionRelayCdpEndpoint(cdpUrl).catch(() => false) : false;
|
|
1933
|
-
if (cdpUrl && isExtensionRelay) {
|
|
1934
|
-
try {
|
|
1935
|
-
const matched = await findPageByTargetIdViaTargetList(pages, targetId, cdpUrl);
|
|
1936
|
-
if (matched) return matched;
|
|
1937
|
-
} catch {
|
|
1938
|
-
}
|
|
1939
|
-
return pages.length === 1 ? pages[0] ?? null : null;
|
|
1940
|
-
}
|
|
1993
|
+
const pages = getAllPages(browser);
|
|
1941
1994
|
let resolvedViaCdp = false;
|
|
1942
1995
|
for (const page of pages) {
|
|
1943
1996
|
let tid = null;
|
|
@@ -1947,9 +2000,9 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
|
1947
2000
|
} catch {
|
|
1948
2001
|
tid = null;
|
|
1949
2002
|
}
|
|
1950
|
-
if (tid && tid === targetId) return page;
|
|
2003
|
+
if (tid !== null && tid !== "" && tid === targetId) return page;
|
|
1951
2004
|
}
|
|
1952
|
-
if (cdpUrl) {
|
|
2005
|
+
if (cdpUrl !== void 0 && cdpUrl !== "") {
|
|
1953
2006
|
try {
|
|
1954
2007
|
return await findPageByTargetIdViaTargetList(pages, targetId, cdpUrl);
|
|
1955
2008
|
} catch {
|
|
@@ -1960,14 +2013,16 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
|
1960
2013
|
}
|
|
1961
2014
|
async function getPageForTargetId(opts) {
|
|
1962
2015
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
1963
|
-
const pages =
|
|
2016
|
+
const pages = getAllPages(browser);
|
|
1964
2017
|
if (!pages.length) throw new Error("No pages available in the connected browser.");
|
|
1965
2018
|
const first = pages[0];
|
|
1966
|
-
if (
|
|
2019
|
+
if (opts.targetId === void 0 || opts.targetId === "") return first;
|
|
1967
2020
|
const found = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
|
|
1968
2021
|
if (!found) {
|
|
1969
2022
|
if (pages.length === 1) return first;
|
|
1970
|
-
throw new BrowserTabNotFoundError(
|
|
2023
|
+
throw new BrowserTabNotFoundError(
|
|
2024
|
+
`Tab not found (targetId: ${opts.targetId}). Call browser.tabs() to list open tabs.`
|
|
2025
|
+
);
|
|
1971
2026
|
}
|
|
1972
2027
|
return found;
|
|
1973
2028
|
}
|
|
@@ -2001,7 +2056,7 @@ function resolveInteractionTimeoutMs(timeoutMs) {
|
|
|
2001
2056
|
function resolveBoundedDelayMs(value, label, maxMs) {
|
|
2002
2057
|
const normalized = Math.floor(value ?? 0);
|
|
2003
2058
|
if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
|
|
2004
|
-
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${maxMs}ms`);
|
|
2059
|
+
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${String(maxMs)}ms`);
|
|
2005
2060
|
return normalized;
|
|
2006
2061
|
}
|
|
2007
2062
|
async function getRestoredPageForTarget(opts) {
|
|
@@ -2012,17 +2067,17 @@ async function getRestoredPageForTarget(opts) {
|
|
|
2012
2067
|
}
|
|
2013
2068
|
function refLocator(page, ref) {
|
|
2014
2069
|
const normalized = ref.startsWith("@") ? ref.slice(1) : ref.startsWith("ref=") ? ref.slice(4) : ref;
|
|
2015
|
-
if (
|
|
2070
|
+
if (normalized.trim() === "") throw new Error("ref is required");
|
|
2016
2071
|
if (/^e\d+$/.test(normalized)) {
|
|
2017
2072
|
const state = pageStates.get(page);
|
|
2018
2073
|
if (state?.roleRefsMode === "aria") {
|
|
2019
|
-
return (state.roleRefsFrameSelector ? page.frameLocator(state.roleRefsFrameSelector) : page).locator(`aria-ref=${normalized}`);
|
|
2074
|
+
return (state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page).locator(`aria-ref=${normalized}`);
|
|
2020
2075
|
}
|
|
2021
2076
|
const info = state?.roleRefs?.[normalized];
|
|
2022
2077
|
if (!info) throw new Error(`Unknown ref "${normalized}". Run a new snapshot and use a ref from that snapshot.`);
|
|
2023
|
-
const locAny = state
|
|
2078
|
+
const locAny = state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page;
|
|
2024
2079
|
const role = info.role;
|
|
2025
|
-
const locator = info.name ? locAny.getByRole(role, { name: info.name, exact: true }) : locAny.getByRole(role);
|
|
2080
|
+
const locator = info.name !== void 0 && info.name !== "" ? locAny.getByRole(role, { name: info.name, exact: true }) : locAny.getByRole(role);
|
|
2026
2081
|
return info.nth !== void 0 ? locator.nth(info.nth) : locator;
|
|
2027
2082
|
}
|
|
2028
2083
|
return page.locator(`aria-ref=${normalized}`);
|
|
@@ -2030,15 +2085,21 @@ function refLocator(page, ref) {
|
|
|
2030
2085
|
function toAIFriendlyError(error, selector) {
|
|
2031
2086
|
const message = error instanceof Error ? error.message : String(error);
|
|
2032
2087
|
if (message.includes("strict mode violation")) {
|
|
2033
|
-
const countMatch =
|
|
2088
|
+
const countMatch = /resolved to (\d+) elements/.exec(message);
|
|
2034
2089
|
const count = countMatch ? countMatch[1] : "multiple";
|
|
2035
|
-
return new Error(
|
|
2090
|
+
return new Error(
|
|
2091
|
+
`Selector "${selector}" matched ${count} elements. Run a new snapshot to get updated refs, or use a different ref.`
|
|
2092
|
+
);
|
|
2036
2093
|
}
|
|
2037
2094
|
if ((message.includes("Timeout") || message.includes("waiting for")) && (message.includes("to be visible") || message.includes("not visible"))) {
|
|
2038
|
-
return new Error(
|
|
2095
|
+
return new Error(
|
|
2096
|
+
`Element "${selector}" not found or not visible. Run a new snapshot to see current page elements.`
|
|
2097
|
+
);
|
|
2039
2098
|
}
|
|
2040
2099
|
if (message.includes("intercepts pointer events") || message.includes("not visible") || message.includes("not receive pointer events")) {
|
|
2041
|
-
return new Error(
|
|
2100
|
+
return new Error(
|
|
2101
|
+
`Element "${selector}" is not interactable (hidden or covered). Try scrolling it into view, closing overlays, or re-snapshotting.`
|
|
2102
|
+
);
|
|
2042
2103
|
}
|
|
2043
2104
|
return error instanceof Error ? error : new Error(message);
|
|
2044
2105
|
}
|
|
@@ -2046,437 +2107,151 @@ function normalizeTimeoutMs(timeoutMs, fallback, maxMs = 12e4) {
|
|
|
2046
2107
|
return Math.max(500, Math.min(maxMs, timeoutMs ?? fallback));
|
|
2047
2108
|
}
|
|
2048
2109
|
|
|
2049
|
-
// src/
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
"
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
"columnheader",
|
|
2074
|
-
"rowheader",
|
|
2075
|
-
"listitem",
|
|
2076
|
-
"article",
|
|
2077
|
-
"region",
|
|
2078
|
-
"main",
|
|
2079
|
-
"navigation"
|
|
2080
|
-
]);
|
|
2081
|
-
var STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
|
|
2082
|
-
"generic",
|
|
2083
|
-
"group",
|
|
2084
|
-
"list",
|
|
2085
|
-
"table",
|
|
2086
|
-
"row",
|
|
2087
|
-
"rowgroup",
|
|
2088
|
-
"grid",
|
|
2089
|
-
"treegrid",
|
|
2090
|
-
"menu",
|
|
2091
|
-
"menubar",
|
|
2092
|
-
"toolbar",
|
|
2093
|
-
"tablist",
|
|
2094
|
-
"tree",
|
|
2095
|
-
"directory",
|
|
2096
|
-
"document",
|
|
2097
|
-
"application",
|
|
2098
|
-
"presentation",
|
|
2099
|
-
"none"
|
|
2100
|
-
]);
|
|
2101
|
-
function getIndentLevel(line) {
|
|
2102
|
-
const match = line.match(/^(\s*)/);
|
|
2103
|
-
return match ? Math.floor(match[1].length / 2) : 0;
|
|
2104
|
-
}
|
|
2105
|
-
function matchInteractiveSnapshotLine(line, options) {
|
|
2106
|
-
const depth = getIndentLevel(line);
|
|
2107
|
-
if (options.maxDepth !== void 0 && depth > options.maxDepth) {
|
|
2108
|
-
return null;
|
|
2109
|
-
}
|
|
2110
|
-
const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
|
|
2111
|
-
if (!match) {
|
|
2112
|
-
return null;
|
|
2113
|
-
}
|
|
2114
|
-
const [, , roleRaw, name, suffix] = match;
|
|
2115
|
-
if (roleRaw.startsWith("/")) {
|
|
2116
|
-
return null;
|
|
2117
|
-
}
|
|
2118
|
-
const role = roleRaw.toLowerCase();
|
|
2119
|
-
return {
|
|
2120
|
-
roleRaw,
|
|
2121
|
-
role,
|
|
2122
|
-
...name ? { name } : {},
|
|
2123
|
-
suffix
|
|
2124
|
-
};
|
|
2125
|
-
}
|
|
2126
|
-
function createRoleNameTracker() {
|
|
2127
|
-
const counts = /* @__PURE__ */ new Map();
|
|
2128
|
-
const refsByKey = /* @__PURE__ */ new Map();
|
|
2129
|
-
return {
|
|
2130
|
-
counts,
|
|
2131
|
-
refsByKey,
|
|
2132
|
-
getKey(role, name) {
|
|
2133
|
-
return `${role}:${name ?? ""}`;
|
|
2134
|
-
},
|
|
2135
|
-
getNextIndex(role, name) {
|
|
2136
|
-
const key = this.getKey(role, name);
|
|
2137
|
-
const current = counts.get(key) ?? 0;
|
|
2138
|
-
counts.set(key, current + 1);
|
|
2139
|
-
return current;
|
|
2140
|
-
},
|
|
2141
|
-
trackRef(role, name, ref) {
|
|
2142
|
-
const key = this.getKey(role, name);
|
|
2143
|
-
const list = refsByKey.get(key) ?? [];
|
|
2144
|
-
list.push(ref);
|
|
2145
|
-
refsByKey.set(key, list);
|
|
2146
|
-
},
|
|
2147
|
-
getDuplicateKeys() {
|
|
2148
|
-
const out = /* @__PURE__ */ new Set();
|
|
2149
|
-
for (const [key, refs] of refsByKey) if (refs.length > 1) out.add(key);
|
|
2150
|
-
return out;
|
|
2110
|
+
// src/actions/evaluate.ts
|
|
2111
|
+
async function evaluateInAllFramesViaPlaywright(opts) {
|
|
2112
|
+
const fnText = opts.fn.trim();
|
|
2113
|
+
if (!fnText) throw new Error("function is required");
|
|
2114
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2115
|
+
const frames = page.frames();
|
|
2116
|
+
const results = [];
|
|
2117
|
+
for (const frame of frames) {
|
|
2118
|
+
try {
|
|
2119
|
+
const result = await frame.evaluate((fnBody) => {
|
|
2120
|
+
"use strict";
|
|
2121
|
+
try {
|
|
2122
|
+
const candidate = (0, eval)("(" + fnBody + ")");
|
|
2123
|
+
return typeof candidate === "function" ? candidate() : candidate;
|
|
2124
|
+
} catch (err) {
|
|
2125
|
+
throw new Error("Invalid evaluate function: " + (err instanceof Error ? err.message : String(err)));
|
|
2126
|
+
}
|
|
2127
|
+
}, fnText);
|
|
2128
|
+
results.push({
|
|
2129
|
+
frameUrl: frame.url(),
|
|
2130
|
+
frameName: frame.name(),
|
|
2131
|
+
result
|
|
2132
|
+
});
|
|
2133
|
+
} catch {
|
|
2151
2134
|
}
|
|
2152
|
-
};
|
|
2153
|
-
}
|
|
2154
|
-
function removeNthFromNonDuplicates(refs, tracker) {
|
|
2155
|
-
const duplicates = tracker.getDuplicateKeys();
|
|
2156
|
-
for (const [ref, data] of Object.entries(refs)) {
|
|
2157
|
-
const key = tracker.getKey(data.role, data.name);
|
|
2158
|
-
if (!duplicates.has(key)) delete refs[ref]?.nth;
|
|
2159
2135
|
}
|
|
2136
|
+
return results;
|
|
2160
2137
|
}
|
|
2161
|
-
function
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
}
|
|
2170
|
-
if (line.includes(":") && !line.trimEnd().endsWith(":")) {
|
|
2171
|
-
result.push(line);
|
|
2172
|
-
continue;
|
|
2173
|
-
}
|
|
2174
|
-
const currentIndent = getIndentLevel(line);
|
|
2175
|
-
let hasRelevantChildren = false;
|
|
2176
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
2177
|
-
if (getIndentLevel(lines[j]) <= currentIndent) break;
|
|
2178
|
-
if (lines[j]?.includes("[ref=")) {
|
|
2179
|
-
hasRelevantChildren = true;
|
|
2180
|
-
break;
|
|
2181
|
-
}
|
|
2182
|
-
}
|
|
2183
|
-
if (hasRelevantChildren) result.push(line);
|
|
2138
|
+
async function awaitEvalWithAbort(evalPromise, abortPromise) {
|
|
2139
|
+
if (!abortPromise) return await evalPromise;
|
|
2140
|
+
try {
|
|
2141
|
+
return await Promise.race([evalPromise, abortPromise]);
|
|
2142
|
+
} catch (err) {
|
|
2143
|
+
evalPromise.catch(() => {
|
|
2144
|
+
});
|
|
2145
|
+
throw err;
|
|
2184
2146
|
}
|
|
2185
|
-
return result.join("\n");
|
|
2186
2147
|
}
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
2203
|
-
const prefix = line.match(/^(\s*-\s*)/)?.[1] ?? "";
|
|
2204
|
-
const ref = nextRef();
|
|
2205
|
-
const nth = tracker.getNextIndex(role, name);
|
|
2206
|
-
tracker.trackRef(role, name, ref);
|
|
2207
|
-
refs[ref] = { role, name, nth };
|
|
2208
|
-
let enhanced = `${prefix}${roleRaw}`;
|
|
2209
|
-
if (name) enhanced += ` "${name}"`;
|
|
2210
|
-
enhanced += ` [ref=${ref}]`;
|
|
2211
|
-
if (nth > 0) enhanced += ` [nth=${nth}]`;
|
|
2212
|
-
if (suffix.includes("[")) enhanced += suffix;
|
|
2213
|
-
result2.push(enhanced);
|
|
2148
|
+
var BROWSER_EVALUATOR = new Function(
|
|
2149
|
+
"args",
|
|
2150
|
+
`
|
|
2151
|
+
"use strict";
|
|
2152
|
+
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
2153
|
+
try {
|
|
2154
|
+
var candidate = eval("(" + fnBody + ")");
|
|
2155
|
+
var result = typeof candidate === "function" ? candidate() : candidate;
|
|
2156
|
+
if (result && typeof result.then === "function") {
|
|
2157
|
+
return Promise.race([
|
|
2158
|
+
result,
|
|
2159
|
+
new Promise(function(_, reject) {
|
|
2160
|
+
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
2161
|
+
})
|
|
2162
|
+
]);
|
|
2214
2163
|
}
|
|
2215
|
-
|
|
2216
|
-
|
|
2164
|
+
return result;
|
|
2165
|
+
} catch (err) {
|
|
2166
|
+
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
2217
2167
|
}
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
if (options.compact && isStructural && !name) continue;
|
|
2237
|
-
if (!(isInteractive || isContent && name)) {
|
|
2238
|
-
result.push(line);
|
|
2239
|
-
continue;
|
|
2240
|
-
}
|
|
2241
|
-
const ref = nextRef();
|
|
2242
|
-
const nth = tracker.getNextIndex(role, name);
|
|
2243
|
-
tracker.trackRef(role, name, ref);
|
|
2244
|
-
refs[ref] = { role, name, nth };
|
|
2245
|
-
let enhanced = `${prefix}${roleRaw}`;
|
|
2246
|
-
if (name) enhanced += ` "${name}"`;
|
|
2247
|
-
enhanced += ` [ref=${ref}]`;
|
|
2248
|
-
if (nth > 0) enhanced += ` [nth=${nth}]`;
|
|
2249
|
-
if (suffix) enhanced += suffix;
|
|
2250
|
-
result.push(enhanced);
|
|
2251
|
-
}
|
|
2252
|
-
removeNthFromNonDuplicates(refs, tracker);
|
|
2253
|
-
const tree = result.join("\n") || "(empty)";
|
|
2254
|
-
return { snapshot: options.compact ? compactTree(tree) : tree, refs };
|
|
2255
|
-
}
|
|
2256
|
-
function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
|
|
2257
|
-
const lines = String(aiSnapshot ?? "").split("\n");
|
|
2258
|
-
const refs = {};
|
|
2259
|
-
function parseAiSnapshotRef(suffix) {
|
|
2260
|
-
const match = suffix.match(/\[ref=(e\d+)\]/i);
|
|
2261
|
-
return match ? match[1] : null;
|
|
2262
|
-
}
|
|
2263
|
-
if (options.interactive) {
|
|
2264
|
-
const out2 = [];
|
|
2265
|
-
for (const line of lines) {
|
|
2266
|
-
const parsed = matchInteractiveSnapshotLine(line, options);
|
|
2267
|
-
if (!parsed) continue;
|
|
2268
|
-
const { roleRaw, role, name, suffix } = parsed;
|
|
2269
|
-
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
2270
|
-
const ref = parseAiSnapshotRef(suffix);
|
|
2271
|
-
if (!ref) continue;
|
|
2272
|
-
const prefix = line.match(/^(\s*-\s*)/)?.[1] ?? "";
|
|
2273
|
-
refs[ref] = { role, ...name ? { name } : {} };
|
|
2274
|
-
out2.push(`${prefix}${roleRaw}${name ? ` "${name}"` : ""}${suffix}`);
|
|
2275
|
-
}
|
|
2276
|
-
return { snapshot: out2.join("\n") || "(no interactive elements)", refs };
|
|
2277
|
-
}
|
|
2278
|
-
const out = [];
|
|
2279
|
-
for (const line of lines) {
|
|
2280
|
-
const depth = getIndentLevel(line);
|
|
2281
|
-
if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
|
|
2282
|
-
const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
|
|
2283
|
-
if (!match) {
|
|
2284
|
-
out.push(line);
|
|
2285
|
-
continue;
|
|
2286
|
-
}
|
|
2287
|
-
const [, , roleRaw, name, suffix] = match;
|
|
2288
|
-
if (roleRaw.startsWith("/")) {
|
|
2289
|
-
out.push(line);
|
|
2290
|
-
continue;
|
|
2168
|
+
`
|
|
2169
|
+
);
|
|
2170
|
+
var ELEMENT_EVALUATOR = new Function(
|
|
2171
|
+
"el",
|
|
2172
|
+
"args",
|
|
2173
|
+
`
|
|
2174
|
+
"use strict";
|
|
2175
|
+
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
2176
|
+
try {
|
|
2177
|
+
var candidate = eval("(" + fnBody + ")");
|
|
2178
|
+
var result = typeof candidate === "function" ? candidate(el) : candidate;
|
|
2179
|
+
if (result && typeof result.then === "function") {
|
|
2180
|
+
return Promise.race([
|
|
2181
|
+
result,
|
|
2182
|
+
new Promise(function(_, reject) {
|
|
2183
|
+
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
2184
|
+
})
|
|
2185
|
+
]);
|
|
2291
2186
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
const ref = parseAiSnapshotRef(suffix);
|
|
2296
|
-
if (ref) refs[ref] = { role, ...name ? { name } : {} };
|
|
2297
|
-
out.push(line);
|
|
2298
|
-
}
|
|
2299
|
-
const tree = out.join("\n") || "(empty)";
|
|
2300
|
-
return { snapshot: options.compact ? compactTree(tree) : tree, refs };
|
|
2301
|
-
}
|
|
2302
|
-
function getRoleSnapshotStats(snapshot, refs) {
|
|
2303
|
-
const interactive = Object.values(refs).filter((r) => INTERACTIVE_ROLES.has(r.role)).length;
|
|
2304
|
-
return {
|
|
2305
|
-
lines: snapshot.split("\n").length,
|
|
2306
|
-
chars: snapshot.length,
|
|
2307
|
-
refs: Object.keys(refs).length,
|
|
2308
|
-
interactive
|
|
2309
|
-
};
|
|
2310
|
-
}
|
|
2311
|
-
|
|
2312
|
-
// src/snapshot/ai-snapshot.ts
|
|
2313
|
-
async function snapshotAi(opts) {
|
|
2314
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2315
|
-
ensurePageState(page);
|
|
2316
|
-
const maybe = page;
|
|
2317
|
-
if (!maybe._snapshotForAI) {
|
|
2318
|
-
throw new Error("Playwright _snapshotForAI is not available. Upgrade playwright-core to >= 1.50.");
|
|
2319
|
-
}
|
|
2320
|
-
const sourceUrl = page.url();
|
|
2321
|
-
const result = await maybe._snapshotForAI({
|
|
2322
|
-
timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3, 6e4),
|
|
2323
|
-
track: "response"
|
|
2324
|
-
});
|
|
2325
|
-
let snapshot = String(result?.full ?? "");
|
|
2326
|
-
const maxChars = opts.maxChars;
|
|
2327
|
-
const limit = typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : void 0;
|
|
2328
|
-
let truncated = false;
|
|
2329
|
-
if (limit && snapshot.length > limit) {
|
|
2330
|
-
const lastNewline = snapshot.lastIndexOf("\n", limit);
|
|
2331
|
-
const cutoff = lastNewline > 0 ? lastNewline : limit;
|
|
2332
|
-
snapshot = `${snapshot.slice(0, cutoff)}
|
|
2333
|
-
|
|
2334
|
-
[...TRUNCATED - page too large]`;
|
|
2335
|
-
truncated = true;
|
|
2187
|
+
return result;
|
|
2188
|
+
} catch (err) {
|
|
2189
|
+
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
2336
2190
|
}
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
refs: built.refs,
|
|
2343
|
-
mode: "aria"
|
|
2344
|
-
});
|
|
2345
|
-
return {
|
|
2346
|
-
snapshot: built.snapshot,
|
|
2347
|
-
refs: built.refs,
|
|
2348
|
-
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
2349
|
-
...truncated ? { truncated } : {},
|
|
2350
|
-
untrusted: true,
|
|
2351
|
-
contentMeta: {
|
|
2352
|
-
sourceUrl,
|
|
2353
|
-
contentType: "browser-snapshot",
|
|
2354
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2355
|
-
}
|
|
2356
|
-
};
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
// src/snapshot/aria-snapshot.ts
|
|
2360
|
-
async function snapshotRole(opts) {
|
|
2191
|
+
`
|
|
2192
|
+
);
|
|
2193
|
+
async function evaluateViaPlaywright(opts) {
|
|
2194
|
+
const fnText = opts.fn.trim();
|
|
2195
|
+
if (!fnText) throw new Error("function is required");
|
|
2361
2196
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2362
2197
|
ensurePageState(page);
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
cdpUrl: opts.cdpUrl,
|
|
2377
|
-
targetId: opts.targetId,
|
|
2378
|
-
refs: built2.refs,
|
|
2379
|
-
mode: "aria"
|
|
2198
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
2199
|
+
const outerTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
|
|
2200
|
+
let evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 500));
|
|
2201
|
+
evaluateTimeout = Math.min(evaluateTimeout, outerTimeout);
|
|
2202
|
+
const signal = opts.signal;
|
|
2203
|
+
let abortListener;
|
|
2204
|
+
let abortReject;
|
|
2205
|
+
let abortPromise;
|
|
2206
|
+
if (signal !== void 0) {
|
|
2207
|
+
abortPromise = new Promise((_, reject) => {
|
|
2208
|
+
abortReject = reject;
|
|
2209
|
+
});
|
|
2210
|
+
abortPromise.catch(() => {
|
|
2380
2211
|
});
|
|
2381
|
-
return {
|
|
2382
|
-
snapshot: built2.snapshot,
|
|
2383
|
-
refs: built2.refs,
|
|
2384
|
-
stats: getRoleSnapshotStats(built2.snapshot, built2.refs),
|
|
2385
|
-
untrusted: true,
|
|
2386
|
-
contentMeta: {
|
|
2387
|
-
sourceUrl,
|
|
2388
|
-
contentType: "browser-snapshot",
|
|
2389
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2390
|
-
}
|
|
2391
|
-
};
|
|
2392
2212
|
}
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
frameSelector: frameSelector || void 0,
|
|
2404
|
-
mode: "role"
|
|
2405
|
-
});
|
|
2406
|
-
return {
|
|
2407
|
-
snapshot: built.snapshot,
|
|
2408
|
-
refs: built.refs,
|
|
2409
|
-
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
2410
|
-
untrusted: true,
|
|
2411
|
-
contentMeta: {
|
|
2412
|
-
sourceUrl,
|
|
2413
|
-
contentType: "browser-snapshot",
|
|
2414
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2213
|
+
if (signal !== void 0) {
|
|
2214
|
+
const disconnect = () => {
|
|
2215
|
+
forceDisconnectPlaywrightForTarget({
|
|
2216
|
+
cdpUrl: opts.cdpUrl,
|
|
2217
|
+
targetId: opts.targetId}).catch(() => {
|
|
2218
|
+
});
|
|
2219
|
+
};
|
|
2220
|
+
if (signal.aborted) {
|
|
2221
|
+
disconnect();
|
|
2222
|
+
throw signal.reason ?? new Error("aborted");
|
|
2415
2223
|
}
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
await session.send("Accessibility.enable").catch(() => {
|
|
2425
|
-
});
|
|
2426
|
-
return await session.send("Accessibility.getFullAXTree");
|
|
2427
|
-
});
|
|
2428
|
-
return {
|
|
2429
|
-
nodes: formatAriaNodes(Array.isArray(res?.nodes) ? res.nodes : [], limit),
|
|
2430
|
-
untrusted: true,
|
|
2431
|
-
contentMeta: {
|
|
2432
|
-
sourceUrl,
|
|
2433
|
-
contentType: "browser-aria-tree",
|
|
2434
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2224
|
+
abortListener = () => {
|
|
2225
|
+
disconnect();
|
|
2226
|
+
abortReject?.(signal.reason ?? new Error("aborted"));
|
|
2227
|
+
};
|
|
2228
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
2229
|
+
if (signal.aborted) {
|
|
2230
|
+
abortListener();
|
|
2231
|
+
throw signal.reason ?? new Error("aborted");
|
|
2435
2232
|
}
|
|
2436
|
-
}
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
}
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
for (const n of nodes) if (n.nodeId) byId.set(n.nodeId, n);
|
|
2448
|
-
const referenced = /* @__PURE__ */ new Set();
|
|
2449
|
-
for (const n of nodes) for (const c of n.childIds ?? []) referenced.add(c);
|
|
2450
|
-
const root = nodes.find((n) => n.nodeId && !referenced.has(n.nodeId)) ?? nodes[0];
|
|
2451
|
-
if (!root?.nodeId) return [];
|
|
2452
|
-
const out = [];
|
|
2453
|
-
const stack = [{ id: root.nodeId, depth: 0 }];
|
|
2454
|
-
while (stack.length && out.length < limit) {
|
|
2455
|
-
const popped = stack.pop();
|
|
2456
|
-
if (!popped) break;
|
|
2457
|
-
const { id, depth } = popped;
|
|
2458
|
-
const n = byId.get(id);
|
|
2459
|
-
if (!n) continue;
|
|
2460
|
-
const role = axValue(n.role);
|
|
2461
|
-
const name = axValue(n.name);
|
|
2462
|
-
const value = axValue(n.value);
|
|
2463
|
-
const description = axValue(n.description);
|
|
2464
|
-
const ref = `ax${out.length + 1}`;
|
|
2465
|
-
out.push({
|
|
2466
|
-
ref,
|
|
2467
|
-
role: role || "unknown",
|
|
2468
|
-
name: name || "",
|
|
2469
|
-
...value ? { value } : {},
|
|
2470
|
-
...description ? { description } : {},
|
|
2471
|
-
...typeof n.backendDOMNodeId === "number" ? { backendDOMNodeId: n.backendDOMNodeId } : {},
|
|
2472
|
-
depth
|
|
2473
|
-
});
|
|
2474
|
-
const children = (n.childIds ?? []).filter((c) => byId.has(c));
|
|
2475
|
-
for (let i = children.length - 1; i >= 0; i--) {
|
|
2476
|
-
if (children[i]) stack.push({ id: children[i], depth: depth + 1 });
|
|
2233
|
+
}
|
|
2234
|
+
try {
|
|
2235
|
+
if (opts.ref !== void 0 && opts.ref !== "") {
|
|
2236
|
+
const locator = refLocator(page, opts.ref);
|
|
2237
|
+
return await awaitEvalWithAbort(
|
|
2238
|
+
locator.evaluate(ELEMENT_EVALUATOR, {
|
|
2239
|
+
fnBody: fnText,
|
|
2240
|
+
timeoutMs: evaluateTimeout
|
|
2241
|
+
}),
|
|
2242
|
+
abortPromise
|
|
2243
|
+
);
|
|
2477
2244
|
}
|
|
2245
|
+
return await awaitEvalWithAbort(
|
|
2246
|
+
page.evaluate(BROWSER_EVALUATOR, {
|
|
2247
|
+
fnBody: fnText,
|
|
2248
|
+
timeoutMs: evaluateTimeout
|
|
2249
|
+
}),
|
|
2250
|
+
abortPromise
|
|
2251
|
+
);
|
|
2252
|
+
} finally {
|
|
2253
|
+
if (signal && abortListener) signal.removeEventListener("abort", abortListener);
|
|
2478
2254
|
}
|
|
2479
|
-
return out;
|
|
2480
2255
|
}
|
|
2481
2256
|
|
|
2482
2257
|
// src/security.ts
|
|
@@ -2493,11 +2268,7 @@ function withBrowserNavigationPolicy(ssrfPolicy) {
|
|
|
2493
2268
|
var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
2494
2269
|
var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
|
|
2495
2270
|
var PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"];
|
|
2496
|
-
var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
2497
|
-
"localhost",
|
|
2498
|
-
"localhost.localdomain",
|
|
2499
|
-
"metadata.google.internal"
|
|
2500
|
-
]);
|
|
2271
|
+
var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "localhost.localdomain", "metadata.google.internal"]);
|
|
2501
2272
|
function isAllowedNonNetworkNavigationUrl(parsed) {
|
|
2502
2273
|
return SAFE_NON_NETWORK_URLS.has(parsed.href);
|
|
2503
2274
|
}
|
|
@@ -2512,7 +2283,7 @@ function hasProxyEnvConfigured2(env = process.env) {
|
|
|
2512
2283
|
return false;
|
|
2513
2284
|
}
|
|
2514
2285
|
function normalizeHostname(hostname) {
|
|
2515
|
-
let h =
|
|
2286
|
+
let h = hostname.trim().toLowerCase();
|
|
2516
2287
|
if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
|
|
2517
2288
|
if (h.endsWith(".")) h = h.slice(0, -1);
|
|
2518
2289
|
return h;
|
|
@@ -2531,13 +2302,7 @@ var BLOCKED_IPV4_RANGES = /* @__PURE__ */ new Set([
|
|
|
2531
2302
|
"private",
|
|
2532
2303
|
"reserved"
|
|
2533
2304
|
]);
|
|
2534
|
-
var BLOCKED_IPV6_RANGES = /* @__PURE__ */ new Set([
|
|
2535
|
-
"unspecified",
|
|
2536
|
-
"loopback",
|
|
2537
|
-
"linkLocal",
|
|
2538
|
-
"uniqueLocal",
|
|
2539
|
-
"multicast"
|
|
2540
|
-
]);
|
|
2305
|
+
var BLOCKED_IPV6_RANGES = /* @__PURE__ */ new Set(["unspecified", "loopback", "linkLocal", "uniqueLocal", "multicast"]);
|
|
2541
2306
|
var RFC2544_BENCHMARK_PREFIX = [ipaddr.IPv4.parse("198.18.0.0"), 15];
|
|
2542
2307
|
var EMBEDDED_IPV4_SENTINEL_RULES = [
|
|
2543
2308
|
// IPv4-compatible (::a.b.c.d)
|
|
@@ -2586,12 +2351,12 @@ function parseIpv6WithEmbeddedIpv4(raw) {
|
|
|
2586
2351
|
}
|
|
2587
2352
|
function normalizeIpParseInput(raw) {
|
|
2588
2353
|
const trimmed = raw?.trim();
|
|
2589
|
-
if (
|
|
2354
|
+
if (trimmed === void 0 || trimmed === "") return;
|
|
2590
2355
|
return stripIpv6Brackets(trimmed);
|
|
2591
2356
|
}
|
|
2592
2357
|
function parseCanonicalIpAddress(raw) {
|
|
2593
2358
|
const normalized = normalizeIpParseInput(raw);
|
|
2594
|
-
if (
|
|
2359
|
+
if (normalized === void 0) return;
|
|
2595
2360
|
if (ipaddr.IPv4.isValid(normalized)) {
|
|
2596
2361
|
if (!ipaddr.IPv4.isValidFourPartDecimal(normalized)) return;
|
|
2597
2362
|
return ipaddr.IPv4.parse(normalized);
|
|
@@ -2601,20 +2366,20 @@ function parseCanonicalIpAddress(raw) {
|
|
|
2601
2366
|
}
|
|
2602
2367
|
function parseLooseIpAddress(raw) {
|
|
2603
2368
|
const normalized = normalizeIpParseInput(raw);
|
|
2604
|
-
if (
|
|
2369
|
+
if (normalized === void 0) return;
|
|
2605
2370
|
if (ipaddr.isValid(normalized)) return ipaddr.parse(normalized);
|
|
2606
2371
|
return parseIpv6WithEmbeddedIpv4(normalized);
|
|
2607
2372
|
}
|
|
2608
2373
|
function isCanonicalDottedDecimalIPv4(raw) {
|
|
2609
|
-
const trimmed = raw
|
|
2610
|
-
if (
|
|
2374
|
+
const trimmed = raw.trim();
|
|
2375
|
+
if (trimmed === "") return false;
|
|
2611
2376
|
const normalized = stripIpv6Brackets(trimmed);
|
|
2612
2377
|
if (!normalized) return false;
|
|
2613
2378
|
return ipaddr.IPv4.isValidFourPartDecimal(normalized);
|
|
2614
2379
|
}
|
|
2615
2380
|
function isLegacyIpv4Literal(raw) {
|
|
2616
|
-
const trimmed = raw
|
|
2617
|
-
if (
|
|
2381
|
+
const trimmed = raw.trim();
|
|
2382
|
+
if (trimmed === "") return false;
|
|
2618
2383
|
const normalized = stripIpv6Brackets(trimmed);
|
|
2619
2384
|
if (!normalized || normalized.includes(":")) return false;
|
|
2620
2385
|
if (isCanonicalDottedDecimalIPv4(normalized)) return false;
|
|
@@ -2640,12 +2405,7 @@ function isBlockedSpecialUseIpv6Address(address) {
|
|
|
2640
2405
|
return (address.parts[0] & 65472) === 65216;
|
|
2641
2406
|
}
|
|
2642
2407
|
function decodeIpv4FromHextets(high, low) {
|
|
2643
|
-
const octets = [
|
|
2644
|
-
high >>> 8 & 255,
|
|
2645
|
-
high & 255,
|
|
2646
|
-
low >>> 8 & 255,
|
|
2647
|
-
low & 255
|
|
2648
|
-
];
|
|
2408
|
+
const octets = [high >>> 8 & 255, high & 255, low >>> 8 & 255, low & 255];
|
|
2649
2409
|
return ipaddr.IPv4.parse(octets.join("."));
|
|
2650
2410
|
}
|
|
2651
2411
|
function extractEmbeddedIpv4FromIpv6(address) {
|
|
@@ -2692,9 +2452,7 @@ function normalizeHostnameSet(values) {
|
|
|
2692
2452
|
function normalizeHostnameAllowlist(values) {
|
|
2693
2453
|
if (!values || values.length === 0) return [];
|
|
2694
2454
|
return Array.from(
|
|
2695
|
-
new Set(
|
|
2696
|
-
values.map((v) => normalizeHostname(v)).filter((v) => v !== "*" && v !== "*." && v.length > 0)
|
|
2697
|
-
)
|
|
2455
|
+
new Set(values.map((v) => normalizeHostname(v)).filter((v) => v !== "*" && v !== "*." && v.length > 0))
|
|
2698
2456
|
);
|
|
2699
2457
|
}
|
|
2700
2458
|
function isHostnameAllowedByPattern(hostname, pattern) {
|
|
@@ -2729,19 +2487,25 @@ function createPinnedLookup(params) {
|
|
|
2729
2487
|
family: address.includes(":") ? 6 : 4
|
|
2730
2488
|
}));
|
|
2731
2489
|
let index = 0;
|
|
2732
|
-
return ((
|
|
2733
|
-
const
|
|
2734
|
-
|
|
2735
|
-
const
|
|
2736
|
-
if (
|
|
2737
|
-
|
|
2738
|
-
|
|
2490
|
+
return ((_host, ...rest) => {
|
|
2491
|
+
const second = rest[0];
|
|
2492
|
+
const third = rest[1];
|
|
2493
|
+
const cb = typeof second === "function" ? second : typeof third === "function" ? third : void 0;
|
|
2494
|
+
if (cb === void 0) return;
|
|
2495
|
+
const normalized = normalizeHostname(_host);
|
|
2496
|
+
if (normalized === "" || normalized !== normalizedHost) {
|
|
2497
|
+
if (typeof second === "function" || second === void 0) {
|
|
2498
|
+
fallback(_host, cb);
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
fallback(_host, second, cb);
|
|
2502
|
+
return;
|
|
2739
2503
|
}
|
|
2740
|
-
const opts = typeof
|
|
2741
|
-
const requestedFamily = typeof
|
|
2504
|
+
const opts = typeof second === "object" ? second : {};
|
|
2505
|
+
const requestedFamily = typeof second === "number" ? second : typeof opts.family === "number" ? opts.family : 0;
|
|
2742
2506
|
const candidates = requestedFamily === 4 || requestedFamily === 6 ? records.filter((entry) => entry.family === requestedFamily) : records;
|
|
2743
2507
|
const usable = candidates.length > 0 ? candidates : records;
|
|
2744
|
-
if (opts.all) {
|
|
2508
|
+
if (opts.all === true) {
|
|
2745
2509
|
cb(null, usable);
|
|
2746
2510
|
return;
|
|
2747
2511
|
}
|
|
@@ -2759,9 +2523,7 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
2759
2523
|
const isExplicitlyAllowed = allowedHostnames.has(normalized);
|
|
2760
2524
|
const skipPrivateNetworkChecks = allowPrivateNetwork || isExplicitlyAllowed;
|
|
2761
2525
|
if (!matchesHostnameAllowlist(normalized, hostnameAllowlist)) {
|
|
2762
|
-
throw new InvalidBrowserNavigationUrlError(
|
|
2763
|
-
`Navigation blocked: hostname "${hostname}" is not in the allowlist.`
|
|
2764
|
-
);
|
|
2526
|
+
throw new InvalidBrowserNavigationUrlError(`Navigation blocked: hostname "${hostname}" is not in the allowlist.`);
|
|
2765
2527
|
}
|
|
2766
2528
|
if (!skipPrivateNetworkChecks) {
|
|
2767
2529
|
if (isBlockedHostnameOrIp(normalized, params.policy)) {
|
|
@@ -2779,7 +2541,7 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
2779
2541
|
`Navigation to internal/loopback address blocked: unable to resolve "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
2780
2542
|
);
|
|
2781
2543
|
}
|
|
2782
|
-
if (
|
|
2544
|
+
if (results.length === 0) {
|
|
2783
2545
|
throw new InvalidBrowserNavigationUrlError(
|
|
2784
2546
|
`Navigation to internal/loopback address blocked: unable to resolve "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
2785
2547
|
);
|
|
@@ -2806,8 +2568,8 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
2806
2568
|
};
|
|
2807
2569
|
}
|
|
2808
2570
|
async function assertBrowserNavigationAllowed(opts) {
|
|
2809
|
-
const rawUrl =
|
|
2810
|
-
if (
|
|
2571
|
+
const rawUrl = opts.url.trim();
|
|
2572
|
+
if (rawUrl === "") throw new InvalidBrowserNavigationUrlError("url is required");
|
|
2811
2573
|
let parsed;
|
|
2812
2574
|
try {
|
|
2813
2575
|
parsed = new URL(rawUrl);
|
|
@@ -2836,7 +2598,7 @@ async function assertSafeOutputPath(path2, allowedRoots) {
|
|
|
2836
2598
|
if (normalized.includes("..")) {
|
|
2837
2599
|
throw new Error(`Unsafe output path: directory traversal detected in "${path2}".`);
|
|
2838
2600
|
}
|
|
2839
|
-
if (allowedRoots
|
|
2601
|
+
if (allowedRoots !== void 0 && allowedRoots.length > 0) {
|
|
2840
2602
|
const resolved = resolve(normalized);
|
|
2841
2603
|
let parentReal;
|
|
2842
2604
|
try {
|
|
@@ -2894,8 +2656,8 @@ async function resolveStrictExistingUploadPaths(params) {
|
|
|
2894
2656
|
}
|
|
2895
2657
|
}
|
|
2896
2658
|
function sanitizeUntrustedFileName(fileName, fallbackName) {
|
|
2897
|
-
const trimmed =
|
|
2898
|
-
if (
|
|
2659
|
+
const trimmed = fileName.trim();
|
|
2660
|
+
if (trimmed === "") return fallbackName;
|
|
2899
2661
|
let base = posix.basename(trimmed);
|
|
2900
2662
|
base = win32.basename(base);
|
|
2901
2663
|
let cleaned = "";
|
|
@@ -2929,16 +2691,17 @@ async function writeViaSiblingTempPath(params) {
|
|
|
2929
2691
|
await rename(tempPath, targetPath);
|
|
2930
2692
|
renameSucceeded = true;
|
|
2931
2693
|
} finally {
|
|
2932
|
-
if (!renameSucceeded)
|
|
2933
|
-
|
|
2694
|
+
if (!renameSucceeded)
|
|
2695
|
+
await rm(tempPath, { force: true }).catch(() => {
|
|
2696
|
+
});
|
|
2934
2697
|
}
|
|
2935
2698
|
}
|
|
2936
2699
|
function isAbsolute(p) {
|
|
2937
2700
|
return p.startsWith("/") || /^[a-zA-Z]:/.test(p);
|
|
2938
2701
|
}
|
|
2939
2702
|
async function assertBrowserNavigationResultAllowed(opts) {
|
|
2940
|
-
const rawUrl =
|
|
2941
|
-
if (
|
|
2703
|
+
const rawUrl = opts.url.trim();
|
|
2704
|
+
if (rawUrl === "") return;
|
|
2942
2705
|
let parsed;
|
|
2943
2706
|
try {
|
|
2944
2707
|
parsed = new URL(rawUrl);
|
|
@@ -2968,18 +2731,20 @@ function requiresInspectableBrowserNavigationRedirects(ssrfPolicy) {
|
|
|
2968
2731
|
var MAX_CLICK_DELAY_MS = 5e3;
|
|
2969
2732
|
var CHECKABLE_ROLES = /* @__PURE__ */ new Set(["menuitemcheckbox", "menuitemradio", "checkbox", "switch"]);
|
|
2970
2733
|
function resolveLocator(page, resolved) {
|
|
2971
|
-
|
|
2734
|
+
if (resolved.ref !== void 0 && resolved.ref !== "") return refLocator(page, resolved.ref);
|
|
2735
|
+
const sel = resolved.selector ?? "";
|
|
2736
|
+
return page.locator(sel);
|
|
2972
2737
|
}
|
|
2973
2738
|
async function clickViaPlaywright(opts) {
|
|
2974
2739
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
2975
2740
|
const page = await getRestoredPageForTarget(opts);
|
|
2976
|
-
const label = resolved.ref ?? resolved.selector;
|
|
2741
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
2977
2742
|
const locator = resolveLocator(page, resolved);
|
|
2978
2743
|
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
2979
2744
|
let checkableRole = false;
|
|
2980
|
-
if (resolved.ref) {
|
|
2745
|
+
if (resolved.ref !== void 0 && resolved.ref !== "") {
|
|
2981
2746
|
const refId = parseRoleRef(resolved.ref);
|
|
2982
|
-
if (refId) {
|
|
2747
|
+
if (refId !== null) {
|
|
2983
2748
|
const state = ensurePageState(page);
|
|
2984
2749
|
const info = state.roleRefs?.[refId];
|
|
2985
2750
|
if (info && CHECKABLE_ROLES.has(info.role)) checkableRole = true;
|
|
@@ -2992,15 +2757,15 @@ async function clickViaPlaywright(opts) {
|
|
|
2992
2757
|
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
2993
2758
|
}
|
|
2994
2759
|
let ariaCheckedBefore;
|
|
2995
|
-
if (checkableRole &&
|
|
2760
|
+
if (checkableRole && opts.doubleClick !== true) {
|
|
2996
2761
|
ariaCheckedBefore = await locator.getAttribute("aria-checked", { timeout }).catch(() => void 0);
|
|
2997
2762
|
}
|
|
2998
|
-
if (opts.doubleClick) {
|
|
2763
|
+
if (opts.doubleClick === true) {
|
|
2999
2764
|
await locator.dblclick({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
3000
2765
|
} else {
|
|
3001
2766
|
await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
3002
2767
|
}
|
|
3003
|
-
if (checkableRole &&
|
|
2768
|
+
if (checkableRole && opts.doubleClick !== true && ariaCheckedBefore !== void 0) {
|
|
3004
2769
|
const POLL_INTERVAL_MS = 50;
|
|
3005
2770
|
const POLL_TIMEOUT_MS = 500;
|
|
3006
2771
|
let changed = false;
|
|
@@ -3013,7 +2778,9 @@ async function clickViaPlaywright(opts) {
|
|
|
3013
2778
|
await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
|
|
3014
2779
|
}
|
|
3015
2780
|
if (!changed) {
|
|
3016
|
-
await locator.evaluate((el) =>
|
|
2781
|
+
await locator.evaluate((el) => {
|
|
2782
|
+
el.click();
|
|
2783
|
+
}).catch(() => {
|
|
3017
2784
|
});
|
|
3018
2785
|
}
|
|
3019
2786
|
}
|
|
@@ -3024,7 +2791,7 @@ async function clickViaPlaywright(opts) {
|
|
|
3024
2791
|
async function hoverViaPlaywright(opts) {
|
|
3025
2792
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
3026
2793
|
const page = await getRestoredPageForTarget(opts);
|
|
3027
|
-
const label = resolved.ref ?? resolved.selector;
|
|
2794
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
3028
2795
|
const locator = resolveLocator(page, resolved);
|
|
3029
2796
|
try {
|
|
3030
2797
|
await locator.hover({ timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
|
|
@@ -3034,28 +2801,28 @@ async function hoverViaPlaywright(opts) {
|
|
|
3034
2801
|
}
|
|
3035
2802
|
async function typeViaPlaywright(opts) {
|
|
3036
2803
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
3037
|
-
const text =
|
|
2804
|
+
const text = opts.text;
|
|
3038
2805
|
const page = await getRestoredPageForTarget(opts);
|
|
3039
|
-
const label = resolved.ref ?? resolved.selector;
|
|
2806
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
3040
2807
|
const locator = resolveLocator(page, resolved);
|
|
3041
2808
|
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
3042
2809
|
try {
|
|
3043
|
-
if (opts.slowly) {
|
|
2810
|
+
if (opts.slowly === true) {
|
|
3044
2811
|
await locator.click({ timeout });
|
|
3045
2812
|
await locator.pressSequentially(text, { timeout, delay: 75 });
|
|
3046
2813
|
} else {
|
|
3047
2814
|
await locator.fill(text, { timeout });
|
|
3048
2815
|
}
|
|
3049
|
-
if (opts.submit) await locator.press("Enter", { timeout });
|
|
2816
|
+
if (opts.submit === true) await locator.press("Enter", { timeout });
|
|
3050
2817
|
} catch (err) {
|
|
3051
2818
|
throw toAIFriendlyError(err, label);
|
|
3052
2819
|
}
|
|
3053
2820
|
}
|
|
3054
2821
|
async function selectOptionViaPlaywright(opts) {
|
|
3055
2822
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
3056
|
-
if (
|
|
2823
|
+
if (opts.values.length === 0) throw new Error("values are required");
|
|
3057
2824
|
const page = await getRestoredPageForTarget(opts);
|
|
3058
|
-
const label = resolved.ref ?? resolved.selector;
|
|
2825
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
3059
2826
|
const locator = resolveLocator(page, resolved);
|
|
3060
2827
|
try {
|
|
3061
2828
|
await locator.selectOption(opts.values, { timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
|
|
@@ -3069,8 +2836,8 @@ async function dragViaPlaywright(opts) {
|
|
|
3069
2836
|
const page = await getRestoredPageForTarget(opts);
|
|
3070
2837
|
const startLocator = resolveLocator(page, resolvedStart);
|
|
3071
2838
|
const endLocator = resolveLocator(page, resolvedEnd);
|
|
3072
|
-
const startLabel = resolvedStart.ref ?? resolvedStart.selector;
|
|
3073
|
-
const endLabel = resolvedEnd.ref ?? resolvedEnd.selector;
|
|
2839
|
+
const startLabel = resolvedStart.ref ?? resolvedStart.selector ?? "";
|
|
2840
|
+
const endLabel = resolvedEnd.ref ?? resolvedEnd.selector ?? "";
|
|
3074
2841
|
try {
|
|
3075
2842
|
await startLocator.dragTo(endLocator, { timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
|
|
3076
2843
|
} catch (err) {
|
|
@@ -3106,7 +2873,7 @@ async function fillFormViaPlaywright(opts) {
|
|
|
3106
2873
|
async function scrollIntoViewViaPlaywright(opts) {
|
|
3107
2874
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
3108
2875
|
const page = await getRestoredPageForTarget(opts);
|
|
3109
|
-
const label = resolved.ref ?? resolved.selector;
|
|
2876
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
3110
2877
|
const locator = resolveLocator(page, resolved);
|
|
3111
2878
|
try {
|
|
3112
2879
|
await locator.scrollIntoViewIfNeeded({ timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
|
|
@@ -3174,7 +2941,7 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
3174
2941
|
const armId = state.armIdUpload;
|
|
3175
2942
|
page.waitForEvent("filechooser", { timeout }).then(async (fileChooser) => {
|
|
3176
2943
|
if (state.armIdUpload !== armId) return;
|
|
3177
|
-
if (
|
|
2944
|
+
if (opts.paths === void 0 || opts.paths.length === 0) {
|
|
3178
2945
|
try {
|
|
3179
2946
|
await page.keyboard.press("Escape");
|
|
3180
2947
|
} catch {
|
|
@@ -3195,7 +2962,7 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
3195
2962
|
await fileChooser.setFiles(uploadPathsResult.paths);
|
|
3196
2963
|
try {
|
|
3197
2964
|
const input = typeof fileChooser.element === "function" ? await Promise.resolve(fileChooser.element()) : null;
|
|
3198
|
-
if (input) {
|
|
2965
|
+
if (input !== null) {
|
|
3199
2966
|
await input.evaluate((el) => {
|
|
3200
2967
|
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
3201
2968
|
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
@@ -3209,7 +2976,7 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
3209
2976
|
|
|
3210
2977
|
// src/actions/keyboard.ts
|
|
3211
2978
|
async function pressKeyViaPlaywright(opts) {
|
|
3212
|
-
const key =
|
|
2979
|
+
const key = opts.key.trim();
|
|
3213
2980
|
if (!key) throw new Error("key is required");
|
|
3214
2981
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3215
2982
|
ensurePageState(page);
|
|
@@ -3222,9 +2989,9 @@ function isRetryableNavigateError(err) {
|
|
|
3222
2989
|
return msg.includes("frame has been detached") || msg.includes("target page, context or browser has been closed");
|
|
3223
2990
|
}
|
|
3224
2991
|
async function navigateViaPlaywright(opts) {
|
|
3225
|
-
const url =
|
|
2992
|
+
const url = opts.url.trim();
|
|
3226
2993
|
if (!url) throw new Error("url is required");
|
|
3227
|
-
const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
2994
|
+
const policy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3228
2995
|
await assertBrowserNavigationAllowed({ url, ...withBrowserNavigationPolicy(policy) });
|
|
3229
2996
|
const timeout = Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 2e4));
|
|
3230
2997
|
let page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
@@ -3243,23 +3010,27 @@ async function navigateViaPlaywright(opts) {
|
|
|
3243
3010
|
ensurePageState(page);
|
|
3244
3011
|
response = await navigate();
|
|
3245
3012
|
}
|
|
3246
|
-
await assertBrowserNavigationRedirectChainAllowed({
|
|
3013
|
+
await assertBrowserNavigationRedirectChainAllowed({
|
|
3014
|
+
request: response?.request(),
|
|
3015
|
+
...withBrowserNavigationPolicy(policy)
|
|
3016
|
+
});
|
|
3247
3017
|
const finalUrl = page.url();
|
|
3248
3018
|
await assertBrowserNavigationResultAllowed({ url: finalUrl, ...withBrowserNavigationPolicy(policy) });
|
|
3249
3019
|
return { url: finalUrl };
|
|
3250
3020
|
}
|
|
3251
3021
|
async function listPagesViaPlaywright(opts) {
|
|
3252
3022
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
3253
|
-
const pages =
|
|
3023
|
+
const pages = getAllPages(browser);
|
|
3254
3024
|
const results = [];
|
|
3255
3025
|
for (const page of pages) {
|
|
3256
3026
|
const tid = await pageTargetId(page).catch(() => null);
|
|
3257
|
-
if (tid)
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3027
|
+
if (tid !== null && tid !== "")
|
|
3028
|
+
results.push({
|
|
3029
|
+
targetId: tid,
|
|
3030
|
+
title: await page.title().catch(() => ""),
|
|
3031
|
+
url: page.url(),
|
|
3032
|
+
type: "page"
|
|
3033
|
+
});
|
|
3263
3034
|
}
|
|
3264
3035
|
return results;
|
|
3265
3036
|
}
|
|
@@ -3270,7 +3041,7 @@ async function createPageViaPlaywright(opts) {
|
|
|
3270
3041
|
const page = await context.newPage();
|
|
3271
3042
|
ensurePageState(page);
|
|
3272
3043
|
const targetUrl = (opts.url ?? "").trim() || "about:blank";
|
|
3273
|
-
const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3044
|
+
const policy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3274
3045
|
if (targetUrl !== "about:blank") {
|
|
3275
3046
|
const navigationPolicy = withBrowserNavigationPolicy(policy);
|
|
3276
3047
|
await assertBrowserNavigationAllowed({ url: targetUrl, ...navigationPolicy });
|
|
@@ -3281,7 +3052,7 @@ async function createPageViaPlaywright(opts) {
|
|
|
3281
3052
|
await assertBrowserNavigationResultAllowed({ url: page.url(), ...navigationPolicy });
|
|
3282
3053
|
}
|
|
3283
3054
|
const tid = await pageTargetId(page).catch(() => null);
|
|
3284
|
-
if (
|
|
3055
|
+
if (tid === null || tid === "") throw new Error("Failed to get targetId for new page");
|
|
3285
3056
|
return {
|
|
3286
3057
|
targetId: tid,
|
|
3287
3058
|
title: await page.title().catch(() => ""),
|
|
@@ -3303,202 +3074,64 @@ async function focusPageByTargetIdViaPlaywright(opts) {
|
|
|
3303
3074
|
await page.bringToFront();
|
|
3304
3075
|
} catch (err) {
|
|
3305
3076
|
try {
|
|
3306
|
-
await withPageScopedCdpClient({
|
|
3307
|
-
cdpUrl: opts.cdpUrl,
|
|
3308
|
-
page,
|
|
3309
|
-
targetId: opts.targetId,
|
|
3310
|
-
fn: async (send) => {
|
|
3311
|
-
await send("Page.bringToFront");
|
|
3312
|
-
}
|
|
3313
|
-
});
|
|
3314
|
-
return;
|
|
3315
|
-
} catch {
|
|
3316
|
-
throw err;
|
|
3317
|
-
}
|
|
3318
|
-
}
|
|
3319
|
-
}
|
|
3320
|
-
async function resizeViewportViaPlaywright(opts) {
|
|
3321
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3322
|
-
ensurePageState(page);
|
|
3323
|
-
await page.setViewportSize({
|
|
3324
|
-
width: Math.max(1, Math.floor(opts.width)),
|
|
3325
|
-
height: Math.max(1, Math.floor(opts.height))
|
|
3326
|
-
});
|
|
3327
|
-
}
|
|
3328
|
-
|
|
3329
|
-
// src/actions/wait.ts
|
|
3330
|
-
var MAX_WAIT_TIME_MS = 3e4;
|
|
3331
|
-
function resolveBoundedDelayMs2(value, label, maxMs) {
|
|
3332
|
-
const normalized = Math.floor(value ?? 0);
|
|
3333
|
-
if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
|
|
3334
|
-
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${maxMs}ms`);
|
|
3335
|
-
return normalized;
|
|
3336
|
-
}
|
|
3337
|
-
async function waitForViaPlaywright(opts) {
|
|
3338
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3339
|
-
ensurePageState(page);
|
|
3340
|
-
const timeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
|
|
3341
|
-
if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
|
|
3342
|
-
await page.waitForTimeout(resolveBoundedDelayMs2(opts.timeMs, "wait timeMs", MAX_WAIT_TIME_MS));
|
|
3343
|
-
}
|
|
3344
|
-
if (opts.text) {
|
|
3345
|
-
await page.getByText(opts.text).first().waitFor({ state: "visible", timeout });
|
|
3346
|
-
}
|
|
3347
|
-
if (opts.textGone) {
|
|
3348
|
-
await page.getByText(opts.textGone).first().waitFor({ state: "hidden", timeout });
|
|
3349
|
-
}
|
|
3350
|
-
if (opts.selector) {
|
|
3351
|
-
const selector = String(opts.selector).trim();
|
|
3352
|
-
if (selector) await page.locator(selector).first().waitFor({ state: "visible", timeout });
|
|
3353
|
-
}
|
|
3354
|
-
if (opts.url) {
|
|
3355
|
-
const url = String(opts.url).trim();
|
|
3356
|
-
if (url) await page.waitForURL(url, { timeout });
|
|
3357
|
-
}
|
|
3358
|
-
if (opts.loadState) {
|
|
3359
|
-
await page.waitForLoadState(opts.loadState, { timeout });
|
|
3360
|
-
}
|
|
3361
|
-
if (opts.fn) {
|
|
3362
|
-
const fn = String(opts.fn).trim();
|
|
3363
|
-
if (fn) await page.waitForFunction(fn, void 0, { timeout });
|
|
3364
|
-
}
|
|
3365
|
-
}
|
|
3366
|
-
|
|
3367
|
-
// src/actions/evaluate.ts
|
|
3368
|
-
async function evaluateInAllFramesViaPlaywright(opts) {
|
|
3369
|
-
const fnText = String(opts.fn ?? "").trim();
|
|
3370
|
-
if (!fnText) throw new Error("function is required");
|
|
3371
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3372
|
-
const frames = page.frames();
|
|
3373
|
-
const results = [];
|
|
3374
|
-
for (const frame of frames) {
|
|
3375
|
-
try {
|
|
3376
|
-
const result = await frame.evaluate(
|
|
3377
|
-
// eslint-disable-next-line no-eval
|
|
3378
|
-
(fnBody) => {
|
|
3379
|
-
"use strict";
|
|
3380
|
-
try {
|
|
3381
|
-
const candidate = (0, eval)("(" + fnBody + ")");
|
|
3382
|
-
return typeof candidate === "function" ? candidate() : candidate;
|
|
3383
|
-
} catch (err) {
|
|
3384
|
-
throw new Error("Invalid evaluate function: " + (err instanceof Error ? err.message : String(err)));
|
|
3385
|
-
}
|
|
3386
|
-
},
|
|
3387
|
-
fnText
|
|
3388
|
-
);
|
|
3389
|
-
results.push({
|
|
3390
|
-
frameUrl: frame.url(),
|
|
3391
|
-
frameName: frame.name(),
|
|
3392
|
-
result
|
|
3393
|
-
});
|
|
3394
|
-
} catch {
|
|
3395
|
-
}
|
|
3396
|
-
}
|
|
3397
|
-
return results;
|
|
3398
|
-
}
|
|
3399
|
-
async function awaitEvalWithAbort(evalPromise, abortPromise) {
|
|
3400
|
-
if (!abortPromise) return await evalPromise;
|
|
3401
|
-
try {
|
|
3402
|
-
return await Promise.race([evalPromise, abortPromise]);
|
|
3403
|
-
} catch (err) {
|
|
3404
|
-
evalPromise.catch(() => {
|
|
3405
|
-
});
|
|
3406
|
-
throw err;
|
|
3407
|
-
}
|
|
3408
|
-
}
|
|
3409
|
-
var BROWSER_EVALUATOR = new Function("args", `
|
|
3410
|
-
"use strict";
|
|
3411
|
-
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
3412
|
-
try {
|
|
3413
|
-
var candidate = eval("(" + fnBody + ")");
|
|
3414
|
-
var result = typeof candidate === "function" ? candidate() : candidate;
|
|
3415
|
-
if (result && typeof result.then === "function") {
|
|
3416
|
-
return Promise.race([
|
|
3417
|
-
result,
|
|
3418
|
-
new Promise(function(_, reject) {
|
|
3419
|
-
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
3420
|
-
})
|
|
3421
|
-
]);
|
|
3422
|
-
}
|
|
3423
|
-
return result;
|
|
3424
|
-
} catch (err) {
|
|
3425
|
-
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
3426
|
-
}
|
|
3427
|
-
`);
|
|
3428
|
-
var ELEMENT_EVALUATOR = new Function("el", "args", `
|
|
3429
|
-
"use strict";
|
|
3430
|
-
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
3431
|
-
try {
|
|
3432
|
-
var candidate = eval("(" + fnBody + ")");
|
|
3433
|
-
var result = typeof candidate === "function" ? candidate(el) : candidate;
|
|
3434
|
-
if (result && typeof result.then === "function") {
|
|
3435
|
-
return Promise.race([
|
|
3436
|
-
result,
|
|
3437
|
-
new Promise(function(_, reject) {
|
|
3438
|
-
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
3439
|
-
})
|
|
3440
|
-
]);
|
|
3441
|
-
}
|
|
3442
|
-
return result;
|
|
3443
|
-
} catch (err) {
|
|
3444
|
-
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
3445
|
-
}
|
|
3446
|
-
`);
|
|
3447
|
-
async function evaluateViaPlaywright(opts) {
|
|
3448
|
-
const fnText = String(opts.fn ?? "").trim();
|
|
3449
|
-
if (!fnText) throw new Error("function is required");
|
|
3450
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3451
|
-
ensurePageState(page);
|
|
3452
|
-
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
3453
|
-
const outerTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
|
|
3454
|
-
let evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 500));
|
|
3455
|
-
evaluateTimeout = Math.min(evaluateTimeout, outerTimeout);
|
|
3456
|
-
const signal = opts.signal;
|
|
3457
|
-
let abortListener;
|
|
3458
|
-
let abortReject;
|
|
3459
|
-
let abortPromise;
|
|
3460
|
-
if (signal) {
|
|
3461
|
-
abortPromise = new Promise((_, reject) => {
|
|
3462
|
-
abortReject = reject;
|
|
3463
|
-
});
|
|
3464
|
-
abortPromise.catch(() => {
|
|
3465
|
-
});
|
|
3466
|
-
}
|
|
3467
|
-
if (signal) {
|
|
3468
|
-
const disconnect = () => {
|
|
3469
|
-
forceDisconnectPlaywrightForTarget({
|
|
3077
|
+
await withPageScopedCdpClient({
|
|
3470
3078
|
cdpUrl: opts.cdpUrl,
|
|
3471
|
-
|
|
3079
|
+
page,
|
|
3080
|
+
targetId: opts.targetId,
|
|
3081
|
+
fn: async (send) => {
|
|
3082
|
+
await send("Page.bringToFront");
|
|
3083
|
+
}
|
|
3472
3084
|
});
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
throw signal.reason ?? new Error("aborted");
|
|
3477
|
-
}
|
|
3478
|
-
abortListener = () => {
|
|
3479
|
-
disconnect();
|
|
3480
|
-
abortReject?.(signal.reason ?? new Error("aborted"));
|
|
3481
|
-
};
|
|
3482
|
-
signal.addEventListener("abort", abortListener, { once: true });
|
|
3483
|
-
if (signal.aborted) {
|
|
3484
|
-
abortListener();
|
|
3485
|
-
throw signal.reason ?? new Error("aborted");
|
|
3085
|
+
return;
|
|
3086
|
+
} catch {
|
|
3087
|
+
throw err;
|
|
3486
3088
|
}
|
|
3487
3089
|
}
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3090
|
+
}
|
|
3091
|
+
async function resizeViewportViaPlaywright(opts) {
|
|
3092
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3093
|
+
ensurePageState(page);
|
|
3094
|
+
await page.setViewportSize({
|
|
3095
|
+
width: Math.max(1, Math.floor(opts.width)),
|
|
3096
|
+
height: Math.max(1, Math.floor(opts.height))
|
|
3097
|
+
});
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
// src/actions/wait.ts
|
|
3101
|
+
var MAX_WAIT_TIME_MS = 3e4;
|
|
3102
|
+
function resolveBoundedDelayMs2(value, label, maxMs) {
|
|
3103
|
+
const normalized = Math.floor(value ?? 0);
|
|
3104
|
+
if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
|
|
3105
|
+
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${String(maxMs)}ms`);
|
|
3106
|
+
return normalized;
|
|
3107
|
+
}
|
|
3108
|
+
async function waitForViaPlaywright(opts) {
|
|
3109
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3110
|
+
ensurePageState(page);
|
|
3111
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
|
|
3112
|
+
if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
|
|
3113
|
+
await page.waitForTimeout(resolveBoundedDelayMs2(opts.timeMs, "wait timeMs", MAX_WAIT_TIME_MS));
|
|
3114
|
+
}
|
|
3115
|
+
if (opts.text !== void 0 && opts.text !== "") {
|
|
3116
|
+
await page.getByText(opts.text).first().waitFor({ state: "visible", timeout });
|
|
3117
|
+
}
|
|
3118
|
+
if (opts.textGone !== void 0 && opts.textGone !== "") {
|
|
3119
|
+
await page.getByText(opts.textGone).first().waitFor({ state: "hidden", timeout });
|
|
3120
|
+
}
|
|
3121
|
+
if (opts.selector !== void 0 && opts.selector !== "") {
|
|
3122
|
+
const selector = opts.selector.trim();
|
|
3123
|
+
if (selector !== "") await page.locator(selector).first().waitFor({ state: "visible", timeout });
|
|
3124
|
+
}
|
|
3125
|
+
if (opts.url !== void 0 && opts.url !== "") {
|
|
3126
|
+
const url = opts.url.trim();
|
|
3127
|
+
if (url !== "") await page.waitForURL(url, { timeout });
|
|
3128
|
+
}
|
|
3129
|
+
if (opts.loadState !== void 0) {
|
|
3130
|
+
await page.waitForLoadState(opts.loadState, { timeout });
|
|
3131
|
+
}
|
|
3132
|
+
if (opts.fn !== void 0 && opts.fn !== "") {
|
|
3133
|
+
const fn = opts.fn.trim();
|
|
3134
|
+
if (fn !== "") await page.waitForFunction(fn, void 0, { timeout });
|
|
3502
3135
|
}
|
|
3503
3136
|
}
|
|
3504
3137
|
|
|
@@ -3506,7 +3139,7 @@ async function evaluateViaPlaywright(opts) {
|
|
|
3506
3139
|
var MAX_BATCH_DEPTH = 5;
|
|
3507
3140
|
var MAX_BATCH_ACTIONS = 100;
|
|
3508
3141
|
async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, depth = 0) {
|
|
3509
|
-
if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${MAX_BATCH_DEPTH}`);
|
|
3142
|
+
if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${String(MAX_BATCH_DEPTH)}`);
|
|
3510
3143
|
const effectiveTargetId = action.targetId ?? targetId;
|
|
3511
3144
|
switch (action.kind) {
|
|
3512
3145
|
case "click":
|
|
@@ -3598,7 +3231,8 @@ async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, de
|
|
|
3598
3231
|
});
|
|
3599
3232
|
break;
|
|
3600
3233
|
case "wait":
|
|
3601
|
-
if (action.fn
|
|
3234
|
+
if (action.fn !== void 0 && action.fn !== "" && !evaluateEnabled)
|
|
3235
|
+
throw new Error("wait --fn is disabled by config (browser.evaluateEnabled=false)");
|
|
3602
3236
|
await waitForViaPlaywright({
|
|
3603
3237
|
cdpUrl,
|
|
3604
3238
|
targetId: effectiveTargetId,
|
|
@@ -3639,13 +3273,14 @@ async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, de
|
|
|
3639
3273
|
});
|
|
3640
3274
|
break;
|
|
3641
3275
|
default:
|
|
3642
|
-
throw new Error(`Unsupported batch action kind: ${action.kind}`);
|
|
3276
|
+
throw new Error(`Unsupported batch action kind: ${String(action.kind)}`);
|
|
3643
3277
|
}
|
|
3644
3278
|
}
|
|
3645
3279
|
async function batchViaPlaywright(opts) {
|
|
3646
3280
|
const depth = opts.depth ?? 0;
|
|
3647
|
-
if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${MAX_BATCH_DEPTH}`);
|
|
3648
|
-
if (opts.actions.length > MAX_BATCH_ACTIONS)
|
|
3281
|
+
if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${String(MAX_BATCH_DEPTH)}`);
|
|
3282
|
+
if (opts.actions.length > MAX_BATCH_ACTIONS)
|
|
3283
|
+
throw new Error(`Batch exceeds maximum of ${String(MAX_BATCH_ACTIONS)} actions`);
|
|
3649
3284
|
const results = [];
|
|
3650
3285
|
const evaluateEnabled = opts.evaluateEnabled !== false;
|
|
3651
3286
|
for (const action of opts.actions) {
|
|
@@ -3719,392 +3354,830 @@ async function awaitDownloadPayload(params) {
|
|
|
3719
3354
|
throw err;
|
|
3720
3355
|
}
|
|
3721
3356
|
}
|
|
3722
|
-
async function downloadViaPlaywright(opts) {
|
|
3723
|
-
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
3357
|
+
async function downloadViaPlaywright(opts) {
|
|
3358
|
+
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
3359
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3360
|
+
const state = ensurePageState(page);
|
|
3361
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
3362
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
3363
|
+
const outPath = opts.path.trim();
|
|
3364
|
+
if (!outPath) throw new Error("path is required");
|
|
3365
|
+
state.armIdDownload = bumpDownloadArmId();
|
|
3366
|
+
const armId = state.armIdDownload;
|
|
3367
|
+
const waiter = createPageDownloadWaiter(page, timeout);
|
|
3368
|
+
try {
|
|
3369
|
+
const locator = refLocator(page, opts.ref);
|
|
3370
|
+
try {
|
|
3371
|
+
await locator.click({ timeout });
|
|
3372
|
+
} catch (err) {
|
|
3373
|
+
throw toAIFriendlyError(err, opts.ref);
|
|
3374
|
+
}
|
|
3375
|
+
return await awaitDownloadPayload({ waiter, state, armId, outPath });
|
|
3376
|
+
} catch (err) {
|
|
3377
|
+
waiter.cancel();
|
|
3378
|
+
throw err;
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
async function waitForDownloadViaPlaywright(opts) {
|
|
3382
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3383
|
+
const state = ensurePageState(page);
|
|
3384
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
3385
|
+
state.armIdDownload = bumpDownloadArmId();
|
|
3386
|
+
const armId = state.armIdDownload;
|
|
3387
|
+
const waiter = createPageDownloadWaiter(page, timeout);
|
|
3388
|
+
try {
|
|
3389
|
+
const download = await waiter.promise;
|
|
3390
|
+
if (state.armIdDownload !== armId) throw new Error("Download was superseded by another waiter");
|
|
3391
|
+
const savePath = opts.path ?? download.suggestedFilename();
|
|
3392
|
+
await assertSafeOutputPath(savePath, opts.allowedOutputRoots);
|
|
3393
|
+
return await saveDownloadPayload(download, savePath);
|
|
3394
|
+
} catch (err) {
|
|
3395
|
+
waiter.cancel();
|
|
3396
|
+
throw err;
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
async function emulateMediaViaPlaywright(opts) {
|
|
3400
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3401
|
+
ensurePageState(page);
|
|
3402
|
+
await page.emulateMedia({ colorScheme: opts.colorScheme });
|
|
3403
|
+
}
|
|
3404
|
+
async function setDeviceViaPlaywright(opts) {
|
|
3405
|
+
const name = opts.name.trim();
|
|
3406
|
+
if (!name) throw new Error("device name is required");
|
|
3407
|
+
const device = devices[name];
|
|
3408
|
+
if (device === void 0) {
|
|
3409
|
+
throw new Error(`Unknown device "${name}".`);
|
|
3410
|
+
}
|
|
3411
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3412
|
+
ensurePageState(page);
|
|
3413
|
+
if (device.viewport !== null) {
|
|
3414
|
+
await page.setViewportSize({
|
|
3415
|
+
width: device.viewport.width,
|
|
3416
|
+
height: device.viewport.height
|
|
3417
|
+
});
|
|
3418
|
+
}
|
|
3419
|
+
await withPageScopedCdpClient({
|
|
3420
|
+
cdpUrl: opts.cdpUrl,
|
|
3421
|
+
page,
|
|
3422
|
+
targetId: opts.targetId,
|
|
3423
|
+
fn: async (send) => {
|
|
3424
|
+
const locale = device.locale;
|
|
3425
|
+
if (device.userAgent !== "" || locale !== void 0 && locale !== "") {
|
|
3426
|
+
await send("Emulation.setUserAgentOverride", {
|
|
3427
|
+
userAgent: device.userAgent,
|
|
3428
|
+
acceptLanguage: locale
|
|
3429
|
+
});
|
|
3430
|
+
}
|
|
3431
|
+
if (device.viewport !== null) {
|
|
3432
|
+
await send("Emulation.setDeviceMetricsOverride", {
|
|
3433
|
+
mobile: device.isMobile,
|
|
3434
|
+
width: device.viewport.width,
|
|
3435
|
+
height: device.viewport.height,
|
|
3436
|
+
deviceScaleFactor: device.deviceScaleFactor,
|
|
3437
|
+
screenWidth: device.viewport.width,
|
|
3438
|
+
screenHeight: device.viewport.height
|
|
3439
|
+
});
|
|
3440
|
+
}
|
|
3441
|
+
if (device.hasTouch) {
|
|
3442
|
+
await send("Emulation.setTouchEmulationEnabled", { enabled: true });
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
});
|
|
3446
|
+
}
|
|
3447
|
+
async function setExtraHTTPHeadersViaPlaywright(opts) {
|
|
3448
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3449
|
+
ensurePageState(page);
|
|
3450
|
+
await page.context().setExtraHTTPHeaders(opts.headers);
|
|
3451
|
+
}
|
|
3452
|
+
async function setGeolocationViaPlaywright(opts) {
|
|
3453
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3454
|
+
ensurePageState(page);
|
|
3455
|
+
const context = page.context();
|
|
3456
|
+
if (opts.clear === true) {
|
|
3457
|
+
await context.setGeolocation(null);
|
|
3458
|
+
await context.clearPermissions().catch(() => {
|
|
3459
|
+
});
|
|
3460
|
+
return;
|
|
3461
|
+
}
|
|
3462
|
+
if (typeof opts.latitude !== "number" || typeof opts.longitude !== "number") {
|
|
3463
|
+
throw new Error("latitude and longitude are required (or set clear=true)");
|
|
3464
|
+
}
|
|
3465
|
+
await context.setGeolocation({
|
|
3466
|
+
latitude: opts.latitude,
|
|
3467
|
+
longitude: opts.longitude,
|
|
3468
|
+
accuracy: typeof opts.accuracy === "number" ? opts.accuracy : void 0
|
|
3469
|
+
});
|
|
3470
|
+
const origin = (opts.origin !== void 0 && opts.origin !== "" ? opts.origin.trim() : "") || (() => {
|
|
3471
|
+
try {
|
|
3472
|
+
return new URL(page.url()).origin;
|
|
3473
|
+
} catch {
|
|
3474
|
+
return "";
|
|
3475
|
+
}
|
|
3476
|
+
})();
|
|
3477
|
+
if (origin !== "")
|
|
3478
|
+
await context.grantPermissions(["geolocation"], { origin }).catch(() => {
|
|
3479
|
+
});
|
|
3480
|
+
}
|
|
3481
|
+
async function setHttpCredentialsViaPlaywright(opts) {
|
|
3482
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3483
|
+
ensurePageState(page);
|
|
3484
|
+
if (opts.clear === true) {
|
|
3485
|
+
await page.context().setHTTPCredentials(null);
|
|
3486
|
+
return;
|
|
3487
|
+
}
|
|
3488
|
+
const username = opts.username ?? "";
|
|
3489
|
+
const password = opts.password ?? "";
|
|
3490
|
+
if (!username) throw new Error("username is required (or set clear=true)");
|
|
3491
|
+
await page.context().setHTTPCredentials({ username, password });
|
|
3492
|
+
}
|
|
3493
|
+
async function setLocaleViaPlaywright(opts) {
|
|
3494
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3495
|
+
ensurePageState(page);
|
|
3496
|
+
const locale = opts.locale.trim();
|
|
3497
|
+
if (!locale) throw new Error("locale is required");
|
|
3498
|
+
await withPageScopedCdpClient({
|
|
3499
|
+
cdpUrl: opts.cdpUrl,
|
|
3500
|
+
page,
|
|
3501
|
+
targetId: opts.targetId,
|
|
3502
|
+
fn: async (send) => {
|
|
3503
|
+
try {
|
|
3504
|
+
await send("Emulation.setLocaleOverride", { locale });
|
|
3505
|
+
} catch (err) {
|
|
3506
|
+
if (String(err).includes("Another locale override is already in effect")) return;
|
|
3507
|
+
throw err;
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
});
|
|
3511
|
+
}
|
|
3512
|
+
async function setOfflineViaPlaywright(opts) {
|
|
3513
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3514
|
+
ensurePageState(page);
|
|
3515
|
+
await page.context().setOffline(opts.offline);
|
|
3516
|
+
}
|
|
3517
|
+
async function setTimezoneViaPlaywright(opts) {
|
|
3518
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3519
|
+
ensurePageState(page);
|
|
3520
|
+
const timezoneId = opts.timezoneId.trim();
|
|
3521
|
+
if (!timezoneId) throw new Error("timezoneId is required");
|
|
3522
|
+
await withPageScopedCdpClient({
|
|
3523
|
+
cdpUrl: opts.cdpUrl,
|
|
3524
|
+
page,
|
|
3525
|
+
targetId: opts.targetId,
|
|
3526
|
+
fn: async (send) => {
|
|
3527
|
+
try {
|
|
3528
|
+
await send("Emulation.setTimezoneOverride", { timezoneId });
|
|
3529
|
+
} catch (err) {
|
|
3530
|
+
const msg = String(err);
|
|
3531
|
+
if (msg.includes("Timezone override is already in effect")) return;
|
|
3532
|
+
if (msg.includes("Invalid timezone")) throw new Error(`Invalid timezone ID: ${timezoneId}`, { cause: err });
|
|
3533
|
+
throw err;
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
});
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
// src/capture/activity.ts
|
|
3540
|
+
function consolePriority(level) {
|
|
3541
|
+
switch (level) {
|
|
3542
|
+
case "error":
|
|
3543
|
+
return 3;
|
|
3544
|
+
case "warning":
|
|
3545
|
+
case "warn":
|
|
3546
|
+
return 2;
|
|
3547
|
+
case "info":
|
|
3548
|
+
case "log":
|
|
3549
|
+
return 1;
|
|
3550
|
+
case "debug":
|
|
3551
|
+
return 0;
|
|
3552
|
+
default:
|
|
3553
|
+
return 1;
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
async function getConsoleMessagesViaPlaywright(opts) {
|
|
3557
|
+
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
3558
|
+
const messages = opts.level !== void 0 && opts.level !== "" ? state.console.filter((msg) => consolePriority(msg.type) >= consolePriority(opts.level ?? "")) : [...state.console];
|
|
3559
|
+
if (opts.clear === true) state.console = [];
|
|
3560
|
+
return messages;
|
|
3561
|
+
}
|
|
3562
|
+
async function getPageErrorsViaPlaywright(opts) {
|
|
3563
|
+
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
3564
|
+
const errors = [...state.errors];
|
|
3565
|
+
if (opts.clear === true) state.errors = [];
|
|
3566
|
+
return { errors };
|
|
3567
|
+
}
|
|
3568
|
+
async function getNetworkRequestsViaPlaywright(opts) {
|
|
3569
|
+
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
3570
|
+
const raw = [...state.requests];
|
|
3571
|
+
const filter = typeof opts.filter === "string" ? opts.filter.trim() : "";
|
|
3572
|
+
const requests = filter ? raw.filter((r) => r.url.includes(filter)) : raw;
|
|
3573
|
+
if (opts.clear === true) {
|
|
3574
|
+
state.requests = [];
|
|
3575
|
+
state.requestIds = /* @__PURE__ */ new WeakMap();
|
|
3576
|
+
}
|
|
3577
|
+
return { requests };
|
|
3578
|
+
}
|
|
3579
|
+
|
|
3580
|
+
// src/capture/pdf.ts
|
|
3581
|
+
async function pdfViaPlaywright(opts) {
|
|
3724
3582
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
const
|
|
3583
|
+
ensurePageState(page);
|
|
3584
|
+
return { buffer: await page.pdf({ printBackground: true }) };
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3587
|
+
// src/capture/response.ts
|
|
3588
|
+
function matchUrlPattern(pattern, url) {
|
|
3589
|
+
if (!pattern || !url) return false;
|
|
3590
|
+
if (pattern === url) return true;
|
|
3591
|
+
if (pattern.includes("*")) {
|
|
3592
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
3735
3593
|
try {
|
|
3736
|
-
|
|
3737
|
-
} catch
|
|
3738
|
-
|
|
3594
|
+
return new RegExp(`^${escaped}$`).test(url);
|
|
3595
|
+
} catch {
|
|
3596
|
+
return false;
|
|
3739
3597
|
}
|
|
3740
|
-
return await awaitDownloadPayload({ waiter, state, armId, outPath });
|
|
3741
|
-
} catch (err) {
|
|
3742
|
-
waiter.cancel();
|
|
3743
|
-
throw err;
|
|
3744
3598
|
}
|
|
3599
|
+
return url.includes(pattern);
|
|
3745
3600
|
}
|
|
3746
|
-
async function
|
|
3601
|
+
async function responseBodyViaPlaywright(opts) {
|
|
3747
3602
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3748
|
-
|
|
3749
|
-
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
const
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
}
|
|
3760
|
-
|
|
3761
|
-
|
|
3603
|
+
ensurePageState(page);
|
|
3604
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
|
|
3605
|
+
const pattern = opts.url.trim();
|
|
3606
|
+
if (!pattern) throw new Error("url is required");
|
|
3607
|
+
const response = await page.waitForResponse((resp) => matchUrlPattern(pattern, resp.url()), { timeout });
|
|
3608
|
+
let body = await response.text();
|
|
3609
|
+
let truncated = false;
|
|
3610
|
+
const maxChars = typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars) ? Math.max(1, Math.min(5e6, Math.floor(opts.maxChars))) : 2e5;
|
|
3611
|
+
if (body.length > maxChars) {
|
|
3612
|
+
body = body.slice(0, maxChars);
|
|
3613
|
+
truncated = true;
|
|
3614
|
+
}
|
|
3615
|
+
const headers = {};
|
|
3616
|
+
const allHeaders = response.headers();
|
|
3617
|
+
for (const [key, value] of Object.entries(allHeaders)) {
|
|
3618
|
+
headers[key] = value;
|
|
3762
3619
|
}
|
|
3620
|
+
return {
|
|
3621
|
+
url: response.url(),
|
|
3622
|
+
status: response.status(),
|
|
3623
|
+
headers,
|
|
3624
|
+
body,
|
|
3625
|
+
truncated
|
|
3626
|
+
};
|
|
3763
3627
|
}
|
|
3764
|
-
|
|
3628
|
+
|
|
3629
|
+
// src/capture/screenshot.ts
|
|
3630
|
+
async function takeScreenshotViaPlaywright(opts) {
|
|
3765
3631
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3766
3632
|
ensurePageState(page);
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
if (
|
|
3774
|
-
throw new Error(
|
|
3633
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
3634
|
+
const type = opts.type ?? "png";
|
|
3635
|
+
if (opts.ref !== void 0 && opts.ref !== "") {
|
|
3636
|
+
if (opts.fullPage === true) throw new Error("fullPage is not supported for element screenshots");
|
|
3637
|
+
return { buffer: await refLocator(page, opts.ref).screenshot({ type }) };
|
|
3638
|
+
}
|
|
3639
|
+
if (opts.element !== void 0 && opts.element !== "") {
|
|
3640
|
+
if (opts.fullPage === true) throw new Error("fullPage is not supported for element screenshots");
|
|
3641
|
+
return { buffer: await page.locator(opts.element).first().screenshot({ type }) };
|
|
3775
3642
|
}
|
|
3643
|
+
return { buffer: await page.screenshot({ type, fullPage: Boolean(opts.fullPage) }) };
|
|
3644
|
+
}
|
|
3645
|
+
async function screenshotWithLabelsViaPlaywright(opts) {
|
|
3776
3646
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3777
3647
|
ensurePageState(page);
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
if (device.viewport) {
|
|
3797
|
-
await send("Emulation.setDeviceMetricsOverride", {
|
|
3798
|
-
mobile: Boolean(device.isMobile),
|
|
3799
|
-
width: device.viewport.width,
|
|
3800
|
-
height: device.viewport.height,
|
|
3801
|
-
deviceScaleFactor: device.deviceScaleFactor ?? 1,
|
|
3802
|
-
screenWidth: device.viewport.width,
|
|
3803
|
-
screenHeight: device.viewport.height
|
|
3804
|
-
});
|
|
3648
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
3649
|
+
const maxLabels = typeof opts.maxLabels === "number" && Number.isFinite(opts.maxLabels) ? Math.max(1, Math.floor(opts.maxLabels)) : 150;
|
|
3650
|
+
const type = opts.type ?? "png";
|
|
3651
|
+
const refs = opts.refs.slice(0, maxLabels);
|
|
3652
|
+
const skipped = opts.refs.slice(maxLabels);
|
|
3653
|
+
const viewport = await page.evaluate(() => ({
|
|
3654
|
+
width: window.innerWidth || 0,
|
|
3655
|
+
height: window.innerHeight || 0
|
|
3656
|
+
}));
|
|
3657
|
+
const labels = [];
|
|
3658
|
+
for (let i = 0; i < refs.length; i++) {
|
|
3659
|
+
const ref = refs[i];
|
|
3660
|
+
try {
|
|
3661
|
+
const locator = refLocator(page, ref);
|
|
3662
|
+
const box = await locator.boundingBox({ timeout: 2e3 });
|
|
3663
|
+
if (!box) {
|
|
3664
|
+
skipped.push(ref);
|
|
3665
|
+
continue;
|
|
3805
3666
|
}
|
|
3806
|
-
|
|
3807
|
-
|
|
3667
|
+
const x1 = box.x + box.width;
|
|
3668
|
+
const y1 = box.y + box.height;
|
|
3669
|
+
if (x1 < 0 || box.x > viewport.width || y1 < 0 || box.y > viewport.height) {
|
|
3670
|
+
skipped.push(ref);
|
|
3671
|
+
continue;
|
|
3808
3672
|
}
|
|
3673
|
+
labels.push({ ref, index: i + 1, box });
|
|
3674
|
+
} catch {
|
|
3675
|
+
skipped.push(ref);
|
|
3809
3676
|
}
|
|
3810
|
-
}
|
|
3677
|
+
}
|
|
3678
|
+
try {
|
|
3679
|
+
if (labels.length > 0) {
|
|
3680
|
+
await page.evaluate(
|
|
3681
|
+
(labelData) => {
|
|
3682
|
+
document.querySelectorAll("[data-browserclaw-labels]").forEach((el) => {
|
|
3683
|
+
el.remove();
|
|
3684
|
+
});
|
|
3685
|
+
const container = document.createElement("div");
|
|
3686
|
+
container.setAttribute("data-browserclaw-labels", "1");
|
|
3687
|
+
container.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:2147483647;";
|
|
3688
|
+
for (const { index, box } of labelData) {
|
|
3689
|
+
const border = document.createElement("div");
|
|
3690
|
+
border.style.cssText = `position:absolute;left:${String(box.x)}px;top:${String(box.y)}px;width:${String(box.width)}px;height:${String(box.height)}px;border:2px solid #FF4500;box-sizing:border-box;`;
|
|
3691
|
+
container.appendChild(border);
|
|
3692
|
+
const badge = document.createElement("div");
|
|
3693
|
+
badge.textContent = String(index);
|
|
3694
|
+
badge.style.cssText = `position:absolute;left:${String(box.x)}px;top:${String(Math.max(0, box.y - 18))}px;background:#FF4500;color:#fff;font:bold 12px/16px monospace;padding:0 4px;border-radius:2px;`;
|
|
3695
|
+
container.appendChild(badge);
|
|
3696
|
+
}
|
|
3697
|
+
document.documentElement.appendChild(container);
|
|
3698
|
+
},
|
|
3699
|
+
labels.map((l) => ({ index: l.index, box: l.box }))
|
|
3700
|
+
);
|
|
3701
|
+
}
|
|
3702
|
+
return {
|
|
3703
|
+
buffer: await page.screenshot({ type }),
|
|
3704
|
+
labels,
|
|
3705
|
+
skipped
|
|
3706
|
+
};
|
|
3707
|
+
} finally {
|
|
3708
|
+
await page.evaluate(() => {
|
|
3709
|
+
document.querySelectorAll("[data-browserclaw-labels]").forEach((el) => {
|
|
3710
|
+
el.remove();
|
|
3711
|
+
});
|
|
3712
|
+
}).catch(() => {
|
|
3713
|
+
});
|
|
3714
|
+
}
|
|
3811
3715
|
}
|
|
3812
|
-
async function
|
|
3716
|
+
async function traceStartViaPlaywright(opts) {
|
|
3813
3717
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3814
3718
|
ensurePageState(page);
|
|
3815
|
-
|
|
3719
|
+
const context = page.context();
|
|
3720
|
+
const ctxState = ensureContextState(context);
|
|
3721
|
+
if (ctxState.traceActive) {
|
|
3722
|
+
throw new Error("Trace already running. Stop the current trace before starting a new one.");
|
|
3723
|
+
}
|
|
3724
|
+
await context.tracing.start({
|
|
3725
|
+
screenshots: opts.screenshots ?? true,
|
|
3726
|
+
snapshots: opts.snapshots ?? true,
|
|
3727
|
+
sources: opts.sources ?? false
|
|
3728
|
+
});
|
|
3729
|
+
ctxState.traceActive = true;
|
|
3816
3730
|
}
|
|
3817
|
-
async function
|
|
3731
|
+
async function traceStopViaPlaywright(opts) {
|
|
3732
|
+
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
3818
3733
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3819
3734
|
ensurePageState(page);
|
|
3820
3735
|
const context = page.context();
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3736
|
+
const ctxState = ensureContextState(context);
|
|
3737
|
+
if (!ctxState.traceActive) {
|
|
3738
|
+
throw new Error("No active trace. Start a trace before stopping it.");
|
|
3739
|
+
}
|
|
3740
|
+
await writeViaSiblingTempPath({
|
|
3741
|
+
rootDir: dirname(opts.path),
|
|
3742
|
+
targetPath: opts.path,
|
|
3743
|
+
writeTemp: async (tempPath) => {
|
|
3744
|
+
await context.tracing.stop({ path: tempPath });
|
|
3745
|
+
}
|
|
3746
|
+
});
|
|
3747
|
+
ctxState.traceActive = false;
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3750
|
+
// src/snapshot/ref-map.ts
|
|
3751
|
+
var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
3752
|
+
"button",
|
|
3753
|
+
"link",
|
|
3754
|
+
"textbox",
|
|
3755
|
+
"checkbox",
|
|
3756
|
+
"radio",
|
|
3757
|
+
"combobox",
|
|
3758
|
+
"listbox",
|
|
3759
|
+
"menuitem",
|
|
3760
|
+
"menuitemcheckbox",
|
|
3761
|
+
"menuitemradio",
|
|
3762
|
+
"option",
|
|
3763
|
+
"searchbox",
|
|
3764
|
+
"slider",
|
|
3765
|
+
"spinbutton",
|
|
3766
|
+
"switch",
|
|
3767
|
+
"tab",
|
|
3768
|
+
"treeitem"
|
|
3769
|
+
]);
|
|
3770
|
+
var CONTENT_ROLES = /* @__PURE__ */ new Set([
|
|
3771
|
+
"heading",
|
|
3772
|
+
"cell",
|
|
3773
|
+
"gridcell",
|
|
3774
|
+
"columnheader",
|
|
3775
|
+
"rowheader",
|
|
3776
|
+
"listitem",
|
|
3777
|
+
"article",
|
|
3778
|
+
"region",
|
|
3779
|
+
"main",
|
|
3780
|
+
"navigation"
|
|
3781
|
+
]);
|
|
3782
|
+
var STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
|
|
3783
|
+
"generic",
|
|
3784
|
+
"group",
|
|
3785
|
+
"list",
|
|
3786
|
+
"table",
|
|
3787
|
+
"row",
|
|
3788
|
+
"rowgroup",
|
|
3789
|
+
"grid",
|
|
3790
|
+
"treegrid",
|
|
3791
|
+
"menu",
|
|
3792
|
+
"menubar",
|
|
3793
|
+
"toolbar",
|
|
3794
|
+
"tablist",
|
|
3795
|
+
"tree",
|
|
3796
|
+
"directory",
|
|
3797
|
+
"document",
|
|
3798
|
+
"application",
|
|
3799
|
+
"presentation",
|
|
3800
|
+
"none"
|
|
3801
|
+
]);
|
|
3802
|
+
function getIndentLevel(line) {
|
|
3803
|
+
const match = /^(\s*)/.exec(line);
|
|
3804
|
+
return match ? Math.floor(match[1].length / 2) : 0;
|
|
3805
|
+
}
|
|
3806
|
+
function matchInteractiveSnapshotLine(line, options) {
|
|
3807
|
+
const depth = getIndentLevel(line);
|
|
3808
|
+
if (options.maxDepth !== void 0 && depth > options.maxDepth) {
|
|
3809
|
+
return null;
|
|
3826
3810
|
}
|
|
3827
|
-
|
|
3828
|
-
|
|
3811
|
+
const match = /^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/.exec(line);
|
|
3812
|
+
if (!match) {
|
|
3813
|
+
return null;
|
|
3829
3814
|
}
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
accuracy: typeof opts.accuracy === "number" ? opts.accuracy : void 0
|
|
3834
|
-
});
|
|
3835
|
-
const origin = opts.origin?.trim() || (() => {
|
|
3836
|
-
try {
|
|
3837
|
-
return new URL(page.url()).origin;
|
|
3838
|
-
} catch {
|
|
3839
|
-
return "";
|
|
3840
|
-
}
|
|
3841
|
-
})();
|
|
3842
|
-
if (origin) await context.grantPermissions(["geolocation"], { origin }).catch(() => {
|
|
3843
|
-
});
|
|
3844
|
-
}
|
|
3845
|
-
async function setHttpCredentialsViaPlaywright(opts) {
|
|
3846
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3847
|
-
ensurePageState(page);
|
|
3848
|
-
if (opts.clear) {
|
|
3849
|
-
await page.context().setHTTPCredentials(null);
|
|
3850
|
-
return;
|
|
3815
|
+
const [, , roleRaw, name, suffix] = match;
|
|
3816
|
+
if (roleRaw.startsWith("/")) {
|
|
3817
|
+
return null;
|
|
3851
3818
|
}
|
|
3852
|
-
const
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3819
|
+
const role = roleRaw.toLowerCase();
|
|
3820
|
+
return {
|
|
3821
|
+
roleRaw,
|
|
3822
|
+
role,
|
|
3823
|
+
...name ? { name } : {},
|
|
3824
|
+
suffix
|
|
3825
|
+
};
|
|
3856
3826
|
}
|
|
3857
|
-
|
|
3858
|
-
const
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3827
|
+
function createRoleNameTracker() {
|
|
3828
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3829
|
+
const refsByKey = /* @__PURE__ */ new Map();
|
|
3830
|
+
return {
|
|
3831
|
+
counts,
|
|
3832
|
+
refsByKey,
|
|
3833
|
+
getKey(role, name) {
|
|
3834
|
+
return `${role}:${name ?? ""}`;
|
|
3835
|
+
},
|
|
3836
|
+
getNextIndex(role, name) {
|
|
3837
|
+
const key = this.getKey(role, name);
|
|
3838
|
+
const current = counts.get(key) ?? 0;
|
|
3839
|
+
counts.set(key, current + 1);
|
|
3840
|
+
return current;
|
|
3841
|
+
},
|
|
3842
|
+
trackRef(role, name, ref) {
|
|
3843
|
+
const key = this.getKey(role, name);
|
|
3844
|
+
const list = refsByKey.get(key) ?? [];
|
|
3845
|
+
list.push(ref);
|
|
3846
|
+
refsByKey.set(key, list);
|
|
3847
|
+
},
|
|
3848
|
+
getDuplicateKeys() {
|
|
3849
|
+
const out = /* @__PURE__ */ new Set();
|
|
3850
|
+
for (const [key, refs] of refsByKey) if (refs.length > 1) out.add(key);
|
|
3851
|
+
return out;
|
|
3873
3852
|
}
|
|
3874
|
-
}
|
|
3853
|
+
};
|
|
3875
3854
|
}
|
|
3876
|
-
|
|
3877
|
-
const
|
|
3878
|
-
|
|
3879
|
-
|
|
3855
|
+
function removeNthFromNonDuplicates(refs, tracker) {
|
|
3856
|
+
const duplicates = tracker.getDuplicateKeys();
|
|
3857
|
+
for (const [ref, data] of Object.entries(refs)) {
|
|
3858
|
+
const key = tracker.getKey(data.role, data.name);
|
|
3859
|
+
if (!duplicates.has(key)) delete refs[ref].nth;
|
|
3860
|
+
}
|
|
3880
3861
|
}
|
|
3881
|
-
|
|
3882
|
-
const
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3862
|
+
function compactTree(tree) {
|
|
3863
|
+
const lines = tree.split("\n");
|
|
3864
|
+
const result = [];
|
|
3865
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3866
|
+
const line = lines[i];
|
|
3867
|
+
if (line.includes("[ref=")) {
|
|
3868
|
+
result.push(line);
|
|
3869
|
+
continue;
|
|
3870
|
+
}
|
|
3871
|
+
if (line.includes(":") && !line.trimEnd().endsWith(":")) {
|
|
3872
|
+
result.push(line);
|
|
3873
|
+
continue;
|
|
3874
|
+
}
|
|
3875
|
+
const currentIndent = getIndentLevel(line);
|
|
3876
|
+
let hasRelevantChildren = false;
|
|
3877
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
3878
|
+
if (getIndentLevel(lines[j]) <= currentIndent) break;
|
|
3879
|
+
if (lines[j]?.includes("[ref=")) {
|
|
3880
|
+
hasRelevantChildren = true;
|
|
3881
|
+
break;
|
|
3898
3882
|
}
|
|
3899
3883
|
}
|
|
3900
|
-
|
|
3884
|
+
if (hasRelevantChildren) result.push(line);
|
|
3885
|
+
}
|
|
3886
|
+
return result.join("\n");
|
|
3901
3887
|
}
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
const
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3888
|
+
function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
|
|
3889
|
+
const lines = ariaSnapshot.split("\n");
|
|
3890
|
+
const refs = {};
|
|
3891
|
+
const tracker = createRoleNameTracker();
|
|
3892
|
+
let counter = 0;
|
|
3893
|
+
const nextRef = () => {
|
|
3894
|
+
counter++;
|
|
3895
|
+
return `e${String(counter)}`;
|
|
3896
|
+
};
|
|
3897
|
+
if (options.interactive === true) {
|
|
3898
|
+
const result2 = [];
|
|
3899
|
+
for (const line of lines) {
|
|
3900
|
+
const parsed = matchInteractiveSnapshotLine(line, options);
|
|
3901
|
+
if (!parsed) continue;
|
|
3902
|
+
const { roleRaw, role, name, suffix } = parsed;
|
|
3903
|
+
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
3904
|
+
const prefix = /^(\s*-\s*)/.exec(line)?.[1] ?? "";
|
|
3905
|
+
const ref = nextRef();
|
|
3906
|
+
const nth = tracker.getNextIndex(role, name);
|
|
3907
|
+
tracker.trackRef(role, name, ref);
|
|
3908
|
+
refs[ref] = { role, name, nth };
|
|
3909
|
+
let enhanced = `${prefix}${roleRaw}`;
|
|
3910
|
+
if (name !== void 0 && name !== "") enhanced += ` "${name}"`;
|
|
3911
|
+
enhanced += ` [ref=${ref}]`;
|
|
3912
|
+
if (nth > 0) enhanced += ` [nth=${String(nth)}]`;
|
|
3913
|
+
if (suffix.includes("[")) enhanced += suffix;
|
|
3914
|
+
result2.push(enhanced);
|
|
3915
|
+
}
|
|
3916
|
+
removeNthFromNonDuplicates(refs, tracker);
|
|
3917
|
+
return { snapshot: result2.join("\n") || "(no interactive elements)", refs };
|
|
3912
3918
|
}
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3919
|
+
const result = [];
|
|
3920
|
+
for (const line of lines) {
|
|
3921
|
+
const depth = getIndentLevel(line);
|
|
3922
|
+
if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
|
|
3923
|
+
const match = /^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/.exec(line);
|
|
3924
|
+
if (!match) {
|
|
3925
|
+
result.push(line);
|
|
3926
|
+
continue;
|
|
3927
|
+
}
|
|
3928
|
+
const [, prefix, roleRaw, name, suffix] = match;
|
|
3929
|
+
if (roleRaw.startsWith("/")) {
|
|
3930
|
+
result.push(line);
|
|
3931
|
+
continue;
|
|
3932
|
+
}
|
|
3933
|
+
const role = roleRaw.toLowerCase();
|
|
3934
|
+
const isInteractive = INTERACTIVE_ROLES.has(role);
|
|
3935
|
+
const isContent = CONTENT_ROLES.has(role);
|
|
3936
|
+
const isStructural = STRUCTURAL_ROLES.has(role);
|
|
3937
|
+
if (options.compact === true && isStructural && name === "") continue;
|
|
3938
|
+
if (!(isInteractive || isContent && name !== "")) {
|
|
3939
|
+
result.push(line);
|
|
3940
|
+
continue;
|
|
3941
|
+
}
|
|
3942
|
+
const ref = nextRef();
|
|
3943
|
+
const nth = tracker.getNextIndex(role, name);
|
|
3944
|
+
tracker.trackRef(role, name, ref);
|
|
3945
|
+
refs[ref] = { role, name, nth };
|
|
3946
|
+
let enhanced = `${prefix}${roleRaw}`;
|
|
3947
|
+
if (name !== "") enhanced += ` "${name}"`;
|
|
3948
|
+
enhanced += ` [ref=${ref}]`;
|
|
3949
|
+
if (nth > 0) enhanced += ` [nth=${String(nth)}]`;
|
|
3950
|
+
if (suffix !== "") enhanced += suffix;
|
|
3951
|
+
result.push(enhanced);
|
|
3916
3952
|
}
|
|
3917
|
-
|
|
3953
|
+
removeNthFromNonDuplicates(refs, tracker);
|
|
3954
|
+
const tree = result.join("\n") || "(empty)";
|
|
3955
|
+
return { snapshot: options.compact === true ? compactTree(tree) : tree, refs };
|
|
3918
3956
|
}
|
|
3919
|
-
|
|
3920
|
-
const
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
const
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
skipped.push(ref);
|
|
3939
|
-
continue;
|
|
3940
|
-
}
|
|
3941
|
-
const x1 = box.x + box.width;
|
|
3942
|
-
const y1 = box.y + box.height;
|
|
3943
|
-
if (x1 < 0 || box.x > viewport.width || y1 < 0 || box.y > viewport.height) {
|
|
3944
|
-
skipped.push(ref);
|
|
3945
|
-
continue;
|
|
3946
|
-
}
|
|
3947
|
-
labels.push({ ref, index: i + 1, box });
|
|
3948
|
-
} catch {
|
|
3949
|
-
skipped.push(ref);
|
|
3957
|
+
function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
|
|
3958
|
+
const lines = aiSnapshot.split("\n");
|
|
3959
|
+
const refs = {};
|
|
3960
|
+
function parseAiSnapshotRef(suffix) {
|
|
3961
|
+
const match = /\[ref=(e\d+)\]/i.exec(suffix);
|
|
3962
|
+
return match ? match[1] : null;
|
|
3963
|
+
}
|
|
3964
|
+
if (options.interactive === true) {
|
|
3965
|
+
const out2 = [];
|
|
3966
|
+
for (const line of lines) {
|
|
3967
|
+
const parsed = matchInteractiveSnapshotLine(line, options);
|
|
3968
|
+
if (!parsed) continue;
|
|
3969
|
+
const { roleRaw, role, name, suffix } = parsed;
|
|
3970
|
+
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
3971
|
+
const ref = parseAiSnapshotRef(suffix);
|
|
3972
|
+
if (ref === null) continue;
|
|
3973
|
+
const prefix = /^(\s*-\s*)/.exec(line)?.[1] ?? "";
|
|
3974
|
+
refs[ref] = { role, ...name !== void 0 && name !== "" ? { name } : {} };
|
|
3975
|
+
out2.push(`${prefix}${roleRaw}${name !== void 0 && name !== "" ? ` "${name}"` : ""}${suffix}`);
|
|
3950
3976
|
}
|
|
3977
|
+
return { snapshot: out2.join("\n") || "(no interactive elements)", refs };
|
|
3951
3978
|
}
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
const border = document.createElement("div");
|
|
3961
|
-
border.style.cssText = `position:absolute;left:${box.x}px;top:${box.y}px;width:${box.width}px;height:${box.height}px;border:2px solid #FF4500;box-sizing:border-box;`;
|
|
3962
|
-
container.appendChild(border);
|
|
3963
|
-
const badge = document.createElement("div");
|
|
3964
|
-
badge.textContent = String(index);
|
|
3965
|
-
badge.style.cssText = `position:absolute;left:${box.x}px;top:${Math.max(0, box.y - 18)}px;background:#FF4500;color:#fff;font:bold 12px/16px monospace;padding:0 4px;border-radius:2px;`;
|
|
3966
|
-
container.appendChild(badge);
|
|
3967
|
-
}
|
|
3968
|
-
document.documentElement.appendChild(container);
|
|
3969
|
-
}, labels.map((l) => ({ index: l.index, box: l.box })));
|
|
3979
|
+
const out = [];
|
|
3980
|
+
for (const line of lines) {
|
|
3981
|
+
const depth = getIndentLevel(line);
|
|
3982
|
+
if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
|
|
3983
|
+
const match = /^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/.exec(line);
|
|
3984
|
+
if (!match) {
|
|
3985
|
+
out.push(line);
|
|
3986
|
+
continue;
|
|
3970
3987
|
}
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
}
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
}
|
|
3988
|
+
const [, , roleRaw, name, suffix] = match;
|
|
3989
|
+
if (roleRaw.startsWith("/")) {
|
|
3990
|
+
out.push(line);
|
|
3991
|
+
continue;
|
|
3992
|
+
}
|
|
3993
|
+
const role = roleRaw.toLowerCase();
|
|
3994
|
+
const isStructural = STRUCTURAL_ROLES.has(role);
|
|
3995
|
+
if (options.compact === true && isStructural && name === "") continue;
|
|
3996
|
+
const ref = parseAiSnapshotRef(suffix);
|
|
3997
|
+
if (ref !== null) refs[ref] = { role, ...name !== "" ? { name } : {} };
|
|
3998
|
+
out.push(line);
|
|
3981
3999
|
}
|
|
4000
|
+
const tree = out.join("\n") || "(empty)";
|
|
4001
|
+
return { snapshot: options.compact === true ? compactTree(tree) : tree, refs };
|
|
3982
4002
|
}
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
4003
|
+
function getRoleSnapshotStats(snapshot, refs) {
|
|
4004
|
+
const interactive = Object.values(refs).filter((r) => INTERACTIVE_ROLES.has(r.role)).length;
|
|
4005
|
+
return {
|
|
4006
|
+
lines: snapshot.split("\n").length,
|
|
4007
|
+
chars: snapshot.length,
|
|
4008
|
+
refs: Object.keys(refs).length,
|
|
4009
|
+
interactive
|
|
4010
|
+
};
|
|
3989
4011
|
}
|
|
3990
|
-
|
|
4012
|
+
|
|
4013
|
+
// src/snapshot/ai-snapshot.ts
|
|
4014
|
+
async function snapshotAi(opts) {
|
|
3991
4015
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3992
4016
|
ensurePageState(page);
|
|
3993
|
-
const
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
throw new Error("Trace already running. Stop the current trace before starting a new one.");
|
|
4017
|
+
const maybe = page;
|
|
4018
|
+
if (!maybe._snapshotForAI) {
|
|
4019
|
+
throw new Error("Playwright _snapshotForAI is not available. Upgrade playwright-core to >= 1.50.");
|
|
3997
4020
|
}
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4021
|
+
const sourceUrl = page.url();
|
|
4022
|
+
const result = await maybe._snapshotForAI({
|
|
4023
|
+
timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3, 6e4),
|
|
4024
|
+
track: "response"
|
|
4002
4025
|
});
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4026
|
+
let snapshot = String(result.full);
|
|
4027
|
+
const maxChars = opts.maxChars;
|
|
4028
|
+
const limit = typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : void 0;
|
|
4029
|
+
let truncated = false;
|
|
4030
|
+
if (limit !== void 0 && snapshot.length > limit) {
|
|
4031
|
+
const lastNewline = snapshot.lastIndexOf("\n", limit);
|
|
4032
|
+
const cutoff = lastNewline > 0 ? lastNewline : limit;
|
|
4033
|
+
snapshot = `${snapshot.slice(0, cutoff)}
|
|
4034
|
+
|
|
4035
|
+
[...TRUNCATED - page too large]`;
|
|
4036
|
+
truncated = true;
|
|
4013
4037
|
}
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4038
|
+
const built = buildRoleSnapshotFromAiSnapshot(snapshot, opts.options);
|
|
4039
|
+
storeRoleRefsForTarget({
|
|
4040
|
+
page,
|
|
4041
|
+
cdpUrl: opts.cdpUrl,
|
|
4042
|
+
targetId: opts.targetId,
|
|
4043
|
+
refs: built.refs,
|
|
4044
|
+
mode: "aria"
|
|
4020
4045
|
});
|
|
4021
|
-
|
|
4046
|
+
return {
|
|
4047
|
+
snapshot: built.snapshot,
|
|
4048
|
+
refs: built.refs,
|
|
4049
|
+
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
4050
|
+
...truncated ? { truncated } : {},
|
|
4051
|
+
untrusted: true,
|
|
4052
|
+
contentMeta: {
|
|
4053
|
+
sourceUrl,
|
|
4054
|
+
contentType: "browser-snapshot",
|
|
4055
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4056
|
+
}
|
|
4057
|
+
};
|
|
4022
4058
|
}
|
|
4023
4059
|
|
|
4024
|
-
// src/
|
|
4025
|
-
function
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
}
|
|
4033
|
-
|
|
4060
|
+
// src/snapshot/aria-snapshot.ts
|
|
4061
|
+
async function snapshotRole(opts) {
|
|
4062
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
4063
|
+
ensurePageState(page);
|
|
4064
|
+
const sourceUrl = page.url();
|
|
4065
|
+
if (opts.refsMode === "aria") {
|
|
4066
|
+
if (opts.selector !== void 0 && opts.selector.trim() !== "" || opts.frameSelector !== void 0 && opts.frameSelector.trim() !== "") {
|
|
4067
|
+
throw new Error("refs=aria does not support selector/frame snapshots yet.");
|
|
4068
|
+
}
|
|
4069
|
+
const maybe = page;
|
|
4070
|
+
if (!maybe._snapshotForAI) {
|
|
4071
|
+
throw new Error("refs=aria requires Playwright _snapshotForAI support.");
|
|
4034
4072
|
}
|
|
4073
|
+
const result = await maybe._snapshotForAI({ timeout: 5e3, track: "response" });
|
|
4074
|
+
const built2 = buildRoleSnapshotFromAiSnapshot(String(result.full), opts.options);
|
|
4075
|
+
storeRoleRefsForTarget({
|
|
4076
|
+
page,
|
|
4077
|
+
cdpUrl: opts.cdpUrl,
|
|
4078
|
+
targetId: opts.targetId,
|
|
4079
|
+
refs: built2.refs,
|
|
4080
|
+
mode: "aria"
|
|
4081
|
+
});
|
|
4082
|
+
return {
|
|
4083
|
+
snapshot: built2.snapshot,
|
|
4084
|
+
refs: built2.refs,
|
|
4085
|
+
stats: getRoleSnapshotStats(built2.snapshot, built2.refs),
|
|
4086
|
+
untrusted: true,
|
|
4087
|
+
contentMeta: {
|
|
4088
|
+
sourceUrl,
|
|
4089
|
+
contentType: "browser-snapshot",
|
|
4090
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4091
|
+
}
|
|
4092
|
+
};
|
|
4035
4093
|
}
|
|
4036
|
-
|
|
4094
|
+
const frameSelector = opts.frameSelector?.trim() ?? "";
|
|
4095
|
+
const selector = opts.selector?.trim() ?? "";
|
|
4096
|
+
const locator = frameSelector ? selector ? page.frameLocator(frameSelector).locator(selector) : page.frameLocator(frameSelector).locator(":root") : selector ? page.locator(selector) : page.locator(":root");
|
|
4097
|
+
const ariaSnapshot = await locator.ariaSnapshot({ timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3) });
|
|
4098
|
+
const built = buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, opts.options);
|
|
4099
|
+
storeRoleRefsForTarget({
|
|
4100
|
+
page,
|
|
4101
|
+
cdpUrl: opts.cdpUrl,
|
|
4102
|
+
targetId: opts.targetId,
|
|
4103
|
+
refs: built.refs,
|
|
4104
|
+
frameSelector: frameSelector !== "" ? frameSelector : void 0,
|
|
4105
|
+
mode: "role"
|
|
4106
|
+
});
|
|
4107
|
+
return {
|
|
4108
|
+
snapshot: built.snapshot,
|
|
4109
|
+
refs: built.refs,
|
|
4110
|
+
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
4111
|
+
untrusted: true,
|
|
4112
|
+
contentMeta: {
|
|
4113
|
+
sourceUrl,
|
|
4114
|
+
contentType: "browser-snapshot",
|
|
4115
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4116
|
+
}
|
|
4117
|
+
};
|
|
4037
4118
|
}
|
|
4038
|
-
async function
|
|
4119
|
+
async function snapshotAria(opts) {
|
|
4120
|
+
const limit = Math.max(1, Math.min(2e3, Math.floor(opts.limit ?? 500)));
|
|
4039
4121
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
4040
4122
|
ensurePageState(page);
|
|
4041
|
-
const
|
|
4042
|
-
const
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
);
|
|
4048
|
-
let body = await response.text();
|
|
4049
|
-
let truncated = false;
|
|
4050
|
-
const maxChars = typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars) ? Math.max(1, Math.min(5e6, Math.floor(opts.maxChars))) : 2e5;
|
|
4051
|
-
if (body.length > maxChars) {
|
|
4052
|
-
body = body.slice(0, maxChars);
|
|
4053
|
-
truncated = true;
|
|
4054
|
-
}
|
|
4055
|
-
const headers = {};
|
|
4056
|
-
const allHeaders = response.headers();
|
|
4057
|
-
for (const [key, value] of Object.entries(allHeaders)) {
|
|
4058
|
-
headers[key] = value;
|
|
4059
|
-
}
|
|
4123
|
+
const sourceUrl = page.url();
|
|
4124
|
+
const res = await withPlaywrightPageCdpSession(page, async (session) => {
|
|
4125
|
+
await session.send("Accessibility.enable").catch(() => {
|
|
4126
|
+
});
|
|
4127
|
+
return await session.send("Accessibility.getFullAXTree");
|
|
4128
|
+
});
|
|
4060
4129
|
return {
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4130
|
+
nodes: formatAriaNodes(Array.isArray(res.nodes) ? res.nodes : [], limit),
|
|
4131
|
+
untrusted: true,
|
|
4132
|
+
contentMeta: {
|
|
4133
|
+
sourceUrl,
|
|
4134
|
+
contentType: "browser-aria-tree",
|
|
4135
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4136
|
+
}
|
|
4066
4137
|
};
|
|
4067
4138
|
}
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
case "warning":
|
|
4075
|
-
case "warn":
|
|
4076
|
-
return 2;
|
|
4077
|
-
case "info":
|
|
4078
|
-
case "log":
|
|
4079
|
-
return 1;
|
|
4080
|
-
case "debug":
|
|
4081
|
-
return 0;
|
|
4082
|
-
default:
|
|
4083
|
-
return 1;
|
|
4084
|
-
}
|
|
4085
|
-
}
|
|
4086
|
-
async function getConsoleMessagesViaPlaywright(opts) {
|
|
4087
|
-
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
4088
|
-
const messages = opts.level ? state.console.filter((msg) => consolePriority(msg.type) >= consolePriority(opts.level)) : [...state.console];
|
|
4089
|
-
if (opts.clear) state.console = [];
|
|
4090
|
-
return messages;
|
|
4091
|
-
}
|
|
4092
|
-
async function getPageErrorsViaPlaywright(opts) {
|
|
4093
|
-
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
4094
|
-
const errors = [...state.errors];
|
|
4095
|
-
if (opts.clear) state.errors = [];
|
|
4096
|
-
return { errors };
|
|
4139
|
+
function axValue(v) {
|
|
4140
|
+
if (!v || typeof v !== "object") return "";
|
|
4141
|
+
const value = v.value;
|
|
4142
|
+
if (typeof value === "string") return value;
|
|
4143
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
4144
|
+
return "";
|
|
4097
4145
|
}
|
|
4098
|
-
|
|
4099
|
-
const
|
|
4100
|
-
const
|
|
4101
|
-
const
|
|
4102
|
-
const
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4146
|
+
function formatAriaNodes(nodes, limit) {
|
|
4147
|
+
const byId = /* @__PURE__ */ new Map();
|
|
4148
|
+
for (const n of nodes) if (n.nodeId) byId.set(n.nodeId, n);
|
|
4149
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
4150
|
+
for (const n of nodes) for (const c of n.childIds ?? []) referenced.add(c);
|
|
4151
|
+
const root = nodes.find((n) => n.nodeId !== "" && !referenced.has(n.nodeId)) ?? nodes[0];
|
|
4152
|
+
if (root.nodeId === "") return [];
|
|
4153
|
+
const out = [];
|
|
4154
|
+
const stack = [{ id: root.nodeId, depth: 0 }];
|
|
4155
|
+
while (stack.length && out.length < limit) {
|
|
4156
|
+
const popped = stack.pop();
|
|
4157
|
+
if (!popped) break;
|
|
4158
|
+
const { id, depth } = popped;
|
|
4159
|
+
const n = byId.get(id);
|
|
4160
|
+
if (!n) continue;
|
|
4161
|
+
const role = axValue(n.role);
|
|
4162
|
+
const name = axValue(n.name);
|
|
4163
|
+
const value = axValue(n.value);
|
|
4164
|
+
const description = axValue(n.description);
|
|
4165
|
+
const ref = `ax${String(out.length + 1)}`;
|
|
4166
|
+
out.push({
|
|
4167
|
+
ref,
|
|
4168
|
+
role: role || "unknown",
|
|
4169
|
+
name: name || "",
|
|
4170
|
+
...value ? { value } : {},
|
|
4171
|
+
...description ? { description } : {},
|
|
4172
|
+
...typeof n.backendDOMNodeId === "number" ? { backendDOMNodeId: n.backendDOMNodeId } : {},
|
|
4173
|
+
depth
|
|
4174
|
+
});
|
|
4175
|
+
const children = (n.childIds ?? []).filter((c) => byId.has(c));
|
|
4176
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
4177
|
+
if (children[i]) stack.push({ id: children[i], depth: depth + 1 });
|
|
4178
|
+
}
|
|
4106
4179
|
}
|
|
4107
|
-
return
|
|
4180
|
+
return out;
|
|
4108
4181
|
}
|
|
4109
4182
|
|
|
4110
4183
|
// src/storage/index.ts
|
|
@@ -4117,9 +4190,9 @@ async function cookiesSetViaPlaywright(opts) {
|
|
|
4117
4190
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
4118
4191
|
ensurePageState(page);
|
|
4119
4192
|
const cookie = opts.cookie;
|
|
4120
|
-
if (
|
|
4121
|
-
const hasUrl = typeof cookie.url === "string" && cookie.url.trim();
|
|
4122
|
-
const hasDomainPath = typeof cookie.domain === "string" && cookie.domain.trim() && typeof cookie.path === "string" && cookie.path.trim();
|
|
4193
|
+
if (cookie.name === "") throw new Error("cookie name and value are required");
|
|
4194
|
+
const hasUrl = typeof cookie.url === "string" && cookie.url.trim() !== "";
|
|
4195
|
+
const hasDomainPath = typeof cookie.domain === "string" && cookie.domain.trim() !== "" && typeof cookie.path === "string" && cookie.path.trim() !== "";
|
|
4123
4196
|
if (!hasUrl && !hasDomainPath) throw new Error("cookie requires url, or domain+path");
|
|
4124
4197
|
await page.context().addCookies([cookie]);
|
|
4125
4198
|
}
|
|
@@ -4135,33 +4208,33 @@ async function storageGetViaPlaywright(opts) {
|
|
|
4135
4208
|
values: await page.evaluate(
|
|
4136
4209
|
({ kind, key }) => {
|
|
4137
4210
|
const store = kind === "session" ? window.sessionStorage : window.localStorage;
|
|
4138
|
-
if (key) {
|
|
4211
|
+
if (key !== void 0 && key !== "") {
|
|
4139
4212
|
const value = store.getItem(key);
|
|
4140
4213
|
return value === null ? {} : { [key]: value };
|
|
4141
4214
|
}
|
|
4142
4215
|
const out = {};
|
|
4143
4216
|
for (let i = 0; i < store.length; i++) {
|
|
4144
4217
|
const k = store.key(i);
|
|
4145
|
-
if (
|
|
4218
|
+
if (k === null || k === "") continue;
|
|
4146
4219
|
const v = store.getItem(k);
|
|
4147
4220
|
if (v !== null) out[k] = v;
|
|
4148
4221
|
}
|
|
4149
4222
|
return out;
|
|
4150
4223
|
},
|
|
4151
4224
|
{ kind: opts.kind, key: opts.key }
|
|
4152
|
-
)
|
|
4225
|
+
)
|
|
4153
4226
|
};
|
|
4154
4227
|
}
|
|
4155
4228
|
async function storageSetViaPlaywright(opts) {
|
|
4156
|
-
const key =
|
|
4157
|
-
if (
|
|
4229
|
+
const key = opts.key;
|
|
4230
|
+
if (key === "") throw new Error("key is required");
|
|
4158
4231
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
4159
4232
|
ensurePageState(page);
|
|
4160
4233
|
await page.evaluate(
|
|
4161
4234
|
({ kind, key: k, value }) => {
|
|
4162
4235
|
(kind === "session" ? window.sessionStorage : window.localStorage).setItem(k, value);
|
|
4163
4236
|
},
|
|
4164
|
-
{ kind: opts.kind, key, value:
|
|
4237
|
+
{ kind: opts.kind, key, value: opts.value }
|
|
4165
4238
|
);
|
|
4166
4239
|
}
|
|
4167
4240
|
async function storageClearViaPlaywright(opts) {
|
|
@@ -4228,8 +4301,10 @@ var CrawlPage = class {
|
|
|
4228
4301
|
}
|
|
4229
4302
|
});
|
|
4230
4303
|
}
|
|
4231
|
-
if (opts?.selector || opts?.frameSelector) {
|
|
4232
|
-
throw new Error(
|
|
4304
|
+
if (opts?.selector !== void 0 && opts.selector !== "" || opts?.frameSelector !== void 0 && opts.frameSelector !== "") {
|
|
4305
|
+
throw new Error(
|
|
4306
|
+
'selector and frameSelector are only supported in role mode. Use { mode: "role" } or omit these options.'
|
|
4307
|
+
);
|
|
4233
4308
|
}
|
|
4234
4309
|
return snapshotAi({
|
|
4235
4310
|
cdpUrl: this.cdpUrl,
|
|
@@ -5100,8 +5175,8 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
5100
5175
|
*/
|
|
5101
5176
|
static async launch(opts = {}) {
|
|
5102
5177
|
const chrome = await launchChrome(opts);
|
|
5103
|
-
const cdpUrl = `http://127.0.0.1:${chrome.cdpPort}`;
|
|
5104
|
-
const ssrfPolicy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
5178
|
+
const cdpUrl = `http://127.0.0.1:${String(chrome.cdpPort)}`;
|
|
5179
|
+
const ssrfPolicy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
5105
5180
|
return new _BrowserClaw(cdpUrl, chrome, ssrfPolicy);
|
|
5106
5181
|
}
|
|
5107
5182
|
/**
|
|
@@ -5123,7 +5198,7 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
5123
5198
|
throw new Error(`Cannot connect to Chrome at ${cdpUrl}. Is Chrome running with --remote-debugging-port?`);
|
|
5124
5199
|
}
|
|
5125
5200
|
await connectBrowser(cdpUrl, opts?.authToken);
|
|
5126
|
-
const ssrfPolicy = opts?.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts?.ssrfPolicy;
|
|
5201
|
+
const ssrfPolicy = opts?.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts?.ssrfPolicy;
|
|
5127
5202
|
return new _BrowserClaw(cdpUrl, null, ssrfPolicy);
|
|
5128
5203
|
}
|
|
5129
5204
|
/**
|
|
@@ -5149,10 +5224,10 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
5149
5224
|
*/
|
|
5150
5225
|
async currentPage() {
|
|
5151
5226
|
const { browser } = await connectBrowser(this.cdpUrl);
|
|
5152
|
-
const pages =
|
|
5227
|
+
const pages = getAllPages(browser);
|
|
5153
5228
|
if (!pages.length) throw new Error("No pages available. Use browser.open(url) to create a tab.");
|
|
5154
5229
|
const tid = await pageTargetId(pages[0]).catch(() => null);
|
|
5155
|
-
if (
|
|
5230
|
+
if (tid === null || tid === "") throw new Error("Failed to get targetId for the current page.");
|
|
5156
5231
|
return new CrawlPage(this.cdpUrl, tid, this.ssrfPolicy);
|
|
5157
5232
|
}
|
|
5158
5233
|
/**
|