cdp-toolkit 1.0.0 → 1.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.
- package/CONTRACT.md +1 -1
- package/README.md +34 -0
- package/dist/cli.js +3156 -1290
- package/dist/index.js +1116 -927
- package/dist/mcp.js +4472 -2626
- package/package.json +12 -6
- package/src/backend.ts +86 -0
- package/src/bidi/client.ts +319 -0
- package/src/bidi/driver.ts +823 -0
- package/src/bidi/launch.ts +192 -0
- package/src/bidi/protocol.ts +782 -0
- package/src/bidi/snapshot.ts +419 -0
- package/src/bidi-tools.ts +193 -0
- package/src/capabilities.ts +43 -0
- package/src/cdp/driver.ts +660 -0
- package/src/cli.ts +72 -14
- package/src/driver.ts +424 -0
- package/src/firefox-tools.ts +14 -0
- package/src/index.ts +49 -41
- package/src/mcp.ts +59 -9
- package/src/shared-tools.ts +420 -0
- package/src/tools/dialogs.ts +0 -236
- package/src/tools/emulation.ts +0 -212
- package/src/tools/input.ts +0 -399
- package/src/tools/pages.ts +0 -159
- package/src/tools/screenshot.ts +0 -180
package/dist/index.js
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
// src/tools/pages.ts
|
|
2
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname } from "node:path";
|
|
4
|
-
|
|
5
1
|
// src/client.ts
|
|
6
2
|
var BASE = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
7
3
|
var DEFAULT_TIMEOUT_MS = Number(process.env.CDP_TIMEOUT_MS ?? 15000);
|
|
@@ -205,191 +201,6 @@ async function withPage(selector, fn, opts = {}) {
|
|
|
205
201
|
}
|
|
206
202
|
}
|
|
207
203
|
|
|
208
|
-
// src/tools/pages.ts
|
|
209
|
-
var STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
210
|
-
var SELECTED_FILE = `${STATE_DIR}/selected`;
|
|
211
|
-
function toPageInfo(t) {
|
|
212
|
-
return { id: t.id, url: t.url, title: t.title, type: t.type };
|
|
213
|
-
}
|
|
214
|
-
async function listPages(args = {}) {
|
|
215
|
-
const targets = await listTargets();
|
|
216
|
-
const filtered = args.all ? targets : targets.filter((t) => t.type === "page");
|
|
217
|
-
const pages = filtered.map(toPageInfo);
|
|
218
|
-
return { pages, count: pages.length };
|
|
219
|
-
}
|
|
220
|
-
async function newPage(args = {}) {
|
|
221
|
-
const url = args.url ?? "about:blank";
|
|
222
|
-
const conn = await openBrowser();
|
|
223
|
-
try {
|
|
224
|
-
const { targetId } = await conn.send("Target.createTarget", { url });
|
|
225
|
-
if (!targetId)
|
|
226
|
-
throw new CdpError("Target.createTarget returned no targetId");
|
|
227
|
-
return { targetId, url };
|
|
228
|
-
} finally {
|
|
229
|
-
conn.close();
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
async function closePage(args) {
|
|
233
|
-
if (args.target === undefined || args.target === "") {
|
|
234
|
-
throw new CdpError("close_page requires an explicit target; refusing to guess which page to close");
|
|
235
|
-
}
|
|
236
|
-
const target = await resolveTarget(args.target);
|
|
237
|
-
const conn = await openBrowser();
|
|
238
|
-
try {
|
|
239
|
-
const res = await conn.send("Target.closeTarget", { targetId: target.id });
|
|
240
|
-
return { closed: target.id, success: res.success ?? true };
|
|
241
|
-
} finally {
|
|
242
|
-
conn.close();
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
async function selectPage(args) {
|
|
246
|
-
if (args.target === undefined || args.target === "") {
|
|
247
|
-
throw new CdpError("select_page requires an explicit target");
|
|
248
|
-
}
|
|
249
|
-
const target = await resolveTarget(args.target);
|
|
250
|
-
const conn = await openBrowser();
|
|
251
|
-
try {
|
|
252
|
-
await conn.send("Target.activateTarget", { targetId: target.id });
|
|
253
|
-
} finally {
|
|
254
|
-
conn.close();
|
|
255
|
-
}
|
|
256
|
-
await mkdir(dirname(SELECTED_FILE), { recursive: true });
|
|
257
|
-
await writeFile(SELECTED_FILE, target.id, "utf8");
|
|
258
|
-
return { selected: target.id };
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// src/tools/navigation.ts
|
|
262
|
-
async function navigatePage(args) {
|
|
263
|
-
const reload = args.reload === true;
|
|
264
|
-
if (!reload && (!args.url || typeof args.url !== "string")) {
|
|
265
|
-
throw new CdpError("navigate_page: 'url' is required (or pass reload:true to reload the current page)");
|
|
266
|
-
}
|
|
267
|
-
const waitUntil = args.waitUntil ?? "load";
|
|
268
|
-
return withPage(args.target, async (conn) => {
|
|
269
|
-
await conn.send("Page.enable");
|
|
270
|
-
const milestoneMethod = waitUntil === "load" ? "Page.loadEventFired" : "Page.domContentEventFired";
|
|
271
|
-
const milestone = conn.waitFor(milestoneMethod, undefined, args.timeoutMs).then(() => waitUntil).catch(() => {
|
|
272
|
-
return;
|
|
273
|
-
});
|
|
274
|
-
const stopped = conn.waitFor("Page.frameStoppedLoading", undefined, args.timeoutMs).then(() => "frameStoppedLoading").catch(() => {
|
|
275
|
-
return;
|
|
276
|
-
});
|
|
277
|
-
let resolvedUrl;
|
|
278
|
-
let frameId;
|
|
279
|
-
if (reload) {
|
|
280
|
-
await conn.send("Page.reload", { ignoreCache: args.ignoreCache === true }, args.timeoutMs ? { timeoutMs: args.timeoutMs } : {});
|
|
281
|
-
const tree = await conn.send("Page.getFrameTree").catch(() => {
|
|
282
|
-
return;
|
|
283
|
-
});
|
|
284
|
-
frameId = tree?.frameTree?.frame?.id ?? "";
|
|
285
|
-
resolvedUrl = tree?.frameTree?.frame?.url ?? args.url ?? "";
|
|
286
|
-
} else {
|
|
287
|
-
const nav = await conn.send("Page.navigate", { url: args.url }, args.timeoutMs ? { timeoutMs: args.timeoutMs } : {});
|
|
288
|
-
if (nav.errorText) {
|
|
289
|
-
throw new CdpError(`navigate_page: ${nav.errorText} (${args.url})`);
|
|
290
|
-
}
|
|
291
|
-
frameId = nav.frameId;
|
|
292
|
-
resolvedUrl = args.url;
|
|
293
|
-
}
|
|
294
|
-
const settled = await Promise.race([milestone, stopped]);
|
|
295
|
-
const waitedFor = settled ?? "navigate-only";
|
|
296
|
-
return reload ? { url: resolvedUrl, frameId, reloaded: true, waitedFor } : { url: resolvedUrl, frameId, waitedFor };
|
|
297
|
-
}, args.timeoutMs ? { timeoutMs: args.timeoutMs } : {});
|
|
298
|
-
}
|
|
299
|
-
var DEFAULT_WAIT_TIMEOUT_MS = 15000;
|
|
300
|
-
var DEFAULT_POLL_MS = 250;
|
|
301
|
-
async function waitForText(args) {
|
|
302
|
-
if (typeof args.text !== "string" || args.text.length === 0) {
|
|
303
|
-
throw new CdpError("wait_for: 'text' is required");
|
|
304
|
-
}
|
|
305
|
-
const timeoutMs = args.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
306
|
-
const pollMs = args.pollMs ?? DEFAULT_POLL_MS;
|
|
307
|
-
const start = Date.now();
|
|
308
|
-
return withPage(args.target, async (conn, _target) => {
|
|
309
|
-
await conn.send("Runtime.enable");
|
|
310
|
-
const expr = `(() => { const b = document.body; return !!b && typeof b.innerText === 'string' && b.innerText.includes(${JSON.stringify(args.text)}); })()`;
|
|
311
|
-
for (;; ) {
|
|
312
|
-
const { result } = await conn.send("Runtime.evaluate", {
|
|
313
|
-
expression: expr,
|
|
314
|
-
returnByValue: true
|
|
315
|
-
});
|
|
316
|
-
if (result?.value === true) {
|
|
317
|
-
return { found: true, waitedMs: Date.now() - start };
|
|
318
|
-
}
|
|
319
|
-
if (Date.now() - start >= timeoutMs) {
|
|
320
|
-
throw new CdpError(`wait_for: text not found within ${timeoutMs}ms: ${JSON.stringify(args.text)}`);
|
|
321
|
-
}
|
|
322
|
-
await delay(Math.min(pollMs, Math.max(0, timeoutMs - (Date.now() - start))));
|
|
323
|
-
}
|
|
324
|
-
}, { timeoutMs: Math.max(timeoutMs, DEFAULT_WAIT_TIMEOUT_MS) });
|
|
325
|
-
}
|
|
326
|
-
function delay(ms) {
|
|
327
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// src/tools/evaluate.ts
|
|
331
|
-
function exceptionMessage(details) {
|
|
332
|
-
const ex = details.exception;
|
|
333
|
-
const fromObject = ex?.description ?? (typeof ex?.value === "string" ? ex.value : ex?.value !== undefined ? JSON.stringify(ex.value) : undefined);
|
|
334
|
-
return fromObject ?? details.text ?? "evaluation threw";
|
|
335
|
-
}
|
|
336
|
-
function unwrap(obj) {
|
|
337
|
-
if (Object.prototype.hasOwnProperty.call(obj, "value"))
|
|
338
|
-
return obj.value;
|
|
339
|
-
if (obj.unserializableValue !== undefined)
|
|
340
|
-
return obj.unserializableValue;
|
|
341
|
-
if (obj.type === "undefined")
|
|
342
|
-
return;
|
|
343
|
-
return obj.description ?? null;
|
|
344
|
-
}
|
|
345
|
-
async function evaluateExpression(conn, expression, awaitPromise) {
|
|
346
|
-
const res = await conn.send("Runtime.evaluate", {
|
|
347
|
-
expression,
|
|
348
|
-
returnByValue: true,
|
|
349
|
-
awaitPromise,
|
|
350
|
-
replMode: false,
|
|
351
|
-
silent: false
|
|
352
|
-
});
|
|
353
|
-
if (res.exceptionDetails)
|
|
354
|
-
throw new CdpError(exceptionMessage(res.exceptionDetails));
|
|
355
|
-
return unwrap(res.result);
|
|
356
|
-
}
|
|
357
|
-
async function evaluateFunction(conn, expression, awaitPromise, args) {
|
|
358
|
-
const globalObj = await conn.send("Runtime.evaluate", {
|
|
359
|
-
expression: "globalThis",
|
|
360
|
-
returnByValue: false
|
|
361
|
-
});
|
|
362
|
-
const objectId = globalObj.result.objectId;
|
|
363
|
-
if (!objectId)
|
|
364
|
-
throw new CdpError("could not resolve global object for function call");
|
|
365
|
-
const res = await conn.send("Runtime.callFunctionOn", {
|
|
366
|
-
objectId,
|
|
367
|
-
functionDeclaration: expression,
|
|
368
|
-
arguments: args.map((value) => ({ value })),
|
|
369
|
-
returnByValue: true,
|
|
370
|
-
awaitPromise,
|
|
371
|
-
silent: false
|
|
372
|
-
});
|
|
373
|
-
if (res.exceptionDetails)
|
|
374
|
-
throw new CdpError(exceptionMessage(res.exceptionDetails));
|
|
375
|
-
return unwrap(res.result);
|
|
376
|
-
}
|
|
377
|
-
async function evaluateScript(args) {
|
|
378
|
-
const { expression } = args;
|
|
379
|
-
if (typeof expression !== "string" || expression.length === 0) {
|
|
380
|
-
throw new CdpError("evaluateScript: 'expression' must be a non-empty string");
|
|
381
|
-
}
|
|
382
|
-
const awaitPromise = args.awaitPromise ?? true;
|
|
383
|
-
const callArgs = args.args;
|
|
384
|
-
return withPage(args.target, async (conn) => {
|
|
385
|
-
await conn.send("Runtime.enable");
|
|
386
|
-
if (callArgs && callArgs.length > 0) {
|
|
387
|
-
return evaluateFunction(conn, expression, awaitPromise, callArgs);
|
|
388
|
-
}
|
|
389
|
-
return evaluateExpression(conn, expression, awaitPromise);
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
|
|
393
204
|
// src/tools/snapshot.ts
|
|
394
205
|
var INTERACTIVE_ROLES = new Set([
|
|
395
206
|
"button",
|
|
@@ -412,92 +223,6 @@ var INTERACTIVE_ROLES = new Set([
|
|
|
412
223
|
"MenuItem",
|
|
413
224
|
"Button"
|
|
414
225
|
]);
|
|
415
|
-
function axString(v) {
|
|
416
|
-
if (!v)
|
|
417
|
-
return;
|
|
418
|
-
const raw = v.value;
|
|
419
|
-
if (raw === undefined || raw === null)
|
|
420
|
-
return;
|
|
421
|
-
const s = String(raw).trim();
|
|
422
|
-
return s.length ? s : undefined;
|
|
423
|
-
}
|
|
424
|
-
function propValue(node, name) {
|
|
425
|
-
const p = node.properties?.find((x) => x.name === name);
|
|
426
|
-
return axString(p?.value);
|
|
427
|
-
}
|
|
428
|
-
function isInteractive(role) {
|
|
429
|
-
if (!role)
|
|
430
|
-
return false;
|
|
431
|
-
return INTERACTIVE_ROLES.has(role) || INTERACTIVE_ROLES.has(role.toLowerCase());
|
|
432
|
-
}
|
|
433
|
-
function formatNode(node, interactiveOnly) {
|
|
434
|
-
const role = axString(node.role);
|
|
435
|
-
const name = axString(node.name);
|
|
436
|
-
const uid = node.backendDOMNodeId;
|
|
437
|
-
if (uid === undefined)
|
|
438
|
-
return;
|
|
439
|
-
if (interactiveOnly && !isInteractive(role))
|
|
440
|
-
return;
|
|
441
|
-
if (!interactiveOnly && !role && !name)
|
|
442
|
-
return;
|
|
443
|
-
const extras = [];
|
|
444
|
-
const value = axString(node.value);
|
|
445
|
-
if (value)
|
|
446
|
-
extras.push(`value=${JSON.stringify(value)}`);
|
|
447
|
-
const checked = propValue(node, "checked");
|
|
448
|
-
if (checked && checked !== "false")
|
|
449
|
-
extras.push(`checked=${checked}`);
|
|
450
|
-
const expanded = propValue(node, "expanded");
|
|
451
|
-
if (expanded)
|
|
452
|
-
extras.push(`expanded=${expanded}`);
|
|
453
|
-
const disabled = propValue(node, "disabled");
|
|
454
|
-
if (disabled === "true")
|
|
455
|
-
extras.push("disabled");
|
|
456
|
-
const url = propValue(node, "url");
|
|
457
|
-
if (url)
|
|
458
|
-
extras.push(`url=${url}`);
|
|
459
|
-
const focused = propValue(node, "focused");
|
|
460
|
-
if (focused === "true")
|
|
461
|
-
extras.push("focused");
|
|
462
|
-
const label = name ? ` ${JSON.stringify(name)}` : "";
|
|
463
|
-
const extra = extras.length ? ` [${extras.join(" ")}]` : "";
|
|
464
|
-
return `[${uid}] ${role ?? "generic"}${label}${extra}`;
|
|
465
|
-
}
|
|
466
|
-
async function takeSnapshot(args = {}) {
|
|
467
|
-
const interactiveOnly = args.interactiveOnly ?? false;
|
|
468
|
-
return withPage(args.target, async (conn, target) => {
|
|
469
|
-
await conn.send("Accessibility.enable");
|
|
470
|
-
const { nodes } = await conn.send("Accessibility.getFullAXTree");
|
|
471
|
-
const byId = new Map;
|
|
472
|
-
for (const n of nodes)
|
|
473
|
-
byId.set(n.nodeId, n);
|
|
474
|
-
const roots = nodes.filter((n) => !n.parentId || !byId.has(n.parentId));
|
|
475
|
-
const lines = [];
|
|
476
|
-
let nodeCount = 0;
|
|
477
|
-
const walk = (node, depth) => {
|
|
478
|
-
const line = node.ignored ? undefined : formatNode(node, interactiveOnly);
|
|
479
|
-
if (line !== undefined) {
|
|
480
|
-
const indent = interactiveOnly ? "" : " ".repeat(depth);
|
|
481
|
-
lines.push(`${indent}${line}`);
|
|
482
|
-
nodeCount++;
|
|
483
|
-
}
|
|
484
|
-
const childDepth = line !== undefined && !interactiveOnly ? depth + 1 : depth;
|
|
485
|
-
for (const childId of node.childIds ?? []) {
|
|
486
|
-
const child = byId.get(childId);
|
|
487
|
-
if (child)
|
|
488
|
-
walk(child, childDepth);
|
|
489
|
-
}
|
|
490
|
-
};
|
|
491
|
-
for (const root of roots)
|
|
492
|
-
walk(root, 0);
|
|
493
|
-
return {
|
|
494
|
-
snapshot: lines.join(`
|
|
495
|
-
`),
|
|
496
|
-
target: { id: target.id, url: target.url, title: target.title },
|
|
497
|
-
nodeCount
|
|
498
|
-
};
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
226
|
async function resolveUid(conn, uid) {
|
|
502
227
|
const { object } = await conn.send("DOM.resolveNode", {
|
|
503
228
|
backendNodeId: uid
|
|
@@ -508,409 +233,670 @@ async function resolveUid(conn, uid) {
|
|
|
508
233
|
return { objectId: object.objectId };
|
|
509
234
|
}
|
|
510
235
|
|
|
511
|
-
// src/tools/
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const hasSelector = typeof t.selector === "string" && t.selector.length > 0;
|
|
515
|
-
if (hasUid === hasSelector) {
|
|
516
|
-
throw new CdpError("provide exactly one of { uid } or { selector }");
|
|
517
|
-
}
|
|
518
|
-
if (hasUid) {
|
|
519
|
-
return resolveUid(conn, t.uid);
|
|
520
|
-
}
|
|
521
|
-
const { result, exceptionDetails } = await conn.send("Runtime.evaluate", {
|
|
522
|
-
expression: `document.querySelector(${JSON.stringify(t.selector)})`,
|
|
523
|
-
returnByValue: false
|
|
524
|
-
});
|
|
525
|
-
if (exceptionDetails) {
|
|
526
|
-
throw new CdpError(`selector '${t.selector}' evaluation failed: ${exceptionDetails.text ?? "error"}`);
|
|
527
|
-
}
|
|
528
|
-
if (!result.objectId || result.subtype === "null") {
|
|
529
|
-
throw new CdpError(`selector '${t.selector}' matched no element`);
|
|
530
|
-
}
|
|
531
|
-
return { objectId: result.objectId };
|
|
236
|
+
// src/tools/network_mock.ts
|
|
237
|
+
function escapeRegExpChar(ch) {
|
|
238
|
+
return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
|
|
532
239
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
240
|
+
function globToRegExp(pattern) {
|
|
241
|
+
let re = "";
|
|
242
|
+
for (let i = 0;i < pattern.length; i++) {
|
|
243
|
+
const ch = pattern[i];
|
|
244
|
+
if (ch === "\\") {
|
|
245
|
+
const next = pattern[i + 1];
|
|
246
|
+
if (next !== undefined) {
|
|
247
|
+
re += escapeRegExpChar(next);
|
|
248
|
+
i++;
|
|
249
|
+
} else {
|
|
250
|
+
re += "\\\\";
|
|
251
|
+
}
|
|
252
|
+
} else if (ch === "*") {
|
|
253
|
+
re += ".*";
|
|
254
|
+
} else if (ch === "?") {
|
|
255
|
+
re += ".";
|
|
256
|
+
} else {
|
|
257
|
+
re += escapeRegExpChar(ch);
|
|
258
|
+
}
|
|
544
259
|
}
|
|
545
|
-
return
|
|
546
|
-
}
|
|
547
|
-
async function focusElement(conn, objectId) {
|
|
548
|
-
await conn.send("Runtime.callFunctionOn", {
|
|
549
|
-
objectId,
|
|
550
|
-
functionDeclaration: "function(){this.focus&&this.focus();}",
|
|
551
|
-
returnByValue: true
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
async function click(args) {
|
|
555
|
-
const button = args.button ?? "left";
|
|
556
|
-
const clickCount = args.clickCount ?? 1;
|
|
557
|
-
return withPage(args.target, async (conn) => {
|
|
558
|
-
const { objectId } = await resolveElement(conn, args);
|
|
559
|
-
const { x, y } = await centerOf(conn, objectId);
|
|
560
|
-
await conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y });
|
|
561
|
-
await conn.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button, clickCount });
|
|
562
|
-
await conn.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button, clickCount });
|
|
563
|
-
return { clicked: true, x, y };
|
|
564
|
-
});
|
|
260
|
+
return new RegExp(`^${re}$`);
|
|
565
261
|
}
|
|
566
|
-
|
|
567
|
-
return
|
|
568
|
-
const { objectId } = await resolveElement(conn, args);
|
|
569
|
-
const { x, y } = await centerOf(conn, objectId);
|
|
570
|
-
await conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y });
|
|
571
|
-
return { hovered: true, x, y };
|
|
572
|
-
});
|
|
262
|
+
function urlMatches(pattern, url) {
|
|
263
|
+
return globToRegExp(pattern).test(url);
|
|
573
264
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
throw new CdpError("drag requires { from } and { to }");
|
|
577
|
-
return withPage(args.target, async (conn) => {
|
|
578
|
-
const { objectId: srcId } = await resolveElement(conn, args.from);
|
|
579
|
-
const from = await centerOf(conn, srcId);
|
|
580
|
-
const { objectId: dstId } = await resolveElement(conn, args.to);
|
|
581
|
-
const to = await centerOf(conn, dstId);
|
|
582
|
-
await conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: from.x, y: from.y });
|
|
583
|
-
await conn.send("Input.dispatchMouseEvent", {
|
|
584
|
-
type: "mousePressed",
|
|
585
|
-
x: from.x,
|
|
586
|
-
y: from.y,
|
|
587
|
-
button: "left",
|
|
588
|
-
clickCount: 1
|
|
589
|
-
});
|
|
590
|
-
await conn.send("Input.dispatchMouseEvent", {
|
|
591
|
-
type: "mouseMoved",
|
|
592
|
-
x: (from.x + to.x) / 2,
|
|
593
|
-
y: (from.y + to.y) / 2,
|
|
594
|
-
button: "left"
|
|
595
|
-
});
|
|
596
|
-
await conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: to.x, y: to.y, button: "left" });
|
|
597
|
-
await conn.send("Input.dispatchMouseEvent", {
|
|
598
|
-
type: "mouseReleased",
|
|
599
|
-
x: to.x,
|
|
600
|
-
y: to.y,
|
|
601
|
-
button: "left",
|
|
602
|
-
clickCount: 1
|
|
603
|
-
});
|
|
604
|
-
return { dragged: true, from, to };
|
|
605
|
-
});
|
|
265
|
+
function selectRule(rules, url, method) {
|
|
266
|
+
return rules.find((r) => urlMatches(r.urlPattern, url) && (!r.method || r.method.toUpperCase() === method.toUpperCase()));
|
|
606
267
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
returnByValue: true
|
|
613
|
-
});
|
|
614
|
-
if (value.length) {
|
|
615
|
-
await conn.send("Input.insertText", { text: value });
|
|
268
|
+
function buildFulfillParams(requestId, rule) {
|
|
269
|
+
const merged = new Map;
|
|
270
|
+
merged.set("content-type", { name: "Content-Type", value: rule.contentType ?? "application/json" });
|
|
271
|
+
for (const [name, value] of Object.entries(rule.headers ?? {})) {
|
|
272
|
+
merged.set(name.toLowerCase(), { name, value });
|
|
616
273
|
}
|
|
274
|
+
return {
|
|
275
|
+
requestId,
|
|
276
|
+
responseCode: rule.status ?? 200,
|
|
277
|
+
responseHeaders: [...merged.values()],
|
|
278
|
+
body: Buffer.from(rule.body ?? "", "utf8").toString("base64")
|
|
279
|
+
};
|
|
617
280
|
}
|
|
618
|
-
|
|
619
|
-
if (
|
|
620
|
-
|
|
621
|
-
return
|
|
622
|
-
const { objectId } = await resolveElement(conn, args);
|
|
623
|
-
await setValue(conn, objectId, args.value);
|
|
624
|
-
return { filled: true, value: args.value };
|
|
625
|
-
});
|
|
281
|
+
function effectiveAction(rule, rnd) {
|
|
282
|
+
if (rule.failRate !== undefined && rule.failRate > 0 && rnd < rule.failRate)
|
|
283
|
+
return "fail";
|
|
284
|
+
return rule.action;
|
|
626
285
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
return withPage(args.target, async (conn) => {
|
|
631
|
-
const { objectId } = await resolveElement(conn, args);
|
|
632
|
-
await focusElement(conn, objectId);
|
|
633
|
-
if (args.text.length)
|
|
634
|
-
await conn.send("Input.insertText", { text: args.text });
|
|
635
|
-
return { typed: true, text: args.text };
|
|
636
|
-
});
|
|
286
|
+
var sessions = new Map;
|
|
287
|
+
function delay(ms) {
|
|
288
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
637
289
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
throw new CdpError("each fill_form field requires a string value");
|
|
290
|
+
function registerHandler(session) {
|
|
291
|
+
session.conn.on("Fetch.requestPaused", (params) => {
|
|
292
|
+
const p = params;
|
|
293
|
+
session.chain = session.chain.then(async () => {
|
|
294
|
+
const rule = selectRule(session.rules, p.request.url, p.request.method);
|
|
295
|
+
if (!rule) {
|
|
296
|
+
await session.conn.send("Fetch.continueRequest", { requestId: p.requestId });
|
|
297
|
+
return;
|
|
647
298
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
space: { key: " ", code: "Space", keyCode: 32, text: " " },
|
|
664
|
-
arrowup: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
|
|
665
|
-
arrowdown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
|
|
666
|
-
arrowleft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
|
|
667
|
-
arrowright: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 },
|
|
668
|
-
up: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
|
|
669
|
-
down: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
|
|
670
|
-
left: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
|
|
671
|
-
right: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 },
|
|
672
|
-
home: { key: "Home", code: "Home", keyCode: 36 },
|
|
673
|
-
end: { key: "End", code: "End", keyCode: 35 },
|
|
674
|
-
pageup: { key: "PageUp", code: "PageUp", keyCode: 33 },
|
|
675
|
-
pagedown: { key: "PageDown", code: "PageDown", keyCode: 34 }
|
|
676
|
-
};
|
|
677
|
-
var MODIFIER_BITS = {
|
|
678
|
-
alt: 1,
|
|
679
|
-
control: 2,
|
|
680
|
-
ctrl: 2,
|
|
681
|
-
meta: 4,
|
|
682
|
-
cmd: 4,
|
|
683
|
-
command: 4,
|
|
684
|
-
shift: 8
|
|
685
|
-
};
|
|
686
|
-
function resolveKey(key) {
|
|
687
|
-
const named = NAMED_KEYS[key.toLowerCase()];
|
|
688
|
-
if (named)
|
|
689
|
-
return named;
|
|
690
|
-
if (key.length === 1) {
|
|
691
|
-
const upper = key.toUpperCase();
|
|
692
|
-
const isLetter = upper >= "A" && upper <= "Z";
|
|
693
|
-
const isDigit = key >= "0" && key <= "9";
|
|
694
|
-
const code = isLetter ? `Key${upper}` : isDigit ? `Digit${key}` : `Key${upper}`;
|
|
695
|
-
const keyCode = upper.charCodeAt(0);
|
|
696
|
-
return { key, code, keyCode, text: key };
|
|
697
|
-
}
|
|
698
|
-
throw new CdpError(`unknown key '${key}' (use a named key like Enter/Tab/ArrowDown or a single character)`);
|
|
699
|
-
}
|
|
700
|
-
async function pressKey(args) {
|
|
701
|
-
if (typeof args.key !== "string" || !args.key.length) {
|
|
702
|
-
throw new CdpError("press_key requires a non-empty { key }");
|
|
703
|
-
}
|
|
704
|
-
const spec = resolveKey(args.key);
|
|
705
|
-
let modBits = 0;
|
|
706
|
-
const mods = args.modifiers ?? [];
|
|
707
|
-
for (const m of mods) {
|
|
708
|
-
const bit = MODIFIER_BITS[m.toLowerCase()];
|
|
709
|
-
if (bit === undefined)
|
|
710
|
-
throw new CdpError(`unknown modifier '${m}'`);
|
|
711
|
-
modBits |= bit;
|
|
712
|
-
}
|
|
713
|
-
const suppressText = (modBits & ~MODIFIER_BITS.shift) !== 0;
|
|
714
|
-
return withPage(args.target, async (conn) => {
|
|
715
|
-
const down = {
|
|
716
|
-
type: "keyDown",
|
|
717
|
-
key: spec.key,
|
|
718
|
-
code: spec.code,
|
|
719
|
-
windowsVirtualKeyCode: spec.keyCode,
|
|
720
|
-
nativeVirtualKeyCode: spec.keyCode,
|
|
721
|
-
modifiers: modBits
|
|
722
|
-
};
|
|
723
|
-
if (spec.text && !suppressText) {
|
|
724
|
-
down.text = spec.text;
|
|
725
|
-
}
|
|
726
|
-
await conn.send("Input.dispatchKeyEvent", down);
|
|
727
|
-
await conn.send("Input.dispatchKeyEvent", {
|
|
728
|
-
type: "keyUp",
|
|
729
|
-
key: spec.key,
|
|
730
|
-
code: spec.code,
|
|
731
|
-
windowsVirtualKeyCode: spec.keyCode,
|
|
732
|
-
nativeVirtualKeyCode: spec.keyCode,
|
|
733
|
-
modifiers: modBits
|
|
299
|
+
if (rule.delayMs)
|
|
300
|
+
await delay(rule.delayMs);
|
|
301
|
+
const act = effectiveAction(rule, Math.random());
|
|
302
|
+
if (act === "fulfill") {
|
|
303
|
+
await session.conn.send("Fetch.fulfillRequest", { ...buildFulfillParams(p.requestId, rule) });
|
|
304
|
+
} else if (act === "fail") {
|
|
305
|
+
await session.conn.send("Fetch.failRequest", { requestId: p.requestId, errorReason: rule.errorReason ?? "Failed" });
|
|
306
|
+
} else {
|
|
307
|
+
await session.conn.send("Fetch.continueRequest", { requestId: p.requestId });
|
|
308
|
+
}
|
|
309
|
+
rule.hits++;
|
|
310
|
+
session.intercepted.push({ url: p.request.url, method: p.request.method, action: act });
|
|
311
|
+
}).catch((e) => {
|
|
312
|
+
if (/closed|connection|Target|Inspected/i.test(String(e)))
|
|
313
|
+
session.dead = true;
|
|
734
314
|
});
|
|
735
|
-
return { pressed: spec.key, modifiers: mods };
|
|
736
|
-
});
|
|
737
|
-
}
|
|
738
|
-
async function uploadFile(args) {
|
|
739
|
-
const files = Array.isArray(args.files) ? args.files : [args.files];
|
|
740
|
-
if (!files.length || files.some((f) => typeof f !== "string" || !f.length)) {
|
|
741
|
-
throw new CdpError("upload_file requires { files } as a non-empty path or array of paths");
|
|
742
|
-
}
|
|
743
|
-
return withPage(args.target, async (conn) => {
|
|
744
|
-
const { objectId } = await resolveElement(conn, args);
|
|
745
|
-
await conn.send("DOM.setFileInputFiles", { files, objectId });
|
|
746
|
-
return { uploaded: files };
|
|
747
315
|
});
|
|
748
316
|
}
|
|
317
|
+
async function mockRequest(args) {
|
|
318
|
+
if (!args.urlPattern)
|
|
319
|
+
throw new CdpError("mock_request requires a { urlPattern }");
|
|
320
|
+
const rule = {
|
|
321
|
+
urlPattern: args.urlPattern,
|
|
322
|
+
action: args.action ?? "fulfill",
|
|
323
|
+
status: args.status,
|
|
324
|
+
body: args.body,
|
|
325
|
+
contentType: args.contentType,
|
|
326
|
+
headers: args.headers,
|
|
327
|
+
errorReason: args.errorReason,
|
|
328
|
+
method: args.method,
|
|
329
|
+
delayMs: args.delayMs,
|
|
330
|
+
failRate: args.failRate,
|
|
331
|
+
hits: 0
|
|
332
|
+
};
|
|
333
|
+
const resolved = await resolveTarget(args.target);
|
|
334
|
+
let session = sessions.get(resolved.id);
|
|
335
|
+
if (session && !session.dead) {
|
|
336
|
+
const idx = session.rules.findIndex((r) => r.urlPattern === rule.urlPattern && (r.method ?? "") === (rule.method ?? ""));
|
|
337
|
+
if (idx >= 0)
|
|
338
|
+
session.rules[idx] = rule;
|
|
339
|
+
else
|
|
340
|
+
session.rules.push(rule);
|
|
341
|
+
await session.conn.send("Fetch.enable", { patterns: session.rules.map((r) => ({ urlPattern: r.urlPattern })) });
|
|
342
|
+
} else {
|
|
343
|
+
const { conn, target } = await openPage(resolved.id);
|
|
344
|
+
session = { conn, target, rules: [rule], intercepted: [], chain: Promise.resolve(), dead: false };
|
|
345
|
+
registerHandler(session);
|
|
346
|
+
await conn.send("Fetch.enable", { patterns: [{ urlPattern: rule.urlPattern }] });
|
|
347
|
+
sessions.set(target.id, session);
|
|
348
|
+
}
|
|
349
|
+
let reloaded = false;
|
|
350
|
+
if (args.reload) {
|
|
351
|
+
await session.conn.send("Page.enable");
|
|
352
|
+
await session.conn.send("Page.reload", { ignoreCache: true });
|
|
353
|
+
await delay(800);
|
|
354
|
+
reloaded = true;
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
target: { id: session.target.id, url: session.target.url, title: session.target.title },
|
|
358
|
+
pattern: rule.urlPattern,
|
|
359
|
+
ruleCount: session.rules.length,
|
|
360
|
+
reloaded
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
async function listMocks(_args = {}) {
|
|
364
|
+
for (const [id, s] of sessions) {
|
|
365
|
+
if (s.dead) {
|
|
366
|
+
try {
|
|
367
|
+
s.conn.close();
|
|
368
|
+
} catch {}
|
|
369
|
+
sessions.delete(id);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
await s.conn.send("Runtime.evaluate", { expression: "1", returnByValue: true }, { timeoutMs: 3000 });
|
|
374
|
+
} catch {
|
|
375
|
+
try {
|
|
376
|
+
s.conn.close();
|
|
377
|
+
} catch {}
|
|
378
|
+
sessions.delete(id);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const mocks = [...sessions.values()].map((s) => ({
|
|
382
|
+
target: { id: s.target.id, url: s.target.url, title: s.target.title },
|
|
383
|
+
rules: s.rules.map((r) => ({ urlPattern: r.urlPattern, action: r.action, method: r.method, hits: r.hits })),
|
|
384
|
+
hits: s.intercepted.length
|
|
385
|
+
}));
|
|
386
|
+
return { count: mocks.length, mocks };
|
|
387
|
+
}
|
|
388
|
+
async function clearMocks(args = {}) {
|
|
389
|
+
let ids;
|
|
390
|
+
if (args.all) {
|
|
391
|
+
ids = [...sessions.keys()];
|
|
392
|
+
} else {
|
|
393
|
+
const resolved = await resolveTarget(args.target);
|
|
394
|
+
ids = sessions.has(resolved.id) ? [resolved.id] : [];
|
|
395
|
+
}
|
|
396
|
+
let cleared = 0;
|
|
397
|
+
for (const id of ids) {
|
|
398
|
+
const s = sessions.get(id);
|
|
399
|
+
if (!s)
|
|
400
|
+
continue;
|
|
401
|
+
try {
|
|
402
|
+
await s.conn.send("Fetch.disable");
|
|
403
|
+
} catch {}
|
|
404
|
+
try {
|
|
405
|
+
s.conn.close();
|
|
406
|
+
} catch {}
|
|
407
|
+
sessions.delete(id);
|
|
408
|
+
cleared++;
|
|
409
|
+
}
|
|
410
|
+
return { cleared };
|
|
411
|
+
}
|
|
749
412
|
|
|
750
|
-
// src/
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
var
|
|
754
|
-
|
|
755
|
-
|
|
413
|
+
// src/driver.ts
|
|
414
|
+
var UID_STAMP_ATTR = "data-cdp-uid";
|
|
415
|
+
var LEGACY_NUMERIC_UID = /^[0-9]+$/;
|
|
416
|
+
var REQUIRED_CAPABILITIES = {
|
|
417
|
+
performance_start_trace: ["trace.performance"],
|
|
418
|
+
performance_stop_trace: ["trace.performance"],
|
|
419
|
+
performance_analyze_insight: ["trace.performance"],
|
|
420
|
+
performance_trace: ["trace.performance"],
|
|
421
|
+
take_heapsnapshot: ["heap.snapshot"],
|
|
422
|
+
lighthouse_audit: ["audit.lighthouse"],
|
|
423
|
+
mock_request: ["network.intercept"],
|
|
424
|
+
list_mocks: ["network.intercept"],
|
|
425
|
+
clear_mocks: ["network.intercept"]
|
|
426
|
+
};
|
|
427
|
+
function isDriverError(e) {
|
|
428
|
+
return e instanceof Error && typeof e.code === "string";
|
|
756
429
|
}
|
|
757
|
-
|
|
758
|
-
|
|
430
|
+
|
|
431
|
+
// src/cdp/driver.ts
|
|
432
|
+
function driverError(code, message, data) {
|
|
433
|
+
return Object.assign(new Error(message), { code, ...data !== undefined ? { data } : {} });
|
|
434
|
+
}
|
|
435
|
+
function encodeUid(backendNodeId) {
|
|
436
|
+
return `cdp:${backendNodeId}`;
|
|
437
|
+
}
|
|
438
|
+
function decodeUid(uid) {
|
|
439
|
+
if (LEGACY_NUMERIC_UID.test(uid))
|
|
440
|
+
return Number(uid);
|
|
441
|
+
if (!uid.startsWith("cdp:"))
|
|
442
|
+
throw driverError("foreign-uid", `uid is not owned by the cdp driver: ${uid}`);
|
|
443
|
+
const n = Number(uid.slice(4));
|
|
444
|
+
if (!Number.isSafeInteger(n) || n <= 0)
|
|
445
|
+
throw driverError("foreign-uid", `malformed cdp uid payload: ${uid}`);
|
|
446
|
+
return n;
|
|
447
|
+
}
|
|
448
|
+
async function centerOf(conn, objectId) {
|
|
449
|
+
const fn = "function(){this.scrollIntoView({block:'center',inline:'center'});const r=this.getBoundingClientRect();if(r.width===0&&r.height===0)return null;return {x:r.left+r.width/2,y:r.top+r.height/2};}";
|
|
450
|
+
const { result, exceptionDetails } = await conn.send("Runtime.callFunctionOn", { objectId, functionDeclaration: fn, returnByValue: true });
|
|
451
|
+
if (exceptionDetails)
|
|
452
|
+
throw driverError("page-error", `could not measure element: ${exceptionDetails.text ?? "error"}`);
|
|
453
|
+
if (!result.value)
|
|
454
|
+
throw driverError("page-error", "element has zero size / is not visible; cannot compute a click point");
|
|
455
|
+
return result.value;
|
|
456
|
+
}
|
|
457
|
+
async function focusElement(conn, objectId) {
|
|
458
|
+
await conn.send("Runtime.callFunctionOn", { objectId, functionDeclaration: "function(){this.focus&&this.focus();}", returnByValue: true });
|
|
459
|
+
}
|
|
460
|
+
async function setValueOnObject(conn, objectId, value) {
|
|
461
|
+
await focusElement(conn, objectId);
|
|
462
|
+
const fn = "function(){if('value'in this){this.value='';this.dispatchEvent(new Event('input',{bubbles:true}));}else if(this.isContentEditable){this.textContent='';}}";
|
|
463
|
+
await conn.send("Runtime.callFunctionOn", { objectId, functionDeclaration: fn, returnByValue: true });
|
|
464
|
+
if (value.length)
|
|
465
|
+
await conn.send("Input.insertText", { text: value });
|
|
466
|
+
}
|
|
467
|
+
var NAMED_KEYS = {
|
|
468
|
+
enter: { key: "Enter", code: "Enter", keyCode: 13, text: "\r" },
|
|
469
|
+
return: { key: "Enter", code: "Enter", keyCode: 13, text: "\r" },
|
|
470
|
+
tab: { key: "Tab", code: "Tab", keyCode: 9 },
|
|
471
|
+
escape: { key: "Escape", code: "Escape", keyCode: 27 },
|
|
472
|
+
esc: { key: "Escape", code: "Escape", keyCode: 27 },
|
|
473
|
+
backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
|
|
474
|
+
delete: { key: "Delete", code: "Delete", keyCode: 46 },
|
|
475
|
+
space: { key: " ", code: "Space", keyCode: 32, text: " " },
|
|
476
|
+
arrowup: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
|
|
477
|
+
arrowdown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
|
|
478
|
+
arrowleft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
|
|
479
|
+
arrowright: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 },
|
|
480
|
+
up: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
|
|
481
|
+
down: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
|
|
482
|
+
left: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
|
|
483
|
+
right: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 },
|
|
484
|
+
home: { key: "Home", code: "Home", keyCode: 36 },
|
|
485
|
+
end: { key: "End", code: "End", keyCode: 35 },
|
|
486
|
+
pageup: { key: "PageUp", code: "PageUp", keyCode: 33 },
|
|
487
|
+
pagedown: { key: "PageDown", code: "PageDown", keyCode: 34 }
|
|
488
|
+
};
|
|
489
|
+
var MODIFIER_BITS = { alt: 1, control: 2, ctrl: 2, meta: 4, cmd: 4, command: 4, shift: 8 };
|
|
490
|
+
function resolveKey(key) {
|
|
491
|
+
const named = NAMED_KEYS[key.toLowerCase()];
|
|
492
|
+
if (named)
|
|
493
|
+
return named;
|
|
494
|
+
if (key.length === 1) {
|
|
495
|
+
const upper = key.toUpperCase();
|
|
496
|
+
const isLetter = upper >= "A" && upper <= "Z";
|
|
497
|
+
const isDigit = key >= "0" && key <= "9";
|
|
498
|
+
const code = isLetter ? `Key${upper}` : isDigit ? `Digit${key}` : `Key${upper}`;
|
|
499
|
+
return { key, code, keyCode: upper.charCodeAt(0), text: key };
|
|
500
|
+
}
|
|
501
|
+
throw driverError("page-error", `unknown key '${key}' (use a named key like Enter/Tab/ArrowDown or a single character)`);
|
|
502
|
+
}
|
|
503
|
+
function unwrap(obj) {
|
|
504
|
+
if (Object.prototype.hasOwnProperty.call(obj, "value"))
|
|
505
|
+
return obj.value;
|
|
506
|
+
if (obj.unserializableValue !== undefined)
|
|
507
|
+
return obj.unserializableValue;
|
|
508
|
+
return obj.type === "undefined" ? undefined : obj.description ?? null;
|
|
509
|
+
}
|
|
510
|
+
function exceptionMessage(details) {
|
|
511
|
+
const ex = details.exception;
|
|
512
|
+
const fromObject = ex?.description ?? (typeof ex?.value === "string" ? ex.value : ex?.value !== undefined ? JSON.stringify(ex.value) : undefined);
|
|
513
|
+
return fromObject ?? details.text ?? "evaluation threw";
|
|
514
|
+
}
|
|
515
|
+
var INTERACTIVE_ROLES2 = new Set([
|
|
516
|
+
"button",
|
|
517
|
+
"link",
|
|
518
|
+
"textbox",
|
|
519
|
+
"searchbox",
|
|
520
|
+
"checkbox",
|
|
521
|
+
"radio",
|
|
522
|
+
"combobox",
|
|
523
|
+
"listbox",
|
|
524
|
+
"option",
|
|
525
|
+
"menuitem",
|
|
526
|
+
"menuitemcheckbox",
|
|
527
|
+
"menuitemradio",
|
|
528
|
+
"tab",
|
|
529
|
+
"switch",
|
|
530
|
+
"slider",
|
|
531
|
+
"spinbutton",
|
|
532
|
+
"textfield"
|
|
533
|
+
]);
|
|
534
|
+
function axString(v) {
|
|
535
|
+
if (!v || v.value === undefined || v.value === null)
|
|
536
|
+
return;
|
|
537
|
+
const s = String(v.value).trim();
|
|
538
|
+
return s.length ? s : undefined;
|
|
539
|
+
}
|
|
540
|
+
function propValue(node, name) {
|
|
541
|
+
return axString(node.properties?.find((p) => p.name === name)?.value);
|
|
542
|
+
}
|
|
543
|
+
function isInteractive(role) {
|
|
544
|
+
return !!role && (INTERACTIVE_ROLES2.has(role) || INTERACTIVE_ROLES2.has(role.toLowerCase()));
|
|
545
|
+
}
|
|
546
|
+
function axExtras(node) {
|
|
547
|
+
const extras = {};
|
|
548
|
+
const value = axString(node.value);
|
|
549
|
+
if (value)
|
|
550
|
+
extras.value = value;
|
|
551
|
+
const checked = propValue(node, "checked");
|
|
552
|
+
if (checked && checked !== "false")
|
|
553
|
+
extras.checked = checked;
|
|
554
|
+
const expanded = propValue(node, "expanded");
|
|
555
|
+
if (expanded)
|
|
556
|
+
extras.expanded = expanded;
|
|
557
|
+
if (propValue(node, "disabled") === "true")
|
|
558
|
+
extras.disabled = "true";
|
|
559
|
+
const url = propValue(node, "url");
|
|
560
|
+
if (url)
|
|
561
|
+
extras.url = url;
|
|
562
|
+
if (propValue(node, "focused") === "true")
|
|
563
|
+
extras.focused = "true";
|
|
564
|
+
return extras;
|
|
565
|
+
}
|
|
566
|
+
async function resolveElementLocator(conn, loc) {
|
|
567
|
+
if ("uid" in loc) {
|
|
568
|
+
return resolveUid(conn, decodeUid(loc.uid)).catch(() => {
|
|
569
|
+
throw driverError("stale-uid", `uid does not resolve: ${loc.uid}`);
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
if ("css" in loc) {
|
|
573
|
+
const { result, exceptionDetails } = await conn.send("Runtime.evaluate", { expression: `document.querySelector(${JSON.stringify(loc.css)})`, returnByValue: false });
|
|
574
|
+
if (exceptionDetails)
|
|
575
|
+
throw driverError("page-error", `css '${loc.css}' evaluation failed: ${exceptionDetails.text ?? "error"}`);
|
|
576
|
+
if (!result.objectId || result.subtype === "null")
|
|
577
|
+
throw driverError("no-such-element", `css '${loc.css}' matched no element`);
|
|
578
|
+
return { objectId: result.objectId };
|
|
579
|
+
}
|
|
580
|
+
const backendNodeId = await searchLocate(conn, "xpath" in loc ? loc.xpath : loc.text);
|
|
581
|
+
return resolveUid(conn, backendNodeId).catch(() => {
|
|
582
|
+
throw driverError("stale-uid", "text/xpath match no longer resolves");
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
async function searchLocate(conn, query) {
|
|
586
|
+
await conn.send("DOM.getDocument", { depth: 0 }).catch(() => {
|
|
587
|
+
return;
|
|
588
|
+
});
|
|
589
|
+
const { searchId, resultCount } = await conn.send("DOM.performSearch", { query });
|
|
590
|
+
try {
|
|
591
|
+
if (!resultCount)
|
|
592
|
+
throw driverError("no-such-element", `locator matched nothing: ${query}`);
|
|
593
|
+
const { nodeIds } = await conn.send("DOM.getSearchResults", { searchId, fromIndex: 0, toIndex: 1 });
|
|
594
|
+
const nodeId = nodeIds[0];
|
|
595
|
+
if (nodeId === undefined)
|
|
596
|
+
throw driverError("no-such-element", `locator matched nothing: ${query}`);
|
|
597
|
+
const { node } = await conn.send("DOM.describeNode", { nodeId });
|
|
598
|
+
return node.backendNodeId;
|
|
599
|
+
} finally {
|
|
600
|
+
await conn.send("DOM.discardSearchResults", { searchId }).catch(() => {
|
|
601
|
+
return;
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async function backendNodeIdOf(conn, loc) {
|
|
606
|
+
if ("uid" in loc)
|
|
607
|
+
return decodeUid(loc.uid);
|
|
608
|
+
if ("css" in loc) {
|
|
609
|
+
const { objectId } = await resolveElementLocator(conn, loc);
|
|
610
|
+
const { node } = await conn.send("DOM.describeNode", { objectId });
|
|
611
|
+
return node.backendNodeId;
|
|
612
|
+
}
|
|
613
|
+
return searchLocate(conn, "xpath" in loc ? loc.xpath : loc.text);
|
|
759
614
|
}
|
|
760
615
|
async function fullPageClip(conn) {
|
|
761
616
|
const m = await conn.send("Page.getLayoutMetrics");
|
|
762
617
|
const size = m.cssContentSize ?? m.contentSize;
|
|
763
618
|
if (!size)
|
|
764
|
-
throw
|
|
765
|
-
return { x: 0, y: 0, width: Math.ceil(size.width), height: Math.ceil(size.height)
|
|
766
|
-
}
|
|
767
|
-
async function elementClip(conn,
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
}
|
|
772
|
-
const { root } = await conn.send("DOM.getDocument", { depth: 0 });
|
|
773
|
-
const { nodeId } = await conn.send("DOM.querySelector", {
|
|
774
|
-
nodeId: root.nodeId,
|
|
775
|
-
selector
|
|
776
|
-
});
|
|
777
|
-
if (!nodeId)
|
|
778
|
-
throw new CdpError(`selector matched no element: ${selector}`);
|
|
779
|
-
const desc = await conn.send("DOM.describeNode", { nodeId });
|
|
780
|
-
backendNodeId = desc.node.backendNodeId;
|
|
781
|
-
} else {
|
|
782
|
-
throw new CdpError("elementClip requires uid or selector");
|
|
783
|
-
}
|
|
784
|
-
try {
|
|
785
|
-
await conn.send("DOM.scrollIntoViewIfNeeded", { backendNodeId });
|
|
786
|
-
} catch {}
|
|
619
|
+
throw driverError("page-error", "Page.getLayoutMetrics returned no content size");
|
|
620
|
+
return { x: 0, y: 0, width: Math.ceil(size.width), height: Math.ceil(size.height) };
|
|
621
|
+
}
|
|
622
|
+
async function elementClip(conn, loc) {
|
|
623
|
+
const backendNodeId = await backendNodeIdOf(conn, loc);
|
|
624
|
+
await conn.send("DOM.scrollIntoViewIfNeeded", { backendNodeId }).catch(() => {
|
|
625
|
+
return;
|
|
626
|
+
});
|
|
787
627
|
const box = await conn.send("DOM.getBoxModel", { backendNodeId });
|
|
788
628
|
const q = box.model.content;
|
|
789
|
-
const q0 = q[0];
|
|
790
|
-
const q1 = q[1];
|
|
791
|
-
if (q0 == null || q1 == null)
|
|
792
|
-
throw new CdpError("DOM.getBoxModel returned an empty content quad");
|
|
793
629
|
const xs = [q[0], q[2], q[4], q[6]].filter((n) => n != null);
|
|
794
630
|
const ys = [q[1], q[3], q[5], q[7]].filter((n) => n != null);
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
const
|
|
798
|
-
const
|
|
799
|
-
const width = Math.ceil(maxX - minX);
|
|
800
|
-
const height = Math.ceil(maxY - minY);
|
|
631
|
+
if (!xs.length || !ys.length)
|
|
632
|
+
throw driverError("page-error", "DOM.getBoxModel returned an empty content quad");
|
|
633
|
+
const minX = Math.min(...xs), minY = Math.min(...ys);
|
|
634
|
+
const width = Math.ceil(Math.max(...xs) - minX), height = Math.ceil(Math.max(...ys) - minY);
|
|
801
635
|
if (width <= 0 || height <= 0)
|
|
802
|
-
throw
|
|
803
|
-
return { x: minX, y: minY, width, height
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
636
|
+
throw driverError("page-error", "resolved element has zero area");
|
|
637
|
+
return { x: minX, y: minY, width, height };
|
|
638
|
+
}
|
|
639
|
+
var CDP_CAPABILITIES = new Set([
|
|
640
|
+
"trace.performance",
|
|
641
|
+
"heap.snapshot",
|
|
642
|
+
"audit.lighthouse",
|
|
643
|
+
"emulate.cpuThrottling",
|
|
644
|
+
"emulate.mediaFeatures",
|
|
645
|
+
"emulate.deviceMetrics",
|
|
646
|
+
"emulate.networkConditions",
|
|
647
|
+
"screenshot.fullPage",
|
|
648
|
+
"screenshot.element",
|
|
649
|
+
"network.intercept",
|
|
650
|
+
"snapshot.accessibilityTree",
|
|
651
|
+
"input.insertTextAtomic",
|
|
652
|
+
"locate.text",
|
|
653
|
+
"locate.xpath"
|
|
654
|
+
]);
|
|
655
|
+
|
|
656
|
+
class CdpPageDriver {
|
|
657
|
+
conn;
|
|
658
|
+
browser;
|
|
659
|
+
info;
|
|
660
|
+
enabledDomains = new Set;
|
|
661
|
+
released = false;
|
|
662
|
+
constructor(conn, target, browser) {
|
|
663
|
+
this.conn = conn;
|
|
664
|
+
this.browser = browser;
|
|
665
|
+
this.info = { id: target.id, url: target.url, title: target.title, type: target.type };
|
|
666
|
+
}
|
|
667
|
+
async ensureDomain(name) {
|
|
668
|
+
if (this.enabledDomains.has(name))
|
|
669
|
+
return;
|
|
670
|
+
this.enabledDomains.add(name);
|
|
671
|
+
await this.conn.send(`${name}.enable`);
|
|
672
|
+
}
|
|
673
|
+
async ensureDomainSoft(name) {
|
|
674
|
+
if (this.enabledDomains.has(name))
|
|
675
|
+
return;
|
|
676
|
+
this.enabledDomains.add(name);
|
|
677
|
+
await this.conn.send(`${name}.enable`).catch(() => {
|
|
678
|
+
return;
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
async navigate(opts) {
|
|
682
|
+
await this.ensureDomain("Page");
|
|
683
|
+
const reload = opts.reload === true;
|
|
684
|
+
if (!reload && !opts.url)
|
|
685
|
+
throw driverError("page-error", "navigate: 'url' is required unless reload:true");
|
|
686
|
+
const waitUntil = opts.waitUntil ?? "load";
|
|
687
|
+
const milestoneMethod = waitUntil === "load" ? "Page.loadEventFired" : "Page.domContentEventFired";
|
|
688
|
+
const milestone = this.conn.waitFor(milestoneMethod, undefined, opts.timeoutMs).then(() => waitUntil).catch(() => {
|
|
689
|
+
return;
|
|
690
|
+
});
|
|
691
|
+
const stopped = this.conn.waitFor("Page.frameStoppedLoading", undefined, opts.timeoutMs).then(() => "committed").catch(() => {
|
|
692
|
+
return;
|
|
693
|
+
});
|
|
694
|
+
let url, contextId, reloaded;
|
|
695
|
+
if (reload) {
|
|
696
|
+
await this.conn.send("Page.reload", { ignoreCache: opts.ignoreCache === true }, opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {});
|
|
697
|
+
const tree = await this.conn.send("Page.getFrameTree").catch(() => {
|
|
698
|
+
return;
|
|
699
|
+
});
|
|
700
|
+
contextId = tree?.frameTree?.frame?.id ?? "";
|
|
701
|
+
url = tree?.frameTree?.frame?.url ?? opts.url ?? "";
|
|
702
|
+
reloaded = true;
|
|
703
|
+
} else {
|
|
704
|
+
const nav = await this.conn.send("Page.navigate", { url: opts.url }, opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {});
|
|
705
|
+
if (nav.errorText)
|
|
706
|
+
throw driverError("page-error", `navigate: ${nav.errorText} (${opts.url})`);
|
|
707
|
+
contextId = nav.frameId;
|
|
708
|
+
url = opts.url;
|
|
709
|
+
}
|
|
710
|
+
const settled = await Promise.race([milestone, stopped]);
|
|
711
|
+
return { url, contextId, ...reloaded ? { reloaded } : {}, waitedFor: settled ?? "timeout" };
|
|
712
|
+
}
|
|
713
|
+
async waitForText(text, timeoutMs = 15000, pollMs = 250) {
|
|
714
|
+
await this.ensureDomain("Runtime");
|
|
715
|
+
const start = Date.now();
|
|
716
|
+
const expr = `(() => { const b = document.body; return !!b && typeof b.innerText === 'string' && b.innerText.includes(${JSON.stringify(text)}); })()`;
|
|
717
|
+
for (;; ) {
|
|
718
|
+
const { result } = await this.conn.send("Runtime.evaluate", { expression: expr, returnByValue: true });
|
|
719
|
+
if (result?.value === true)
|
|
720
|
+
return { found: true, elapsedMs: Date.now() - start };
|
|
721
|
+
if (Date.now() - start >= timeoutMs)
|
|
722
|
+
throw driverError("timeout", `waitForText: '${text}' not found within ${timeoutMs}ms`);
|
|
723
|
+
await new Promise((r) => setTimeout(r, Math.min(pollMs, Math.max(0, timeoutMs - (Date.now() - start)))));
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async callFn(objectId, functionDeclaration, args, awaitPromise) {
|
|
727
|
+
const res = await this.conn.send("Runtime.callFunctionOn", { objectId, functionDeclaration, arguments: args.map((value) => ({ value })), returnByValue: true, awaitPromise });
|
|
728
|
+
if (res.exceptionDetails)
|
|
729
|
+
throw driverError("page-error", exceptionMessage(res.exceptionDetails));
|
|
730
|
+
return unwrap(res.result);
|
|
731
|
+
}
|
|
732
|
+
async evaluate(expression, opts) {
|
|
733
|
+
await this.ensureDomain("Runtime");
|
|
734
|
+
const awaitPromise = opts?.awaitPromise ?? true;
|
|
735
|
+
if (opts?.args && opts.args.length > 0) {
|
|
736
|
+
const globalObj = await this.conn.send("Runtime.evaluate", { expression: "globalThis", returnByValue: false });
|
|
737
|
+
const objectId = globalObj.result.objectId;
|
|
738
|
+
if (!objectId)
|
|
739
|
+
throw driverError("page-error", "could not resolve global object for function call");
|
|
740
|
+
return this.callFn(objectId, expression, opts.args, awaitPromise);
|
|
741
|
+
}
|
|
742
|
+
const res = await this.conn.send("Runtime.evaluate", { expression, returnByValue: true, awaitPromise });
|
|
743
|
+
if (res.exceptionDetails)
|
|
744
|
+
throw driverError("page-error", exceptionMessage(res.exceptionDetails));
|
|
745
|
+
return unwrap(res.result);
|
|
746
|
+
}
|
|
747
|
+
async callOnElement(loc, functionDeclaration, args = []) {
|
|
748
|
+
const { objectId } = await resolveElementLocator(this.conn, loc);
|
|
749
|
+
return this.callFn(objectId, functionDeclaration, args, true);
|
|
750
|
+
}
|
|
751
|
+
async snapshot(opts) {
|
|
752
|
+
const interactiveOnly = opts?.interactiveOnly ?? false;
|
|
753
|
+
await this.ensureDomain("Accessibility");
|
|
754
|
+
const { nodes } = await this.conn.send("Accessibility.getFullAXTree");
|
|
755
|
+
const byId = new Map;
|
|
756
|
+
for (const n of nodes)
|
|
757
|
+
byId.set(n.nodeId, n);
|
|
758
|
+
const roots = nodes.filter((n) => !n.parentId || !byId.has(n.parentId));
|
|
759
|
+
const out = [];
|
|
760
|
+
const walk = (node, depth) => {
|
|
761
|
+
const role = axString(node.role);
|
|
762
|
+
const name = axString(node.name);
|
|
763
|
+
const backendNodeId = node.backendDOMNodeId;
|
|
764
|
+
const emitted = !node.ignored && backendNodeId !== undefined && (interactiveOnly ? isInteractive(role) : !!(role || name));
|
|
765
|
+
if (emitted) {
|
|
766
|
+
const extras = axExtras(node);
|
|
767
|
+
out.push({ uid: encodeUid(backendNodeId), role: role ?? "generic", depth, ...name ? { name } : {}, ...Object.keys(extras).length ? { extras } : {} });
|
|
768
|
+
}
|
|
769
|
+
const childDepth = emitted && !interactiveOnly ? depth + 1 : depth;
|
|
770
|
+
for (const childId of node.childIds ?? []) {
|
|
771
|
+
const child = byId.get(childId);
|
|
772
|
+
if (child)
|
|
773
|
+
walk(child, childDepth);
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
for (const root of roots)
|
|
777
|
+
walk(root, 0);
|
|
778
|
+
return out;
|
|
779
|
+
}
|
|
780
|
+
async locate(loc) {
|
|
781
|
+
return encodeUid(await backendNodeIdOf(this.conn, loc));
|
|
782
|
+
}
|
|
783
|
+
async resolve(uid) {
|
|
784
|
+
await resolveUid(this.conn, decodeUid(uid)).catch(() => {
|
|
785
|
+
throw driverError("stale-uid", `uid does not resolve: ${uid}`);
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
async hover(loc) {
|
|
789
|
+
const { objectId } = await resolveElementLocator(this.conn, loc);
|
|
790
|
+
const { x, y } = await centerOf(this.conn, objectId);
|
|
791
|
+
await this.conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y });
|
|
792
|
+
return { x, y };
|
|
793
|
+
}
|
|
794
|
+
async click(loc, opts) {
|
|
795
|
+
const button = opts?.button ?? "left";
|
|
796
|
+
const clickCount = opts?.clickCount ?? 1;
|
|
797
|
+
const { x, y } = await this.hover(loc);
|
|
798
|
+
await this.conn.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button, clickCount });
|
|
799
|
+
await this.conn.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button, clickCount });
|
|
800
|
+
return { x, y };
|
|
801
|
+
}
|
|
802
|
+
async drag(from, to) {
|
|
803
|
+
const fromPt = await this.hover(from);
|
|
804
|
+
const { objectId: dstId } = await resolveElementLocator(this.conn, to);
|
|
805
|
+
const toPt = await centerOf(this.conn, dstId);
|
|
806
|
+
await this.conn.send("Input.dispatchMouseEvent", { type: "mousePressed", x: fromPt.x, y: fromPt.y, button: "left", clickCount: 1 });
|
|
807
|
+
await this.conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: (fromPt.x + toPt.x) / 2, y: (fromPt.y + toPt.y) / 2, button: "left" });
|
|
808
|
+
await this.conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: toPt.x, y: toPt.y, button: "left" });
|
|
809
|
+
await this.conn.send("Input.dispatchMouseEvent", { type: "mouseReleased", x: toPt.x, y: toPt.y, button: "left", clickCount: 1 });
|
|
810
|
+
return { from: fromPt, to: toPt };
|
|
811
|
+
}
|
|
812
|
+
async setValue(loc, value) {
|
|
813
|
+
const { objectId } = await resolveElementLocator(this.conn, loc);
|
|
814
|
+
await setValueOnObject(this.conn, objectId, value);
|
|
815
|
+
}
|
|
816
|
+
async typeText(loc, text) {
|
|
817
|
+
const { objectId } = await resolveElementLocator(this.conn, loc);
|
|
818
|
+
await focusElement(this.conn, objectId);
|
|
819
|
+
if (text.length)
|
|
820
|
+
await this.conn.send("Input.insertText", { text });
|
|
821
|
+
}
|
|
822
|
+
async pressKey(press) {
|
|
823
|
+
const spec = resolveKey(press.key);
|
|
824
|
+
let modBits = 0;
|
|
825
|
+
for (const m of press.modifiers ?? []) {
|
|
826
|
+
const bit = MODIFIER_BITS[m.toLowerCase()];
|
|
827
|
+
if (bit === undefined)
|
|
828
|
+
throw driverError("page-error", `unknown modifier '${m}'`);
|
|
829
|
+
modBits |= bit;
|
|
830
|
+
}
|
|
831
|
+
const suppressText = (modBits & ~MODIFIER_BITS.shift) !== 0;
|
|
832
|
+
const down = { type: "keyDown", key: spec.key, code: spec.code, windowsVirtualKeyCode: spec.keyCode, nativeVirtualKeyCode: spec.keyCode, modifiers: modBits };
|
|
833
|
+
if (spec.text && !suppressText)
|
|
834
|
+
down.text = spec.text;
|
|
835
|
+
await this.conn.send("Input.dispatchKeyEvent", down);
|
|
836
|
+
await this.conn.send("Input.dispatchKeyEvent", { type: "keyUp", key: spec.key, code: spec.code, windowsVirtualKeyCode: spec.keyCode, nativeVirtualKeyCode: spec.keyCode, modifiers: modBits });
|
|
837
|
+
}
|
|
838
|
+
async setFiles(loc, files) {
|
|
839
|
+
const { objectId } = await resolveElementLocator(this.conn, loc);
|
|
840
|
+
await this.conn.send("DOM.setFileInputFiles", { files: [...files], objectId });
|
|
841
|
+
}
|
|
842
|
+
async screenshot(opts) {
|
|
843
|
+
await this.ensureDomain("Page");
|
|
844
|
+
const format = opts?.format ?? "png";
|
|
845
|
+
const quality = format === "jpeg" ? opts?.quality ?? 80 : undefined;
|
|
813
846
|
const params = { format, captureBeyondViewport: true };
|
|
814
847
|
if (quality != null)
|
|
815
848
|
params.quality = quality;
|
|
816
|
-
if (
|
|
817
|
-
params.clip = await elementClip(conn,
|
|
818
|
-
|
|
819
|
-
params.clip = await fullPageClip(conn);
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
const path = savePath ?? join(ARTIFACT_DIR, `screenshot-${shortId(t.id)}-${stamp()}.${ext}`);
|
|
828
|
-
await writeFile2(path, buf);
|
|
829
|
-
const result = {
|
|
830
|
-
path,
|
|
831
|
-
bytes: buf.byteLength,
|
|
832
|
-
format,
|
|
833
|
-
target: { id: t.id, url: t.url, title: t.title }
|
|
834
|
-
};
|
|
835
|
-
if (returnBase64)
|
|
836
|
-
result.base64 = data;
|
|
837
|
-
return result;
|
|
838
|
-
});
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
// src/tools/emulation.ts
|
|
842
|
-
async function clearAll(conn) {
|
|
843
|
-
await conn.send("Emulation.clearDeviceMetricsOverride").catch(() => {
|
|
844
|
-
return;
|
|
845
|
-
});
|
|
846
|
-
await conn.send("Emulation.setUserAgentOverride", { userAgent: "" }).catch(() => {
|
|
847
|
-
return;
|
|
848
|
-
});
|
|
849
|
-
await conn.send("Emulation.setCPUThrottlingRate", { rate: 1 }).catch(() => {
|
|
850
|
-
return;
|
|
851
|
-
});
|
|
852
|
-
await conn.send("Emulation.setEmulatedMedia", { media: "", features: [] }).catch(() => {
|
|
853
|
-
return;
|
|
854
|
-
});
|
|
855
|
-
await conn.send("Network.enable").catch(() => {
|
|
856
|
-
return;
|
|
857
|
-
});
|
|
858
|
-
await conn.send("Network.emulateNetworkConditions", {
|
|
859
|
-
offline: false,
|
|
860
|
-
latency: 0,
|
|
861
|
-
downloadThroughput: -1,
|
|
862
|
-
uploadThroughput: -1
|
|
863
|
-
}).catch(() => {
|
|
864
|
-
return;
|
|
865
|
-
});
|
|
866
|
-
}
|
|
867
|
-
async function emulate(args = {}) {
|
|
868
|
-
const { target } = args;
|
|
869
|
-
return withPage(target, async (conn, t) => {
|
|
870
|
-
if (args.clearOverrides) {
|
|
871
|
-
await clearAll(conn);
|
|
872
|
-
return {
|
|
873
|
-
target: { id: t.id, url: t.url, title: t.title },
|
|
874
|
-
applied: [],
|
|
875
|
-
cleared: true
|
|
849
|
+
if (opts?.clip)
|
|
850
|
+
params.clip = { ...await elementClip(this.conn, opts.clip), scale: 1 };
|
|
851
|
+
else if (opts?.fullPage)
|
|
852
|
+
params.clip = { ...await fullPageClip(this.conn), scale: 1 };
|
|
853
|
+
const { data } = await this.conn.send("Page.captureScreenshot", params);
|
|
854
|
+
return { data: new Uint8Array(Buffer.from(data, "base64")), format };
|
|
855
|
+
}
|
|
856
|
+
async emulate(opts) {
|
|
857
|
+
if (opts.clearOverrides) {
|
|
858
|
+
const c = () => {
|
|
859
|
+
return;
|
|
876
860
|
};
|
|
861
|
+
await this.conn.send("Emulation.clearDeviceMetricsOverride").catch(c);
|
|
862
|
+
await this.conn.send("Emulation.setUserAgentOverride", { userAgent: "" }).catch(c);
|
|
863
|
+
await this.conn.send("Emulation.setCPUThrottlingRate", { rate: 1 }).catch(c);
|
|
864
|
+
await this.conn.send("Emulation.setEmulatedMedia", { media: "", features: [] }).catch(c);
|
|
865
|
+
await this.ensureDomainSoft("Network");
|
|
866
|
+
await this.conn.send("Network.emulateNetworkConditions", { offline: false, latency: 0, downloadThroughput: -1, uploadThroughput: -1 }).catch(c);
|
|
867
|
+
return { applied: [] };
|
|
877
868
|
}
|
|
878
869
|
const applied = [];
|
|
879
|
-
if (
|
|
880
|
-
if (
|
|
881
|
-
throw
|
|
870
|
+
if (opts.width != null || opts.height != null) {
|
|
871
|
+
if (opts.width == null || opts.height == null) {
|
|
872
|
+
throw driverError("page-error", "emulate device metrics require both width and height");
|
|
882
873
|
}
|
|
883
|
-
await conn.send("Emulation.setDeviceMetricsOverride", {
|
|
884
|
-
width:
|
|
885
|
-
height:
|
|
886
|
-
deviceScaleFactor:
|
|
887
|
-
mobile:
|
|
874
|
+
await this.conn.send("Emulation.setDeviceMetricsOverride", {
|
|
875
|
+
width: opts.width,
|
|
876
|
+
height: opts.height,
|
|
877
|
+
deviceScaleFactor: opts.deviceScaleFactor ?? 0,
|
|
878
|
+
mobile: opts.mobile ?? false
|
|
888
879
|
});
|
|
889
880
|
applied.push("deviceMetrics");
|
|
890
881
|
}
|
|
891
|
-
if (
|
|
892
|
-
await conn.send("Emulation.setUserAgentOverride", { userAgent:
|
|
882
|
+
if (opts.userAgent != null) {
|
|
883
|
+
await this.conn.send("Emulation.setUserAgentOverride", { userAgent: opts.userAgent });
|
|
893
884
|
applied.push("userAgent");
|
|
894
885
|
}
|
|
895
|
-
if (
|
|
896
|
-
if (
|
|
897
|
-
throw
|
|
898
|
-
await conn.send("Emulation.setCPUThrottlingRate", { rate:
|
|
886
|
+
if (opts.cpuThrottlingRate != null) {
|
|
887
|
+
if (opts.cpuThrottlingRate < 1)
|
|
888
|
+
throw driverError("page-error", "cpuThrottlingRate must be >= 1");
|
|
889
|
+
await this.conn.send("Emulation.setCPUThrottlingRate", { rate: opts.cpuThrottlingRate });
|
|
899
890
|
applied.push("cpuThrottlingRate");
|
|
900
891
|
}
|
|
901
|
-
if (
|
|
902
|
-
await conn.send("Emulation.setEmulatedMedia", {
|
|
903
|
-
media: args.media ?? "",
|
|
904
|
-
features: args.mediaFeatures ?? []
|
|
905
|
-
});
|
|
892
|
+
if (opts.media != null || opts.mediaFeatures != null) {
|
|
893
|
+
await this.conn.send("Emulation.setEmulatedMedia", { media: opts.media ?? "", features: opts.mediaFeatures ?? [] });
|
|
906
894
|
applied.push("emulatedMedia");
|
|
907
895
|
}
|
|
908
|
-
if (
|
|
909
|
-
const n =
|
|
910
|
-
await
|
|
911
|
-
|
|
912
|
-
});
|
|
913
|
-
await conn.send("Network.emulateNetworkConditions", {
|
|
896
|
+
if (opts.networkConditions != null) {
|
|
897
|
+
const n = opts.networkConditions;
|
|
898
|
+
await this.ensureDomainSoft("Network");
|
|
899
|
+
await this.conn.send("Network.emulateNetworkConditions", {
|
|
914
900
|
offline: n.offline ?? false,
|
|
915
901
|
latency: n.latency ?? 0,
|
|
916
902
|
downloadThroughput: n.downloadThroughput ?? -1,
|
|
@@ -919,98 +905,474 @@ async function emulate(args = {}) {
|
|
|
919
905
|
});
|
|
920
906
|
applied.push("networkConditions");
|
|
921
907
|
}
|
|
922
|
-
|
|
923
|
-
|
|
908
|
+
return { applied };
|
|
909
|
+
}
|
|
910
|
+
static EVENT_MAP = {
|
|
911
|
+
navigated: { method: "Page.frameNavigated" },
|
|
912
|
+
console: { method: "Runtime.consoleAPICalled" },
|
|
913
|
+
"network.request": { method: "Network.requestWillBeSent", domain: "Network" },
|
|
914
|
+
"network.response": { method: "Network.responseReceived", domain: "Network" },
|
|
915
|
+
dialog: { method: "Page.javascriptDialogOpening" },
|
|
916
|
+
"page.closed": { method: "Inspector.detached", domain: "Inspector" }
|
|
917
|
+
};
|
|
918
|
+
on(event, handler) {
|
|
919
|
+
const spec = CdpPageDriver.EVENT_MAP[event];
|
|
920
|
+
if (spec.domain && !this.enabledDomains.has(spec.domain)) {
|
|
921
|
+
this.enabledDomains.add(spec.domain);
|
|
922
|
+
this.conn.send(`${spec.domain}.enable`);
|
|
923
|
+
}
|
|
924
|
+
return this.conn.on(spec.method, (params) => handler(params));
|
|
925
|
+
}
|
|
926
|
+
async waitForEvent(event, predicate, timeoutMs) {
|
|
927
|
+
const spec = CdpPageDriver.EVENT_MAP[event];
|
|
928
|
+
if (spec.domain)
|
|
929
|
+
await this.ensureDomain(spec.domain);
|
|
930
|
+
return this.conn.waitFor(spec.method, predicate, timeoutMs);
|
|
931
|
+
}
|
|
932
|
+
armDialog(accept, promptText, onHandled) {
|
|
933
|
+
return this.conn.on("Page.javascriptDialogOpening", (params) => {
|
|
934
|
+
const p = params;
|
|
935
|
+
this.conn.send("Page.handleJavaScriptDialog", { accept, ...promptText !== undefined ? { promptText } : {} }).then(() => onHandled({
|
|
936
|
+
type: p.type,
|
|
937
|
+
message: p.message,
|
|
938
|
+
url: p.url,
|
|
939
|
+
...p.defaultPrompt !== undefined ? { defaultPrompt: p.defaultPrompt } : {},
|
|
940
|
+
accept,
|
|
941
|
+
...promptText !== undefined ? { promptText } : {},
|
|
942
|
+
handled: true
|
|
943
|
+
})).catch(() => {
|
|
944
|
+
return;
|
|
945
|
+
});
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
async handleDialog(accept, promptText, opts) {
|
|
949
|
+
await this.ensureDomain("Page");
|
|
950
|
+
if (opts?.autoMs !== undefined) {
|
|
951
|
+
const handled = [];
|
|
952
|
+
const off = this.armDialog(accept, promptText, (d) => handled.push(d));
|
|
953
|
+
await new Promise((resolve) => setTimeout(resolve, opts.autoMs));
|
|
954
|
+
off();
|
|
955
|
+
return { handled, count: handled.length };
|
|
956
|
+
}
|
|
957
|
+
const timeoutMs = opts?.timeoutMs ?? 15000;
|
|
958
|
+
return new Promise((resolve, reject) => {
|
|
959
|
+
const timer = setTimeout(() => {
|
|
960
|
+
off();
|
|
961
|
+
reject(driverError("timeout", `handleDialog: no dialog opened within ${timeoutMs}ms`));
|
|
962
|
+
}, timeoutMs);
|
|
963
|
+
const off = this.armDialog(accept, promptText, (d) => {
|
|
964
|
+
clearTimeout(timer);
|
|
965
|
+
off();
|
|
966
|
+
resolve(d);
|
|
967
|
+
});
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
async startBodyCapture() {
|
|
971
|
+
await this.ensureDomain("Network");
|
|
972
|
+
return async () => {
|
|
973
|
+
await this.conn.send("Network.disable").catch(() => {
|
|
974
|
+
return;
|
|
975
|
+
});
|
|
976
|
+
this.enabledDomains.delete("Network");
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
async getResponseBody(requestId) {
|
|
980
|
+
return this.conn.send("Network.getResponseBody", { requestId });
|
|
981
|
+
}
|
|
982
|
+
async intercept(rules) {
|
|
983
|
+
await this.conn.send("Fetch.enable", { patterns: rules.map((r) => ({ urlPattern: r.urlPattern })) });
|
|
984
|
+
let chain = Promise.resolve();
|
|
985
|
+
const off = this.conn.on("Fetch.requestPaused", (params) => {
|
|
986
|
+
const p = params;
|
|
987
|
+
chain = chain.then(async () => {
|
|
988
|
+
const rule = selectRule(rules, p.request.url, p.request.method);
|
|
989
|
+
if (!rule) {
|
|
990
|
+
await this.conn.send("Fetch.continueRequest", { requestId: p.requestId });
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (rule.delayMs)
|
|
994
|
+
await new Promise((r) => setTimeout(r, rule.delayMs));
|
|
995
|
+
const act = effectiveAction(rule, Math.random());
|
|
996
|
+
if (act === "fulfill")
|
|
997
|
+
await this.conn.send("Fetch.fulfillRequest", { ...buildFulfillParams(p.requestId, rule) });
|
|
998
|
+
else if (act === "fail")
|
|
999
|
+
await this.conn.send("Fetch.failRequest", { requestId: p.requestId, errorReason: rule.errorReason ?? "Failed" });
|
|
1000
|
+
else
|
|
1001
|
+
await this.conn.send("Fetch.continueRequest", { requestId: p.requestId });
|
|
1002
|
+
}).catch(() => {
|
|
1003
|
+
return;
|
|
1004
|
+
});
|
|
1005
|
+
});
|
|
1006
|
+
return async () => {
|
|
1007
|
+
off();
|
|
1008
|
+
await this.conn.send("Fetch.disable").catch(() => {
|
|
1009
|
+
return;
|
|
1010
|
+
});
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
async release() {
|
|
1014
|
+
if (this.released)
|
|
1015
|
+
return;
|
|
1016
|
+
this.released = true;
|
|
1017
|
+
this.conn.close();
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
async function withBrowserConn(fn) {
|
|
1021
|
+
const conn = await openBrowser();
|
|
1022
|
+
try {
|
|
1023
|
+
return await fn(conn);
|
|
1024
|
+
} finally {
|
|
1025
|
+
conn.close();
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
function noSuchTarget(e) {
|
|
1029
|
+
throw driverError("no-such-target", e instanceof CdpError ? e.message : String(e));
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
class CdpBrowserDriver {
|
|
1033
|
+
scheme = "cdp";
|
|
1034
|
+
lifetime = "per-call";
|
|
1035
|
+
capabilities = CDP_CAPABILITIES;
|
|
1036
|
+
uidStability = "browser-node";
|
|
1037
|
+
snapshotFidelity = "accessibility-tree";
|
|
1038
|
+
async listPages(opts) {
|
|
1039
|
+
const targets = await listTargets();
|
|
1040
|
+
const filtered = opts?.all ? targets : targets.filter((t) => t.type === "page");
|
|
1041
|
+
return filtered.map((t) => ({ id: t.id, url: t.url, title: t.title, type: t.type }));
|
|
1042
|
+
}
|
|
1043
|
+
async newPage(url) {
|
|
1044
|
+
return withBrowserConn(async (conn) => {
|
|
1045
|
+
const { targetId } = await conn.send("Target.createTarget", { url: url ?? "about:blank" });
|
|
1046
|
+
if (!targetId)
|
|
1047
|
+
throw driverError("page-error", "Target.createTarget returned no targetId");
|
|
1048
|
+
return { id: targetId, url: url ?? "about:blank", title: "" };
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
async closePage(id) {
|
|
1052
|
+
return withBrowserConn(async (conn) => {
|
|
1053
|
+
const res = await conn.send("Target.closeTarget", { targetId: id });
|
|
1054
|
+
return { success: res.success ?? true };
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
async activatePage(id) {
|
|
1058
|
+
const target = await resolveTarget(id).catch(noSuchTarget);
|
|
1059
|
+
await withBrowserConn((conn) => conn.send("Target.activateTarget", { targetId: target.id }));
|
|
1060
|
+
return { id: target.id, url: target.url, title: target.title };
|
|
1061
|
+
}
|
|
1062
|
+
async page(selector) {
|
|
1063
|
+
const { conn, target } = await openPage(selector).catch(noSuchTarget);
|
|
1064
|
+
return new CdpPageDriver(conn, target, this);
|
|
1065
|
+
}
|
|
1066
|
+
async dispose() {}
|
|
1067
|
+
}
|
|
1068
|
+
function createCdpDriver() {
|
|
1069
|
+
return new CdpBrowserDriver;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// src/shared-tools.ts
|
|
1073
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
1074
|
+
import { join } from "node:path";
|
|
1075
|
+
var ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1076
|
+
var STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
1077
|
+
|
|
1078
|
+
class SharedToolError extends Error {
|
|
1079
|
+
}
|
|
1080
|
+
function target3(p) {
|
|
1081
|
+
return { id: p.id, url: p.url, title: p.title };
|
|
1082
|
+
}
|
|
1083
|
+
async function withPage2(driver, target, fn) {
|
|
1084
|
+
const page = await driver.page(target);
|
|
1085
|
+
try {
|
|
1086
|
+
return await fn(page);
|
|
1087
|
+
} finally {
|
|
1088
|
+
await page.release();
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
async function resolvePage(driver, selector) {
|
|
1092
|
+
const pages = await driver.listPages();
|
|
1093
|
+
if (selector === undefined || selector === "active") {
|
|
1094
|
+
if (!pages.length)
|
|
1095
|
+
throw new SharedToolError("no page targets open");
|
|
1096
|
+
return pages[0];
|
|
1097
|
+
}
|
|
1098
|
+
if (selector.startsWith("index:")) {
|
|
1099
|
+
const p = pages[Number(selector.slice(6))];
|
|
1100
|
+
if (!p)
|
|
1101
|
+
throw new SharedToolError(`no page target at index ${selector.slice(6)} (have ${pages.length})`);
|
|
1102
|
+
return p;
|
|
1103
|
+
}
|
|
1104
|
+
if (selector.startsWith("url:")) {
|
|
1105
|
+
const needle = selector.slice(4);
|
|
1106
|
+
const p = pages.find((x) => x.url.includes(needle));
|
|
1107
|
+
if (!p)
|
|
1108
|
+
throw new SharedToolError(`no page url containing '${needle}'`);
|
|
1109
|
+
return p;
|
|
1110
|
+
}
|
|
1111
|
+
if (selector.startsWith("title:")) {
|
|
1112
|
+
const needle = selector.slice(6);
|
|
1113
|
+
const p = pages.find((x) => x.title.includes(needle));
|
|
1114
|
+
if (!p)
|
|
1115
|
+
throw new SharedToolError(`no page title containing '${needle}'`);
|
|
1116
|
+
return p;
|
|
1117
|
+
}
|
|
1118
|
+
const exactPage = pages.find((x) => x.id === selector);
|
|
1119
|
+
if (exactPage)
|
|
1120
|
+
return exactPage;
|
|
1121
|
+
const all = await driver.listPages({ all: true });
|
|
1122
|
+
const exact = all.find((x) => x.id === selector);
|
|
1123
|
+
if (exact)
|
|
1124
|
+
return exact;
|
|
1125
|
+
throw new SharedToolError(`no target with id '${selector}'`);
|
|
1126
|
+
}
|
|
1127
|
+
function locatorOf(args) {
|
|
1128
|
+
const hasUid = args.uid !== undefined && args.uid !== null && args.uid !== "";
|
|
1129
|
+
const hasSelector = typeof args.selector === "string" && args.selector.length > 0;
|
|
1130
|
+
if (hasUid === hasSelector)
|
|
1131
|
+
throw new SharedToolError("provide exactly one of { uid } or { selector }");
|
|
1132
|
+
return hasUid ? { uid: args.uid } : { css: args.selector };
|
|
1133
|
+
}
|
|
1134
|
+
async function listPages(driver, args = {}) {
|
|
1135
|
+
const pages = await driver.listPages({ all: args.all });
|
|
1136
|
+
return { pages, count: pages.length };
|
|
1137
|
+
}
|
|
1138
|
+
async function newPage(driver, args = {}) {
|
|
1139
|
+
const p = await driver.newPage(args.url);
|
|
1140
|
+
return { targetId: p.id, url: p.url };
|
|
1141
|
+
}
|
|
1142
|
+
async function closePage(driver, args) {
|
|
1143
|
+
if (args.target === undefined || args.target === "")
|
|
1144
|
+
throw new SharedToolError("close_page requires an explicit target; refusing to guess which page to close");
|
|
1145
|
+
const p = await resolvePage(driver, args.target);
|
|
1146
|
+
const res = await driver.closePage(p.id);
|
|
1147
|
+
return { closed: p.id, success: res.success };
|
|
1148
|
+
}
|
|
1149
|
+
async function writeSelectedFile(id) {
|
|
1150
|
+
await mkdir(STATE_DIR, { recursive: true });
|
|
1151
|
+
await writeFile(join(STATE_DIR, "selected"), id, "utf8");
|
|
1152
|
+
}
|
|
1153
|
+
async function selectPage(driver, args) {
|
|
1154
|
+
if (args.target === undefined || args.target === "")
|
|
1155
|
+
throw new SharedToolError("select_page requires an explicit target");
|
|
1156
|
+
const p = await resolvePage(driver, args.target);
|
|
1157
|
+
const activated = await driver.activatePage(p.id);
|
|
1158
|
+
await writeSelectedFile(activated.id);
|
|
1159
|
+
return { selected: activated.id };
|
|
1160
|
+
}
|
|
1161
|
+
function toLegacyWaitedFor(w) {
|
|
1162
|
+
if (w === "committed")
|
|
1163
|
+
return "frameStoppedLoading";
|
|
1164
|
+
if (w === "timeout")
|
|
1165
|
+
return "navigate-only";
|
|
1166
|
+
return w;
|
|
1167
|
+
}
|
|
1168
|
+
async function navigatePage(driver, args) {
|
|
1169
|
+
const reload = args.reload === true;
|
|
1170
|
+
if (!reload && (!args.url || typeof args.url !== "string")) {
|
|
1171
|
+
throw new SharedToolError("navigate_page: 'url' is required (or pass reload:true to reload the current page)");
|
|
1172
|
+
}
|
|
1173
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1174
|
+
const r = await page.navigate(args);
|
|
1175
|
+
return {
|
|
1176
|
+
url: r.url,
|
|
1177
|
+
frameId: r.contextId,
|
|
1178
|
+
...r.reloaded ? { reloaded: r.reloaded } : {},
|
|
1179
|
+
waitedFor: toLegacyWaitedFor(r.waitedFor)
|
|
1180
|
+
};
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
async function waitForText(driver, args) {
|
|
1184
|
+
if (typeof args.text !== "string" || args.text.length === 0)
|
|
1185
|
+
throw new SharedToolError("wait_for: 'text' is required");
|
|
1186
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1187
|
+
const r = await page.waitForText(args.text, args.timeoutMs ?? 15000, args.pollMs ?? 250);
|
|
1188
|
+
return { found: true, waitedMs: r.elapsedMs };
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
async function evaluateScript(driver, args) {
|
|
1192
|
+
if (typeof args.expression !== "string" || args.expression.length === 0) {
|
|
1193
|
+
throw new SharedToolError("evaluateScript: 'expression' must be a non-empty string");
|
|
1194
|
+
}
|
|
1195
|
+
return withPage2(driver, args.target, (page) => page.evaluate(args.expression, { args: args.args, awaitPromise: args.awaitPromise ?? true }));
|
|
1196
|
+
}
|
|
1197
|
+
var BARE_TOKEN_EXTRAS = new Set(["disabled", "focused"]);
|
|
1198
|
+
var UNQUOTED_EXTRAS = new Set(["checked", "expanded", "url"]);
|
|
1199
|
+
function renderSnapshotLine(n, scheme) {
|
|
1200
|
+
const indent = " ".repeat(n.depth);
|
|
1201
|
+
const uidText = scheme === "cdp" && n.uid.startsWith("cdp:") ? n.uid.slice(4) : n.uid;
|
|
1202
|
+
const name = n.name !== undefined ? ` ${JSON.stringify(n.name)}` : "";
|
|
1203
|
+
const extrasToks = [];
|
|
1204
|
+
if (n.extras) {
|
|
1205
|
+
for (const [k, v] of Object.entries(n.extras)) {
|
|
1206
|
+
if (BARE_TOKEN_EXTRAS.has(k))
|
|
1207
|
+
extrasToks.push(k);
|
|
1208
|
+
else if (UNQUOTED_EXTRAS.has(k))
|
|
1209
|
+
extrasToks.push(`${k}=${v}`);
|
|
1210
|
+
else
|
|
1211
|
+
extrasToks.push(`${k}=${JSON.stringify(v)}`);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
const extra = extrasToks.length ? ` [${extrasToks.join(" ")}]` : "";
|
|
1215
|
+
return `${indent}[${uidText}] ${n.role}${name}${extra}`;
|
|
1216
|
+
}
|
|
1217
|
+
async function takeSnapshot(driver, args = {}) {
|
|
1218
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1219
|
+
const nodes = await page.snapshot({ interactiveOnly: args.interactiveOnly ?? false });
|
|
1220
|
+
const scheme = page.browser.scheme;
|
|
1221
|
+
return { snapshot: nodes.map((n) => renderSnapshotLine(n, scheme)).join(`
|
|
1222
|
+
`), target: target3(page.info), nodeCount: nodes.length };
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
async function click(driver, args) {
|
|
1226
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1227
|
+
const { x, y } = await page.click(locatorOf(args), { button: args.button ?? "left", clickCount: args.clickCount ?? 1 });
|
|
1228
|
+
return { clicked: true, x, y };
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
async function hover(driver, args) {
|
|
1232
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1233
|
+
const { x, y } = await page.hover(locatorOf(args));
|
|
1234
|
+
return { hovered: true, x, y };
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
async function drag(driver, args) {
|
|
1238
|
+
if (!args.from || !args.to)
|
|
1239
|
+
throw new SharedToolError("drag requires { from } and { to }");
|
|
1240
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1241
|
+
const { from, to } = await page.drag(locatorOf(args.from), locatorOf(args.to));
|
|
1242
|
+
return { dragged: true, from, to };
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
async function fill(driver, args) {
|
|
1246
|
+
if (typeof args.value !== "string")
|
|
1247
|
+
throw new SharedToolError("fill requires a string { value }");
|
|
1248
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1249
|
+
await page.setValue(locatorOf(args), args.value);
|
|
1250
|
+
return { filled: true, value: args.value };
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
async function fillForm(driver, args) {
|
|
1254
|
+
if (!Array.isArray(args.fields) || args.fields.length === 0)
|
|
1255
|
+
throw new SharedToolError("fill_form requires a non-empty { fields } array");
|
|
1256
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1257
|
+
let filled = 0;
|
|
1258
|
+
for (const f of args.fields) {
|
|
1259
|
+
if (typeof f.value !== "string")
|
|
1260
|
+
throw new SharedToolError("each fill_form field requires a string value");
|
|
1261
|
+
await page.setValue(locatorOf(f), f.value);
|
|
1262
|
+
filled++;
|
|
924
1263
|
}
|
|
925
|
-
return {
|
|
1264
|
+
return { filled, fields: args.fields.length };
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
async function typeText(driver, args) {
|
|
1268
|
+
if (typeof args.text !== "string")
|
|
1269
|
+
throw new SharedToolError("type_text requires a string { text }");
|
|
1270
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1271
|
+
await page.typeText(locatorOf(args), args.text);
|
|
1272
|
+
return { typed: true, text: args.text };
|
|
926
1273
|
});
|
|
927
1274
|
}
|
|
928
|
-
async function
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
width,
|
|
936
|
-
height,
|
|
937
|
-
deviceScaleFactor: args.deviceScaleFactor ?? 0,
|
|
938
|
-
mobile: args.mobile ?? false
|
|
939
|
-
});
|
|
940
|
-
await conn.send("Runtime.enable").catch(() => {
|
|
941
|
-
return;
|
|
942
|
-
});
|
|
943
|
-
const { result } = await conn.send("Runtime.evaluate", {
|
|
944
|
-
expression: "({ w: window.innerWidth, h: window.innerHeight })",
|
|
945
|
-
returnByValue: true
|
|
946
|
-
});
|
|
947
|
-
const v = result.value;
|
|
948
|
-
return {
|
|
949
|
-
target: { id: t.id, url: t.url, title: t.title },
|
|
950
|
-
width,
|
|
951
|
-
height,
|
|
952
|
-
innerWidth: v?.w ?? width,
|
|
953
|
-
innerHeight: v?.h ?? height
|
|
954
|
-
};
|
|
1275
|
+
async function pressKey(driver, args) {
|
|
1276
|
+
if (typeof args.key !== "string" || !args.key.length)
|
|
1277
|
+
throw new SharedToolError("press_key requires a non-empty { key }");
|
|
1278
|
+
const modifiers = args.modifiers ?? [];
|
|
1279
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1280
|
+
await page.pressKey({ key: args.key, modifiers });
|
|
1281
|
+
return { pressed: args.key, modifiers };
|
|
955
1282
|
});
|
|
956
1283
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
accept,
|
|
966
|
-
...promptText !== undefined ? { promptText } : {}
|
|
967
|
-
});
|
|
968
|
-
onHandled({
|
|
969
|
-
type: ev.type,
|
|
970
|
-
message: ev.message,
|
|
971
|
-
url: ev.url,
|
|
972
|
-
...ev.defaultPrompt !== undefined ? { defaultPrompt: ev.defaultPrompt } : {},
|
|
973
|
-
accept,
|
|
974
|
-
...promptText !== undefined ? { promptText } : {},
|
|
975
|
-
handled: true
|
|
976
|
-
});
|
|
977
|
-
} catch {}
|
|
978
|
-
})();
|
|
1284
|
+
async function uploadFile(driver, args) {
|
|
1285
|
+
const files = Array.isArray(args.files) ? args.files : [args.files];
|
|
1286
|
+
if (!files.length || files.some((f) => typeof f !== "string" || !f.length)) {
|
|
1287
|
+
throw new SharedToolError("upload_file requires { files } as a non-empty path or array of paths");
|
|
1288
|
+
}
|
|
1289
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1290
|
+
await page.setFiles(locatorOf(args), files);
|
|
1291
|
+
return { uploaded: files };
|
|
979
1292
|
});
|
|
980
1293
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1294
|
+
function stamp() {
|
|
1295
|
+
return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
|
|
1296
|
+
}
|
|
1297
|
+
async function takeScreenshot(driver, args = {}) {
|
|
1298
|
+
if (args.uid !== undefined && args.uid !== "" && args.selector)
|
|
1299
|
+
throw new SharedToolError("provide exactly one of uid or selector, not both");
|
|
1300
|
+
const format = args.format ?? "png";
|
|
1301
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1302
|
+
const clip = args.uid !== undefined || args.selector ? locatorOf(args) : undefined;
|
|
1303
|
+
const { data, format: outFormat } = await page.screenshot({ format, quality: args.quality, fullPage: args.fullPage, ...clip ? { clip } : {} });
|
|
1304
|
+
if (data.byteLength === 0)
|
|
1305
|
+
throw new SharedToolError("captureScreenshot returned empty data");
|
|
1306
|
+
await mkdir(ARTIFACT_DIR, { recursive: true });
|
|
1307
|
+
const ext = outFormat === "jpeg" ? "jpg" : "png";
|
|
1308
|
+
const path = args.savePath ?? join(ARTIFACT_DIR, `screenshot-${page.info.id.slice(0, 8)}-${stamp()}.${ext}`);
|
|
1309
|
+
await writeFile(path, Buffer.from(data));
|
|
1310
|
+
const result = {
|
|
1311
|
+
path,
|
|
1312
|
+
bytes: data.byteLength,
|
|
1313
|
+
format: outFormat,
|
|
1314
|
+
target: target3(page.info)
|
|
1315
|
+
};
|
|
1316
|
+
if (args.returnBase64)
|
|
1317
|
+
result.base64 = Buffer.from(data).toString("base64");
|
|
1318
|
+
return result;
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
async function emulate(driver, args = {}) {
|
|
1322
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1323
|
+
if (args.clearOverrides) {
|
|
1324
|
+
await page.emulate({ clearOverrides: true });
|
|
1325
|
+
return { target: target3(page.info), applied: [], cleared: true };
|
|
992
1326
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
});
|
|
1003
|
-
});
|
|
1004
|
-
} finally {
|
|
1005
|
-
conn.close();
|
|
1327
|
+
const { applied } = await page.emulate(args);
|
|
1328
|
+
if (applied.length === 0)
|
|
1329
|
+
throw new SharedToolError("emulate: no overrides specified (pass clearOverrides:true to reset)");
|
|
1330
|
+
return { target: target3(page.info), applied };
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
async function resizePage(driver, args) {
|
|
1334
|
+
if (!Number.isFinite(args.width) || !Number.isFinite(args.height) || args.width <= 0 || args.height <= 0) {
|
|
1335
|
+
throw new SharedToolError("resize_page requires positive numeric width and height");
|
|
1006
1336
|
}
|
|
1337
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1338
|
+
await page.emulate({ width: args.width, height: args.height, deviceScaleFactor: args.deviceScaleFactor, mobile: args.mobile });
|
|
1339
|
+
const v = await page.evaluate("({ w: window.innerWidth, h: window.innerHeight })");
|
|
1340
|
+
return { target: target3(page.info), width: args.width, height: args.height, innerWidth: v?.w ?? args.width, innerHeight: v?.h ?? args.height };
|
|
1341
|
+
});
|
|
1007
1342
|
}
|
|
1343
|
+
async function handleDialog(driver, args) {
|
|
1344
|
+
if (args.accept === undefined)
|
|
1345
|
+
throw new SharedToolError("handle_dialog requires { accept }");
|
|
1346
|
+
return withPage2(driver, args.target, (page) => page.handleDialog(args.accept, args.promptText, { timeoutMs: args.timeoutMs, autoMs: args.autoMs }));
|
|
1347
|
+
}
|
|
1348
|
+
var SHARED_TOOLS = {
|
|
1349
|
+
list_pages: listPages,
|
|
1350
|
+
new_page: newPage,
|
|
1351
|
+
close_page: closePage,
|
|
1352
|
+
select_page: selectPage,
|
|
1353
|
+
navigate_page: navigatePage,
|
|
1354
|
+
wait_for: waitForText,
|
|
1355
|
+
evaluate_script: evaluateScript,
|
|
1356
|
+
take_snapshot: takeSnapshot,
|
|
1357
|
+
click,
|
|
1358
|
+
hover,
|
|
1359
|
+
drag,
|
|
1360
|
+
fill,
|
|
1361
|
+
fill_form: fillForm,
|
|
1362
|
+
type_text: typeText,
|
|
1363
|
+
press_key: pressKey,
|
|
1364
|
+
upload_file: uploadFile,
|
|
1365
|
+
take_screenshot: takeScreenshot,
|
|
1366
|
+
emulate,
|
|
1367
|
+
resize_page: resizePage,
|
|
1368
|
+
handle_dialog: handleDialog
|
|
1369
|
+
};
|
|
1008
1370
|
|
|
1009
1371
|
// src/tools/console.ts
|
|
1010
1372
|
import { readFile } from "node:fs/promises";
|
|
1011
1373
|
|
|
1012
1374
|
// src/tools/recorder.ts
|
|
1013
|
-
import { mkdir as
|
|
1375
|
+
import { mkdir as mkdir2, appendFile, writeFile as writeFile2, copyFile } from "node:fs/promises";
|
|
1014
1376
|
var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1015
1377
|
function recFile(targetId) {
|
|
1016
1378
|
return `${ARTIFACT_DIR2}/rec-${targetId}.jsonl`;
|
|
@@ -1031,11 +1393,11 @@ var CONSOLE_EVENTS = ["Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Lo
|
|
|
1031
1393
|
async function startRecorder(target, opts = {}) {
|
|
1032
1394
|
const network = opts.network ?? true;
|
|
1033
1395
|
const consoleCap = opts.console ?? true;
|
|
1034
|
-
await
|
|
1396
|
+
await mkdir2(ARTIFACT_DIR2, { recursive: true });
|
|
1035
1397
|
const { conn, target: resolved } = await openPage(target, { timeoutMs: opts.timeoutMs });
|
|
1036
1398
|
const file = opts.file ?? recFile(resolved.id);
|
|
1037
1399
|
if (opts.truncate) {
|
|
1038
|
-
await
|
|
1400
|
+
await writeFile2(file, "");
|
|
1039
1401
|
}
|
|
1040
1402
|
let chain = Promise.resolve();
|
|
1041
1403
|
let dropped = 0;
|
|
@@ -1382,7 +1744,7 @@ async function getNetworkRequest(args = {}) {
|
|
|
1382
1744
|
}
|
|
1383
1745
|
|
|
1384
1746
|
// src/tools/performance.ts
|
|
1385
|
-
import { mkdir as
|
|
1747
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3, stat, unlink } from "node:fs/promises";
|
|
1386
1748
|
import { join as join2 } from "node:path";
|
|
1387
1749
|
var ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1388
1750
|
var STATE_DIR2 = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
@@ -1397,7 +1759,7 @@ var liveTraces = new Map;
|
|
|
1397
1759
|
function stamp2() {
|
|
1398
1760
|
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
1399
1761
|
}
|
|
1400
|
-
function
|
|
1762
|
+
function shortId(id) {
|
|
1401
1763
|
return id.slice(0, 8);
|
|
1402
1764
|
}
|
|
1403
1765
|
function stateFilePath() {
|
|
@@ -1423,10 +1785,10 @@ async function endTrace(conn, sink, timeoutMs = 30000) {
|
|
|
1423
1785
|
return sink;
|
|
1424
1786
|
}
|
|
1425
1787
|
async function writeTrace(events, targetId) {
|
|
1426
|
-
await
|
|
1427
|
-
const path = join2(ARTIFACT_DIR3, `trace-${
|
|
1788
|
+
await mkdir3(ARTIFACT_DIR3, { recursive: true });
|
|
1789
|
+
const path = join2(ARTIFACT_DIR3, `trace-${shortId(targetId)}-${stamp2()}.json`);
|
|
1428
1790
|
const body = JSON.stringify({ traceEvents: events, metadata: { source: "cdp-toolkit" } });
|
|
1429
|
-
await
|
|
1791
|
+
await writeFile3(path, body, "utf8");
|
|
1430
1792
|
const { size } = await stat(path);
|
|
1431
1793
|
return { path, bytes: size };
|
|
1432
1794
|
}
|
|
@@ -1435,7 +1797,7 @@ async function performanceStartTrace(args = {}) {
|
|
|
1435
1797
|
const { conn, target } = await openPage(args.target);
|
|
1436
1798
|
if (liveTraces.has(target.id)) {
|
|
1437
1799
|
conn.close();
|
|
1438
|
-
throw new CdpError(`a trace is already in progress for target ${
|
|
1800
|
+
throw new CdpError(`a trace is already in progress for target ${shortId(target.id)}; call performanceStopTrace first`);
|
|
1439
1801
|
}
|
|
1440
1802
|
const events = [];
|
|
1441
1803
|
bufferTraceData(conn, events);
|
|
@@ -1446,7 +1808,7 @@ async function performanceStartTrace(args = {}) {
|
|
|
1446
1808
|
throw err;
|
|
1447
1809
|
}
|
|
1448
1810
|
liveTraces.set(target.id, { conn, target, events, startedAt: Date.now() });
|
|
1449
|
-
await
|
|
1811
|
+
await mkdir3(STATE_DIR2, { recursive: true });
|
|
1450
1812
|
const state = {
|
|
1451
1813
|
targetId: target.id,
|
|
1452
1814
|
url: target.url,
|
|
@@ -1454,7 +1816,7 @@ async function performanceStartTrace(args = {}) {
|
|
|
1454
1816
|
pid: process.pid,
|
|
1455
1817
|
categories
|
|
1456
1818
|
};
|
|
1457
|
-
await
|
|
1819
|
+
await writeFile3(stateFilePath(), JSON.stringify(state), "utf8");
|
|
1458
1820
|
return {
|
|
1459
1821
|
target: { id: target.id, url: target.url, title: target.title },
|
|
1460
1822
|
categories,
|
|
@@ -1469,7 +1831,7 @@ async function performanceStopTrace(args = {}) {
|
|
|
1469
1831
|
try {
|
|
1470
1832
|
const raw = await readFile3(stateFilePath(), "utf8");
|
|
1471
1833
|
const state = JSON.parse(raw);
|
|
1472
|
-
hint = state.pid === process.pid ? ` (state file lists target ${
|
|
1834
|
+
hint = state.pid === process.pid ? ` (state file lists target ${shortId(state.targetId)} but its live connection is gone)` : ` (trace was started by a DIFFERENT process pid=${state.pid}; live recordings cannot cross process boundaries)`;
|
|
1473
1835
|
} catch {}
|
|
1474
1836
|
throw new CdpError(`no in-process trace to stop${hint}. Use performanceTrace for a robust single-call trace.`);
|
|
1475
1837
|
}
|
|
@@ -1614,7 +1976,7 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
1614
1976
|
}
|
|
1615
1977
|
|
|
1616
1978
|
// src/tools/heap.ts
|
|
1617
|
-
import { mkdir as
|
|
1979
|
+
import { mkdir as mkdir4, writeFile as writeFile4 } from "node:fs/promises";
|
|
1618
1980
|
import { join as join3 } from "node:path";
|
|
1619
1981
|
var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1620
1982
|
function stamp3(targetId) {
|
|
@@ -1640,9 +2002,9 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
1640
2002
|
if (snapshot.length === 0) {
|
|
1641
2003
|
throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
|
|
1642
2004
|
}
|
|
1643
|
-
await
|
|
2005
|
+
await mkdir4(ARTIFACT_DIR4, { recursive: true });
|
|
1644
2006
|
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join3(ARTIFACT_DIR4, args.savePath) : join3(ARTIFACT_DIR4, `take_heapsnapshot-${stamp3(target.id)}.heapsnapshot`);
|
|
1645
|
-
await
|
|
2007
|
+
await writeFile4(path, snapshot, "utf8");
|
|
1646
2008
|
const bytes = Buffer.byteLength(snapshot, "utf8");
|
|
1647
2009
|
return {
|
|
1648
2010
|
path,
|
|
@@ -1655,7 +2017,7 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
1655
2017
|
|
|
1656
2018
|
// src/tools/lighthouse.ts
|
|
1657
2019
|
import { spawn } from "node:child_process";
|
|
1658
|
-
import { mkdir as
|
|
2020
|
+
import { mkdir as mkdir5, readFile as readFile4, stat as stat2 } from "node:fs/promises";
|
|
1659
2021
|
var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1660
2022
|
function cdpPort() {
|
|
1661
2023
|
const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
@@ -1716,7 +2078,7 @@ async function lighthouseAudit(args) {
|
|
|
1716
2078
|
const url = args.url.trim();
|
|
1717
2079
|
const timeoutMs = args.timeoutMs ?? 120000;
|
|
1718
2080
|
const port = cdpPort();
|
|
1719
|
-
await
|
|
2081
|
+
await mkdir5(ARTIFACT_DIR5, { recursive: true });
|
|
1720
2082
|
const outPath = args.savePath ?? `${ARTIFACT_DIR5}/lighthouse-${shortHost(url)}-${stamp4()}.json`;
|
|
1721
2083
|
const formFactor = args.formFactor ?? "desktop";
|
|
1722
2084
|
const lhArgs = [
|
|
@@ -1789,205 +2151,32 @@ function truncate(s, max = 600) {
|
|
|
1789
2151
|
return t.length > max ? `${t.slice(0, max)}…` : t;
|
|
1790
2152
|
}
|
|
1791
2153
|
|
|
1792
|
-
// src/tools/network_mock.ts
|
|
1793
|
-
function escapeRegExpChar(ch) {
|
|
1794
|
-
return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
|
|
1795
|
-
}
|
|
1796
|
-
function globToRegExp(pattern) {
|
|
1797
|
-
let re = "";
|
|
1798
|
-
for (let i = 0;i < pattern.length; i++) {
|
|
1799
|
-
const ch = pattern[i];
|
|
1800
|
-
if (ch === "\\") {
|
|
1801
|
-
const next = pattern[i + 1];
|
|
1802
|
-
if (next !== undefined) {
|
|
1803
|
-
re += escapeRegExpChar(next);
|
|
1804
|
-
i++;
|
|
1805
|
-
} else {
|
|
1806
|
-
re += "\\\\";
|
|
1807
|
-
}
|
|
1808
|
-
} else if (ch === "*") {
|
|
1809
|
-
re += ".*";
|
|
1810
|
-
} else if (ch === "?") {
|
|
1811
|
-
re += ".";
|
|
1812
|
-
} else {
|
|
1813
|
-
re += escapeRegExpChar(ch);
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
return new RegExp(`^${re}$`);
|
|
1817
|
-
}
|
|
1818
|
-
function urlMatches(pattern, url) {
|
|
1819
|
-
return globToRegExp(pattern).test(url);
|
|
1820
|
-
}
|
|
1821
|
-
function selectRule(rules, url, method) {
|
|
1822
|
-
return rules.find((r) => urlMatches(r.urlPattern, url) && (!r.method || r.method.toUpperCase() === method.toUpperCase()));
|
|
1823
|
-
}
|
|
1824
|
-
function buildFulfillParams(requestId, rule) {
|
|
1825
|
-
const merged = new Map;
|
|
1826
|
-
merged.set("content-type", { name: "Content-Type", value: rule.contentType ?? "application/json" });
|
|
1827
|
-
for (const [name, value] of Object.entries(rule.headers ?? {})) {
|
|
1828
|
-
merged.set(name.toLowerCase(), { name, value });
|
|
1829
|
-
}
|
|
1830
|
-
return {
|
|
1831
|
-
requestId,
|
|
1832
|
-
responseCode: rule.status ?? 200,
|
|
1833
|
-
responseHeaders: [...merged.values()],
|
|
1834
|
-
body: Buffer.from(rule.body ?? "", "utf8").toString("base64")
|
|
1835
|
-
};
|
|
1836
|
-
}
|
|
1837
|
-
function effectiveAction(rule, rnd) {
|
|
1838
|
-
if (rule.failRate !== undefined && rule.failRate > 0 && rnd < rule.failRate)
|
|
1839
|
-
return "fail";
|
|
1840
|
-
return rule.action;
|
|
1841
|
-
}
|
|
1842
|
-
var sessions = new Map;
|
|
1843
|
-
function delay3(ms) {
|
|
1844
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
1845
|
-
}
|
|
1846
|
-
function registerHandler(session) {
|
|
1847
|
-
session.conn.on("Fetch.requestPaused", (params) => {
|
|
1848
|
-
const p = params;
|
|
1849
|
-
session.chain = session.chain.then(async () => {
|
|
1850
|
-
const rule = selectRule(session.rules, p.request.url, p.request.method);
|
|
1851
|
-
if (!rule) {
|
|
1852
|
-
await session.conn.send("Fetch.continueRequest", { requestId: p.requestId });
|
|
1853
|
-
return;
|
|
1854
|
-
}
|
|
1855
|
-
if (rule.delayMs)
|
|
1856
|
-
await delay3(rule.delayMs);
|
|
1857
|
-
const act = effectiveAction(rule, Math.random());
|
|
1858
|
-
if (act === "fulfill") {
|
|
1859
|
-
await session.conn.send("Fetch.fulfillRequest", { ...buildFulfillParams(p.requestId, rule) });
|
|
1860
|
-
} else if (act === "fail") {
|
|
1861
|
-
await session.conn.send("Fetch.failRequest", { requestId: p.requestId, errorReason: rule.errorReason ?? "Failed" });
|
|
1862
|
-
} else {
|
|
1863
|
-
await session.conn.send("Fetch.continueRequest", { requestId: p.requestId });
|
|
1864
|
-
}
|
|
1865
|
-
rule.hits++;
|
|
1866
|
-
session.intercepted.push({ url: p.request.url, method: p.request.method, action: act });
|
|
1867
|
-
}).catch((e) => {
|
|
1868
|
-
if (/closed|connection|Target|Inspected/i.test(String(e)))
|
|
1869
|
-
session.dead = true;
|
|
1870
|
-
});
|
|
1871
|
-
});
|
|
1872
|
-
}
|
|
1873
|
-
async function mockRequest(args) {
|
|
1874
|
-
if (!args.urlPattern)
|
|
1875
|
-
throw new CdpError("mock_request requires a { urlPattern }");
|
|
1876
|
-
const rule = {
|
|
1877
|
-
urlPattern: args.urlPattern,
|
|
1878
|
-
action: args.action ?? "fulfill",
|
|
1879
|
-
status: args.status,
|
|
1880
|
-
body: args.body,
|
|
1881
|
-
contentType: args.contentType,
|
|
1882
|
-
headers: args.headers,
|
|
1883
|
-
errorReason: args.errorReason,
|
|
1884
|
-
method: args.method,
|
|
1885
|
-
delayMs: args.delayMs,
|
|
1886
|
-
failRate: args.failRate,
|
|
1887
|
-
hits: 0
|
|
1888
|
-
};
|
|
1889
|
-
const resolved = await resolveTarget(args.target);
|
|
1890
|
-
let session = sessions.get(resolved.id);
|
|
1891
|
-
if (session && !session.dead) {
|
|
1892
|
-
const idx = session.rules.findIndex((r) => r.urlPattern === rule.urlPattern && (r.method ?? "") === (rule.method ?? ""));
|
|
1893
|
-
if (idx >= 0)
|
|
1894
|
-
session.rules[idx] = rule;
|
|
1895
|
-
else
|
|
1896
|
-
session.rules.push(rule);
|
|
1897
|
-
await session.conn.send("Fetch.enable", { patterns: session.rules.map((r) => ({ urlPattern: r.urlPattern })) });
|
|
1898
|
-
} else {
|
|
1899
|
-
const { conn, target } = await openPage(resolved.id);
|
|
1900
|
-
session = { conn, target, rules: [rule], intercepted: [], chain: Promise.resolve(), dead: false };
|
|
1901
|
-
registerHandler(session);
|
|
1902
|
-
await conn.send("Fetch.enable", { patterns: [{ urlPattern: rule.urlPattern }] });
|
|
1903
|
-
sessions.set(target.id, session);
|
|
1904
|
-
}
|
|
1905
|
-
let reloaded = false;
|
|
1906
|
-
if (args.reload) {
|
|
1907
|
-
await session.conn.send("Page.enable");
|
|
1908
|
-
await session.conn.send("Page.reload", { ignoreCache: true });
|
|
1909
|
-
await delay3(800);
|
|
1910
|
-
reloaded = true;
|
|
1911
|
-
}
|
|
1912
|
-
return {
|
|
1913
|
-
target: { id: session.target.id, url: session.target.url, title: session.target.title },
|
|
1914
|
-
pattern: rule.urlPattern,
|
|
1915
|
-
ruleCount: session.rules.length,
|
|
1916
|
-
reloaded
|
|
1917
|
-
};
|
|
1918
|
-
}
|
|
1919
|
-
async function listMocks(_args = {}) {
|
|
1920
|
-
for (const [id, s] of sessions) {
|
|
1921
|
-
if (s.dead) {
|
|
1922
|
-
try {
|
|
1923
|
-
s.conn.close();
|
|
1924
|
-
} catch {}
|
|
1925
|
-
sessions.delete(id);
|
|
1926
|
-
continue;
|
|
1927
|
-
}
|
|
1928
|
-
try {
|
|
1929
|
-
await s.conn.send("Runtime.evaluate", { expression: "1", returnByValue: true }, { timeoutMs: 3000 });
|
|
1930
|
-
} catch {
|
|
1931
|
-
try {
|
|
1932
|
-
s.conn.close();
|
|
1933
|
-
} catch {}
|
|
1934
|
-
sessions.delete(id);
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
const mocks = [...sessions.values()].map((s) => ({
|
|
1938
|
-
target: { id: s.target.id, url: s.target.url, title: s.target.title },
|
|
1939
|
-
rules: s.rules.map((r) => ({ urlPattern: r.urlPattern, action: r.action, method: r.method, hits: r.hits })),
|
|
1940
|
-
hits: s.intercepted.length
|
|
1941
|
-
}));
|
|
1942
|
-
return { count: mocks.length, mocks };
|
|
1943
|
-
}
|
|
1944
|
-
async function clearMocks(args = {}) {
|
|
1945
|
-
let ids;
|
|
1946
|
-
if (args.all) {
|
|
1947
|
-
ids = [...sessions.keys()];
|
|
1948
|
-
} else {
|
|
1949
|
-
const resolved = await resolveTarget(args.target);
|
|
1950
|
-
ids = sessions.has(resolved.id) ? [resolved.id] : [];
|
|
1951
|
-
}
|
|
1952
|
-
let cleared = 0;
|
|
1953
|
-
for (const id of ids) {
|
|
1954
|
-
const s = sessions.get(id);
|
|
1955
|
-
if (!s)
|
|
1956
|
-
continue;
|
|
1957
|
-
try {
|
|
1958
|
-
await s.conn.send("Fetch.disable");
|
|
1959
|
-
} catch {}
|
|
1960
|
-
try {
|
|
1961
|
-
s.conn.close();
|
|
1962
|
-
} catch {}
|
|
1963
|
-
sessions.delete(id);
|
|
1964
|
-
cleared++;
|
|
1965
|
-
}
|
|
1966
|
-
return { cleared };
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
2154
|
// src/index.ts
|
|
2155
|
+
var cdpDriver = createCdpDriver();
|
|
2156
|
+
function onCdp(fn) {
|
|
2157
|
+
return (args) => fn(cdpDriver, args);
|
|
2158
|
+
}
|
|
1970
2159
|
var TOOLS = {
|
|
1971
|
-
list_pages:
|
|
1972
|
-
new_page:
|
|
1973
|
-
close_page:
|
|
1974
|
-
select_page:
|
|
1975
|
-
navigate_page:
|
|
1976
|
-
wait_for:
|
|
1977
|
-
evaluate_script:
|
|
1978
|
-
take_snapshot:
|
|
1979
|
-
click,
|
|
1980
|
-
hover,
|
|
1981
|
-
drag,
|
|
1982
|
-
fill,
|
|
1983
|
-
fill_form:
|
|
1984
|
-
type_text:
|
|
1985
|
-
press_key:
|
|
1986
|
-
upload_file:
|
|
1987
|
-
take_screenshot:
|
|
1988
|
-
emulate,
|
|
1989
|
-
resize_page:
|
|
1990
|
-
handle_dialog:
|
|
2160
|
+
list_pages: onCdp(SHARED_TOOLS.list_pages),
|
|
2161
|
+
new_page: onCdp(SHARED_TOOLS.new_page),
|
|
2162
|
+
close_page: onCdp(SHARED_TOOLS.close_page),
|
|
2163
|
+
select_page: onCdp(SHARED_TOOLS.select_page),
|
|
2164
|
+
navigate_page: onCdp(SHARED_TOOLS.navigate_page),
|
|
2165
|
+
wait_for: onCdp(SHARED_TOOLS.wait_for),
|
|
2166
|
+
evaluate_script: onCdp(SHARED_TOOLS.evaluate_script),
|
|
2167
|
+
take_snapshot: onCdp(SHARED_TOOLS.take_snapshot),
|
|
2168
|
+
click: onCdp(SHARED_TOOLS.click),
|
|
2169
|
+
hover: onCdp(SHARED_TOOLS.hover),
|
|
2170
|
+
drag: onCdp(SHARED_TOOLS.drag),
|
|
2171
|
+
fill: onCdp(SHARED_TOOLS.fill),
|
|
2172
|
+
fill_form: onCdp(SHARED_TOOLS.fill_form),
|
|
2173
|
+
type_text: onCdp(SHARED_TOOLS.type_text),
|
|
2174
|
+
press_key: onCdp(SHARED_TOOLS.press_key),
|
|
2175
|
+
upload_file: onCdp(SHARED_TOOLS.upload_file),
|
|
2176
|
+
take_screenshot: onCdp(SHARED_TOOLS.take_screenshot),
|
|
2177
|
+
emulate: onCdp(SHARED_TOOLS.emulate),
|
|
2178
|
+
resize_page: onCdp(SHARED_TOOLS.resize_page),
|
|
2179
|
+
handle_dialog: onCdp(SHARED_TOOLS.handle_dialog),
|
|
1991
2180
|
list_console_messages: listConsoleMessages,
|
|
1992
2181
|
get_console_message: getConsoleMessage,
|
|
1993
2182
|
list_network_requests: listNetworkRequests,
|