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