cdp-toolkit 1.7.0 → 1.8.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/CHANGELOG.md +284 -0
- package/CONTRACT.md +5 -1
- package/README.md +35 -13
- package/dist/cli.js +973 -56
- package/dist/index.js +834 -44
- package/dist/mcp.js +1213 -66
- package/package.json +5 -3
- package/src/activity.ts +533 -0
- package/src/bidi/driver.ts +231 -8
- package/src/capabilities.ts +1 -1
- package/src/cdp/driver.ts +472 -25
- package/src/driver.ts +161 -3
- package/src/index.ts +26 -4
- package/src/leases-tools.ts +125 -4
- package/src/leases.ts +1 -1
- package/src/manifest.ts +239 -10
- package/src/mcp.ts +6 -1
- package/src/reap.ts +55 -4
- package/src/shared-tools.ts +268 -27
- package/src/tools/browser-session.ts +246 -0
- package/src/tools/dispatch-mouse.ts +108 -0
- package/src/tools/downloads.ts +143 -0
- package/src/tools/permissions.ts +130 -0
package/dist/index.js
CHANGED
|
@@ -484,6 +484,141 @@ async function withPage(selector, fn, opts = {}) {
|
|
|
484
484
|
}
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
+
// src/activity.ts
|
|
488
|
+
var BEACON_DATA_GLOBAL = "__cdpToolkitLastInput";
|
|
489
|
+
var BEACON_INSTALLED_GLOBAL = "__cdpToolkitBeacon";
|
|
490
|
+
var BEACON_EVENTS = ["pointerdown", "mousedown", "keydown", "wheel", "touchstart"];
|
|
491
|
+
var BEACON_BODY = `
|
|
492
|
+
if (!window.${BEACON_INSTALLED_GLOBAL}) {
|
|
493
|
+
window.${BEACON_INSTALLED_GLOBAL} = true;
|
|
494
|
+
var mark = function () { window.${BEACON_DATA_GLOBAL} = Date.now(); };
|
|
495
|
+
var events = ${JSON.stringify([...BEACON_EVENTS])};
|
|
496
|
+
for (var i = 0; i < events.length; i++) {
|
|
497
|
+
window.addEventListener(events[i], mark, { capture: true, passive: true });
|
|
498
|
+
}
|
|
499
|
+
}`;
|
|
500
|
+
var BEACON_SOURCE = `(function () {${BEACON_BODY}
|
|
501
|
+
})();`;
|
|
502
|
+
var BEACON_FUNCTION_DECLARATION = `function () {${BEACON_BODY}
|
|
503
|
+
}`;
|
|
504
|
+
var BEACON_READ_EXPRESSION = `(typeof window.${BEACON_DATA_GLOBAL} === "number" ? window.${BEACON_DATA_GLOBAL} : null)`;
|
|
505
|
+
var DISPATCH_ATTRIBUTION_WINDOW_MS = 1500;
|
|
506
|
+
function dispatchKey(backend, targetId) {
|
|
507
|
+
return `${backend}:${targetId}`;
|
|
508
|
+
}
|
|
509
|
+
var dispatchLog = new Map;
|
|
510
|
+
var DISPATCH_LOG_LIMIT = 512;
|
|
511
|
+
function recordDispatch(backend, targetId, now = Date.now()) {
|
|
512
|
+
const key = dispatchKey(backend, targetId);
|
|
513
|
+
dispatchLog.delete(key);
|
|
514
|
+
dispatchLog.set(key, now);
|
|
515
|
+
while (dispatchLog.size > DISPATCH_LOG_LIMIT) {
|
|
516
|
+
const oldest = dispatchLog.keys().next();
|
|
517
|
+
if (oldest.done)
|
|
518
|
+
break;
|
|
519
|
+
dispatchLog.delete(oldest.value);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
var NON_SYNTHESIZING_INPUT_METHODS = new Set(["Input.setInterceptDrags", "Input.setIgnoreInputEvents"]);
|
|
523
|
+
async function sendInput(conn, backend, targetId, method, params = {}) {
|
|
524
|
+
if (!NON_SYNTHESIZING_INPUT_METHODS.has(method))
|
|
525
|
+
recordDispatch(backend, targetId);
|
|
526
|
+
return conn.send(method, params);
|
|
527
|
+
}
|
|
528
|
+
function lastDispatchAt(backend, targetId) {
|
|
529
|
+
return dispatchLog.get(dispatchKey(backend, targetId));
|
|
530
|
+
}
|
|
531
|
+
function isHumanAttributed(beaconTs, dispatchAt, windowMs = DISPATCH_ATTRIBUTION_WINDOW_MS) {
|
|
532
|
+
if (typeof beaconTs !== "number" || !Number.isFinite(beaconTs))
|
|
533
|
+
return false;
|
|
534
|
+
return beaconTs > (dispatchAt ?? 0) + windowMs;
|
|
535
|
+
}
|
|
536
|
+
function humanActiveMs(beaconTs, dispatchAt, now = Date.now(), windowMs = DISPATCH_ATTRIBUTION_WINDOW_MS) {
|
|
537
|
+
if (!isHumanAttributed(beaconTs, dispatchAt, windowMs))
|
|
538
|
+
return null;
|
|
539
|
+
return Math.max(0, now - beaconTs);
|
|
540
|
+
}
|
|
541
|
+
var CONTENTION_WINDOW_MS = 30000;
|
|
542
|
+
function contentionWarning(ms, opts) {
|
|
543
|
+
if (!opts.takeover)
|
|
544
|
+
return;
|
|
545
|
+
if (ms === null || ms >= CONTENTION_WINDOW_MS)
|
|
546
|
+
return;
|
|
547
|
+
return `a person appears to be using this tab right now: input not dispatched by this server landed ${ms}ms ago. ` + `The claim SUCCEEDED and you hold the lease — this is a warning, not a refusal. ` + `Driving the tab now will fight them for the keyboard and mouse. ` + `Consider claim_page with no target to work in your own tab instead, or ask before continuing.`;
|
|
548
|
+
}
|
|
549
|
+
function beaconSupported(driver) {
|
|
550
|
+
return typeof driver.installActivityBeacon === "function" && typeof driver.readActivityBeacon === "function";
|
|
551
|
+
}
|
|
552
|
+
async function installBeacon(driver, targetId) {
|
|
553
|
+
if (typeof driver.installActivityBeacon !== "function")
|
|
554
|
+
return false;
|
|
555
|
+
return driver.installActivityBeacon(targetId).catch(() => false);
|
|
556
|
+
}
|
|
557
|
+
async function readHumanActiveMs(driver, backend, targetId, now = Date.now()) {
|
|
558
|
+
if (typeof driver.readActivityBeacon !== "function")
|
|
559
|
+
return null;
|
|
560
|
+
const beaconTs = await driver.readActivityBeacon(targetId).catch(() => null);
|
|
561
|
+
return humanActiveMs(beaconTs, lastDispatchAt(backend, targetId), now);
|
|
562
|
+
}
|
|
563
|
+
var RENDERER_PROBE_TIMEOUT_MS = 500;
|
|
564
|
+
var RENDERER_PROBE_EXPRESSION = `[1, ${BEACON_READ_EXPRESSION}]`;
|
|
565
|
+
function rendererProbeSupported(driver) {
|
|
566
|
+
return typeof driver.probeRenderer === "function";
|
|
567
|
+
}
|
|
568
|
+
async function probeRendererActivity(driver, backend, targetId, now = Date.now()) {
|
|
569
|
+
if (typeof driver.probeRenderer !== "function")
|
|
570
|
+
return { responsive: false };
|
|
571
|
+
const { responsive, beaconTs } = await driver.probeRenderer(targetId, RENDERER_PROBE_TIMEOUT_MS).catch(() => ({ responsive: false, beaconTs: null }));
|
|
572
|
+
const ms = humanActiveMs(beaconTs, lastDispatchAt(backend, targetId), now);
|
|
573
|
+
return ms === null ? { responsive } : { responsive, humanActiveMs: ms };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
class BeaconSessions {
|
|
577
|
+
close;
|
|
578
|
+
limit;
|
|
579
|
+
live = new Map;
|
|
580
|
+
constructor(close, limit = 32) {
|
|
581
|
+
this.close = close;
|
|
582
|
+
this.limit = limit;
|
|
583
|
+
}
|
|
584
|
+
get(key) {
|
|
585
|
+
return this.live.get(key);
|
|
586
|
+
}
|
|
587
|
+
set(key, value) {
|
|
588
|
+
const previous = this.live.get(key);
|
|
589
|
+
if (previous !== undefined) {
|
|
590
|
+
this.live.delete(key);
|
|
591
|
+
this.safeClose(previous);
|
|
592
|
+
}
|
|
593
|
+
this.live.set(key, value);
|
|
594
|
+
while (this.live.size > this.limit) {
|
|
595
|
+
const oldest = this.live.keys().next();
|
|
596
|
+
if (oldest.done)
|
|
597
|
+
break;
|
|
598
|
+
this.drop(oldest.value);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
drop(key) {
|
|
602
|
+
const value = this.live.get(key);
|
|
603
|
+
if (value === undefined)
|
|
604
|
+
return;
|
|
605
|
+
this.live.delete(key);
|
|
606
|
+
this.safeClose(value);
|
|
607
|
+
}
|
|
608
|
+
clear() {
|
|
609
|
+
for (const key of [...this.live.keys()])
|
|
610
|
+
this.drop(key);
|
|
611
|
+
}
|
|
612
|
+
get size() {
|
|
613
|
+
return this.live.size;
|
|
614
|
+
}
|
|
615
|
+
safeClose(value) {
|
|
616
|
+
try {
|
|
617
|
+
this.close(value);
|
|
618
|
+
} catch {}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
487
622
|
// src/tools/snapshot.ts
|
|
488
623
|
var INTERACTIVE_ROLES = new Set([
|
|
489
624
|
"button",
|
|
@@ -707,11 +842,22 @@ var REQUIRED_CAPABILITIES = {
|
|
|
707
842
|
stop_screen_recording: ["capture.screencast"],
|
|
708
843
|
mock_request: ["network.intercept"],
|
|
709
844
|
list_mocks: ["network.intercept"],
|
|
710
|
-
clear_mocks: ["network.intercept"]
|
|
845
|
+
clear_mocks: ["network.intercept"],
|
|
846
|
+
dispatch_mouse: ["input.raw"],
|
|
847
|
+
wait_for_download: ["browser.downloads"],
|
|
848
|
+
grant_permissions: ["browser.permissions"]
|
|
711
849
|
};
|
|
712
850
|
function isDriverError(e) {
|
|
713
851
|
return e instanceof Error && typeof e.code === "string";
|
|
714
852
|
}
|
|
853
|
+
function interpolatePoints(from, to, steps) {
|
|
854
|
+
const out = [];
|
|
855
|
+
for (let i = 1;i < steps; i++) {
|
|
856
|
+
out.push({ x: from.x + (to.x - from.x) * i / steps, y: from.y + (to.y - from.y) * i / steps });
|
|
857
|
+
}
|
|
858
|
+
out.push({ x: to.x, y: to.y });
|
|
859
|
+
return out;
|
|
860
|
+
}
|
|
715
861
|
|
|
716
862
|
// src/cdp/driver.ts
|
|
717
863
|
function driverError(code, message, data) {
|
|
@@ -767,12 +913,21 @@ async function centerOf(conn, objectId) {
|
|
|
767
913
|
async function focusElement(conn, objectId) {
|
|
768
914
|
await conn.send("Runtime.callFunctionOn", { objectId, functionDeclaration: "function(){this.focus&&this.focus();}", returnByValue: true });
|
|
769
915
|
}
|
|
770
|
-
|
|
916
|
+
var ARM_SCROLL_SETTLE_WATCH = "(function(){window.__cdpScrollSettle=new Promise((resolve)=>{let t;const done=()=>{window.removeEventListener('scroll',on,true);resolve(true);};" + "const on=()=>{clearTimeout(t);t=setTimeout(done,60);};window.addEventListener('scroll',on,true);t=setTimeout(done,60);setTimeout(done,500);});})()";
|
|
917
|
+
async function armScrollSettleWatch(conn) {
|
|
918
|
+
await conn.send("Runtime.evaluate", { expression: ARM_SCROLL_SETTLE_WATCH, returnByValue: true });
|
|
919
|
+
}
|
|
920
|
+
async function awaitScrollSettle(conn) {
|
|
921
|
+
await conn.send("Runtime.evaluate", { expression: "window.__cdpScrollSettle", awaitPromise: true, returnByValue: true }).catch(() => {
|
|
922
|
+
return;
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
async function setValueOnObject(conn, objectId, value, sendInput2) {
|
|
771
926
|
await focusElement(conn, objectId);
|
|
772
927
|
const fn = "function(){if('value'in this){this.value='';this.dispatchEvent(new Event('input',{bubbles:true}));}else if(this.isContentEditable){this.textContent='';}}";
|
|
773
928
|
await conn.send("Runtime.callFunctionOn", { objectId, functionDeclaration: fn, returnByValue: true });
|
|
774
929
|
if (value.length)
|
|
775
|
-
await
|
|
930
|
+
await sendInput2("Input.insertText", { text: value });
|
|
776
931
|
}
|
|
777
932
|
var NAMED_KEYS = {
|
|
778
933
|
enter: { key: "Enter", code: "Enter", keyCode: 13, text: "\r" },
|
|
@@ -797,6 +952,18 @@ var NAMED_KEYS = {
|
|
|
797
952
|
pagedown: { key: "PageDown", code: "PageDown", keyCode: 34 }
|
|
798
953
|
};
|
|
799
954
|
var MODIFIER_BITS = { alt: 1, control: 2, ctrl: 2, meta: 4, cmd: 4, command: 4, shift: 8 };
|
|
955
|
+
var CLICK_MODIFIER_BITS = { Alt: 1, Control: 2, Meta: 4, Shift: 8 };
|
|
956
|
+
function inputModifierBits(mods) {
|
|
957
|
+
let bits = 0;
|
|
958
|
+
for (const m of mods ?? []) {
|
|
959
|
+
const bit = CLICK_MODIFIER_BITS[m];
|
|
960
|
+
if (bit === undefined)
|
|
961
|
+
throw driverError("page-error", `unknown modifier '${m}' (use Alt, Control, Meta, or Shift)`);
|
|
962
|
+
bits |= bit;
|
|
963
|
+
}
|
|
964
|
+
return bits;
|
|
965
|
+
}
|
|
966
|
+
var HTML5_DRAG_INTERCEPT_TIMEOUT_MS = 5000;
|
|
800
967
|
function resolveKey(key) {
|
|
801
968
|
const named = NAMED_KEYS[key.toLowerCase()];
|
|
802
969
|
if (named)
|
|
@@ -961,7 +1128,11 @@ var CDP_CAPABILITIES = new Set([
|
|
|
961
1128
|
"input.insertTextAtomic",
|
|
962
1129
|
"locate.text",
|
|
963
1130
|
"locate.xpath",
|
|
964
|
-
"capture.screencast"
|
|
1131
|
+
"capture.screencast",
|
|
1132
|
+
"input.raw",
|
|
1133
|
+
"input.html5Drag",
|
|
1134
|
+
"browser.downloads",
|
|
1135
|
+
"browser.permissions"
|
|
965
1136
|
]);
|
|
966
1137
|
|
|
967
1138
|
class CdpPageDriver {
|
|
@@ -981,6 +1152,9 @@ class CdpPageDriver {
|
|
|
981
1152
|
this.enabledDomains.add(name);
|
|
982
1153
|
await this.conn.send(`${name}.enable`);
|
|
983
1154
|
}
|
|
1155
|
+
async dispatchInput(method, params = {}) {
|
|
1156
|
+
return sendInput(this.conn, "chrome", this.info.id, method, params);
|
|
1157
|
+
}
|
|
984
1158
|
async ensureDomainSoft(name) {
|
|
985
1159
|
if (this.enabledDomains.has(name))
|
|
986
1160
|
return;
|
|
@@ -989,11 +1163,22 @@ class CdpPageDriver {
|
|
|
989
1163
|
return;
|
|
990
1164
|
});
|
|
991
1165
|
}
|
|
1166
|
+
async historyEntry(direction) {
|
|
1167
|
+
const h = await this.conn.send("Page.getNavigationHistory");
|
|
1168
|
+
const entries = h.entries ?? [];
|
|
1169
|
+
const entry = entries[h.currentIndex + (direction === "back" ? -1 : 1)];
|
|
1170
|
+
if (!entry) {
|
|
1171
|
+
throw driverError("page-error", `navigate: no history entry to go ${direction} to (at entry ${h.currentIndex + 1} of ${entries.length})`);
|
|
1172
|
+
}
|
|
1173
|
+
return entry;
|
|
1174
|
+
}
|
|
992
1175
|
async navigate(opts) {
|
|
993
1176
|
await this.ensureDomain("Page");
|
|
994
1177
|
const reload = opts.reload === true;
|
|
995
|
-
|
|
1178
|
+
const history = opts.history;
|
|
1179
|
+
if (!reload && !history && !opts.url)
|
|
996
1180
|
throw driverError("page-error", "navigate: 'url' is required unless reload:true");
|
|
1181
|
+
const entry = history ? await this.historyEntry(history) : undefined;
|
|
997
1182
|
const waitUntil = opts.waitUntil ?? "load";
|
|
998
1183
|
const milestoneMethod = waitUntil === "load" ? "Page.loadEventFired" : "Page.domContentEventFired";
|
|
999
1184
|
const milestone = this.conn.waitFor(milestoneMethod, undefined, opts.timeoutMs).then(() => waitUntil).catch(() => {
|
|
@@ -1011,6 +1196,10 @@ class CdpPageDriver {
|
|
|
1011
1196
|
contextId = tree?.frameTree?.frame?.id ?? "";
|
|
1012
1197
|
url = tree?.frameTree?.frame?.url ?? opts.url ?? "";
|
|
1013
1198
|
reloaded = true;
|
|
1199
|
+
} else if (entry) {
|
|
1200
|
+
await this.conn.send("Page.navigateToHistoryEntry", { entryId: entry.id }, opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {});
|
|
1201
|
+
contextId = "";
|
|
1202
|
+
url = entry.url;
|
|
1014
1203
|
} else {
|
|
1015
1204
|
const nav = await this.conn.send("Page.navigate", { url: opts.url }, opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {});
|
|
1016
1205
|
if (nav.errorText)
|
|
@@ -1019,7 +1208,20 @@ class CdpPageDriver {
|
|
|
1019
1208
|
url = opts.url;
|
|
1020
1209
|
}
|
|
1021
1210
|
const settled = await Promise.race([milestone, stopped]);
|
|
1022
|
-
|
|
1211
|
+
if (entry) {
|
|
1212
|
+
const tree = await this.conn.send("Page.getFrameTree").catch(() => {
|
|
1213
|
+
return;
|
|
1214
|
+
});
|
|
1215
|
+
contextId = tree?.frameTree?.frame?.id ?? contextId;
|
|
1216
|
+
url = tree?.frameTree?.frame?.url ?? url;
|
|
1217
|
+
}
|
|
1218
|
+
return {
|
|
1219
|
+
url,
|
|
1220
|
+
contextId,
|
|
1221
|
+
...reloaded ? { reloaded } : {},
|
|
1222
|
+
...history ? { traversed: history } : {},
|
|
1223
|
+
waitedFor: settled ?? "timeout"
|
|
1224
|
+
};
|
|
1023
1225
|
}
|
|
1024
1226
|
async waitForText(text, timeoutMs = 15000, pollMs = 250) {
|
|
1025
1227
|
await this.ensureDomain("Runtime");
|
|
@@ -1099,36 +1301,96 @@ class CdpPageDriver {
|
|
|
1099
1301
|
async hover(loc) {
|
|
1100
1302
|
const { objectId } = await resolveElementLocator(this.conn, loc);
|
|
1101
1303
|
const { x, y } = await centerOf(this.conn, objectId);
|
|
1102
|
-
await this.
|
|
1304
|
+
await this.dispatchInput("Input.dispatchMouseEvent", { type: "mouseMoved", x, y });
|
|
1103
1305
|
return { x, y };
|
|
1104
1306
|
}
|
|
1105
1307
|
async click(loc, opts) {
|
|
1106
1308
|
const button = opts?.button ?? "left";
|
|
1107
1309
|
const clickCount = opts?.clickCount ?? 1;
|
|
1310
|
+
const modifiers = inputModifierBits(opts?.modifiers);
|
|
1108
1311
|
const { x, y } = await this.hover(loc);
|
|
1109
|
-
await this.
|
|
1110
|
-
await this.
|
|
1312
|
+
await this.dispatchInput("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button, clickCount, modifiers });
|
|
1313
|
+
await this.dispatchInput("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button, clickCount, modifiers });
|
|
1314
|
+
return { x, y };
|
|
1315
|
+
}
|
|
1316
|
+
async scroll(anchor, opts) {
|
|
1317
|
+
await this.ensureDomain("Runtime");
|
|
1318
|
+
const { x, y } = await this.resolveScrollPoint(anchor);
|
|
1319
|
+
await armScrollSettleWatch(this.conn);
|
|
1320
|
+
await this.dispatchInput("Input.dispatchMouseEvent", { type: "mouseWheel", x, y, deltaX: opts.deltaX ?? 0, deltaY: opts.deltaY ?? 0 });
|
|
1321
|
+
await awaitScrollSettle(this.conn);
|
|
1111
1322
|
return { x, y };
|
|
1112
1323
|
}
|
|
1113
|
-
async
|
|
1324
|
+
async resolveScrollPoint(anchor) {
|
|
1325
|
+
if (anchor === undefined) {
|
|
1326
|
+
await this.ensureDomain("Runtime");
|
|
1327
|
+
const { result, exceptionDetails } = await this.conn.send("Runtime.evaluate", { expression: "({x: window.innerWidth / 2, y: window.innerHeight / 2})", returnByValue: true });
|
|
1328
|
+
if (exceptionDetails || !result.value)
|
|
1329
|
+
throw driverError("page-error", `could not measure viewport for scroll center: ${exceptionDetails?.text ?? "no value"}`);
|
|
1330
|
+
return result.value;
|
|
1331
|
+
}
|
|
1332
|
+
if ("x" in anchor)
|
|
1333
|
+
return anchor;
|
|
1334
|
+
const { objectId } = await resolveElementLocator(this.conn, anchor);
|
|
1335
|
+
return centerOf(this.conn, objectId);
|
|
1336
|
+
}
|
|
1337
|
+
async drag(from, to, opts) {
|
|
1114
1338
|
const fromPt = await this.hover(from);
|
|
1115
|
-
const
|
|
1116
|
-
const
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1339
|
+
const toPt = await this.resolveDragDestination(to, fromPt);
|
|
1340
|
+
const steps = Math.max(1, Math.trunc(opts?.steps ?? 2));
|
|
1341
|
+
if ((opts?.mode ?? "mouse") === "html5") {
|
|
1342
|
+
await this.html5Drag(fromPt, toPt, steps);
|
|
1343
|
+
return { from: fromPt, to: toPt };
|
|
1344
|
+
}
|
|
1345
|
+
await this.dispatchInput("Input.dispatchMouseEvent", { type: "mousePressed", x: fromPt.x, y: fromPt.y, button: "left", clickCount: 1 });
|
|
1346
|
+
for (const pt of interpolatePoints(fromPt, toPt, steps)) {
|
|
1347
|
+
await this.dispatchInput("Input.dispatchMouseEvent", { type: "mouseMoved", x: pt.x, y: pt.y, button: "left" });
|
|
1348
|
+
}
|
|
1349
|
+
await this.dispatchInput("Input.dispatchMouseEvent", { type: "mouseReleased", x: toPt.x, y: toPt.y, button: "left", clickCount: 1 });
|
|
1121
1350
|
return { from: fromPt, to: toPt };
|
|
1122
1351
|
}
|
|
1352
|
+
async resolveDragDestination(to, fromPt) {
|
|
1353
|
+
if ("dx" in to)
|
|
1354
|
+
return { x: fromPt.x + to.dx, y: fromPt.y + to.dy };
|
|
1355
|
+
if ("x" in to)
|
|
1356
|
+
return to;
|
|
1357
|
+
const { objectId } = await resolveElementLocator(this.conn, to);
|
|
1358
|
+
return centerOf(this.conn, objectId);
|
|
1359
|
+
}
|
|
1360
|
+
async html5Drag(fromPt, toPt, steps) {
|
|
1361
|
+
await this.dispatchInput("Input.setInterceptDrags", { enabled: true });
|
|
1362
|
+
try {
|
|
1363
|
+
const intercepted = this.conn.waitFor("Input.dragIntercepted", undefined, HTML5_DRAG_INTERCEPT_TIMEOUT_MS).catch(() => {
|
|
1364
|
+
return;
|
|
1365
|
+
});
|
|
1366
|
+
await this.dispatchInput("Input.dispatchMouseEvent", { type: "mousePressed", x: fromPt.x, y: fromPt.y, button: "left", clickCount: 1 });
|
|
1367
|
+
for (const pt of interpolatePoints(fromPt, toPt, steps)) {
|
|
1368
|
+
await this.dispatchInput("Input.dispatchMouseEvent", { type: "mouseMoved", x: pt.x, y: pt.y, button: "left" });
|
|
1369
|
+
}
|
|
1370
|
+
const event = await intercepted;
|
|
1371
|
+
if (!event?.data) {
|
|
1372
|
+
throw driverError("page-error", `drag mode:"html5": the page never started a drag (no Input.dragIntercepted within ${HTML5_DRAG_INTERCEPT_TIMEOUT_MS}ms). ` + 'The source element is probably not draggable — HTML5 drag-and-drop needs draggable="true" (or a native draggable element) ' + 'plus a dragstart handler. Use the default mode:"mouse" for widgets built on raw pointer events.');
|
|
1373
|
+
}
|
|
1374
|
+
const data = event.data;
|
|
1375
|
+
for (const type of ["dragEnter", "dragOver", "drop"]) {
|
|
1376
|
+
await this.dispatchInput("Input.dispatchDragEvent", { type, x: toPt.x, y: toPt.y, data });
|
|
1377
|
+
}
|
|
1378
|
+
await this.dispatchInput("Input.dispatchMouseEvent", { type: "mouseReleased", x: toPt.x, y: toPt.y, button: "left", clickCount: 1 });
|
|
1379
|
+
} finally {
|
|
1380
|
+
await this.dispatchInput("Input.setInterceptDrags", { enabled: false }).catch(() => {
|
|
1381
|
+
return;
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1123
1385
|
async setValue(loc, value) {
|
|
1124
1386
|
const { objectId } = await resolveElementLocator(this.conn, loc);
|
|
1125
|
-
await setValueOnObject(this.conn, objectId, value);
|
|
1387
|
+
await setValueOnObject(this.conn, objectId, value, (method, params) => this.dispatchInput(method, params));
|
|
1126
1388
|
}
|
|
1127
1389
|
async typeText(loc, text) {
|
|
1128
1390
|
const { objectId } = await resolveElementLocator(this.conn, loc);
|
|
1129
1391
|
await focusElement(this.conn, objectId);
|
|
1130
1392
|
if (text.length)
|
|
1131
|
-
await this.
|
|
1393
|
+
await this.dispatchInput("Input.insertText", { text });
|
|
1132
1394
|
}
|
|
1133
1395
|
async pressKey(press) {
|
|
1134
1396
|
const spec = resolveKey(press.key);
|
|
@@ -1143,8 +1405,8 @@ class CdpPageDriver {
|
|
|
1143
1405
|
const down = { type: "keyDown", key: spec.key, code: spec.code, windowsVirtualKeyCode: spec.keyCode, nativeVirtualKeyCode: spec.keyCode, modifiers: modBits };
|
|
1144
1406
|
if (spec.text && !suppressText)
|
|
1145
1407
|
down.text = spec.text;
|
|
1146
|
-
await this.
|
|
1147
|
-
await this.
|
|
1408
|
+
await this.dispatchInput("Input.dispatchKeyEvent", down);
|
|
1409
|
+
await this.dispatchInput("Input.dispatchKeyEvent", { type: "keyUp", key: spec.key, code: spec.code, windowsVirtualKeyCode: spec.keyCode, nativeVirtualKeyCode: spec.keyCode, modifiers: modBits });
|
|
1148
1410
|
}
|
|
1149
1411
|
async setFiles(loc, files) {
|
|
1150
1412
|
const { objectId } = await resolveElementLocator(this.conn, loc);
|
|
@@ -1360,6 +1622,36 @@ class CdpPageDriver {
|
|
|
1360
1622
|
this.conn.close();
|
|
1361
1623
|
}
|
|
1362
1624
|
}
|
|
1625
|
+
var beaconSessions = new BeaconSessions((conn) => conn.close());
|
|
1626
|
+
async function targetWsUrl(targetId) {
|
|
1627
|
+
const hit = (await listTargets()).find((t) => t.id === targetId);
|
|
1628
|
+
return hit?.webSocketDebuggerUrl;
|
|
1629
|
+
}
|
|
1630
|
+
async function readBeaconOver(conn) {
|
|
1631
|
+
try {
|
|
1632
|
+
const res = await conn.send("Runtime.evaluate", {
|
|
1633
|
+
expression: BEACON_READ_EXPRESSION,
|
|
1634
|
+
returnByValue: true
|
|
1635
|
+
});
|
|
1636
|
+
const value = res.result?.value;
|
|
1637
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1638
|
+
} catch {
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
async function probeOver(conn, timeoutMs) {
|
|
1643
|
+
try {
|
|
1644
|
+
const res = await conn.send("Runtime.evaluate", { expression: RENDERER_PROBE_EXPRESSION, returnByValue: true }, { timeoutMs });
|
|
1645
|
+
const value = res.result?.value;
|
|
1646
|
+
if (Array.isArray(value) && value[0] === 1) {
|
|
1647
|
+
const ts = value[1];
|
|
1648
|
+
return { responsive: true, beaconTs: typeof ts === "number" && Number.isFinite(ts) ? ts : null };
|
|
1649
|
+
}
|
|
1650
|
+
return { responsive: false, beaconTs: null };
|
|
1651
|
+
} catch {
|
|
1652
|
+
return { responsive: false, beaconTs: null };
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1363
1655
|
async function withBrowserConn(fn) {
|
|
1364
1656
|
const conn = await openBrowser();
|
|
1365
1657
|
try {
|
|
@@ -1398,6 +1690,8 @@ class CdpBrowserDriver {
|
|
|
1398
1690
|
async closePage(id) {
|
|
1399
1691
|
return withBrowserConn(async (conn) => {
|
|
1400
1692
|
const res = await conn.send("Target.closeTarget", { targetId: id });
|
|
1693
|
+
if (res.success !== false)
|
|
1694
|
+
beaconSessions.drop(id);
|
|
1401
1695
|
return { success: res.success ?? true };
|
|
1402
1696
|
});
|
|
1403
1697
|
}
|
|
@@ -1410,7 +1704,83 @@ class CdpBrowserDriver {
|
|
|
1410
1704
|
const { conn, target } = await openPage(selector).catch(noSuchTarget);
|
|
1411
1705
|
return new CdpPageDriver(conn, target, this);
|
|
1412
1706
|
}
|
|
1413
|
-
async
|
|
1707
|
+
async installActivityBeacon(targetId) {
|
|
1708
|
+
const held = beaconSessions.get(targetId);
|
|
1709
|
+
if (held) {
|
|
1710
|
+
const rearmed = await held.send("Runtime.evaluate", { expression: BEACON_SOURCE, returnByValue: true }).then(() => true).catch(() => false);
|
|
1711
|
+
if (rearmed)
|
|
1712
|
+
return true;
|
|
1713
|
+
beaconSessions.drop(targetId);
|
|
1714
|
+
}
|
|
1715
|
+
const wsUrl = await targetWsUrl(targetId).catch(() => {
|
|
1716
|
+
return;
|
|
1717
|
+
});
|
|
1718
|
+
if (!wsUrl)
|
|
1719
|
+
return false;
|
|
1720
|
+
const conn = await new CdpConnection(wsUrl).connect().catch(() => {
|
|
1721
|
+
return;
|
|
1722
|
+
});
|
|
1723
|
+
if (!conn)
|
|
1724
|
+
return false;
|
|
1725
|
+
try {
|
|
1726
|
+
await conn.send("Page.enable");
|
|
1727
|
+
await conn.send("Page.addScriptToEvaluateOnNewDocument", { source: BEACON_SOURCE });
|
|
1728
|
+
await conn.send("Runtime.evaluate", { expression: BEACON_SOURCE, returnByValue: true });
|
|
1729
|
+
} catch {
|
|
1730
|
+
conn.close();
|
|
1731
|
+
return false;
|
|
1732
|
+
}
|
|
1733
|
+
beaconSessions.set(targetId, conn);
|
|
1734
|
+
return true;
|
|
1735
|
+
}
|
|
1736
|
+
async readActivityBeacon(targetId) {
|
|
1737
|
+
const held = beaconSessions.get(targetId);
|
|
1738
|
+
if (held) {
|
|
1739
|
+
const value = await readBeaconOver(held);
|
|
1740
|
+
if (value !== undefined)
|
|
1741
|
+
return value;
|
|
1742
|
+
beaconSessions.drop(targetId);
|
|
1743
|
+
}
|
|
1744
|
+
const wsUrl = await targetWsUrl(targetId).catch(() => {
|
|
1745
|
+
return;
|
|
1746
|
+
});
|
|
1747
|
+
if (!wsUrl)
|
|
1748
|
+
return null;
|
|
1749
|
+
const conn = await new CdpConnection(wsUrl).connect().catch(() => {
|
|
1750
|
+
return;
|
|
1751
|
+
});
|
|
1752
|
+
if (!conn)
|
|
1753
|
+
return null;
|
|
1754
|
+
try {
|
|
1755
|
+
return await readBeaconOver(conn) ?? null;
|
|
1756
|
+
} finally {
|
|
1757
|
+
conn.close();
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
async probeRenderer(targetId, timeoutMs = RENDERER_PROBE_TIMEOUT_MS) {
|
|
1761
|
+
const held = beaconSessions.get(targetId);
|
|
1762
|
+
if (held)
|
|
1763
|
+
return probeOver(held, timeoutMs);
|
|
1764
|
+
const wsUrl = await targetWsUrl(targetId).catch(() => {
|
|
1765
|
+
return;
|
|
1766
|
+
});
|
|
1767
|
+
if (!wsUrl)
|
|
1768
|
+
return { responsive: false, beaconTs: null };
|
|
1769
|
+
const conn = new CdpConnection(wsUrl, { timeoutMs });
|
|
1770
|
+
try {
|
|
1771
|
+
await conn.connect();
|
|
1772
|
+
} catch {
|
|
1773
|
+
return { responsive: false, beaconTs: null };
|
|
1774
|
+
}
|
|
1775
|
+
try {
|
|
1776
|
+
return await probeOver(conn, timeoutMs);
|
|
1777
|
+
} finally {
|
|
1778
|
+
conn.close();
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
async dispose() {
|
|
1782
|
+
beaconSessions.clear();
|
|
1783
|
+
}
|
|
1414
1784
|
}
|
|
1415
1785
|
function createCdpDriver() {
|
|
1416
1786
|
return new CdpBrowserDriver;
|
|
@@ -1532,6 +1902,8 @@ function staleAgentTabs(input) {
|
|
|
1532
1902
|
continue;
|
|
1533
1903
|
if (row.stale !== "dead-pid" && row.stale !== "expired")
|
|
1534
1904
|
continue;
|
|
1905
|
+
if (row.stale === "expired" && input.now - row.lastUsedAt <= row.ttlMs + input.reapGraceMs)
|
|
1906
|
+
continue;
|
|
1535
1907
|
if (!live.has(row.targetId))
|
|
1536
1908
|
continue;
|
|
1537
1909
|
const origin = input.origins.get(row.targetId);
|
|
@@ -1541,13 +1913,19 @@ function staleAgentTabs(input) {
|
|
|
1541
1913
|
}
|
|
1542
1914
|
return out;
|
|
1543
1915
|
}
|
|
1916
|
+
var DEFAULT_REAP_GRACE_MS = 2700000;
|
|
1917
|
+
function reapGraceMs() {
|
|
1918
|
+
const raw = Number(process.env.CDP_REAP_GRACE_MS);
|
|
1919
|
+
return Number.isFinite(raw) && raw >= 0 ? raw : DEFAULT_REAP_GRACE_MS;
|
|
1920
|
+
}
|
|
1544
1921
|
async function reapStaleAgentTabs(driver, backend) {
|
|
1922
|
+
const now = Date.now();
|
|
1545
1923
|
const livePageIds = (await driver.listPages()).map((p) => p.id);
|
|
1546
1924
|
const [origins, leases] = await Promise.all([
|
|
1547
1925
|
originIndex(backend, undefined),
|
|
1548
|
-
listLeases({ liveIds: livePageIds, liveBackend: backend }).catch(() => [])
|
|
1926
|
+
listLeases({ now, liveIds: livePageIds, liveBackend: backend }).catch(() => [])
|
|
1549
1927
|
]);
|
|
1550
|
-
const candidates = staleAgentTabs({ backend, livePageIds, origins, leases });
|
|
1928
|
+
const candidates = staleAgentTabs({ backend, livePageIds, origins, leases, now, reapGraceMs: reapGraceMs() });
|
|
1551
1929
|
const closed = [];
|
|
1552
1930
|
for (const c of candidates) {
|
|
1553
1931
|
const res = await driver.closePage(c.targetId).catch(() => ({ success: false }));
|
|
@@ -1639,18 +2017,29 @@ async function reapSet(driver, pages, all) {
|
|
|
1639
2017
|
}
|
|
1640
2018
|
}
|
|
1641
2019
|
async function listPages(driver, args = {}) {
|
|
1642
|
-
const
|
|
2020
|
+
const backend = backendOf(driver);
|
|
2021
|
+
const reaped = requireLease() ? await reapStaleAgentTabs(driver, backend) : [];
|
|
1643
2022
|
const reapedIds = new Set(reaped.map((r) => r.targetId));
|
|
1644
2023
|
const pages = (await driver.listPages({ all: args.all })).filter((p) => !reapedIds.has(p.id));
|
|
1645
|
-
const ledger = await originIndex(
|
|
1646
|
-
const
|
|
2024
|
+
const ledger = await originIndex(backend, await reapSet(driver, pages, args.all));
|
|
2025
|
+
const now = Date.now();
|
|
2026
|
+
const leaseByTarget = new Map((await listLeases({ now, liveIds: pages.map((p) => p.id), liveBackend: backend })).filter((l) => l.backend === backend && l.unreadable === undefined).map((l) => [l.targetId, l]));
|
|
2027
|
+
const doProbe = args.probe === true && rendererProbeSupported(driver);
|
|
2028
|
+
const annotated = await Promise.all(pages.map(async (p) => {
|
|
1647
2029
|
const rec = ledger.get(p.id);
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
if (
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
2030
|
+
const base = !rec ? { ...p, origin: "unknown" } : rec.unreadable !== undefined ? { ...p, origin: "unknown", originUnreadable: rec.unreadable } : { ...p, origin: "agent", label: rec.label, createdAt: rec.createdAt };
|
|
2031
|
+
const leaseRow = leaseByTarget.get(p.id);
|
|
2032
|
+
if (leaseRow) {
|
|
2033
|
+
base.lease = { label: leaseRow.label, pid: leaseRow.pid, idleMs: now - leaseRow.lastUsedAt, expiresAt: leaseRow.lastUsedAt + leaseRow.ttlMs, stale: leaseRow.stale };
|
|
2034
|
+
}
|
|
2035
|
+
if (doProbe && (p.type === undefined || p.type === "page")) {
|
|
2036
|
+
const probe = await probeRendererActivity(driver, backend, p.id, now);
|
|
2037
|
+
base.responsive = probe.responsive;
|
|
2038
|
+
if (probe.humanActiveMs !== undefined)
|
|
2039
|
+
base.humanActiveMs = probe.humanActiveMs;
|
|
2040
|
+
}
|
|
2041
|
+
return base;
|
|
2042
|
+
}));
|
|
1654
2043
|
return { pages: annotated, count: annotated.length, ...reaped.length ? { reaped } : {} };
|
|
1655
2044
|
}
|
|
1656
2045
|
async function newPage(driver, args = {}) {
|
|
@@ -1664,6 +2053,7 @@ async function newPage(driver, args = {}) {
|
|
|
1664
2053
|
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
|
|
1665
2054
|
auto: !explicit
|
|
1666
2055
|
});
|
|
2056
|
+
await installBeacon(driver, p.id);
|
|
1667
2057
|
return { targetId: p.id, url: p.url, lease: token, label: record.label, expiresAt: record.lastUsedAt + record.ttlMs };
|
|
1668
2058
|
}
|
|
1669
2059
|
async function closePage(driver, args) {
|
|
@@ -1695,17 +2085,36 @@ function toLegacyWaitedFor(w) {
|
|
|
1695
2085
|
return "navigate-only";
|
|
1696
2086
|
return w;
|
|
1697
2087
|
}
|
|
1698
|
-
|
|
2088
|
+
function resolveNavigateMode(args) {
|
|
1699
2089
|
const reload = args.reload === true;
|
|
1700
|
-
|
|
1701
|
-
|
|
2090
|
+
const hasUrl = typeof args.url === "string" && args.url.length > 0;
|
|
2091
|
+
const hasHistory = args.history !== undefined && args.history !== null;
|
|
2092
|
+
const given = [hasUrl ? "url" : "", reload ? "reload" : "", hasHistory ? "history" : ""].filter(Boolean);
|
|
2093
|
+
if (given.length > 1) {
|
|
2094
|
+
throw new SharedToolError(`navigate_page: 'url', 'reload' and 'history' are mutually exclusive (got ${given.join(" + ")})`);
|
|
2095
|
+
}
|
|
2096
|
+
if (hasHistory) {
|
|
2097
|
+
if (args.history !== "back" && args.history !== "forward") {
|
|
2098
|
+
throw new SharedToolError(`navigate_page: 'history' must be 'back' or 'forward' (got ${JSON.stringify(args.history)})`);
|
|
2099
|
+
}
|
|
2100
|
+
return { mode: "history", history: args.history };
|
|
2101
|
+
}
|
|
2102
|
+
if (reload)
|
|
2103
|
+
return { mode: "reload" };
|
|
2104
|
+
if (!hasUrl) {
|
|
2105
|
+
throw new SharedToolError("navigate_page: 'url' is required (or pass reload:true to reload the current page, or history:'back'|'forward' to go back/forward in this tab's session history)");
|
|
1702
2106
|
}
|
|
2107
|
+
return { mode: "url", url: args.url };
|
|
2108
|
+
}
|
|
2109
|
+
async function navigatePage(driver, args) {
|
|
2110
|
+
const mode = resolveNavigateMode(args);
|
|
1703
2111
|
return withPage2(driver, args.target, async (page) => {
|
|
1704
|
-
const r = await page.navigate(args);
|
|
2112
|
+
const r = await page.navigate(mode.mode === "history" ? { ...args, history: mode.history } : args);
|
|
1705
2113
|
return {
|
|
1706
2114
|
url: r.url,
|
|
1707
2115
|
frameId: r.contextId,
|
|
1708
2116
|
...r.reloaded ? { reloaded: r.reloaded } : {},
|
|
2117
|
+
...r.traversed ? { traversed: r.traversed } : {},
|
|
1709
2118
|
waitedFor: toLegacyWaitedFor(r.waitedFor)
|
|
1710
2119
|
};
|
|
1711
2120
|
});
|
|
@@ -1851,22 +2260,113 @@ async function takeSnapshot(driver, args = {}) {
|
|
|
1851
2260
|
}
|
|
1852
2261
|
async function click(driver, args) {
|
|
1853
2262
|
return withPage2(driver, args.target, async (page) => {
|
|
1854
|
-
const { x, y } = await page.click(locatorOf(args), { button: args.button ?? "left", clickCount: args.clickCount ?? 1 });
|
|
2263
|
+
const { x, y } = await page.click(locatorOf(args), { button: args.button ?? "left", clickCount: args.clickCount ?? 1, modifiers: args.modifiers });
|
|
1855
2264
|
return { clicked: true, x, y };
|
|
1856
2265
|
});
|
|
1857
2266
|
}
|
|
2267
|
+
function resolveScrollAnchor(args) {
|
|
2268
|
+
const hasUid = args.uid !== undefined && args.uid !== null && args.uid !== "";
|
|
2269
|
+
const hasSelector = typeof args.selector === "string" && args.selector.length > 0;
|
|
2270
|
+
const hasX = args.x !== undefined;
|
|
2271
|
+
const hasY = args.y !== undefined;
|
|
2272
|
+
if (hasX !== hasY)
|
|
2273
|
+
throw new SharedToolError("scroll: 'x' and 'y' must be provided together");
|
|
2274
|
+
const hasXY = hasX && hasY;
|
|
2275
|
+
const anchorCount = [hasUid, hasSelector, hasXY].filter(Boolean).length;
|
|
2276
|
+
if (anchorCount > 1)
|
|
2277
|
+
throw new SharedToolError("scroll: provide at most one of { uid }, { selector }, or { x, y } (omit all three to scroll at the viewport center)");
|
|
2278
|
+
if (hasUid || hasSelector)
|
|
2279
|
+
return locatorOf({ uid: args.uid, selector: args.selector });
|
|
2280
|
+
if (hasXY)
|
|
2281
|
+
return { x: args.x, y: args.y };
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
function resolveScrollDelta(args) {
|
|
2285
|
+
const deltaX = args.deltaX ?? 0;
|
|
2286
|
+
const deltaY = args.deltaY ?? 0;
|
|
2287
|
+
if (deltaX === 0 && deltaY === 0)
|
|
2288
|
+
throw new SharedToolError("scroll requires at least one of { deltaX } or { deltaY }");
|
|
2289
|
+
return { deltaX, deltaY };
|
|
2290
|
+
}
|
|
2291
|
+
async function scroll(driver, args) {
|
|
2292
|
+
const anchor = resolveScrollAnchor(args);
|
|
2293
|
+
const { deltaX, deltaY } = resolveScrollDelta(args);
|
|
2294
|
+
return withPage2(driver, args.target, async (page) => {
|
|
2295
|
+
const { x, y } = await page.scroll(anchor, { deltaX, deltaY });
|
|
2296
|
+
return { x, y, deltaX, deltaY, target: target3(page.info) };
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
1858
2299
|
async function hover(driver, args) {
|
|
1859
2300
|
return withPage2(driver, args.target, async (page) => {
|
|
1860
2301
|
const { x, y } = await page.hover(locatorOf(args));
|
|
1861
2302
|
return { hovered: true, x, y };
|
|
1862
2303
|
});
|
|
1863
2304
|
}
|
|
2305
|
+
function resolveDragTo(to) {
|
|
2306
|
+
const hasUid = to.uid !== undefined && to.uid !== null && to.uid !== "";
|
|
2307
|
+
const hasSelector = typeof to.selector === "string" && to.selector.length > 0;
|
|
2308
|
+
const hasX = to.x !== undefined;
|
|
2309
|
+
const hasY = to.y !== undefined;
|
|
2310
|
+
if (hasX !== hasY)
|
|
2311
|
+
throw new SharedToolError("drag: 'to.x' and 'to.y' must be provided together");
|
|
2312
|
+
const hasXY = hasX && hasY;
|
|
2313
|
+
const count = [hasUid, hasSelector, hasXY].filter(Boolean).length;
|
|
2314
|
+
if (count !== 1)
|
|
2315
|
+
throw new SharedToolError("drag: 'to' takes exactly one of { uid }, { selector }, or { x, y }");
|
|
2316
|
+
if (hasXY) {
|
|
2317
|
+
if (!Number.isFinite(to.x) || !Number.isFinite(to.y))
|
|
2318
|
+
throw new SharedToolError("drag: 'to.x' and 'to.y' must be finite numbers");
|
|
2319
|
+
return { x: to.x, y: to.y };
|
|
2320
|
+
}
|
|
2321
|
+
return locatorOf({ uid: to.uid, selector: to.selector });
|
|
2322
|
+
}
|
|
2323
|
+
function resolveDragDestination(args) {
|
|
2324
|
+
const hasTo = args.to !== undefined && args.to !== null;
|
|
2325
|
+
const hasBy = args.by !== undefined && args.by !== null;
|
|
2326
|
+
if (hasTo === hasBy)
|
|
2327
|
+
throw new SharedToolError("drag requires exactly one of { to } or { by }");
|
|
2328
|
+
if (hasBy) {
|
|
2329
|
+
const by = args.by;
|
|
2330
|
+
const hasDx = by.dx !== undefined;
|
|
2331
|
+
const hasDy = by.dy !== undefined;
|
|
2332
|
+
if (!hasDx && !hasDy)
|
|
2333
|
+
throw new SharedToolError("drag: 'by' requires at least one of { dx } or { dy }");
|
|
2334
|
+
const dx = by.dx ?? 0;
|
|
2335
|
+
const dy = by.dy ?? 0;
|
|
2336
|
+
if (!Number.isFinite(dx) || !Number.isFinite(dy))
|
|
2337
|
+
throw new SharedToolError("drag: 'by.dx' and 'by.dy' must be finite numbers");
|
|
2338
|
+
return { dx, dy };
|
|
2339
|
+
}
|
|
2340
|
+
return resolveDragTo(args.to);
|
|
2341
|
+
}
|
|
2342
|
+
var MAX_DRAG_STEPS = 500;
|
|
2343
|
+
function resolveDragSteps(steps) {
|
|
2344
|
+
if (steps === undefined)
|
|
2345
|
+
return 2;
|
|
2346
|
+
if (typeof steps !== "number" || !Number.isInteger(steps) || steps < 1 || steps > MAX_DRAG_STEPS) {
|
|
2347
|
+
throw new SharedToolError(`drag: 'steps' must be an integer between 1 and ${MAX_DRAG_STEPS} (default 2)`);
|
|
2348
|
+
}
|
|
2349
|
+
return steps;
|
|
2350
|
+
}
|
|
2351
|
+
function resolveDragMode(mode, capabilities) {
|
|
2352
|
+
if (mode === undefined || mode === "mouse")
|
|
2353
|
+
return "mouse";
|
|
2354
|
+
if (mode !== "html5")
|
|
2355
|
+
throw new SharedToolError(`drag: unknown mode '${mode}' (use 'mouse' or 'html5')`);
|
|
2356
|
+
if (!capabilities.has("input.html5Drag")) {
|
|
2357
|
+
throw new SharedToolError("drag: mode:'html5' is not supported by this backend (WebDriver BiDi has no drag-interception primitive). " + "Omit 'mode' for the synthetic-mouse drag, or use --browser chrome for real HTML5 drag events.");
|
|
2358
|
+
}
|
|
2359
|
+
return "html5";
|
|
2360
|
+
}
|
|
1864
2361
|
async function drag(driver, args) {
|
|
1865
|
-
if (!args.from
|
|
1866
|
-
throw new SharedToolError("drag requires { from }
|
|
2362
|
+
if (!args.from)
|
|
2363
|
+
throw new SharedToolError("drag requires { from }");
|
|
2364
|
+
const destination = resolveDragDestination(args);
|
|
2365
|
+
const steps = resolveDragSteps(args.steps);
|
|
2366
|
+
const mode = resolveDragMode(args.mode, driver.capabilities);
|
|
1867
2367
|
return withPage2(driver, args.target, async (page) => {
|
|
1868
|
-
const { from, to } = await page.drag(locatorOf(args.from),
|
|
1869
|
-
return { dragged: true, from, to };
|
|
2368
|
+
const { from, to } = await page.drag(locatorOf(args.from), destination, { mode, steps });
|
|
2369
|
+
return { dragged: true, mode, steps, from, to };
|
|
1870
2370
|
});
|
|
1871
2371
|
}
|
|
1872
2372
|
async function fill(driver, args) {
|
|
@@ -1987,6 +2487,7 @@ var SHARED_TOOLS = {
|
|
|
1987
2487
|
click,
|
|
1988
2488
|
hover,
|
|
1989
2489
|
drag,
|
|
2490
|
+
scroll,
|
|
1990
2491
|
fill,
|
|
1991
2492
|
fill_form: fillForm,
|
|
1992
2493
|
type_text: typeText,
|
|
@@ -2036,12 +2537,15 @@ async function claimPage(driver, args = {}) {
|
|
|
2036
2537
|
url = page.url;
|
|
2037
2538
|
opened = true;
|
|
2038
2539
|
}
|
|
2540
|
+
const humanMs = opened ? null : await readHumanActiveMs(driver, backend, targetId);
|
|
2039
2541
|
const liveIds = [...pages.map((p) => p.id), targetId];
|
|
2040
2542
|
const { record, token } = await claimLease(backend, targetId, {
|
|
2041
2543
|
label,
|
|
2042
2544
|
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
|
|
2043
2545
|
liveIds
|
|
2044
2546
|
});
|
|
2547
|
+
await installBeacon(driver, targetId);
|
|
2548
|
+
const contention = contentionWarning(humanMs, { takeover: !opened });
|
|
2045
2549
|
return {
|
|
2046
2550
|
lease: token,
|
|
2047
2551
|
targetId,
|
|
@@ -2049,7 +2553,9 @@ async function claimPage(driver, args = {}) {
|
|
|
2049
2553
|
label: record.label,
|
|
2050
2554
|
ttlMs: record.ttlMs,
|
|
2051
2555
|
expiresAt: record.lastUsedAt + record.ttlMs,
|
|
2052
|
-
opened
|
|
2556
|
+
opened,
|
|
2557
|
+
...beaconSupported(driver) ? { humanActiveMs: humanMs } : {},
|
|
2558
|
+
...contention !== undefined ? { contention } : {}
|
|
2053
2559
|
};
|
|
2054
2560
|
}
|
|
2055
2561
|
async function releasePage(driver, args = {}) {
|
|
@@ -2084,9 +2590,22 @@ async function listLeasesTool(driver, _args = {}) {
|
|
|
2084
2590
|
const backend = backendOf2(driver);
|
|
2085
2591
|
const reaped = requireLease() ? await reapStaleAgentTabs(driver, backend) : [];
|
|
2086
2592
|
const reapedIds = new Set(reaped.map((r) => r.targetId));
|
|
2087
|
-
const
|
|
2593
|
+
const livePages = await driver.listPages();
|
|
2594
|
+
const liveIds = livePages.map((p) => p.id);
|
|
2088
2595
|
const all = await listLeases({ liveIds, liveBackend: backend });
|
|
2089
|
-
const
|
|
2596
|
+
const kept = all.filter((l) => !(l.backend === backend && reapedIds.has(l.targetId)));
|
|
2597
|
+
const liveSet = new Set(liveIds);
|
|
2598
|
+
const now = Date.now();
|
|
2599
|
+
const leases = [];
|
|
2600
|
+
for (const row of kept) {
|
|
2601
|
+
const computed = row.unreadable !== undefined ? row : { ...row, idleMs: now - row.lastUsedAt, expiresAt: row.lastUsedAt + row.ttlMs };
|
|
2602
|
+
if (row.backend !== backend || row.unreadable !== undefined || !liveSet.has(row.targetId)) {
|
|
2603
|
+
leases.push(computed);
|
|
2604
|
+
continue;
|
|
2605
|
+
}
|
|
2606
|
+
const humanMs = await readHumanActiveMs(driver, backend, row.targetId, now).catch(() => null);
|
|
2607
|
+
leases.push(humanMs === null ? computed : { ...computed, humanActiveMs: humanMs });
|
|
2608
|
+
}
|
|
2090
2609
|
return { leases, count: leases.length, ...reaped.length ? { reaped } : {} };
|
|
2091
2610
|
}
|
|
2092
2611
|
var LEASE_TOOLS = {
|
|
@@ -3242,6 +3761,273 @@ function truncate(s, max = 600) {
|
|
|
3242
3761
|
return t.length > max ? `${t.slice(0, max)}…` : t;
|
|
3243
3762
|
}
|
|
3244
3763
|
|
|
3764
|
+
// src/tools/dispatch-mouse.ts
|
|
3765
|
+
var CLICK_MODIFIER_BITS2 = { Alt: 1, Control: 2, Meta: 4, Shift: 8 };
|
|
3766
|
+
var BUTTONS = new Set(["left", "right", "middle"]);
|
|
3767
|
+
var CDP_EVENT_TYPE = {
|
|
3768
|
+
move: "mouseMoved",
|
|
3769
|
+
down: "mousePressed",
|
|
3770
|
+
up: "mouseReleased"
|
|
3771
|
+
};
|
|
3772
|
+
function modifierBits(mods) {
|
|
3773
|
+
let bits = 0;
|
|
3774
|
+
for (const m of mods ?? []) {
|
|
3775
|
+
const bit = CLICK_MODIFIER_BITS2[m];
|
|
3776
|
+
if (bit === undefined)
|
|
3777
|
+
throw new Error(`dispatch_mouse: unknown modifier '${m}' (use Alt, Control, Meta, or Shift)`);
|
|
3778
|
+
bits |= bit;
|
|
3779
|
+
}
|
|
3780
|
+
return bits;
|
|
3781
|
+
}
|
|
3782
|
+
function validateDispatchMouseArgs(args) {
|
|
3783
|
+
if (args.action !== "move" && args.action !== "down" && args.action !== "up") {
|
|
3784
|
+
throw new Error("dispatch_mouse requires { action } of 'move', 'down', or 'up'");
|
|
3785
|
+
}
|
|
3786
|
+
if (typeof args.x !== "number" || typeof args.y !== "number" || !Number.isFinite(args.x) || !Number.isFinite(args.y)) {
|
|
3787
|
+
throw new Error("dispatch_mouse requires numeric { x } and { y } on every call");
|
|
3788
|
+
}
|
|
3789
|
+
const button = args.button ?? "left";
|
|
3790
|
+
if (!BUTTONS.has(button))
|
|
3791
|
+
throw new Error(`dispatch_mouse: unknown button '${button}' (use left, right, or middle)`);
|
|
3792
|
+
}
|
|
3793
|
+
async function dispatchMouse(args) {
|
|
3794
|
+
validateDispatchMouseArgs(args);
|
|
3795
|
+
const button = args.button ?? "left";
|
|
3796
|
+
const modifiers = modifierBits(args.modifiers);
|
|
3797
|
+
return withPage(args.target, async (conn, target) => {
|
|
3798
|
+
const params = { type: CDP_EVENT_TYPE[args.action], x: args.x, y: args.y, button, modifiers };
|
|
3799
|
+
if (args.action !== "move")
|
|
3800
|
+
params.clickCount = args.clickCount ?? 1;
|
|
3801
|
+
await sendInput(conn, "chrome", target.id, "Input.dispatchMouseEvent", params);
|
|
3802
|
+
return { dispatched: args.action, x: args.x, y: args.y };
|
|
3803
|
+
});
|
|
3804
|
+
}
|
|
3805
|
+
|
|
3806
|
+
// src/tools/downloads.ts
|
|
3807
|
+
import { readdir as readdir3, rename, stat as stat4 } from "node:fs/promises";
|
|
3808
|
+
import { basename, extname, join as join8 } from "node:path";
|
|
3809
|
+
|
|
3810
|
+
// src/tools/browser-session.ts
|
|
3811
|
+
import { mkdir as mkdir9 } from "node:fs/promises";
|
|
3812
|
+
import { join as join7 } from "node:path";
|
|
3813
|
+
var ARTIFACT_DIR7 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
3814
|
+
var DOWNLOADS_DIR = join7(ARTIFACT_DIR7, "downloads");
|
|
3815
|
+
var DOWNLOAD_BUFFER_LIMIT = 50;
|
|
3816
|
+
var session;
|
|
3817
|
+
function isDisconnect(e) {
|
|
3818
|
+
const m = e instanceof Error ? e.message : String(e);
|
|
3819
|
+
return /connection (not open|closed)|closed by caller|websocket|ECONNREFUSED|socket/i.test(m);
|
|
3820
|
+
}
|
|
3821
|
+
function record(s, guid) {
|
|
3822
|
+
let r = s.downloads.find((d) => d.guid === guid);
|
|
3823
|
+
if (!r) {
|
|
3824
|
+
r = { guid, state: "inProgress", consumed: false };
|
|
3825
|
+
s.downloads.push(r);
|
|
3826
|
+
while (s.downloads.length > DOWNLOAD_BUFFER_LIMIT) {
|
|
3827
|
+
const i = s.downloads.findIndex((d) => d.consumed);
|
|
3828
|
+
s.downloads.splice(i >= 0 ? i : 0, 1);
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
return r;
|
|
3832
|
+
}
|
|
3833
|
+
function subscribe(s) {
|
|
3834
|
+
s.conn.on("Browser.downloadWillBegin", (p) => {
|
|
3835
|
+
const r = record(s, String(p.guid));
|
|
3836
|
+
if (typeof p.url === "string")
|
|
3837
|
+
r.url = p.url;
|
|
3838
|
+
if (typeof p.suggestedFilename === "string")
|
|
3839
|
+
r.suggestedFilename = p.suggestedFilename;
|
|
3840
|
+
});
|
|
3841
|
+
s.conn.on("Browser.downloadProgress", (p) => {
|
|
3842
|
+
const r = record(s, String(p.guid));
|
|
3843
|
+
if (typeof p.receivedBytes === "number")
|
|
3844
|
+
r.bytes = p.receivedBytes;
|
|
3845
|
+
if (typeof p.filePath === "string")
|
|
3846
|
+
r.filePath = p.filePath;
|
|
3847
|
+
const state = String(p.state);
|
|
3848
|
+
if (state === "completed" || state === "canceled") {
|
|
3849
|
+
r.state = state;
|
|
3850
|
+
if (state === "completed") {
|
|
3851
|
+
const waiter = s.waiters.shift();
|
|
3852
|
+
if (waiter) {
|
|
3853
|
+
r.consumed = true;
|
|
3854
|
+
waiter(r);
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
});
|
|
3859
|
+
}
|
|
3860
|
+
async function getBrowserSession() {
|
|
3861
|
+
if (session && !session.dead)
|
|
3862
|
+
return session;
|
|
3863
|
+
const previousPath = session?.armedPath;
|
|
3864
|
+
const conn = await openBrowser();
|
|
3865
|
+
const s = { conn, downloads: [], waiters: [], dead: false };
|
|
3866
|
+
subscribe(s);
|
|
3867
|
+
session = s;
|
|
3868
|
+
if (previousPath)
|
|
3869
|
+
await armDownloadsAt(previousPath);
|
|
3870
|
+
return s;
|
|
3871
|
+
}
|
|
3872
|
+
async function browserSend(method, params = {}) {
|
|
3873
|
+
const s = await getBrowserSession();
|
|
3874
|
+
try {
|
|
3875
|
+
return await s.conn.send(method, params);
|
|
3876
|
+
} catch (e) {
|
|
3877
|
+
if (isDisconnect(e)) {
|
|
3878
|
+
s.dead = true;
|
|
3879
|
+
const fresh = await getBrowserSession();
|
|
3880
|
+
return await fresh.conn.send(method, params);
|
|
3881
|
+
}
|
|
3882
|
+
throw e;
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
async function armDownloadsAt(path) {
|
|
3886
|
+
await mkdir9(path, { recursive: true });
|
|
3887
|
+
const s = await getBrowserSession();
|
|
3888
|
+
await s.conn.send("Browser.setDownloadBehavior", { behavior: "allowAndName", downloadPath: path, eventsEnabled: true });
|
|
3889
|
+
s.armedPath = path;
|
|
3890
|
+
}
|
|
3891
|
+
async function armDownloads() {
|
|
3892
|
+
const s = await getBrowserSession();
|
|
3893
|
+
if (s.armedPath !== DOWNLOADS_DIR)
|
|
3894
|
+
await armDownloadsAt(DOWNLOADS_DIR);
|
|
3895
|
+
return { downloadPath: DOWNLOADS_DIR };
|
|
3896
|
+
}
|
|
3897
|
+
function pendingDownloads() {
|
|
3898
|
+
return (session?.downloads ?? []).filter((d) => d.state === "completed" && !d.consumed);
|
|
3899
|
+
}
|
|
3900
|
+
async function nextCompletedDownload(timeoutMs) {
|
|
3901
|
+
const s = await getBrowserSession();
|
|
3902
|
+
const buffered = s.downloads.find((d) => d.state === "completed" && !d.consumed);
|
|
3903
|
+
if (buffered) {
|
|
3904
|
+
buffered.consumed = true;
|
|
3905
|
+
return buffered;
|
|
3906
|
+
}
|
|
3907
|
+
return new Promise((resolve, reject) => {
|
|
3908
|
+
const waiter = (r) => {
|
|
3909
|
+
clearTimeout(timer);
|
|
3910
|
+
resolve(r);
|
|
3911
|
+
};
|
|
3912
|
+
const timer = setTimeout(() => {
|
|
3913
|
+
const i = s.waiters.indexOf(waiter);
|
|
3914
|
+
if (i >= 0)
|
|
3915
|
+
s.waiters.splice(i, 1);
|
|
3916
|
+
reject(new Error(`wait_for_download: no download completed within ${timeoutMs}ms. Downloads are only captured while this server holds the arming connection, so the triggering click must come AFTER a wait_for_download call (use arm:true to arm without waiting).`));
|
|
3917
|
+
}, timeoutMs);
|
|
3918
|
+
s.waiters.push(waiter);
|
|
3919
|
+
});
|
|
3920
|
+
}
|
|
3921
|
+
async function disposeBrowserSession() {
|
|
3922
|
+
const s = session;
|
|
3923
|
+
session = undefined;
|
|
3924
|
+
if (!s)
|
|
3925
|
+
return;
|
|
3926
|
+
s.dead = true;
|
|
3927
|
+
try {
|
|
3928
|
+
s.conn.close();
|
|
3929
|
+
} catch {}
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3932
|
+
// src/tools/downloads.ts
|
|
3933
|
+
function safeFilename(suggested) {
|
|
3934
|
+
const raw = (suggested ?? "").replace(/[\u0000-\u001f]/g, "");
|
|
3935
|
+
const name = basename(raw.split("\\").pop() ?? "").trim();
|
|
3936
|
+
if (!name || name === "." || name === "..")
|
|
3937
|
+
return "download";
|
|
3938
|
+
return name;
|
|
3939
|
+
}
|
|
3940
|
+
function nextFreeName(name, taken) {
|
|
3941
|
+
if (!taken.has(name))
|
|
3942
|
+
return name;
|
|
3943
|
+
const ext = extname(name);
|
|
3944
|
+
const stem = ext ? name.slice(0, -ext.length) : name;
|
|
3945
|
+
for (let i = 1;; i++) {
|
|
3946
|
+
const candidate = `${stem}-${i}${ext}`;
|
|
3947
|
+
if (!taken.has(candidate))
|
|
3948
|
+
return candidate;
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
function validateWaitForDownloadArgs(args) {
|
|
3952
|
+
if (args.timeoutMs !== undefined) {
|
|
3953
|
+
if (typeof args.timeoutMs !== "number" || !Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0) {
|
|
3954
|
+
throw new Error(`wait_for_download: 'timeoutMs' must be a positive number (got ${JSON.stringify(args.timeoutMs)})`);
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
if (args.arm !== undefined && typeof args.arm !== "boolean") {
|
|
3958
|
+
throw new Error(`wait_for_download: 'arm' must be a boolean (got ${JSON.stringify(args.arm)})`);
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
async function waitForDownload(args = {}) {
|
|
3962
|
+
validateWaitForDownloadArgs(args);
|
|
3963
|
+
const t = await resolveTarget(args.target);
|
|
3964
|
+
const target = { id: t.id, url: t.url, title: t.title };
|
|
3965
|
+
const { downloadPath } = await armDownloads();
|
|
3966
|
+
if (args.arm === true)
|
|
3967
|
+
return { armed: true, downloadPath, pending: pendingDownloads().length, target };
|
|
3968
|
+
const rec = await nextCompletedDownload(args.timeoutMs ?? 30000);
|
|
3969
|
+
if (!rec.filePath) {
|
|
3970
|
+
throw new Error(`wait_for_download: download ${rec.guid} completed without a file path (nothing to return)`);
|
|
3971
|
+
}
|
|
3972
|
+
const suggestedFilename = safeFilename(rec.suggestedFilename);
|
|
3973
|
+
const taken = new Set(await readdir3(DOWNLOADS_DIR).catch(() => []));
|
|
3974
|
+
taken.delete(basename(rec.filePath));
|
|
3975
|
+
const finalName = nextFreeName(suggestedFilename, taken);
|
|
3976
|
+
const path = join8(DOWNLOADS_DIR, finalName);
|
|
3977
|
+
await rename(rec.filePath, path);
|
|
3978
|
+
const bytes = (await stat4(path)).size;
|
|
3979
|
+
return { path, suggestedFilename, bytes, ...rec.url ? { url: rec.url } : {}, target };
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3982
|
+
// src/tools/permissions.ts
|
|
3983
|
+
function originOf(url) {
|
|
3984
|
+
try {
|
|
3985
|
+
const o = new URL(url).origin;
|
|
3986
|
+
return o && o !== "null" ? o : undefined;
|
|
3987
|
+
} catch {
|
|
3988
|
+
return;
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
function validateGrantPermissionsArgs(args) {
|
|
3992
|
+
const reset = args.reset === true;
|
|
3993
|
+
if (args.reset !== undefined && typeof args.reset !== "boolean") {
|
|
3994
|
+
throw new Error(`grant_permissions: 'reset' must be a boolean (got ${JSON.stringify(args.reset)})`);
|
|
3995
|
+
}
|
|
3996
|
+
if (args.permissions === undefined) {
|
|
3997
|
+
if (!reset)
|
|
3998
|
+
throw new Error("grant_permissions: 'permissions' is required (or pass reset:true to clear previous grants)");
|
|
3999
|
+
return;
|
|
4000
|
+
}
|
|
4001
|
+
if (!Array.isArray(args.permissions)) {
|
|
4002
|
+
throw new Error(`grant_permissions: 'permissions' must be an array of CDP PermissionType strings (got ${JSON.stringify(args.permissions)})`);
|
|
4003
|
+
}
|
|
4004
|
+
if (args.permissions.length === 0 && !reset) {
|
|
4005
|
+
throw new Error("grant_permissions: 'permissions' must not be empty (pass reset:true if the intent is to clear grants)");
|
|
4006
|
+
}
|
|
4007
|
+
for (const p of args.permissions) {
|
|
4008
|
+
if (typeof p !== "string" || p.length === 0) {
|
|
4009
|
+
throw new Error(`grant_permissions: every entry of 'permissions' must be a non-empty string (got ${JSON.stringify(p)})`);
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
async function grantPermissions(args = {}) {
|
|
4014
|
+
validateGrantPermissionsArgs(args);
|
|
4015
|
+
const t = await resolveTarget(args.target);
|
|
4016
|
+
const target = { id: t.id, url: t.url, title: t.title };
|
|
4017
|
+
const reset = args.reset === true;
|
|
4018
|
+
const permissions = args.permissions ?? [];
|
|
4019
|
+
if (reset)
|
|
4020
|
+
await browserSend("Browser.resetPermissions", {});
|
|
4021
|
+
if (permissions.length === 0)
|
|
4022
|
+
return { reset: true, target };
|
|
4023
|
+
const origin = args.origin ?? originOf(t.url);
|
|
4024
|
+
if (!origin) {
|
|
4025
|
+
throw new Error(`grant_permissions: cannot derive an origin from the target tab's url (${JSON.stringify(t.url)}); pass 'origin' explicitly, e.g. origin:"https://example.com"`);
|
|
4026
|
+
}
|
|
4027
|
+
await browserSend("Browser.grantPermissions", { origin, permissions });
|
|
4028
|
+
return { granted: [...permissions], origin, ...reset ? { reset: true } : {}, target };
|
|
4029
|
+
}
|
|
4030
|
+
|
|
3245
4031
|
// src/index.ts
|
|
3246
4032
|
var cdpDriver = createCdpDriver();
|
|
3247
4033
|
function onCdp(fn) {
|
|
@@ -3262,6 +4048,7 @@ var TOOLS = {
|
|
|
3262
4048
|
click: onCdp(SHARED_TOOLS.click),
|
|
3263
4049
|
hover: onCdp(SHARED_TOOLS.hover),
|
|
3264
4050
|
drag: onCdp(SHARED_TOOLS.drag),
|
|
4051
|
+
scroll: onCdp(SHARED_TOOLS.scroll),
|
|
3265
4052
|
fill: onCdp(SHARED_TOOLS.fill),
|
|
3266
4053
|
fill_form: onCdp(SHARED_TOOLS.fill_form),
|
|
3267
4054
|
type_text: onCdp(SHARED_TOOLS.type_text),
|
|
@@ -3286,6 +4073,9 @@ var TOOLS = {
|
|
|
3286
4073
|
mock_request: mockRequest,
|
|
3287
4074
|
list_mocks: listMocks,
|
|
3288
4075
|
clear_mocks: clearMocks,
|
|
4076
|
+
dispatch_mouse: dispatchMouse,
|
|
4077
|
+
wait_for_download: waitForDownload,
|
|
4078
|
+
grant_permissions: grantPermissions,
|
|
3289
4079
|
claim_page: onCdp(LEASE_TOOLS.claim_page),
|
|
3290
4080
|
release_page: onCdp(LEASE_TOOLS.release_page),
|
|
3291
4081
|
list_leases: onCdp(LEASE_TOOLS.list_leases)
|