browserclaw 0.1.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.
Potentially problematic release.
This version of browserclaw might be problematic. Click here for more details.
- package/LICENSE +22 -0
- package/README.md +278 -0
- package/dist/index.cjs +2183 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +728 -0
- package/dist/index.d.ts +728 -0
- package/dist/index.js +2173 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2183 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var os = require('os');
|
|
4
|
+
var path = require('path');
|
|
5
|
+
var fs = require('fs');
|
|
6
|
+
var net = require('net');
|
|
7
|
+
var child_process = require('child_process');
|
|
8
|
+
var playwrightCore = require('playwright-core');
|
|
9
|
+
|
|
10
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
+
|
|
12
|
+
var os__default = /*#__PURE__*/_interopDefault(os);
|
|
13
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
14
|
+
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
15
|
+
var net__default = /*#__PURE__*/_interopDefault(net);
|
|
16
|
+
|
|
17
|
+
// src/chrome-launcher.ts
|
|
18
|
+
var CHROMIUM_BUNDLE_IDS = /* @__PURE__ */ new Set([
|
|
19
|
+
"com.google.Chrome",
|
|
20
|
+
"com.google.Chrome.beta",
|
|
21
|
+
"com.google.Chrome.canary",
|
|
22
|
+
"com.google.Chrome.dev",
|
|
23
|
+
"com.brave.Browser",
|
|
24
|
+
"com.brave.Browser.beta",
|
|
25
|
+
"com.brave.Browser.nightly",
|
|
26
|
+
"com.microsoft.Edge",
|
|
27
|
+
"com.microsoft.EdgeBeta",
|
|
28
|
+
"com.microsoft.EdgeDev",
|
|
29
|
+
"com.microsoft.EdgeCanary",
|
|
30
|
+
"org.chromium.Chromium",
|
|
31
|
+
"com.vivaldi.Vivaldi",
|
|
32
|
+
"com.operasoftware.Opera",
|
|
33
|
+
"com.operasoftware.OperaGX",
|
|
34
|
+
"com.yandex.desktop.yandex-browser",
|
|
35
|
+
"company.thebrowser.Browser"
|
|
36
|
+
]);
|
|
37
|
+
var CHROMIUM_DESKTOP_IDS = /* @__PURE__ */ new Set([
|
|
38
|
+
"google-chrome.desktop",
|
|
39
|
+
"google-chrome-beta.desktop",
|
|
40
|
+
"google-chrome-unstable.desktop",
|
|
41
|
+
"brave-browser.desktop",
|
|
42
|
+
"microsoft-edge.desktop",
|
|
43
|
+
"microsoft-edge-beta.desktop",
|
|
44
|
+
"microsoft-edge-dev.desktop",
|
|
45
|
+
"microsoft-edge-canary.desktop",
|
|
46
|
+
"chromium.desktop",
|
|
47
|
+
"chromium-browser.desktop",
|
|
48
|
+
"vivaldi.desktop",
|
|
49
|
+
"vivaldi-stable.desktop",
|
|
50
|
+
"opera.desktop",
|
|
51
|
+
"opera-gx.desktop",
|
|
52
|
+
"yandex-browser.desktop",
|
|
53
|
+
"org.chromium.Chromium.desktop"
|
|
54
|
+
]);
|
|
55
|
+
var CHROMIUM_EXE_NAMES = /* @__PURE__ */ new Set([
|
|
56
|
+
"chrome.exe",
|
|
57
|
+
"msedge.exe",
|
|
58
|
+
"brave.exe",
|
|
59
|
+
"brave-browser.exe",
|
|
60
|
+
"chromium.exe",
|
|
61
|
+
"vivaldi.exe",
|
|
62
|
+
"opera.exe",
|
|
63
|
+
"launcher.exe",
|
|
64
|
+
"yandex.exe",
|
|
65
|
+
"yandexbrowser.exe",
|
|
66
|
+
"google chrome",
|
|
67
|
+
"google chrome canary",
|
|
68
|
+
"brave browser",
|
|
69
|
+
"microsoft edge",
|
|
70
|
+
"chromium",
|
|
71
|
+
"chrome",
|
|
72
|
+
"brave",
|
|
73
|
+
"msedge",
|
|
74
|
+
"brave-browser",
|
|
75
|
+
"google-chrome",
|
|
76
|
+
"google-chrome-stable",
|
|
77
|
+
"google-chrome-beta",
|
|
78
|
+
"google-chrome-unstable",
|
|
79
|
+
"microsoft-edge",
|
|
80
|
+
"microsoft-edge-beta",
|
|
81
|
+
"microsoft-edge-dev",
|
|
82
|
+
"microsoft-edge-canary",
|
|
83
|
+
"chromium-browser",
|
|
84
|
+
"vivaldi",
|
|
85
|
+
"vivaldi-stable",
|
|
86
|
+
"opera",
|
|
87
|
+
"opera-stable",
|
|
88
|
+
"opera-gx",
|
|
89
|
+
"yandex-browser"
|
|
90
|
+
]);
|
|
91
|
+
function fileExists(filePath) {
|
|
92
|
+
try {
|
|
93
|
+
return fs__default.default.existsSync(filePath);
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function execText(command, args, timeoutMs = 1200) {
|
|
99
|
+
try {
|
|
100
|
+
const output = child_process.execFileSync(command, args, {
|
|
101
|
+
timeout: timeoutMs,
|
|
102
|
+
encoding: "utf8",
|
|
103
|
+
maxBuffer: 1024 * 1024
|
|
104
|
+
});
|
|
105
|
+
return String(output ?? "").trim() || null;
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function inferKindFromIdentifier(identifier) {
|
|
111
|
+
const id = identifier.toLowerCase();
|
|
112
|
+
if (id.includes("brave")) return "brave";
|
|
113
|
+
if (id.includes("edge")) return "edge";
|
|
114
|
+
if (id.includes("chromium")) return "chromium";
|
|
115
|
+
if (id.includes("canary")) return "canary";
|
|
116
|
+
if (id.includes("opera") || id.includes("vivaldi") || id.includes("yandex") || id.includes("thebrowser")) return "chromium";
|
|
117
|
+
return "chrome";
|
|
118
|
+
}
|
|
119
|
+
function inferKindFromExeName(name) {
|
|
120
|
+
const lower = name.toLowerCase();
|
|
121
|
+
if (lower.includes("brave")) return "brave";
|
|
122
|
+
if (lower.includes("edge") || lower.includes("msedge")) return "edge";
|
|
123
|
+
if (lower.includes("chromium")) return "chromium";
|
|
124
|
+
if (lower.includes("canary") || lower.includes("sxs")) return "canary";
|
|
125
|
+
if (lower.includes("opera") || lower.includes("vivaldi") || lower.includes("yandex")) return "chromium";
|
|
126
|
+
return "chrome";
|
|
127
|
+
}
|
|
128
|
+
function findFirstExe(candidates) {
|
|
129
|
+
for (const c of candidates) if (fileExists(c.path)) return c;
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
function detectDefaultBrowserBundleIdMac() {
|
|
133
|
+
const plistPath = path__default.default.join(os__default.default.homedir(), "Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist");
|
|
134
|
+
if (!fileExists(plistPath)) return null;
|
|
135
|
+
const handlersRaw = execText("/usr/bin/plutil", ["-extract", "LSHandlers", "json", "-o", "-", "--", plistPath], 2e3);
|
|
136
|
+
if (!handlersRaw) return null;
|
|
137
|
+
let handlers;
|
|
138
|
+
try {
|
|
139
|
+
handlers = JSON.parse(handlersRaw);
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
if (!Array.isArray(handlers)) return null;
|
|
144
|
+
const resolveScheme = (scheme) => {
|
|
145
|
+
let candidate = null;
|
|
146
|
+
for (const entry of handlers) {
|
|
147
|
+
if (!entry || typeof entry !== "object") continue;
|
|
148
|
+
if (entry.LSHandlerURLScheme !== scheme) continue;
|
|
149
|
+
const role = typeof entry.LSHandlerRoleAll === "string" && entry.LSHandlerRoleAll || typeof entry.LSHandlerRoleViewer === "string" && entry.LSHandlerRoleViewer || null;
|
|
150
|
+
if (role) candidate = role;
|
|
151
|
+
}
|
|
152
|
+
return candidate;
|
|
153
|
+
};
|
|
154
|
+
return resolveScheme("http") ?? resolveScheme("https");
|
|
155
|
+
}
|
|
156
|
+
function detectDefaultChromiumMac() {
|
|
157
|
+
const bundleId = detectDefaultBrowserBundleIdMac();
|
|
158
|
+
if (!bundleId || !CHROMIUM_BUNDLE_IDS.has(bundleId)) return null;
|
|
159
|
+
const appPathRaw = execText("/usr/bin/osascript", ["-e", `POSIX path of (path to application id "${bundleId}")`]);
|
|
160
|
+
if (!appPathRaw) return null;
|
|
161
|
+
const appPath = appPathRaw.trim().replace(/\/$/, "");
|
|
162
|
+
const exeName = execText("/usr/bin/defaults", ["read", path__default.default.join(appPath, "Contents", "Info"), "CFBundleExecutable"]);
|
|
163
|
+
if (!exeName) return null;
|
|
164
|
+
const exePath = path__default.default.join(appPath, "Contents", "MacOS", exeName.trim());
|
|
165
|
+
if (!fileExists(exePath)) return null;
|
|
166
|
+
return { kind: inferKindFromIdentifier(bundleId), path: exePath };
|
|
167
|
+
}
|
|
168
|
+
function findChromeMac() {
|
|
169
|
+
return findFirstExe([
|
|
170
|
+
{ kind: "chrome", path: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" },
|
|
171
|
+
{ kind: "chrome", path: path__default.default.join(os__default.default.homedir(), "Applications/Google Chrome.app/Contents/MacOS/Google Chrome") },
|
|
172
|
+
{ kind: "brave", path: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" },
|
|
173
|
+
{ kind: "brave", path: path__default.default.join(os__default.default.homedir(), "Applications/Brave Browser.app/Contents/MacOS/Brave Browser") },
|
|
174
|
+
{ kind: "edge", path: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge" },
|
|
175
|
+
{ kind: "edge", path: path__default.default.join(os__default.default.homedir(), "Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge") },
|
|
176
|
+
{ kind: "chromium", path: "/Applications/Chromium.app/Contents/MacOS/Chromium" },
|
|
177
|
+
{ kind: "chromium", path: path__default.default.join(os__default.default.homedir(), "Applications/Chromium.app/Contents/MacOS/Chromium") },
|
|
178
|
+
{ kind: "canary", path: "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary" },
|
|
179
|
+
{ kind: "canary", path: path__default.default.join(os__default.default.homedir(), "Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary") }
|
|
180
|
+
]);
|
|
181
|
+
}
|
|
182
|
+
function detectDefaultChromiumLinux() {
|
|
183
|
+
const desktopId = execText("xdg-settings", ["get", "default-web-browser"]) || execText("xdg-mime", ["query", "default", "x-scheme-handler/http"]);
|
|
184
|
+
if (!desktopId) return null;
|
|
185
|
+
const trimmed = desktopId.trim();
|
|
186
|
+
if (!CHROMIUM_DESKTOP_IDS.has(trimmed)) return null;
|
|
187
|
+
const searchDirs = [
|
|
188
|
+
path__default.default.join(os__default.default.homedir(), ".local", "share", "applications"),
|
|
189
|
+
"/usr/local/share/applications",
|
|
190
|
+
"/usr/share/applications",
|
|
191
|
+
"/var/lib/snapd/desktop/applications"
|
|
192
|
+
];
|
|
193
|
+
let desktopPath = null;
|
|
194
|
+
for (const dir of searchDirs) {
|
|
195
|
+
const candidate = path__default.default.join(dir, trimmed);
|
|
196
|
+
if (fileExists(candidate)) {
|
|
197
|
+
desktopPath = candidate;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (!desktopPath) return null;
|
|
202
|
+
let execLine = null;
|
|
203
|
+
try {
|
|
204
|
+
const lines = fs__default.default.readFileSync(desktopPath, "utf8").split(/\r?\n/);
|
|
205
|
+
for (const line of lines) if (line.startsWith("Exec=")) {
|
|
206
|
+
execLine = line.slice(5).trim();
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
}
|
|
211
|
+
if (!execLine) return null;
|
|
212
|
+
const tokens = execLine.split(/\s+/);
|
|
213
|
+
let command = null;
|
|
214
|
+
for (const token of tokens) {
|
|
215
|
+
if (!token || token === "env" || token.includes("=") && !token.startsWith("/")) continue;
|
|
216
|
+
command = token.replace(/^["']|["']$/g, "");
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
if (!command) return null;
|
|
220
|
+
const resolved = command.startsWith("/") ? command : execText("which", [command], 800)?.trim() ?? null;
|
|
221
|
+
if (!resolved) return null;
|
|
222
|
+
const exeName = path__default.default.posix.basename(resolved).toLowerCase();
|
|
223
|
+
if (!CHROMIUM_EXE_NAMES.has(exeName)) return null;
|
|
224
|
+
return { kind: inferKindFromExeName(exeName), path: resolved };
|
|
225
|
+
}
|
|
226
|
+
function findChromeLinux() {
|
|
227
|
+
return findFirstExe([
|
|
228
|
+
{ kind: "chrome", path: "/usr/bin/google-chrome" },
|
|
229
|
+
{ kind: "chrome", path: "/usr/bin/google-chrome-stable" },
|
|
230
|
+
{ kind: "chrome", path: "/usr/bin/chrome" },
|
|
231
|
+
{ kind: "brave", path: "/usr/bin/brave-browser" },
|
|
232
|
+
{ kind: "brave", path: "/usr/bin/brave-browser-stable" },
|
|
233
|
+
{ kind: "brave", path: "/usr/bin/brave" },
|
|
234
|
+
{ kind: "brave", path: "/snap/bin/brave" },
|
|
235
|
+
{ kind: "edge", path: "/usr/bin/microsoft-edge" },
|
|
236
|
+
{ kind: "edge", path: "/usr/bin/microsoft-edge-stable" },
|
|
237
|
+
{ kind: "chromium", path: "/usr/bin/chromium" },
|
|
238
|
+
{ kind: "chromium", path: "/usr/bin/chromium-browser" },
|
|
239
|
+
{ kind: "chromium", path: "/snap/bin/chromium" }
|
|
240
|
+
]);
|
|
241
|
+
}
|
|
242
|
+
function findChromeWindows() {
|
|
243
|
+
const localAppData = process.env.LOCALAPPDATA ?? "";
|
|
244
|
+
const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
|
|
245
|
+
const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
|
|
246
|
+
const j = path__default.default.win32.join;
|
|
247
|
+
const candidates = [];
|
|
248
|
+
if (localAppData) {
|
|
249
|
+
candidates.push({ kind: "chrome", path: j(localAppData, "Google", "Chrome", "Application", "chrome.exe") });
|
|
250
|
+
candidates.push({ kind: "brave", path: j(localAppData, "BraveSoftware", "Brave-Browser", "Application", "brave.exe") });
|
|
251
|
+
candidates.push({ kind: "edge", path: j(localAppData, "Microsoft", "Edge", "Application", "msedge.exe") });
|
|
252
|
+
candidates.push({ kind: "chromium", path: j(localAppData, "Chromium", "Application", "chrome.exe") });
|
|
253
|
+
candidates.push({ kind: "canary", path: j(localAppData, "Google", "Chrome SxS", "Application", "chrome.exe") });
|
|
254
|
+
}
|
|
255
|
+
candidates.push({ kind: "chrome", path: j(programFiles, "Google", "Chrome", "Application", "chrome.exe") });
|
|
256
|
+
candidates.push({ kind: "chrome", path: j(programFilesX86, "Google", "Chrome", "Application", "chrome.exe") });
|
|
257
|
+
candidates.push({ kind: "brave", path: j(programFiles, "BraveSoftware", "Brave-Browser", "Application", "brave.exe") });
|
|
258
|
+
candidates.push({ kind: "brave", path: j(programFilesX86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe") });
|
|
259
|
+
candidates.push({ kind: "edge", path: j(programFiles, "Microsoft", "Edge", "Application", "msedge.exe") });
|
|
260
|
+
candidates.push({ kind: "edge", path: j(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe") });
|
|
261
|
+
return findFirstExe(candidates);
|
|
262
|
+
}
|
|
263
|
+
function resolveBrowserExecutable(opts) {
|
|
264
|
+
if (opts?.executablePath) {
|
|
265
|
+
if (!fileExists(opts.executablePath)) throw new Error(`executablePath not found: ${opts.executablePath}`);
|
|
266
|
+
return { kind: "custom", path: opts.executablePath };
|
|
267
|
+
}
|
|
268
|
+
const platform = process.platform;
|
|
269
|
+
if (platform === "darwin") return detectDefaultChromiumMac() ?? findChromeMac();
|
|
270
|
+
if (platform === "linux") return detectDefaultChromiumLinux() ?? findChromeLinux();
|
|
271
|
+
if (platform === "win32") return findChromeWindows();
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
async function ensurePortAvailable(port) {
|
|
275
|
+
await new Promise((resolve, reject) => {
|
|
276
|
+
const tester = net__default.default.createServer().once("error", (err) => {
|
|
277
|
+
if (err.code === "EADDRINUSE") reject(new Error(`Port ${port} is already in use`));
|
|
278
|
+
else reject(err);
|
|
279
|
+
}).once("listening", () => {
|
|
280
|
+
tester.close(() => resolve());
|
|
281
|
+
}).listen(port);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function safeReadJson(filePath) {
|
|
285
|
+
try {
|
|
286
|
+
if (!fs__default.default.existsSync(filePath)) return null;
|
|
287
|
+
const parsed = JSON.parse(fs__default.default.readFileSync(filePath, "utf-8"));
|
|
288
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
289
|
+
return parsed;
|
|
290
|
+
} catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function safeWriteJson(filePath, data) {
|
|
295
|
+
fs__default.default.mkdirSync(path__default.default.dirname(filePath), { recursive: true });
|
|
296
|
+
fs__default.default.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
297
|
+
}
|
|
298
|
+
function setDeep(obj, keys, value) {
|
|
299
|
+
let node = obj;
|
|
300
|
+
for (const key of keys.slice(0, -1)) {
|
|
301
|
+
const next = node[key];
|
|
302
|
+
if (typeof next !== "object" || next === null || Array.isArray(next)) node[key] = {};
|
|
303
|
+
node = node[key];
|
|
304
|
+
}
|
|
305
|
+
node[keys[keys.length - 1]] = value;
|
|
306
|
+
}
|
|
307
|
+
function parseHexRgbToSignedArgbInt(hex) {
|
|
308
|
+
const cleaned = hex.trim().replace(/^#/, "");
|
|
309
|
+
if (!/^[0-9a-fA-F]{6}$/.test(cleaned)) return null;
|
|
310
|
+
const argbUnsigned = 255 << 24 | Number.parseInt(cleaned, 16);
|
|
311
|
+
return argbUnsigned > 2147483647 ? argbUnsigned - 4294967296 : argbUnsigned;
|
|
312
|
+
}
|
|
313
|
+
function decorateProfile(userDataDir, name, color) {
|
|
314
|
+
const colorInt = parseHexRgbToSignedArgbInt(color);
|
|
315
|
+
const localStatePath = path__default.default.join(userDataDir, "Local State");
|
|
316
|
+
const preferencesPath = path__default.default.join(userDataDir, "Default", "Preferences");
|
|
317
|
+
const localState = safeReadJson(localStatePath) ?? {};
|
|
318
|
+
setDeep(localState, ["profile", "info_cache", "Default", "name"], name);
|
|
319
|
+
setDeep(localState, ["profile", "info_cache", "Default", "shortcut_name"], name);
|
|
320
|
+
setDeep(localState, ["profile", "info_cache", "Default", "user_name"], name);
|
|
321
|
+
setDeep(localState, ["profile", "info_cache", "Default", "profile_color"], color);
|
|
322
|
+
if (colorInt != null) {
|
|
323
|
+
setDeep(localState, ["profile", "info_cache", "Default", "profile_color_seed"], colorInt);
|
|
324
|
+
setDeep(localState, ["profile", "info_cache", "Default", "profile_highlight_color"], colorInt);
|
|
325
|
+
}
|
|
326
|
+
safeWriteJson(localStatePath, localState);
|
|
327
|
+
const prefs = safeReadJson(preferencesPath) ?? {};
|
|
328
|
+
setDeep(prefs, ["profile", "name"], name);
|
|
329
|
+
setDeep(prefs, ["profile", "profile_color"], color);
|
|
330
|
+
if (colorInt != null) {
|
|
331
|
+
setDeep(prefs, ["autogenerated", "theme", "color"], colorInt);
|
|
332
|
+
setDeep(prefs, ["browser", "theme", "user_color2"], colorInt);
|
|
333
|
+
}
|
|
334
|
+
safeWriteJson(preferencesPath, prefs);
|
|
335
|
+
}
|
|
336
|
+
function ensureCleanExit(userDataDir) {
|
|
337
|
+
const preferencesPath = path__default.default.join(userDataDir, "Default", "Preferences");
|
|
338
|
+
const prefs = safeReadJson(preferencesPath) ?? {};
|
|
339
|
+
setDeep(prefs, ["exit_type"], "Normal");
|
|
340
|
+
setDeep(prefs, ["exited_cleanly"], true);
|
|
341
|
+
safeWriteJson(preferencesPath, prefs);
|
|
342
|
+
}
|
|
343
|
+
var DEFAULT_CDP_PORT = 9222;
|
|
344
|
+
var DEFAULT_PROFILE_NAME = "browserclaw";
|
|
345
|
+
var DEFAULT_PROFILE_COLOR = "#FF4500";
|
|
346
|
+
function resolveUserDataDir(profileName) {
|
|
347
|
+
const configDir = process.env.XDG_CONFIG_HOME ?? path__default.default.join(os__default.default.homedir(), ".config");
|
|
348
|
+
return path__default.default.join(configDir, "browserclaw", "profiles", profileName, "user-data");
|
|
349
|
+
}
|
|
350
|
+
async function isChromeReachable(cdpUrl, timeoutMs = 500) {
|
|
351
|
+
const ctrl = new AbortController();
|
|
352
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
353
|
+
try {
|
|
354
|
+
const res = await fetch(`${cdpUrl.replace(/\/+$/, "")}/json/version`, { signal: ctrl.signal });
|
|
355
|
+
return res.ok;
|
|
356
|
+
} catch {
|
|
357
|
+
return false;
|
|
358
|
+
} finally {
|
|
359
|
+
clearTimeout(t);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async function getChromeWebSocketUrl(cdpUrl, timeoutMs = 500) {
|
|
363
|
+
const ctrl = new AbortController();
|
|
364
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
365
|
+
try {
|
|
366
|
+
const res = await fetch(`${cdpUrl.replace(/\/+$/, "")}/json/version`, { signal: ctrl.signal });
|
|
367
|
+
if (!res.ok) return null;
|
|
368
|
+
const data = await res.json();
|
|
369
|
+
return String(data?.webSocketDebuggerUrl ?? "").trim() || null;
|
|
370
|
+
} catch {
|
|
371
|
+
return null;
|
|
372
|
+
} finally {
|
|
373
|
+
clearTimeout(t);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async function launchChrome(opts = {}) {
|
|
377
|
+
const cdpPort = opts.cdpPort ?? DEFAULT_CDP_PORT;
|
|
378
|
+
await ensurePortAvailable(cdpPort);
|
|
379
|
+
const exe = resolveBrowserExecutable({ executablePath: opts.executablePath });
|
|
380
|
+
if (!exe) throw new Error("No supported browser found (Chrome/Brave/Edge/Chromium). Install one or provide executablePath.");
|
|
381
|
+
const profileName = opts.profileName ?? DEFAULT_PROFILE_NAME;
|
|
382
|
+
const userDataDir = opts.userDataDir ?? resolveUserDataDir(profileName);
|
|
383
|
+
fs__default.default.mkdirSync(userDataDir, { recursive: true });
|
|
384
|
+
const spawnChrome = () => {
|
|
385
|
+
const args = [
|
|
386
|
+
`--remote-debugging-port=${cdpPort}`,
|
|
387
|
+
`--user-data-dir=${userDataDir}`,
|
|
388
|
+
"--no-first-run",
|
|
389
|
+
"--no-default-browser-check",
|
|
390
|
+
"--disable-sync",
|
|
391
|
+
"--disable-background-networking",
|
|
392
|
+
"--disable-component-update",
|
|
393
|
+
"--disable-features=Translate,MediaRouter",
|
|
394
|
+
"--disable-session-crashed-bubble",
|
|
395
|
+
"--hide-crash-restore-bubble",
|
|
396
|
+
"--password-store=basic"
|
|
397
|
+
];
|
|
398
|
+
if (opts.headless) {
|
|
399
|
+
args.push("--headless=new", "--disable-gpu");
|
|
400
|
+
}
|
|
401
|
+
if (opts.noSandbox) {
|
|
402
|
+
args.push("--no-sandbox", "--disable-setuid-sandbox");
|
|
403
|
+
}
|
|
404
|
+
if (process.platform === "linux") args.push("--disable-dev-shm-usage");
|
|
405
|
+
if (opts.chromeArgs?.length) args.push(...opts.chromeArgs);
|
|
406
|
+
args.push("about:blank");
|
|
407
|
+
return child_process.spawn(exe.path, args, {
|
|
408
|
+
stdio: "pipe",
|
|
409
|
+
env: { ...process.env, HOME: os__default.default.homedir() }
|
|
410
|
+
});
|
|
411
|
+
};
|
|
412
|
+
const startedAt = Date.now();
|
|
413
|
+
const localStatePath = path__default.default.join(userDataDir, "Local State");
|
|
414
|
+
const preferencesPath = path__default.default.join(userDataDir, "Default", "Preferences");
|
|
415
|
+
if (!fileExists(localStatePath) || !fileExists(preferencesPath)) {
|
|
416
|
+
const bootstrap = spawnChrome();
|
|
417
|
+
const deadline = Date.now() + 1e4;
|
|
418
|
+
while (Date.now() < deadline) {
|
|
419
|
+
if (fileExists(localStatePath) && fileExists(preferencesPath)) break;
|
|
420
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
bootstrap.kill("SIGTERM");
|
|
424
|
+
} catch {
|
|
425
|
+
}
|
|
426
|
+
const exitDeadline = Date.now() + 5e3;
|
|
427
|
+
while (Date.now() < exitDeadline) {
|
|
428
|
+
if (bootstrap.exitCode != null) break;
|
|
429
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
try {
|
|
433
|
+
decorateProfile(userDataDir, profileName, opts.profileColor ?? DEFAULT_PROFILE_COLOR);
|
|
434
|
+
} catch {
|
|
435
|
+
}
|
|
436
|
+
try {
|
|
437
|
+
ensureCleanExit(userDataDir);
|
|
438
|
+
} catch {
|
|
439
|
+
}
|
|
440
|
+
const proc = spawnChrome();
|
|
441
|
+
const cdpUrl = `http://127.0.0.1:${cdpPort}`;
|
|
442
|
+
const readyDeadline = Date.now() + 15e3;
|
|
443
|
+
while (Date.now() < readyDeadline) {
|
|
444
|
+
if (await isChromeReachable(cdpUrl, 500)) break;
|
|
445
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
446
|
+
}
|
|
447
|
+
if (!await isChromeReachable(cdpUrl, 500)) {
|
|
448
|
+
try {
|
|
449
|
+
proc.kill("SIGKILL");
|
|
450
|
+
} catch {
|
|
451
|
+
}
|
|
452
|
+
throw new Error(`Failed to start Chrome CDP on port ${cdpPort}. Chrome may not have started correctly.`);
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
pid: proc.pid ?? -1,
|
|
456
|
+
exe,
|
|
457
|
+
userDataDir,
|
|
458
|
+
cdpPort,
|
|
459
|
+
startedAt,
|
|
460
|
+
proc
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
async function stopChrome(running, timeoutMs = 2500) {
|
|
464
|
+
const proc = running.proc;
|
|
465
|
+
if (proc.killed) return;
|
|
466
|
+
try {
|
|
467
|
+
proc.kill("SIGTERM");
|
|
468
|
+
} catch {
|
|
469
|
+
}
|
|
470
|
+
const start = Date.now();
|
|
471
|
+
while (Date.now() - start < timeoutMs) {
|
|
472
|
+
if (proc.exitCode != null || proc.killed) return;
|
|
473
|
+
if (!await isChromeReachable(`http://127.0.0.1:${running.cdpPort}`, 200)) return;
|
|
474
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
475
|
+
}
|
|
476
|
+
try {
|
|
477
|
+
proc.kill("SIGKILL");
|
|
478
|
+
} catch {
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
var cached = null;
|
|
482
|
+
var connecting = null;
|
|
483
|
+
var pageStates = /* @__PURE__ */ new WeakMap();
|
|
484
|
+
var contextStates = /* @__PURE__ */ new WeakMap();
|
|
485
|
+
var observedContexts = /* @__PURE__ */ new WeakSet();
|
|
486
|
+
var observedPages = /* @__PURE__ */ new WeakSet();
|
|
487
|
+
var roleRefsByTarget = /* @__PURE__ */ new Map();
|
|
488
|
+
var MAX_ROLE_REFS_CACHE = 50;
|
|
489
|
+
var MAX_CONSOLE_MESSAGES = 500;
|
|
490
|
+
var MAX_PAGE_ERRORS = 200;
|
|
491
|
+
var MAX_NETWORK_REQUESTS = 500;
|
|
492
|
+
function normalizeCdpUrl(raw) {
|
|
493
|
+
return raw.replace(/\/$/, "");
|
|
494
|
+
}
|
|
495
|
+
function roleRefsKey(cdpUrl, targetId) {
|
|
496
|
+
return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
|
|
497
|
+
}
|
|
498
|
+
function ensurePageState(page) {
|
|
499
|
+
const existing = pageStates.get(page);
|
|
500
|
+
if (existing) return existing;
|
|
501
|
+
const state = {
|
|
502
|
+
console: [],
|
|
503
|
+
errors: [],
|
|
504
|
+
requests: [],
|
|
505
|
+
requestIds: /* @__PURE__ */ new WeakMap(),
|
|
506
|
+
nextRequestId: 0,
|
|
507
|
+
armIdUpload: 0,
|
|
508
|
+
armIdDialog: 0,
|
|
509
|
+
armIdDownload: 0
|
|
510
|
+
};
|
|
511
|
+
pageStates.set(page, state);
|
|
512
|
+
if (!observedPages.has(page)) {
|
|
513
|
+
observedPages.add(page);
|
|
514
|
+
page.on("console", (msg) => {
|
|
515
|
+
state.console.push({
|
|
516
|
+
type: msg.type(),
|
|
517
|
+
text: msg.text(),
|
|
518
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
519
|
+
location: msg.location()
|
|
520
|
+
});
|
|
521
|
+
if (state.console.length > MAX_CONSOLE_MESSAGES) state.console.shift();
|
|
522
|
+
});
|
|
523
|
+
page.on("pageerror", (err) => {
|
|
524
|
+
state.errors.push({
|
|
525
|
+
message: err?.message ? String(err.message) : String(err),
|
|
526
|
+
name: err?.name ? String(err.name) : void 0,
|
|
527
|
+
stack: err?.stack ? String(err.stack) : void 0,
|
|
528
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
529
|
+
});
|
|
530
|
+
if (state.errors.length > MAX_PAGE_ERRORS) state.errors.shift();
|
|
531
|
+
});
|
|
532
|
+
page.on("request", (req) => {
|
|
533
|
+
state.nextRequestId += 1;
|
|
534
|
+
const id = `r${state.nextRequestId}`;
|
|
535
|
+
state.requestIds.set(req, id);
|
|
536
|
+
state.requests.push({
|
|
537
|
+
id,
|
|
538
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
539
|
+
method: req.method(),
|
|
540
|
+
url: req.url(),
|
|
541
|
+
resourceType: req.resourceType()
|
|
542
|
+
});
|
|
543
|
+
if (state.requests.length > MAX_NETWORK_REQUESTS) state.requests.shift();
|
|
544
|
+
});
|
|
545
|
+
page.on("response", (resp) => {
|
|
546
|
+
const req = resp.request();
|
|
547
|
+
const id = state.requestIds.get(req);
|
|
548
|
+
if (!id) return;
|
|
549
|
+
for (let i = state.requests.length - 1; i >= 0; i--) {
|
|
550
|
+
const rec = state.requests[i];
|
|
551
|
+
if (rec && rec.id === id) {
|
|
552
|
+
rec.status = resp.status();
|
|
553
|
+
rec.ok = resp.ok();
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
page.on("requestfailed", (req) => {
|
|
559
|
+
const id = state.requestIds.get(req);
|
|
560
|
+
if (!id) return;
|
|
561
|
+
for (let i = state.requests.length - 1; i >= 0; i--) {
|
|
562
|
+
const rec = state.requests[i];
|
|
563
|
+
if (rec && rec.id === id) {
|
|
564
|
+
rec.failureText = req.failure()?.errorText;
|
|
565
|
+
rec.ok = false;
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
page.on("close", () => {
|
|
571
|
+
pageStates.delete(page);
|
|
572
|
+
observedPages.delete(page);
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
return state;
|
|
576
|
+
}
|
|
577
|
+
function ensureContextState(context) {
|
|
578
|
+
const existing = contextStates.get(context);
|
|
579
|
+
if (existing) return existing;
|
|
580
|
+
const state = { traceActive: false };
|
|
581
|
+
contextStates.set(context, state);
|
|
582
|
+
return state;
|
|
583
|
+
}
|
|
584
|
+
function observeContext(context) {
|
|
585
|
+
if (observedContexts.has(context)) return;
|
|
586
|
+
observedContexts.add(context);
|
|
587
|
+
ensureContextState(context);
|
|
588
|
+
for (const page of context.pages()) ensurePageState(page);
|
|
589
|
+
context.on("page", (page) => ensurePageState(page));
|
|
590
|
+
}
|
|
591
|
+
function observeBrowser(browser) {
|
|
592
|
+
for (const context of browser.contexts()) observeContext(context);
|
|
593
|
+
}
|
|
594
|
+
function storeRoleRefsForTarget(opts) {
|
|
595
|
+
const state = ensurePageState(opts.page);
|
|
596
|
+
state.roleRefs = opts.refs;
|
|
597
|
+
state.roleRefsFrameSelector = opts.frameSelector;
|
|
598
|
+
state.roleRefsMode = opts.mode;
|
|
599
|
+
const targetId = opts.targetId?.trim();
|
|
600
|
+
if (!targetId) return;
|
|
601
|
+
roleRefsByTarget.set(roleRefsKey(opts.cdpUrl, targetId), {
|
|
602
|
+
refs: opts.refs,
|
|
603
|
+
...opts.frameSelector ? { frameSelector: opts.frameSelector } : {},
|
|
604
|
+
...opts.mode ? { mode: opts.mode } : {}
|
|
605
|
+
});
|
|
606
|
+
while (roleRefsByTarget.size > MAX_ROLE_REFS_CACHE) {
|
|
607
|
+
const first = roleRefsByTarget.keys().next();
|
|
608
|
+
if (first.done) break;
|
|
609
|
+
roleRefsByTarget.delete(first.value);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function restoreRoleRefsForTarget(opts) {
|
|
613
|
+
const targetId = opts.targetId?.trim() || "";
|
|
614
|
+
if (!targetId) return;
|
|
615
|
+
const cached2 = roleRefsByTarget.get(roleRefsKey(opts.cdpUrl, targetId));
|
|
616
|
+
if (!cached2) return;
|
|
617
|
+
const state = ensurePageState(opts.page);
|
|
618
|
+
if (state.roleRefs) return;
|
|
619
|
+
state.roleRefs = cached2.refs;
|
|
620
|
+
state.roleRefsFrameSelector = cached2.frameSelector;
|
|
621
|
+
state.roleRefsMode = cached2.mode;
|
|
622
|
+
}
|
|
623
|
+
async function connectBrowser(cdpUrl) {
|
|
624
|
+
const normalized = normalizeCdpUrl(cdpUrl);
|
|
625
|
+
if (cached?.cdpUrl === normalized) return cached;
|
|
626
|
+
if (connecting) return await connecting;
|
|
627
|
+
const connectWithRetry = async () => {
|
|
628
|
+
let lastErr;
|
|
629
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
630
|
+
try {
|
|
631
|
+
const timeout = 5e3 + attempt * 2e3;
|
|
632
|
+
const endpoint = await getChromeWebSocketUrl(normalized, timeout).catch(() => null) ?? normalized;
|
|
633
|
+
const browser = await playwrightCore.chromium.connectOverCDP(endpoint, { timeout });
|
|
634
|
+
const connected = { browser, cdpUrl: normalized };
|
|
635
|
+
cached = connected;
|
|
636
|
+
observeBrowser(browser);
|
|
637
|
+
browser.on("disconnected", () => {
|
|
638
|
+
if (cached?.browser === browser) cached = null;
|
|
639
|
+
});
|
|
640
|
+
return connected;
|
|
641
|
+
} catch (err) {
|
|
642
|
+
lastErr = err;
|
|
643
|
+
await new Promise((r) => setTimeout(r, 250 + attempt * 250));
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
throw lastErr instanceof Error ? lastErr : new Error("CDP connect failed");
|
|
647
|
+
};
|
|
648
|
+
connecting = connectWithRetry().finally(() => {
|
|
649
|
+
connecting = null;
|
|
650
|
+
});
|
|
651
|
+
return await connecting;
|
|
652
|
+
}
|
|
653
|
+
async function disconnectBrowser() {
|
|
654
|
+
const cur = cached;
|
|
655
|
+
cached = null;
|
|
656
|
+
if (cur) await cur.browser.close().catch(() => {
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
async function getAllPages(browser) {
|
|
660
|
+
return browser.contexts().flatMap((c) => c.pages());
|
|
661
|
+
}
|
|
662
|
+
async function pageTargetId(page) {
|
|
663
|
+
const session = await page.context().newCDPSession(page);
|
|
664
|
+
try {
|
|
665
|
+
const info = await session.send("Target.getTargetInfo");
|
|
666
|
+
return String(info?.targetInfo?.targetId ?? "").trim() || null;
|
|
667
|
+
} finally {
|
|
668
|
+
await session.detach().catch(() => {
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
673
|
+
const pages = await getAllPages(browser);
|
|
674
|
+
for (const page of pages) {
|
|
675
|
+
const tid = await pageTargetId(page).catch(() => null);
|
|
676
|
+
if (tid && tid === targetId) return page;
|
|
677
|
+
}
|
|
678
|
+
if (cdpUrl) {
|
|
679
|
+
try {
|
|
680
|
+
const listUrl = `${cdpUrl.replace(/\/+$/, "").replace(/^ws:/, "http:").replace(/\/cdp$/, "")}/json/list`;
|
|
681
|
+
const response = await fetch(listUrl);
|
|
682
|
+
if (response.ok) {
|
|
683
|
+
const targets = await response.json();
|
|
684
|
+
const target = targets.find((t) => t.id === targetId);
|
|
685
|
+
if (target) {
|
|
686
|
+
const urlMatch = pages.filter((p) => p.url() === target.url);
|
|
687
|
+
if (urlMatch.length === 1) return urlMatch[0];
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
} catch {
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
async function getPageForTargetId(opts) {
|
|
696
|
+
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
697
|
+
const pages = await getAllPages(browser);
|
|
698
|
+
if (!pages.length) throw new Error("No pages available in the connected browser.");
|
|
699
|
+
const first = pages[0];
|
|
700
|
+
if (!opts.targetId) return first;
|
|
701
|
+
const found = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
|
|
702
|
+
if (!found) {
|
|
703
|
+
if (pages.length === 1) return first;
|
|
704
|
+
throw new Error(`Tab not found (targetId: ${opts.targetId}). Use browser.tabs() to list open tabs.`);
|
|
705
|
+
}
|
|
706
|
+
return found;
|
|
707
|
+
}
|
|
708
|
+
function refLocator(page, ref) {
|
|
709
|
+
const normalized = ref.startsWith("@") ? ref.slice(1) : ref.startsWith("ref=") ? ref.slice(4) : ref;
|
|
710
|
+
if (/^e\d+$/.test(normalized)) {
|
|
711
|
+
const state = pageStates.get(page);
|
|
712
|
+
if (state?.roleRefsMode === "aria") {
|
|
713
|
+
return (state.roleRefsFrameSelector ? page.frameLocator(state.roleRefsFrameSelector) : page).locator(`aria-ref=${normalized}`);
|
|
714
|
+
}
|
|
715
|
+
const info = state?.roleRefs?.[normalized];
|
|
716
|
+
if (!info) throw new Error(`Unknown ref "${normalized}". Run a new snapshot and use a ref from that snapshot.`);
|
|
717
|
+
const locAny = state?.roleRefsFrameSelector ? page.frameLocator(state.roleRefsFrameSelector) : page;
|
|
718
|
+
const locator = info.name ? locAny.getByRole(info.role, { name: info.name, exact: true }) : locAny.getByRole(info.role);
|
|
719
|
+
return info.nth !== void 0 ? locator.nth(info.nth) : locator;
|
|
720
|
+
}
|
|
721
|
+
return page.locator(`aria-ref=${normalized}`);
|
|
722
|
+
}
|
|
723
|
+
function toAIFriendlyError(error, selector) {
|
|
724
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
725
|
+
if (message.includes("strict mode violation")) {
|
|
726
|
+
const countMatch = message.match(/resolved to (\d+) elements/);
|
|
727
|
+
const count = countMatch ? countMatch[1] : "multiple";
|
|
728
|
+
return new Error(`Selector "${selector}" matched ${count} elements. Run a new snapshot to get updated refs, or use a different ref.`);
|
|
729
|
+
}
|
|
730
|
+
if ((message.includes("Timeout") || message.includes("waiting for")) && (message.includes("to be visible") || message.includes("not visible"))) {
|
|
731
|
+
return new Error(`Element "${selector}" not found or not visible. Run a new snapshot to see current page elements.`);
|
|
732
|
+
}
|
|
733
|
+
if (message.includes("intercepts pointer events") || message.includes("not visible") || message.includes("not receive pointer events")) {
|
|
734
|
+
return new Error(`Element "${selector}" is not interactable (hidden or covered). Try scrolling it into view, closing overlays, or re-snapshotting.`);
|
|
735
|
+
}
|
|
736
|
+
return error instanceof Error ? error : new Error(message);
|
|
737
|
+
}
|
|
738
|
+
function normalizeTimeoutMs(timeoutMs, fallback) {
|
|
739
|
+
return Math.max(500, Math.min(12e4, timeoutMs ?? fallback));
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/snapshot/ref-map.ts
|
|
743
|
+
var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
744
|
+
"button",
|
|
745
|
+
"link",
|
|
746
|
+
"textbox",
|
|
747
|
+
"checkbox",
|
|
748
|
+
"radio",
|
|
749
|
+
"combobox",
|
|
750
|
+
"listbox",
|
|
751
|
+
"menuitem",
|
|
752
|
+
"menuitemcheckbox",
|
|
753
|
+
"menuitemradio",
|
|
754
|
+
"option",
|
|
755
|
+
"searchbox",
|
|
756
|
+
"slider",
|
|
757
|
+
"spinbutton",
|
|
758
|
+
"switch",
|
|
759
|
+
"tab",
|
|
760
|
+
"treeitem"
|
|
761
|
+
]);
|
|
762
|
+
var CONTENT_ROLES = /* @__PURE__ */ new Set([
|
|
763
|
+
"heading",
|
|
764
|
+
"cell",
|
|
765
|
+
"gridcell",
|
|
766
|
+
"columnheader",
|
|
767
|
+
"rowheader",
|
|
768
|
+
"listitem",
|
|
769
|
+
"article",
|
|
770
|
+
"region",
|
|
771
|
+
"main",
|
|
772
|
+
"navigation"
|
|
773
|
+
]);
|
|
774
|
+
var STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
|
|
775
|
+
"generic",
|
|
776
|
+
"group",
|
|
777
|
+
"list",
|
|
778
|
+
"table",
|
|
779
|
+
"row",
|
|
780
|
+
"rowgroup",
|
|
781
|
+
"grid",
|
|
782
|
+
"treegrid",
|
|
783
|
+
"menu",
|
|
784
|
+
"menubar",
|
|
785
|
+
"toolbar",
|
|
786
|
+
"tablist",
|
|
787
|
+
"tree",
|
|
788
|
+
"directory",
|
|
789
|
+
"document",
|
|
790
|
+
"application",
|
|
791
|
+
"presentation",
|
|
792
|
+
"none"
|
|
793
|
+
]);
|
|
794
|
+
function getIndentLevel(line) {
|
|
795
|
+
const match = line.match(/^(\s*)/);
|
|
796
|
+
return match ? Math.floor(match[1].length / 2) : 0;
|
|
797
|
+
}
|
|
798
|
+
function createRoleNameTracker() {
|
|
799
|
+
const counts = /* @__PURE__ */ new Map();
|
|
800
|
+
const refsByKey = /* @__PURE__ */ new Map();
|
|
801
|
+
return {
|
|
802
|
+
counts,
|
|
803
|
+
refsByKey,
|
|
804
|
+
getKey(role, name) {
|
|
805
|
+
return `${role}:${name ?? ""}`;
|
|
806
|
+
},
|
|
807
|
+
getNextIndex(role, name) {
|
|
808
|
+
const key = this.getKey(role, name);
|
|
809
|
+
const current = counts.get(key) ?? 0;
|
|
810
|
+
counts.set(key, current + 1);
|
|
811
|
+
return current;
|
|
812
|
+
},
|
|
813
|
+
trackRef(role, name, ref) {
|
|
814
|
+
const key = this.getKey(role, name);
|
|
815
|
+
const list = refsByKey.get(key) ?? [];
|
|
816
|
+
list.push(ref);
|
|
817
|
+
refsByKey.set(key, list);
|
|
818
|
+
},
|
|
819
|
+
getDuplicateKeys() {
|
|
820
|
+
const out = /* @__PURE__ */ new Set();
|
|
821
|
+
for (const [key, refs] of refsByKey) if (refs.length > 1) out.add(key);
|
|
822
|
+
return out;
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
function removeNthFromNonDuplicates(refs, tracker) {
|
|
827
|
+
const duplicates = tracker.getDuplicateKeys();
|
|
828
|
+
for (const [ref, data] of Object.entries(refs)) {
|
|
829
|
+
const key = tracker.getKey(data.role, data.name);
|
|
830
|
+
if (!duplicates.has(key)) delete refs[ref]?.nth;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
function compactTree(tree) {
|
|
834
|
+
const lines = tree.split("\n");
|
|
835
|
+
const result = [];
|
|
836
|
+
for (let i = 0; i < lines.length; i++) {
|
|
837
|
+
const line = lines[i];
|
|
838
|
+
if (line.includes("[ref=")) {
|
|
839
|
+
result.push(line);
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
if (line.includes(":") && !line.trimEnd().endsWith(":")) {
|
|
843
|
+
result.push(line);
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
const currentIndent = getIndentLevel(line);
|
|
847
|
+
let hasRelevantChildren = false;
|
|
848
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
849
|
+
if (getIndentLevel(lines[j]) <= currentIndent) break;
|
|
850
|
+
if (lines[j]?.includes("[ref=")) {
|
|
851
|
+
hasRelevantChildren = true;
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
if (hasRelevantChildren) result.push(line);
|
|
856
|
+
}
|
|
857
|
+
return result.join("\n");
|
|
858
|
+
}
|
|
859
|
+
function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
|
|
860
|
+
const lines = ariaSnapshot.split("\n");
|
|
861
|
+
const refs = {};
|
|
862
|
+
const tracker = createRoleNameTracker();
|
|
863
|
+
let counter = 0;
|
|
864
|
+
const nextRef = () => {
|
|
865
|
+
counter++;
|
|
866
|
+
return `e${counter}`;
|
|
867
|
+
};
|
|
868
|
+
if (options.interactive) {
|
|
869
|
+
const result2 = [];
|
|
870
|
+
for (const line of lines) {
|
|
871
|
+
const depth = getIndentLevel(line);
|
|
872
|
+
if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
|
|
873
|
+
const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
|
|
874
|
+
if (!match) continue;
|
|
875
|
+
const [, , roleRaw, name, suffix] = match;
|
|
876
|
+
if (roleRaw.startsWith("/")) continue;
|
|
877
|
+
const role = roleRaw.toLowerCase();
|
|
878
|
+
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
879
|
+
const ref = nextRef();
|
|
880
|
+
const nth = tracker.getNextIndex(role, name);
|
|
881
|
+
tracker.trackRef(role, name, ref);
|
|
882
|
+
refs[ref] = { role, name, nth };
|
|
883
|
+
let enhanced = `- ${roleRaw}`;
|
|
884
|
+
if (name) enhanced += ` "${name}"`;
|
|
885
|
+
enhanced += ` [ref=${ref}]`;
|
|
886
|
+
if (nth > 0) enhanced += ` [nth=${nth}]`;
|
|
887
|
+
if (suffix.includes("[")) enhanced += suffix;
|
|
888
|
+
result2.push(enhanced);
|
|
889
|
+
}
|
|
890
|
+
removeNthFromNonDuplicates(refs, tracker);
|
|
891
|
+
return { snapshot: result2.join("\n") || "(no interactive elements)", refs };
|
|
892
|
+
}
|
|
893
|
+
const result = [];
|
|
894
|
+
for (const line of lines) {
|
|
895
|
+
const depth = getIndentLevel(line);
|
|
896
|
+
if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
|
|
897
|
+
const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
|
|
898
|
+
if (!match) {
|
|
899
|
+
if (!options.interactive) result.push(line);
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
const [, prefix, roleRaw, name, suffix] = match;
|
|
903
|
+
if (roleRaw.startsWith("/")) {
|
|
904
|
+
result.push(line);
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
const role = roleRaw.toLowerCase();
|
|
908
|
+
const isInteractive = INTERACTIVE_ROLES.has(role);
|
|
909
|
+
const isContent = CONTENT_ROLES.has(role);
|
|
910
|
+
const isStructural = STRUCTURAL_ROLES.has(role);
|
|
911
|
+
if (options.compact && isStructural && !name) continue;
|
|
912
|
+
if (!(isInteractive || isContent && name)) {
|
|
913
|
+
result.push(line);
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
const ref = nextRef();
|
|
917
|
+
const nth = tracker.getNextIndex(role, name);
|
|
918
|
+
tracker.trackRef(role, name, ref);
|
|
919
|
+
refs[ref] = { role, name, nth };
|
|
920
|
+
let enhanced = `${prefix}${roleRaw}`;
|
|
921
|
+
if (name) enhanced += ` "${name}"`;
|
|
922
|
+
enhanced += ` [ref=${ref}]`;
|
|
923
|
+
if (nth > 0) enhanced += ` [nth=${nth}]`;
|
|
924
|
+
if (suffix) enhanced += suffix;
|
|
925
|
+
result.push(enhanced);
|
|
926
|
+
}
|
|
927
|
+
removeNthFromNonDuplicates(refs, tracker);
|
|
928
|
+
const tree = result.join("\n") || "(empty)";
|
|
929
|
+
return { snapshot: options.compact ? compactTree(tree) : tree, refs };
|
|
930
|
+
}
|
|
931
|
+
function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
|
|
932
|
+
const lines = String(aiSnapshot ?? "").split("\n");
|
|
933
|
+
const refs = {};
|
|
934
|
+
function parseAiSnapshotRef(suffix) {
|
|
935
|
+
const match = suffix.match(/\[ref=(e\d+)\]/i);
|
|
936
|
+
return match ? match[1] : null;
|
|
937
|
+
}
|
|
938
|
+
if (options.interactive) {
|
|
939
|
+
const out2 = [];
|
|
940
|
+
for (const line of lines) {
|
|
941
|
+
const depth = getIndentLevel(line);
|
|
942
|
+
if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
|
|
943
|
+
const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
|
|
944
|
+
if (!match) continue;
|
|
945
|
+
const [, , roleRaw, name, suffix] = match;
|
|
946
|
+
if (roleRaw.startsWith("/")) continue;
|
|
947
|
+
const role = roleRaw.toLowerCase();
|
|
948
|
+
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
949
|
+
const ref = parseAiSnapshotRef(suffix);
|
|
950
|
+
if (!ref) continue;
|
|
951
|
+
refs[ref] = { role, ...name ? { name } : {} };
|
|
952
|
+
out2.push(`- ${roleRaw}${name ? ` "${name}"` : ""}${suffix}`);
|
|
953
|
+
}
|
|
954
|
+
return { snapshot: out2.join("\n") || "(no interactive elements)", refs };
|
|
955
|
+
}
|
|
956
|
+
const out = [];
|
|
957
|
+
for (const line of lines) {
|
|
958
|
+
const depth = getIndentLevel(line);
|
|
959
|
+
if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
|
|
960
|
+
const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
|
|
961
|
+
if (!match) {
|
|
962
|
+
out.push(line);
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
const [, , roleRaw, name, suffix] = match;
|
|
966
|
+
if (roleRaw.startsWith("/")) {
|
|
967
|
+
out.push(line);
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
const role = roleRaw.toLowerCase();
|
|
971
|
+
const isStructural = STRUCTURAL_ROLES.has(role);
|
|
972
|
+
if (options.compact && isStructural && !name) continue;
|
|
973
|
+
const ref = parseAiSnapshotRef(suffix);
|
|
974
|
+
if (ref) refs[ref] = { role, ...name ? { name } : {} };
|
|
975
|
+
out.push(line);
|
|
976
|
+
}
|
|
977
|
+
const tree = out.join("\n") || "(empty)";
|
|
978
|
+
return { snapshot: options.compact ? compactTree(tree) : tree, refs };
|
|
979
|
+
}
|
|
980
|
+
function getRoleSnapshotStats(snapshot, refs) {
|
|
981
|
+
const interactive = Object.values(refs).filter((r) => INTERACTIVE_ROLES.has(r.role)).length;
|
|
982
|
+
return {
|
|
983
|
+
lines: snapshot.split("\n").length,
|
|
984
|
+
chars: snapshot.length,
|
|
985
|
+
refs: Object.keys(refs).length,
|
|
986
|
+
interactive
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// src/snapshot/ai-snapshot.ts
|
|
991
|
+
async function snapshotAi(opts) {
|
|
992
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
993
|
+
ensurePageState(page);
|
|
994
|
+
const maybe = page;
|
|
995
|
+
if (!maybe._snapshotForAI) {
|
|
996
|
+
throw new Error("Playwright _snapshotForAI is not available. Upgrade playwright-core to >= 1.50.");
|
|
997
|
+
}
|
|
998
|
+
const result = await maybe._snapshotForAI({
|
|
999
|
+
timeout: Math.max(500, Math.min(6e4, Math.floor(opts.timeoutMs ?? 5e3))),
|
|
1000
|
+
track: "response"
|
|
1001
|
+
});
|
|
1002
|
+
let snapshot = String(result?.full ?? "");
|
|
1003
|
+
const maxChars = opts.maxChars;
|
|
1004
|
+
const limit = typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : void 0;
|
|
1005
|
+
if (limit && snapshot.length > limit) {
|
|
1006
|
+
snapshot = `${snapshot.slice(0, limit)}
|
|
1007
|
+
|
|
1008
|
+
[...TRUNCATED - page too large]`;
|
|
1009
|
+
}
|
|
1010
|
+
const built = buildRoleSnapshotFromAiSnapshot(snapshot, opts.options);
|
|
1011
|
+
storeRoleRefsForTarget({
|
|
1012
|
+
page,
|
|
1013
|
+
cdpUrl: opts.cdpUrl,
|
|
1014
|
+
targetId: opts.targetId,
|
|
1015
|
+
refs: built.refs,
|
|
1016
|
+
mode: "aria"
|
|
1017
|
+
});
|
|
1018
|
+
return {
|
|
1019
|
+
snapshot: built.snapshot,
|
|
1020
|
+
refs: built.refs,
|
|
1021
|
+
stats: getRoleSnapshotStats(built.snapshot, built.refs)
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// src/snapshot/aria-snapshot.ts
|
|
1026
|
+
async function snapshotRole(opts) {
|
|
1027
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1028
|
+
ensurePageState(page);
|
|
1029
|
+
const frameSelector = opts.frameSelector?.trim() || "";
|
|
1030
|
+
const selector = opts.selector?.trim() || "";
|
|
1031
|
+
const locator = frameSelector ? selector ? page.frameLocator(frameSelector).locator(selector) : page.frameLocator(frameSelector).locator(":root") : selector ? page.locator(selector) : page.locator(":root");
|
|
1032
|
+
const ariaSnapshot = await locator.ariaSnapshot();
|
|
1033
|
+
const built = buildRoleSnapshotFromAriaSnapshot(String(ariaSnapshot ?? ""), opts.options);
|
|
1034
|
+
storeRoleRefsForTarget({
|
|
1035
|
+
page,
|
|
1036
|
+
cdpUrl: opts.cdpUrl,
|
|
1037
|
+
targetId: opts.targetId,
|
|
1038
|
+
refs: built.refs,
|
|
1039
|
+
frameSelector: frameSelector || void 0,
|
|
1040
|
+
mode: "role"
|
|
1041
|
+
});
|
|
1042
|
+
return {
|
|
1043
|
+
snapshot: built.snapshot,
|
|
1044
|
+
refs: built.refs,
|
|
1045
|
+
stats: getRoleSnapshotStats(built.snapshot, built.refs)
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
async function snapshotAria(opts) {
|
|
1049
|
+
const limit = Math.max(1, Math.min(2e3, Math.floor(opts.limit ?? 500)));
|
|
1050
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1051
|
+
ensurePageState(page);
|
|
1052
|
+
const session = await page.context().newCDPSession(page);
|
|
1053
|
+
try {
|
|
1054
|
+
await session.send("Accessibility.enable").catch(() => {
|
|
1055
|
+
});
|
|
1056
|
+
const res = await session.send("Accessibility.getFullAXTree");
|
|
1057
|
+
return { nodes: formatAriaNodes(Array.isArray(res?.nodes) ? res.nodes : [], limit) };
|
|
1058
|
+
} finally {
|
|
1059
|
+
await session.detach().catch(() => {
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
function axValue(v) {
|
|
1064
|
+
if (!v || typeof v !== "object") return "";
|
|
1065
|
+
const value = v.value;
|
|
1066
|
+
if (typeof value === "string") return value;
|
|
1067
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1068
|
+
return "";
|
|
1069
|
+
}
|
|
1070
|
+
function formatAriaNodes(nodes, limit) {
|
|
1071
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1072
|
+
for (const n of nodes) if (n.nodeId) byId.set(n.nodeId, n);
|
|
1073
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
1074
|
+
for (const n of nodes) for (const c of n.childIds ?? []) referenced.add(c);
|
|
1075
|
+
const root = nodes.find((n) => n.nodeId && !referenced.has(n.nodeId)) ?? nodes[0];
|
|
1076
|
+
if (!root?.nodeId) return [];
|
|
1077
|
+
const out = [];
|
|
1078
|
+
const stack = [{ id: root.nodeId, depth: 0 }];
|
|
1079
|
+
while (stack.length && out.length < limit) {
|
|
1080
|
+
const popped = stack.pop();
|
|
1081
|
+
if (!popped) break;
|
|
1082
|
+
const { id, depth } = popped;
|
|
1083
|
+
const n = byId.get(id);
|
|
1084
|
+
if (!n) continue;
|
|
1085
|
+
const role = axValue(n.role);
|
|
1086
|
+
const name = axValue(n.name);
|
|
1087
|
+
const value = axValue(n.value);
|
|
1088
|
+
const description = axValue(n.description);
|
|
1089
|
+
const ref = `ax${out.length + 1}`;
|
|
1090
|
+
out.push({
|
|
1091
|
+
ref,
|
|
1092
|
+
role: role || "unknown",
|
|
1093
|
+
name: name || "",
|
|
1094
|
+
...value ? { value } : {},
|
|
1095
|
+
...description ? { description } : {},
|
|
1096
|
+
...typeof n.backendDOMNodeId === "number" ? { backendDOMNodeId: n.backendDOMNodeId } : {},
|
|
1097
|
+
depth
|
|
1098
|
+
});
|
|
1099
|
+
const children = (n.childIds ?? []).filter((c) => byId.has(c));
|
|
1100
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
1101
|
+
if (children[i]) stack.push({ id: children[i], depth: depth + 1 });
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return out;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// src/actions/interaction.ts
|
|
1108
|
+
async function clickViaPlaywright(opts) {
|
|
1109
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1110
|
+
ensurePageState(page);
|
|
1111
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1112
|
+
const locator = refLocator(page, opts.ref);
|
|
1113
|
+
const timeout = Math.max(500, Math.min(6e4, Math.floor(opts.timeoutMs ?? 8e3)));
|
|
1114
|
+
try {
|
|
1115
|
+
if (opts.doubleClick) {
|
|
1116
|
+
await locator.dblclick({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
1117
|
+
} else {
|
|
1118
|
+
await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
1119
|
+
}
|
|
1120
|
+
} catch (err) {
|
|
1121
|
+
throw toAIFriendlyError(err, opts.ref);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
async function hoverViaPlaywright(opts) {
|
|
1125
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1126
|
+
ensurePageState(page);
|
|
1127
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1128
|
+
try {
|
|
1129
|
+
await refLocator(page, opts.ref).hover({
|
|
1130
|
+
timeout: Math.max(500, Math.min(6e4, opts.timeoutMs ?? 8e3))
|
|
1131
|
+
});
|
|
1132
|
+
} catch (err) {
|
|
1133
|
+
throw toAIFriendlyError(err, opts.ref);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
async function typeViaPlaywright(opts) {
|
|
1137
|
+
const text = String(opts.text ?? "");
|
|
1138
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1139
|
+
ensurePageState(page);
|
|
1140
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1141
|
+
const locator = refLocator(page, opts.ref);
|
|
1142
|
+
const timeout = Math.max(500, Math.min(6e4, opts.timeoutMs ?? 8e3));
|
|
1143
|
+
try {
|
|
1144
|
+
if (opts.slowly) {
|
|
1145
|
+
await locator.click({ timeout });
|
|
1146
|
+
await locator.type(text, { timeout, delay: 75 });
|
|
1147
|
+
} else {
|
|
1148
|
+
await locator.fill(text, { timeout });
|
|
1149
|
+
}
|
|
1150
|
+
if (opts.submit) await locator.press("Enter", { timeout });
|
|
1151
|
+
} catch (err) {
|
|
1152
|
+
throw toAIFriendlyError(err, opts.ref);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
async function selectOptionViaPlaywright(opts) {
|
|
1156
|
+
if (!opts.values?.length) throw new Error("values are required");
|
|
1157
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1158
|
+
ensurePageState(page);
|
|
1159
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1160
|
+
try {
|
|
1161
|
+
await refLocator(page, opts.ref).selectOption(opts.values, {
|
|
1162
|
+
timeout: Math.max(500, Math.min(6e4, opts.timeoutMs ?? 8e3))
|
|
1163
|
+
});
|
|
1164
|
+
} catch (err) {
|
|
1165
|
+
throw toAIFriendlyError(err, opts.ref);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
async function dragViaPlaywright(opts) {
|
|
1169
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1170
|
+
ensurePageState(page);
|
|
1171
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1172
|
+
try {
|
|
1173
|
+
await refLocator(page, opts.startRef).dragTo(refLocator(page, opts.endRef), {
|
|
1174
|
+
timeout: Math.max(500, Math.min(6e4, opts.timeoutMs ?? 8e3))
|
|
1175
|
+
});
|
|
1176
|
+
} catch (err) {
|
|
1177
|
+
throw toAIFriendlyError(err, `${opts.startRef} -> ${opts.endRef}`);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
async function fillFormViaPlaywright(opts) {
|
|
1181
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1182
|
+
ensurePageState(page);
|
|
1183
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1184
|
+
const timeout = Math.max(500, Math.min(6e4, opts.timeoutMs ?? 8e3));
|
|
1185
|
+
for (const field of opts.fields) {
|
|
1186
|
+
const ref = field.ref.trim();
|
|
1187
|
+
const type = field.type.trim();
|
|
1188
|
+
const rawValue = field.value;
|
|
1189
|
+
const value = typeof rawValue === "string" ? rawValue : typeof rawValue === "number" || typeof rawValue === "boolean" ? String(rawValue) : "";
|
|
1190
|
+
if (!ref || !type) continue;
|
|
1191
|
+
const locator = refLocator(page, ref);
|
|
1192
|
+
if (type === "checkbox" || type === "radio") {
|
|
1193
|
+
const checked = rawValue === true || rawValue === 1 || rawValue === "1" || rawValue === "true";
|
|
1194
|
+
try {
|
|
1195
|
+
await locator.setChecked(checked, { timeout });
|
|
1196
|
+
} catch (err) {
|
|
1197
|
+
throw toAIFriendlyError(err, ref);
|
|
1198
|
+
}
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
try {
|
|
1202
|
+
await locator.fill(value, { timeout });
|
|
1203
|
+
} catch (err) {
|
|
1204
|
+
throw toAIFriendlyError(err, ref);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
async function scrollIntoViewViaPlaywright(opts) {
|
|
1209
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1210
|
+
ensurePageState(page);
|
|
1211
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1212
|
+
try {
|
|
1213
|
+
await refLocator(page, opts.ref).scrollIntoViewIfNeeded({
|
|
1214
|
+
timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4)
|
|
1215
|
+
});
|
|
1216
|
+
} catch (err) {
|
|
1217
|
+
throw toAIFriendlyError(err, opts.ref);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// src/actions/keyboard.ts
|
|
1222
|
+
async function pressKeyViaPlaywright(opts) {
|
|
1223
|
+
const key = String(opts.key ?? "").trim();
|
|
1224
|
+
if (!key) throw new Error("key is required");
|
|
1225
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1226
|
+
ensurePageState(page);
|
|
1227
|
+
await page.keyboard.press(key, { delay: Math.max(0, Math.floor(opts.delayMs ?? 0)) });
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// src/actions/navigation.ts
|
|
1231
|
+
async function navigateViaPlaywright(opts) {
|
|
1232
|
+
const url = String(opts.url ?? "").trim();
|
|
1233
|
+
if (!url) throw new Error("url is required");
|
|
1234
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1235
|
+
ensurePageState(page);
|
|
1236
|
+
await page.goto(url, { timeout: Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 2e4)) });
|
|
1237
|
+
return { url: page.url() };
|
|
1238
|
+
}
|
|
1239
|
+
async function listPagesViaPlaywright(opts) {
|
|
1240
|
+
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
1241
|
+
const pages = await getAllPages(browser);
|
|
1242
|
+
const results = [];
|
|
1243
|
+
for (const page of pages) {
|
|
1244
|
+
const tid = await pageTargetId(page).catch(() => null);
|
|
1245
|
+
if (tid) results.push({
|
|
1246
|
+
targetId: tid,
|
|
1247
|
+
title: await page.title().catch(() => ""),
|
|
1248
|
+
url: page.url(),
|
|
1249
|
+
type: "page"
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
return results;
|
|
1253
|
+
}
|
|
1254
|
+
async function createPageViaPlaywright(opts) {
|
|
1255
|
+
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
1256
|
+
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
1257
|
+
ensureContextState(context);
|
|
1258
|
+
const page = await context.newPage();
|
|
1259
|
+
ensurePageState(page);
|
|
1260
|
+
const targetUrl = (opts.url ?? "").trim() || "about:blank";
|
|
1261
|
+
if (targetUrl !== "about:blank") {
|
|
1262
|
+
await page.goto(targetUrl, { timeout: 3e4 }).catch(() => {
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
const tid = await pageTargetId(page).catch(() => null);
|
|
1266
|
+
if (!tid) throw new Error("Failed to get targetId for new page");
|
|
1267
|
+
return {
|
|
1268
|
+
targetId: tid,
|
|
1269
|
+
title: await page.title().catch(() => ""),
|
|
1270
|
+
url: page.url(),
|
|
1271
|
+
type: "page"
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
async function closePageByTargetIdViaPlaywright(opts) {
|
|
1275
|
+
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
1276
|
+
const page = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
|
|
1277
|
+
if (!page) throw new Error(`Tab not found (targetId: ${opts.targetId}). Use browser.tabs() to list open tabs.`);
|
|
1278
|
+
await page.close();
|
|
1279
|
+
}
|
|
1280
|
+
async function focusPageByTargetIdViaPlaywright(opts) {
|
|
1281
|
+
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
1282
|
+
const page = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
|
|
1283
|
+
if (!page) throw new Error(`Tab not found (targetId: ${opts.targetId}). Use browser.tabs() to list open tabs.`);
|
|
1284
|
+
try {
|
|
1285
|
+
await page.bringToFront();
|
|
1286
|
+
} catch (err) {
|
|
1287
|
+
const session = await page.context().newCDPSession(page);
|
|
1288
|
+
try {
|
|
1289
|
+
await session.send("Page.bringToFront");
|
|
1290
|
+
} catch {
|
|
1291
|
+
throw err;
|
|
1292
|
+
} finally {
|
|
1293
|
+
await session.detach().catch(() => {
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
async function resizeViewportViaPlaywright(opts) {
|
|
1299
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1300
|
+
ensurePageState(page);
|
|
1301
|
+
await page.setViewportSize({
|
|
1302
|
+
width: Math.max(1, Math.floor(opts.width)),
|
|
1303
|
+
height: Math.max(1, Math.floor(opts.height))
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// src/actions/wait.ts
|
|
1308
|
+
async function waitForViaPlaywright(opts) {
|
|
1309
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1310
|
+
ensurePageState(page);
|
|
1311
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
|
|
1312
|
+
if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
|
|
1313
|
+
await page.waitForTimeout(Math.max(0, opts.timeMs));
|
|
1314
|
+
}
|
|
1315
|
+
if (opts.text) {
|
|
1316
|
+
await page.getByText(opts.text).first().waitFor({ state: "visible", timeout });
|
|
1317
|
+
}
|
|
1318
|
+
if (opts.textGone) {
|
|
1319
|
+
await page.getByText(opts.textGone).first().waitFor({ state: "hidden", timeout });
|
|
1320
|
+
}
|
|
1321
|
+
if (opts.selector) {
|
|
1322
|
+
const selector = String(opts.selector).trim();
|
|
1323
|
+
if (selector) await page.locator(selector).first().waitFor({ state: "visible", timeout });
|
|
1324
|
+
}
|
|
1325
|
+
if (opts.url) {
|
|
1326
|
+
const url = String(opts.url).trim();
|
|
1327
|
+
if (url) await page.waitForURL(url, { timeout });
|
|
1328
|
+
}
|
|
1329
|
+
if (opts.loadState) {
|
|
1330
|
+
await page.waitForLoadState(opts.loadState, { timeout });
|
|
1331
|
+
}
|
|
1332
|
+
if (opts.fn) {
|
|
1333
|
+
const fn = String(opts.fn).trim();
|
|
1334
|
+
if (fn) await page.waitForFunction(fn, { timeout });
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// src/actions/evaluate.ts
|
|
1339
|
+
async function evaluateInAllFramesViaPlaywright(opts) {
|
|
1340
|
+
const fnText = String(opts.fn ?? "").trim();
|
|
1341
|
+
if (!fnText) throw new Error("function is required");
|
|
1342
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1343
|
+
const frames = page.frames();
|
|
1344
|
+
const results = [];
|
|
1345
|
+
for (const frame of frames) {
|
|
1346
|
+
try {
|
|
1347
|
+
const result = await frame.evaluate(
|
|
1348
|
+
// eslint-disable-next-line no-eval
|
|
1349
|
+
(fnBody) => {
|
|
1350
|
+
"use strict";
|
|
1351
|
+
try {
|
|
1352
|
+
const candidate = (0, eval)("(" + fnBody + ")");
|
|
1353
|
+
return typeof candidate === "function" ? candidate() : candidate;
|
|
1354
|
+
} catch (err) {
|
|
1355
|
+
throw new Error("Invalid evaluate function: " + (err?.message ?? String(err)));
|
|
1356
|
+
}
|
|
1357
|
+
},
|
|
1358
|
+
fnText
|
|
1359
|
+
);
|
|
1360
|
+
results.push({
|
|
1361
|
+
frameUrl: frame.url(),
|
|
1362
|
+
frameName: frame.name(),
|
|
1363
|
+
result
|
|
1364
|
+
});
|
|
1365
|
+
} catch {
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
return results;
|
|
1369
|
+
}
|
|
1370
|
+
async function evaluateViaPlaywright(opts) {
|
|
1371
|
+
const fnText = String(opts.fn ?? "").trim();
|
|
1372
|
+
if (!fnText) throw new Error("function is required");
|
|
1373
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1374
|
+
ensurePageState(page);
|
|
1375
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1376
|
+
if (opts.ref) {
|
|
1377
|
+
const locator = refLocator(page, opts.ref);
|
|
1378
|
+
return await locator.evaluate(
|
|
1379
|
+
// eslint-disable-next-line no-eval
|
|
1380
|
+
(el, fnBody) => {
|
|
1381
|
+
try {
|
|
1382
|
+
const candidate = (0, eval)("(" + fnBody + ")");
|
|
1383
|
+
return typeof candidate === "function" ? candidate(el) : candidate;
|
|
1384
|
+
} catch (err) {
|
|
1385
|
+
throw new Error("Invalid evaluate function: " + (err?.message ?? String(err)));
|
|
1386
|
+
}
|
|
1387
|
+
},
|
|
1388
|
+
fnText
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
return await page.evaluate(
|
|
1392
|
+
// eslint-disable-next-line no-eval
|
|
1393
|
+
(fnBody) => {
|
|
1394
|
+
try {
|
|
1395
|
+
const candidate = (0, eval)("(" + fnBody + ")");
|
|
1396
|
+
return typeof candidate === "function" ? candidate() : candidate;
|
|
1397
|
+
} catch (err) {
|
|
1398
|
+
throw new Error("Invalid evaluate function: " + (err?.message ?? String(err)));
|
|
1399
|
+
}
|
|
1400
|
+
},
|
|
1401
|
+
fnText
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// src/capture/screenshot.ts
|
|
1406
|
+
async function takeScreenshotViaPlaywright(opts) {
|
|
1407
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1408
|
+
ensurePageState(page);
|
|
1409
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1410
|
+
const type = opts.type ?? "png";
|
|
1411
|
+
if (opts.ref) {
|
|
1412
|
+
if (opts.fullPage) throw new Error("fullPage is not supported for element screenshots");
|
|
1413
|
+
return { buffer: await refLocator(page, opts.ref).screenshot({ type }) };
|
|
1414
|
+
}
|
|
1415
|
+
if (opts.element) {
|
|
1416
|
+
if (opts.fullPage) throw new Error("fullPage is not supported for element screenshots");
|
|
1417
|
+
return { buffer: await page.locator(opts.element).first().screenshot({ type }) };
|
|
1418
|
+
}
|
|
1419
|
+
return { buffer: await page.screenshot({ type, fullPage: Boolean(opts.fullPage) }) };
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// src/capture/pdf.ts
|
|
1423
|
+
async function pdfViaPlaywright(opts) {
|
|
1424
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1425
|
+
ensurePageState(page);
|
|
1426
|
+
return { buffer: await page.pdf({ printBackground: true }) };
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// src/capture/activity.ts
|
|
1430
|
+
function consolePriority(level) {
|
|
1431
|
+
switch (level) {
|
|
1432
|
+
case "error":
|
|
1433
|
+
return 3;
|
|
1434
|
+
case "warning":
|
|
1435
|
+
return 2;
|
|
1436
|
+
case "info":
|
|
1437
|
+
case "log":
|
|
1438
|
+
return 1;
|
|
1439
|
+
case "debug":
|
|
1440
|
+
return 0;
|
|
1441
|
+
default:
|
|
1442
|
+
return 1;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
async function getConsoleMessagesViaPlaywright(opts) {
|
|
1446
|
+
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
1447
|
+
if (!opts.level) return [...state.console];
|
|
1448
|
+
const min = consolePriority(opts.level);
|
|
1449
|
+
return state.console.filter((msg) => consolePriority(msg.type) >= min);
|
|
1450
|
+
}
|
|
1451
|
+
async function getPageErrorsViaPlaywright(opts) {
|
|
1452
|
+
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
1453
|
+
const errors = [...state.errors];
|
|
1454
|
+
if (opts.clear) state.errors = [];
|
|
1455
|
+
return { errors };
|
|
1456
|
+
}
|
|
1457
|
+
async function getNetworkRequestsViaPlaywright(opts) {
|
|
1458
|
+
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
1459
|
+
const raw = [...state.requests];
|
|
1460
|
+
const filter = typeof opts.filter === "string" ? opts.filter.trim() : "";
|
|
1461
|
+
const requests = filter ? raw.filter((r) => r.url.includes(filter)) : raw;
|
|
1462
|
+
if (opts.clear) {
|
|
1463
|
+
state.requests = [];
|
|
1464
|
+
state.requestIds = /* @__PURE__ */ new WeakMap();
|
|
1465
|
+
}
|
|
1466
|
+
return { requests };
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// src/storage/index.ts
|
|
1470
|
+
async function cookiesGetViaPlaywright(opts) {
|
|
1471
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1472
|
+
ensurePageState(page);
|
|
1473
|
+
return { cookies: await page.context().cookies() };
|
|
1474
|
+
}
|
|
1475
|
+
async function cookiesSetViaPlaywright(opts) {
|
|
1476
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1477
|
+
ensurePageState(page);
|
|
1478
|
+
const cookie = opts.cookie;
|
|
1479
|
+
if (!cookie.name || cookie.value === void 0) throw new Error("cookie name and value are required");
|
|
1480
|
+
const hasUrl = typeof cookie.url === "string" && cookie.url.trim();
|
|
1481
|
+
const hasDomainPath = typeof cookie.domain === "string" && cookie.domain.trim() && typeof cookie.path === "string" && cookie.path.trim();
|
|
1482
|
+
if (!hasUrl && !hasDomainPath) throw new Error("cookie requires url, or domain+path");
|
|
1483
|
+
await page.context().addCookies([cookie]);
|
|
1484
|
+
}
|
|
1485
|
+
async function cookiesClearViaPlaywright(opts) {
|
|
1486
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1487
|
+
ensurePageState(page);
|
|
1488
|
+
await page.context().clearCookies();
|
|
1489
|
+
}
|
|
1490
|
+
async function storageGetViaPlaywright(opts) {
|
|
1491
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1492
|
+
ensurePageState(page);
|
|
1493
|
+
return {
|
|
1494
|
+
values: await page.evaluate(
|
|
1495
|
+
({ kind, key }) => {
|
|
1496
|
+
const store = kind === "session" ? window.sessionStorage : window.localStorage;
|
|
1497
|
+
if (key) {
|
|
1498
|
+
const value = store.getItem(key);
|
|
1499
|
+
return value === null ? {} : { [key]: value };
|
|
1500
|
+
}
|
|
1501
|
+
const out = {};
|
|
1502
|
+
for (let i = 0; i < store.length; i++) {
|
|
1503
|
+
const k = store.key(i);
|
|
1504
|
+
if (!k) continue;
|
|
1505
|
+
const v = store.getItem(k);
|
|
1506
|
+
if (v !== null) out[k] = v;
|
|
1507
|
+
}
|
|
1508
|
+
return out;
|
|
1509
|
+
},
|
|
1510
|
+
{ kind: opts.kind, key: opts.key }
|
|
1511
|
+
) ?? {}
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
async function storageSetViaPlaywright(opts) {
|
|
1515
|
+
const key = String(opts.key ?? "");
|
|
1516
|
+
if (!key) throw new Error("key is required");
|
|
1517
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1518
|
+
ensurePageState(page);
|
|
1519
|
+
await page.evaluate(
|
|
1520
|
+
({ kind, key: k, value }) => {
|
|
1521
|
+
(kind === "session" ? window.sessionStorage : window.localStorage).setItem(k, value);
|
|
1522
|
+
},
|
|
1523
|
+
{ kind: opts.kind, key, value: String(opts.value ?? "") }
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
async function storageClearViaPlaywright(opts) {
|
|
1527
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1528
|
+
ensurePageState(page);
|
|
1529
|
+
await page.evaluate(
|
|
1530
|
+
({ kind }) => {
|
|
1531
|
+
(kind === "session" ? window.sessionStorage : window.localStorage).clear();
|
|
1532
|
+
},
|
|
1533
|
+
{ kind: opts.kind }
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// src/browser.ts
|
|
1538
|
+
var CrawlPage = class {
|
|
1539
|
+
cdpUrl;
|
|
1540
|
+
targetId;
|
|
1541
|
+
/** @internal */
|
|
1542
|
+
constructor(cdpUrl, targetId) {
|
|
1543
|
+
this.cdpUrl = cdpUrl;
|
|
1544
|
+
this.targetId = targetId;
|
|
1545
|
+
}
|
|
1546
|
+
/** The CDP target ID for this page. Use this to identify the page in multi-tab scenarios. */
|
|
1547
|
+
get id() {
|
|
1548
|
+
return this.targetId;
|
|
1549
|
+
}
|
|
1550
|
+
// ── Snapshot ──────────────────────────────────────────────────
|
|
1551
|
+
/**
|
|
1552
|
+
* Take an AI-readable snapshot of the page.
|
|
1553
|
+
*
|
|
1554
|
+
* Returns a text tree with numbered refs (`e1`, `e2`, ...) that map to
|
|
1555
|
+
* interactive elements. Use these refs with actions like `click()` and `type()`.
|
|
1556
|
+
*
|
|
1557
|
+
* @param opts - Snapshot options (mode, filtering, depth limits)
|
|
1558
|
+
* @returns Snapshot text, ref map, and statistics
|
|
1559
|
+
*
|
|
1560
|
+
* @example
|
|
1561
|
+
* ```ts
|
|
1562
|
+
* // Default snapshot (aria mode)
|
|
1563
|
+
* const { snapshot, refs } = await page.snapshot();
|
|
1564
|
+
*
|
|
1565
|
+
* // Interactive elements only, compact
|
|
1566
|
+
* const result = await page.snapshot({ interactive: true, compact: true });
|
|
1567
|
+
*
|
|
1568
|
+
* // Role-based mode (uses getByRole resolution)
|
|
1569
|
+
* const result = await page.snapshot({ mode: 'role' });
|
|
1570
|
+
* ```
|
|
1571
|
+
*/
|
|
1572
|
+
async snapshot(opts) {
|
|
1573
|
+
if (opts?.mode === "role") {
|
|
1574
|
+
return snapshotRole({
|
|
1575
|
+
cdpUrl: this.cdpUrl,
|
|
1576
|
+
targetId: this.targetId,
|
|
1577
|
+
selector: opts?.selector,
|
|
1578
|
+
frameSelector: opts?.frameSelector,
|
|
1579
|
+
options: {
|
|
1580
|
+
interactive: opts?.interactive,
|
|
1581
|
+
compact: opts?.compact,
|
|
1582
|
+
maxDepth: opts?.maxDepth
|
|
1583
|
+
}
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
return snapshotAi({
|
|
1587
|
+
cdpUrl: this.cdpUrl,
|
|
1588
|
+
targetId: this.targetId,
|
|
1589
|
+
maxChars: opts?.maxChars,
|
|
1590
|
+
options: {
|
|
1591
|
+
interactive: opts?.interactive,
|
|
1592
|
+
compact: opts?.compact,
|
|
1593
|
+
maxDepth: opts?.maxDepth
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Take a raw ARIA accessibility tree snapshot via CDP.
|
|
1599
|
+
*
|
|
1600
|
+
* Unlike `snapshot()`, this returns structured node data rather than
|
|
1601
|
+
* an AI-readable text tree. Useful for programmatic accessibility analysis.
|
|
1602
|
+
*
|
|
1603
|
+
* @param opts - Options (limit: max nodes to return, default 500)
|
|
1604
|
+
* @returns Array of accessibility tree nodes
|
|
1605
|
+
*/
|
|
1606
|
+
async ariaSnapshot(opts) {
|
|
1607
|
+
return snapshotAria({ cdpUrl: this.cdpUrl, targetId: this.targetId, limit: opts?.limit });
|
|
1608
|
+
}
|
|
1609
|
+
// ── Interactions ─────────────────────────────────────────────
|
|
1610
|
+
/**
|
|
1611
|
+
* Click an element by ref.
|
|
1612
|
+
*
|
|
1613
|
+
* @param ref - Ref ID from a snapshot (e.g. `'e1'`)
|
|
1614
|
+
* @param opts - Click options (double-click, button, modifiers)
|
|
1615
|
+
*
|
|
1616
|
+
* @example
|
|
1617
|
+
* ```ts
|
|
1618
|
+
* await page.click('e1');
|
|
1619
|
+
* await page.click('e2', { doubleClick: true });
|
|
1620
|
+
* await page.click('e3', { button: 'right' });
|
|
1621
|
+
* await page.click('e4', { modifiers: ['Control'] });
|
|
1622
|
+
* ```
|
|
1623
|
+
*/
|
|
1624
|
+
async click(ref, opts) {
|
|
1625
|
+
return clickViaPlaywright({
|
|
1626
|
+
cdpUrl: this.cdpUrl,
|
|
1627
|
+
targetId: this.targetId,
|
|
1628
|
+
ref,
|
|
1629
|
+
doubleClick: opts?.doubleClick,
|
|
1630
|
+
button: opts?.button,
|
|
1631
|
+
modifiers: opts?.modifiers,
|
|
1632
|
+
timeoutMs: opts?.timeoutMs
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Type text into an input element by ref.
|
|
1637
|
+
*
|
|
1638
|
+
* By default, uses Playwright's `fill()` for instant input. Use `slowly: true`
|
|
1639
|
+
* to simulate real keystroke typing with a 75ms delay per character.
|
|
1640
|
+
*
|
|
1641
|
+
* @param ref - Ref ID of the input element (e.g. `'e3'`)
|
|
1642
|
+
* @param text - Text to type
|
|
1643
|
+
* @param opts - Type options (submit, slowly)
|
|
1644
|
+
*
|
|
1645
|
+
* @example
|
|
1646
|
+
* ```ts
|
|
1647
|
+
* await page.type('e3', 'hello world');
|
|
1648
|
+
* await page.type('e3', 'slow typing', { slowly: true });
|
|
1649
|
+
* await page.type('e3', 'search query', { submit: true }); // press Enter after
|
|
1650
|
+
* ```
|
|
1651
|
+
*/
|
|
1652
|
+
async type(ref, text, opts) {
|
|
1653
|
+
return typeViaPlaywright({
|
|
1654
|
+
cdpUrl: this.cdpUrl,
|
|
1655
|
+
targetId: this.targetId,
|
|
1656
|
+
ref,
|
|
1657
|
+
text,
|
|
1658
|
+
submit: opts?.submit,
|
|
1659
|
+
slowly: opts?.slowly,
|
|
1660
|
+
timeoutMs: opts?.timeoutMs
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Hover over an element by ref.
|
|
1665
|
+
*
|
|
1666
|
+
* @param ref - Ref ID from a snapshot
|
|
1667
|
+
* @param opts - Timeout options
|
|
1668
|
+
*/
|
|
1669
|
+
async hover(ref, opts) {
|
|
1670
|
+
return hoverViaPlaywright({
|
|
1671
|
+
cdpUrl: this.cdpUrl,
|
|
1672
|
+
targetId: this.targetId,
|
|
1673
|
+
ref,
|
|
1674
|
+
timeoutMs: opts?.timeoutMs
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
/**
|
|
1678
|
+
* Select option(s) in a `<select>` dropdown by ref.
|
|
1679
|
+
*
|
|
1680
|
+
* @param ref - Ref ID of the select element
|
|
1681
|
+
* @param values - One or more option labels/values to select
|
|
1682
|
+
*
|
|
1683
|
+
* @example
|
|
1684
|
+
* ```ts
|
|
1685
|
+
* await page.select('e5', 'Option A');
|
|
1686
|
+
* await page.select('e5', 'Option A', 'Option B'); // multi-select
|
|
1687
|
+
* ```
|
|
1688
|
+
*/
|
|
1689
|
+
async select(ref, ...values) {
|
|
1690
|
+
return selectOptionViaPlaywright({
|
|
1691
|
+
cdpUrl: this.cdpUrl,
|
|
1692
|
+
targetId: this.targetId,
|
|
1693
|
+
ref,
|
|
1694
|
+
values
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Drag one element to another.
|
|
1699
|
+
*
|
|
1700
|
+
* @param startRef - Ref ID of the element to drag
|
|
1701
|
+
* @param endRef - Ref ID of the drop target
|
|
1702
|
+
* @param opts - Timeout options
|
|
1703
|
+
*/
|
|
1704
|
+
async drag(startRef, endRef, opts) {
|
|
1705
|
+
return dragViaPlaywright({
|
|
1706
|
+
cdpUrl: this.cdpUrl,
|
|
1707
|
+
targetId: this.targetId,
|
|
1708
|
+
startRef,
|
|
1709
|
+
endRef,
|
|
1710
|
+
timeoutMs: opts?.timeoutMs
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Fill multiple form fields at once.
|
|
1715
|
+
*
|
|
1716
|
+
* Supports text inputs, checkboxes, and radio buttons.
|
|
1717
|
+
*
|
|
1718
|
+
* @param fields - Array of form fields to fill
|
|
1719
|
+
*
|
|
1720
|
+
* @example
|
|
1721
|
+
* ```ts
|
|
1722
|
+
* await page.fill([
|
|
1723
|
+
* { ref: 'e2', type: 'text', value: 'Jane Doe' },
|
|
1724
|
+
* { ref: 'e4', type: 'text', value: 'jane@example.com' },
|
|
1725
|
+
* { ref: 'e6', type: 'checkbox', value: true },
|
|
1726
|
+
* ]);
|
|
1727
|
+
* ```
|
|
1728
|
+
*/
|
|
1729
|
+
async fill(fields) {
|
|
1730
|
+
return fillFormViaPlaywright({
|
|
1731
|
+
cdpUrl: this.cdpUrl,
|
|
1732
|
+
targetId: this.targetId,
|
|
1733
|
+
fields
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Scroll an element into the visible viewport.
|
|
1738
|
+
*
|
|
1739
|
+
* @param ref - Ref ID of the element to scroll to
|
|
1740
|
+
* @param opts - Timeout options
|
|
1741
|
+
*/
|
|
1742
|
+
async scrollIntoView(ref, opts) {
|
|
1743
|
+
return scrollIntoViewViaPlaywright({
|
|
1744
|
+
cdpUrl: this.cdpUrl,
|
|
1745
|
+
targetId: this.targetId,
|
|
1746
|
+
ref,
|
|
1747
|
+
timeoutMs: opts?.timeoutMs
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
// ── Keyboard ─────────────────────────────────────────────────
|
|
1751
|
+
/**
|
|
1752
|
+
* Press a keyboard key or key combination.
|
|
1753
|
+
*
|
|
1754
|
+
* Uses Playwright's key names. Supports combinations with `+`.
|
|
1755
|
+
*
|
|
1756
|
+
* @param key - Key to press (e.g. `'Enter'`, `'Tab'`, `'Control+a'`, `'Meta+c'`)
|
|
1757
|
+
* @param opts - Options (delayMs: hold time between keydown and keyup)
|
|
1758
|
+
*
|
|
1759
|
+
* @example
|
|
1760
|
+
* ```ts
|
|
1761
|
+
* await page.press('Enter');
|
|
1762
|
+
* await page.press('Control+a');
|
|
1763
|
+
* await page.press('Meta+Shift+p');
|
|
1764
|
+
* ```
|
|
1765
|
+
*/
|
|
1766
|
+
async press(key, opts) {
|
|
1767
|
+
return pressKeyViaPlaywright({
|
|
1768
|
+
cdpUrl: this.cdpUrl,
|
|
1769
|
+
targetId: this.targetId,
|
|
1770
|
+
key,
|
|
1771
|
+
delayMs: opts?.delayMs
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
// ── Navigation ───────────────────────────────────────────────
|
|
1775
|
+
/**
|
|
1776
|
+
* Navigate to a URL.
|
|
1777
|
+
*
|
|
1778
|
+
* @param url - The URL to navigate to
|
|
1779
|
+
* @param opts - Timeout options
|
|
1780
|
+
* @returns The final URL after navigation (may differ due to redirects)
|
|
1781
|
+
*/
|
|
1782
|
+
async goto(url, opts) {
|
|
1783
|
+
return navigateViaPlaywright({
|
|
1784
|
+
cdpUrl: this.cdpUrl,
|
|
1785
|
+
targetId: this.targetId,
|
|
1786
|
+
url,
|
|
1787
|
+
timeoutMs: opts?.timeoutMs
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
// ── Wait ─────────────────────────────────────────────────────
|
|
1791
|
+
/**
|
|
1792
|
+
* Wait for various conditions on the page.
|
|
1793
|
+
*
|
|
1794
|
+
* Multiple conditions can be specified — they are checked in order.
|
|
1795
|
+
*
|
|
1796
|
+
* @param opts - Wait conditions (text, URL, load state, selector, etc.)
|
|
1797
|
+
*
|
|
1798
|
+
* @example
|
|
1799
|
+
* ```ts
|
|
1800
|
+
* await page.waitFor({ loadState: 'networkidle' });
|
|
1801
|
+
* await page.waitFor({ text: 'Welcome back' });
|
|
1802
|
+
* await page.waitFor({ url: '**\/dashboard' });
|
|
1803
|
+
* await page.waitFor({ timeMs: 1000 }); // sleep
|
|
1804
|
+
* ```
|
|
1805
|
+
*/
|
|
1806
|
+
async waitFor(opts) {
|
|
1807
|
+
return waitForViaPlaywright({
|
|
1808
|
+
cdpUrl: this.cdpUrl,
|
|
1809
|
+
targetId: this.targetId,
|
|
1810
|
+
...opts
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
// ── Evaluate ─────────────────────────────────────────────────
|
|
1814
|
+
/**
|
|
1815
|
+
* Run JavaScript in the browser page context.
|
|
1816
|
+
*
|
|
1817
|
+
* The function string is evaluated in the browser's sandbox, not in Node.js.
|
|
1818
|
+
* Pass a `ref` to receive the element as the first argument.
|
|
1819
|
+
*
|
|
1820
|
+
* @param fn - JavaScript function body as a string
|
|
1821
|
+
* @param opts - Options (ref: scope evaluation to a specific element)
|
|
1822
|
+
* @returns The return value of the evaluated function
|
|
1823
|
+
*
|
|
1824
|
+
* @example
|
|
1825
|
+
* ```ts
|
|
1826
|
+
* const title = await page.evaluate('() => document.title');
|
|
1827
|
+
* const text = await page.evaluate('(el) => el.textContent', { ref: 'e1' });
|
|
1828
|
+
* const count = await page.evaluate('() => document.querySelectorAll("img").length');
|
|
1829
|
+
* ```
|
|
1830
|
+
*/
|
|
1831
|
+
async evaluate(fn, opts) {
|
|
1832
|
+
return evaluateViaPlaywright({
|
|
1833
|
+
cdpUrl: this.cdpUrl,
|
|
1834
|
+
targetId: this.targetId,
|
|
1835
|
+
fn,
|
|
1836
|
+
ref: opts?.ref
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Run JavaScript in ALL frames on the page (including cross-origin iframes).
|
|
1841
|
+
*
|
|
1842
|
+
* Playwright can access cross-origin frames via CDP, bypassing the same-origin policy.
|
|
1843
|
+
* This is essential for filling payment iframes (Stripe, etc.).
|
|
1844
|
+
*
|
|
1845
|
+
* @param fn - JavaScript function body as a string
|
|
1846
|
+
* @returns Array of results from each frame where evaluation succeeded
|
|
1847
|
+
*
|
|
1848
|
+
* @example
|
|
1849
|
+
* ```ts
|
|
1850
|
+
* const results = await page.evaluateInAllFrames(`() => {
|
|
1851
|
+
* const el = document.querySelector('input[name="cardnumber"]');
|
|
1852
|
+
* return el ? 'found' : null;
|
|
1853
|
+
* }`);
|
|
1854
|
+
* ```
|
|
1855
|
+
*/
|
|
1856
|
+
async evaluateInAllFrames(fn) {
|
|
1857
|
+
return evaluateInAllFramesViaPlaywright({
|
|
1858
|
+
cdpUrl: this.cdpUrl,
|
|
1859
|
+
targetId: this.targetId,
|
|
1860
|
+
fn
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
// ── Capture ──────────────────────────────────────────────────
|
|
1864
|
+
/**
|
|
1865
|
+
* Take a screenshot of the page or a specific element.
|
|
1866
|
+
*
|
|
1867
|
+
* @param opts - Screenshot options (fullPage, ref, element, type)
|
|
1868
|
+
* @returns PNG or JPEG image as a Buffer
|
|
1869
|
+
*
|
|
1870
|
+
* @example
|
|
1871
|
+
* ```ts
|
|
1872
|
+
* const screenshot = await page.screenshot();
|
|
1873
|
+
* const fullPage = await page.screenshot({ fullPage: true });
|
|
1874
|
+
* const element = await page.screenshot({ ref: 'e1' });
|
|
1875
|
+
* ```
|
|
1876
|
+
*/
|
|
1877
|
+
async screenshot(opts) {
|
|
1878
|
+
const result = await takeScreenshotViaPlaywright({
|
|
1879
|
+
cdpUrl: this.cdpUrl,
|
|
1880
|
+
targetId: this.targetId,
|
|
1881
|
+
fullPage: opts?.fullPage,
|
|
1882
|
+
ref: opts?.ref,
|
|
1883
|
+
element: opts?.element,
|
|
1884
|
+
type: opts?.type
|
|
1885
|
+
});
|
|
1886
|
+
return result.buffer;
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Export the page as a PDF.
|
|
1890
|
+
*
|
|
1891
|
+
* Only works in headless mode.
|
|
1892
|
+
*
|
|
1893
|
+
* @returns PDF document as a Buffer
|
|
1894
|
+
*/
|
|
1895
|
+
async pdf() {
|
|
1896
|
+
const result = await pdfViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
|
|
1897
|
+
return result.buffer;
|
|
1898
|
+
}
|
|
1899
|
+
/**
|
|
1900
|
+
* Get console messages captured from the page.
|
|
1901
|
+
*
|
|
1902
|
+
* Messages are buffered automatically. Use `level` to filter by minimum severity.
|
|
1903
|
+
*
|
|
1904
|
+
* @param opts - Filter options (level: `'debug'` | `'log'` | `'info'` | `'warning'` | `'error'`)
|
|
1905
|
+
* @returns Array of captured console messages
|
|
1906
|
+
*/
|
|
1907
|
+
async consoleLogs(opts) {
|
|
1908
|
+
return getConsoleMessagesViaPlaywright({
|
|
1909
|
+
cdpUrl: this.cdpUrl,
|
|
1910
|
+
targetId: this.targetId,
|
|
1911
|
+
level: opts?.level
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Get uncaught errors from the page.
|
|
1916
|
+
*
|
|
1917
|
+
* @param opts - Options (clear: reset the error buffer after reading)
|
|
1918
|
+
* @returns Array of captured page errors
|
|
1919
|
+
*/
|
|
1920
|
+
async pageErrors(opts) {
|
|
1921
|
+
const result = await getPageErrorsViaPlaywright({
|
|
1922
|
+
cdpUrl: this.cdpUrl,
|
|
1923
|
+
targetId: this.targetId,
|
|
1924
|
+
clear: opts?.clear
|
|
1925
|
+
});
|
|
1926
|
+
return result.errors;
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Get network requests captured from the page.
|
|
1930
|
+
*
|
|
1931
|
+
* @param opts - Options (filter: URL substring match, clear: reset the buffer)
|
|
1932
|
+
* @returns Array of captured network requests
|
|
1933
|
+
*
|
|
1934
|
+
* @example
|
|
1935
|
+
* ```ts
|
|
1936
|
+
* const all = await page.networkRequests();
|
|
1937
|
+
* const apiCalls = await page.networkRequests({ filter: '/api/' });
|
|
1938
|
+
* const fresh = await page.networkRequests({ clear: true }); // read and clear
|
|
1939
|
+
* ```
|
|
1940
|
+
*/
|
|
1941
|
+
async networkRequests(opts) {
|
|
1942
|
+
const result = await getNetworkRequestsViaPlaywright({
|
|
1943
|
+
cdpUrl: this.cdpUrl,
|
|
1944
|
+
targetId: this.targetId,
|
|
1945
|
+
filter: opts?.filter,
|
|
1946
|
+
clear: opts?.clear
|
|
1947
|
+
});
|
|
1948
|
+
return result.requests;
|
|
1949
|
+
}
|
|
1950
|
+
// ── Viewport ─────────────────────────────────────────────────
|
|
1951
|
+
/**
|
|
1952
|
+
* Resize the browser viewport.
|
|
1953
|
+
*
|
|
1954
|
+
* @param width - Viewport width in pixels
|
|
1955
|
+
* @param height - Viewport height in pixels
|
|
1956
|
+
*/
|
|
1957
|
+
async resize(width, height) {
|
|
1958
|
+
return resizeViewportViaPlaywright({
|
|
1959
|
+
cdpUrl: this.cdpUrl,
|
|
1960
|
+
targetId: this.targetId,
|
|
1961
|
+
width,
|
|
1962
|
+
height
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
// ── Storage ──────────────────────────────────────────────────
|
|
1966
|
+
/**
|
|
1967
|
+
* Get all cookies for the current browser context.
|
|
1968
|
+
*
|
|
1969
|
+
* @returns Array of cookie objects
|
|
1970
|
+
*/
|
|
1971
|
+
async cookies() {
|
|
1972
|
+
const result = await cookiesGetViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
|
|
1973
|
+
return result.cookies;
|
|
1974
|
+
}
|
|
1975
|
+
/**
|
|
1976
|
+
* Set a cookie in the browser context.
|
|
1977
|
+
*
|
|
1978
|
+
* @param cookie - Cookie data (must include `name`, `value`, and either `url` or `domain`+`path`)
|
|
1979
|
+
*
|
|
1980
|
+
* @example
|
|
1981
|
+
* ```ts
|
|
1982
|
+
* await page.setCookie({
|
|
1983
|
+
* name: 'token',
|
|
1984
|
+
* value: 'abc123',
|
|
1985
|
+
* url: 'https://example.com',
|
|
1986
|
+
* });
|
|
1987
|
+
* ```
|
|
1988
|
+
*/
|
|
1989
|
+
async setCookie(cookie) {
|
|
1990
|
+
return cookiesSetViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId, cookie });
|
|
1991
|
+
}
|
|
1992
|
+
/** Clear all cookies in the browser context. */
|
|
1993
|
+
async clearCookies() {
|
|
1994
|
+
return cookiesClearViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
|
|
1995
|
+
}
|
|
1996
|
+
/**
|
|
1997
|
+
* Get values from localStorage or sessionStorage.
|
|
1998
|
+
*
|
|
1999
|
+
* @param kind - `'local'` for localStorage, `'session'` for sessionStorage
|
|
2000
|
+
* @param key - Optional specific key to retrieve (returns all if omitted)
|
|
2001
|
+
* @returns Key-value map of storage entries
|
|
2002
|
+
*/
|
|
2003
|
+
async storageGet(kind, key) {
|
|
2004
|
+
const result = await storageGetViaPlaywright({
|
|
2005
|
+
cdpUrl: this.cdpUrl,
|
|
2006
|
+
targetId: this.targetId,
|
|
2007
|
+
kind,
|
|
2008
|
+
key
|
|
2009
|
+
});
|
|
2010
|
+
return result.values;
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* Set a value in localStorage or sessionStorage.
|
|
2014
|
+
*
|
|
2015
|
+
* @param kind - `'local'` for localStorage, `'session'` for sessionStorage
|
|
2016
|
+
* @param key - Storage key
|
|
2017
|
+
* @param value - Storage value
|
|
2018
|
+
*/
|
|
2019
|
+
async storageSet(kind, key, value) {
|
|
2020
|
+
return storageSetViaPlaywright({
|
|
2021
|
+
cdpUrl: this.cdpUrl,
|
|
2022
|
+
targetId: this.targetId,
|
|
2023
|
+
kind,
|
|
2024
|
+
key,
|
|
2025
|
+
value
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
/**
|
|
2029
|
+
* Clear all entries in localStorage or sessionStorage.
|
|
2030
|
+
*
|
|
2031
|
+
* @param kind - `'local'` for localStorage, `'session'` for sessionStorage
|
|
2032
|
+
*/
|
|
2033
|
+
async storageClear(kind) {
|
|
2034
|
+
return storageClearViaPlaywright({
|
|
2035
|
+
cdpUrl: this.cdpUrl,
|
|
2036
|
+
targetId: this.targetId,
|
|
2037
|
+
kind
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
};
|
|
2041
|
+
var BrowserClaw = class _BrowserClaw {
|
|
2042
|
+
cdpUrl;
|
|
2043
|
+
chrome;
|
|
2044
|
+
constructor(cdpUrl, chrome) {
|
|
2045
|
+
this.cdpUrl = cdpUrl;
|
|
2046
|
+
this.chrome = chrome;
|
|
2047
|
+
}
|
|
2048
|
+
/**
|
|
2049
|
+
* Launch a new Chrome instance and connect to it.
|
|
2050
|
+
*
|
|
2051
|
+
* Automatically detects Chrome, Brave, Edge, or Chromium on the system.
|
|
2052
|
+
* Creates a dedicated browser profile to avoid conflicts with your daily browser.
|
|
2053
|
+
*
|
|
2054
|
+
* @param opts - Launch options (headless, executablePath, cdpPort, etc.)
|
|
2055
|
+
* @returns A connected BrowserClaw instance
|
|
2056
|
+
*
|
|
2057
|
+
* @example
|
|
2058
|
+
* ```ts
|
|
2059
|
+
* // Default: visible Chrome window
|
|
2060
|
+
* const browser = await BrowserClaw.launch();
|
|
2061
|
+
*
|
|
2062
|
+
* // Headless mode
|
|
2063
|
+
* const browser = await BrowserClaw.launch({ headless: true });
|
|
2064
|
+
*
|
|
2065
|
+
* // Specific browser
|
|
2066
|
+
* const browser = await BrowserClaw.launch({
|
|
2067
|
+
* executablePath: '/usr/bin/google-chrome',
|
|
2068
|
+
* });
|
|
2069
|
+
* ```
|
|
2070
|
+
*/
|
|
2071
|
+
static async launch(opts = {}) {
|
|
2072
|
+
const chrome = await launchChrome(opts);
|
|
2073
|
+
const cdpUrl = `http://127.0.0.1:${chrome.cdpPort}`;
|
|
2074
|
+
return new _BrowserClaw(cdpUrl, chrome);
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Connect to an already-running Chrome instance via its CDP endpoint.
|
|
2078
|
+
*
|
|
2079
|
+
* The Chrome instance must have been started with `--remote-debugging-port`.
|
|
2080
|
+
*
|
|
2081
|
+
* @param cdpUrl - CDP endpoint URL (e.g. `'http://localhost:9222'`)
|
|
2082
|
+
* @returns A connected BrowserClaw instance
|
|
2083
|
+
*
|
|
2084
|
+
* @example
|
|
2085
|
+
* ```ts
|
|
2086
|
+
* // Chrome started with: chrome --remote-debugging-port=9222
|
|
2087
|
+
* const browser = await BrowserClaw.connect('http://localhost:9222');
|
|
2088
|
+
* ```
|
|
2089
|
+
*/
|
|
2090
|
+
static async connect(cdpUrl) {
|
|
2091
|
+
if (!await isChromeReachable(cdpUrl, 3e3)) {
|
|
2092
|
+
throw new Error(`Cannot connect to Chrome at ${cdpUrl}. Is Chrome running with --remote-debugging-port?`);
|
|
2093
|
+
}
|
|
2094
|
+
await connectBrowser(cdpUrl);
|
|
2095
|
+
return new _BrowserClaw(cdpUrl, null);
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Open a URL in a new tab and return the page handle.
|
|
2099
|
+
*
|
|
2100
|
+
* @param url - URL to navigate to
|
|
2101
|
+
* @returns A CrawlPage for the new tab
|
|
2102
|
+
*
|
|
2103
|
+
* @example
|
|
2104
|
+
* ```ts
|
|
2105
|
+
* const page = await browser.open('https://example.com');
|
|
2106
|
+
* const { snapshot, refs } = await page.snapshot();
|
|
2107
|
+
* ```
|
|
2108
|
+
*/
|
|
2109
|
+
async open(url) {
|
|
2110
|
+
const tab = await createPageViaPlaywright({ cdpUrl: this.cdpUrl, url });
|
|
2111
|
+
return new CrawlPage(this.cdpUrl, tab.targetId);
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Get a CrawlPage handle for the currently active tab.
|
|
2115
|
+
*
|
|
2116
|
+
* @returns CrawlPage for the first/active page
|
|
2117
|
+
*/
|
|
2118
|
+
async currentPage() {
|
|
2119
|
+
const { browser } = await connectBrowser(this.cdpUrl);
|
|
2120
|
+
const pages = await getAllPages(browser);
|
|
2121
|
+
if (!pages.length) throw new Error("No pages available");
|
|
2122
|
+
const tid = await pageTargetId(pages[0]).catch(() => null);
|
|
2123
|
+
return new CrawlPage(this.cdpUrl, tid ?? "");
|
|
2124
|
+
}
|
|
2125
|
+
/**
|
|
2126
|
+
* List all open tabs.
|
|
2127
|
+
*
|
|
2128
|
+
* @returns Array of tab information objects
|
|
2129
|
+
*/
|
|
2130
|
+
async tabs() {
|
|
2131
|
+
return listPagesViaPlaywright({ cdpUrl: this.cdpUrl });
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* Bring a tab to the foreground.
|
|
2135
|
+
*
|
|
2136
|
+
* @param targetId - CDP target ID of the tab (from `tabs()` or `page.id`)
|
|
2137
|
+
*/
|
|
2138
|
+
async focus(targetId) {
|
|
2139
|
+
return focusPageByTargetIdViaPlaywright({ cdpUrl: this.cdpUrl, targetId });
|
|
2140
|
+
}
|
|
2141
|
+
/**
|
|
2142
|
+
* Close a tab.
|
|
2143
|
+
*
|
|
2144
|
+
* @param targetId - CDP target ID of the tab to close
|
|
2145
|
+
*/
|
|
2146
|
+
async close(targetId) {
|
|
2147
|
+
return closePageByTargetIdViaPlaywright({ cdpUrl: this.cdpUrl, targetId });
|
|
2148
|
+
}
|
|
2149
|
+
/**
|
|
2150
|
+
* Get a CrawlPage handle for a specific tab by its target ID.
|
|
2151
|
+
*
|
|
2152
|
+
* Unlike `open()`, this doesn't create a new tab — it wraps an existing one.
|
|
2153
|
+
*
|
|
2154
|
+
* @param targetId - CDP target ID of the tab
|
|
2155
|
+
* @returns CrawlPage for the specified tab
|
|
2156
|
+
*/
|
|
2157
|
+
page(targetId) {
|
|
2158
|
+
return new CrawlPage(this.cdpUrl, targetId);
|
|
2159
|
+
}
|
|
2160
|
+
/** The CDP endpoint URL for this browser connection. */
|
|
2161
|
+
get url() {
|
|
2162
|
+
return this.cdpUrl;
|
|
2163
|
+
}
|
|
2164
|
+
/**
|
|
2165
|
+
* Stop the browser and clean up all resources.
|
|
2166
|
+
*
|
|
2167
|
+
* If the browser was launched by `BrowserClaw.launch()`, the Chrome process
|
|
2168
|
+
* will be terminated. If connected via `BrowserClaw.connect()`, only the
|
|
2169
|
+
* Playwright connection is closed.
|
|
2170
|
+
*/
|
|
2171
|
+
async stop() {
|
|
2172
|
+
await disconnectBrowser();
|
|
2173
|
+
if (this.chrome) {
|
|
2174
|
+
await stopChrome(this.chrome);
|
|
2175
|
+
this.chrome = null;
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
};
|
|
2179
|
+
|
|
2180
|
+
exports.BrowserClaw = BrowserClaw;
|
|
2181
|
+
exports.CrawlPage = CrawlPage;
|
|
2182
|
+
//# sourceMappingURL=index.cjs.map
|
|
2183
|
+
//# sourceMappingURL=index.cjs.map
|