browserclaw 0.5.7 → 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 +1638 -1202
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +222 -17
- package/dist/index.d.ts +222 -17
- package/dist/index.js +1622 -1200
- 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,9 +1486,55 @@ async function stopChrome(running, timeoutMs = 2500) {
|
|
|
1441
1486
|
} catch {
|
|
1442
1487
|
}
|
|
1443
1488
|
}
|
|
1489
|
+
|
|
1490
|
+
// src/connection.ts
|
|
1491
|
+
var BrowserTabNotFoundError = class extends Error {
|
|
1492
|
+
constructor(message = "Tab not found") {
|
|
1493
|
+
super(message);
|
|
1494
|
+
this.name = "BrowserTabNotFoundError";
|
|
1495
|
+
}
|
|
1496
|
+
};
|
|
1497
|
+
async function fetchJsonForCdp(url, timeoutMs) {
|
|
1498
|
+
const ctrl = new AbortController();
|
|
1499
|
+
const t = setTimeout(() => {
|
|
1500
|
+
ctrl.abort();
|
|
1501
|
+
}, timeoutMs);
|
|
1502
|
+
try {
|
|
1503
|
+
const res = await fetch(url, { signal: ctrl.signal });
|
|
1504
|
+
if (!res.ok) return null;
|
|
1505
|
+
return await res.json();
|
|
1506
|
+
} catch {
|
|
1507
|
+
return null;
|
|
1508
|
+
} finally {
|
|
1509
|
+
clearTimeout(t);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
function appendCdpPath2(cdpUrl, cdpPath) {
|
|
1513
|
+
try {
|
|
1514
|
+
const url = new URL(cdpUrl);
|
|
1515
|
+
url.pathname = `${url.pathname.replace(/\/$/, "")}${cdpPath.startsWith("/") ? cdpPath : `/${cdpPath}`}`;
|
|
1516
|
+
return url.toString();
|
|
1517
|
+
} catch {
|
|
1518
|
+
return `${cdpUrl.replace(/\/$/, "")}${cdpPath}`;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
async function withPlaywrightPageCdpSession(page, fn) {
|
|
1522
|
+
const session = await page.context().newCDPSession(page);
|
|
1523
|
+
try {
|
|
1524
|
+
return await fn(session);
|
|
1525
|
+
} finally {
|
|
1526
|
+
await session.detach().catch(() => {
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
async function withPageScopedCdpClient(opts) {
|
|
1531
|
+
return await withPlaywrightPageCdpSession(opts.page, async (session) => {
|
|
1532
|
+
return await opts.fn((method, params) => session.send(method, params));
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1444
1535
|
var LOOPBACK_ENTRIES = "localhost,127.0.0.1,[::1]";
|
|
1445
1536
|
function noProxyAlreadyCoversLocalhost() {
|
|
1446
|
-
const current = process.env.NO_PROXY
|
|
1537
|
+
const current = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
|
|
1447
1538
|
return current.includes("localhost") && current.includes("127.0.0.1") && current.includes("[::1]");
|
|
1448
1539
|
}
|
|
1449
1540
|
function isLoopbackCdpUrl(url) {
|
|
@@ -1461,7 +1552,7 @@ var NoProxyLeaseManager = class {
|
|
|
1461
1552
|
if (this.leaseCount === 0 && !noProxyAlreadyCoversLocalhost()) {
|
|
1462
1553
|
const noProxy = process.env.NO_PROXY;
|
|
1463
1554
|
const noProxyLower = process.env.no_proxy;
|
|
1464
|
-
const current = noProxy
|
|
1555
|
+
const current = noProxy ?? noProxyLower ?? "";
|
|
1465
1556
|
const applied = current ? `${current},${LOOPBACK_ENTRIES}` : LOOPBACK_ENTRIES;
|
|
1466
1557
|
process.env.NO_PROXY = applied;
|
|
1467
1558
|
process.env.no_proxy = applied;
|
|
@@ -1506,9 +1597,12 @@ function getHeadersWithAuth(endpoint, baseHeaders = {}) {
|
|
|
1506
1597
|
const headers = { ...baseHeaders };
|
|
1507
1598
|
try {
|
|
1508
1599
|
const parsed = new URL(endpoint);
|
|
1509
|
-
if (
|
|
1510
|
-
|
|
1511
|
-
|
|
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}`;
|
|
1512
1606
|
}
|
|
1513
1607
|
} catch {
|
|
1514
1608
|
}
|
|
@@ -1556,7 +1650,7 @@ function roleRefsKey(cdpUrl, targetId) {
|
|
|
1556
1650
|
function findNetworkRequestById(state, id) {
|
|
1557
1651
|
for (let i = state.requests.length - 1; i >= 0; i--) {
|
|
1558
1652
|
const candidate = state.requests[i];
|
|
1559
|
-
if (candidate
|
|
1653
|
+
if (candidate.id === id) return candidate;
|
|
1560
1654
|
}
|
|
1561
1655
|
return void 0;
|
|
1562
1656
|
}
|
|
@@ -1587,16 +1681,16 @@ function ensurePageState(page) {
|
|
|
1587
1681
|
});
|
|
1588
1682
|
page.on("pageerror", (err) => {
|
|
1589
1683
|
state.errors.push({
|
|
1590
|
-
message: err
|
|
1591
|
-
name: err
|
|
1592
|
-
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,
|
|
1593
1687
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1594
1688
|
});
|
|
1595
1689
|
if (state.errors.length > MAX_PAGE_ERRORS) state.errors.shift();
|
|
1596
1690
|
});
|
|
1597
1691
|
page.on("request", (req) => {
|
|
1598
1692
|
state.nextRequestId += 1;
|
|
1599
|
-
const id = `r${state.nextRequestId}`;
|
|
1693
|
+
const id = `r${String(state.nextRequestId)}`;
|
|
1600
1694
|
state.requestIds.set(req, id);
|
|
1601
1695
|
state.requests.push({
|
|
1602
1696
|
id,
|
|
@@ -1610,7 +1704,7 @@ function ensurePageState(page) {
|
|
|
1610
1704
|
page.on("response", (resp) => {
|
|
1611
1705
|
const req = resp.request();
|
|
1612
1706
|
const id = state.requestIds.get(req);
|
|
1613
|
-
if (
|
|
1707
|
+
if (id === void 0) return;
|
|
1614
1708
|
const rec = findNetworkRequestById(state, id);
|
|
1615
1709
|
if (rec) {
|
|
1616
1710
|
rec.status = resp.status();
|
|
@@ -1619,7 +1713,7 @@ function ensurePageState(page) {
|
|
|
1619
1713
|
});
|
|
1620
1714
|
page.on("requestfailed", (req) => {
|
|
1621
1715
|
const id = state.requestIds.get(req);
|
|
1622
|
-
if (
|
|
1716
|
+
if (id === void 0) return;
|
|
1623
1717
|
const rec = findNetworkRequestById(state, id);
|
|
1624
1718
|
if (rec) {
|
|
1625
1719
|
rec.failureText = req.failure()?.errorText;
|
|
@@ -1636,7 +1730,8 @@ function ensurePageState(page) {
|
|
|
1636
1730
|
var STEALTH_SCRIPT = `Object.defineProperty(navigator, 'webdriver', { get: () => undefined })`;
|
|
1637
1731
|
function applyStealthToPage(page) {
|
|
1638
1732
|
page.evaluate(STEALTH_SCRIPT).catch((e) => {
|
|
1639
|
-
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));
|
|
1640
1735
|
});
|
|
1641
1736
|
}
|
|
1642
1737
|
function observeContext(context) {
|
|
@@ -1644,7 +1739,8 @@ function observeContext(context) {
|
|
|
1644
1739
|
observedContexts.add(context);
|
|
1645
1740
|
ensureContextState(context);
|
|
1646
1741
|
context.addInitScript(STEALTH_SCRIPT).catch((e) => {
|
|
1647
|
-
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));
|
|
1648
1744
|
});
|
|
1649
1745
|
for (const page of context.pages()) {
|
|
1650
1746
|
ensurePageState(page);
|
|
@@ -1660,15 +1756,15 @@ function observeBrowser(browser) {
|
|
|
1660
1756
|
}
|
|
1661
1757
|
function rememberRoleRefsForTarget(opts) {
|
|
1662
1758
|
const targetId = opts.targetId.trim();
|
|
1663
|
-
if (
|
|
1759
|
+
if (targetId === "") return;
|
|
1664
1760
|
roleRefsByTarget.set(roleRefsKey(opts.cdpUrl, targetId), {
|
|
1665
1761
|
refs: opts.refs,
|
|
1666
|
-
...opts.frameSelector ? { frameSelector: opts.frameSelector } : {},
|
|
1667
|
-
...opts.mode ? { mode: opts.mode } : {}
|
|
1762
|
+
...opts.frameSelector !== void 0 && opts.frameSelector !== "" ? { frameSelector: opts.frameSelector } : {},
|
|
1763
|
+
...opts.mode !== void 0 ? { mode: opts.mode } : {}
|
|
1668
1764
|
});
|
|
1669
1765
|
while (roleRefsByTarget.size > MAX_ROLE_REFS_CACHE) {
|
|
1670
1766
|
const first = roleRefsByTarget.keys().next();
|
|
1671
|
-
if (first.done) break;
|
|
1767
|
+
if (first.done === true) break;
|
|
1672
1768
|
roleRefsByTarget.delete(first.value);
|
|
1673
1769
|
}
|
|
1674
1770
|
}
|
|
@@ -1677,7 +1773,7 @@ function storeRoleRefsForTarget(opts) {
|
|
|
1677
1773
|
state.roleRefs = opts.refs;
|
|
1678
1774
|
state.roleRefsFrameSelector = opts.frameSelector;
|
|
1679
1775
|
state.roleRefsMode = opts.mode;
|
|
1680
|
-
if (
|
|
1776
|
+
if (opts.targetId === void 0 || opts.targetId.trim() === "") return;
|
|
1681
1777
|
rememberRoleRefsForTarget({
|
|
1682
1778
|
cdpUrl: opts.cdpUrl,
|
|
1683
1779
|
targetId: opts.targetId,
|
|
@@ -1687,8 +1783,8 @@ function storeRoleRefsForTarget(opts) {
|
|
|
1687
1783
|
});
|
|
1688
1784
|
}
|
|
1689
1785
|
function restoreRoleRefsForTarget(opts) {
|
|
1690
|
-
const targetId = opts.targetId?.trim()
|
|
1691
|
-
if (
|
|
1786
|
+
const targetId = opts.targetId?.trim() ?? "";
|
|
1787
|
+
if (targetId === "") return;
|
|
1692
1788
|
const entry = roleRefsByTarget.get(roleRefsKey(opts.cdpUrl, targetId));
|
|
1693
1789
|
if (!entry) return;
|
|
1694
1790
|
const state = ensurePageState(opts.page);
|
|
@@ -1710,12 +1806,18 @@ async function connectBrowser(cdpUrl, authToken) {
|
|
|
1710
1806
|
const timeout = 5e3 + attempt * 2e3;
|
|
1711
1807
|
const endpoint = await getChromeWebSocketUrl(normalized, timeout, authToken).catch(() => null) ?? normalized;
|
|
1712
1808
|
const headers = getHeadersWithAuth(endpoint);
|
|
1713
|
-
if (authToken &&
|
|
1714
|
-
|
|
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
|
+
);
|
|
1715
1815
|
const onDisconnected = () => {
|
|
1716
|
-
if (cachedByCdpUrl.get(normalized)?.browser === browser)
|
|
1717
|
-
|
|
1718
|
-
|
|
1816
|
+
if (cachedByCdpUrl.get(normalized)?.browser === browser) {
|
|
1817
|
+
cachedByCdpUrl.delete(normalized);
|
|
1818
|
+
for (const key of roleRefsByTarget.keys()) {
|
|
1819
|
+
if (key.startsWith(normalized + "::")) roleRefsByTarget.delete(key);
|
|
1820
|
+
}
|
|
1719
1821
|
}
|
|
1720
1822
|
};
|
|
1721
1823
|
const connected = { browser, cdpUrl: normalized, onDisconnected };
|
|
@@ -1763,7 +1865,9 @@ function cdpSocketNeedsAttach(wsUrl) {
|
|
|
1763
1865
|
async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
1764
1866
|
const httpBase = normalizeCdpHttpBaseForJsonEndpoints(cdpUrl);
|
|
1765
1867
|
const ctrl = new AbortController();
|
|
1766
|
-
const t = setTimeout(() =>
|
|
1868
|
+
const t = setTimeout(() => {
|
|
1869
|
+
ctrl.abort();
|
|
1870
|
+
}, 2e3);
|
|
1767
1871
|
let targets;
|
|
1768
1872
|
try {
|
|
1769
1873
|
const res = await fetch(`${httpBase}/json/list`, { signal: ctrl.signal });
|
|
@@ -1775,9 +1879,12 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
|
1775
1879
|
clearTimeout(t);
|
|
1776
1880
|
}
|
|
1777
1881
|
if (!Array.isArray(targets)) return;
|
|
1778
|
-
const target = targets.find((entry) =>
|
|
1779
|
-
|
|
1780
|
-
|
|
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;
|
|
1781
1888
|
const wsUrl = normalizeCdpWsUrl(wsUrlRaw, httpBase);
|
|
1782
1889
|
const needsAttach = cdpSocketNeedsAttach(wsUrl);
|
|
1783
1890
|
await new Promise((resolve2) => {
|
|
@@ -1813,10 +1920,18 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
|
1813
1920
|
if (!needsAttach) return;
|
|
1814
1921
|
try {
|
|
1815
1922
|
const msg = JSON.parse(String(event.data));
|
|
1816
|
-
|
|
1817
|
-
|
|
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" }));
|
|
1818
1927
|
try {
|
|
1819
|
-
ws.send(
|
|
1928
|
+
ws.send(
|
|
1929
|
+
JSON.stringify({
|
|
1930
|
+
id: nextId++,
|
|
1931
|
+
method: "Target.detachFromTarget",
|
|
1932
|
+
params: { sessionId }
|
|
1933
|
+
})
|
|
1934
|
+
);
|
|
1820
1935
|
} catch {
|
|
1821
1936
|
}
|
|
1822
1937
|
setTimeout(finish, 300);
|
|
@@ -1824,8 +1939,12 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
|
1824
1939
|
} catch {
|
|
1825
1940
|
}
|
|
1826
1941
|
};
|
|
1827
|
-
ws.onerror = () =>
|
|
1828
|
-
|
|
1942
|
+
ws.onerror = () => {
|
|
1943
|
+
finish();
|
|
1944
|
+
};
|
|
1945
|
+
ws.onclose = () => {
|
|
1946
|
+
finish();
|
|
1947
|
+
};
|
|
1829
1948
|
});
|
|
1830
1949
|
}
|
|
1831
1950
|
async function forceDisconnectPlaywrightForTarget(opts) {
|
|
@@ -1837,15 +1956,15 @@ async function forceDisconnectPlaywrightForTarget(opts) {
|
|
|
1837
1956
|
if (cur.onDisconnected && typeof cur.browser.off === "function") {
|
|
1838
1957
|
cur.browser.off("disconnected", cur.onDisconnected);
|
|
1839
1958
|
}
|
|
1840
|
-
const targetId = opts.targetId?.trim()
|
|
1841
|
-
if (targetId) {
|
|
1959
|
+
const targetId = opts.targetId?.trim() ?? "";
|
|
1960
|
+
if (targetId !== "") {
|
|
1842
1961
|
await tryTerminateExecutionViaCdp(normalized, targetId).catch(() => {
|
|
1843
1962
|
});
|
|
1844
1963
|
}
|
|
1845
1964
|
cur.browser.close().catch(() => {
|
|
1846
1965
|
});
|
|
1847
1966
|
}
|
|
1848
|
-
|
|
1967
|
+
function getAllPages(browser) {
|
|
1849
1968
|
return browser.contexts().flatMap((c) => c.pages());
|
|
1850
1969
|
}
|
|
1851
1970
|
async function pageTargetId(page) {
|
|
@@ -1853,14 +1972,36 @@ async function pageTargetId(page) {
|
|
|
1853
1972
|
try {
|
|
1854
1973
|
const info = await session.send("Target.getTargetInfo");
|
|
1855
1974
|
const targetInfo = info.targetInfo;
|
|
1856
|
-
return
|
|
1975
|
+
return (targetInfo?.targetId ?? "").trim() || null;
|
|
1857
1976
|
} finally {
|
|
1858
1977
|
await session.detach().catch(() => {
|
|
1859
1978
|
});
|
|
1860
1979
|
}
|
|
1861
1980
|
}
|
|
1981
|
+
function matchPageByTargetList(pages, targets, targetId) {
|
|
1982
|
+
const target = targets.find((entry) => entry.id === targetId);
|
|
1983
|
+
if (!target) return null;
|
|
1984
|
+
const urlMatch = pages.filter((page) => page.url() === target.url);
|
|
1985
|
+
if (urlMatch.length === 1) return urlMatch[0] ?? null;
|
|
1986
|
+
if (urlMatch.length > 1) {
|
|
1987
|
+
const sameUrlTargets = targets.filter((entry) => entry.url === target.url);
|
|
1988
|
+
if (sameUrlTargets.length === urlMatch.length) {
|
|
1989
|
+
const idx = sameUrlTargets.findIndex((entry) => entry.id === targetId);
|
|
1990
|
+
if (idx >= 0 && idx < urlMatch.length) return urlMatch[idx] ?? null;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
return null;
|
|
1994
|
+
}
|
|
1995
|
+
async function findPageByTargetIdViaTargetList(pages, targetId, cdpUrl) {
|
|
1996
|
+
const targets = await fetchJsonForCdp(
|
|
1997
|
+
appendCdpPath2(normalizeCdpHttpBaseForJsonEndpoints(cdpUrl), "/json/list"),
|
|
1998
|
+
2e3
|
|
1999
|
+
);
|
|
2000
|
+
if (!Array.isArray(targets)) return null;
|
|
2001
|
+
return matchPageByTargetList(pages, targets, targetId);
|
|
2002
|
+
}
|
|
1862
2003
|
async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
1863
|
-
const pages =
|
|
2004
|
+
const pages = getAllPages(browser);
|
|
1864
2005
|
let resolvedViaCdp = false;
|
|
1865
2006
|
for (const page of pages) {
|
|
1866
2007
|
let tid = null;
|
|
@@ -1870,66 +2011,84 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
|
1870
2011
|
} catch {
|
|
1871
2012
|
tid = null;
|
|
1872
2013
|
}
|
|
1873
|
-
if (tid && tid === targetId) return page;
|
|
2014
|
+
if (tid !== null && tid !== "" && tid === targetId) return page;
|
|
1874
2015
|
}
|
|
1875
|
-
if (cdpUrl) {
|
|
2016
|
+
if (cdpUrl !== void 0 && cdpUrl !== "") {
|
|
1876
2017
|
try {
|
|
1877
|
-
|
|
1878
|
-
const headers = {};
|
|
1879
|
-
const ctrl = new AbortController();
|
|
1880
|
-
const t = setTimeout(() => ctrl.abort(), 2e3);
|
|
1881
|
-
try {
|
|
1882
|
-
const response = await fetch(listUrl, { headers, signal: ctrl.signal });
|
|
1883
|
-
if (response.ok) {
|
|
1884
|
-
const targets = await response.json();
|
|
1885
|
-
const target = targets.find((entry) => entry.id === targetId);
|
|
1886
|
-
if (target) {
|
|
1887
|
-
const urlMatch = pages.filter((p) => p.url() === target.url);
|
|
1888
|
-
if (urlMatch.length === 1) return urlMatch[0];
|
|
1889
|
-
if (urlMatch.length > 1) {
|
|
1890
|
-
const sameUrlTargets = targets.filter((entry) => entry.url === target.url);
|
|
1891
|
-
if (sameUrlTargets.length === urlMatch.length) {
|
|
1892
|
-
const idx = sameUrlTargets.findIndex((entry) => entry.id === targetId);
|
|
1893
|
-
if (idx >= 0 && idx < urlMatch.length) return urlMatch[idx];
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
} finally {
|
|
1899
|
-
clearTimeout(t);
|
|
1900
|
-
}
|
|
2018
|
+
return await findPageByTargetIdViaTargetList(pages, targetId, cdpUrl);
|
|
1901
2019
|
} catch {
|
|
1902
2020
|
}
|
|
1903
2021
|
}
|
|
1904
|
-
if (!resolvedViaCdp && pages.length === 1) return pages[0];
|
|
2022
|
+
if (!resolvedViaCdp && pages.length === 1) return pages[0] ?? null;
|
|
1905
2023
|
return null;
|
|
1906
2024
|
}
|
|
1907
2025
|
async function getPageForTargetId(opts) {
|
|
1908
2026
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
1909
|
-
const pages =
|
|
2027
|
+
const pages = getAllPages(browser);
|
|
1910
2028
|
if (!pages.length) throw new Error("No pages available in the connected browser.");
|
|
1911
2029
|
const first = pages[0];
|
|
1912
|
-
if (
|
|
2030
|
+
if (opts.targetId === void 0 || opts.targetId === "") return first;
|
|
1913
2031
|
const found = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
|
|
1914
2032
|
if (!found) {
|
|
1915
2033
|
if (pages.length === 1) return first;
|
|
1916
|
-
throw new
|
|
2034
|
+
throw new BrowserTabNotFoundError(
|
|
2035
|
+
`Tab not found (targetId: ${opts.targetId}). Call browser.tabs() to list open tabs.`
|
|
2036
|
+
);
|
|
1917
2037
|
}
|
|
1918
2038
|
return found;
|
|
1919
2039
|
}
|
|
2040
|
+
async function resolvePageByTargetIdOrThrow(opts) {
|
|
2041
|
+
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
2042
|
+
const page = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
|
|
2043
|
+
if (!page) throw new BrowserTabNotFoundError();
|
|
2044
|
+
return page;
|
|
2045
|
+
}
|
|
2046
|
+
function parseRoleRef(raw) {
|
|
2047
|
+
const trimmed = raw.trim();
|
|
2048
|
+
if (!trimmed) return null;
|
|
2049
|
+
const normalized = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed.startsWith("ref=") ? trimmed.slice(4) : trimmed;
|
|
2050
|
+
return /^e\d+$/.test(normalized) ? normalized : null;
|
|
2051
|
+
}
|
|
2052
|
+
function requireRef(value) {
|
|
2053
|
+
const raw = typeof value === "string" ? value.trim() : "";
|
|
2054
|
+
const ref = (raw ? parseRoleRef(raw) : null) ?? (raw.startsWith("@") ? raw.slice(1) : raw);
|
|
2055
|
+
if (!ref) throw new Error("ref is required");
|
|
2056
|
+
return ref;
|
|
2057
|
+
}
|
|
2058
|
+
function requireRefOrSelector(ref, selector) {
|
|
2059
|
+
const trimmedRef = typeof ref === "string" ? ref.trim() : "";
|
|
2060
|
+
const trimmedSelector = typeof selector === "string" ? selector.trim() : "";
|
|
2061
|
+
if (!trimmedRef && !trimmedSelector) throw new Error("ref or selector is required");
|
|
2062
|
+
return { ref: trimmedRef || void 0, selector: trimmedSelector || void 0 };
|
|
2063
|
+
}
|
|
2064
|
+
function resolveInteractionTimeoutMs(timeoutMs) {
|
|
2065
|
+
return Math.max(500, Math.min(6e4, Math.floor(timeoutMs ?? 8e3)));
|
|
2066
|
+
}
|
|
2067
|
+
function resolveBoundedDelayMs(value, label, maxMs) {
|
|
2068
|
+
const normalized = Math.floor(value ?? 0);
|
|
2069
|
+
if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
|
|
2070
|
+
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${String(maxMs)}ms`);
|
|
2071
|
+
return normalized;
|
|
2072
|
+
}
|
|
2073
|
+
async function getRestoredPageForTarget(opts) {
|
|
2074
|
+
const page = await getPageForTargetId(opts);
|
|
2075
|
+
ensurePageState(page);
|
|
2076
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
2077
|
+
return page;
|
|
2078
|
+
}
|
|
1920
2079
|
function refLocator(page, ref) {
|
|
1921
2080
|
const normalized = ref.startsWith("@") ? ref.slice(1) : ref.startsWith("ref=") ? ref.slice(4) : ref;
|
|
1922
|
-
if (
|
|
2081
|
+
if (normalized.trim() === "") throw new Error("ref is required");
|
|
1923
2082
|
if (/^e\d+$/.test(normalized)) {
|
|
1924
2083
|
const state = pageStates.get(page);
|
|
1925
2084
|
if (state?.roleRefsMode === "aria") {
|
|
1926
|
-
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}`);
|
|
1927
2086
|
}
|
|
1928
2087
|
const info = state?.roleRefs?.[normalized];
|
|
1929
2088
|
if (!info) throw new Error(`Unknown ref "${normalized}". Run a new snapshot and use a ref from that snapshot.`);
|
|
1930
|
-
const locAny = state
|
|
2089
|
+
const locAny = state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page;
|
|
1931
2090
|
const role = info.role;
|
|
1932
|
-
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);
|
|
1933
2092
|
return info.nth !== void 0 ? locator.nth(info.nth) : locator;
|
|
1934
2093
|
}
|
|
1935
2094
|
return page.locator(`aria-ref=${normalized}`);
|
|
@@ -1937,15 +2096,21 @@ function refLocator(page, ref) {
|
|
|
1937
2096
|
function toAIFriendlyError(error, selector) {
|
|
1938
2097
|
const message = error instanceof Error ? error.message : String(error);
|
|
1939
2098
|
if (message.includes("strict mode violation")) {
|
|
1940
|
-
const countMatch =
|
|
2099
|
+
const countMatch = /resolved to (\d+) elements/.exec(message);
|
|
1941
2100
|
const count = countMatch ? countMatch[1] : "multiple";
|
|
1942
|
-
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
|
+
);
|
|
1943
2104
|
}
|
|
1944
2105
|
if ((message.includes("Timeout") || message.includes("waiting for")) && (message.includes("to be visible") || message.includes("not visible"))) {
|
|
1945
|
-
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
|
+
);
|
|
1946
2109
|
}
|
|
1947
2110
|
if (message.includes("intercepts pointer events") || message.includes("not visible") || message.includes("not receive pointer events")) {
|
|
1948
|
-
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
|
+
);
|
|
1949
2114
|
}
|
|
1950
2115
|
return error instanceof Error ? error : new Error(message);
|
|
1951
2116
|
}
|
|
@@ -1953,438 +2118,151 @@ function normalizeTimeoutMs(timeoutMs, fallback, maxMs = 12e4) {
|
|
|
1953
2118
|
return Math.max(500, Math.min(maxMs, timeoutMs ?? fallback));
|
|
1954
2119
|
}
|
|
1955
2120
|
|
|
1956
|
-
// src/
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
"
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
"columnheader",
|
|
1981
|
-
"rowheader",
|
|
1982
|
-
"listitem",
|
|
1983
|
-
"article",
|
|
1984
|
-
"region",
|
|
1985
|
-
"main",
|
|
1986
|
-
"navigation"
|
|
1987
|
-
]);
|
|
1988
|
-
var STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
|
|
1989
|
-
"generic",
|
|
1990
|
-
"group",
|
|
1991
|
-
"list",
|
|
1992
|
-
"table",
|
|
1993
|
-
"row",
|
|
1994
|
-
"rowgroup",
|
|
1995
|
-
"grid",
|
|
1996
|
-
"treegrid",
|
|
1997
|
-
"menu",
|
|
1998
|
-
"menubar",
|
|
1999
|
-
"toolbar",
|
|
2000
|
-
"tablist",
|
|
2001
|
-
"tree",
|
|
2002
|
-
"directory",
|
|
2003
|
-
"document",
|
|
2004
|
-
"application",
|
|
2005
|
-
"presentation",
|
|
2006
|
-
"none"
|
|
2007
|
-
]);
|
|
2008
|
-
function getIndentLevel(line) {
|
|
2009
|
-
const match = line.match(/^(\s*)/);
|
|
2010
|
-
return match ? Math.floor(match[1].length / 2) : 0;
|
|
2011
|
-
}
|
|
2012
|
-
function matchInteractiveSnapshotLine(line, options) {
|
|
2013
|
-
const depth = getIndentLevel(line);
|
|
2014
|
-
if (options.maxDepth !== void 0 && depth > options.maxDepth) {
|
|
2015
|
-
return null;
|
|
2016
|
-
}
|
|
2017
|
-
const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
|
|
2018
|
-
if (!match) {
|
|
2019
|
-
return null;
|
|
2020
|
-
}
|
|
2021
|
-
const [, , roleRaw, name, suffix] = match;
|
|
2022
|
-
if (roleRaw.startsWith("/")) {
|
|
2023
|
-
return null;
|
|
2024
|
-
}
|
|
2025
|
-
const role = roleRaw.toLowerCase();
|
|
2026
|
-
return {
|
|
2027
|
-
roleRaw,
|
|
2028
|
-
role,
|
|
2029
|
-
...name ? { name } : {},
|
|
2030
|
-
suffix
|
|
2031
|
-
};
|
|
2032
|
-
}
|
|
2033
|
-
function createRoleNameTracker() {
|
|
2034
|
-
const counts = /* @__PURE__ */ new Map();
|
|
2035
|
-
const refsByKey = /* @__PURE__ */ new Map();
|
|
2036
|
-
return {
|
|
2037
|
-
counts,
|
|
2038
|
-
refsByKey,
|
|
2039
|
-
getKey(role, name) {
|
|
2040
|
-
return `${role}:${name ?? ""}`;
|
|
2041
|
-
},
|
|
2042
|
-
getNextIndex(role, name) {
|
|
2043
|
-
const key = this.getKey(role, name);
|
|
2044
|
-
const current = counts.get(key) ?? 0;
|
|
2045
|
-
counts.set(key, current + 1);
|
|
2046
|
-
return current;
|
|
2047
|
-
},
|
|
2048
|
-
trackRef(role, name, ref) {
|
|
2049
|
-
const key = this.getKey(role, name);
|
|
2050
|
-
const list = refsByKey.get(key) ?? [];
|
|
2051
|
-
list.push(ref);
|
|
2052
|
-
refsByKey.set(key, list);
|
|
2053
|
-
},
|
|
2054
|
-
getDuplicateKeys() {
|
|
2055
|
-
const out = /* @__PURE__ */ new Set();
|
|
2056
|
-
for (const [key, refs] of refsByKey) if (refs.length > 1) out.add(key);
|
|
2057
|
-
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 {
|
|
2058
2145
|
}
|
|
2059
|
-
}
|
|
2146
|
+
}
|
|
2147
|
+
return results;
|
|
2060
2148
|
}
|
|
2061
|
-
function
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
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;
|
|
2066
2157
|
}
|
|
2067
2158
|
}
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
2084
|
-
if (getIndentLevel(lines[j]) <= currentIndent) break;
|
|
2085
|
-
if (lines[j]?.includes("[ref=")) {
|
|
2086
|
-
hasRelevantChildren = true;
|
|
2087
|
-
break;
|
|
2088
|
-
}
|
|
2089
|
-
}
|
|
2090
|
-
if (hasRelevantChildren) result.push(line);
|
|
2091
|
-
}
|
|
2092
|
-
return result.join("\n");
|
|
2093
|
-
}
|
|
2094
|
-
function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
|
|
2095
|
-
const lines = ariaSnapshot.split("\n");
|
|
2096
|
-
const refs = {};
|
|
2097
|
-
const tracker = createRoleNameTracker();
|
|
2098
|
-
let counter = 0;
|
|
2099
|
-
const nextRef = () => {
|
|
2100
|
-
counter++;
|
|
2101
|
-
return `e${counter}`;
|
|
2102
|
-
};
|
|
2103
|
-
if (options.interactive) {
|
|
2104
|
-
const result2 = [];
|
|
2105
|
-
for (const line of lines) {
|
|
2106
|
-
const parsed = matchInteractiveSnapshotLine(line, options);
|
|
2107
|
-
if (!parsed) continue;
|
|
2108
|
-
const { roleRaw, role, name, suffix } = parsed;
|
|
2109
|
-
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
2110
|
-
const prefix = line.match(/^(\s*-\s*)/)?.[1] ?? "";
|
|
2111
|
-
const ref = nextRef();
|
|
2112
|
-
const nth = tracker.getNextIndex(role, name);
|
|
2113
|
-
tracker.trackRef(role, name, ref);
|
|
2114
|
-
refs[ref] = { role, name, nth };
|
|
2115
|
-
let enhanced = `${prefix}${roleRaw}`;
|
|
2116
|
-
if (name) enhanced += ` "${name}"`;
|
|
2117
|
-
enhanced += ` [ref=${ref}]`;
|
|
2118
|
-
if (nth > 0) enhanced += ` [nth=${nth}]`;
|
|
2119
|
-
if (suffix.includes("[")) enhanced += suffix;
|
|
2120
|
-
result2.push(enhanced);
|
|
2121
|
-
}
|
|
2122
|
-
removeNthFromNonDuplicates(refs, tracker);
|
|
2123
|
-
return { snapshot: result2.join("\n") || "(no interactive elements)", refs };
|
|
2124
|
-
}
|
|
2125
|
-
const result = [];
|
|
2126
|
-
for (const line of lines) {
|
|
2127
|
-
const depth = getIndentLevel(line);
|
|
2128
|
-
if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
|
|
2129
|
-
const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
|
|
2130
|
-
if (!match) {
|
|
2131
|
-
result.push(line);
|
|
2132
|
-
continue;
|
|
2133
|
-
}
|
|
2134
|
-
const [, prefix, roleRaw, name, suffix] = match;
|
|
2135
|
-
if (roleRaw.startsWith("/")) {
|
|
2136
|
-
result.push(line);
|
|
2137
|
-
continue;
|
|
2138
|
-
}
|
|
2139
|
-
const role = roleRaw.toLowerCase();
|
|
2140
|
-
const isInteractive = INTERACTIVE_ROLES.has(role);
|
|
2141
|
-
const isContent = CONTENT_ROLES.has(role);
|
|
2142
|
-
const isStructural = STRUCTURAL_ROLES.has(role);
|
|
2143
|
-
if (options.compact && isStructural && !name) continue;
|
|
2144
|
-
if (!(isInteractive || isContent && name)) {
|
|
2145
|
-
result.push(line);
|
|
2146
|
-
continue;
|
|
2147
|
-
}
|
|
2148
|
-
const ref = nextRef();
|
|
2149
|
-
const nth = tracker.getNextIndex(role, name);
|
|
2150
|
-
tracker.trackRef(role, name, ref);
|
|
2151
|
-
refs[ref] = { role, name, nth };
|
|
2152
|
-
let enhanced = `${prefix}${roleRaw}`;
|
|
2153
|
-
if (name) enhanced += ` "${name}"`;
|
|
2154
|
-
enhanced += ` [ref=${ref}]`;
|
|
2155
|
-
if (nth > 0) enhanced += ` [nth=${nth}]`;
|
|
2156
|
-
if (suffix) enhanced += suffix;
|
|
2157
|
-
result.push(enhanced);
|
|
2158
|
-
}
|
|
2159
|
-
removeNthFromNonDuplicates(refs, tracker);
|
|
2160
|
-
const tree = result.join("\n") || "(empty)";
|
|
2161
|
-
return { snapshot: options.compact ? compactTree(tree) : tree, refs };
|
|
2162
|
-
}
|
|
2163
|
-
function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
|
|
2164
|
-
const lines = String(aiSnapshot ?? "").split("\n");
|
|
2165
|
-
const refs = {};
|
|
2166
|
-
function parseAiSnapshotRef(suffix) {
|
|
2167
|
-
const match = suffix.match(/\[ref=(e\d+)\]/i);
|
|
2168
|
-
return match ? match[1] : null;
|
|
2169
|
-
}
|
|
2170
|
-
if (options.interactive) {
|
|
2171
|
-
const out2 = [];
|
|
2172
|
-
for (const line of lines) {
|
|
2173
|
-
const parsed = matchInteractiveSnapshotLine(line, options);
|
|
2174
|
-
if (!parsed) continue;
|
|
2175
|
-
const { roleRaw, role, name, suffix } = parsed;
|
|
2176
|
-
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
2177
|
-
const ref = parseAiSnapshotRef(suffix);
|
|
2178
|
-
if (!ref) continue;
|
|
2179
|
-
const prefix = line.match(/^(\s*-\s*)/)?.[1] ?? "";
|
|
2180
|
-
refs[ref] = { role, ...name ? { name } : {} };
|
|
2181
|
-
out2.push(`${prefix}${roleRaw}${name ? ` "${name}"` : ""}${suffix}`);
|
|
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
|
+
]);
|
|
2182
2174
|
}
|
|
2183
|
-
return
|
|
2175
|
+
return result;
|
|
2176
|
+
} catch (err) {
|
|
2177
|
+
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
2184
2178
|
}
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
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
|
+
]);
|
|
2198
2197
|
}
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
const ref = parseAiSnapshotRef(suffix);
|
|
2203
|
-
if (ref) refs[ref] = { role, ...name ? { name } : {} };
|
|
2204
|
-
out.push(line);
|
|
2205
|
-
}
|
|
2206
|
-
const tree = out.join("\n") || "(empty)";
|
|
2207
|
-
return { snapshot: options.compact ? compactTree(tree) : tree, refs };
|
|
2208
|
-
}
|
|
2209
|
-
function getRoleSnapshotStats(snapshot, refs) {
|
|
2210
|
-
const interactive = Object.values(refs).filter((r) => INTERACTIVE_ROLES.has(r.role)).length;
|
|
2211
|
-
return {
|
|
2212
|
-
lines: snapshot.split("\n").length,
|
|
2213
|
-
chars: snapshot.length,
|
|
2214
|
-
refs: Object.keys(refs).length,
|
|
2215
|
-
interactive
|
|
2216
|
-
};
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
// src/snapshot/ai-snapshot.ts
|
|
2220
|
-
async function snapshotAi(opts) {
|
|
2221
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2222
|
-
ensurePageState(page);
|
|
2223
|
-
const maybe = page;
|
|
2224
|
-
if (!maybe._snapshotForAI) {
|
|
2225
|
-
throw new Error("Playwright _snapshotForAI is not available. Upgrade playwright-core to >= 1.50.");
|
|
2226
|
-
}
|
|
2227
|
-
const sourceUrl = page.url();
|
|
2228
|
-
const result = await maybe._snapshotForAI({
|
|
2229
|
-
timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3, 6e4),
|
|
2230
|
-
track: "response"
|
|
2231
|
-
});
|
|
2232
|
-
let snapshot = String(result?.full ?? "");
|
|
2233
|
-
const maxChars = opts.maxChars;
|
|
2234
|
-
const limit = typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : void 0;
|
|
2235
|
-
if (limit && snapshot.length > limit) {
|
|
2236
|
-
const lastNewline = snapshot.lastIndexOf("\n", limit);
|
|
2237
|
-
const cutoff = lastNewline > 0 ? lastNewline : limit;
|
|
2238
|
-
snapshot = `${snapshot.slice(0, cutoff)}
|
|
2239
|
-
|
|
2240
|
-
[...TRUNCATED - page too large]`;
|
|
2198
|
+
return result;
|
|
2199
|
+
} catch (err) {
|
|
2200
|
+
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
2241
2201
|
}
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
refs: built.refs,
|
|
2248
|
-
mode: "aria"
|
|
2249
|
-
});
|
|
2250
|
-
return {
|
|
2251
|
-
snapshot: built.snapshot,
|
|
2252
|
-
refs: built.refs,
|
|
2253
|
-
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
2254
|
-
untrusted: true,
|
|
2255
|
-
contentMeta: {
|
|
2256
|
-
sourceUrl,
|
|
2257
|
-
contentType: "browser-snapshot",
|
|
2258
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2259
|
-
}
|
|
2260
|
-
};
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
|
-
// src/snapshot/aria-snapshot.ts
|
|
2264
|
-
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");
|
|
2265
2207
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2266
2208
|
ensurePageState(page);
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
cdpUrl: opts.cdpUrl,
|
|
2281
|
-
targetId: opts.targetId,
|
|
2282
|
-
refs: built2.refs,
|
|
2283
|
-
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(() => {
|
|
2284
2222
|
});
|
|
2285
|
-
return {
|
|
2286
|
-
snapshot: built2.snapshot,
|
|
2287
|
-
refs: built2.refs,
|
|
2288
|
-
stats: getRoleSnapshotStats(built2.snapshot, built2.refs),
|
|
2289
|
-
untrusted: true,
|
|
2290
|
-
contentMeta: {
|
|
2291
|
-
sourceUrl,
|
|
2292
|
-
contentType: "browser-snapshot",
|
|
2293
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2294
|
-
}
|
|
2295
|
-
};
|
|
2296
2223
|
}
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
frameSelector: frameSelector || void 0,
|
|
2308
|
-
mode: "role"
|
|
2309
|
-
});
|
|
2310
|
-
return {
|
|
2311
|
-
snapshot: built.snapshot,
|
|
2312
|
-
refs: built.refs,
|
|
2313
|
-
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
2314
|
-
untrusted: true,
|
|
2315
|
-
contentMeta: {
|
|
2316
|
-
sourceUrl,
|
|
2317
|
-
contentType: "browser-snapshot",
|
|
2318
|
-
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");
|
|
2319
2234
|
}
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
const limit = Math.max(1, Math.min(2e3, Math.floor(opts.limit ?? 500)));
|
|
2324
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2325
|
-
ensurePageState(page);
|
|
2326
|
-
const sourceUrl = page.url();
|
|
2327
|
-
const session = await page.context().newCDPSession(page);
|
|
2328
|
-
try {
|
|
2329
|
-
await session.send("Accessibility.enable").catch(() => {
|
|
2330
|
-
});
|
|
2331
|
-
const res = await session.send("Accessibility.getFullAXTree");
|
|
2332
|
-
return {
|
|
2333
|
-
nodes: formatAriaNodes(Array.isArray(res?.nodes) ? res.nodes : [], limit),
|
|
2334
|
-
untrusted: true,
|
|
2335
|
-
contentMeta: {
|
|
2336
|
-
sourceUrl,
|
|
2337
|
-
contentType: "browser-aria-tree",
|
|
2338
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2339
|
-
}
|
|
2235
|
+
abortListener = () => {
|
|
2236
|
+
disconnect();
|
|
2237
|
+
abortReject?.(signal.reason ?? new Error("aborted"));
|
|
2340
2238
|
};
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2239
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
2240
|
+
if (signal.aborted) {
|
|
2241
|
+
abortListener();
|
|
2242
|
+
throw signal.reason ?? new Error("aborted");
|
|
2243
|
+
}
|
|
2344
2244
|
}
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
}
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
for (const n of nodes) if (n.nodeId) byId.set(n.nodeId, n);
|
|
2356
|
-
const referenced = /* @__PURE__ */ new Set();
|
|
2357
|
-
for (const n of nodes) for (const c of n.childIds ?? []) referenced.add(c);
|
|
2358
|
-
const root = nodes.find((n) => n.nodeId && !referenced.has(n.nodeId)) ?? nodes[0];
|
|
2359
|
-
if (!root?.nodeId) return [];
|
|
2360
|
-
const out = [];
|
|
2361
|
-
const stack = [{ id: root.nodeId, depth: 0 }];
|
|
2362
|
-
while (stack.length && out.length < limit) {
|
|
2363
|
-
const popped = stack.pop();
|
|
2364
|
-
if (!popped) break;
|
|
2365
|
-
const { id, depth } = popped;
|
|
2366
|
-
const n = byId.get(id);
|
|
2367
|
-
if (!n) continue;
|
|
2368
|
-
const role = axValue(n.role);
|
|
2369
|
-
const name = axValue(n.name);
|
|
2370
|
-
const value = axValue(n.value);
|
|
2371
|
-
const description = axValue(n.description);
|
|
2372
|
-
const ref = `ax${out.length + 1}`;
|
|
2373
|
-
out.push({
|
|
2374
|
-
ref,
|
|
2375
|
-
role: role || "unknown",
|
|
2376
|
-
name: name || "",
|
|
2377
|
-
...value ? { value } : {},
|
|
2378
|
-
...description ? { description } : {},
|
|
2379
|
-
...typeof n.backendDOMNodeId === "number" ? { backendDOMNodeId: n.backendDOMNodeId } : {},
|
|
2380
|
-
depth
|
|
2381
|
-
});
|
|
2382
|
-
const children = (n.childIds ?? []).filter((c) => byId.has(c));
|
|
2383
|
-
for (let i = children.length - 1; i >= 0; i--) {
|
|
2384
|
-
if (children[i]) stack.push({ id: children[i], depth: depth + 1 });
|
|
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
|
+
);
|
|
2385
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);
|
|
2386
2265
|
}
|
|
2387
|
-
return out;
|
|
2388
2266
|
}
|
|
2389
2267
|
|
|
2390
2268
|
// src/security.ts
|
|
@@ -2401,11 +2279,7 @@ function withBrowserNavigationPolicy(ssrfPolicy) {
|
|
|
2401
2279
|
var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
2402
2280
|
var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
|
|
2403
2281
|
var PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"];
|
|
2404
|
-
var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
2405
|
-
"localhost",
|
|
2406
|
-
"localhost.localdomain",
|
|
2407
|
-
"metadata.google.internal"
|
|
2408
|
-
]);
|
|
2282
|
+
var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "localhost.localdomain", "metadata.google.internal"]);
|
|
2409
2283
|
function isAllowedNonNetworkNavigationUrl(parsed) {
|
|
2410
2284
|
return SAFE_NON_NETWORK_URLS.has(parsed.href);
|
|
2411
2285
|
}
|
|
@@ -2420,7 +2294,7 @@ function hasProxyEnvConfigured2(env = process.env) {
|
|
|
2420
2294
|
return false;
|
|
2421
2295
|
}
|
|
2422
2296
|
function normalizeHostname(hostname) {
|
|
2423
|
-
let h =
|
|
2297
|
+
let h = hostname.trim().toLowerCase();
|
|
2424
2298
|
if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
|
|
2425
2299
|
if (h.endsWith(".")) h = h.slice(0, -1);
|
|
2426
2300
|
return h;
|
|
@@ -2439,13 +2313,7 @@ var BLOCKED_IPV4_RANGES = /* @__PURE__ */ new Set([
|
|
|
2439
2313
|
"private",
|
|
2440
2314
|
"reserved"
|
|
2441
2315
|
]);
|
|
2442
|
-
var BLOCKED_IPV6_RANGES = /* @__PURE__ */ new Set([
|
|
2443
|
-
"unspecified",
|
|
2444
|
-
"loopback",
|
|
2445
|
-
"linkLocal",
|
|
2446
|
-
"uniqueLocal",
|
|
2447
|
-
"multicast"
|
|
2448
|
-
]);
|
|
2316
|
+
var BLOCKED_IPV6_RANGES = /* @__PURE__ */ new Set(["unspecified", "loopback", "linkLocal", "uniqueLocal", "multicast"]);
|
|
2449
2317
|
var RFC2544_BENCHMARK_PREFIX = [ipaddr.IPv4.parse("198.18.0.0"), 15];
|
|
2450
2318
|
var EMBEDDED_IPV4_SENTINEL_RULES = [
|
|
2451
2319
|
// IPv4-compatible (::a.b.c.d)
|
|
@@ -2494,12 +2362,12 @@ function parseIpv6WithEmbeddedIpv4(raw) {
|
|
|
2494
2362
|
}
|
|
2495
2363
|
function normalizeIpParseInput(raw) {
|
|
2496
2364
|
const trimmed = raw?.trim();
|
|
2497
|
-
if (
|
|
2365
|
+
if (trimmed === void 0 || trimmed === "") return;
|
|
2498
2366
|
return stripIpv6Brackets(trimmed);
|
|
2499
2367
|
}
|
|
2500
2368
|
function parseCanonicalIpAddress(raw) {
|
|
2501
2369
|
const normalized = normalizeIpParseInput(raw);
|
|
2502
|
-
if (
|
|
2370
|
+
if (normalized === void 0) return;
|
|
2503
2371
|
if (ipaddr.IPv4.isValid(normalized)) {
|
|
2504
2372
|
if (!ipaddr.IPv4.isValidFourPartDecimal(normalized)) return;
|
|
2505
2373
|
return ipaddr.IPv4.parse(normalized);
|
|
@@ -2509,20 +2377,20 @@ function parseCanonicalIpAddress(raw) {
|
|
|
2509
2377
|
}
|
|
2510
2378
|
function parseLooseIpAddress(raw) {
|
|
2511
2379
|
const normalized = normalizeIpParseInput(raw);
|
|
2512
|
-
if (
|
|
2380
|
+
if (normalized === void 0) return;
|
|
2513
2381
|
if (ipaddr.isValid(normalized)) return ipaddr.parse(normalized);
|
|
2514
2382
|
return parseIpv6WithEmbeddedIpv4(normalized);
|
|
2515
2383
|
}
|
|
2516
2384
|
function isCanonicalDottedDecimalIPv4(raw) {
|
|
2517
|
-
const trimmed = raw
|
|
2518
|
-
if (
|
|
2385
|
+
const trimmed = raw.trim();
|
|
2386
|
+
if (trimmed === "") return false;
|
|
2519
2387
|
const normalized = stripIpv6Brackets(trimmed);
|
|
2520
2388
|
if (!normalized) return false;
|
|
2521
2389
|
return ipaddr.IPv4.isValidFourPartDecimal(normalized);
|
|
2522
2390
|
}
|
|
2523
2391
|
function isLegacyIpv4Literal(raw) {
|
|
2524
|
-
const trimmed = raw
|
|
2525
|
-
if (
|
|
2392
|
+
const trimmed = raw.trim();
|
|
2393
|
+
if (trimmed === "") return false;
|
|
2526
2394
|
const normalized = stripIpv6Brackets(trimmed);
|
|
2527
2395
|
if (!normalized || normalized.includes(":")) return false;
|
|
2528
2396
|
if (isCanonicalDottedDecimalIPv4(normalized)) return false;
|
|
@@ -2548,12 +2416,7 @@ function isBlockedSpecialUseIpv6Address(address) {
|
|
|
2548
2416
|
return (address.parts[0] & 65472) === 65216;
|
|
2549
2417
|
}
|
|
2550
2418
|
function decodeIpv4FromHextets(high, low) {
|
|
2551
|
-
const octets = [
|
|
2552
|
-
high >>> 8 & 255,
|
|
2553
|
-
high & 255,
|
|
2554
|
-
low >>> 8 & 255,
|
|
2555
|
-
low & 255
|
|
2556
|
-
];
|
|
2419
|
+
const octets = [high >>> 8 & 255, high & 255, low >>> 8 & 255, low & 255];
|
|
2557
2420
|
return ipaddr.IPv4.parse(octets.join("."));
|
|
2558
2421
|
}
|
|
2559
2422
|
function extractEmbeddedIpv4FromIpv6(address) {
|
|
@@ -2600,9 +2463,7 @@ function normalizeHostnameSet(values) {
|
|
|
2600
2463
|
function normalizeHostnameAllowlist(values) {
|
|
2601
2464
|
if (!values || values.length === 0) return [];
|
|
2602
2465
|
return Array.from(
|
|
2603
|
-
new Set(
|
|
2604
|
-
values.map((v) => normalizeHostname(v)).filter((v) => v !== "*" && v !== "*." && v.length > 0)
|
|
2605
|
-
)
|
|
2466
|
+
new Set(values.map((v) => normalizeHostname(v)).filter((v) => v !== "*" && v !== "*." && v.length > 0))
|
|
2606
2467
|
);
|
|
2607
2468
|
}
|
|
2608
2469
|
function isHostnameAllowedByPattern(hostname, pattern) {
|
|
@@ -2637,19 +2498,25 @@ function createPinnedLookup(params) {
|
|
|
2637
2498
|
family: address.includes(":") ? 6 : 4
|
|
2638
2499
|
}));
|
|
2639
2500
|
let index = 0;
|
|
2640
|
-
return ((
|
|
2641
|
-
const
|
|
2642
|
-
|
|
2643
|
-
const
|
|
2644
|
-
if (
|
|
2645
|
-
|
|
2646
|
-
|
|
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;
|
|
2647
2514
|
}
|
|
2648
|
-
const opts = typeof
|
|
2649
|
-
const requestedFamily = typeof
|
|
2515
|
+
const opts = typeof second === "object" ? second : {};
|
|
2516
|
+
const requestedFamily = typeof second === "number" ? second : typeof opts.family === "number" ? opts.family : 0;
|
|
2650
2517
|
const candidates = requestedFamily === 4 || requestedFamily === 6 ? records.filter((entry) => entry.family === requestedFamily) : records;
|
|
2651
2518
|
const usable = candidates.length > 0 ? candidates : records;
|
|
2652
|
-
if (opts.all) {
|
|
2519
|
+
if (opts.all === true) {
|
|
2653
2520
|
cb(null, usable);
|
|
2654
2521
|
return;
|
|
2655
2522
|
}
|
|
@@ -2667,9 +2534,7 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
2667
2534
|
const isExplicitlyAllowed = allowedHostnames.has(normalized);
|
|
2668
2535
|
const skipPrivateNetworkChecks = allowPrivateNetwork || isExplicitlyAllowed;
|
|
2669
2536
|
if (!matchesHostnameAllowlist(normalized, hostnameAllowlist)) {
|
|
2670
|
-
throw new InvalidBrowserNavigationUrlError(
|
|
2671
|
-
`Navigation blocked: hostname "${hostname}" is not in the allowlist.`
|
|
2672
|
-
);
|
|
2537
|
+
throw new InvalidBrowserNavigationUrlError(`Navigation blocked: hostname "${hostname}" is not in the allowlist.`);
|
|
2673
2538
|
}
|
|
2674
2539
|
if (!skipPrivateNetworkChecks) {
|
|
2675
2540
|
if (isBlockedHostnameOrIp(normalized, params.policy)) {
|
|
@@ -2687,7 +2552,7 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
2687
2552
|
`Navigation to internal/loopback address blocked: unable to resolve "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
2688
2553
|
);
|
|
2689
2554
|
}
|
|
2690
|
-
if (
|
|
2555
|
+
if (results.length === 0) {
|
|
2691
2556
|
throw new InvalidBrowserNavigationUrlError(
|
|
2692
2557
|
`Navigation to internal/loopback address blocked: unable to resolve "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
2693
2558
|
);
|
|
@@ -2714,8 +2579,8 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
2714
2579
|
};
|
|
2715
2580
|
}
|
|
2716
2581
|
async function assertBrowserNavigationAllowed(opts) {
|
|
2717
|
-
const rawUrl =
|
|
2718
|
-
if (
|
|
2582
|
+
const rawUrl = opts.url.trim();
|
|
2583
|
+
if (rawUrl === "") throw new InvalidBrowserNavigationUrlError("url is required");
|
|
2719
2584
|
let parsed;
|
|
2720
2585
|
try {
|
|
2721
2586
|
parsed = new URL(rawUrl);
|
|
@@ -2744,7 +2609,7 @@ async function assertSafeOutputPath(path2, allowedRoots) {
|
|
|
2744
2609
|
if (normalized.includes("..")) {
|
|
2745
2610
|
throw new Error(`Unsafe output path: directory traversal detected in "${path2}".`);
|
|
2746
2611
|
}
|
|
2747
|
-
if (allowedRoots
|
|
2612
|
+
if (allowedRoots !== void 0 && allowedRoots.length > 0) {
|
|
2748
2613
|
const resolved = path.resolve(normalized);
|
|
2749
2614
|
let parentReal;
|
|
2750
2615
|
try {
|
|
@@ -2793,9 +2658,17 @@ async function assertSafeUploadPaths(paths) {
|
|
|
2793
2658
|
}
|
|
2794
2659
|
}
|
|
2795
2660
|
}
|
|
2661
|
+
async function resolveStrictExistingUploadPaths(params) {
|
|
2662
|
+
try {
|
|
2663
|
+
await assertSafeUploadPaths(params.requestedPaths);
|
|
2664
|
+
return { ok: true, paths: params.requestedPaths };
|
|
2665
|
+
} catch (err) {
|
|
2666
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2796
2669
|
function sanitizeUntrustedFileName(fileName, fallbackName) {
|
|
2797
|
-
const trimmed =
|
|
2798
|
-
if (
|
|
2670
|
+
const trimmed = fileName.trim();
|
|
2671
|
+
if (trimmed === "") return fallbackName;
|
|
2799
2672
|
let base = path.posix.basename(trimmed);
|
|
2800
2673
|
base = path.win32.basename(base);
|
|
2801
2674
|
let cleaned = "";
|
|
@@ -2829,16 +2702,17 @@ async function writeViaSiblingTempPath(params) {
|
|
|
2829
2702
|
await promises$1.rename(tempPath, targetPath);
|
|
2830
2703
|
renameSucceeded = true;
|
|
2831
2704
|
} finally {
|
|
2832
|
-
if (!renameSucceeded)
|
|
2833
|
-
|
|
2705
|
+
if (!renameSucceeded)
|
|
2706
|
+
await promises$1.rm(tempPath, { force: true }).catch(() => {
|
|
2707
|
+
});
|
|
2834
2708
|
}
|
|
2835
2709
|
}
|
|
2836
2710
|
function isAbsolute(p) {
|
|
2837
2711
|
return p.startsWith("/") || /^[a-zA-Z]:/.test(p);
|
|
2838
2712
|
}
|
|
2839
2713
|
async function assertBrowserNavigationResultAllowed(opts) {
|
|
2840
|
-
const rawUrl =
|
|
2841
|
-
if (
|
|
2714
|
+
const rawUrl = opts.url.trim();
|
|
2715
|
+
if (rawUrl === "") return;
|
|
2842
2716
|
let parsed;
|
|
2843
2717
|
try {
|
|
2844
2718
|
parsed = new URL(rawUrl);
|
|
@@ -2866,47 +2740,61 @@ function requiresInspectableBrowserNavigationRedirects(ssrfPolicy) {
|
|
|
2866
2740
|
|
|
2867
2741
|
// src/actions/interaction.ts
|
|
2868
2742
|
var MAX_CLICK_DELAY_MS = 5e3;
|
|
2869
|
-
|
|
2870
|
-
const normalized = Math.floor(value ?? 0);
|
|
2871
|
-
if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
|
|
2872
|
-
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${maxMs}ms`);
|
|
2873
|
-
return normalized;
|
|
2874
|
-
}
|
|
2875
|
-
function resolveInteractionTimeoutMs(timeoutMs) {
|
|
2876
|
-
return Math.max(500, Math.min(6e4, Math.floor(timeoutMs ?? 8e3)));
|
|
2877
|
-
}
|
|
2878
|
-
function requireRefOrSelector(ref, selector) {
|
|
2879
|
-
const trimmedRef = typeof ref === "string" ? ref.trim() : "";
|
|
2880
|
-
const trimmedSelector = typeof selector === "string" ? selector.trim() : "";
|
|
2881
|
-
if (!trimmedRef && !trimmedSelector) throw new Error("ref or selector is required");
|
|
2882
|
-
return { ref: trimmedRef || void 0, selector: trimmedSelector || void 0 };
|
|
2883
|
-
}
|
|
2743
|
+
var CHECKABLE_ROLES = /* @__PURE__ */ new Set(["menuitemcheckbox", "menuitemradio", "checkbox", "switch"]);
|
|
2884
2744
|
function resolveLocator(page, resolved) {
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2889
|
-
ensurePageState(page);
|
|
2890
|
-
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
2891
|
-
return page;
|
|
2745
|
+
if (resolved.ref !== void 0 && resolved.ref !== "") return refLocator(page, resolved.ref);
|
|
2746
|
+
const sel = resolved.selector ?? "";
|
|
2747
|
+
return page.locator(sel);
|
|
2892
2748
|
}
|
|
2893
2749
|
async function clickViaPlaywright(opts) {
|
|
2894
2750
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
2895
2751
|
const page = await getRestoredPageForTarget(opts);
|
|
2896
|
-
const label = resolved.ref ?? resolved.selector;
|
|
2752
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
2897
2753
|
const locator = resolveLocator(page, resolved);
|
|
2898
2754
|
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
2755
|
+
let checkableRole = false;
|
|
2756
|
+
if (resolved.ref !== void 0 && resolved.ref !== "") {
|
|
2757
|
+
const refId = parseRoleRef(resolved.ref);
|
|
2758
|
+
if (refId !== null) {
|
|
2759
|
+
const state = ensurePageState(page);
|
|
2760
|
+
const info = state.roleRefs?.[refId];
|
|
2761
|
+
if (info && CHECKABLE_ROLES.has(info.role)) checkableRole = true;
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2899
2764
|
try {
|
|
2900
2765
|
const delayMs = resolveBoundedDelayMs(opts.delayMs, "click delayMs", MAX_CLICK_DELAY_MS);
|
|
2901
2766
|
if (delayMs > 0) {
|
|
2902
2767
|
await locator.hover({ timeout });
|
|
2903
2768
|
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
2904
2769
|
}
|
|
2905
|
-
|
|
2770
|
+
let ariaCheckedBefore;
|
|
2771
|
+
if (checkableRole && opts.doubleClick !== true) {
|
|
2772
|
+
ariaCheckedBefore = await locator.getAttribute("aria-checked", { timeout }).catch(() => void 0);
|
|
2773
|
+
}
|
|
2774
|
+
if (opts.doubleClick === true) {
|
|
2906
2775
|
await locator.dblclick({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
2907
2776
|
} else {
|
|
2908
2777
|
await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
2909
2778
|
}
|
|
2779
|
+
if (checkableRole && opts.doubleClick !== true && ariaCheckedBefore !== void 0) {
|
|
2780
|
+
const POLL_INTERVAL_MS = 50;
|
|
2781
|
+
const POLL_TIMEOUT_MS = 500;
|
|
2782
|
+
let changed = false;
|
|
2783
|
+
for (let elapsed = 0; elapsed < POLL_TIMEOUT_MS; elapsed += POLL_INTERVAL_MS) {
|
|
2784
|
+
const current = await locator.getAttribute("aria-checked", { timeout }).catch(() => void 0);
|
|
2785
|
+
if (current === void 0 || current !== ariaCheckedBefore) {
|
|
2786
|
+
changed = true;
|
|
2787
|
+
break;
|
|
2788
|
+
}
|
|
2789
|
+
await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
|
|
2790
|
+
}
|
|
2791
|
+
if (!changed) {
|
|
2792
|
+
await locator.evaluate((el) => {
|
|
2793
|
+
el.click();
|
|
2794
|
+
}).catch(() => {
|
|
2795
|
+
});
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2910
2798
|
} catch (err) {
|
|
2911
2799
|
throw toAIFriendlyError(err, label);
|
|
2912
2800
|
}
|
|
@@ -2914,7 +2802,7 @@ async function clickViaPlaywright(opts) {
|
|
|
2914
2802
|
async function hoverViaPlaywright(opts) {
|
|
2915
2803
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
2916
2804
|
const page = await getRestoredPageForTarget(opts);
|
|
2917
|
-
const label = resolved.ref ?? resolved.selector;
|
|
2805
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
2918
2806
|
const locator = resolveLocator(page, resolved);
|
|
2919
2807
|
try {
|
|
2920
2808
|
await locator.hover({ timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
|
|
@@ -2924,28 +2812,28 @@ async function hoverViaPlaywright(opts) {
|
|
|
2924
2812
|
}
|
|
2925
2813
|
async function typeViaPlaywright(opts) {
|
|
2926
2814
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
2927
|
-
const text =
|
|
2815
|
+
const text = opts.text;
|
|
2928
2816
|
const page = await getRestoredPageForTarget(opts);
|
|
2929
|
-
const label = resolved.ref ?? resolved.selector;
|
|
2817
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
2930
2818
|
const locator = resolveLocator(page, resolved);
|
|
2931
2819
|
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
2932
2820
|
try {
|
|
2933
|
-
if (opts.slowly) {
|
|
2821
|
+
if (opts.slowly === true) {
|
|
2934
2822
|
await locator.click({ timeout });
|
|
2935
2823
|
await locator.pressSequentially(text, { timeout, delay: 75 });
|
|
2936
2824
|
} else {
|
|
2937
2825
|
await locator.fill(text, { timeout });
|
|
2938
2826
|
}
|
|
2939
|
-
if (opts.submit) await locator.press("Enter", { timeout });
|
|
2827
|
+
if (opts.submit === true) await locator.press("Enter", { timeout });
|
|
2940
2828
|
} catch (err) {
|
|
2941
2829
|
throw toAIFriendlyError(err, label);
|
|
2942
2830
|
}
|
|
2943
2831
|
}
|
|
2944
2832
|
async function selectOptionViaPlaywright(opts) {
|
|
2945
2833
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
2946
|
-
if (
|
|
2834
|
+
if (opts.values.length === 0) throw new Error("values are required");
|
|
2947
2835
|
const page = await getRestoredPageForTarget(opts);
|
|
2948
|
-
const label = resolved.ref ?? resolved.selector;
|
|
2836
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
2949
2837
|
const locator = resolveLocator(page, resolved);
|
|
2950
2838
|
try {
|
|
2951
2839
|
await locator.selectOption(opts.values, { timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
|
|
@@ -2959,8 +2847,8 @@ async function dragViaPlaywright(opts) {
|
|
|
2959
2847
|
const page = await getRestoredPageForTarget(opts);
|
|
2960
2848
|
const startLocator = resolveLocator(page, resolvedStart);
|
|
2961
2849
|
const endLocator = resolveLocator(page, resolvedEnd);
|
|
2962
|
-
const startLabel = resolvedStart.ref ?? resolvedStart.selector;
|
|
2963
|
-
const endLabel = resolvedEnd.ref ?? resolvedEnd.selector;
|
|
2850
|
+
const startLabel = resolvedStart.ref ?? resolvedStart.selector ?? "";
|
|
2851
|
+
const endLabel = resolvedEnd.ref ?? resolvedEnd.selector ?? "";
|
|
2964
2852
|
try {
|
|
2965
2853
|
await startLocator.dragTo(endLocator, { timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
|
|
2966
2854
|
} catch (err) {
|
|
@@ -2996,7 +2884,7 @@ async function fillFormViaPlaywright(opts) {
|
|
|
2996
2884
|
async function scrollIntoViewViaPlaywright(opts) {
|
|
2997
2885
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
2998
2886
|
const page = await getRestoredPageForTarget(opts);
|
|
2999
|
-
const label = resolved.ref ?? resolved.selector;
|
|
2887
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
3000
2888
|
const locator = resolveLocator(page, resolved);
|
|
3001
2889
|
try {
|
|
3002
2890
|
await locator.scrollIntoViewIfNeeded({ timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
|
|
@@ -3006,10 +2894,11 @@ async function scrollIntoViewViaPlaywright(opts) {
|
|
|
3006
2894
|
}
|
|
3007
2895
|
async function highlightViaPlaywright(opts) {
|
|
3008
2896
|
const page = await getRestoredPageForTarget(opts);
|
|
2897
|
+
const ref = requireRef(opts.ref);
|
|
3009
2898
|
try {
|
|
3010
|
-
await refLocator(page,
|
|
2899
|
+
await refLocator(page, ref).highlight();
|
|
3011
2900
|
} catch (err) {
|
|
3012
|
-
throw toAIFriendlyError(err,
|
|
2901
|
+
throw toAIFriendlyError(err, ref);
|
|
3013
2902
|
}
|
|
3014
2903
|
}
|
|
3015
2904
|
async function setInputFilesViaPlaywright(opts) {
|
|
@@ -3020,9 +2909,14 @@ async function setInputFilesViaPlaywright(opts) {
|
|
|
3020
2909
|
if (inputRef && element) throw new Error("ref and element are mutually exclusive");
|
|
3021
2910
|
if (!inputRef && !element) throw new Error("Either ref or element is required for setInputFiles");
|
|
3022
2911
|
const locator = inputRef ? refLocator(page, inputRef) : page.locator(element).first();
|
|
3023
|
-
await
|
|
2912
|
+
const uploadPathsResult = await resolveStrictExistingUploadPaths({
|
|
2913
|
+
requestedPaths: opts.paths,
|
|
2914
|
+
scopeLabel: "uploads directory"
|
|
2915
|
+
});
|
|
2916
|
+
if (!uploadPathsResult.ok) throw new Error(uploadPathsResult.error);
|
|
2917
|
+
const resolvedPaths = uploadPathsResult.paths;
|
|
3024
2918
|
try {
|
|
3025
|
-
await locator.setInputFiles(
|
|
2919
|
+
await locator.setInputFiles(resolvedPaths);
|
|
3026
2920
|
} catch (err) {
|
|
3027
2921
|
throw toAIFriendlyError(err, inputRef || element);
|
|
3028
2922
|
}
|
|
@@ -3058,26 +2952,28 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
3058
2952
|
const armId = state.armIdUpload;
|
|
3059
2953
|
page.waitForEvent("filechooser", { timeout }).then(async (fileChooser) => {
|
|
3060
2954
|
if (state.armIdUpload !== armId) return;
|
|
3061
|
-
if (
|
|
2955
|
+
if (opts.paths === void 0 || opts.paths.length === 0) {
|
|
3062
2956
|
try {
|
|
3063
2957
|
await page.keyboard.press("Escape");
|
|
3064
2958
|
} catch {
|
|
3065
2959
|
}
|
|
3066
2960
|
return;
|
|
3067
2961
|
}
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
2962
|
+
const uploadPathsResult = await resolveStrictExistingUploadPaths({
|
|
2963
|
+
requestedPaths: opts.paths,
|
|
2964
|
+
scopeLabel: "uploads directory"
|
|
2965
|
+
});
|
|
2966
|
+
if (!uploadPathsResult.ok) {
|
|
3071
2967
|
try {
|
|
3072
2968
|
await page.keyboard.press("Escape");
|
|
3073
2969
|
} catch {
|
|
3074
2970
|
}
|
|
3075
2971
|
return;
|
|
3076
2972
|
}
|
|
3077
|
-
await fileChooser.setFiles(
|
|
2973
|
+
await fileChooser.setFiles(uploadPathsResult.paths);
|
|
3078
2974
|
try {
|
|
3079
2975
|
const input = typeof fileChooser.element === "function" ? await Promise.resolve(fileChooser.element()) : null;
|
|
3080
|
-
if (input) {
|
|
2976
|
+
if (input !== null) {
|
|
3081
2977
|
await input.evaluate((el) => {
|
|
3082
2978
|
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
3083
2979
|
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
@@ -3091,7 +2987,7 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
3091
2987
|
|
|
3092
2988
|
// src/actions/keyboard.ts
|
|
3093
2989
|
async function pressKeyViaPlaywright(opts) {
|
|
3094
|
-
const key =
|
|
2990
|
+
const key = opts.key.trim();
|
|
3095
2991
|
if (!key) throw new Error("key is required");
|
|
3096
2992
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3097
2993
|
ensurePageState(page);
|
|
@@ -3104,9 +3000,9 @@ function isRetryableNavigateError(err) {
|
|
|
3104
3000
|
return msg.includes("frame has been detached") || msg.includes("target page, context or browser has been closed");
|
|
3105
3001
|
}
|
|
3106
3002
|
async function navigateViaPlaywright(opts) {
|
|
3107
|
-
const url =
|
|
3003
|
+
const url = opts.url.trim();
|
|
3108
3004
|
if (!url) throw new Error("url is required");
|
|
3109
|
-
const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3005
|
+
const policy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3110
3006
|
await assertBrowserNavigationAllowed({ url, ...withBrowserNavigationPolicy(policy) });
|
|
3111
3007
|
const timeout = Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 2e4));
|
|
3112
3008
|
let page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
@@ -3125,45 +3021,49 @@ async function navigateViaPlaywright(opts) {
|
|
|
3125
3021
|
ensurePageState(page);
|
|
3126
3022
|
response = await navigate();
|
|
3127
3023
|
}
|
|
3128
|
-
await assertBrowserNavigationRedirectChainAllowed({
|
|
3024
|
+
await assertBrowserNavigationRedirectChainAllowed({
|
|
3025
|
+
request: response?.request(),
|
|
3026
|
+
...withBrowserNavigationPolicy(policy)
|
|
3027
|
+
});
|
|
3129
3028
|
const finalUrl = page.url();
|
|
3130
3029
|
await assertBrowserNavigationResultAllowed({ url: finalUrl, ...withBrowserNavigationPolicy(policy) });
|
|
3131
3030
|
return { url: finalUrl };
|
|
3132
3031
|
}
|
|
3133
3032
|
async function listPagesViaPlaywright(opts) {
|
|
3134
3033
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
3135
|
-
const pages =
|
|
3034
|
+
const pages = getAllPages(browser);
|
|
3136
3035
|
const results = [];
|
|
3137
3036
|
for (const page of pages) {
|
|
3138
3037
|
const tid = await pageTargetId(page).catch(() => null);
|
|
3139
|
-
if (tid)
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
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
|
+
});
|
|
3145
3045
|
}
|
|
3146
3046
|
return results;
|
|
3147
3047
|
}
|
|
3148
3048
|
async function createPageViaPlaywright(opts) {
|
|
3149
|
-
const targetUrl = (opts.url ?? "").trim() || "about:blank";
|
|
3150
|
-
const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3151
|
-
if (targetUrl !== "about:blank") {
|
|
3152
|
-
await assertBrowserNavigationAllowed({ url: targetUrl, ssrfPolicy: policy });
|
|
3153
|
-
}
|
|
3154
3049
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
3155
3050
|
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
3156
3051
|
ensureContextState(context);
|
|
3157
3052
|
const page = await context.newPage();
|
|
3158
3053
|
ensurePageState(page);
|
|
3054
|
+
const targetUrl = (opts.url ?? "").trim() || "about:blank";
|
|
3055
|
+
const policy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3159
3056
|
if (targetUrl !== "about:blank") {
|
|
3160
3057
|
const navigationPolicy = withBrowserNavigationPolicy(policy);
|
|
3161
|
-
|
|
3162
|
-
await assertBrowserNavigationRedirectChainAllowed({
|
|
3163
|
-
|
|
3058
|
+
await assertBrowserNavigationAllowed({ url: targetUrl, ...navigationPolicy });
|
|
3059
|
+
await assertBrowserNavigationRedirectChainAllowed({
|
|
3060
|
+
request: (await page.goto(targetUrl, { timeout: 3e4 }).catch(() => null))?.request(),
|
|
3061
|
+
...navigationPolicy
|
|
3062
|
+
});
|
|
3063
|
+
await assertBrowserNavigationResultAllowed({ url: page.url(), ...navigationPolicy });
|
|
3164
3064
|
}
|
|
3165
3065
|
const tid = await pageTargetId(page).catch(() => null);
|
|
3166
|
-
if (
|
|
3066
|
+
if (tid === null || tid === "") throw new Error("Failed to get targetId for new page");
|
|
3167
3067
|
return {
|
|
3168
3068
|
targetId: tid,
|
|
3169
3069
|
title: await page.title().catch(() => ""),
|
|
@@ -3171,213 +3071,240 @@ async function createPageViaPlaywright(opts) {
|
|
|
3171
3071
|
type: "page"
|
|
3172
3072
|
};
|
|
3173
3073
|
}
|
|
3174
|
-
async function
|
|
3175
|
-
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
3176
|
-
const page = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
|
|
3177
|
-
if (!page) throw new Error(`Tab not found (targetId: ${opts.targetId}). Use browser.tabs() to list open tabs.`);
|
|
3178
|
-
await page.close();
|
|
3179
|
-
}
|
|
3180
|
-
async function focusPageByTargetIdViaPlaywright(opts) {
|
|
3181
|
-
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
3182
|
-
const page = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
|
|
3183
|
-
if (!page) throw new Error(`Tab not found (targetId: ${opts.targetId}). Use browser.tabs() to list open tabs.`);
|
|
3184
|
-
try {
|
|
3185
|
-
await page.bringToFront();
|
|
3186
|
-
} catch (err) {
|
|
3187
|
-
const session = await page.context().newCDPSession(page);
|
|
3188
|
-
try {
|
|
3189
|
-
await session.send("Page.bringToFront");
|
|
3190
|
-
} catch {
|
|
3191
|
-
throw err;
|
|
3192
|
-
} finally {
|
|
3193
|
-
await session.detach().catch(() => {
|
|
3194
|
-
});
|
|
3195
|
-
}
|
|
3196
|
-
}
|
|
3197
|
-
}
|
|
3198
|
-
async function resizeViewportViaPlaywright(opts) {
|
|
3199
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3200
|
-
ensurePageState(page);
|
|
3201
|
-
await page.setViewportSize({
|
|
3202
|
-
width: Math.max(1, Math.floor(opts.width)),
|
|
3203
|
-
height: Math.max(1, Math.floor(opts.height))
|
|
3204
|
-
});
|
|
3205
|
-
}
|
|
3206
|
-
|
|
3207
|
-
// src/actions/wait.ts
|
|
3208
|
-
var MAX_WAIT_TIME_MS = 3e4;
|
|
3209
|
-
function resolveBoundedDelayMs2(value, label, maxMs) {
|
|
3210
|
-
const normalized = Math.floor(value ?? 0);
|
|
3211
|
-
if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
|
|
3212
|
-
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${maxMs}ms`);
|
|
3213
|
-
return normalized;
|
|
3214
|
-
}
|
|
3215
|
-
async function waitForViaPlaywright(opts) {
|
|
3074
|
+
async function closePageViaPlaywright(opts) {
|
|
3216
3075
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3217
3076
|
ensurePageState(page);
|
|
3218
|
-
|
|
3219
|
-
if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
|
|
3220
|
-
await page.waitForTimeout(resolveBoundedDelayMs2(opts.timeMs, "wait timeMs", MAX_WAIT_TIME_MS));
|
|
3221
|
-
}
|
|
3222
|
-
if (opts.text) {
|
|
3223
|
-
await page.getByText(opts.text).first().waitFor({ state: "visible", timeout });
|
|
3224
|
-
}
|
|
3225
|
-
if (opts.textGone) {
|
|
3226
|
-
await page.getByText(opts.textGone).first().waitFor({ state: "hidden", timeout });
|
|
3227
|
-
}
|
|
3228
|
-
if (opts.selector) {
|
|
3229
|
-
const selector = String(opts.selector).trim();
|
|
3230
|
-
if (selector) await page.locator(selector).first().waitFor({ state: "visible", timeout });
|
|
3231
|
-
}
|
|
3232
|
-
if (opts.url) {
|
|
3233
|
-
const url = String(opts.url).trim();
|
|
3234
|
-
if (url) await page.waitForURL(url, { timeout });
|
|
3235
|
-
}
|
|
3236
|
-
if (opts.loadState) {
|
|
3237
|
-
await page.waitForLoadState(opts.loadState, { timeout });
|
|
3238
|
-
}
|
|
3239
|
-
if (opts.fn) {
|
|
3240
|
-
const fn = String(opts.fn).trim();
|
|
3241
|
-
if (fn) await page.waitForFunction(fn, void 0, { timeout });
|
|
3242
|
-
}
|
|
3243
|
-
}
|
|
3244
|
-
|
|
3245
|
-
// src/actions/evaluate.ts
|
|
3246
|
-
async function evaluateInAllFramesViaPlaywright(opts) {
|
|
3247
|
-
const fnText = String(opts.fn ?? "").trim();
|
|
3248
|
-
if (!fnText) throw new Error("function is required");
|
|
3249
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3250
|
-
const frames = page.frames();
|
|
3251
|
-
const results = [];
|
|
3252
|
-
for (const frame of frames) {
|
|
3253
|
-
try {
|
|
3254
|
-
const result = await frame.evaluate(
|
|
3255
|
-
// eslint-disable-next-line no-eval
|
|
3256
|
-
(fnBody) => {
|
|
3257
|
-
"use strict";
|
|
3258
|
-
try {
|
|
3259
|
-
const candidate = (0, eval)("(" + fnBody + ")");
|
|
3260
|
-
return typeof candidate === "function" ? candidate() : candidate;
|
|
3261
|
-
} catch (err) {
|
|
3262
|
-
throw new Error("Invalid evaluate function: " + (err instanceof Error ? err.message : String(err)));
|
|
3263
|
-
}
|
|
3264
|
-
},
|
|
3265
|
-
fnText
|
|
3266
|
-
);
|
|
3267
|
-
results.push({
|
|
3268
|
-
frameUrl: frame.url(),
|
|
3269
|
-
frameName: frame.name(),
|
|
3270
|
-
result
|
|
3271
|
-
});
|
|
3272
|
-
} catch {
|
|
3273
|
-
}
|
|
3274
|
-
}
|
|
3275
|
-
return results;
|
|
3077
|
+
await page.close();
|
|
3276
3078
|
}
|
|
3277
|
-
async function
|
|
3278
|
-
|
|
3279
|
-
try {
|
|
3280
|
-
return await Promise.race([evalPromise, abortPromise]);
|
|
3281
|
-
} catch (err) {
|
|
3282
|
-
evalPromise.catch(() => {
|
|
3283
|
-
});
|
|
3284
|
-
throw err;
|
|
3285
|
-
}
|
|
3079
|
+
async function closePageByTargetIdViaPlaywright(opts) {
|
|
3080
|
+
await (await resolvePageByTargetIdOrThrow(opts)).close();
|
|
3286
3081
|
}
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
3290
|
-
try {
|
|
3291
|
-
var candidate = eval("(" + fnBody + ")");
|
|
3292
|
-
var result = typeof candidate === "function" ? candidate() : candidate;
|
|
3293
|
-
if (result && typeof result.then === "function") {
|
|
3294
|
-
return Promise.race([
|
|
3295
|
-
result,
|
|
3296
|
-
new Promise(function(_, reject) {
|
|
3297
|
-
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
3298
|
-
})
|
|
3299
|
-
]);
|
|
3300
|
-
}
|
|
3301
|
-
return result;
|
|
3302
|
-
} catch (err) {
|
|
3303
|
-
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
3304
|
-
}
|
|
3305
|
-
`);
|
|
3306
|
-
var ELEMENT_EVALUATOR = new Function("el", "args", `
|
|
3307
|
-
"use strict";
|
|
3308
|
-
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
3082
|
+
async function focusPageByTargetIdViaPlaywright(opts) {
|
|
3083
|
+
const page = await resolvePageByTargetIdOrThrow(opts);
|
|
3309
3084
|
try {
|
|
3310
|
-
|
|
3311
|
-
var result = typeof candidate === "function" ? candidate(el) : candidate;
|
|
3312
|
-
if (result && typeof result.then === "function") {
|
|
3313
|
-
return Promise.race([
|
|
3314
|
-
result,
|
|
3315
|
-
new Promise(function(_, reject) {
|
|
3316
|
-
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
3317
|
-
})
|
|
3318
|
-
]);
|
|
3319
|
-
}
|
|
3320
|
-
return result;
|
|
3085
|
+
await page.bringToFront();
|
|
3321
3086
|
} catch (err) {
|
|
3322
|
-
|
|
3087
|
+
try {
|
|
3088
|
+
await withPageScopedCdpClient({
|
|
3089
|
+
cdpUrl: opts.cdpUrl,
|
|
3090
|
+
page,
|
|
3091
|
+
targetId: opts.targetId,
|
|
3092
|
+
fn: async (send) => {
|
|
3093
|
+
await send("Page.bringToFront");
|
|
3094
|
+
}
|
|
3095
|
+
});
|
|
3096
|
+
return;
|
|
3097
|
+
} catch {
|
|
3098
|
+
throw err;
|
|
3099
|
+
}
|
|
3323
3100
|
}
|
|
3324
|
-
|
|
3325
|
-
async function
|
|
3326
|
-
const fnText = String(opts.fn ?? "").trim();
|
|
3327
|
-
if (!fnText) throw new Error("function is required");
|
|
3101
|
+
}
|
|
3102
|
+
async function resizeViewportViaPlaywright(opts) {
|
|
3328
3103
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3329
3104
|
ensurePageState(page);
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
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));
|
|
3344
3125
|
}
|
|
3345
|
-
if (
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
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 });
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
// src/actions/batch.ts
|
|
3150
|
+
var MAX_BATCH_DEPTH = 5;
|
|
3151
|
+
var MAX_BATCH_ACTIONS = 100;
|
|
3152
|
+
async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, depth = 0) {
|
|
3153
|
+
if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${String(MAX_BATCH_DEPTH)}`);
|
|
3154
|
+
const effectiveTargetId = action.targetId ?? targetId;
|
|
3155
|
+
switch (action.kind) {
|
|
3156
|
+
case "click":
|
|
3157
|
+
await clickViaPlaywright({
|
|
3158
|
+
cdpUrl,
|
|
3159
|
+
targetId: effectiveTargetId,
|
|
3160
|
+
ref: action.ref,
|
|
3161
|
+
selector: action.selector,
|
|
3162
|
+
doubleClick: action.doubleClick,
|
|
3163
|
+
button: action.button,
|
|
3164
|
+
modifiers: action.modifiers,
|
|
3165
|
+
delayMs: action.delayMs,
|
|
3166
|
+
timeoutMs: action.timeoutMs
|
|
3350
3167
|
});
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3168
|
+
break;
|
|
3169
|
+
case "type":
|
|
3170
|
+
await typeViaPlaywright({
|
|
3171
|
+
cdpUrl,
|
|
3172
|
+
targetId: effectiveTargetId,
|
|
3173
|
+
ref: action.ref,
|
|
3174
|
+
selector: action.selector,
|
|
3175
|
+
text: action.text,
|
|
3176
|
+
submit: action.submit,
|
|
3177
|
+
slowly: action.slowly,
|
|
3178
|
+
timeoutMs: action.timeoutMs
|
|
3179
|
+
});
|
|
3180
|
+
break;
|
|
3181
|
+
case "press":
|
|
3182
|
+
await pressKeyViaPlaywright({
|
|
3183
|
+
cdpUrl,
|
|
3184
|
+
targetId: effectiveTargetId,
|
|
3185
|
+
key: action.key,
|
|
3186
|
+
delayMs: action.delayMs
|
|
3187
|
+
});
|
|
3188
|
+
break;
|
|
3189
|
+
case "hover":
|
|
3190
|
+
await hoverViaPlaywright({
|
|
3191
|
+
cdpUrl,
|
|
3192
|
+
targetId: effectiveTargetId,
|
|
3193
|
+
ref: action.ref,
|
|
3194
|
+
selector: action.selector,
|
|
3195
|
+
timeoutMs: action.timeoutMs
|
|
3196
|
+
});
|
|
3197
|
+
break;
|
|
3198
|
+
case "scrollIntoView":
|
|
3199
|
+
await scrollIntoViewViaPlaywright({
|
|
3200
|
+
cdpUrl,
|
|
3201
|
+
targetId: effectiveTargetId,
|
|
3202
|
+
ref: action.ref,
|
|
3203
|
+
selector: action.selector,
|
|
3204
|
+
timeoutMs: action.timeoutMs
|
|
3205
|
+
});
|
|
3206
|
+
break;
|
|
3207
|
+
case "drag":
|
|
3208
|
+
await dragViaPlaywright({
|
|
3209
|
+
cdpUrl,
|
|
3210
|
+
targetId: effectiveTargetId,
|
|
3211
|
+
startRef: action.startRef,
|
|
3212
|
+
startSelector: action.startSelector,
|
|
3213
|
+
endRef: action.endRef,
|
|
3214
|
+
endSelector: action.endSelector,
|
|
3215
|
+
timeoutMs: action.timeoutMs
|
|
3216
|
+
});
|
|
3217
|
+
break;
|
|
3218
|
+
case "select":
|
|
3219
|
+
await selectOptionViaPlaywright({
|
|
3220
|
+
cdpUrl,
|
|
3221
|
+
targetId: effectiveTargetId,
|
|
3222
|
+
ref: action.ref,
|
|
3223
|
+
selector: action.selector,
|
|
3224
|
+
values: action.values,
|
|
3225
|
+
timeoutMs: action.timeoutMs
|
|
3226
|
+
});
|
|
3227
|
+
break;
|
|
3228
|
+
case "fill":
|
|
3229
|
+
await fillFormViaPlaywright({
|
|
3230
|
+
cdpUrl,
|
|
3231
|
+
targetId: effectiveTargetId,
|
|
3232
|
+
fields: action.fields,
|
|
3233
|
+
timeoutMs: action.timeoutMs
|
|
3234
|
+
});
|
|
3235
|
+
break;
|
|
3236
|
+
case "resize":
|
|
3237
|
+
await resizeViewportViaPlaywright({
|
|
3238
|
+
cdpUrl,
|
|
3239
|
+
targetId: effectiveTargetId,
|
|
3240
|
+
width: action.width,
|
|
3241
|
+
height: action.height
|
|
3242
|
+
});
|
|
3243
|
+
break;
|
|
3244
|
+
case "wait":
|
|
3245
|
+
if (action.fn !== void 0 && action.fn !== "" && !evaluateEnabled)
|
|
3246
|
+
throw new Error("wait --fn is disabled by config (browser.evaluateEnabled=false)");
|
|
3247
|
+
await waitForViaPlaywright({
|
|
3248
|
+
cdpUrl,
|
|
3249
|
+
targetId: effectiveTargetId,
|
|
3250
|
+
timeMs: action.timeMs,
|
|
3251
|
+
text: action.text,
|
|
3252
|
+
textGone: action.textGone,
|
|
3253
|
+
selector: action.selector,
|
|
3254
|
+
url: action.url,
|
|
3255
|
+
loadState: action.loadState,
|
|
3256
|
+
fn: action.fn,
|
|
3257
|
+
timeoutMs: action.timeoutMs
|
|
3258
|
+
});
|
|
3259
|
+
break;
|
|
3260
|
+
case "evaluate":
|
|
3261
|
+
if (!evaluateEnabled) throw new Error("act:evaluate is disabled by config (browser.evaluateEnabled=false)");
|
|
3262
|
+
await evaluateViaPlaywright({
|
|
3263
|
+
cdpUrl,
|
|
3264
|
+
targetId: effectiveTargetId,
|
|
3265
|
+
fn: action.fn,
|
|
3266
|
+
ref: action.ref,
|
|
3267
|
+
timeoutMs: action.timeoutMs
|
|
3268
|
+
});
|
|
3269
|
+
break;
|
|
3270
|
+
case "close":
|
|
3271
|
+
await closePageViaPlaywright({
|
|
3272
|
+
cdpUrl,
|
|
3273
|
+
targetId: effectiveTargetId
|
|
3274
|
+
});
|
|
3275
|
+
break;
|
|
3276
|
+
case "batch":
|
|
3277
|
+
await batchViaPlaywright({
|
|
3278
|
+
cdpUrl,
|
|
3279
|
+
targetId: effectiveTargetId,
|
|
3280
|
+
actions: action.actions,
|
|
3281
|
+
stopOnError: action.stopOnError,
|
|
3282
|
+
evaluateEnabled,
|
|
3283
|
+
depth: depth + 1
|
|
3284
|
+
});
|
|
3285
|
+
break;
|
|
3286
|
+
default:
|
|
3287
|
+
throw new Error(`Unsupported batch action kind: ${String(action.kind)}`);
|
|
3365
3288
|
}
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3289
|
+
}
|
|
3290
|
+
async function batchViaPlaywright(opts) {
|
|
3291
|
+
const depth = opts.depth ?? 0;
|
|
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`);
|
|
3295
|
+
const results = [];
|
|
3296
|
+
const evaluateEnabled = opts.evaluateEnabled !== false;
|
|
3297
|
+
for (const action of opts.actions) {
|
|
3298
|
+
try {
|
|
3299
|
+
await executeSingleAction(action, opts.cdpUrl, opts.targetId, evaluateEnabled, depth);
|
|
3300
|
+
results.push({ ok: true });
|
|
3301
|
+
} catch (err) {
|
|
3302
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3303
|
+
results.push({ ok: false, error: message });
|
|
3304
|
+
if (opts.stopOnError !== false) break;
|
|
3373
3305
|
}
|
|
3374
|
-
return await awaitEvalWithAbort(
|
|
3375
|
-
page.evaluate(BROWSER_EVALUATOR, { fnBody: fnText, timeoutMs: evaluateTimeout }),
|
|
3376
|
-
abortPromise
|
|
3377
|
-
);
|
|
3378
|
-
} finally {
|
|
3379
|
-
if (signal && abortListener) signal.removeEventListener("abort", abortListener);
|
|
3380
3306
|
}
|
|
3307
|
+
return { results };
|
|
3381
3308
|
}
|
|
3382
3309
|
function createPageDownloadWaiter(page, timeoutMs) {
|
|
3383
3310
|
let done = false;
|
|
@@ -3412,379 +3339,856 @@ function createPageDownloadWaiter(page, timeoutMs) {
|
|
|
3412
3339
|
done = true;
|
|
3413
3340
|
cleanup();
|
|
3414
3341
|
}
|
|
3415
|
-
};
|
|
3342
|
+
};
|
|
3343
|
+
}
|
|
3344
|
+
async function saveDownloadPayload(download, outPath) {
|
|
3345
|
+
await writeViaSiblingTempPath({
|
|
3346
|
+
rootDir: path.dirname(outPath),
|
|
3347
|
+
targetPath: outPath,
|
|
3348
|
+
writeTemp: async (tempPath) => {
|
|
3349
|
+
await download.saveAs(tempPath);
|
|
3350
|
+
}
|
|
3351
|
+
});
|
|
3352
|
+
return {
|
|
3353
|
+
url: download.url(),
|
|
3354
|
+
suggestedFilename: download.suggestedFilename(),
|
|
3355
|
+
path: outPath
|
|
3356
|
+
};
|
|
3357
|
+
}
|
|
3358
|
+
async function awaitDownloadPayload(params) {
|
|
3359
|
+
try {
|
|
3360
|
+
const download = await params.waiter.promise;
|
|
3361
|
+
if (params.state.armIdDownload !== params.armId) throw new Error("Download was superseded by another waiter");
|
|
3362
|
+
return await saveDownloadPayload(download, params.outPath);
|
|
3363
|
+
} catch (err) {
|
|
3364
|
+
params.waiter.cancel();
|
|
3365
|
+
throw err;
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
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
|
+
});
|
|
3416
3522
|
}
|
|
3417
|
-
async function
|
|
3418
|
-
await
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
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
|
+
}
|
|
3423
3546
|
}
|
|
3424
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) {
|
|
3593
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
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, ".*");
|
|
3604
|
+
try {
|
|
3605
|
+
return new RegExp(`^${escaped}$`).test(url);
|
|
3606
|
+
} catch {
|
|
3607
|
+
return false;
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
return url.includes(pattern);
|
|
3611
|
+
}
|
|
3612
|
+
async function responseBodyViaPlaywright(opts) {
|
|
3613
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
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;
|
|
3630
|
+
}
|
|
3425
3631
|
return {
|
|
3426
|
-
url:
|
|
3427
|
-
|
|
3428
|
-
|
|
3632
|
+
url: response.url(),
|
|
3633
|
+
status: response.status(),
|
|
3634
|
+
headers,
|
|
3635
|
+
body,
|
|
3636
|
+
truncated
|
|
3429
3637
|
};
|
|
3430
3638
|
}
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3639
|
+
|
|
3640
|
+
// src/capture/screenshot.ts
|
|
3641
|
+
async function takeScreenshotViaPlaywright(opts) {
|
|
3642
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3643
|
+
ensurePageState(page);
|
|
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 }) };
|
|
3439
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 }) };
|
|
3653
|
+
}
|
|
3654
|
+
return { buffer: await page.screenshot({ type, fullPage: Boolean(opts.fullPage) }) };
|
|
3440
3655
|
}
|
|
3441
|
-
async function
|
|
3442
|
-
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
3656
|
+
async function screenshotWithLabelsViaPlaywright(opts) {
|
|
3443
3657
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3444
|
-
|
|
3658
|
+
ensurePageState(page);
|
|
3445
3659
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
3446
|
-
const
|
|
3447
|
-
const
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
const
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
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];
|
|
3454
3671
|
try {
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3672
|
+
const locator = refLocator(page, ref);
|
|
3673
|
+
const box = await locator.boundingBox({ timeout: 2e3 });
|
|
3674
|
+
if (!box) {
|
|
3675
|
+
skipped.push(ref);
|
|
3676
|
+
continue;
|
|
3677
|
+
}
|
|
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;
|
|
3683
|
+
}
|
|
3684
|
+
labels.push({ ref, index: i + 1, box });
|
|
3685
|
+
} catch {
|
|
3686
|
+
skipped.push(ref);
|
|
3458
3687
|
}
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
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
|
+
});
|
|
3463
3725
|
}
|
|
3464
3726
|
}
|
|
3465
|
-
async function
|
|
3727
|
+
async function traceStartViaPlaywright(opts) {
|
|
3466
3728
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3467
|
-
|
|
3468
|
-
const
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
try {
|
|
3473
|
-
const download = await waiter.promise;
|
|
3474
|
-
if (state.armIdDownload !== armId) throw new Error("Download was superseded by another waiter");
|
|
3475
|
-
const savePath = opts.path ?? download.suggestedFilename();
|
|
3476
|
-
await assertSafeOutputPath(savePath, opts.allowedOutputRoots);
|
|
3477
|
-
return await saveDownloadPayload(download, savePath);
|
|
3478
|
-
} catch (err) {
|
|
3479
|
-
waiter.cancel();
|
|
3480
|
-
throw err;
|
|
3729
|
+
ensurePageState(page);
|
|
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.");
|
|
3481
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;
|
|
3482
3741
|
}
|
|
3483
|
-
async function
|
|
3742
|
+
async function traceStopViaPlaywright(opts) {
|
|
3743
|
+
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
3484
3744
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3485
3745
|
ensurePageState(page);
|
|
3486
|
-
|
|
3746
|
+
const context = page.context();
|
|
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;
|
|
3487
3816
|
}
|
|
3488
|
-
|
|
3489
|
-
const
|
|
3490
|
-
if (
|
|
3491
|
-
|
|
3492
|
-
if (!device) {
|
|
3493
|
-
throw new Error(`Unknown device "${name}".`);
|
|
3817
|
+
function matchInteractiveSnapshotLine(line, options) {
|
|
3818
|
+
const depth = getIndentLevel(line);
|
|
3819
|
+
if (options.maxDepth !== void 0 && depth > options.maxDepth) {
|
|
3820
|
+
return null;
|
|
3494
3821
|
}
|
|
3495
|
-
const
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
await page.setViewportSize({
|
|
3499
|
-
width: device.viewport.width,
|
|
3500
|
-
height: device.viewport.height
|
|
3501
|
-
});
|
|
3822
|
+
const match = /^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/.exec(line);
|
|
3823
|
+
if (!match) {
|
|
3824
|
+
return null;
|
|
3502
3825
|
}
|
|
3503
|
-
const
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
if (device.userAgent || locale) {
|
|
3507
|
-
await session.send("Emulation.setUserAgentOverride", {
|
|
3508
|
-
userAgent: device.userAgent ?? "",
|
|
3509
|
-
acceptLanguage: locale ?? void 0
|
|
3510
|
-
});
|
|
3511
|
-
}
|
|
3512
|
-
if (device.viewport) {
|
|
3513
|
-
await session.send("Emulation.setDeviceMetricsOverride", {
|
|
3514
|
-
mobile: Boolean(device.isMobile),
|
|
3515
|
-
width: device.viewport.width,
|
|
3516
|
-
height: device.viewport.height,
|
|
3517
|
-
deviceScaleFactor: device.deviceScaleFactor ?? 1,
|
|
3518
|
-
screenWidth: device.viewport.width,
|
|
3519
|
-
screenHeight: device.viewport.height
|
|
3520
|
-
});
|
|
3521
|
-
}
|
|
3522
|
-
if (device.hasTouch) {
|
|
3523
|
-
await session.send("Emulation.setTouchEmulationEnabled", { enabled: true });
|
|
3524
|
-
}
|
|
3525
|
-
} finally {
|
|
3526
|
-
await session.detach().catch(() => {
|
|
3527
|
-
});
|
|
3826
|
+
const [, , roleRaw, name, suffix] = match;
|
|
3827
|
+
if (roleRaw.startsWith("/")) {
|
|
3828
|
+
return null;
|
|
3528
3829
|
}
|
|
3830
|
+
const role = roleRaw.toLowerCase();
|
|
3831
|
+
return {
|
|
3832
|
+
roleRaw,
|
|
3833
|
+
role,
|
|
3834
|
+
...name ? { name } : {},
|
|
3835
|
+
suffix
|
|
3836
|
+
};
|
|
3529
3837
|
}
|
|
3530
|
-
|
|
3531
|
-
const
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
return new URL(page.url()).origin;
|
|
3556
|
-
} catch {
|
|
3557
|
-
return "";
|
|
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;
|
|
3558
3863
|
}
|
|
3559
|
-
}
|
|
3560
|
-
if (origin) await context.grantPermissions(["geolocation"], { origin }).catch(() => {
|
|
3561
|
-
});
|
|
3864
|
+
};
|
|
3562
3865
|
}
|
|
3563
|
-
|
|
3564
|
-
const
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
return;
|
|
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;
|
|
3569
3871
|
}
|
|
3570
|
-
const username = String(opts.username ?? "");
|
|
3571
|
-
const password = String(opts.password ?? "");
|
|
3572
|
-
if (!username) throw new Error("username is required (or set clear=true)");
|
|
3573
|
-
await page.context().setHTTPCredentials({ username, password });
|
|
3574
3872
|
}
|
|
3575
|
-
|
|
3576
|
-
const
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
await session.send("Emulation.setLocaleOverride", { locale });
|
|
3584
|
-
} catch (err) {
|
|
3585
|
-
if (String(err).includes("Another locale override is already in effect")) return;
|
|
3586
|
-
throw err;
|
|
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;
|
|
3587
3881
|
}
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
}
|
|
3592
|
-
}
|
|
3593
|
-
async function setOfflineViaPlaywright(opts) {
|
|
3594
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3595
|
-
ensurePageState(page);
|
|
3596
|
-
await page.context().setOffline(Boolean(opts.offline));
|
|
3597
|
-
}
|
|
3598
|
-
async function setTimezoneViaPlaywright(opts) {
|
|
3599
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3600
|
-
ensurePageState(page);
|
|
3601
|
-
const timezoneId = String(opts.timezoneId ?? "").trim();
|
|
3602
|
-
if (!timezoneId) throw new Error("timezoneId is required");
|
|
3603
|
-
const session = await page.context().newCDPSession(page);
|
|
3604
|
-
try {
|
|
3605
|
-
try {
|
|
3606
|
-
await session.send("Emulation.setTimezoneOverride", { timezoneId });
|
|
3607
|
-
} catch (err) {
|
|
3608
|
-
const msg = String(err);
|
|
3609
|
-
if (msg.includes("Timezone override is already in effect")) return;
|
|
3610
|
-
if (msg.includes("Invalid timezone")) throw new Error(`Invalid timezone ID: ${timezoneId}`, { cause: err });
|
|
3611
|
-
throw err;
|
|
3882
|
+
if (line.includes(":") && !line.trimEnd().endsWith(":")) {
|
|
3883
|
+
result.push(line);
|
|
3884
|
+
continue;
|
|
3612
3885
|
}
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
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;
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
if (hasRelevantChildren) result.push(line);
|
|
3616
3896
|
}
|
|
3897
|
+
return result.join("\n");
|
|
3617
3898
|
}
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
const
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
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 };
|
|
3628
3929
|
}
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
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);
|
|
3632
3963
|
}
|
|
3633
|
-
|
|
3964
|
+
removeNthFromNonDuplicates(refs, tracker);
|
|
3965
|
+
const tree = result.join("\n") || "(empty)";
|
|
3966
|
+
return { snapshot: options.compact === true ? compactTree(tree) : tree, refs };
|
|
3634
3967
|
}
|
|
3635
|
-
|
|
3636
|
-
const
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
const
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
}
|
|
3654
|
-
} catch {
|
|
3655
|
-
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}`);
|
|
3656
3987
|
}
|
|
3988
|
+
return { snapshot: out2.join("\n") || "(no interactive elements)", refs };
|
|
3657
3989
|
}
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
const badge = document.createElement("div");
|
|
3667
|
-
badge.textContent = String(index);
|
|
3668
|
-
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;`;
|
|
3669
|
-
container.appendChild(badge);
|
|
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;
|
|
3670
3998
|
}
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
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);
|
|
4010
|
+
}
|
|
4011
|
+
const tree = out.join("\n") || "(empty)";
|
|
4012
|
+
return { snapshot: options.compact === true ? compactTree(tree) : tree, refs };
|
|
3680
4013
|
}
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
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
|
+
};
|
|
3687
4022
|
}
|
|
3688
|
-
|
|
4023
|
+
|
|
4024
|
+
// src/snapshot/ai-snapshot.ts
|
|
4025
|
+
async function snapshotAi(opts) {
|
|
3689
4026
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3690
4027
|
ensurePageState(page);
|
|
3691
|
-
const
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
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.");
|
|
3695
4031
|
}
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
4032
|
+
const sourceUrl = page.url();
|
|
4033
|
+
const result = await maybe._snapshotForAI({
|
|
4034
|
+
timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3, 6e4),
|
|
4035
|
+
track: "response"
|
|
3700
4036
|
});
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
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;
|
|
3711
4048
|
}
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
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"
|
|
3718
4056
|
});
|
|
3719
|
-
|
|
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
|
+
};
|
|
3720
4069
|
}
|
|
3721
4070
|
|
|
3722
|
-
// src/
|
|
3723
|
-
async function
|
|
4071
|
+
// src/snapshot/aria-snapshot.ts
|
|
4072
|
+
async function snapshotRole(opts) {
|
|
3724
4073
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3725
4074
|
ensurePageState(page);
|
|
3726
|
-
const
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
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.");
|
|
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
|
+
};
|
|
3739
4104
|
}
|
|
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
|
+
});
|
|
3740
4118
|
return {
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
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
|
+
}
|
|
3746
4128
|
};
|
|
3747
4129
|
}
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
}
|
|
3766
|
-
|
|
3767
|
-
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
3768
|
-
const messages = opts.level ? state.console.filter((msg) => consolePriority(msg.type) >= consolePriority(opts.level)) : [...state.console];
|
|
3769
|
-
if (opts.clear) state.console = [];
|
|
3770
|
-
return messages;
|
|
4130
|
+
async function snapshotAria(opts) {
|
|
4131
|
+
const limit = Math.max(1, Math.min(2e3, Math.floor(opts.limit ?? 500)));
|
|
4132
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
4133
|
+
ensurePageState(page);
|
|
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
|
+
});
|
|
4140
|
+
return {
|
|
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
|
+
}
|
|
4148
|
+
};
|
|
3771
4149
|
}
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
const
|
|
3775
|
-
if (
|
|
3776
|
-
|
|
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 "";
|
|
3777
4156
|
}
|
|
3778
|
-
|
|
3779
|
-
const
|
|
3780
|
-
const
|
|
3781
|
-
const
|
|
3782
|
-
const
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
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
|
+
}
|
|
3786
4190
|
}
|
|
3787
|
-
return
|
|
4191
|
+
return out;
|
|
3788
4192
|
}
|
|
3789
4193
|
|
|
3790
4194
|
// src/storage/index.ts
|
|
@@ -3797,9 +4201,9 @@ async function cookiesSetViaPlaywright(opts) {
|
|
|
3797
4201
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3798
4202
|
ensurePageState(page);
|
|
3799
4203
|
const cookie = opts.cookie;
|
|
3800
|
-
if (
|
|
3801
|
-
const hasUrl = typeof cookie.url === "string" && cookie.url.trim();
|
|
3802
|
-
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() !== "";
|
|
3803
4207
|
if (!hasUrl && !hasDomainPath) throw new Error("cookie requires url, or domain+path");
|
|
3804
4208
|
await page.context().addCookies([cookie]);
|
|
3805
4209
|
}
|
|
@@ -3815,33 +4219,33 @@ async function storageGetViaPlaywright(opts) {
|
|
|
3815
4219
|
values: await page.evaluate(
|
|
3816
4220
|
({ kind, key }) => {
|
|
3817
4221
|
const store = kind === "session" ? window.sessionStorage : window.localStorage;
|
|
3818
|
-
if (key) {
|
|
4222
|
+
if (key !== void 0 && key !== "") {
|
|
3819
4223
|
const value = store.getItem(key);
|
|
3820
4224
|
return value === null ? {} : { [key]: value };
|
|
3821
4225
|
}
|
|
3822
4226
|
const out = {};
|
|
3823
4227
|
for (let i = 0; i < store.length; i++) {
|
|
3824
4228
|
const k = store.key(i);
|
|
3825
|
-
if (
|
|
4229
|
+
if (k === null || k === "") continue;
|
|
3826
4230
|
const v = store.getItem(k);
|
|
3827
4231
|
if (v !== null) out[k] = v;
|
|
3828
4232
|
}
|
|
3829
4233
|
return out;
|
|
3830
4234
|
},
|
|
3831
4235
|
{ kind: opts.kind, key: opts.key }
|
|
3832
|
-
)
|
|
4236
|
+
)
|
|
3833
4237
|
};
|
|
3834
4238
|
}
|
|
3835
4239
|
async function storageSetViaPlaywright(opts) {
|
|
3836
|
-
const key =
|
|
3837
|
-
if (
|
|
4240
|
+
const key = opts.key;
|
|
4241
|
+
if (key === "") throw new Error("key is required");
|
|
3838
4242
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3839
4243
|
ensurePageState(page);
|
|
3840
4244
|
await page.evaluate(
|
|
3841
4245
|
({ kind, key: k, value }) => {
|
|
3842
4246
|
(kind === "session" ? window.sessionStorage : window.localStorage).setItem(k, value);
|
|
3843
4247
|
},
|
|
3844
|
-
{ kind: opts.kind, key, value:
|
|
4248
|
+
{ kind: opts.kind, key, value: opts.value }
|
|
3845
4249
|
);
|
|
3846
4250
|
}
|
|
3847
4251
|
async function storageClearViaPlaywright(opts) {
|
|
@@ -3908,8 +4312,10 @@ var CrawlPage = class {
|
|
|
3908
4312
|
}
|
|
3909
4313
|
});
|
|
3910
4314
|
}
|
|
3911
|
-
if (opts?.selector || opts?.frameSelector) {
|
|
3912
|
-
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
|
+
);
|
|
3913
4319
|
}
|
|
3914
4320
|
return snapshotAi({
|
|
3915
4321
|
cdpUrl: this.cdpUrl,
|
|
@@ -4148,6 +4554,22 @@ var CrawlPage = class {
|
|
|
4148
4554
|
timeoutMs: opts?.timeoutMs
|
|
4149
4555
|
});
|
|
4150
4556
|
}
|
|
4557
|
+
/**
|
|
4558
|
+
* Execute multiple browser actions in sequence.
|
|
4559
|
+
*
|
|
4560
|
+
* @param actions - Array of actions to execute
|
|
4561
|
+
* @param opts - Options (stopOnError: stop on first failure, default true)
|
|
4562
|
+
* @returns Array of per-action results
|
|
4563
|
+
*/
|
|
4564
|
+
async batch(actions, opts) {
|
|
4565
|
+
return batchViaPlaywright({
|
|
4566
|
+
cdpUrl: this.cdpUrl,
|
|
4567
|
+
targetId: this.targetId,
|
|
4568
|
+
actions,
|
|
4569
|
+
stopOnError: opts?.stopOnError,
|
|
4570
|
+
evaluateEnabled: opts?.evaluateEnabled
|
|
4571
|
+
});
|
|
4572
|
+
}
|
|
4151
4573
|
// ── Keyboard ─────────────────────────────────────────────────
|
|
4152
4574
|
/**
|
|
4153
4575
|
* Press a keyboard key or key combination.
|
|
@@ -4764,8 +5186,8 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
4764
5186
|
*/
|
|
4765
5187
|
static async launch(opts = {}) {
|
|
4766
5188
|
const chrome = await launchChrome(opts);
|
|
4767
|
-
const cdpUrl = `http://127.0.0.1:${chrome.cdpPort}`;
|
|
4768
|
-
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;
|
|
4769
5191
|
return new _BrowserClaw(cdpUrl, chrome, ssrfPolicy);
|
|
4770
5192
|
}
|
|
4771
5193
|
/**
|
|
@@ -4787,7 +5209,7 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
4787
5209
|
throw new Error(`Cannot connect to Chrome at ${cdpUrl}. Is Chrome running with --remote-debugging-port?`);
|
|
4788
5210
|
}
|
|
4789
5211
|
await connectBrowser(cdpUrl, opts?.authToken);
|
|
4790
|
-
const ssrfPolicy = opts?.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts?.ssrfPolicy;
|
|
5212
|
+
const ssrfPolicy = opts?.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts?.ssrfPolicy;
|
|
4791
5213
|
return new _BrowserClaw(cdpUrl, null, ssrfPolicy);
|
|
4792
5214
|
}
|
|
4793
5215
|
/**
|
|
@@ -4813,10 +5235,10 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
4813
5235
|
*/
|
|
4814
5236
|
async currentPage() {
|
|
4815
5237
|
const { browser } = await connectBrowser(this.cdpUrl);
|
|
4816
|
-
const pages =
|
|
5238
|
+
const pages = getAllPages(browser);
|
|
4817
5239
|
if (!pages.length) throw new Error("No pages available. Use browser.open(url) to create a tab.");
|
|
4818
5240
|
const tid = await pageTargetId(pages[0]).catch(() => null);
|
|
4819
|
-
if (
|
|
5241
|
+
if (tid === null || tid === "") throw new Error("Failed to get targetId for the current page.");
|
|
4820
5242
|
return new CrawlPage(this.cdpUrl, tid, this.ssrfPolicy);
|
|
4821
5243
|
}
|
|
4822
5244
|
/**
|
|
@@ -4875,22 +5297,36 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
4875
5297
|
};
|
|
4876
5298
|
|
|
4877
5299
|
exports.BrowserClaw = BrowserClaw;
|
|
5300
|
+
exports.BrowserTabNotFoundError = BrowserTabNotFoundError;
|
|
4878
5301
|
exports.CrawlPage = CrawlPage;
|
|
4879
5302
|
exports.InvalidBrowserNavigationUrlError = InvalidBrowserNavigationUrlError;
|
|
4880
5303
|
exports.assertBrowserNavigationAllowed = assertBrowserNavigationAllowed;
|
|
4881
5304
|
exports.assertBrowserNavigationRedirectChainAllowed = assertBrowserNavigationRedirectChainAllowed;
|
|
4882
5305
|
exports.assertBrowserNavigationResultAllowed = assertBrowserNavigationResultAllowed;
|
|
5306
|
+
exports.assertSafeUploadPaths = assertSafeUploadPaths;
|
|
5307
|
+
exports.batchViaPlaywright = batchViaPlaywright;
|
|
4883
5308
|
exports.createPinnedLookup = createPinnedLookup;
|
|
4884
5309
|
exports.ensureContextState = ensureContextState;
|
|
5310
|
+
exports.executeSingleAction = executeSingleAction;
|
|
4885
5311
|
exports.forceDisconnectPlaywrightForTarget = forceDisconnectPlaywrightForTarget;
|
|
4886
5312
|
exports.getChromeWebSocketUrl = getChromeWebSocketUrl;
|
|
5313
|
+
exports.getRestoredPageForTarget = getRestoredPageForTarget;
|
|
4887
5314
|
exports.isChromeCdpReady = isChromeCdpReady;
|
|
4888
5315
|
exports.isChromeReachable = isChromeReachable;
|
|
4889
5316
|
exports.normalizeCdpHttpBaseForJsonEndpoints = normalizeCdpHttpBaseForJsonEndpoints;
|
|
5317
|
+
exports.parseRoleRef = parseRoleRef;
|
|
5318
|
+
exports.requireRef = requireRef;
|
|
5319
|
+
exports.requireRefOrSelector = requireRefOrSelector;
|
|
4890
5320
|
exports.requiresInspectableBrowserNavigationRedirects = requiresInspectableBrowserNavigationRedirects;
|
|
5321
|
+
exports.resolveBoundedDelayMs = resolveBoundedDelayMs;
|
|
5322
|
+
exports.resolveInteractionTimeoutMs = resolveInteractionTimeoutMs;
|
|
5323
|
+
exports.resolvePageByTargetIdOrThrow = resolvePageByTargetIdOrThrow;
|
|
4891
5324
|
exports.resolvePinnedHostnameWithPolicy = resolvePinnedHostnameWithPolicy;
|
|
5325
|
+
exports.resolveStrictExistingUploadPaths = resolveStrictExistingUploadPaths;
|
|
4892
5326
|
exports.sanitizeUntrustedFileName = sanitizeUntrustedFileName;
|
|
4893
5327
|
exports.withBrowserNavigationPolicy = withBrowserNavigationPolicy;
|
|
5328
|
+
exports.withPageScopedCdpClient = withPageScopedCdpClient;
|
|
5329
|
+
exports.withPlaywrightPageCdpSession = withPlaywrightPageCdpSession;
|
|
4894
5330
|
exports.writeViaSiblingTempPath = writeViaSiblingTempPath;
|
|
4895
5331
|
//# sourceMappingURL=index.cjs.map
|
|
4896
5332
|
//# sourceMappingURL=index.cjs.map
|