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.cjs
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var os = require('os');
|
|
4
|
-
var path = require('path');
|
|
5
|
-
var fs = require('fs');
|
|
6
|
-
var net = require('net');
|
|
7
|
-
var child_process = require('child_process');
|
|
8
|
-
var playwrightCore = require('playwright-core');
|
|
9
3
|
var http = require('http');
|
|
10
4
|
var https = require('https');
|
|
11
|
-
var
|
|
5
|
+
var playwrightCore = require('playwright-core');
|
|
6
|
+
var child_process = require('child_process');
|
|
7
|
+
var fs = require('fs');
|
|
8
|
+
var net = require('net');
|
|
9
|
+
var os = require('os');
|
|
10
|
+
var path = require('path');
|
|
11
|
+
var crypto = require('crypto');
|
|
12
12
|
var dns = require('dns');
|
|
13
|
+
var promises = require('dns/promises');
|
|
13
14
|
var promises$1 = require('fs/promises');
|
|
14
|
-
var crypto = require('crypto');
|
|
15
15
|
|
|
16
16
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
17
17
|
|
|
18
|
-
var os__default = /*#__PURE__*/_interopDefault(os);
|
|
19
|
-
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
20
|
-
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
21
|
-
var net__default = /*#__PURE__*/_interopDefault(net);
|
|
22
18
|
var http__default = /*#__PURE__*/_interopDefault(http);
|
|
23
19
|
var https__default = /*#__PURE__*/_interopDefault(https);
|
|
20
|
+
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
21
|
+
var net__default = /*#__PURE__*/_interopDefault(net);
|
|
22
|
+
var os__default = /*#__PURE__*/_interopDefault(os);
|
|
23
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
24
24
|
|
|
25
25
|
var __create = Object.create;
|
|
26
26
|
var __defProp = Object.defineProperty;
|
|
@@ -921,7 +921,7 @@ function execText(command, args, timeoutMs = 1200) {
|
|
|
921
921
|
encoding: "utf8",
|
|
922
922
|
maxBuffer: 1024 * 1024
|
|
923
923
|
});
|
|
924
|
-
return
|
|
924
|
+
return output.trim() || null;
|
|
925
925
|
} catch {
|
|
926
926
|
return null;
|
|
927
927
|
}
|
|
@@ -932,7 +932,8 @@ function inferKindFromIdentifier(identifier) {
|
|
|
932
932
|
if (id.includes("edge")) return "edge";
|
|
933
933
|
if (id.includes("chromium")) return "chromium";
|
|
934
934
|
if (id.includes("canary")) return "canary";
|
|
935
|
-
if (id.includes("opera") || id.includes("vivaldi") || id.includes("yandex") || id.includes("thebrowser"))
|
|
935
|
+
if (id.includes("opera") || id.includes("vivaldi") || id.includes("yandex") || id.includes("thebrowser"))
|
|
936
|
+
return "chromium";
|
|
936
937
|
return "chrome";
|
|
937
938
|
}
|
|
938
939
|
function inferKindFromExeName(name) {
|
|
@@ -949,24 +950,29 @@ function findFirstExe(candidates) {
|
|
|
949
950
|
return null;
|
|
950
951
|
}
|
|
951
952
|
function detectDefaultBrowserBundleIdMac() {
|
|
952
|
-
const plistPath = path__default.default.join(
|
|
953
|
+
const plistPath = path__default.default.join(
|
|
954
|
+
os__default.default.homedir(),
|
|
955
|
+
"Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist"
|
|
956
|
+
);
|
|
953
957
|
if (!fileExists(plistPath)) return null;
|
|
954
958
|
const handlersRaw = execText("/usr/bin/plutil", ["-extract", "LSHandlers", "json", "-o", "-", "--", plistPath], 2e3);
|
|
955
|
-
if (
|
|
959
|
+
if (handlersRaw === null) return null;
|
|
956
960
|
let handlers;
|
|
957
961
|
try {
|
|
958
|
-
|
|
962
|
+
const parsed = JSON.parse(handlersRaw);
|
|
963
|
+
if (!Array.isArray(parsed)) return null;
|
|
964
|
+
handlers = parsed;
|
|
959
965
|
} catch {
|
|
960
966
|
return null;
|
|
961
967
|
}
|
|
962
|
-
if (!Array.isArray(handlers)) return null;
|
|
963
968
|
const resolveScheme = (scheme) => {
|
|
964
969
|
let candidate = null;
|
|
965
970
|
for (const entry of handlers) {
|
|
966
|
-
if (
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
971
|
+
if (entry === null || entry === void 0 || typeof entry !== "object") continue;
|
|
972
|
+
const rec = entry;
|
|
973
|
+
if (rec.LSHandlerURLScheme !== scheme) continue;
|
|
974
|
+
const role = (typeof rec.LSHandlerRoleAll === "string" ? rec.LSHandlerRoleAll : null) ?? (typeof rec.LSHandlerRoleViewer === "string" ? rec.LSHandlerRoleViewer : null) ?? null;
|
|
975
|
+
if (role !== null) candidate = role;
|
|
970
976
|
}
|
|
971
977
|
return candidate;
|
|
972
978
|
};
|
|
@@ -974,12 +980,12 @@ function detectDefaultBrowserBundleIdMac() {
|
|
|
974
980
|
}
|
|
975
981
|
function detectDefaultChromiumMac() {
|
|
976
982
|
const bundleId = detectDefaultBrowserBundleIdMac();
|
|
977
|
-
if (
|
|
983
|
+
if (bundleId === null || !CHROMIUM_BUNDLE_IDS.has(bundleId)) return null;
|
|
978
984
|
const appPathRaw = execText("/usr/bin/osascript", ["-e", `POSIX path of (path to application id "${bundleId}")`]);
|
|
979
|
-
if (
|
|
985
|
+
if (appPathRaw === null) return null;
|
|
980
986
|
const appPath = appPathRaw.trim().replace(/\/$/, "");
|
|
981
987
|
const exeName = execText("/usr/bin/defaults", ["read", path__default.default.join(appPath, "Contents", "Info"), "CFBundleExecutable"]);
|
|
982
|
-
if (
|
|
988
|
+
if (exeName === null) return null;
|
|
983
989
|
const exePath = path__default.default.join(appPath, "Contents", "MacOS", exeName.trim());
|
|
984
990
|
if (!fileExists(exePath)) return null;
|
|
985
991
|
return { kind: inferKindFromIdentifier(bundleId), path: exePath };
|
|
@@ -995,12 +1001,15 @@ function findChromeMac() {
|
|
|
995
1001
|
{ kind: "chromium", path: "/Applications/Chromium.app/Contents/MacOS/Chromium" },
|
|
996
1002
|
{ kind: "chromium", path: path__default.default.join(os__default.default.homedir(), "Applications/Chromium.app/Contents/MacOS/Chromium") },
|
|
997
1003
|
{ kind: "canary", path: "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary" },
|
|
998
|
-
{
|
|
1004
|
+
{
|
|
1005
|
+
kind: "canary",
|
|
1006
|
+
path: path__default.default.join(os__default.default.homedir(), "Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary")
|
|
1007
|
+
}
|
|
999
1008
|
]);
|
|
1000
1009
|
}
|
|
1001
1010
|
function detectDefaultChromiumLinux() {
|
|
1002
|
-
const desktopId = execText("xdg-settings", ["get", "default-web-browser"])
|
|
1003
|
-
if (
|
|
1011
|
+
const desktopId = execText("xdg-settings", ["get", "default-web-browser"]) ?? execText("xdg-mime", ["query", "default", "x-scheme-handler/http"]);
|
|
1012
|
+
if (desktopId === null) return null;
|
|
1004
1013
|
const trimmed = desktopId.trim();
|
|
1005
1014
|
if (!CHROMIUM_DESKTOP_IDS.has(trimmed)) return null;
|
|
1006
1015
|
const searchDirs = [
|
|
@@ -1017,17 +1026,18 @@ function detectDefaultChromiumLinux() {
|
|
|
1017
1026
|
break;
|
|
1018
1027
|
}
|
|
1019
1028
|
}
|
|
1020
|
-
if (
|
|
1029
|
+
if (desktopPath === null) return null;
|
|
1021
1030
|
let execLine = null;
|
|
1022
1031
|
try {
|
|
1023
1032
|
const lines = fs__default.default.readFileSync(desktopPath, "utf8").split(/\r?\n/);
|
|
1024
|
-
for (const line of lines)
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1033
|
+
for (const line of lines)
|
|
1034
|
+
if (line.startsWith("Exec=")) {
|
|
1035
|
+
execLine = line.slice(5).trim();
|
|
1036
|
+
break;
|
|
1037
|
+
}
|
|
1028
1038
|
} catch {
|
|
1029
1039
|
}
|
|
1030
|
-
if (
|
|
1040
|
+
if (execLine === null) return null;
|
|
1031
1041
|
const tokens = execLine.split(/\s+/);
|
|
1032
1042
|
let command = null;
|
|
1033
1043
|
for (const token of tokens) {
|
|
@@ -1035,9 +1045,9 @@ function detectDefaultChromiumLinux() {
|
|
|
1035
1045
|
command = token.replace(/^["']|["']$/g, "");
|
|
1036
1046
|
break;
|
|
1037
1047
|
}
|
|
1038
|
-
if (
|
|
1048
|
+
if (command === null) return null;
|
|
1039
1049
|
const resolved = command.startsWith("/") ? command : execText("which", [command], 800)?.trim() ?? null;
|
|
1040
|
-
if (
|
|
1050
|
+
if (resolved === null || resolved === "") return null;
|
|
1041
1051
|
const exeName = path__default.default.posix.basename(resolved).toLowerCase();
|
|
1042
1052
|
if (!CHROMIUM_EXE_NAMES.has(exeName)) return null;
|
|
1043
1053
|
return { kind: inferKindFromExeName(exeName), path: resolved };
|
|
@@ -1066,21 +1076,30 @@ function findChromeWindows() {
|
|
|
1066
1076
|
const candidates = [];
|
|
1067
1077
|
if (localAppData) {
|
|
1068
1078
|
candidates.push({ kind: "chrome", path: j(localAppData, "Google", "Chrome", "Application", "chrome.exe") });
|
|
1069
|
-
candidates.push({
|
|
1079
|
+
candidates.push({
|
|
1080
|
+
kind: "brave",
|
|
1081
|
+
path: j(localAppData, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
|
|
1082
|
+
});
|
|
1070
1083
|
candidates.push({ kind: "edge", path: j(localAppData, "Microsoft", "Edge", "Application", "msedge.exe") });
|
|
1071
1084
|
candidates.push({ kind: "chromium", path: j(localAppData, "Chromium", "Application", "chrome.exe") });
|
|
1072
1085
|
candidates.push({ kind: "canary", path: j(localAppData, "Google", "Chrome SxS", "Application", "chrome.exe") });
|
|
1073
1086
|
}
|
|
1074
1087
|
candidates.push({ kind: "chrome", path: j(programFiles, "Google", "Chrome", "Application", "chrome.exe") });
|
|
1075
1088
|
candidates.push({ kind: "chrome", path: j(programFilesX86, "Google", "Chrome", "Application", "chrome.exe") });
|
|
1076
|
-
candidates.push({
|
|
1077
|
-
|
|
1089
|
+
candidates.push({
|
|
1090
|
+
kind: "brave",
|
|
1091
|
+
path: j(programFiles, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
|
|
1092
|
+
});
|
|
1093
|
+
candidates.push({
|
|
1094
|
+
kind: "brave",
|
|
1095
|
+
path: j(programFilesX86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
|
|
1096
|
+
});
|
|
1078
1097
|
candidates.push({ kind: "edge", path: j(programFiles, "Microsoft", "Edge", "Application", "msedge.exe") });
|
|
1079
1098
|
candidates.push({ kind: "edge", path: j(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe") });
|
|
1080
1099
|
return findFirstExe(candidates);
|
|
1081
1100
|
}
|
|
1082
1101
|
function resolveBrowserExecutable(opts) {
|
|
1083
|
-
if (opts?.executablePath) {
|
|
1102
|
+
if (opts?.executablePath !== void 0 && opts.executablePath !== "") {
|
|
1084
1103
|
if (!fileExists(opts.executablePath)) throw new Error(`executablePath not found: ${opts.executablePath}`);
|
|
1085
1104
|
return { kind: "custom", path: opts.executablePath };
|
|
1086
1105
|
}
|
|
@@ -1093,10 +1112,12 @@ function resolveBrowserExecutable(opts) {
|
|
|
1093
1112
|
async function ensurePortAvailable(port) {
|
|
1094
1113
|
await new Promise((resolve2, reject) => {
|
|
1095
1114
|
const tester = net__default.default.createServer().once("error", (err) => {
|
|
1096
|
-
if (err.code === "EADDRINUSE") reject(new Error(`Port ${port} is already in use`));
|
|
1115
|
+
if (err.code === "EADDRINUSE") reject(new Error(`Port ${String(port)} is already in use`));
|
|
1097
1116
|
else reject(err);
|
|
1098
1117
|
}).once("listening", () => {
|
|
1099
|
-
tester.close(() =>
|
|
1118
|
+
tester.close(() => {
|
|
1119
|
+
resolve2();
|
|
1120
|
+
});
|
|
1100
1121
|
}).listen(port);
|
|
1101
1122
|
});
|
|
1102
1123
|
}
|
|
@@ -1178,7 +1199,7 @@ function isLoopbackHost(hostname) {
|
|
|
1178
1199
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
1179
1200
|
}
|
|
1180
1201
|
function hasProxyEnvConfigured() {
|
|
1181
|
-
return
|
|
1202
|
+
return (process.env.HTTP_PROXY ?? process.env.HTTPS_PROXY ?? process.env.http_proxy ?? process.env.https_proxy ?? "") !== "";
|
|
1182
1203
|
}
|
|
1183
1204
|
function normalizeCdpWsUrl(wsUrl, cdpUrl) {
|
|
1184
1205
|
const ws = new URL(wsUrl);
|
|
@@ -1230,7 +1251,12 @@ async function canOpenWebSocket(url, timeoutMs) {
|
|
|
1230
1251
|
}
|
|
1231
1252
|
resolve2(value);
|
|
1232
1253
|
};
|
|
1233
|
-
const timer = setTimeout(
|
|
1254
|
+
const timer = setTimeout(
|
|
1255
|
+
() => {
|
|
1256
|
+
finish(false);
|
|
1257
|
+
},
|
|
1258
|
+
Math.max(50, timeoutMs + 25)
|
|
1259
|
+
);
|
|
1234
1260
|
let ws;
|
|
1235
1261
|
try {
|
|
1236
1262
|
ws = new WebSocket(url);
|
|
@@ -1238,21 +1264,27 @@ async function canOpenWebSocket(url, timeoutMs) {
|
|
|
1238
1264
|
finish(false);
|
|
1239
1265
|
return;
|
|
1240
1266
|
}
|
|
1241
|
-
ws.onopen = () =>
|
|
1242
|
-
|
|
1267
|
+
ws.onopen = () => {
|
|
1268
|
+
finish(true);
|
|
1269
|
+
};
|
|
1270
|
+
ws.onerror = () => {
|
|
1271
|
+
finish(false);
|
|
1272
|
+
};
|
|
1243
1273
|
});
|
|
1244
1274
|
}
|
|
1245
1275
|
async function fetchChromeVersion(cdpUrl, timeoutMs = 500, authToken) {
|
|
1246
1276
|
const ctrl = new AbortController();
|
|
1247
|
-
const t = setTimeout(() =>
|
|
1277
|
+
const t = setTimeout(() => {
|
|
1278
|
+
ctrl.abort();
|
|
1279
|
+
}, timeoutMs);
|
|
1248
1280
|
try {
|
|
1249
1281
|
const httpBase = isWebSocketUrl(cdpUrl) ? normalizeCdpHttpBaseForJsonEndpoints(cdpUrl) : cdpUrl;
|
|
1250
1282
|
const headers = {};
|
|
1251
|
-
if (authToken) headers
|
|
1283
|
+
if (authToken !== void 0 && authToken !== "") headers.Authorization = `Bearer ${authToken}`;
|
|
1252
1284
|
const res = await fetch(appendCdpPath(httpBase, "/json/version"), { signal: ctrl.signal, headers });
|
|
1253
1285
|
if (!res.ok) return null;
|
|
1254
1286
|
const data = await res.json();
|
|
1255
|
-
if (
|
|
1287
|
+
if (data === null || data === void 0 || typeof data !== "object") return null;
|
|
1256
1288
|
return data;
|
|
1257
1289
|
} catch {
|
|
1258
1290
|
return null;
|
|
@@ -1268,13 +1300,14 @@ async function isChromeReachable(cdpUrl, timeoutMs = 500, authToken) {
|
|
|
1268
1300
|
async function getChromeWebSocketUrl(cdpUrl, timeoutMs = 500, authToken) {
|
|
1269
1301
|
if (isWebSocketUrl(cdpUrl)) return cdpUrl;
|
|
1270
1302
|
const version = await fetchChromeVersion(cdpUrl, timeoutMs, authToken);
|
|
1271
|
-
const
|
|
1272
|
-
|
|
1303
|
+
const rawWsUrl = version?.webSocketDebuggerUrl;
|
|
1304
|
+
const wsUrl = typeof rawWsUrl === "string" ? rawWsUrl.trim() : "";
|
|
1305
|
+
if (wsUrl === "") return null;
|
|
1273
1306
|
return normalizeCdpWsUrl(wsUrl, cdpUrl);
|
|
1274
1307
|
}
|
|
1275
1308
|
async function isChromeCdpReady(cdpUrl, timeoutMs = 500, handshakeTimeoutMs = 800) {
|
|
1276
1309
|
const wsUrl = await getChromeWebSocketUrl(cdpUrl, timeoutMs);
|
|
1277
|
-
if (
|
|
1310
|
+
if (wsUrl === null) return false;
|
|
1278
1311
|
return await canRunCdpHealthCommand(wsUrl, handshakeTimeoutMs);
|
|
1279
1312
|
}
|
|
1280
1313
|
async function canRunCdpHealthCommand(wsUrl, timeoutMs = 800) {
|
|
@@ -1290,7 +1323,12 @@ async function canRunCdpHealthCommand(wsUrl, timeoutMs = 800) {
|
|
|
1290
1323
|
}
|
|
1291
1324
|
resolve2(value);
|
|
1292
1325
|
};
|
|
1293
|
-
const timer = setTimeout(
|
|
1326
|
+
const timer = setTimeout(
|
|
1327
|
+
() => {
|
|
1328
|
+
finish(false);
|
|
1329
|
+
},
|
|
1330
|
+
Math.max(50, timeoutMs + 25)
|
|
1331
|
+
);
|
|
1294
1332
|
let ws;
|
|
1295
1333
|
try {
|
|
1296
1334
|
ws = new WebSocket(wsUrl);
|
|
@@ -1308,26 +1346,33 @@ async function canRunCdpHealthCommand(wsUrl, timeoutMs = 800) {
|
|
|
1308
1346
|
ws.onmessage = (event) => {
|
|
1309
1347
|
try {
|
|
1310
1348
|
const parsed = JSON.parse(String(event.data));
|
|
1311
|
-
if (parsed
|
|
1312
|
-
|
|
1349
|
+
if (typeof parsed !== "object" || parsed === null) return;
|
|
1350
|
+
const msg = parsed;
|
|
1351
|
+
if (msg.id !== 1) return;
|
|
1352
|
+
finish(typeof msg.result === "object" && msg.result !== null);
|
|
1313
1353
|
} catch {
|
|
1314
1354
|
}
|
|
1315
1355
|
};
|
|
1316
|
-
ws.onerror = () =>
|
|
1317
|
-
|
|
1356
|
+
ws.onerror = () => {
|
|
1357
|
+
finish(false);
|
|
1358
|
+
};
|
|
1359
|
+
ws.onclose = () => {
|
|
1360
|
+
finish(false);
|
|
1361
|
+
};
|
|
1318
1362
|
});
|
|
1319
1363
|
}
|
|
1320
1364
|
async function launchChrome(opts = {}) {
|
|
1321
1365
|
const cdpPort = opts.cdpPort ?? DEFAULT_CDP_PORT;
|
|
1322
1366
|
await ensurePortAvailable(cdpPort);
|
|
1323
1367
|
const exe = resolveBrowserExecutable({ executablePath: opts.executablePath });
|
|
1324
|
-
if (!exe)
|
|
1368
|
+
if (!exe)
|
|
1369
|
+
throw new Error("No supported browser found (Chrome/Brave/Edge/Chromium). Install one or provide executablePath.");
|
|
1325
1370
|
const profileName = opts.profileName ?? DEFAULT_PROFILE_NAME;
|
|
1326
1371
|
const userDataDir = opts.userDataDir ?? resolveUserDataDir(profileName);
|
|
1327
1372
|
fs__default.default.mkdirSync(userDataDir, { recursive: true });
|
|
1328
1373
|
const spawnChrome = () => {
|
|
1329
1374
|
const args = [
|
|
1330
|
-
`--remote-debugging-port=${cdpPort}`,
|
|
1375
|
+
`--remote-debugging-port=${String(cdpPort)}`,
|
|
1331
1376
|
`--user-data-dir=${userDataDir}`,
|
|
1332
1377
|
"--no-first-run",
|
|
1333
1378
|
"--no-default-browser-check",
|
|
@@ -1340,10 +1385,10 @@ async function launchChrome(opts = {}) {
|
|
|
1340
1385
|
"--hide-crash-restore-bubble",
|
|
1341
1386
|
"--password-store=basic"
|
|
1342
1387
|
];
|
|
1343
|
-
if (opts.headless) {
|
|
1388
|
+
if (opts.headless === true) {
|
|
1344
1389
|
args.push("--headless=new", "--disable-gpu");
|
|
1345
1390
|
}
|
|
1346
|
-
if (opts.noSandbox) {
|
|
1391
|
+
if (opts.noSandbox === true) {
|
|
1347
1392
|
args.push("--no-sandbox", "--disable-setuid-sandbox");
|
|
1348
1393
|
}
|
|
1349
1394
|
if (process.platform === "linux") args.push("--disable-dev-shm-usage");
|
|
@@ -1390,12 +1435,12 @@ async function launchChrome(opts = {}) {
|
|
|
1390
1435
|
} catch {
|
|
1391
1436
|
}
|
|
1392
1437
|
const proc = spawnChrome();
|
|
1393
|
-
const cdpUrl = `http://127.0.0.1:${cdpPort}`;
|
|
1438
|
+
const cdpUrl = `http://127.0.0.1:${String(cdpPort)}`;
|
|
1394
1439
|
const stderrChunks = [];
|
|
1395
1440
|
const onStderr = (chunk) => {
|
|
1396
1441
|
stderrChunks.push(chunk);
|
|
1397
1442
|
};
|
|
1398
|
-
proc.stderr
|
|
1443
|
+
proc.stderr.on("data", onStderr);
|
|
1399
1444
|
const readyDeadline = Date.now() + 15e3;
|
|
1400
1445
|
while (Date.now() < readyDeadline) {
|
|
1401
1446
|
if (await isChromeReachable(cdpUrl, 500)) break;
|
|
@@ -1406,14 +1451,14 @@ async function launchChrome(opts = {}) {
|
|
|
1406
1451
|
const stderrHint = stderrOutput ? `
|
|
1407
1452
|
Chrome stderr:
|
|
1408
1453
|
${stderrOutput.slice(0, 2e3)}` : "";
|
|
1409
|
-
const sandboxHint = process.platform === "linux" &&
|
|
1454
|
+
const sandboxHint = process.platform === "linux" && opts.noSandbox !== true ? "\nHint: If running in a container or as root, try setting noSandbox: true." : "";
|
|
1410
1455
|
try {
|
|
1411
1456
|
proc.kill("SIGKILL");
|
|
1412
1457
|
} catch {
|
|
1413
1458
|
}
|
|
1414
|
-
throw new Error(`Failed to start Chrome CDP on port ${cdpPort}.${sandboxHint}${stderrHint}`);
|
|
1459
|
+
throw new Error(`Failed to start Chrome CDP on port ${String(cdpPort)}.${sandboxHint}${stderrHint}`);
|
|
1415
1460
|
}
|
|
1416
|
-
proc.stderr
|
|
1461
|
+
proc.stderr.off("data", onStderr);
|
|
1417
1462
|
stderrChunks.length = 0;
|
|
1418
1463
|
return {
|
|
1419
1464
|
pid: proc.pid ?? -1,
|
|
@@ -1426,14 +1471,14 @@ ${stderrOutput.slice(0, 2e3)}` : "";
|
|
|
1426
1471
|
}
|
|
1427
1472
|
async function stopChrome(running, timeoutMs = 2500) {
|
|
1428
1473
|
const proc = running.proc;
|
|
1429
|
-
if (proc.exitCode
|
|
1474
|
+
if (proc.exitCode !== null) return;
|
|
1430
1475
|
try {
|
|
1431
1476
|
proc.kill("SIGTERM");
|
|
1432
1477
|
} catch {
|
|
1433
1478
|
}
|
|
1434
1479
|
const start = Date.now();
|
|
1435
1480
|
while (Date.now() - start < timeoutMs) {
|
|
1436
|
-
if (proc.exitCode
|
|
1481
|
+
if (proc.exitCode !== null) return;
|
|
1437
1482
|
await new Promise((r) => setTimeout(r, 100));
|
|
1438
1483
|
}
|
|
1439
1484
|
try {
|
|
@@ -1441,17 +1486,19 @@ async function stopChrome(running, timeoutMs = 2500) {
|
|
|
1441
1486
|
} catch {
|
|
1442
1487
|
}
|
|
1443
1488
|
}
|
|
1489
|
+
|
|
1490
|
+
// src/connection.ts
|
|
1444
1491
|
var BrowserTabNotFoundError = class extends Error {
|
|
1445
1492
|
constructor(message = "Tab not found") {
|
|
1446
1493
|
super(message);
|
|
1447
1494
|
this.name = "BrowserTabNotFoundError";
|
|
1448
1495
|
}
|
|
1449
1496
|
};
|
|
1450
|
-
var OPENCLAW_EXTENSION_RELAY_BROWSER = "OpenClaw/extension-relay";
|
|
1451
|
-
var extensionRelayByCdpUrl = /* @__PURE__ */ new Map();
|
|
1452
1497
|
async function fetchJsonForCdp(url, timeoutMs) {
|
|
1453
1498
|
const ctrl = new AbortController();
|
|
1454
|
-
const t = setTimeout(() =>
|
|
1499
|
+
const t = setTimeout(() => {
|
|
1500
|
+
ctrl.abort();
|
|
1501
|
+
}, timeoutMs);
|
|
1455
1502
|
try {
|
|
1456
1503
|
const res = await fetch(url, { signal: ctrl.signal });
|
|
1457
1504
|
if (!res.ok) return null;
|
|
@@ -1471,20 +1518,6 @@ function appendCdpPath2(cdpUrl, cdpPath) {
|
|
|
1471
1518
|
return `${cdpUrl.replace(/\/$/, "")}${cdpPath}`;
|
|
1472
1519
|
}
|
|
1473
1520
|
}
|
|
1474
|
-
async function isExtensionRelayCdpEndpoint(cdpUrl) {
|
|
1475
|
-
const normalized = normalizeCdpUrl(cdpUrl);
|
|
1476
|
-
const cached = extensionRelayByCdpUrl.get(normalized);
|
|
1477
|
-
if (cached !== void 0) return cached;
|
|
1478
|
-
try {
|
|
1479
|
-
const version = await fetchJsonForCdp(appendCdpPath2(normalizeCdpHttpBaseForJsonEndpoints(normalized), "/json/version"), 2e3);
|
|
1480
|
-
const isRelay = String(version?.Browser ?? "").trim() === OPENCLAW_EXTENSION_RELAY_BROWSER;
|
|
1481
|
-
extensionRelayByCdpUrl.set(normalized, isRelay);
|
|
1482
|
-
return isRelay;
|
|
1483
|
-
} catch {
|
|
1484
|
-
extensionRelayByCdpUrl.set(normalized, false);
|
|
1485
|
-
return false;
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
1521
|
async function withPlaywrightPageCdpSession(page, fn) {
|
|
1489
1522
|
const session = await page.context().newCDPSession(page);
|
|
1490
1523
|
try {
|
|
@@ -1501,7 +1534,7 @@ async function withPageScopedCdpClient(opts) {
|
|
|
1501
1534
|
}
|
|
1502
1535
|
var LOOPBACK_ENTRIES = "localhost,127.0.0.1,[::1]";
|
|
1503
1536
|
function noProxyAlreadyCoversLocalhost() {
|
|
1504
|
-
const current = process.env.NO_PROXY
|
|
1537
|
+
const current = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
|
|
1505
1538
|
return current.includes("localhost") && current.includes("127.0.0.1") && current.includes("[::1]");
|
|
1506
1539
|
}
|
|
1507
1540
|
function isLoopbackCdpUrl(url) {
|
|
@@ -1519,7 +1552,7 @@ var NoProxyLeaseManager = class {
|
|
|
1519
1552
|
if (this.leaseCount === 0 && !noProxyAlreadyCoversLocalhost()) {
|
|
1520
1553
|
const noProxy = process.env.NO_PROXY;
|
|
1521
1554
|
const noProxyLower = process.env.no_proxy;
|
|
1522
|
-
const current = noProxy
|
|
1555
|
+
const current = noProxy ?? noProxyLower ?? "";
|
|
1523
1556
|
const applied = current ? `${current},${LOOPBACK_ENTRIES}` : LOOPBACK_ENTRIES;
|
|
1524
1557
|
process.env.NO_PROXY = applied;
|
|
1525
1558
|
process.env.no_proxy = applied;
|
|
@@ -1564,9 +1597,12 @@ function getHeadersWithAuth(endpoint, baseHeaders = {}) {
|
|
|
1564
1597
|
const headers = { ...baseHeaders };
|
|
1565
1598
|
try {
|
|
1566
1599
|
const parsed = new URL(endpoint);
|
|
1567
|
-
if (
|
|
1568
|
-
|
|
1569
|
-
|
|
1600
|
+
if (Object.keys(headers).some((k) => k.toLowerCase() === "authorization")) return headers;
|
|
1601
|
+
if (parsed.username || parsed.password) {
|
|
1602
|
+
const credentials = Buffer.from(
|
|
1603
|
+
`${decodeURIComponent(parsed.username)}:${decodeURIComponent(parsed.password)}`
|
|
1604
|
+
).toString("base64");
|
|
1605
|
+
headers.Authorization = `Basic ${credentials}`;
|
|
1570
1606
|
}
|
|
1571
1607
|
} catch {
|
|
1572
1608
|
}
|
|
@@ -1614,7 +1650,7 @@ function roleRefsKey(cdpUrl, targetId) {
|
|
|
1614
1650
|
function findNetworkRequestById(state, id) {
|
|
1615
1651
|
for (let i = state.requests.length - 1; i >= 0; i--) {
|
|
1616
1652
|
const candidate = state.requests[i];
|
|
1617
|
-
if (candidate
|
|
1653
|
+
if (candidate.id === id) return candidate;
|
|
1618
1654
|
}
|
|
1619
1655
|
return void 0;
|
|
1620
1656
|
}
|
|
@@ -1645,16 +1681,16 @@ function ensurePageState(page) {
|
|
|
1645
1681
|
});
|
|
1646
1682
|
page.on("pageerror", (err) => {
|
|
1647
1683
|
state.errors.push({
|
|
1648
|
-
message: err
|
|
1649
|
-
name: err
|
|
1650
|
-
stack: err
|
|
1684
|
+
message: err.message !== "" ? err.message : String(err),
|
|
1685
|
+
name: err.name !== "" ? err.name : void 0,
|
|
1686
|
+
stack: err.stack !== void 0 && err.stack !== "" ? err.stack : void 0,
|
|
1651
1687
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1652
1688
|
});
|
|
1653
1689
|
if (state.errors.length > MAX_PAGE_ERRORS) state.errors.shift();
|
|
1654
1690
|
});
|
|
1655
1691
|
page.on("request", (req) => {
|
|
1656
1692
|
state.nextRequestId += 1;
|
|
1657
|
-
const id = `r${state.nextRequestId}`;
|
|
1693
|
+
const id = `r${String(state.nextRequestId)}`;
|
|
1658
1694
|
state.requestIds.set(req, id);
|
|
1659
1695
|
state.requests.push({
|
|
1660
1696
|
id,
|
|
@@ -1668,7 +1704,7 @@ function ensurePageState(page) {
|
|
|
1668
1704
|
page.on("response", (resp) => {
|
|
1669
1705
|
const req = resp.request();
|
|
1670
1706
|
const id = state.requestIds.get(req);
|
|
1671
|
-
if (
|
|
1707
|
+
if (id === void 0) return;
|
|
1672
1708
|
const rec = findNetworkRequestById(state, id);
|
|
1673
1709
|
if (rec) {
|
|
1674
1710
|
rec.status = resp.status();
|
|
@@ -1677,7 +1713,7 @@ function ensurePageState(page) {
|
|
|
1677
1713
|
});
|
|
1678
1714
|
page.on("requestfailed", (req) => {
|
|
1679
1715
|
const id = state.requestIds.get(req);
|
|
1680
|
-
if (
|
|
1716
|
+
if (id === void 0) return;
|
|
1681
1717
|
const rec = findNetworkRequestById(state, id);
|
|
1682
1718
|
if (rec) {
|
|
1683
1719
|
rec.failureText = req.failure()?.errorText;
|
|
@@ -1694,7 +1730,8 @@ function ensurePageState(page) {
|
|
|
1694
1730
|
var STEALTH_SCRIPT = `Object.defineProperty(navigator, 'webdriver', { get: () => undefined })`;
|
|
1695
1731
|
function applyStealthToPage(page) {
|
|
1696
1732
|
page.evaluate(STEALTH_SCRIPT).catch((e) => {
|
|
1697
|
-
if (process.env.DEBUG
|
|
1733
|
+
if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
|
|
1734
|
+
console.warn("[browserclaw] stealth evaluate failed:", e instanceof Error ? e.message : String(e));
|
|
1698
1735
|
});
|
|
1699
1736
|
}
|
|
1700
1737
|
function observeContext(context) {
|
|
@@ -1702,7 +1739,8 @@ function observeContext(context) {
|
|
|
1702
1739
|
observedContexts.add(context);
|
|
1703
1740
|
ensureContextState(context);
|
|
1704
1741
|
context.addInitScript(STEALTH_SCRIPT).catch((e) => {
|
|
1705
|
-
if (process.env.DEBUG
|
|
1742
|
+
if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
|
|
1743
|
+
console.warn("[browserclaw] stealth initScript failed:", e instanceof Error ? e.message : String(e));
|
|
1706
1744
|
});
|
|
1707
1745
|
for (const page of context.pages()) {
|
|
1708
1746
|
ensurePageState(page);
|
|
@@ -1718,15 +1756,15 @@ function observeBrowser(browser) {
|
|
|
1718
1756
|
}
|
|
1719
1757
|
function rememberRoleRefsForTarget(opts) {
|
|
1720
1758
|
const targetId = opts.targetId.trim();
|
|
1721
|
-
if (
|
|
1759
|
+
if (targetId === "") return;
|
|
1722
1760
|
roleRefsByTarget.set(roleRefsKey(opts.cdpUrl, targetId), {
|
|
1723
1761
|
refs: opts.refs,
|
|
1724
|
-
...opts.frameSelector ? { frameSelector: opts.frameSelector } : {},
|
|
1725
|
-
...opts.mode ? { mode: opts.mode } : {}
|
|
1762
|
+
...opts.frameSelector !== void 0 && opts.frameSelector !== "" ? { frameSelector: opts.frameSelector } : {},
|
|
1763
|
+
...opts.mode !== void 0 ? { mode: opts.mode } : {}
|
|
1726
1764
|
});
|
|
1727
1765
|
while (roleRefsByTarget.size > MAX_ROLE_REFS_CACHE) {
|
|
1728
1766
|
const first = roleRefsByTarget.keys().next();
|
|
1729
|
-
if (first.done) break;
|
|
1767
|
+
if (first.done === true) break;
|
|
1730
1768
|
roleRefsByTarget.delete(first.value);
|
|
1731
1769
|
}
|
|
1732
1770
|
}
|
|
@@ -1735,7 +1773,7 @@ function storeRoleRefsForTarget(opts) {
|
|
|
1735
1773
|
state.roleRefs = opts.refs;
|
|
1736
1774
|
state.roleRefsFrameSelector = opts.frameSelector;
|
|
1737
1775
|
state.roleRefsMode = opts.mode;
|
|
1738
|
-
if (
|
|
1776
|
+
if (opts.targetId === void 0 || opts.targetId.trim() === "") return;
|
|
1739
1777
|
rememberRoleRefsForTarget({
|
|
1740
1778
|
cdpUrl: opts.cdpUrl,
|
|
1741
1779
|
targetId: opts.targetId,
|
|
@@ -1745,8 +1783,8 @@ function storeRoleRefsForTarget(opts) {
|
|
|
1745
1783
|
});
|
|
1746
1784
|
}
|
|
1747
1785
|
function restoreRoleRefsForTarget(opts) {
|
|
1748
|
-
const targetId = opts.targetId?.trim()
|
|
1749
|
-
if (
|
|
1786
|
+
const targetId = opts.targetId?.trim() ?? "";
|
|
1787
|
+
if (targetId === "") return;
|
|
1750
1788
|
const entry = roleRefsByTarget.get(roleRefsKey(opts.cdpUrl, targetId));
|
|
1751
1789
|
if (!entry) return;
|
|
1752
1790
|
const state = ensurePageState(opts.page);
|
|
@@ -1768,8 +1806,12 @@ async function connectBrowser(cdpUrl, authToken) {
|
|
|
1768
1806
|
const timeout = 5e3 + attempt * 2e3;
|
|
1769
1807
|
const endpoint = await getChromeWebSocketUrl(normalized, timeout, authToken).catch(() => null) ?? normalized;
|
|
1770
1808
|
const headers = getHeadersWithAuth(endpoint);
|
|
1771
|
-
if (authToken &&
|
|
1772
|
-
|
|
1809
|
+
if (authToken !== void 0 && authToken !== "" && !headers.Authorization)
|
|
1810
|
+
headers.Authorization = `Bearer ${authToken}`;
|
|
1811
|
+
const browser = await withNoProxyForCdpUrl(
|
|
1812
|
+
endpoint,
|
|
1813
|
+
() => playwrightCore.chromium.connectOverCDP(endpoint, { timeout, headers })
|
|
1814
|
+
);
|
|
1773
1815
|
const onDisconnected = () => {
|
|
1774
1816
|
if (cachedByCdpUrl.get(normalized)?.browser === browser) {
|
|
1775
1817
|
cachedByCdpUrl.delete(normalized);
|
|
@@ -1823,7 +1865,9 @@ function cdpSocketNeedsAttach(wsUrl) {
|
|
|
1823
1865
|
async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
1824
1866
|
const httpBase = normalizeCdpHttpBaseForJsonEndpoints(cdpUrl);
|
|
1825
1867
|
const ctrl = new AbortController();
|
|
1826
|
-
const t = setTimeout(() =>
|
|
1868
|
+
const t = setTimeout(() => {
|
|
1869
|
+
ctrl.abort();
|
|
1870
|
+
}, 2e3);
|
|
1827
1871
|
let targets;
|
|
1828
1872
|
try {
|
|
1829
1873
|
const res = await fetch(`${httpBase}/json/list`, { signal: ctrl.signal });
|
|
@@ -1835,9 +1879,12 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
|
1835
1879
|
clearTimeout(t);
|
|
1836
1880
|
}
|
|
1837
1881
|
if (!Array.isArray(targets)) return;
|
|
1838
|
-
const target = targets.find((entry) =>
|
|
1839
|
-
|
|
1840
|
-
|
|
1882
|
+
const target = targets.find((entry) => {
|
|
1883
|
+
const e = entry;
|
|
1884
|
+
return (e.id ?? "").trim() === targetId;
|
|
1885
|
+
});
|
|
1886
|
+
const wsUrlRaw = (target?.webSocketDebuggerUrl ?? "").trim();
|
|
1887
|
+
if (wsUrlRaw === "") return;
|
|
1841
1888
|
const wsUrl = normalizeCdpWsUrl(wsUrlRaw, httpBase);
|
|
1842
1889
|
const needsAttach = cdpSocketNeedsAttach(wsUrl);
|
|
1843
1890
|
await new Promise((resolve2) => {
|
|
@@ -1873,10 +1920,18 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
|
1873
1920
|
if (!needsAttach) return;
|
|
1874
1921
|
try {
|
|
1875
1922
|
const msg = JSON.parse(String(event.data));
|
|
1876
|
-
|
|
1877
|
-
|
|
1923
|
+
const result = msg.result;
|
|
1924
|
+
if (msg.id !== void 0 && result?.sessionId !== void 0) {
|
|
1925
|
+
const sessionId = result.sessionId;
|
|
1926
|
+
ws.send(JSON.stringify({ id: nextId++, sessionId, method: "Runtime.terminateExecution" }));
|
|
1878
1927
|
try {
|
|
1879
|
-
ws.send(
|
|
1928
|
+
ws.send(
|
|
1929
|
+
JSON.stringify({
|
|
1930
|
+
id: nextId++,
|
|
1931
|
+
method: "Target.detachFromTarget",
|
|
1932
|
+
params: { sessionId }
|
|
1933
|
+
})
|
|
1934
|
+
);
|
|
1880
1935
|
} catch {
|
|
1881
1936
|
}
|
|
1882
1937
|
setTimeout(finish, 300);
|
|
@@ -1884,8 +1939,12 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
|
1884
1939
|
} catch {
|
|
1885
1940
|
}
|
|
1886
1941
|
};
|
|
1887
|
-
ws.onerror = () =>
|
|
1888
|
-
|
|
1942
|
+
ws.onerror = () => {
|
|
1943
|
+
finish();
|
|
1944
|
+
};
|
|
1945
|
+
ws.onclose = () => {
|
|
1946
|
+
finish();
|
|
1947
|
+
};
|
|
1889
1948
|
});
|
|
1890
1949
|
}
|
|
1891
1950
|
async function forceDisconnectPlaywrightForTarget(opts) {
|
|
@@ -1897,15 +1956,15 @@ async function forceDisconnectPlaywrightForTarget(opts) {
|
|
|
1897
1956
|
if (cur.onDisconnected && typeof cur.browser.off === "function") {
|
|
1898
1957
|
cur.browser.off("disconnected", cur.onDisconnected);
|
|
1899
1958
|
}
|
|
1900
|
-
const targetId = opts.targetId?.trim()
|
|
1901
|
-
if (targetId) {
|
|
1959
|
+
const targetId = opts.targetId?.trim() ?? "";
|
|
1960
|
+
if (targetId !== "") {
|
|
1902
1961
|
await tryTerminateExecutionViaCdp(normalized, targetId).catch(() => {
|
|
1903
1962
|
});
|
|
1904
1963
|
}
|
|
1905
1964
|
cur.browser.close().catch(() => {
|
|
1906
1965
|
});
|
|
1907
1966
|
}
|
|
1908
|
-
|
|
1967
|
+
function getAllPages(browser) {
|
|
1909
1968
|
return browser.contexts().flatMap((c) => c.pages());
|
|
1910
1969
|
}
|
|
1911
1970
|
async function pageTargetId(page) {
|
|
@@ -1913,7 +1972,7 @@ async function pageTargetId(page) {
|
|
|
1913
1972
|
try {
|
|
1914
1973
|
const info = await session.send("Target.getTargetInfo");
|
|
1915
1974
|
const targetInfo = info.targetInfo;
|
|
1916
|
-
return
|
|
1975
|
+
return (targetInfo?.targetId ?? "").trim() || null;
|
|
1917
1976
|
} finally {
|
|
1918
1977
|
await session.detach().catch(() => {
|
|
1919
1978
|
});
|
|
@@ -1934,21 +1993,15 @@ function matchPageByTargetList(pages, targets, targetId) {
|
|
|
1934
1993
|
return null;
|
|
1935
1994
|
}
|
|
1936
1995
|
async function findPageByTargetIdViaTargetList(pages, targetId, cdpUrl) {
|
|
1937
|
-
const targets = await fetchJsonForCdp(
|
|
1996
|
+
const targets = await fetchJsonForCdp(
|
|
1997
|
+
appendCdpPath2(normalizeCdpHttpBaseForJsonEndpoints(cdpUrl), "/json/list"),
|
|
1998
|
+
2e3
|
|
1999
|
+
);
|
|
1938
2000
|
if (!Array.isArray(targets)) return null;
|
|
1939
2001
|
return matchPageByTargetList(pages, targets, targetId);
|
|
1940
2002
|
}
|
|
1941
2003
|
async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
1942
|
-
const pages =
|
|
1943
|
-
const isExtensionRelay = cdpUrl ? await isExtensionRelayCdpEndpoint(cdpUrl).catch(() => false) : false;
|
|
1944
|
-
if (cdpUrl && isExtensionRelay) {
|
|
1945
|
-
try {
|
|
1946
|
-
const matched = await findPageByTargetIdViaTargetList(pages, targetId, cdpUrl);
|
|
1947
|
-
if (matched) return matched;
|
|
1948
|
-
} catch {
|
|
1949
|
-
}
|
|
1950
|
-
return pages.length === 1 ? pages[0] ?? null : null;
|
|
1951
|
-
}
|
|
2004
|
+
const pages = getAllPages(browser);
|
|
1952
2005
|
let resolvedViaCdp = false;
|
|
1953
2006
|
for (const page of pages) {
|
|
1954
2007
|
let tid = null;
|
|
@@ -1958,9 +2011,9 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
|
1958
2011
|
} catch {
|
|
1959
2012
|
tid = null;
|
|
1960
2013
|
}
|
|
1961
|
-
if (tid && tid === targetId) return page;
|
|
2014
|
+
if (tid !== null && tid !== "" && tid === targetId) return page;
|
|
1962
2015
|
}
|
|
1963
|
-
if (cdpUrl) {
|
|
2016
|
+
if (cdpUrl !== void 0 && cdpUrl !== "") {
|
|
1964
2017
|
try {
|
|
1965
2018
|
return await findPageByTargetIdViaTargetList(pages, targetId, cdpUrl);
|
|
1966
2019
|
} catch {
|
|
@@ -1971,14 +2024,16 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
|
1971
2024
|
}
|
|
1972
2025
|
async function getPageForTargetId(opts) {
|
|
1973
2026
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
1974
|
-
const pages =
|
|
2027
|
+
const pages = getAllPages(browser);
|
|
1975
2028
|
if (!pages.length) throw new Error("No pages available in the connected browser.");
|
|
1976
2029
|
const first = pages[0];
|
|
1977
|
-
if (
|
|
2030
|
+
if (opts.targetId === void 0 || opts.targetId === "") return first;
|
|
1978
2031
|
const found = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
|
|
1979
2032
|
if (!found) {
|
|
1980
2033
|
if (pages.length === 1) return first;
|
|
1981
|
-
throw new BrowserTabNotFoundError(
|
|
2034
|
+
throw new BrowserTabNotFoundError(
|
|
2035
|
+
`Tab not found (targetId: ${opts.targetId}). Call browser.tabs() to list open tabs.`
|
|
2036
|
+
);
|
|
1982
2037
|
}
|
|
1983
2038
|
return found;
|
|
1984
2039
|
}
|
|
@@ -2012,7 +2067,7 @@ function resolveInteractionTimeoutMs(timeoutMs) {
|
|
|
2012
2067
|
function resolveBoundedDelayMs(value, label, maxMs) {
|
|
2013
2068
|
const normalized = Math.floor(value ?? 0);
|
|
2014
2069
|
if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
|
|
2015
|
-
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${maxMs}ms`);
|
|
2070
|
+
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${String(maxMs)}ms`);
|
|
2016
2071
|
return normalized;
|
|
2017
2072
|
}
|
|
2018
2073
|
async function getRestoredPageForTarget(opts) {
|
|
@@ -2023,17 +2078,17 @@ async function getRestoredPageForTarget(opts) {
|
|
|
2023
2078
|
}
|
|
2024
2079
|
function refLocator(page, ref) {
|
|
2025
2080
|
const normalized = ref.startsWith("@") ? ref.slice(1) : ref.startsWith("ref=") ? ref.slice(4) : ref;
|
|
2026
|
-
if (
|
|
2081
|
+
if (normalized.trim() === "") throw new Error("ref is required");
|
|
2027
2082
|
if (/^e\d+$/.test(normalized)) {
|
|
2028
2083
|
const state = pageStates.get(page);
|
|
2029
2084
|
if (state?.roleRefsMode === "aria") {
|
|
2030
|
-
return (state.roleRefsFrameSelector ? page.frameLocator(state.roleRefsFrameSelector) : page).locator(`aria-ref=${normalized}`);
|
|
2085
|
+
return (state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page).locator(`aria-ref=${normalized}`);
|
|
2031
2086
|
}
|
|
2032
2087
|
const info = state?.roleRefs?.[normalized];
|
|
2033
2088
|
if (!info) throw new Error(`Unknown ref "${normalized}". Run a new snapshot and use a ref from that snapshot.`);
|
|
2034
|
-
const locAny = state
|
|
2089
|
+
const locAny = state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page;
|
|
2035
2090
|
const role = info.role;
|
|
2036
|
-
const locator = info.name ? locAny.getByRole(role, { name: info.name, exact: true }) : locAny.getByRole(role);
|
|
2091
|
+
const locator = info.name !== void 0 && info.name !== "" ? locAny.getByRole(role, { name: info.name, exact: true }) : locAny.getByRole(role);
|
|
2037
2092
|
return info.nth !== void 0 ? locator.nth(info.nth) : locator;
|
|
2038
2093
|
}
|
|
2039
2094
|
return page.locator(`aria-ref=${normalized}`);
|
|
@@ -2041,15 +2096,21 @@ function refLocator(page, ref) {
|
|
|
2041
2096
|
function toAIFriendlyError(error, selector) {
|
|
2042
2097
|
const message = error instanceof Error ? error.message : String(error);
|
|
2043
2098
|
if (message.includes("strict mode violation")) {
|
|
2044
|
-
const countMatch =
|
|
2099
|
+
const countMatch = /resolved to (\d+) elements/.exec(message);
|
|
2045
2100
|
const count = countMatch ? countMatch[1] : "multiple";
|
|
2046
|
-
return new Error(
|
|
2101
|
+
return new Error(
|
|
2102
|
+
`Selector "${selector}" matched ${count} elements. Run a new snapshot to get updated refs, or use a different ref.`
|
|
2103
|
+
);
|
|
2047
2104
|
}
|
|
2048
2105
|
if ((message.includes("Timeout") || message.includes("waiting for")) && (message.includes("to be visible") || message.includes("not visible"))) {
|
|
2049
|
-
return new Error(
|
|
2106
|
+
return new Error(
|
|
2107
|
+
`Element "${selector}" not found or not visible. Run a new snapshot to see current page elements.`
|
|
2108
|
+
);
|
|
2050
2109
|
}
|
|
2051
2110
|
if (message.includes("intercepts pointer events") || message.includes("not visible") || message.includes("not receive pointer events")) {
|
|
2052
|
-
return new Error(
|
|
2111
|
+
return new Error(
|
|
2112
|
+
`Element "${selector}" is not interactable (hidden or covered). Try scrolling it into view, closing overlays, or re-snapshotting.`
|
|
2113
|
+
);
|
|
2053
2114
|
}
|
|
2054
2115
|
return error instanceof Error ? error : new Error(message);
|
|
2055
2116
|
}
|
|
@@ -2057,437 +2118,151 @@ function normalizeTimeoutMs(timeoutMs, fallback, maxMs = 12e4) {
|
|
|
2057
2118
|
return Math.max(500, Math.min(maxMs, timeoutMs ?? fallback));
|
|
2058
2119
|
}
|
|
2059
2120
|
|
|
2060
|
-
// src/
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
"
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
"columnheader",
|
|
2085
|
-
"rowheader",
|
|
2086
|
-
"listitem",
|
|
2087
|
-
"article",
|
|
2088
|
-
"region",
|
|
2089
|
-
"main",
|
|
2090
|
-
"navigation"
|
|
2091
|
-
]);
|
|
2092
|
-
var STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
|
|
2093
|
-
"generic",
|
|
2094
|
-
"group",
|
|
2095
|
-
"list",
|
|
2096
|
-
"table",
|
|
2097
|
-
"row",
|
|
2098
|
-
"rowgroup",
|
|
2099
|
-
"grid",
|
|
2100
|
-
"treegrid",
|
|
2101
|
-
"menu",
|
|
2102
|
-
"menubar",
|
|
2103
|
-
"toolbar",
|
|
2104
|
-
"tablist",
|
|
2105
|
-
"tree",
|
|
2106
|
-
"directory",
|
|
2107
|
-
"document",
|
|
2108
|
-
"application",
|
|
2109
|
-
"presentation",
|
|
2110
|
-
"none"
|
|
2111
|
-
]);
|
|
2112
|
-
function getIndentLevel(line) {
|
|
2113
|
-
const match = line.match(/^(\s*)/);
|
|
2114
|
-
return match ? Math.floor(match[1].length / 2) : 0;
|
|
2115
|
-
}
|
|
2116
|
-
function matchInteractiveSnapshotLine(line, options) {
|
|
2117
|
-
const depth = getIndentLevel(line);
|
|
2118
|
-
if (options.maxDepth !== void 0 && depth > options.maxDepth) {
|
|
2119
|
-
return null;
|
|
2120
|
-
}
|
|
2121
|
-
const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
|
|
2122
|
-
if (!match) {
|
|
2123
|
-
return null;
|
|
2124
|
-
}
|
|
2125
|
-
const [, , roleRaw, name, suffix] = match;
|
|
2126
|
-
if (roleRaw.startsWith("/")) {
|
|
2127
|
-
return null;
|
|
2128
|
-
}
|
|
2129
|
-
const role = roleRaw.toLowerCase();
|
|
2130
|
-
return {
|
|
2131
|
-
roleRaw,
|
|
2132
|
-
role,
|
|
2133
|
-
...name ? { name } : {},
|
|
2134
|
-
suffix
|
|
2135
|
-
};
|
|
2136
|
-
}
|
|
2137
|
-
function createRoleNameTracker() {
|
|
2138
|
-
const counts = /* @__PURE__ */ new Map();
|
|
2139
|
-
const refsByKey = /* @__PURE__ */ new Map();
|
|
2140
|
-
return {
|
|
2141
|
-
counts,
|
|
2142
|
-
refsByKey,
|
|
2143
|
-
getKey(role, name) {
|
|
2144
|
-
return `${role}:${name ?? ""}`;
|
|
2145
|
-
},
|
|
2146
|
-
getNextIndex(role, name) {
|
|
2147
|
-
const key = this.getKey(role, name);
|
|
2148
|
-
const current = counts.get(key) ?? 0;
|
|
2149
|
-
counts.set(key, current + 1);
|
|
2150
|
-
return current;
|
|
2151
|
-
},
|
|
2152
|
-
trackRef(role, name, ref) {
|
|
2153
|
-
const key = this.getKey(role, name);
|
|
2154
|
-
const list = refsByKey.get(key) ?? [];
|
|
2155
|
-
list.push(ref);
|
|
2156
|
-
refsByKey.set(key, list);
|
|
2157
|
-
},
|
|
2158
|
-
getDuplicateKeys() {
|
|
2159
|
-
const out = /* @__PURE__ */ new Set();
|
|
2160
|
-
for (const [key, refs] of refsByKey) if (refs.length > 1) out.add(key);
|
|
2161
|
-
return out;
|
|
2121
|
+
// src/actions/evaluate.ts
|
|
2122
|
+
async function evaluateInAllFramesViaPlaywright(opts) {
|
|
2123
|
+
const fnText = opts.fn.trim();
|
|
2124
|
+
if (!fnText) throw new Error("function is required");
|
|
2125
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2126
|
+
const frames = page.frames();
|
|
2127
|
+
const results = [];
|
|
2128
|
+
for (const frame of frames) {
|
|
2129
|
+
try {
|
|
2130
|
+
const result = await frame.evaluate((fnBody) => {
|
|
2131
|
+
"use strict";
|
|
2132
|
+
try {
|
|
2133
|
+
const candidate = (0, eval)("(" + fnBody + ")");
|
|
2134
|
+
return typeof candidate === "function" ? candidate() : candidate;
|
|
2135
|
+
} catch (err) {
|
|
2136
|
+
throw new Error("Invalid evaluate function: " + (err instanceof Error ? err.message : String(err)));
|
|
2137
|
+
}
|
|
2138
|
+
}, fnText);
|
|
2139
|
+
results.push({
|
|
2140
|
+
frameUrl: frame.url(),
|
|
2141
|
+
frameName: frame.name(),
|
|
2142
|
+
result
|
|
2143
|
+
});
|
|
2144
|
+
} catch {
|
|
2162
2145
|
}
|
|
2163
|
-
}
|
|
2146
|
+
}
|
|
2147
|
+
return results;
|
|
2164
2148
|
}
|
|
2165
|
-
function
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2149
|
+
async function awaitEvalWithAbort(evalPromise, abortPromise) {
|
|
2150
|
+
if (!abortPromise) return await evalPromise;
|
|
2151
|
+
try {
|
|
2152
|
+
return await Promise.race([evalPromise, abortPromise]);
|
|
2153
|
+
} catch (err) {
|
|
2154
|
+
evalPromise.catch(() => {
|
|
2155
|
+
});
|
|
2156
|
+
throw err;
|
|
2170
2157
|
}
|
|
2171
2158
|
}
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2159
|
+
var BROWSER_EVALUATOR = new Function(
|
|
2160
|
+
"args",
|
|
2161
|
+
`
|
|
2162
|
+
"use strict";
|
|
2163
|
+
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
2164
|
+
try {
|
|
2165
|
+
var candidate = eval("(" + fnBody + ")");
|
|
2166
|
+
var result = typeof candidate === "function" ? candidate() : candidate;
|
|
2167
|
+
if (result && typeof result.then === "function") {
|
|
2168
|
+
return Promise.race([
|
|
2169
|
+
result,
|
|
2170
|
+
new Promise(function(_, reject) {
|
|
2171
|
+
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
2172
|
+
})
|
|
2173
|
+
]);
|
|
2184
2174
|
}
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2175
|
+
return result;
|
|
2176
|
+
} catch (err) {
|
|
2177
|
+
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
2178
|
+
}
|
|
2179
|
+
`
|
|
2180
|
+
);
|
|
2181
|
+
var ELEMENT_EVALUATOR = new Function(
|
|
2182
|
+
"el",
|
|
2183
|
+
"args",
|
|
2184
|
+
`
|
|
2185
|
+
"use strict";
|
|
2186
|
+
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
2187
|
+
try {
|
|
2188
|
+
var candidate = eval("(" + fnBody + ")");
|
|
2189
|
+
var result = typeof candidate === "function" ? candidate(el) : candidate;
|
|
2190
|
+
if (result && typeof result.then === "function") {
|
|
2191
|
+
return Promise.race([
|
|
2192
|
+
result,
|
|
2193
|
+
new Promise(function(_, reject) {
|
|
2194
|
+
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
2195
|
+
})
|
|
2196
|
+
]);
|
|
2193
2197
|
}
|
|
2194
|
-
|
|
2198
|
+
return result;
|
|
2199
|
+
} catch (err) {
|
|
2200
|
+
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
2195
2201
|
}
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
function
|
|
2199
|
-
const
|
|
2200
|
-
|
|
2201
|
-
const tracker = createRoleNameTracker();
|
|
2202
|
-
let counter = 0;
|
|
2203
|
-
const nextRef = () => {
|
|
2204
|
-
counter++;
|
|
2205
|
-
return `e${counter}`;
|
|
2206
|
-
};
|
|
2207
|
-
if (options.interactive) {
|
|
2208
|
-
const result2 = [];
|
|
2209
|
-
for (const line of lines) {
|
|
2210
|
-
const parsed = matchInteractiveSnapshotLine(line, options);
|
|
2211
|
-
if (!parsed) continue;
|
|
2212
|
-
const { roleRaw, role, name, suffix } = parsed;
|
|
2213
|
-
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
2214
|
-
const prefix = line.match(/^(\s*-\s*)/)?.[1] ?? "";
|
|
2215
|
-
const ref = nextRef();
|
|
2216
|
-
const nth = tracker.getNextIndex(role, name);
|
|
2217
|
-
tracker.trackRef(role, name, ref);
|
|
2218
|
-
refs[ref] = { role, name, nth };
|
|
2219
|
-
let enhanced = `${prefix}${roleRaw}`;
|
|
2220
|
-
if (name) enhanced += ` "${name}"`;
|
|
2221
|
-
enhanced += ` [ref=${ref}]`;
|
|
2222
|
-
if (nth > 0) enhanced += ` [nth=${nth}]`;
|
|
2223
|
-
if (suffix.includes("[")) enhanced += suffix;
|
|
2224
|
-
result2.push(enhanced);
|
|
2225
|
-
}
|
|
2226
|
-
removeNthFromNonDuplicates(refs, tracker);
|
|
2227
|
-
return { snapshot: result2.join("\n") || "(no interactive elements)", refs };
|
|
2228
|
-
}
|
|
2229
|
-
const result = [];
|
|
2230
|
-
for (const line of lines) {
|
|
2231
|
-
const depth = getIndentLevel(line);
|
|
2232
|
-
if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
|
|
2233
|
-
const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
|
|
2234
|
-
if (!match) {
|
|
2235
|
-
result.push(line);
|
|
2236
|
-
continue;
|
|
2237
|
-
}
|
|
2238
|
-
const [, prefix, roleRaw, name, suffix] = match;
|
|
2239
|
-
if (roleRaw.startsWith("/")) {
|
|
2240
|
-
result.push(line);
|
|
2241
|
-
continue;
|
|
2242
|
-
}
|
|
2243
|
-
const role = roleRaw.toLowerCase();
|
|
2244
|
-
const isInteractive = INTERACTIVE_ROLES.has(role);
|
|
2245
|
-
const isContent = CONTENT_ROLES.has(role);
|
|
2246
|
-
const isStructural = STRUCTURAL_ROLES.has(role);
|
|
2247
|
-
if (options.compact && isStructural && !name) continue;
|
|
2248
|
-
if (!(isInteractive || isContent && name)) {
|
|
2249
|
-
result.push(line);
|
|
2250
|
-
continue;
|
|
2251
|
-
}
|
|
2252
|
-
const ref = nextRef();
|
|
2253
|
-
const nth = tracker.getNextIndex(role, name);
|
|
2254
|
-
tracker.trackRef(role, name, ref);
|
|
2255
|
-
refs[ref] = { role, name, nth };
|
|
2256
|
-
let enhanced = `${prefix}${roleRaw}`;
|
|
2257
|
-
if (name) enhanced += ` "${name}"`;
|
|
2258
|
-
enhanced += ` [ref=${ref}]`;
|
|
2259
|
-
if (nth > 0) enhanced += ` [nth=${nth}]`;
|
|
2260
|
-
if (suffix) enhanced += suffix;
|
|
2261
|
-
result.push(enhanced);
|
|
2262
|
-
}
|
|
2263
|
-
removeNthFromNonDuplicates(refs, tracker);
|
|
2264
|
-
const tree = result.join("\n") || "(empty)";
|
|
2265
|
-
return { snapshot: options.compact ? compactTree(tree) : tree, refs };
|
|
2266
|
-
}
|
|
2267
|
-
function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
|
|
2268
|
-
const lines = String(aiSnapshot ?? "").split("\n");
|
|
2269
|
-
const refs = {};
|
|
2270
|
-
function parseAiSnapshotRef(suffix) {
|
|
2271
|
-
const match = suffix.match(/\[ref=(e\d+)\]/i);
|
|
2272
|
-
return match ? match[1] : null;
|
|
2273
|
-
}
|
|
2274
|
-
if (options.interactive) {
|
|
2275
|
-
const out2 = [];
|
|
2276
|
-
for (const line of lines) {
|
|
2277
|
-
const parsed = matchInteractiveSnapshotLine(line, options);
|
|
2278
|
-
if (!parsed) continue;
|
|
2279
|
-
const { roleRaw, role, name, suffix } = parsed;
|
|
2280
|
-
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
2281
|
-
const ref = parseAiSnapshotRef(suffix);
|
|
2282
|
-
if (!ref) continue;
|
|
2283
|
-
const prefix = line.match(/^(\s*-\s*)/)?.[1] ?? "";
|
|
2284
|
-
refs[ref] = { role, ...name ? { name } : {} };
|
|
2285
|
-
out2.push(`${prefix}${roleRaw}${name ? ` "${name}"` : ""}${suffix}`);
|
|
2286
|
-
}
|
|
2287
|
-
return { snapshot: out2.join("\n") || "(no interactive elements)", refs };
|
|
2288
|
-
}
|
|
2289
|
-
const out = [];
|
|
2290
|
-
for (const line of lines) {
|
|
2291
|
-
const depth = getIndentLevel(line);
|
|
2292
|
-
if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
|
|
2293
|
-
const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
|
|
2294
|
-
if (!match) {
|
|
2295
|
-
out.push(line);
|
|
2296
|
-
continue;
|
|
2297
|
-
}
|
|
2298
|
-
const [, , roleRaw, name, suffix] = match;
|
|
2299
|
-
if (roleRaw.startsWith("/")) {
|
|
2300
|
-
out.push(line);
|
|
2301
|
-
continue;
|
|
2302
|
-
}
|
|
2303
|
-
const role = roleRaw.toLowerCase();
|
|
2304
|
-
const isStructural = STRUCTURAL_ROLES.has(role);
|
|
2305
|
-
if (options.compact && isStructural && !name) continue;
|
|
2306
|
-
const ref = parseAiSnapshotRef(suffix);
|
|
2307
|
-
if (ref) refs[ref] = { role, ...name ? { name } : {} };
|
|
2308
|
-
out.push(line);
|
|
2309
|
-
}
|
|
2310
|
-
const tree = out.join("\n") || "(empty)";
|
|
2311
|
-
return { snapshot: options.compact ? compactTree(tree) : tree, refs };
|
|
2312
|
-
}
|
|
2313
|
-
function getRoleSnapshotStats(snapshot, refs) {
|
|
2314
|
-
const interactive = Object.values(refs).filter((r) => INTERACTIVE_ROLES.has(r.role)).length;
|
|
2315
|
-
return {
|
|
2316
|
-
lines: snapshot.split("\n").length,
|
|
2317
|
-
chars: snapshot.length,
|
|
2318
|
-
refs: Object.keys(refs).length,
|
|
2319
|
-
interactive
|
|
2320
|
-
};
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
|
-
// src/snapshot/ai-snapshot.ts
|
|
2324
|
-
async function snapshotAi(opts) {
|
|
2325
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2326
|
-
ensurePageState(page);
|
|
2327
|
-
const maybe = page;
|
|
2328
|
-
if (!maybe._snapshotForAI) {
|
|
2329
|
-
throw new Error("Playwright _snapshotForAI is not available. Upgrade playwright-core to >= 1.50.");
|
|
2330
|
-
}
|
|
2331
|
-
const sourceUrl = page.url();
|
|
2332
|
-
const result = await maybe._snapshotForAI({
|
|
2333
|
-
timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3, 6e4),
|
|
2334
|
-
track: "response"
|
|
2335
|
-
});
|
|
2336
|
-
let snapshot = String(result?.full ?? "");
|
|
2337
|
-
const maxChars = opts.maxChars;
|
|
2338
|
-
const limit = typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : void 0;
|
|
2339
|
-
let truncated = false;
|
|
2340
|
-
if (limit && snapshot.length > limit) {
|
|
2341
|
-
const lastNewline = snapshot.lastIndexOf("\n", limit);
|
|
2342
|
-
const cutoff = lastNewline > 0 ? lastNewline : limit;
|
|
2343
|
-
snapshot = `${snapshot.slice(0, cutoff)}
|
|
2344
|
-
|
|
2345
|
-
[...TRUNCATED - page too large]`;
|
|
2346
|
-
truncated = true;
|
|
2347
|
-
}
|
|
2348
|
-
const built = buildRoleSnapshotFromAiSnapshot(snapshot, opts.options);
|
|
2349
|
-
storeRoleRefsForTarget({
|
|
2350
|
-
page,
|
|
2351
|
-
cdpUrl: opts.cdpUrl,
|
|
2352
|
-
targetId: opts.targetId,
|
|
2353
|
-
refs: built.refs,
|
|
2354
|
-
mode: "aria"
|
|
2355
|
-
});
|
|
2356
|
-
return {
|
|
2357
|
-
snapshot: built.snapshot,
|
|
2358
|
-
refs: built.refs,
|
|
2359
|
-
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
2360
|
-
...truncated ? { truncated } : {},
|
|
2361
|
-
untrusted: true,
|
|
2362
|
-
contentMeta: {
|
|
2363
|
-
sourceUrl,
|
|
2364
|
-
contentType: "browser-snapshot",
|
|
2365
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2366
|
-
}
|
|
2367
|
-
};
|
|
2368
|
-
}
|
|
2369
|
-
|
|
2370
|
-
// src/snapshot/aria-snapshot.ts
|
|
2371
|
-
async function snapshotRole(opts) {
|
|
2202
|
+
`
|
|
2203
|
+
);
|
|
2204
|
+
async function evaluateViaPlaywright(opts) {
|
|
2205
|
+
const fnText = opts.fn.trim();
|
|
2206
|
+
if (!fnText) throw new Error("function is required");
|
|
2372
2207
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2373
2208
|
ensurePageState(page);
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
cdpUrl: opts.cdpUrl,
|
|
2388
|
-
targetId: opts.targetId,
|
|
2389
|
-
refs: built2.refs,
|
|
2390
|
-
mode: "aria"
|
|
2209
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
2210
|
+
const outerTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
|
|
2211
|
+
let evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 500));
|
|
2212
|
+
evaluateTimeout = Math.min(evaluateTimeout, outerTimeout);
|
|
2213
|
+
const signal = opts.signal;
|
|
2214
|
+
let abortListener;
|
|
2215
|
+
let abortReject;
|
|
2216
|
+
let abortPromise;
|
|
2217
|
+
if (signal !== void 0) {
|
|
2218
|
+
abortPromise = new Promise((_, reject) => {
|
|
2219
|
+
abortReject = reject;
|
|
2220
|
+
});
|
|
2221
|
+
abortPromise.catch(() => {
|
|
2391
2222
|
});
|
|
2392
|
-
return {
|
|
2393
|
-
snapshot: built2.snapshot,
|
|
2394
|
-
refs: built2.refs,
|
|
2395
|
-
stats: getRoleSnapshotStats(built2.snapshot, built2.refs),
|
|
2396
|
-
untrusted: true,
|
|
2397
|
-
contentMeta: {
|
|
2398
|
-
sourceUrl,
|
|
2399
|
-
contentType: "browser-snapshot",
|
|
2400
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2401
|
-
}
|
|
2402
|
-
};
|
|
2403
2223
|
}
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
frameSelector: frameSelector || void 0,
|
|
2415
|
-
mode: "role"
|
|
2416
|
-
});
|
|
2417
|
-
return {
|
|
2418
|
-
snapshot: built.snapshot,
|
|
2419
|
-
refs: built.refs,
|
|
2420
|
-
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
2421
|
-
untrusted: true,
|
|
2422
|
-
contentMeta: {
|
|
2423
|
-
sourceUrl,
|
|
2424
|
-
contentType: "browser-snapshot",
|
|
2425
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2224
|
+
if (signal !== void 0) {
|
|
2225
|
+
const disconnect = () => {
|
|
2226
|
+
forceDisconnectPlaywrightForTarget({
|
|
2227
|
+
cdpUrl: opts.cdpUrl,
|
|
2228
|
+
targetId: opts.targetId}).catch(() => {
|
|
2229
|
+
});
|
|
2230
|
+
};
|
|
2231
|
+
if (signal.aborted) {
|
|
2232
|
+
disconnect();
|
|
2233
|
+
throw signal.reason ?? new Error("aborted");
|
|
2426
2234
|
}
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
await session.send("Accessibility.enable").catch(() => {
|
|
2436
|
-
});
|
|
2437
|
-
return await session.send("Accessibility.getFullAXTree");
|
|
2438
|
-
});
|
|
2439
|
-
return {
|
|
2440
|
-
nodes: formatAriaNodes(Array.isArray(res?.nodes) ? res.nodes : [], limit),
|
|
2441
|
-
untrusted: true,
|
|
2442
|
-
contentMeta: {
|
|
2443
|
-
sourceUrl,
|
|
2444
|
-
contentType: "browser-aria-tree",
|
|
2445
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2235
|
+
abortListener = () => {
|
|
2236
|
+
disconnect();
|
|
2237
|
+
abortReject?.(signal.reason ?? new Error("aborted"));
|
|
2238
|
+
};
|
|
2239
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
2240
|
+
if (signal.aborted) {
|
|
2241
|
+
abortListener();
|
|
2242
|
+
throw signal.reason ?? new Error("aborted");
|
|
2446
2243
|
}
|
|
2447
|
-
}
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
}
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
for (const n of nodes) if (n.nodeId) byId.set(n.nodeId, n);
|
|
2459
|
-
const referenced = /* @__PURE__ */ new Set();
|
|
2460
|
-
for (const n of nodes) for (const c of n.childIds ?? []) referenced.add(c);
|
|
2461
|
-
const root = nodes.find((n) => n.nodeId && !referenced.has(n.nodeId)) ?? nodes[0];
|
|
2462
|
-
if (!root?.nodeId) return [];
|
|
2463
|
-
const out = [];
|
|
2464
|
-
const stack = [{ id: root.nodeId, depth: 0 }];
|
|
2465
|
-
while (stack.length && out.length < limit) {
|
|
2466
|
-
const popped = stack.pop();
|
|
2467
|
-
if (!popped) break;
|
|
2468
|
-
const { id, depth } = popped;
|
|
2469
|
-
const n = byId.get(id);
|
|
2470
|
-
if (!n) continue;
|
|
2471
|
-
const role = axValue(n.role);
|
|
2472
|
-
const name = axValue(n.name);
|
|
2473
|
-
const value = axValue(n.value);
|
|
2474
|
-
const description = axValue(n.description);
|
|
2475
|
-
const ref = `ax${out.length + 1}`;
|
|
2476
|
-
out.push({
|
|
2477
|
-
ref,
|
|
2478
|
-
role: role || "unknown",
|
|
2479
|
-
name: name || "",
|
|
2480
|
-
...value ? { value } : {},
|
|
2481
|
-
...description ? { description } : {},
|
|
2482
|
-
...typeof n.backendDOMNodeId === "number" ? { backendDOMNodeId: n.backendDOMNodeId } : {},
|
|
2483
|
-
depth
|
|
2484
|
-
});
|
|
2485
|
-
const children = (n.childIds ?? []).filter((c) => byId.has(c));
|
|
2486
|
-
for (let i = children.length - 1; i >= 0; i--) {
|
|
2487
|
-
if (children[i]) stack.push({ id: children[i], depth: depth + 1 });
|
|
2244
|
+
}
|
|
2245
|
+
try {
|
|
2246
|
+
if (opts.ref !== void 0 && opts.ref !== "") {
|
|
2247
|
+
const locator = refLocator(page, opts.ref);
|
|
2248
|
+
return await awaitEvalWithAbort(
|
|
2249
|
+
locator.evaluate(ELEMENT_EVALUATOR, {
|
|
2250
|
+
fnBody: fnText,
|
|
2251
|
+
timeoutMs: evaluateTimeout
|
|
2252
|
+
}),
|
|
2253
|
+
abortPromise
|
|
2254
|
+
);
|
|
2488
2255
|
}
|
|
2256
|
+
return await awaitEvalWithAbort(
|
|
2257
|
+
page.evaluate(BROWSER_EVALUATOR, {
|
|
2258
|
+
fnBody: fnText,
|
|
2259
|
+
timeoutMs: evaluateTimeout
|
|
2260
|
+
}),
|
|
2261
|
+
abortPromise
|
|
2262
|
+
);
|
|
2263
|
+
} finally {
|
|
2264
|
+
if (signal && abortListener) signal.removeEventListener("abort", abortListener);
|
|
2489
2265
|
}
|
|
2490
|
-
return out;
|
|
2491
2266
|
}
|
|
2492
2267
|
|
|
2493
2268
|
// src/security.ts
|
|
@@ -2504,11 +2279,7 @@ function withBrowserNavigationPolicy(ssrfPolicy) {
|
|
|
2504
2279
|
var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
2505
2280
|
var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
|
|
2506
2281
|
var PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"];
|
|
2507
|
-
var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
2508
|
-
"localhost",
|
|
2509
|
-
"localhost.localdomain",
|
|
2510
|
-
"metadata.google.internal"
|
|
2511
|
-
]);
|
|
2282
|
+
var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "localhost.localdomain", "metadata.google.internal"]);
|
|
2512
2283
|
function isAllowedNonNetworkNavigationUrl(parsed) {
|
|
2513
2284
|
return SAFE_NON_NETWORK_URLS.has(parsed.href);
|
|
2514
2285
|
}
|
|
@@ -2523,7 +2294,7 @@ function hasProxyEnvConfigured2(env = process.env) {
|
|
|
2523
2294
|
return false;
|
|
2524
2295
|
}
|
|
2525
2296
|
function normalizeHostname(hostname) {
|
|
2526
|
-
let h =
|
|
2297
|
+
let h = hostname.trim().toLowerCase();
|
|
2527
2298
|
if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
|
|
2528
2299
|
if (h.endsWith(".")) h = h.slice(0, -1);
|
|
2529
2300
|
return h;
|
|
@@ -2542,13 +2313,7 @@ var BLOCKED_IPV4_RANGES = /* @__PURE__ */ new Set([
|
|
|
2542
2313
|
"private",
|
|
2543
2314
|
"reserved"
|
|
2544
2315
|
]);
|
|
2545
|
-
var BLOCKED_IPV6_RANGES = /* @__PURE__ */ new Set([
|
|
2546
|
-
"unspecified",
|
|
2547
|
-
"loopback",
|
|
2548
|
-
"linkLocal",
|
|
2549
|
-
"uniqueLocal",
|
|
2550
|
-
"multicast"
|
|
2551
|
-
]);
|
|
2316
|
+
var BLOCKED_IPV6_RANGES = /* @__PURE__ */ new Set(["unspecified", "loopback", "linkLocal", "uniqueLocal", "multicast"]);
|
|
2552
2317
|
var RFC2544_BENCHMARK_PREFIX = [ipaddr.IPv4.parse("198.18.0.0"), 15];
|
|
2553
2318
|
var EMBEDDED_IPV4_SENTINEL_RULES = [
|
|
2554
2319
|
// IPv4-compatible (::a.b.c.d)
|
|
@@ -2597,12 +2362,12 @@ function parseIpv6WithEmbeddedIpv4(raw) {
|
|
|
2597
2362
|
}
|
|
2598
2363
|
function normalizeIpParseInput(raw) {
|
|
2599
2364
|
const trimmed = raw?.trim();
|
|
2600
|
-
if (
|
|
2365
|
+
if (trimmed === void 0 || trimmed === "") return;
|
|
2601
2366
|
return stripIpv6Brackets(trimmed);
|
|
2602
2367
|
}
|
|
2603
2368
|
function parseCanonicalIpAddress(raw) {
|
|
2604
2369
|
const normalized = normalizeIpParseInput(raw);
|
|
2605
|
-
if (
|
|
2370
|
+
if (normalized === void 0) return;
|
|
2606
2371
|
if (ipaddr.IPv4.isValid(normalized)) {
|
|
2607
2372
|
if (!ipaddr.IPv4.isValidFourPartDecimal(normalized)) return;
|
|
2608
2373
|
return ipaddr.IPv4.parse(normalized);
|
|
@@ -2612,20 +2377,20 @@ function parseCanonicalIpAddress(raw) {
|
|
|
2612
2377
|
}
|
|
2613
2378
|
function parseLooseIpAddress(raw) {
|
|
2614
2379
|
const normalized = normalizeIpParseInput(raw);
|
|
2615
|
-
if (
|
|
2380
|
+
if (normalized === void 0) return;
|
|
2616
2381
|
if (ipaddr.isValid(normalized)) return ipaddr.parse(normalized);
|
|
2617
2382
|
return parseIpv6WithEmbeddedIpv4(normalized);
|
|
2618
2383
|
}
|
|
2619
2384
|
function isCanonicalDottedDecimalIPv4(raw) {
|
|
2620
|
-
const trimmed = raw
|
|
2621
|
-
if (
|
|
2385
|
+
const trimmed = raw.trim();
|
|
2386
|
+
if (trimmed === "") return false;
|
|
2622
2387
|
const normalized = stripIpv6Brackets(trimmed);
|
|
2623
2388
|
if (!normalized) return false;
|
|
2624
2389
|
return ipaddr.IPv4.isValidFourPartDecimal(normalized);
|
|
2625
2390
|
}
|
|
2626
2391
|
function isLegacyIpv4Literal(raw) {
|
|
2627
|
-
const trimmed = raw
|
|
2628
|
-
if (
|
|
2392
|
+
const trimmed = raw.trim();
|
|
2393
|
+
if (trimmed === "") return false;
|
|
2629
2394
|
const normalized = stripIpv6Brackets(trimmed);
|
|
2630
2395
|
if (!normalized || normalized.includes(":")) return false;
|
|
2631
2396
|
if (isCanonicalDottedDecimalIPv4(normalized)) return false;
|
|
@@ -2651,12 +2416,7 @@ function isBlockedSpecialUseIpv6Address(address) {
|
|
|
2651
2416
|
return (address.parts[0] & 65472) === 65216;
|
|
2652
2417
|
}
|
|
2653
2418
|
function decodeIpv4FromHextets(high, low) {
|
|
2654
|
-
const octets = [
|
|
2655
|
-
high >>> 8 & 255,
|
|
2656
|
-
high & 255,
|
|
2657
|
-
low >>> 8 & 255,
|
|
2658
|
-
low & 255
|
|
2659
|
-
];
|
|
2419
|
+
const octets = [high >>> 8 & 255, high & 255, low >>> 8 & 255, low & 255];
|
|
2660
2420
|
return ipaddr.IPv4.parse(octets.join("."));
|
|
2661
2421
|
}
|
|
2662
2422
|
function extractEmbeddedIpv4FromIpv6(address) {
|
|
@@ -2703,9 +2463,7 @@ function normalizeHostnameSet(values) {
|
|
|
2703
2463
|
function normalizeHostnameAllowlist(values) {
|
|
2704
2464
|
if (!values || values.length === 0) return [];
|
|
2705
2465
|
return Array.from(
|
|
2706
|
-
new Set(
|
|
2707
|
-
values.map((v) => normalizeHostname(v)).filter((v) => v !== "*" && v !== "*." && v.length > 0)
|
|
2708
|
-
)
|
|
2466
|
+
new Set(values.map((v) => normalizeHostname(v)).filter((v) => v !== "*" && v !== "*." && v.length > 0))
|
|
2709
2467
|
);
|
|
2710
2468
|
}
|
|
2711
2469
|
function isHostnameAllowedByPattern(hostname, pattern) {
|
|
@@ -2740,19 +2498,25 @@ function createPinnedLookup(params) {
|
|
|
2740
2498
|
family: address.includes(":") ? 6 : 4
|
|
2741
2499
|
}));
|
|
2742
2500
|
let index = 0;
|
|
2743
|
-
return ((
|
|
2744
|
-
const
|
|
2745
|
-
|
|
2746
|
-
const
|
|
2747
|
-
if (
|
|
2748
|
-
|
|
2749
|
-
|
|
2501
|
+
return ((_host, ...rest) => {
|
|
2502
|
+
const second = rest[0];
|
|
2503
|
+
const third = rest[1];
|
|
2504
|
+
const cb = typeof second === "function" ? second : typeof third === "function" ? third : void 0;
|
|
2505
|
+
if (cb === void 0) return;
|
|
2506
|
+
const normalized = normalizeHostname(_host);
|
|
2507
|
+
if (normalized === "" || normalized !== normalizedHost) {
|
|
2508
|
+
if (typeof second === "function" || second === void 0) {
|
|
2509
|
+
fallback(_host, cb);
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
fallback(_host, second, cb);
|
|
2513
|
+
return;
|
|
2750
2514
|
}
|
|
2751
|
-
const opts = typeof
|
|
2752
|
-
const requestedFamily = typeof
|
|
2515
|
+
const opts = typeof second === "object" ? second : {};
|
|
2516
|
+
const requestedFamily = typeof second === "number" ? second : typeof opts.family === "number" ? opts.family : 0;
|
|
2753
2517
|
const candidates = requestedFamily === 4 || requestedFamily === 6 ? records.filter((entry) => entry.family === requestedFamily) : records;
|
|
2754
2518
|
const usable = candidates.length > 0 ? candidates : records;
|
|
2755
|
-
if (opts.all) {
|
|
2519
|
+
if (opts.all === true) {
|
|
2756
2520
|
cb(null, usable);
|
|
2757
2521
|
return;
|
|
2758
2522
|
}
|
|
@@ -2770,9 +2534,7 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
2770
2534
|
const isExplicitlyAllowed = allowedHostnames.has(normalized);
|
|
2771
2535
|
const skipPrivateNetworkChecks = allowPrivateNetwork || isExplicitlyAllowed;
|
|
2772
2536
|
if (!matchesHostnameAllowlist(normalized, hostnameAllowlist)) {
|
|
2773
|
-
throw new InvalidBrowserNavigationUrlError(
|
|
2774
|
-
`Navigation blocked: hostname "${hostname}" is not in the allowlist.`
|
|
2775
|
-
);
|
|
2537
|
+
throw new InvalidBrowserNavigationUrlError(`Navigation blocked: hostname "${hostname}" is not in the allowlist.`);
|
|
2776
2538
|
}
|
|
2777
2539
|
if (!skipPrivateNetworkChecks) {
|
|
2778
2540
|
if (isBlockedHostnameOrIp(normalized, params.policy)) {
|
|
@@ -2790,7 +2552,7 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
2790
2552
|
`Navigation to internal/loopback address blocked: unable to resolve "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
2791
2553
|
);
|
|
2792
2554
|
}
|
|
2793
|
-
if (
|
|
2555
|
+
if (results.length === 0) {
|
|
2794
2556
|
throw new InvalidBrowserNavigationUrlError(
|
|
2795
2557
|
`Navigation to internal/loopback address blocked: unable to resolve "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
2796
2558
|
);
|
|
@@ -2817,8 +2579,8 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
2817
2579
|
};
|
|
2818
2580
|
}
|
|
2819
2581
|
async function assertBrowserNavigationAllowed(opts) {
|
|
2820
|
-
const rawUrl =
|
|
2821
|
-
if (
|
|
2582
|
+
const rawUrl = opts.url.trim();
|
|
2583
|
+
if (rawUrl === "") throw new InvalidBrowserNavigationUrlError("url is required");
|
|
2822
2584
|
let parsed;
|
|
2823
2585
|
try {
|
|
2824
2586
|
parsed = new URL(rawUrl);
|
|
@@ -2847,7 +2609,7 @@ async function assertSafeOutputPath(path2, allowedRoots) {
|
|
|
2847
2609
|
if (normalized.includes("..")) {
|
|
2848
2610
|
throw new Error(`Unsafe output path: directory traversal detected in "${path2}".`);
|
|
2849
2611
|
}
|
|
2850
|
-
if (allowedRoots
|
|
2612
|
+
if (allowedRoots !== void 0 && allowedRoots.length > 0) {
|
|
2851
2613
|
const resolved = path.resolve(normalized);
|
|
2852
2614
|
let parentReal;
|
|
2853
2615
|
try {
|
|
@@ -2905,8 +2667,8 @@ async function resolveStrictExistingUploadPaths(params) {
|
|
|
2905
2667
|
}
|
|
2906
2668
|
}
|
|
2907
2669
|
function sanitizeUntrustedFileName(fileName, fallbackName) {
|
|
2908
|
-
const trimmed =
|
|
2909
|
-
if (
|
|
2670
|
+
const trimmed = fileName.trim();
|
|
2671
|
+
if (trimmed === "") return fallbackName;
|
|
2910
2672
|
let base = path.posix.basename(trimmed);
|
|
2911
2673
|
base = path.win32.basename(base);
|
|
2912
2674
|
let cleaned = "";
|
|
@@ -2940,16 +2702,17 @@ async function writeViaSiblingTempPath(params) {
|
|
|
2940
2702
|
await promises$1.rename(tempPath, targetPath);
|
|
2941
2703
|
renameSucceeded = true;
|
|
2942
2704
|
} finally {
|
|
2943
|
-
if (!renameSucceeded)
|
|
2944
|
-
|
|
2705
|
+
if (!renameSucceeded)
|
|
2706
|
+
await promises$1.rm(tempPath, { force: true }).catch(() => {
|
|
2707
|
+
});
|
|
2945
2708
|
}
|
|
2946
2709
|
}
|
|
2947
2710
|
function isAbsolute(p) {
|
|
2948
2711
|
return p.startsWith("/") || /^[a-zA-Z]:/.test(p);
|
|
2949
2712
|
}
|
|
2950
2713
|
async function assertBrowserNavigationResultAllowed(opts) {
|
|
2951
|
-
const rawUrl =
|
|
2952
|
-
if (
|
|
2714
|
+
const rawUrl = opts.url.trim();
|
|
2715
|
+
if (rawUrl === "") return;
|
|
2953
2716
|
let parsed;
|
|
2954
2717
|
try {
|
|
2955
2718
|
parsed = new URL(rawUrl);
|
|
@@ -2979,18 +2742,20 @@ function requiresInspectableBrowserNavigationRedirects(ssrfPolicy) {
|
|
|
2979
2742
|
var MAX_CLICK_DELAY_MS = 5e3;
|
|
2980
2743
|
var CHECKABLE_ROLES = /* @__PURE__ */ new Set(["menuitemcheckbox", "menuitemradio", "checkbox", "switch"]);
|
|
2981
2744
|
function resolveLocator(page, resolved) {
|
|
2982
|
-
|
|
2745
|
+
if (resolved.ref !== void 0 && resolved.ref !== "") return refLocator(page, resolved.ref);
|
|
2746
|
+
const sel = resolved.selector ?? "";
|
|
2747
|
+
return page.locator(sel);
|
|
2983
2748
|
}
|
|
2984
2749
|
async function clickViaPlaywright(opts) {
|
|
2985
2750
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
2986
2751
|
const page = await getRestoredPageForTarget(opts);
|
|
2987
|
-
const label = resolved.ref ?? resolved.selector;
|
|
2752
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
2988
2753
|
const locator = resolveLocator(page, resolved);
|
|
2989
2754
|
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
2990
2755
|
let checkableRole = false;
|
|
2991
|
-
if (resolved.ref) {
|
|
2756
|
+
if (resolved.ref !== void 0 && resolved.ref !== "") {
|
|
2992
2757
|
const refId = parseRoleRef(resolved.ref);
|
|
2993
|
-
if (refId) {
|
|
2758
|
+
if (refId !== null) {
|
|
2994
2759
|
const state = ensurePageState(page);
|
|
2995
2760
|
const info = state.roleRefs?.[refId];
|
|
2996
2761
|
if (info && CHECKABLE_ROLES.has(info.role)) checkableRole = true;
|
|
@@ -3003,15 +2768,15 @@ async function clickViaPlaywright(opts) {
|
|
|
3003
2768
|
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
3004
2769
|
}
|
|
3005
2770
|
let ariaCheckedBefore;
|
|
3006
|
-
if (checkableRole &&
|
|
2771
|
+
if (checkableRole && opts.doubleClick !== true) {
|
|
3007
2772
|
ariaCheckedBefore = await locator.getAttribute("aria-checked", { timeout }).catch(() => void 0);
|
|
3008
2773
|
}
|
|
3009
|
-
if (opts.doubleClick) {
|
|
2774
|
+
if (opts.doubleClick === true) {
|
|
3010
2775
|
await locator.dblclick({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
3011
2776
|
} else {
|
|
3012
2777
|
await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
3013
2778
|
}
|
|
3014
|
-
if (checkableRole &&
|
|
2779
|
+
if (checkableRole && opts.doubleClick !== true && ariaCheckedBefore !== void 0) {
|
|
3015
2780
|
const POLL_INTERVAL_MS = 50;
|
|
3016
2781
|
const POLL_TIMEOUT_MS = 500;
|
|
3017
2782
|
let changed = false;
|
|
@@ -3024,7 +2789,9 @@ async function clickViaPlaywright(opts) {
|
|
|
3024
2789
|
await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
|
|
3025
2790
|
}
|
|
3026
2791
|
if (!changed) {
|
|
3027
|
-
await locator.evaluate((el) =>
|
|
2792
|
+
await locator.evaluate((el) => {
|
|
2793
|
+
el.click();
|
|
2794
|
+
}).catch(() => {
|
|
3028
2795
|
});
|
|
3029
2796
|
}
|
|
3030
2797
|
}
|
|
@@ -3035,7 +2802,7 @@ async function clickViaPlaywright(opts) {
|
|
|
3035
2802
|
async function hoverViaPlaywright(opts) {
|
|
3036
2803
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
3037
2804
|
const page = await getRestoredPageForTarget(opts);
|
|
3038
|
-
const label = resolved.ref ?? resolved.selector;
|
|
2805
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
3039
2806
|
const locator = resolveLocator(page, resolved);
|
|
3040
2807
|
try {
|
|
3041
2808
|
await locator.hover({ timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
|
|
@@ -3045,28 +2812,28 @@ async function hoverViaPlaywright(opts) {
|
|
|
3045
2812
|
}
|
|
3046
2813
|
async function typeViaPlaywright(opts) {
|
|
3047
2814
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
3048
|
-
const text =
|
|
2815
|
+
const text = opts.text;
|
|
3049
2816
|
const page = await getRestoredPageForTarget(opts);
|
|
3050
|
-
const label = resolved.ref ?? resolved.selector;
|
|
2817
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
3051
2818
|
const locator = resolveLocator(page, resolved);
|
|
3052
2819
|
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
3053
2820
|
try {
|
|
3054
|
-
if (opts.slowly) {
|
|
2821
|
+
if (opts.slowly === true) {
|
|
3055
2822
|
await locator.click({ timeout });
|
|
3056
2823
|
await locator.pressSequentially(text, { timeout, delay: 75 });
|
|
3057
2824
|
} else {
|
|
3058
2825
|
await locator.fill(text, { timeout });
|
|
3059
2826
|
}
|
|
3060
|
-
if (opts.submit) await locator.press("Enter", { timeout });
|
|
2827
|
+
if (opts.submit === true) await locator.press("Enter", { timeout });
|
|
3061
2828
|
} catch (err) {
|
|
3062
2829
|
throw toAIFriendlyError(err, label);
|
|
3063
2830
|
}
|
|
3064
2831
|
}
|
|
3065
2832
|
async function selectOptionViaPlaywright(opts) {
|
|
3066
2833
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
3067
|
-
if (
|
|
2834
|
+
if (opts.values.length === 0) throw new Error("values are required");
|
|
3068
2835
|
const page = await getRestoredPageForTarget(opts);
|
|
3069
|
-
const label = resolved.ref ?? resolved.selector;
|
|
2836
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
3070
2837
|
const locator = resolveLocator(page, resolved);
|
|
3071
2838
|
try {
|
|
3072
2839
|
await locator.selectOption(opts.values, { timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
|
|
@@ -3080,8 +2847,8 @@ async function dragViaPlaywright(opts) {
|
|
|
3080
2847
|
const page = await getRestoredPageForTarget(opts);
|
|
3081
2848
|
const startLocator = resolveLocator(page, resolvedStart);
|
|
3082
2849
|
const endLocator = resolveLocator(page, resolvedEnd);
|
|
3083
|
-
const startLabel = resolvedStart.ref ?? resolvedStart.selector;
|
|
3084
|
-
const endLabel = resolvedEnd.ref ?? resolvedEnd.selector;
|
|
2850
|
+
const startLabel = resolvedStart.ref ?? resolvedStart.selector ?? "";
|
|
2851
|
+
const endLabel = resolvedEnd.ref ?? resolvedEnd.selector ?? "";
|
|
3085
2852
|
try {
|
|
3086
2853
|
await startLocator.dragTo(endLocator, { timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
|
|
3087
2854
|
} catch (err) {
|
|
@@ -3117,7 +2884,7 @@ async function fillFormViaPlaywright(opts) {
|
|
|
3117
2884
|
async function scrollIntoViewViaPlaywright(opts) {
|
|
3118
2885
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
3119
2886
|
const page = await getRestoredPageForTarget(opts);
|
|
3120
|
-
const label = resolved.ref ?? resolved.selector;
|
|
2887
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
3121
2888
|
const locator = resolveLocator(page, resolved);
|
|
3122
2889
|
try {
|
|
3123
2890
|
await locator.scrollIntoViewIfNeeded({ timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
|
|
@@ -3185,7 +2952,7 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
3185
2952
|
const armId = state.armIdUpload;
|
|
3186
2953
|
page.waitForEvent("filechooser", { timeout }).then(async (fileChooser) => {
|
|
3187
2954
|
if (state.armIdUpload !== armId) return;
|
|
3188
|
-
if (
|
|
2955
|
+
if (opts.paths === void 0 || opts.paths.length === 0) {
|
|
3189
2956
|
try {
|
|
3190
2957
|
await page.keyboard.press("Escape");
|
|
3191
2958
|
} catch {
|
|
@@ -3206,7 +2973,7 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
3206
2973
|
await fileChooser.setFiles(uploadPathsResult.paths);
|
|
3207
2974
|
try {
|
|
3208
2975
|
const input = typeof fileChooser.element === "function" ? await Promise.resolve(fileChooser.element()) : null;
|
|
3209
|
-
if (input) {
|
|
2976
|
+
if (input !== null) {
|
|
3210
2977
|
await input.evaluate((el) => {
|
|
3211
2978
|
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
3212
2979
|
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
@@ -3220,7 +2987,7 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
3220
2987
|
|
|
3221
2988
|
// src/actions/keyboard.ts
|
|
3222
2989
|
async function pressKeyViaPlaywright(opts) {
|
|
3223
|
-
const key =
|
|
2990
|
+
const key = opts.key.trim();
|
|
3224
2991
|
if (!key) throw new Error("key is required");
|
|
3225
2992
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3226
2993
|
ensurePageState(page);
|
|
@@ -3233,9 +3000,9 @@ function isRetryableNavigateError(err) {
|
|
|
3233
3000
|
return msg.includes("frame has been detached") || msg.includes("target page, context or browser has been closed");
|
|
3234
3001
|
}
|
|
3235
3002
|
async function navigateViaPlaywright(opts) {
|
|
3236
|
-
const url =
|
|
3003
|
+
const url = opts.url.trim();
|
|
3237
3004
|
if (!url) throw new Error("url is required");
|
|
3238
|
-
const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3005
|
+
const policy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3239
3006
|
await assertBrowserNavigationAllowed({ url, ...withBrowserNavigationPolicy(policy) });
|
|
3240
3007
|
const timeout = Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 2e4));
|
|
3241
3008
|
let page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
@@ -3254,23 +3021,27 @@ async function navigateViaPlaywright(opts) {
|
|
|
3254
3021
|
ensurePageState(page);
|
|
3255
3022
|
response = await navigate();
|
|
3256
3023
|
}
|
|
3257
|
-
await assertBrowserNavigationRedirectChainAllowed({
|
|
3024
|
+
await assertBrowserNavigationRedirectChainAllowed({
|
|
3025
|
+
request: response?.request(),
|
|
3026
|
+
...withBrowserNavigationPolicy(policy)
|
|
3027
|
+
});
|
|
3258
3028
|
const finalUrl = page.url();
|
|
3259
3029
|
await assertBrowserNavigationResultAllowed({ url: finalUrl, ...withBrowserNavigationPolicy(policy) });
|
|
3260
3030
|
return { url: finalUrl };
|
|
3261
3031
|
}
|
|
3262
3032
|
async function listPagesViaPlaywright(opts) {
|
|
3263
3033
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
3264
|
-
const pages =
|
|
3034
|
+
const pages = getAllPages(browser);
|
|
3265
3035
|
const results = [];
|
|
3266
3036
|
for (const page of pages) {
|
|
3267
3037
|
const tid = await pageTargetId(page).catch(() => null);
|
|
3268
|
-
if (tid)
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3038
|
+
if (tid !== null && tid !== "")
|
|
3039
|
+
results.push({
|
|
3040
|
+
targetId: tid,
|
|
3041
|
+
title: await page.title().catch(() => ""),
|
|
3042
|
+
url: page.url(),
|
|
3043
|
+
type: "page"
|
|
3044
|
+
});
|
|
3274
3045
|
}
|
|
3275
3046
|
return results;
|
|
3276
3047
|
}
|
|
@@ -3281,7 +3052,7 @@ async function createPageViaPlaywright(opts) {
|
|
|
3281
3052
|
const page = await context.newPage();
|
|
3282
3053
|
ensurePageState(page);
|
|
3283
3054
|
const targetUrl = (opts.url ?? "").trim() || "about:blank";
|
|
3284
|
-
const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3055
|
+
const policy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3285
3056
|
if (targetUrl !== "about:blank") {
|
|
3286
3057
|
const navigationPolicy = withBrowserNavigationPolicy(policy);
|
|
3287
3058
|
await assertBrowserNavigationAllowed({ url: targetUrl, ...navigationPolicy });
|
|
@@ -3292,7 +3063,7 @@ async function createPageViaPlaywright(opts) {
|
|
|
3292
3063
|
await assertBrowserNavigationResultAllowed({ url: page.url(), ...navigationPolicy });
|
|
3293
3064
|
}
|
|
3294
3065
|
const tid = await pageTargetId(page).catch(() => null);
|
|
3295
|
-
if (
|
|
3066
|
+
if (tid === null || tid === "") throw new Error("Failed to get targetId for new page");
|
|
3296
3067
|
return {
|
|
3297
3068
|
targetId: tid,
|
|
3298
3069
|
title: await page.title().catch(() => ""),
|
|
@@ -3314,202 +3085,64 @@ async function focusPageByTargetIdViaPlaywright(opts) {
|
|
|
3314
3085
|
await page.bringToFront();
|
|
3315
3086
|
} catch (err) {
|
|
3316
3087
|
try {
|
|
3317
|
-
await withPageScopedCdpClient({
|
|
3318
|
-
cdpUrl: opts.cdpUrl,
|
|
3319
|
-
page,
|
|
3320
|
-
targetId: opts.targetId,
|
|
3321
|
-
fn: async (send) => {
|
|
3322
|
-
await send("Page.bringToFront");
|
|
3323
|
-
}
|
|
3324
|
-
});
|
|
3325
|
-
return;
|
|
3326
|
-
} catch {
|
|
3327
|
-
throw err;
|
|
3328
|
-
}
|
|
3329
|
-
}
|
|
3330
|
-
}
|
|
3331
|
-
async function resizeViewportViaPlaywright(opts) {
|
|
3332
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3333
|
-
ensurePageState(page);
|
|
3334
|
-
await page.setViewportSize({
|
|
3335
|
-
width: Math.max(1, Math.floor(opts.width)),
|
|
3336
|
-
height: Math.max(1, Math.floor(opts.height))
|
|
3337
|
-
});
|
|
3338
|
-
}
|
|
3339
|
-
|
|
3340
|
-
// src/actions/wait.ts
|
|
3341
|
-
var MAX_WAIT_TIME_MS = 3e4;
|
|
3342
|
-
function resolveBoundedDelayMs2(value, label, maxMs) {
|
|
3343
|
-
const normalized = Math.floor(value ?? 0);
|
|
3344
|
-
if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
|
|
3345
|
-
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${maxMs}ms`);
|
|
3346
|
-
return normalized;
|
|
3347
|
-
}
|
|
3348
|
-
async function waitForViaPlaywright(opts) {
|
|
3349
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3350
|
-
ensurePageState(page);
|
|
3351
|
-
const timeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
|
|
3352
|
-
if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
|
|
3353
|
-
await page.waitForTimeout(resolveBoundedDelayMs2(opts.timeMs, "wait timeMs", MAX_WAIT_TIME_MS));
|
|
3354
|
-
}
|
|
3355
|
-
if (opts.text) {
|
|
3356
|
-
await page.getByText(opts.text).first().waitFor({ state: "visible", timeout });
|
|
3357
|
-
}
|
|
3358
|
-
if (opts.textGone) {
|
|
3359
|
-
await page.getByText(opts.textGone).first().waitFor({ state: "hidden", timeout });
|
|
3360
|
-
}
|
|
3361
|
-
if (opts.selector) {
|
|
3362
|
-
const selector = String(opts.selector).trim();
|
|
3363
|
-
if (selector) await page.locator(selector).first().waitFor({ state: "visible", timeout });
|
|
3364
|
-
}
|
|
3365
|
-
if (opts.url) {
|
|
3366
|
-
const url = String(opts.url).trim();
|
|
3367
|
-
if (url) await page.waitForURL(url, { timeout });
|
|
3368
|
-
}
|
|
3369
|
-
if (opts.loadState) {
|
|
3370
|
-
await page.waitForLoadState(opts.loadState, { timeout });
|
|
3371
|
-
}
|
|
3372
|
-
if (opts.fn) {
|
|
3373
|
-
const fn = String(opts.fn).trim();
|
|
3374
|
-
if (fn) await page.waitForFunction(fn, void 0, { timeout });
|
|
3375
|
-
}
|
|
3376
|
-
}
|
|
3377
|
-
|
|
3378
|
-
// src/actions/evaluate.ts
|
|
3379
|
-
async function evaluateInAllFramesViaPlaywright(opts) {
|
|
3380
|
-
const fnText = String(opts.fn ?? "").trim();
|
|
3381
|
-
if (!fnText) throw new Error("function is required");
|
|
3382
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3383
|
-
const frames = page.frames();
|
|
3384
|
-
const results = [];
|
|
3385
|
-
for (const frame of frames) {
|
|
3386
|
-
try {
|
|
3387
|
-
const result = await frame.evaluate(
|
|
3388
|
-
// eslint-disable-next-line no-eval
|
|
3389
|
-
(fnBody) => {
|
|
3390
|
-
"use strict";
|
|
3391
|
-
try {
|
|
3392
|
-
const candidate = (0, eval)("(" + fnBody + ")");
|
|
3393
|
-
return typeof candidate === "function" ? candidate() : candidate;
|
|
3394
|
-
} catch (err) {
|
|
3395
|
-
throw new Error("Invalid evaluate function: " + (err instanceof Error ? err.message : String(err)));
|
|
3396
|
-
}
|
|
3397
|
-
},
|
|
3398
|
-
fnText
|
|
3399
|
-
);
|
|
3400
|
-
results.push({
|
|
3401
|
-
frameUrl: frame.url(),
|
|
3402
|
-
frameName: frame.name(),
|
|
3403
|
-
result
|
|
3404
|
-
});
|
|
3405
|
-
} catch {
|
|
3406
|
-
}
|
|
3407
|
-
}
|
|
3408
|
-
return results;
|
|
3409
|
-
}
|
|
3410
|
-
async function awaitEvalWithAbort(evalPromise, abortPromise) {
|
|
3411
|
-
if (!abortPromise) return await evalPromise;
|
|
3412
|
-
try {
|
|
3413
|
-
return await Promise.race([evalPromise, abortPromise]);
|
|
3414
|
-
} catch (err) {
|
|
3415
|
-
evalPromise.catch(() => {
|
|
3416
|
-
});
|
|
3417
|
-
throw err;
|
|
3418
|
-
}
|
|
3419
|
-
}
|
|
3420
|
-
var BROWSER_EVALUATOR = new Function("args", `
|
|
3421
|
-
"use strict";
|
|
3422
|
-
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
3423
|
-
try {
|
|
3424
|
-
var candidate = eval("(" + fnBody + ")");
|
|
3425
|
-
var result = typeof candidate === "function" ? candidate() : candidate;
|
|
3426
|
-
if (result && typeof result.then === "function") {
|
|
3427
|
-
return Promise.race([
|
|
3428
|
-
result,
|
|
3429
|
-
new Promise(function(_, reject) {
|
|
3430
|
-
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
3431
|
-
})
|
|
3432
|
-
]);
|
|
3433
|
-
}
|
|
3434
|
-
return result;
|
|
3435
|
-
} catch (err) {
|
|
3436
|
-
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
3437
|
-
}
|
|
3438
|
-
`);
|
|
3439
|
-
var ELEMENT_EVALUATOR = new Function("el", "args", `
|
|
3440
|
-
"use strict";
|
|
3441
|
-
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
3442
|
-
try {
|
|
3443
|
-
var candidate = eval("(" + fnBody + ")");
|
|
3444
|
-
var result = typeof candidate === "function" ? candidate(el) : candidate;
|
|
3445
|
-
if (result && typeof result.then === "function") {
|
|
3446
|
-
return Promise.race([
|
|
3447
|
-
result,
|
|
3448
|
-
new Promise(function(_, reject) {
|
|
3449
|
-
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
3450
|
-
})
|
|
3451
|
-
]);
|
|
3452
|
-
}
|
|
3453
|
-
return result;
|
|
3454
|
-
} catch (err) {
|
|
3455
|
-
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
3456
|
-
}
|
|
3457
|
-
`);
|
|
3458
|
-
async function evaluateViaPlaywright(opts) {
|
|
3459
|
-
const fnText = String(opts.fn ?? "").trim();
|
|
3460
|
-
if (!fnText) throw new Error("function is required");
|
|
3461
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3462
|
-
ensurePageState(page);
|
|
3463
|
-
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
3464
|
-
const outerTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
|
|
3465
|
-
let evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 500));
|
|
3466
|
-
evaluateTimeout = Math.min(evaluateTimeout, outerTimeout);
|
|
3467
|
-
const signal = opts.signal;
|
|
3468
|
-
let abortListener;
|
|
3469
|
-
let abortReject;
|
|
3470
|
-
let abortPromise;
|
|
3471
|
-
if (signal) {
|
|
3472
|
-
abortPromise = new Promise((_, reject) => {
|
|
3473
|
-
abortReject = reject;
|
|
3474
|
-
});
|
|
3475
|
-
abortPromise.catch(() => {
|
|
3476
|
-
});
|
|
3477
|
-
}
|
|
3478
|
-
if (signal) {
|
|
3479
|
-
const disconnect = () => {
|
|
3480
|
-
forceDisconnectPlaywrightForTarget({
|
|
3088
|
+
await withPageScopedCdpClient({
|
|
3481
3089
|
cdpUrl: opts.cdpUrl,
|
|
3482
|
-
|
|
3090
|
+
page,
|
|
3091
|
+
targetId: opts.targetId,
|
|
3092
|
+
fn: async (send) => {
|
|
3093
|
+
await send("Page.bringToFront");
|
|
3094
|
+
}
|
|
3483
3095
|
});
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
throw signal.reason ?? new Error("aborted");
|
|
3488
|
-
}
|
|
3489
|
-
abortListener = () => {
|
|
3490
|
-
disconnect();
|
|
3491
|
-
abortReject?.(signal.reason ?? new Error("aborted"));
|
|
3492
|
-
};
|
|
3493
|
-
signal.addEventListener("abort", abortListener, { once: true });
|
|
3494
|
-
if (signal.aborted) {
|
|
3495
|
-
abortListener();
|
|
3496
|
-
throw signal.reason ?? new Error("aborted");
|
|
3096
|
+
return;
|
|
3097
|
+
} catch {
|
|
3098
|
+
throw err;
|
|
3497
3099
|
}
|
|
3498
3100
|
}
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3101
|
+
}
|
|
3102
|
+
async function resizeViewportViaPlaywright(opts) {
|
|
3103
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3104
|
+
ensurePageState(page);
|
|
3105
|
+
await page.setViewportSize({
|
|
3106
|
+
width: Math.max(1, Math.floor(opts.width)),
|
|
3107
|
+
height: Math.max(1, Math.floor(opts.height))
|
|
3108
|
+
});
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
// src/actions/wait.ts
|
|
3112
|
+
var MAX_WAIT_TIME_MS = 3e4;
|
|
3113
|
+
function resolveBoundedDelayMs2(value, label, maxMs) {
|
|
3114
|
+
const normalized = Math.floor(value ?? 0);
|
|
3115
|
+
if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
|
|
3116
|
+
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${String(maxMs)}ms`);
|
|
3117
|
+
return normalized;
|
|
3118
|
+
}
|
|
3119
|
+
async function waitForViaPlaywright(opts) {
|
|
3120
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3121
|
+
ensurePageState(page);
|
|
3122
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
|
|
3123
|
+
if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
|
|
3124
|
+
await page.waitForTimeout(resolveBoundedDelayMs2(opts.timeMs, "wait timeMs", MAX_WAIT_TIME_MS));
|
|
3125
|
+
}
|
|
3126
|
+
if (opts.text !== void 0 && opts.text !== "") {
|
|
3127
|
+
await page.getByText(opts.text).first().waitFor({ state: "visible", timeout });
|
|
3128
|
+
}
|
|
3129
|
+
if (opts.textGone !== void 0 && opts.textGone !== "") {
|
|
3130
|
+
await page.getByText(opts.textGone).first().waitFor({ state: "hidden", timeout });
|
|
3131
|
+
}
|
|
3132
|
+
if (opts.selector !== void 0 && opts.selector !== "") {
|
|
3133
|
+
const selector = opts.selector.trim();
|
|
3134
|
+
if (selector !== "") await page.locator(selector).first().waitFor({ state: "visible", timeout });
|
|
3135
|
+
}
|
|
3136
|
+
if (opts.url !== void 0 && opts.url !== "") {
|
|
3137
|
+
const url = opts.url.trim();
|
|
3138
|
+
if (url !== "") await page.waitForURL(url, { timeout });
|
|
3139
|
+
}
|
|
3140
|
+
if (opts.loadState !== void 0) {
|
|
3141
|
+
await page.waitForLoadState(opts.loadState, { timeout });
|
|
3142
|
+
}
|
|
3143
|
+
if (opts.fn !== void 0 && opts.fn !== "") {
|
|
3144
|
+
const fn = opts.fn.trim();
|
|
3145
|
+
if (fn !== "") await page.waitForFunction(fn, void 0, { timeout });
|
|
3513
3146
|
}
|
|
3514
3147
|
}
|
|
3515
3148
|
|
|
@@ -3517,7 +3150,7 @@ async function evaluateViaPlaywright(opts) {
|
|
|
3517
3150
|
var MAX_BATCH_DEPTH = 5;
|
|
3518
3151
|
var MAX_BATCH_ACTIONS = 100;
|
|
3519
3152
|
async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, depth = 0) {
|
|
3520
|
-
if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${MAX_BATCH_DEPTH}`);
|
|
3153
|
+
if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${String(MAX_BATCH_DEPTH)}`);
|
|
3521
3154
|
const effectiveTargetId = action.targetId ?? targetId;
|
|
3522
3155
|
switch (action.kind) {
|
|
3523
3156
|
case "click":
|
|
@@ -3609,7 +3242,8 @@ async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, de
|
|
|
3609
3242
|
});
|
|
3610
3243
|
break;
|
|
3611
3244
|
case "wait":
|
|
3612
|
-
if (action.fn
|
|
3245
|
+
if (action.fn !== void 0 && action.fn !== "" && !evaluateEnabled)
|
|
3246
|
+
throw new Error("wait --fn is disabled by config (browser.evaluateEnabled=false)");
|
|
3613
3247
|
await waitForViaPlaywright({
|
|
3614
3248
|
cdpUrl,
|
|
3615
3249
|
targetId: effectiveTargetId,
|
|
@@ -3650,13 +3284,14 @@ async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, de
|
|
|
3650
3284
|
});
|
|
3651
3285
|
break;
|
|
3652
3286
|
default:
|
|
3653
|
-
throw new Error(`Unsupported batch action kind: ${action.kind}`);
|
|
3287
|
+
throw new Error(`Unsupported batch action kind: ${String(action.kind)}`);
|
|
3654
3288
|
}
|
|
3655
3289
|
}
|
|
3656
3290
|
async function batchViaPlaywright(opts) {
|
|
3657
3291
|
const depth = opts.depth ?? 0;
|
|
3658
|
-
if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${MAX_BATCH_DEPTH}`);
|
|
3659
|
-
if (opts.actions.length > MAX_BATCH_ACTIONS)
|
|
3292
|
+
if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${String(MAX_BATCH_DEPTH)}`);
|
|
3293
|
+
if (opts.actions.length > MAX_BATCH_ACTIONS)
|
|
3294
|
+
throw new Error(`Batch exceeds maximum of ${String(MAX_BATCH_ACTIONS)} actions`);
|
|
3660
3295
|
const results = [];
|
|
3661
3296
|
const evaluateEnabled = opts.evaluateEnabled !== false;
|
|
3662
3297
|
for (const action of opts.actions) {
|
|
@@ -3730,392 +3365,830 @@ async function awaitDownloadPayload(params) {
|
|
|
3730
3365
|
throw err;
|
|
3731
3366
|
}
|
|
3732
3367
|
}
|
|
3733
|
-
async function downloadViaPlaywright(opts) {
|
|
3734
|
-
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
3368
|
+
async function downloadViaPlaywright(opts) {
|
|
3369
|
+
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
3370
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3371
|
+
const state = ensurePageState(page);
|
|
3372
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
3373
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
3374
|
+
const outPath = opts.path.trim();
|
|
3375
|
+
if (!outPath) throw new Error("path is required");
|
|
3376
|
+
state.armIdDownload = bumpDownloadArmId();
|
|
3377
|
+
const armId = state.armIdDownload;
|
|
3378
|
+
const waiter = createPageDownloadWaiter(page, timeout);
|
|
3379
|
+
try {
|
|
3380
|
+
const locator = refLocator(page, opts.ref);
|
|
3381
|
+
try {
|
|
3382
|
+
await locator.click({ timeout });
|
|
3383
|
+
} catch (err) {
|
|
3384
|
+
throw toAIFriendlyError(err, opts.ref);
|
|
3385
|
+
}
|
|
3386
|
+
return await awaitDownloadPayload({ waiter, state, armId, outPath });
|
|
3387
|
+
} catch (err) {
|
|
3388
|
+
waiter.cancel();
|
|
3389
|
+
throw err;
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
async function waitForDownloadViaPlaywright(opts) {
|
|
3393
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3394
|
+
const state = ensurePageState(page);
|
|
3395
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
3396
|
+
state.armIdDownload = bumpDownloadArmId();
|
|
3397
|
+
const armId = state.armIdDownload;
|
|
3398
|
+
const waiter = createPageDownloadWaiter(page, timeout);
|
|
3399
|
+
try {
|
|
3400
|
+
const download = await waiter.promise;
|
|
3401
|
+
if (state.armIdDownload !== armId) throw new Error("Download was superseded by another waiter");
|
|
3402
|
+
const savePath = opts.path ?? download.suggestedFilename();
|
|
3403
|
+
await assertSafeOutputPath(savePath, opts.allowedOutputRoots);
|
|
3404
|
+
return await saveDownloadPayload(download, savePath);
|
|
3405
|
+
} catch (err) {
|
|
3406
|
+
waiter.cancel();
|
|
3407
|
+
throw err;
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
async function emulateMediaViaPlaywright(opts) {
|
|
3411
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3412
|
+
ensurePageState(page);
|
|
3413
|
+
await page.emulateMedia({ colorScheme: opts.colorScheme });
|
|
3414
|
+
}
|
|
3415
|
+
async function setDeviceViaPlaywright(opts) {
|
|
3416
|
+
const name = opts.name.trim();
|
|
3417
|
+
if (!name) throw new Error("device name is required");
|
|
3418
|
+
const device = playwrightCore.devices[name];
|
|
3419
|
+
if (device === void 0) {
|
|
3420
|
+
throw new Error(`Unknown device "${name}".`);
|
|
3421
|
+
}
|
|
3422
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3423
|
+
ensurePageState(page);
|
|
3424
|
+
if (device.viewport !== null) {
|
|
3425
|
+
await page.setViewportSize({
|
|
3426
|
+
width: device.viewport.width,
|
|
3427
|
+
height: device.viewport.height
|
|
3428
|
+
});
|
|
3429
|
+
}
|
|
3430
|
+
await withPageScopedCdpClient({
|
|
3431
|
+
cdpUrl: opts.cdpUrl,
|
|
3432
|
+
page,
|
|
3433
|
+
targetId: opts.targetId,
|
|
3434
|
+
fn: async (send) => {
|
|
3435
|
+
const locale = device.locale;
|
|
3436
|
+
if (device.userAgent !== "" || locale !== void 0 && locale !== "") {
|
|
3437
|
+
await send("Emulation.setUserAgentOverride", {
|
|
3438
|
+
userAgent: device.userAgent,
|
|
3439
|
+
acceptLanguage: locale
|
|
3440
|
+
});
|
|
3441
|
+
}
|
|
3442
|
+
if (device.viewport !== null) {
|
|
3443
|
+
await send("Emulation.setDeviceMetricsOverride", {
|
|
3444
|
+
mobile: device.isMobile,
|
|
3445
|
+
width: device.viewport.width,
|
|
3446
|
+
height: device.viewport.height,
|
|
3447
|
+
deviceScaleFactor: device.deviceScaleFactor,
|
|
3448
|
+
screenWidth: device.viewport.width,
|
|
3449
|
+
screenHeight: device.viewport.height
|
|
3450
|
+
});
|
|
3451
|
+
}
|
|
3452
|
+
if (device.hasTouch) {
|
|
3453
|
+
await send("Emulation.setTouchEmulationEnabled", { enabled: true });
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
});
|
|
3457
|
+
}
|
|
3458
|
+
async function setExtraHTTPHeadersViaPlaywright(opts) {
|
|
3459
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3460
|
+
ensurePageState(page);
|
|
3461
|
+
await page.context().setExtraHTTPHeaders(opts.headers);
|
|
3462
|
+
}
|
|
3463
|
+
async function setGeolocationViaPlaywright(opts) {
|
|
3464
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3465
|
+
ensurePageState(page);
|
|
3466
|
+
const context = page.context();
|
|
3467
|
+
if (opts.clear === true) {
|
|
3468
|
+
await context.setGeolocation(null);
|
|
3469
|
+
await context.clearPermissions().catch(() => {
|
|
3470
|
+
});
|
|
3471
|
+
return;
|
|
3472
|
+
}
|
|
3473
|
+
if (typeof opts.latitude !== "number" || typeof opts.longitude !== "number") {
|
|
3474
|
+
throw new Error("latitude and longitude are required (or set clear=true)");
|
|
3475
|
+
}
|
|
3476
|
+
await context.setGeolocation({
|
|
3477
|
+
latitude: opts.latitude,
|
|
3478
|
+
longitude: opts.longitude,
|
|
3479
|
+
accuracy: typeof opts.accuracy === "number" ? opts.accuracy : void 0
|
|
3480
|
+
});
|
|
3481
|
+
const origin = (opts.origin !== void 0 && opts.origin !== "" ? opts.origin.trim() : "") || (() => {
|
|
3482
|
+
try {
|
|
3483
|
+
return new URL(page.url()).origin;
|
|
3484
|
+
} catch {
|
|
3485
|
+
return "";
|
|
3486
|
+
}
|
|
3487
|
+
})();
|
|
3488
|
+
if (origin !== "")
|
|
3489
|
+
await context.grantPermissions(["geolocation"], { origin }).catch(() => {
|
|
3490
|
+
});
|
|
3491
|
+
}
|
|
3492
|
+
async function setHttpCredentialsViaPlaywright(opts) {
|
|
3493
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3494
|
+
ensurePageState(page);
|
|
3495
|
+
if (opts.clear === true) {
|
|
3496
|
+
await page.context().setHTTPCredentials(null);
|
|
3497
|
+
return;
|
|
3498
|
+
}
|
|
3499
|
+
const username = opts.username ?? "";
|
|
3500
|
+
const password = opts.password ?? "";
|
|
3501
|
+
if (!username) throw new Error("username is required (or set clear=true)");
|
|
3502
|
+
await page.context().setHTTPCredentials({ username, password });
|
|
3503
|
+
}
|
|
3504
|
+
async function setLocaleViaPlaywright(opts) {
|
|
3505
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3506
|
+
ensurePageState(page);
|
|
3507
|
+
const locale = opts.locale.trim();
|
|
3508
|
+
if (!locale) throw new Error("locale is required");
|
|
3509
|
+
await withPageScopedCdpClient({
|
|
3510
|
+
cdpUrl: opts.cdpUrl,
|
|
3511
|
+
page,
|
|
3512
|
+
targetId: opts.targetId,
|
|
3513
|
+
fn: async (send) => {
|
|
3514
|
+
try {
|
|
3515
|
+
await send("Emulation.setLocaleOverride", { locale });
|
|
3516
|
+
} catch (err) {
|
|
3517
|
+
if (String(err).includes("Another locale override is already in effect")) return;
|
|
3518
|
+
throw err;
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
});
|
|
3522
|
+
}
|
|
3523
|
+
async function setOfflineViaPlaywright(opts) {
|
|
3524
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3525
|
+
ensurePageState(page);
|
|
3526
|
+
await page.context().setOffline(opts.offline);
|
|
3527
|
+
}
|
|
3528
|
+
async function setTimezoneViaPlaywright(opts) {
|
|
3529
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3530
|
+
ensurePageState(page);
|
|
3531
|
+
const timezoneId = opts.timezoneId.trim();
|
|
3532
|
+
if (!timezoneId) throw new Error("timezoneId is required");
|
|
3533
|
+
await withPageScopedCdpClient({
|
|
3534
|
+
cdpUrl: opts.cdpUrl,
|
|
3535
|
+
page,
|
|
3536
|
+
targetId: opts.targetId,
|
|
3537
|
+
fn: async (send) => {
|
|
3538
|
+
try {
|
|
3539
|
+
await send("Emulation.setTimezoneOverride", { timezoneId });
|
|
3540
|
+
} catch (err) {
|
|
3541
|
+
const msg = String(err);
|
|
3542
|
+
if (msg.includes("Timezone override is already in effect")) return;
|
|
3543
|
+
if (msg.includes("Invalid timezone")) throw new Error(`Invalid timezone ID: ${timezoneId}`, { cause: err });
|
|
3544
|
+
throw err;
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
});
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
// src/capture/activity.ts
|
|
3551
|
+
function consolePriority(level) {
|
|
3552
|
+
switch (level) {
|
|
3553
|
+
case "error":
|
|
3554
|
+
return 3;
|
|
3555
|
+
case "warning":
|
|
3556
|
+
case "warn":
|
|
3557
|
+
return 2;
|
|
3558
|
+
case "info":
|
|
3559
|
+
case "log":
|
|
3560
|
+
return 1;
|
|
3561
|
+
case "debug":
|
|
3562
|
+
return 0;
|
|
3563
|
+
default:
|
|
3564
|
+
return 1;
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
async function getConsoleMessagesViaPlaywright(opts) {
|
|
3568
|
+
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
3569
|
+
const messages = opts.level !== void 0 && opts.level !== "" ? state.console.filter((msg) => consolePriority(msg.type) >= consolePriority(opts.level ?? "")) : [...state.console];
|
|
3570
|
+
if (opts.clear === true) state.console = [];
|
|
3571
|
+
return messages;
|
|
3572
|
+
}
|
|
3573
|
+
async function getPageErrorsViaPlaywright(opts) {
|
|
3574
|
+
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
3575
|
+
const errors = [...state.errors];
|
|
3576
|
+
if (opts.clear === true) state.errors = [];
|
|
3577
|
+
return { errors };
|
|
3578
|
+
}
|
|
3579
|
+
async function getNetworkRequestsViaPlaywright(opts) {
|
|
3580
|
+
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
3581
|
+
const raw = [...state.requests];
|
|
3582
|
+
const filter = typeof opts.filter === "string" ? opts.filter.trim() : "";
|
|
3583
|
+
const requests = filter ? raw.filter((r) => r.url.includes(filter)) : raw;
|
|
3584
|
+
if (opts.clear === true) {
|
|
3585
|
+
state.requests = [];
|
|
3586
|
+
state.requestIds = /* @__PURE__ */ new WeakMap();
|
|
3587
|
+
}
|
|
3588
|
+
return { requests };
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
// src/capture/pdf.ts
|
|
3592
|
+
async function pdfViaPlaywright(opts) {
|
|
3735
3593
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
const
|
|
3594
|
+
ensurePageState(page);
|
|
3595
|
+
return { buffer: await page.pdf({ printBackground: true }) };
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
// src/capture/response.ts
|
|
3599
|
+
function matchUrlPattern(pattern, url) {
|
|
3600
|
+
if (!pattern || !url) return false;
|
|
3601
|
+
if (pattern === url) return true;
|
|
3602
|
+
if (pattern.includes("*")) {
|
|
3603
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
3746
3604
|
try {
|
|
3747
|
-
|
|
3748
|
-
} catch
|
|
3749
|
-
|
|
3605
|
+
return new RegExp(`^${escaped}$`).test(url);
|
|
3606
|
+
} catch {
|
|
3607
|
+
return false;
|
|
3750
3608
|
}
|
|
3751
|
-
return await awaitDownloadPayload({ waiter, state, armId, outPath });
|
|
3752
|
-
} catch (err) {
|
|
3753
|
-
waiter.cancel();
|
|
3754
|
-
throw err;
|
|
3755
3609
|
}
|
|
3610
|
+
return url.includes(pattern);
|
|
3756
3611
|
}
|
|
3757
|
-
async function
|
|
3612
|
+
async function responseBodyViaPlaywright(opts) {
|
|
3758
3613
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3759
|
-
|
|
3760
|
-
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
const
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
}
|
|
3771
|
-
|
|
3772
|
-
|
|
3614
|
+
ensurePageState(page);
|
|
3615
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
|
|
3616
|
+
const pattern = opts.url.trim();
|
|
3617
|
+
if (!pattern) throw new Error("url is required");
|
|
3618
|
+
const response = await page.waitForResponse((resp) => matchUrlPattern(pattern, resp.url()), { timeout });
|
|
3619
|
+
let body = await response.text();
|
|
3620
|
+
let truncated = false;
|
|
3621
|
+
const maxChars = typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars) ? Math.max(1, Math.min(5e6, Math.floor(opts.maxChars))) : 2e5;
|
|
3622
|
+
if (body.length > maxChars) {
|
|
3623
|
+
body = body.slice(0, maxChars);
|
|
3624
|
+
truncated = true;
|
|
3625
|
+
}
|
|
3626
|
+
const headers = {};
|
|
3627
|
+
const allHeaders = response.headers();
|
|
3628
|
+
for (const [key, value] of Object.entries(allHeaders)) {
|
|
3629
|
+
headers[key] = value;
|
|
3773
3630
|
}
|
|
3631
|
+
return {
|
|
3632
|
+
url: response.url(),
|
|
3633
|
+
status: response.status(),
|
|
3634
|
+
headers,
|
|
3635
|
+
body,
|
|
3636
|
+
truncated
|
|
3637
|
+
};
|
|
3774
3638
|
}
|
|
3775
|
-
|
|
3639
|
+
|
|
3640
|
+
// src/capture/screenshot.ts
|
|
3641
|
+
async function takeScreenshotViaPlaywright(opts) {
|
|
3776
3642
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3777
3643
|
ensurePageState(page);
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
if (
|
|
3785
|
-
throw new Error(
|
|
3644
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
3645
|
+
const type = opts.type ?? "png";
|
|
3646
|
+
if (opts.ref !== void 0 && opts.ref !== "") {
|
|
3647
|
+
if (opts.fullPage === true) throw new Error("fullPage is not supported for element screenshots");
|
|
3648
|
+
return { buffer: await refLocator(page, opts.ref).screenshot({ type }) };
|
|
3649
|
+
}
|
|
3650
|
+
if (opts.element !== void 0 && opts.element !== "") {
|
|
3651
|
+
if (opts.fullPage === true) throw new Error("fullPage is not supported for element screenshots");
|
|
3652
|
+
return { buffer: await page.locator(opts.element).first().screenshot({ type }) };
|
|
3786
3653
|
}
|
|
3654
|
+
return { buffer: await page.screenshot({ type, fullPage: Boolean(opts.fullPage) }) };
|
|
3655
|
+
}
|
|
3656
|
+
async function screenshotWithLabelsViaPlaywright(opts) {
|
|
3787
3657
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3788
3658
|
ensurePageState(page);
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
if (device.viewport) {
|
|
3808
|
-
await send("Emulation.setDeviceMetricsOverride", {
|
|
3809
|
-
mobile: Boolean(device.isMobile),
|
|
3810
|
-
width: device.viewport.width,
|
|
3811
|
-
height: device.viewport.height,
|
|
3812
|
-
deviceScaleFactor: device.deviceScaleFactor ?? 1,
|
|
3813
|
-
screenWidth: device.viewport.width,
|
|
3814
|
-
screenHeight: device.viewport.height
|
|
3815
|
-
});
|
|
3659
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
3660
|
+
const maxLabels = typeof opts.maxLabels === "number" && Number.isFinite(opts.maxLabels) ? Math.max(1, Math.floor(opts.maxLabels)) : 150;
|
|
3661
|
+
const type = opts.type ?? "png";
|
|
3662
|
+
const refs = opts.refs.slice(0, maxLabels);
|
|
3663
|
+
const skipped = opts.refs.slice(maxLabels);
|
|
3664
|
+
const viewport = await page.evaluate(() => ({
|
|
3665
|
+
width: window.innerWidth || 0,
|
|
3666
|
+
height: window.innerHeight || 0
|
|
3667
|
+
}));
|
|
3668
|
+
const labels = [];
|
|
3669
|
+
for (let i = 0; i < refs.length; i++) {
|
|
3670
|
+
const ref = refs[i];
|
|
3671
|
+
try {
|
|
3672
|
+
const locator = refLocator(page, ref);
|
|
3673
|
+
const box = await locator.boundingBox({ timeout: 2e3 });
|
|
3674
|
+
if (!box) {
|
|
3675
|
+
skipped.push(ref);
|
|
3676
|
+
continue;
|
|
3816
3677
|
}
|
|
3817
|
-
|
|
3818
|
-
|
|
3678
|
+
const x1 = box.x + box.width;
|
|
3679
|
+
const y1 = box.y + box.height;
|
|
3680
|
+
if (x1 < 0 || box.x > viewport.width || y1 < 0 || box.y > viewport.height) {
|
|
3681
|
+
skipped.push(ref);
|
|
3682
|
+
continue;
|
|
3819
3683
|
}
|
|
3684
|
+
labels.push({ ref, index: i + 1, box });
|
|
3685
|
+
} catch {
|
|
3686
|
+
skipped.push(ref);
|
|
3820
3687
|
}
|
|
3821
|
-
}
|
|
3688
|
+
}
|
|
3689
|
+
try {
|
|
3690
|
+
if (labels.length > 0) {
|
|
3691
|
+
await page.evaluate(
|
|
3692
|
+
(labelData) => {
|
|
3693
|
+
document.querySelectorAll("[data-browserclaw-labels]").forEach((el) => {
|
|
3694
|
+
el.remove();
|
|
3695
|
+
});
|
|
3696
|
+
const container = document.createElement("div");
|
|
3697
|
+
container.setAttribute("data-browserclaw-labels", "1");
|
|
3698
|
+
container.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:2147483647;";
|
|
3699
|
+
for (const { index, box } of labelData) {
|
|
3700
|
+
const border = document.createElement("div");
|
|
3701
|
+
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;`;
|
|
3702
|
+
container.appendChild(border);
|
|
3703
|
+
const badge = document.createElement("div");
|
|
3704
|
+
badge.textContent = String(index);
|
|
3705
|
+
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;`;
|
|
3706
|
+
container.appendChild(badge);
|
|
3707
|
+
}
|
|
3708
|
+
document.documentElement.appendChild(container);
|
|
3709
|
+
},
|
|
3710
|
+
labels.map((l) => ({ index: l.index, box: l.box }))
|
|
3711
|
+
);
|
|
3712
|
+
}
|
|
3713
|
+
return {
|
|
3714
|
+
buffer: await page.screenshot({ type }),
|
|
3715
|
+
labels,
|
|
3716
|
+
skipped
|
|
3717
|
+
};
|
|
3718
|
+
} finally {
|
|
3719
|
+
await page.evaluate(() => {
|
|
3720
|
+
document.querySelectorAll("[data-browserclaw-labels]").forEach((el) => {
|
|
3721
|
+
el.remove();
|
|
3722
|
+
});
|
|
3723
|
+
}).catch(() => {
|
|
3724
|
+
});
|
|
3725
|
+
}
|
|
3822
3726
|
}
|
|
3823
|
-
async function
|
|
3727
|
+
async function traceStartViaPlaywright(opts) {
|
|
3824
3728
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3825
3729
|
ensurePageState(page);
|
|
3826
|
-
|
|
3730
|
+
const context = page.context();
|
|
3731
|
+
const ctxState = ensureContextState(context);
|
|
3732
|
+
if (ctxState.traceActive) {
|
|
3733
|
+
throw new Error("Trace already running. Stop the current trace before starting a new one.");
|
|
3734
|
+
}
|
|
3735
|
+
await context.tracing.start({
|
|
3736
|
+
screenshots: opts.screenshots ?? true,
|
|
3737
|
+
snapshots: opts.snapshots ?? true,
|
|
3738
|
+
sources: opts.sources ?? false
|
|
3739
|
+
});
|
|
3740
|
+
ctxState.traceActive = true;
|
|
3827
3741
|
}
|
|
3828
|
-
async function
|
|
3742
|
+
async function traceStopViaPlaywright(opts) {
|
|
3743
|
+
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
3829
3744
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3830
3745
|
ensurePageState(page);
|
|
3831
3746
|
const context = page.context();
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3747
|
+
const ctxState = ensureContextState(context);
|
|
3748
|
+
if (!ctxState.traceActive) {
|
|
3749
|
+
throw new Error("No active trace. Start a trace before stopping it.");
|
|
3750
|
+
}
|
|
3751
|
+
await writeViaSiblingTempPath({
|
|
3752
|
+
rootDir: path.dirname(opts.path),
|
|
3753
|
+
targetPath: opts.path,
|
|
3754
|
+
writeTemp: async (tempPath) => {
|
|
3755
|
+
await context.tracing.stop({ path: tempPath });
|
|
3756
|
+
}
|
|
3757
|
+
});
|
|
3758
|
+
ctxState.traceActive = false;
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
// src/snapshot/ref-map.ts
|
|
3762
|
+
var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
3763
|
+
"button",
|
|
3764
|
+
"link",
|
|
3765
|
+
"textbox",
|
|
3766
|
+
"checkbox",
|
|
3767
|
+
"radio",
|
|
3768
|
+
"combobox",
|
|
3769
|
+
"listbox",
|
|
3770
|
+
"menuitem",
|
|
3771
|
+
"menuitemcheckbox",
|
|
3772
|
+
"menuitemradio",
|
|
3773
|
+
"option",
|
|
3774
|
+
"searchbox",
|
|
3775
|
+
"slider",
|
|
3776
|
+
"spinbutton",
|
|
3777
|
+
"switch",
|
|
3778
|
+
"tab",
|
|
3779
|
+
"treeitem"
|
|
3780
|
+
]);
|
|
3781
|
+
var CONTENT_ROLES = /* @__PURE__ */ new Set([
|
|
3782
|
+
"heading",
|
|
3783
|
+
"cell",
|
|
3784
|
+
"gridcell",
|
|
3785
|
+
"columnheader",
|
|
3786
|
+
"rowheader",
|
|
3787
|
+
"listitem",
|
|
3788
|
+
"article",
|
|
3789
|
+
"region",
|
|
3790
|
+
"main",
|
|
3791
|
+
"navigation"
|
|
3792
|
+
]);
|
|
3793
|
+
var STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
|
|
3794
|
+
"generic",
|
|
3795
|
+
"group",
|
|
3796
|
+
"list",
|
|
3797
|
+
"table",
|
|
3798
|
+
"row",
|
|
3799
|
+
"rowgroup",
|
|
3800
|
+
"grid",
|
|
3801
|
+
"treegrid",
|
|
3802
|
+
"menu",
|
|
3803
|
+
"menubar",
|
|
3804
|
+
"toolbar",
|
|
3805
|
+
"tablist",
|
|
3806
|
+
"tree",
|
|
3807
|
+
"directory",
|
|
3808
|
+
"document",
|
|
3809
|
+
"application",
|
|
3810
|
+
"presentation",
|
|
3811
|
+
"none"
|
|
3812
|
+
]);
|
|
3813
|
+
function getIndentLevel(line) {
|
|
3814
|
+
const match = /^(\s*)/.exec(line);
|
|
3815
|
+
return match ? Math.floor(match[1].length / 2) : 0;
|
|
3816
|
+
}
|
|
3817
|
+
function matchInteractiveSnapshotLine(line, options) {
|
|
3818
|
+
const depth = getIndentLevel(line);
|
|
3819
|
+
if (options.maxDepth !== void 0 && depth > options.maxDepth) {
|
|
3820
|
+
return null;
|
|
3837
3821
|
}
|
|
3838
|
-
|
|
3839
|
-
|
|
3822
|
+
const match = /^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/.exec(line);
|
|
3823
|
+
if (!match) {
|
|
3824
|
+
return null;
|
|
3840
3825
|
}
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
accuracy: typeof opts.accuracy === "number" ? opts.accuracy : void 0
|
|
3845
|
-
});
|
|
3846
|
-
const origin = opts.origin?.trim() || (() => {
|
|
3847
|
-
try {
|
|
3848
|
-
return new URL(page.url()).origin;
|
|
3849
|
-
} catch {
|
|
3850
|
-
return "";
|
|
3851
|
-
}
|
|
3852
|
-
})();
|
|
3853
|
-
if (origin) await context.grantPermissions(["geolocation"], { origin }).catch(() => {
|
|
3854
|
-
});
|
|
3855
|
-
}
|
|
3856
|
-
async function setHttpCredentialsViaPlaywright(opts) {
|
|
3857
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3858
|
-
ensurePageState(page);
|
|
3859
|
-
if (opts.clear) {
|
|
3860
|
-
await page.context().setHTTPCredentials(null);
|
|
3861
|
-
return;
|
|
3826
|
+
const [, , roleRaw, name, suffix] = match;
|
|
3827
|
+
if (roleRaw.startsWith("/")) {
|
|
3828
|
+
return null;
|
|
3862
3829
|
}
|
|
3863
|
-
const
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3830
|
+
const role = roleRaw.toLowerCase();
|
|
3831
|
+
return {
|
|
3832
|
+
roleRaw,
|
|
3833
|
+
role,
|
|
3834
|
+
...name ? { name } : {},
|
|
3835
|
+
suffix
|
|
3836
|
+
};
|
|
3867
3837
|
}
|
|
3868
|
-
|
|
3869
|
-
const
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3838
|
+
function createRoleNameTracker() {
|
|
3839
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3840
|
+
const refsByKey = /* @__PURE__ */ new Map();
|
|
3841
|
+
return {
|
|
3842
|
+
counts,
|
|
3843
|
+
refsByKey,
|
|
3844
|
+
getKey(role, name) {
|
|
3845
|
+
return `${role}:${name ?? ""}`;
|
|
3846
|
+
},
|
|
3847
|
+
getNextIndex(role, name) {
|
|
3848
|
+
const key = this.getKey(role, name);
|
|
3849
|
+
const current = counts.get(key) ?? 0;
|
|
3850
|
+
counts.set(key, current + 1);
|
|
3851
|
+
return current;
|
|
3852
|
+
},
|
|
3853
|
+
trackRef(role, name, ref) {
|
|
3854
|
+
const key = this.getKey(role, name);
|
|
3855
|
+
const list = refsByKey.get(key) ?? [];
|
|
3856
|
+
list.push(ref);
|
|
3857
|
+
refsByKey.set(key, list);
|
|
3858
|
+
},
|
|
3859
|
+
getDuplicateKeys() {
|
|
3860
|
+
const out = /* @__PURE__ */ new Set();
|
|
3861
|
+
for (const [key, refs] of refsByKey) if (refs.length > 1) out.add(key);
|
|
3862
|
+
return out;
|
|
3884
3863
|
}
|
|
3885
|
-
}
|
|
3864
|
+
};
|
|
3886
3865
|
}
|
|
3887
|
-
|
|
3888
|
-
const
|
|
3889
|
-
|
|
3890
|
-
|
|
3866
|
+
function removeNthFromNonDuplicates(refs, tracker) {
|
|
3867
|
+
const duplicates = tracker.getDuplicateKeys();
|
|
3868
|
+
for (const [ref, data] of Object.entries(refs)) {
|
|
3869
|
+
const key = tracker.getKey(data.role, data.name);
|
|
3870
|
+
if (!duplicates.has(key)) delete refs[ref].nth;
|
|
3871
|
+
}
|
|
3891
3872
|
}
|
|
3892
|
-
|
|
3893
|
-
const
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3873
|
+
function compactTree(tree) {
|
|
3874
|
+
const lines = tree.split("\n");
|
|
3875
|
+
const result = [];
|
|
3876
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3877
|
+
const line = lines[i];
|
|
3878
|
+
if (line.includes("[ref=")) {
|
|
3879
|
+
result.push(line);
|
|
3880
|
+
continue;
|
|
3881
|
+
}
|
|
3882
|
+
if (line.includes(":") && !line.trimEnd().endsWith(":")) {
|
|
3883
|
+
result.push(line);
|
|
3884
|
+
continue;
|
|
3885
|
+
}
|
|
3886
|
+
const currentIndent = getIndentLevel(line);
|
|
3887
|
+
let hasRelevantChildren = false;
|
|
3888
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
3889
|
+
if (getIndentLevel(lines[j]) <= currentIndent) break;
|
|
3890
|
+
if (lines[j]?.includes("[ref=")) {
|
|
3891
|
+
hasRelevantChildren = true;
|
|
3892
|
+
break;
|
|
3909
3893
|
}
|
|
3910
3894
|
}
|
|
3911
|
-
|
|
3895
|
+
if (hasRelevantChildren) result.push(line);
|
|
3896
|
+
}
|
|
3897
|
+
return result.join("\n");
|
|
3912
3898
|
}
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
const
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3899
|
+
function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
|
|
3900
|
+
const lines = ariaSnapshot.split("\n");
|
|
3901
|
+
const refs = {};
|
|
3902
|
+
const tracker = createRoleNameTracker();
|
|
3903
|
+
let counter = 0;
|
|
3904
|
+
const nextRef = () => {
|
|
3905
|
+
counter++;
|
|
3906
|
+
return `e${String(counter)}`;
|
|
3907
|
+
};
|
|
3908
|
+
if (options.interactive === true) {
|
|
3909
|
+
const result2 = [];
|
|
3910
|
+
for (const line of lines) {
|
|
3911
|
+
const parsed = matchInteractiveSnapshotLine(line, options);
|
|
3912
|
+
if (!parsed) continue;
|
|
3913
|
+
const { roleRaw, role, name, suffix } = parsed;
|
|
3914
|
+
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
3915
|
+
const prefix = /^(\s*-\s*)/.exec(line)?.[1] ?? "";
|
|
3916
|
+
const ref = nextRef();
|
|
3917
|
+
const nth = tracker.getNextIndex(role, name);
|
|
3918
|
+
tracker.trackRef(role, name, ref);
|
|
3919
|
+
refs[ref] = { role, name, nth };
|
|
3920
|
+
let enhanced = `${prefix}${roleRaw}`;
|
|
3921
|
+
if (name !== void 0 && name !== "") enhanced += ` "${name}"`;
|
|
3922
|
+
enhanced += ` [ref=${ref}]`;
|
|
3923
|
+
if (nth > 0) enhanced += ` [nth=${String(nth)}]`;
|
|
3924
|
+
if (suffix.includes("[")) enhanced += suffix;
|
|
3925
|
+
result2.push(enhanced);
|
|
3926
|
+
}
|
|
3927
|
+
removeNthFromNonDuplicates(refs, tracker);
|
|
3928
|
+
return { snapshot: result2.join("\n") || "(no interactive elements)", refs };
|
|
3923
3929
|
}
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3930
|
+
const result = [];
|
|
3931
|
+
for (const line of lines) {
|
|
3932
|
+
const depth = getIndentLevel(line);
|
|
3933
|
+
if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
|
|
3934
|
+
const match = /^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/.exec(line);
|
|
3935
|
+
if (!match) {
|
|
3936
|
+
result.push(line);
|
|
3937
|
+
continue;
|
|
3938
|
+
}
|
|
3939
|
+
const [, prefix, roleRaw, name, suffix] = match;
|
|
3940
|
+
if (roleRaw.startsWith("/")) {
|
|
3941
|
+
result.push(line);
|
|
3942
|
+
continue;
|
|
3943
|
+
}
|
|
3944
|
+
const role = roleRaw.toLowerCase();
|
|
3945
|
+
const isInteractive = INTERACTIVE_ROLES.has(role);
|
|
3946
|
+
const isContent = CONTENT_ROLES.has(role);
|
|
3947
|
+
const isStructural = STRUCTURAL_ROLES.has(role);
|
|
3948
|
+
if (options.compact === true && isStructural && name === "") continue;
|
|
3949
|
+
if (!(isInteractive || isContent && name !== "")) {
|
|
3950
|
+
result.push(line);
|
|
3951
|
+
continue;
|
|
3952
|
+
}
|
|
3953
|
+
const ref = nextRef();
|
|
3954
|
+
const nth = tracker.getNextIndex(role, name);
|
|
3955
|
+
tracker.trackRef(role, name, ref);
|
|
3956
|
+
refs[ref] = { role, name, nth };
|
|
3957
|
+
let enhanced = `${prefix}${roleRaw}`;
|
|
3958
|
+
if (name !== "") enhanced += ` "${name}"`;
|
|
3959
|
+
enhanced += ` [ref=${ref}]`;
|
|
3960
|
+
if (nth > 0) enhanced += ` [nth=${String(nth)}]`;
|
|
3961
|
+
if (suffix !== "") enhanced += suffix;
|
|
3962
|
+
result.push(enhanced);
|
|
3927
3963
|
}
|
|
3928
|
-
|
|
3964
|
+
removeNthFromNonDuplicates(refs, tracker);
|
|
3965
|
+
const tree = result.join("\n") || "(empty)";
|
|
3966
|
+
return { snapshot: options.compact === true ? compactTree(tree) : tree, refs };
|
|
3929
3967
|
}
|
|
3930
|
-
|
|
3931
|
-
const
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
const
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
skipped.push(ref);
|
|
3950
|
-
continue;
|
|
3951
|
-
}
|
|
3952
|
-
const x1 = box.x + box.width;
|
|
3953
|
-
const y1 = box.y + box.height;
|
|
3954
|
-
if (x1 < 0 || box.x > viewport.width || y1 < 0 || box.y > viewport.height) {
|
|
3955
|
-
skipped.push(ref);
|
|
3956
|
-
continue;
|
|
3957
|
-
}
|
|
3958
|
-
labels.push({ ref, index: i + 1, box });
|
|
3959
|
-
} catch {
|
|
3960
|
-
skipped.push(ref);
|
|
3968
|
+
function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
|
|
3969
|
+
const lines = aiSnapshot.split("\n");
|
|
3970
|
+
const refs = {};
|
|
3971
|
+
function parseAiSnapshotRef(suffix) {
|
|
3972
|
+
const match = /\[ref=(e\d+)\]/i.exec(suffix);
|
|
3973
|
+
return match ? match[1] : null;
|
|
3974
|
+
}
|
|
3975
|
+
if (options.interactive === true) {
|
|
3976
|
+
const out2 = [];
|
|
3977
|
+
for (const line of lines) {
|
|
3978
|
+
const parsed = matchInteractiveSnapshotLine(line, options);
|
|
3979
|
+
if (!parsed) continue;
|
|
3980
|
+
const { roleRaw, role, name, suffix } = parsed;
|
|
3981
|
+
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
3982
|
+
const ref = parseAiSnapshotRef(suffix);
|
|
3983
|
+
if (ref === null) continue;
|
|
3984
|
+
const prefix = /^(\s*-\s*)/.exec(line)?.[1] ?? "";
|
|
3985
|
+
refs[ref] = { role, ...name !== void 0 && name !== "" ? { name } : {} };
|
|
3986
|
+
out2.push(`${prefix}${roleRaw}${name !== void 0 && name !== "" ? ` "${name}"` : ""}${suffix}`);
|
|
3961
3987
|
}
|
|
3988
|
+
return { snapshot: out2.join("\n") || "(no interactive elements)", refs };
|
|
3962
3989
|
}
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
const border = document.createElement("div");
|
|
3972
|
-
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;`;
|
|
3973
|
-
container.appendChild(border);
|
|
3974
|
-
const badge = document.createElement("div");
|
|
3975
|
-
badge.textContent = String(index);
|
|
3976
|
-
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;`;
|
|
3977
|
-
container.appendChild(badge);
|
|
3978
|
-
}
|
|
3979
|
-
document.documentElement.appendChild(container);
|
|
3980
|
-
}, labels.map((l) => ({ index: l.index, box: l.box })));
|
|
3990
|
+
const out = [];
|
|
3991
|
+
for (const line of lines) {
|
|
3992
|
+
const depth = getIndentLevel(line);
|
|
3993
|
+
if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
|
|
3994
|
+
const match = /^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/.exec(line);
|
|
3995
|
+
if (!match) {
|
|
3996
|
+
out.push(line);
|
|
3997
|
+
continue;
|
|
3981
3998
|
}
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
}
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
}
|
|
3999
|
+
const [, , roleRaw, name, suffix] = match;
|
|
4000
|
+
if (roleRaw.startsWith("/")) {
|
|
4001
|
+
out.push(line);
|
|
4002
|
+
continue;
|
|
4003
|
+
}
|
|
4004
|
+
const role = roleRaw.toLowerCase();
|
|
4005
|
+
const isStructural = STRUCTURAL_ROLES.has(role);
|
|
4006
|
+
if (options.compact === true && isStructural && name === "") continue;
|
|
4007
|
+
const ref = parseAiSnapshotRef(suffix);
|
|
4008
|
+
if (ref !== null) refs[ref] = { role, ...name !== "" ? { name } : {} };
|
|
4009
|
+
out.push(line);
|
|
3992
4010
|
}
|
|
4011
|
+
const tree = out.join("\n") || "(empty)";
|
|
4012
|
+
return { snapshot: options.compact === true ? compactTree(tree) : tree, refs };
|
|
3993
4013
|
}
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4014
|
+
function getRoleSnapshotStats(snapshot, refs) {
|
|
4015
|
+
const interactive = Object.values(refs).filter((r) => INTERACTIVE_ROLES.has(r.role)).length;
|
|
4016
|
+
return {
|
|
4017
|
+
lines: snapshot.split("\n").length,
|
|
4018
|
+
chars: snapshot.length,
|
|
4019
|
+
refs: Object.keys(refs).length,
|
|
4020
|
+
interactive
|
|
4021
|
+
};
|
|
4000
4022
|
}
|
|
4001
|
-
|
|
4023
|
+
|
|
4024
|
+
// src/snapshot/ai-snapshot.ts
|
|
4025
|
+
async function snapshotAi(opts) {
|
|
4002
4026
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
4003
4027
|
ensurePageState(page);
|
|
4004
|
-
const
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
throw new Error("Trace already running. Stop the current trace before starting a new one.");
|
|
4028
|
+
const maybe = page;
|
|
4029
|
+
if (!maybe._snapshotForAI) {
|
|
4030
|
+
throw new Error("Playwright _snapshotForAI is not available. Upgrade playwright-core to >= 1.50.");
|
|
4008
4031
|
}
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4032
|
+
const sourceUrl = page.url();
|
|
4033
|
+
const result = await maybe._snapshotForAI({
|
|
4034
|
+
timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3, 6e4),
|
|
4035
|
+
track: "response"
|
|
4013
4036
|
});
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4037
|
+
let snapshot = String(result.full);
|
|
4038
|
+
const maxChars = opts.maxChars;
|
|
4039
|
+
const limit = typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : void 0;
|
|
4040
|
+
let truncated = false;
|
|
4041
|
+
if (limit !== void 0 && snapshot.length > limit) {
|
|
4042
|
+
const lastNewline = snapshot.lastIndexOf("\n", limit);
|
|
4043
|
+
const cutoff = lastNewline > 0 ? lastNewline : limit;
|
|
4044
|
+
snapshot = `${snapshot.slice(0, cutoff)}
|
|
4045
|
+
|
|
4046
|
+
[...TRUNCATED - page too large]`;
|
|
4047
|
+
truncated = true;
|
|
4024
4048
|
}
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4049
|
+
const built = buildRoleSnapshotFromAiSnapshot(snapshot, opts.options);
|
|
4050
|
+
storeRoleRefsForTarget({
|
|
4051
|
+
page,
|
|
4052
|
+
cdpUrl: opts.cdpUrl,
|
|
4053
|
+
targetId: opts.targetId,
|
|
4054
|
+
refs: built.refs,
|
|
4055
|
+
mode: "aria"
|
|
4031
4056
|
});
|
|
4032
|
-
|
|
4057
|
+
return {
|
|
4058
|
+
snapshot: built.snapshot,
|
|
4059
|
+
refs: built.refs,
|
|
4060
|
+
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
4061
|
+
...truncated ? { truncated } : {},
|
|
4062
|
+
untrusted: true,
|
|
4063
|
+
contentMeta: {
|
|
4064
|
+
sourceUrl,
|
|
4065
|
+
contentType: "browser-snapshot",
|
|
4066
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4067
|
+
}
|
|
4068
|
+
};
|
|
4033
4069
|
}
|
|
4034
4070
|
|
|
4035
|
-
// src/
|
|
4036
|
-
function
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
}
|
|
4044
|
-
|
|
4071
|
+
// src/snapshot/aria-snapshot.ts
|
|
4072
|
+
async function snapshotRole(opts) {
|
|
4073
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
4074
|
+
ensurePageState(page);
|
|
4075
|
+
const sourceUrl = page.url();
|
|
4076
|
+
if (opts.refsMode === "aria") {
|
|
4077
|
+
if (opts.selector !== void 0 && opts.selector.trim() !== "" || opts.frameSelector !== void 0 && opts.frameSelector.trim() !== "") {
|
|
4078
|
+
throw new Error("refs=aria does not support selector/frame snapshots yet.");
|
|
4079
|
+
}
|
|
4080
|
+
const maybe = page;
|
|
4081
|
+
if (!maybe._snapshotForAI) {
|
|
4082
|
+
throw new Error("refs=aria requires Playwright _snapshotForAI support.");
|
|
4045
4083
|
}
|
|
4084
|
+
const result = await maybe._snapshotForAI({ timeout: 5e3, track: "response" });
|
|
4085
|
+
const built2 = buildRoleSnapshotFromAiSnapshot(String(result.full), opts.options);
|
|
4086
|
+
storeRoleRefsForTarget({
|
|
4087
|
+
page,
|
|
4088
|
+
cdpUrl: opts.cdpUrl,
|
|
4089
|
+
targetId: opts.targetId,
|
|
4090
|
+
refs: built2.refs,
|
|
4091
|
+
mode: "aria"
|
|
4092
|
+
});
|
|
4093
|
+
return {
|
|
4094
|
+
snapshot: built2.snapshot,
|
|
4095
|
+
refs: built2.refs,
|
|
4096
|
+
stats: getRoleSnapshotStats(built2.snapshot, built2.refs),
|
|
4097
|
+
untrusted: true,
|
|
4098
|
+
contentMeta: {
|
|
4099
|
+
sourceUrl,
|
|
4100
|
+
contentType: "browser-snapshot",
|
|
4101
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4102
|
+
}
|
|
4103
|
+
};
|
|
4046
4104
|
}
|
|
4047
|
-
|
|
4105
|
+
const frameSelector = opts.frameSelector?.trim() ?? "";
|
|
4106
|
+
const selector = opts.selector?.trim() ?? "";
|
|
4107
|
+
const locator = frameSelector ? selector ? page.frameLocator(frameSelector).locator(selector) : page.frameLocator(frameSelector).locator(":root") : selector ? page.locator(selector) : page.locator(":root");
|
|
4108
|
+
const ariaSnapshot = await locator.ariaSnapshot({ timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3) });
|
|
4109
|
+
const built = buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, opts.options);
|
|
4110
|
+
storeRoleRefsForTarget({
|
|
4111
|
+
page,
|
|
4112
|
+
cdpUrl: opts.cdpUrl,
|
|
4113
|
+
targetId: opts.targetId,
|
|
4114
|
+
refs: built.refs,
|
|
4115
|
+
frameSelector: frameSelector !== "" ? frameSelector : void 0,
|
|
4116
|
+
mode: "role"
|
|
4117
|
+
});
|
|
4118
|
+
return {
|
|
4119
|
+
snapshot: built.snapshot,
|
|
4120
|
+
refs: built.refs,
|
|
4121
|
+
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
4122
|
+
untrusted: true,
|
|
4123
|
+
contentMeta: {
|
|
4124
|
+
sourceUrl,
|
|
4125
|
+
contentType: "browser-snapshot",
|
|
4126
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4127
|
+
}
|
|
4128
|
+
};
|
|
4048
4129
|
}
|
|
4049
|
-
async function
|
|
4130
|
+
async function snapshotAria(opts) {
|
|
4131
|
+
const limit = Math.max(1, Math.min(2e3, Math.floor(opts.limit ?? 500)));
|
|
4050
4132
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
4051
4133
|
ensurePageState(page);
|
|
4052
|
-
const
|
|
4053
|
-
const
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
);
|
|
4059
|
-
let body = await response.text();
|
|
4060
|
-
let truncated = false;
|
|
4061
|
-
const maxChars = typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars) ? Math.max(1, Math.min(5e6, Math.floor(opts.maxChars))) : 2e5;
|
|
4062
|
-
if (body.length > maxChars) {
|
|
4063
|
-
body = body.slice(0, maxChars);
|
|
4064
|
-
truncated = true;
|
|
4065
|
-
}
|
|
4066
|
-
const headers = {};
|
|
4067
|
-
const allHeaders = response.headers();
|
|
4068
|
-
for (const [key, value] of Object.entries(allHeaders)) {
|
|
4069
|
-
headers[key] = value;
|
|
4070
|
-
}
|
|
4134
|
+
const sourceUrl = page.url();
|
|
4135
|
+
const res = await withPlaywrightPageCdpSession(page, async (session) => {
|
|
4136
|
+
await session.send("Accessibility.enable").catch(() => {
|
|
4137
|
+
});
|
|
4138
|
+
return await session.send("Accessibility.getFullAXTree");
|
|
4139
|
+
});
|
|
4071
4140
|
return {
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4141
|
+
nodes: formatAriaNodes(Array.isArray(res.nodes) ? res.nodes : [], limit),
|
|
4142
|
+
untrusted: true,
|
|
4143
|
+
contentMeta: {
|
|
4144
|
+
sourceUrl,
|
|
4145
|
+
contentType: "browser-aria-tree",
|
|
4146
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4147
|
+
}
|
|
4077
4148
|
};
|
|
4078
4149
|
}
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
case "warning":
|
|
4086
|
-
case "warn":
|
|
4087
|
-
return 2;
|
|
4088
|
-
case "info":
|
|
4089
|
-
case "log":
|
|
4090
|
-
return 1;
|
|
4091
|
-
case "debug":
|
|
4092
|
-
return 0;
|
|
4093
|
-
default:
|
|
4094
|
-
return 1;
|
|
4095
|
-
}
|
|
4096
|
-
}
|
|
4097
|
-
async function getConsoleMessagesViaPlaywright(opts) {
|
|
4098
|
-
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
4099
|
-
const messages = opts.level ? state.console.filter((msg) => consolePriority(msg.type) >= consolePriority(opts.level)) : [...state.console];
|
|
4100
|
-
if (opts.clear) state.console = [];
|
|
4101
|
-
return messages;
|
|
4102
|
-
}
|
|
4103
|
-
async function getPageErrorsViaPlaywright(opts) {
|
|
4104
|
-
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
4105
|
-
const errors = [...state.errors];
|
|
4106
|
-
if (opts.clear) state.errors = [];
|
|
4107
|
-
return { errors };
|
|
4150
|
+
function axValue(v) {
|
|
4151
|
+
if (!v || typeof v !== "object") return "";
|
|
4152
|
+
const value = v.value;
|
|
4153
|
+
if (typeof value === "string") return value;
|
|
4154
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
4155
|
+
return "";
|
|
4108
4156
|
}
|
|
4109
|
-
|
|
4110
|
-
const
|
|
4111
|
-
const
|
|
4112
|
-
const
|
|
4113
|
-
const
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4157
|
+
function formatAriaNodes(nodes, limit) {
|
|
4158
|
+
const byId = /* @__PURE__ */ new Map();
|
|
4159
|
+
for (const n of nodes) if (n.nodeId) byId.set(n.nodeId, n);
|
|
4160
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
4161
|
+
for (const n of nodes) for (const c of n.childIds ?? []) referenced.add(c);
|
|
4162
|
+
const root = nodes.find((n) => n.nodeId !== "" && !referenced.has(n.nodeId)) ?? nodes[0];
|
|
4163
|
+
if (root.nodeId === "") return [];
|
|
4164
|
+
const out = [];
|
|
4165
|
+
const stack = [{ id: root.nodeId, depth: 0 }];
|
|
4166
|
+
while (stack.length && out.length < limit) {
|
|
4167
|
+
const popped = stack.pop();
|
|
4168
|
+
if (!popped) break;
|
|
4169
|
+
const { id, depth } = popped;
|
|
4170
|
+
const n = byId.get(id);
|
|
4171
|
+
if (!n) continue;
|
|
4172
|
+
const role = axValue(n.role);
|
|
4173
|
+
const name = axValue(n.name);
|
|
4174
|
+
const value = axValue(n.value);
|
|
4175
|
+
const description = axValue(n.description);
|
|
4176
|
+
const ref = `ax${String(out.length + 1)}`;
|
|
4177
|
+
out.push({
|
|
4178
|
+
ref,
|
|
4179
|
+
role: role || "unknown",
|
|
4180
|
+
name: name || "",
|
|
4181
|
+
...value ? { value } : {},
|
|
4182
|
+
...description ? { description } : {},
|
|
4183
|
+
...typeof n.backendDOMNodeId === "number" ? { backendDOMNodeId: n.backendDOMNodeId } : {},
|
|
4184
|
+
depth
|
|
4185
|
+
});
|
|
4186
|
+
const children = (n.childIds ?? []).filter((c) => byId.has(c));
|
|
4187
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
4188
|
+
if (children[i]) stack.push({ id: children[i], depth: depth + 1 });
|
|
4189
|
+
}
|
|
4117
4190
|
}
|
|
4118
|
-
return
|
|
4191
|
+
return out;
|
|
4119
4192
|
}
|
|
4120
4193
|
|
|
4121
4194
|
// src/storage/index.ts
|
|
@@ -4128,9 +4201,9 @@ async function cookiesSetViaPlaywright(opts) {
|
|
|
4128
4201
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
4129
4202
|
ensurePageState(page);
|
|
4130
4203
|
const cookie = opts.cookie;
|
|
4131
|
-
if (
|
|
4132
|
-
const hasUrl = typeof cookie.url === "string" && cookie.url.trim();
|
|
4133
|
-
const hasDomainPath = typeof cookie.domain === "string" && cookie.domain.trim() && typeof cookie.path === "string" && cookie.path.trim();
|
|
4204
|
+
if (cookie.name === "") throw new Error("cookie name and value are required");
|
|
4205
|
+
const hasUrl = typeof cookie.url === "string" && cookie.url.trim() !== "";
|
|
4206
|
+
const hasDomainPath = typeof cookie.domain === "string" && cookie.domain.trim() !== "" && typeof cookie.path === "string" && cookie.path.trim() !== "";
|
|
4134
4207
|
if (!hasUrl && !hasDomainPath) throw new Error("cookie requires url, or domain+path");
|
|
4135
4208
|
await page.context().addCookies([cookie]);
|
|
4136
4209
|
}
|
|
@@ -4146,33 +4219,33 @@ async function storageGetViaPlaywright(opts) {
|
|
|
4146
4219
|
values: await page.evaluate(
|
|
4147
4220
|
({ kind, key }) => {
|
|
4148
4221
|
const store = kind === "session" ? window.sessionStorage : window.localStorage;
|
|
4149
|
-
if (key) {
|
|
4222
|
+
if (key !== void 0 && key !== "") {
|
|
4150
4223
|
const value = store.getItem(key);
|
|
4151
4224
|
return value === null ? {} : { [key]: value };
|
|
4152
4225
|
}
|
|
4153
4226
|
const out = {};
|
|
4154
4227
|
for (let i = 0; i < store.length; i++) {
|
|
4155
4228
|
const k = store.key(i);
|
|
4156
|
-
if (
|
|
4229
|
+
if (k === null || k === "") continue;
|
|
4157
4230
|
const v = store.getItem(k);
|
|
4158
4231
|
if (v !== null) out[k] = v;
|
|
4159
4232
|
}
|
|
4160
4233
|
return out;
|
|
4161
4234
|
},
|
|
4162
4235
|
{ kind: opts.kind, key: opts.key }
|
|
4163
|
-
)
|
|
4236
|
+
)
|
|
4164
4237
|
};
|
|
4165
4238
|
}
|
|
4166
4239
|
async function storageSetViaPlaywright(opts) {
|
|
4167
|
-
const key =
|
|
4168
|
-
if (
|
|
4240
|
+
const key = opts.key;
|
|
4241
|
+
if (key === "") throw new Error("key is required");
|
|
4169
4242
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
4170
4243
|
ensurePageState(page);
|
|
4171
4244
|
await page.evaluate(
|
|
4172
4245
|
({ kind, key: k, value }) => {
|
|
4173
4246
|
(kind === "session" ? window.sessionStorage : window.localStorage).setItem(k, value);
|
|
4174
4247
|
},
|
|
4175
|
-
{ kind: opts.kind, key, value:
|
|
4248
|
+
{ kind: opts.kind, key, value: opts.value }
|
|
4176
4249
|
);
|
|
4177
4250
|
}
|
|
4178
4251
|
async function storageClearViaPlaywright(opts) {
|
|
@@ -4239,8 +4312,10 @@ var CrawlPage = class {
|
|
|
4239
4312
|
}
|
|
4240
4313
|
});
|
|
4241
4314
|
}
|
|
4242
|
-
if (opts?.selector || opts?.frameSelector) {
|
|
4243
|
-
throw new Error(
|
|
4315
|
+
if (opts?.selector !== void 0 && opts.selector !== "" || opts?.frameSelector !== void 0 && opts.frameSelector !== "") {
|
|
4316
|
+
throw new Error(
|
|
4317
|
+
'selector and frameSelector are only supported in role mode. Use { mode: "role" } or omit these options.'
|
|
4318
|
+
);
|
|
4244
4319
|
}
|
|
4245
4320
|
return snapshotAi({
|
|
4246
4321
|
cdpUrl: this.cdpUrl,
|
|
@@ -5111,8 +5186,8 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
5111
5186
|
*/
|
|
5112
5187
|
static async launch(opts = {}) {
|
|
5113
5188
|
const chrome = await launchChrome(opts);
|
|
5114
|
-
const cdpUrl = `http://127.0.0.1:${chrome.cdpPort}`;
|
|
5115
|
-
const ssrfPolicy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
5189
|
+
const cdpUrl = `http://127.0.0.1:${String(chrome.cdpPort)}`;
|
|
5190
|
+
const ssrfPolicy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
5116
5191
|
return new _BrowserClaw(cdpUrl, chrome, ssrfPolicy);
|
|
5117
5192
|
}
|
|
5118
5193
|
/**
|
|
@@ -5134,7 +5209,7 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
5134
5209
|
throw new Error(`Cannot connect to Chrome at ${cdpUrl}. Is Chrome running with --remote-debugging-port?`);
|
|
5135
5210
|
}
|
|
5136
5211
|
await connectBrowser(cdpUrl, opts?.authToken);
|
|
5137
|
-
const ssrfPolicy = opts?.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts?.ssrfPolicy;
|
|
5212
|
+
const ssrfPolicy = opts?.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts?.ssrfPolicy;
|
|
5138
5213
|
return new _BrowserClaw(cdpUrl, null, ssrfPolicy);
|
|
5139
5214
|
}
|
|
5140
5215
|
/**
|
|
@@ -5160,10 +5235,10 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
5160
5235
|
*/
|
|
5161
5236
|
async currentPage() {
|
|
5162
5237
|
const { browser } = await connectBrowser(this.cdpUrl);
|
|
5163
|
-
const pages =
|
|
5238
|
+
const pages = getAllPages(browser);
|
|
5164
5239
|
if (!pages.length) throw new Error("No pages available. Use browser.open(url) to create a tab.");
|
|
5165
5240
|
const tid = await pageTargetId(pages[0]).catch(() => null);
|
|
5166
|
-
if (
|
|
5241
|
+
if (tid === null || tid === "") throw new Error("Failed to get targetId for the current page.");
|
|
5167
5242
|
return new CrawlPage(this.cdpUrl, tid, this.ssrfPolicy);
|
|
5168
5243
|
}
|
|
5169
5244
|
/**
|