cdp-toolkit 1.0.0 → 1.2.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 +76 -9
- package/CONTRACT.md +7 -5
- package/README.md +99 -28
- package/dist/cli.js +3248 -1033
- package/dist/index.js +1418 -898
- package/dist/mcp.js +3447 -1064
- package/package.json +12 -6
- package/src/backend.ts +86 -0
- package/src/bidi/client.ts +319 -0
- package/src/bidi/driver.ts +851 -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 +97 -14
- package/src/client.ts +25 -7
- package/src/driver.ts +424 -0
- package/src/firefox-tools.ts +17 -0
- package/src/index.ts +55 -42
- package/src/leases-tools.ts +101 -0
- package/src/leases.ts +533 -0
- package/src/manifest.ts +211 -13
- package/src/mcp.ts +73 -15
- package/src/shared-tools.ts +466 -0
- package/src/tools/console.ts +3 -3
- package/src/tools/evaluate.ts +2 -2
- package/src/tools/heap.ts +3 -3
- package/src/tools/lighthouse.ts +4 -4
- package/src/tools/navigation.ts +1 -1
- package/src/tools/network.ts +7 -7
- package/src/tools/network_mock.ts +8 -8
- package/src/tools/performance.ts +11 -11
- package/src/tools/recorder.ts +5 -5
- package/src/tools/snapshot.ts +2 -2
- 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,6 +1,254 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
// src/leases.ts
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
var DEFAULT_LEASE_TTL_MS = 900000;
|
|
7
|
+
function leaseDir() {
|
|
8
|
+
return process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
9
|
+
}
|
|
10
|
+
function leaseTtlMs() {
|
|
11
|
+
const raw = Number(process.env.CDP_LEASE_TTL_MS);
|
|
12
|
+
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_LEASE_TTL_MS;
|
|
13
|
+
}
|
|
14
|
+
function safeId(id) {
|
|
15
|
+
return id.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
16
|
+
}
|
|
17
|
+
function leaseFile(backend, targetId) {
|
|
18
|
+
return join(leaseDir(), `lease-${backend}-${safeId(targetId)}.json`);
|
|
19
|
+
}
|
|
20
|
+
function mintToken(backend, targetId, nonce) {
|
|
21
|
+
return `${backend}:${targetId}:${nonce}`;
|
|
22
|
+
}
|
|
23
|
+
function tokenParts(token) {
|
|
24
|
+
if (typeof token !== "string")
|
|
25
|
+
return;
|
|
26
|
+
const first = token.indexOf(":");
|
|
27
|
+
const last = token.lastIndexOf(":");
|
|
28
|
+
if (first <= 0 || last <= first)
|
|
29
|
+
return;
|
|
30
|
+
const backend = token.slice(0, first);
|
|
31
|
+
if (backend !== "chrome" && backend !== "firefox")
|
|
32
|
+
return;
|
|
33
|
+
const targetId = token.slice(first + 1, last);
|
|
34
|
+
const nonce = token.slice(last + 1);
|
|
35
|
+
if (!targetId.length || !/^[0-9a-f]{24}$/.test(nonce))
|
|
36
|
+
return;
|
|
37
|
+
return { backend, targetId, nonce };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class LeaseConflictError extends Error {
|
|
41
|
+
targetId;
|
|
42
|
+
holder;
|
|
43
|
+
constructor(message, targetId, holder) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.targetId = targetId;
|
|
46
|
+
this.holder = holder;
|
|
47
|
+
this.name = "LeaseConflictError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function readLease(backend, targetId) {
|
|
51
|
+
let raw;
|
|
52
|
+
try {
|
|
53
|
+
raw = await readFile(leaseFile(backend, targetId), "utf8");
|
|
54
|
+
} catch (err) {
|
|
55
|
+
if (err.code === "ENOENT")
|
|
56
|
+
return;
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const rec = JSON.parse(raw);
|
|
61
|
+
if (typeof rec?.nonce === "string" && typeof rec?.pid === "number")
|
|
62
|
+
return rec;
|
|
63
|
+
} catch {}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
function isPidAlive(pid) {
|
|
67
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
68
|
+
return false;
|
|
69
|
+
try {
|
|
70
|
+
process.kill(pid, 0);
|
|
71
|
+
return true;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
return err.code === "EPERM";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function staleReason(rec, opts) {
|
|
77
|
+
if (!isPidAlive(rec.pid))
|
|
78
|
+
return "dead-pid";
|
|
79
|
+
if (opts.now - rec.lastUsedAt > rec.ttlMs)
|
|
80
|
+
return "expired";
|
|
81
|
+
if (opts.liveIds && !opts.liveIds.includes(rec.targetId))
|
|
82
|
+
return "target-gone";
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
async function claimLease(backend, targetId, opts) {
|
|
86
|
+
const now = opts.now ?? Date.now();
|
|
87
|
+
await mkdir(leaseDir(), { recursive: true });
|
|
88
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
89
|
+
const record = {
|
|
90
|
+
backend,
|
|
91
|
+
targetId,
|
|
92
|
+
nonce: randomBytes(12).toString("hex"),
|
|
93
|
+
pid: process.pid,
|
|
94
|
+
label: opts.label,
|
|
95
|
+
createdAt: now,
|
|
96
|
+
lastUsedAt: now,
|
|
97
|
+
ttlMs: opts.ttlMs ?? leaseTtlMs()
|
|
98
|
+
};
|
|
99
|
+
try {
|
|
100
|
+
await writeFile(leaseFile(backend, targetId), JSON.stringify(record), { flag: "wx" });
|
|
101
|
+
return { token: mintToken(backend, targetId, record.nonce), record };
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if (err.code !== "EEXIST")
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
const existing = await readLease(backend, targetId);
|
|
107
|
+
if (!existing)
|
|
108
|
+
continue;
|
|
109
|
+
const stale = staleReason(existing, { now, liveIds: opts.liveIds });
|
|
110
|
+
if (!stale) {
|
|
111
|
+
throw new LeaseConflictError(`target '${targetId}' is already leased by '${existing.label}' (pid ${existing.pid}); call release_page with that lease's token, or list_leases to inspect it`, targetId, existing.label);
|
|
112
|
+
}
|
|
113
|
+
await unlink(leaseFile(backend, targetId)).catch(() => {
|
|
114
|
+
return;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
throw new LeaseConflictError(`target '${targetId}' could not be claimed after 3 attempts: another process is claiming and releasing it concurrently`, targetId);
|
|
118
|
+
}
|
|
119
|
+
async function releaseLease(token) {
|
|
120
|
+
const parts = tokenParts(token);
|
|
121
|
+
if (!parts)
|
|
122
|
+
return { released: false };
|
|
123
|
+
const rec = await readLease(parts.backend, parts.targetId);
|
|
124
|
+
if (!rec || rec.nonce !== parts.nonce)
|
|
125
|
+
return { released: false };
|
|
126
|
+
await unlink(leaseFile(parts.backend, parts.targetId)).catch(() => {
|
|
127
|
+
return;
|
|
128
|
+
});
|
|
129
|
+
return { released: true };
|
|
130
|
+
}
|
|
131
|
+
async function releaseLeaseFor(backend, targetId) {
|
|
132
|
+
if (!await readLease(backend, targetId))
|
|
133
|
+
return { released: false };
|
|
134
|
+
await unlink(leaseFile(backend, targetId)).catch(() => {
|
|
135
|
+
return;
|
|
136
|
+
});
|
|
137
|
+
return { released: true };
|
|
138
|
+
}
|
|
139
|
+
var leaseScope = new AsyncLocalStorage;
|
|
140
|
+
function withLeaseScope(lease, fn) {
|
|
141
|
+
return leaseScope.run(lease, fn);
|
|
142
|
+
}
|
|
143
|
+
function currentLease() {
|
|
144
|
+
return leaseScope.getStore();
|
|
145
|
+
}
|
|
146
|
+
function leaseFromArgs(args) {
|
|
147
|
+
if (!args || typeof args !== "object")
|
|
148
|
+
return;
|
|
149
|
+
const lease = args.lease;
|
|
150
|
+
return typeof lease === "string" && lease.length > 0 ? lease : undefined;
|
|
151
|
+
}
|
|
152
|
+
async function touchLease(rec, now = Date.now()) {
|
|
153
|
+
const current = await readLease(rec.backend, rec.targetId).catch(() => {
|
|
154
|
+
return;
|
|
155
|
+
});
|
|
156
|
+
if (!current || current.nonce !== rec.nonce)
|
|
157
|
+
return;
|
|
158
|
+
await writeFile(leaseFile(current.backend, current.targetId), JSON.stringify({ ...current, lastUsedAt: now }), "utf8").catch(() => {
|
|
159
|
+
return;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function describeTarget(targetId, ctx) {
|
|
163
|
+
const url = ctx.url ? ` (url ${ctx.url})` : ctx.title ? ` (title ${JSON.stringify(ctx.title)})` : "";
|
|
164
|
+
return `target '${targetId}'${url}`;
|
|
165
|
+
}
|
|
166
|
+
async function assertLeaseOk(backend, targetId, ctx = {}) {
|
|
167
|
+
const token = ctx.lease ?? currentLease();
|
|
168
|
+
const now = ctx.now ?? Date.now();
|
|
169
|
+
const rec = await readLease(backend, targetId);
|
|
170
|
+
const held = rec && !staleReason(rec, { now, liveIds: ctx.liveIds }) ? rec : undefined;
|
|
171
|
+
const where = describeTarget(targetId, ctx);
|
|
172
|
+
if (token === undefined) {
|
|
173
|
+
if (!held)
|
|
174
|
+
return;
|
|
175
|
+
throw new LeaseConflictError(`${where} is leased by '${held.label}' (pid ${held.pid}). Pass that lease's token as the 'lease' argument, or call release_page to free it. list_leases shows every active lease.`, targetId, held.label);
|
|
176
|
+
}
|
|
177
|
+
const parts = tokenParts(token);
|
|
178
|
+
if (!parts) {
|
|
179
|
+
throw new LeaseConflictError(`malformed lease token for ${where}. Pass back the token claim_page returned, never a constructed one.`, targetId);
|
|
180
|
+
}
|
|
181
|
+
if (parts.backend !== backend || parts.targetId !== targetId) {
|
|
182
|
+
if (!held)
|
|
183
|
+
return;
|
|
184
|
+
throw new LeaseConflictError(`the lease token you passed is for ${parts.backend} target '${parts.targetId}', not ${where}, which is leased by '${held.label}'. Resolve the tab you actually hold, or claim this one.`, targetId, held.label);
|
|
185
|
+
}
|
|
186
|
+
if (!held) {
|
|
187
|
+
throw new LeaseConflictError(`${where} is not leased any more: the token you passed belongs to a lease that was released or reclaimed. Call claim_page again to take the tab.`, targetId);
|
|
188
|
+
}
|
|
189
|
+
if (parts.nonce !== held.nonce) {
|
|
190
|
+
throw new LeaseConflictError(`${where} was reclaimed and is now leased by '${held.label}' (pid ${held.pid}). Your token was invalidated by that reclamation. Call claim_page again if you still need this tab.`, targetId, held.label);
|
|
191
|
+
}
|
|
192
|
+
await touchLease(held, now);
|
|
193
|
+
}
|
|
194
|
+
var LEASE_FILE_RE = /^lease-(chrome|firefox)-(.+)\.json$/;
|
|
195
|
+
async function listLeases(opts = {}) {
|
|
196
|
+
const now = opts.now ?? Date.now();
|
|
197
|
+
let names = [];
|
|
198
|
+
try {
|
|
199
|
+
names = await readdir(leaseDir());
|
|
200
|
+
} catch (err) {
|
|
201
|
+
if (err.code === "ENOENT")
|
|
202
|
+
return [];
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
205
|
+
const out = [];
|
|
206
|
+
for (const name of names) {
|
|
207
|
+
const parts = LEASE_FILE_RE.exec(name);
|
|
208
|
+
if (!parts)
|
|
209
|
+
continue;
|
|
210
|
+
const fromName = {
|
|
211
|
+
backend: parts[1],
|
|
212
|
+
targetId: parts[2],
|
|
213
|
+
label: "",
|
|
214
|
+
pid: 0,
|
|
215
|
+
createdAt: 0,
|
|
216
|
+
lastUsedAt: 0,
|
|
217
|
+
ttlMs: 0,
|
|
218
|
+
pidAlive: false,
|
|
219
|
+
stale: false
|
|
220
|
+
};
|
|
221
|
+
let rec;
|
|
222
|
+
try {
|
|
223
|
+
rec = JSON.parse(await readFile(join(leaseDir(), name), "utf8"));
|
|
224
|
+
} catch (err) {
|
|
225
|
+
const code = err.code;
|
|
226
|
+
if (code === "ENOENT")
|
|
227
|
+
continue;
|
|
228
|
+
out.push({ ...fromName, unreadable: code ?? "unparseable" });
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (typeof rec?.nonce !== "string" || typeof rec?.pid !== "number") {
|
|
232
|
+
out.push({ ...fromName, unreadable: "unparseable" });
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
out.push({
|
|
236
|
+
backend: rec.backend,
|
|
237
|
+
targetId: rec.targetId,
|
|
238
|
+
label: rec.label,
|
|
239
|
+
pid: rec.pid,
|
|
240
|
+
createdAt: rec.createdAt,
|
|
241
|
+
lastUsedAt: rec.lastUsedAt,
|
|
242
|
+
ttlMs: rec.ttlMs,
|
|
243
|
+
pidAlive: isPidAlive(rec.pid),
|
|
244
|
+
stale: staleReason(rec, {
|
|
245
|
+
now,
|
|
246
|
+
liveIds: opts.liveBackend === undefined || opts.liveBackend === rec.backend ? opts.liveIds : undefined
|
|
247
|
+
})
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
4
252
|
|
|
5
253
|
// src/client.ts
|
|
6
254
|
var BASE = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
@@ -155,8 +403,13 @@ async function browserWsUrl() {
|
|
|
155
403
|
const v = await httpJson("/json/version");
|
|
156
404
|
return v.webSocketDebuggerUrl;
|
|
157
405
|
}
|
|
158
|
-
async function resolveTarget(selector) {
|
|
406
|
+
async function resolveTarget(selector, opts = {}) {
|
|
159
407
|
const targets = await listTargets();
|
|
408
|
+
const hit = pickTarget(targets, selector);
|
|
409
|
+
await assertLeaseOk("chrome", hit.id, { lease: opts.lease, url: hit.url, title: hit.title });
|
|
410
|
+
return hit;
|
|
411
|
+
}
|
|
412
|
+
function pickTarget(targets, selector) {
|
|
160
413
|
const pages = targets.filter((t) => t.type === "page");
|
|
161
414
|
if (!selector || selector === "active") {
|
|
162
415
|
if (!pages.length)
|
|
@@ -192,7 +445,7 @@ async function openBrowser(opts = {}) {
|
|
|
192
445
|
return new CdpConnection(await browserWsUrl(), opts).connect();
|
|
193
446
|
}
|
|
194
447
|
async function openPage(selector, opts = {}) {
|
|
195
|
-
const target = await resolveTarget(selector);
|
|
448
|
+
const target = await resolveTarget(selector, { lease: opts.lease });
|
|
196
449
|
const conn = await new CdpConnection(target.webSocketDebuggerUrl, opts).connect();
|
|
197
450
|
return { conn, target };
|
|
198
451
|
}
|
|
@@ -205,191 +458,6 @@ async function withPage(selector, fn, opts = {}) {
|
|
|
205
458
|
}
|
|
206
459
|
}
|
|
207
460
|
|
|
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
461
|
// src/tools/snapshot.ts
|
|
394
462
|
var INTERACTIVE_ROLES = new Set([
|
|
395
463
|
"button",
|
|
@@ -412,92 +480,6 @@ var INTERACTIVE_ROLES = new Set([
|
|
|
412
480
|
"MenuItem",
|
|
413
481
|
"Button"
|
|
414
482
|
]);
|
|
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
483
|
async function resolveUid(conn, uid) {
|
|
502
484
|
const { object } = await conn.send("DOM.resolveNode", {
|
|
503
485
|
backendNodeId: uid
|
|
@@ -508,149 +490,236 @@ async function resolveUid(conn, uid) {
|
|
|
508
490
|
return { objectId: object.objectId };
|
|
509
491
|
}
|
|
510
492
|
|
|
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 };
|
|
493
|
+
// src/tools/network_mock.ts
|
|
494
|
+
function escapeRegExpChar(ch) {
|
|
495
|
+
return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
|
|
532
496
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
497
|
+
function globToRegExp(pattern) {
|
|
498
|
+
let re = "";
|
|
499
|
+
for (let i = 0;i < pattern.length; i++) {
|
|
500
|
+
const ch = pattern[i];
|
|
501
|
+
if (ch === "\\") {
|
|
502
|
+
const next = pattern[i + 1];
|
|
503
|
+
if (next !== undefined) {
|
|
504
|
+
re += escapeRegExpChar(next);
|
|
505
|
+
i++;
|
|
506
|
+
} else {
|
|
507
|
+
re += "\\\\";
|
|
508
|
+
}
|
|
509
|
+
} else if (ch === "*") {
|
|
510
|
+
re += ".*";
|
|
511
|
+
} else if (ch === "?") {
|
|
512
|
+
re += ".";
|
|
513
|
+
} else {
|
|
514
|
+
re += escapeRegExpChar(ch);
|
|
515
|
+
}
|
|
544
516
|
}
|
|
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
|
-
});
|
|
517
|
+
return new RegExp(`^${re}$`);
|
|
565
518
|
}
|
|
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
|
-
});
|
|
519
|
+
function urlMatches(pattern, url) {
|
|
520
|
+
return globToRegExp(pattern).test(url);
|
|
573
521
|
}
|
|
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
|
-
});
|
|
522
|
+
function selectRule(rules, url, method) {
|
|
523
|
+
return rules.find((r) => urlMatches(r.urlPattern, url) && (!r.method || r.method.toUpperCase() === method.toUpperCase()));
|
|
606
524
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
returnByValue: true
|
|
613
|
-
});
|
|
614
|
-
if (value.length) {
|
|
615
|
-
await conn.send("Input.insertText", { text: value });
|
|
525
|
+
function buildFulfillParams(requestId, rule) {
|
|
526
|
+
const merged = new Map;
|
|
527
|
+
merged.set("content-type", { name: "Content-Type", value: rule.contentType ?? "application/json" });
|
|
528
|
+
for (const [name, value] of Object.entries(rule.headers ?? {})) {
|
|
529
|
+
merged.set(name.toLowerCase(), { name, value });
|
|
616
530
|
}
|
|
531
|
+
return {
|
|
532
|
+
requestId,
|
|
533
|
+
responseCode: rule.status ?? 200,
|
|
534
|
+
responseHeaders: [...merged.values()],
|
|
535
|
+
body: Buffer.from(rule.body ?? "", "utf8").toString("base64")
|
|
536
|
+
};
|
|
617
537
|
}
|
|
618
|
-
|
|
619
|
-
if (
|
|
620
|
-
|
|
621
|
-
return
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
538
|
+
function effectiveAction(rule, rnd) {
|
|
539
|
+
if (rule.failRate !== undefined && rule.failRate > 0 && rnd < rule.failRate)
|
|
540
|
+
return "fail";
|
|
541
|
+
return rule.action;
|
|
542
|
+
}
|
|
543
|
+
var sessions = new Map;
|
|
544
|
+
function delay(ms) {
|
|
545
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
546
|
+
}
|
|
547
|
+
function registerHandler(session) {
|
|
548
|
+
session.conn.on("Fetch.requestPaused", (params) => {
|
|
549
|
+
const p = params;
|
|
550
|
+
session.chain = session.chain.then(async () => {
|
|
551
|
+
const rule = selectRule(session.rules, p.request.url, p.request.method);
|
|
552
|
+
if (!rule) {
|
|
553
|
+
await session.conn.send("Fetch.continueRequest", { requestId: p.requestId });
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (rule.delayMs)
|
|
557
|
+
await delay(rule.delayMs);
|
|
558
|
+
const act = effectiveAction(rule, Math.random());
|
|
559
|
+
if (act === "fulfill") {
|
|
560
|
+
await session.conn.send("Fetch.fulfillRequest", { ...buildFulfillParams(p.requestId, rule) });
|
|
561
|
+
} else if (act === "fail") {
|
|
562
|
+
await session.conn.send("Fetch.failRequest", { requestId: p.requestId, errorReason: rule.errorReason ?? "Failed" });
|
|
563
|
+
} else {
|
|
564
|
+
await session.conn.send("Fetch.continueRequest", { requestId: p.requestId });
|
|
565
|
+
}
|
|
566
|
+
rule.hits++;
|
|
567
|
+
session.intercepted.push({ url: p.request.url, method: p.request.method, action: act });
|
|
568
|
+
}).catch((e) => {
|
|
569
|
+
if (/closed|connection|Target|Inspected/i.test(String(e)))
|
|
570
|
+
session.dead = true;
|
|
571
|
+
});
|
|
636
572
|
});
|
|
637
573
|
}
|
|
638
|
-
async function
|
|
639
|
-
if (!
|
|
640
|
-
throw new CdpError("
|
|
574
|
+
async function mockRequest(args) {
|
|
575
|
+
if (!args.urlPattern)
|
|
576
|
+
throw new CdpError("mock_request requires a { urlPattern }");
|
|
577
|
+
const rule = {
|
|
578
|
+
urlPattern: args.urlPattern,
|
|
579
|
+
action: args.action ?? "fulfill",
|
|
580
|
+
status: args.status,
|
|
581
|
+
body: args.body,
|
|
582
|
+
contentType: args.contentType,
|
|
583
|
+
headers: args.headers,
|
|
584
|
+
errorReason: args.errorReason,
|
|
585
|
+
method: args.method,
|
|
586
|
+
delayMs: args.delayMs,
|
|
587
|
+
failRate: args.failRate,
|
|
588
|
+
hits: 0
|
|
589
|
+
};
|
|
590
|
+
const resolved = await resolveTarget(args.target);
|
|
591
|
+
let session = sessions.get(resolved.id);
|
|
592
|
+
if (session && !session.dead) {
|
|
593
|
+
const idx = session.rules.findIndex((r) => r.urlPattern === rule.urlPattern && (r.method ?? "") === (rule.method ?? ""));
|
|
594
|
+
if (idx >= 0)
|
|
595
|
+
session.rules[idx] = rule;
|
|
596
|
+
else
|
|
597
|
+
session.rules.push(rule);
|
|
598
|
+
await session.conn.send("Fetch.enable", { patterns: session.rules.map((r) => ({ urlPattern: r.urlPattern })) });
|
|
599
|
+
} else {
|
|
600
|
+
const { conn, target } = await openPage(resolved.id);
|
|
601
|
+
session = { conn, target, rules: [rule], intercepted: [], chain: Promise.resolve(), dead: false };
|
|
602
|
+
registerHandler(session);
|
|
603
|
+
await conn.send("Fetch.enable", { patterns: [{ urlPattern: rule.urlPattern }] });
|
|
604
|
+
sessions.set(target.id, session);
|
|
641
605
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
606
|
+
let reloaded = false;
|
|
607
|
+
if (args.reload) {
|
|
608
|
+
await session.conn.send("Page.enable");
|
|
609
|
+
await session.conn.send("Page.reload", { ignoreCache: true });
|
|
610
|
+
await delay(800);
|
|
611
|
+
reloaded = true;
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
target: { id: session.target.id, url: session.target.url, title: session.target.title },
|
|
615
|
+
pattern: rule.urlPattern,
|
|
616
|
+
ruleCount: session.rules.length,
|
|
617
|
+
reloaded
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
async function listMocks(_args = {}) {
|
|
621
|
+
for (const [id, s] of sessions) {
|
|
622
|
+
if (s.dead) {
|
|
623
|
+
try {
|
|
624
|
+
s.conn.close();
|
|
625
|
+
} catch {}
|
|
626
|
+
sessions.delete(id);
|
|
627
|
+
continue;
|
|
651
628
|
}
|
|
652
|
-
|
|
653
|
-
|
|
629
|
+
try {
|
|
630
|
+
await s.conn.send("Runtime.evaluate", { expression: "1", returnByValue: true }, { timeoutMs: 3000 });
|
|
631
|
+
} catch {
|
|
632
|
+
try {
|
|
633
|
+
s.conn.close();
|
|
634
|
+
} catch {}
|
|
635
|
+
sessions.delete(id);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
const mocks = [...sessions.values()].map((s) => ({
|
|
639
|
+
target: { id: s.target.id, url: s.target.url, title: s.target.title },
|
|
640
|
+
rules: s.rules.map((r) => ({ urlPattern: r.urlPattern, action: r.action, method: r.method, hits: r.hits })),
|
|
641
|
+
hits: s.intercepted.length
|
|
642
|
+
}));
|
|
643
|
+
return { count: mocks.length, mocks };
|
|
644
|
+
}
|
|
645
|
+
async function clearMocks(args = {}) {
|
|
646
|
+
let ids;
|
|
647
|
+
if (args.all) {
|
|
648
|
+
ids = [...sessions.keys()];
|
|
649
|
+
} else {
|
|
650
|
+
const resolved = await resolveTarget(args.target);
|
|
651
|
+
ids = sessions.has(resolved.id) ? [resolved.id] : [];
|
|
652
|
+
}
|
|
653
|
+
let cleared = 0;
|
|
654
|
+
for (const id of ids) {
|
|
655
|
+
const s = sessions.get(id);
|
|
656
|
+
if (!s)
|
|
657
|
+
continue;
|
|
658
|
+
try {
|
|
659
|
+
await s.conn.send("Fetch.disable");
|
|
660
|
+
} catch {}
|
|
661
|
+
try {
|
|
662
|
+
s.conn.close();
|
|
663
|
+
} catch {}
|
|
664
|
+
sessions.delete(id);
|
|
665
|
+
cleared++;
|
|
666
|
+
}
|
|
667
|
+
return { cleared };
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/driver.ts
|
|
671
|
+
var UID_STAMP_ATTR = "data-cdp-uid";
|
|
672
|
+
var LEGACY_NUMERIC_UID = /^[0-9]+$/;
|
|
673
|
+
var REQUIRED_CAPABILITIES = {
|
|
674
|
+
performance_start_trace: ["trace.performance"],
|
|
675
|
+
performance_stop_trace: ["trace.performance"],
|
|
676
|
+
performance_analyze_insight: ["trace.performance"],
|
|
677
|
+
performance_trace: ["trace.performance"],
|
|
678
|
+
take_heapsnapshot: ["heap.snapshot"],
|
|
679
|
+
lighthouse_audit: ["audit.lighthouse"],
|
|
680
|
+
mock_request: ["network.intercept"],
|
|
681
|
+
list_mocks: ["network.intercept"],
|
|
682
|
+
clear_mocks: ["network.intercept"]
|
|
683
|
+
};
|
|
684
|
+
function isDriverError(e) {
|
|
685
|
+
return e instanceof Error && typeof e.code === "string";
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// src/cdp/driver.ts
|
|
689
|
+
function driverError(code, message, data) {
|
|
690
|
+
return Object.assign(new Error(message), { code, ...data !== undefined ? { data } : {} });
|
|
691
|
+
}
|
|
692
|
+
function encodeUid(backendNodeId) {
|
|
693
|
+
return `cdp:${backendNodeId}`;
|
|
694
|
+
}
|
|
695
|
+
function decodeUid(uid) {
|
|
696
|
+
if (LEGACY_NUMERIC_UID.test(uid))
|
|
697
|
+
return Number(uid);
|
|
698
|
+
if (!uid.startsWith("cdp:"))
|
|
699
|
+
throw driverError("foreign-uid", `uid is not owned by the cdp driver: ${uid}`);
|
|
700
|
+
const n = Number(uid.slice(4));
|
|
701
|
+
if (!Number.isSafeInteger(n) || n <= 0)
|
|
702
|
+
throw driverError("foreign-uid", `malformed cdp uid payload: ${uid}`);
|
|
703
|
+
return n;
|
|
704
|
+
}
|
|
705
|
+
async function centerOf(conn, objectId) {
|
|
706
|
+
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};}";
|
|
707
|
+
const { result, exceptionDetails } = await conn.send("Runtime.callFunctionOn", { objectId, functionDeclaration: fn, returnByValue: true });
|
|
708
|
+
if (exceptionDetails)
|
|
709
|
+
throw driverError("page-error", `could not measure element: ${exceptionDetails.text ?? "error"}`);
|
|
710
|
+
if (!result.value)
|
|
711
|
+
throw driverError("page-error", "element has zero size / is not visible; cannot compute a click point");
|
|
712
|
+
return result.value;
|
|
713
|
+
}
|
|
714
|
+
async function focusElement(conn, objectId) {
|
|
715
|
+
await conn.send("Runtime.callFunctionOn", { objectId, functionDeclaration: "function(){this.focus&&this.focus();}", returnByValue: true });
|
|
716
|
+
}
|
|
717
|
+
async function setValueOnObject(conn, objectId, value) {
|
|
718
|
+
await focusElement(conn, objectId);
|
|
719
|
+
const fn = "function(){if('value'in this){this.value='';this.dispatchEvent(new Event('input',{bubbles:true}));}else if(this.isContentEditable){this.textContent='';}}";
|
|
720
|
+
await conn.send("Runtime.callFunctionOn", { objectId, functionDeclaration: fn, returnByValue: true });
|
|
721
|
+
if (value.length)
|
|
722
|
+
await conn.send("Input.insertText", { text: value });
|
|
654
723
|
}
|
|
655
724
|
var NAMED_KEYS = {
|
|
656
725
|
enter: { key: "Enter", code: "Enter", keyCode: 13, text: "\r" },
|
|
@@ -674,15 +743,7 @@ var NAMED_KEYS = {
|
|
|
674
743
|
pageup: { key: "PageUp", code: "PageUp", keyCode: 33 },
|
|
675
744
|
pagedown: { key: "PageDown", code: "PageDown", keyCode: 34 }
|
|
676
745
|
};
|
|
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
|
-
};
|
|
746
|
+
var MODIFIER_BITS = { alt: 1, control: 2, ctrl: 2, meta: 4, cmd: 4, command: 4, shift: 8 };
|
|
686
747
|
function resolveKey(key) {
|
|
687
748
|
const named = NAMED_KEYS[key.toLowerCase()];
|
|
688
749
|
if (named)
|
|
@@ -692,225 +753,407 @@ function resolveKey(key) {
|
|
|
692
753
|
const isLetter = upper >= "A" && upper <= "Z";
|
|
693
754
|
const isDigit = key >= "0" && key <= "9";
|
|
694
755
|
const code = isLetter ? `Key${upper}` : isDigit ? `Digit${key}` : `Key${upper}`;
|
|
695
|
-
|
|
696
|
-
|
|
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
|
|
734
|
-
});
|
|
735
|
-
return { pressed: spec.key, modifiers: mods };
|
|
736
|
-
});
|
|
756
|
+
return { key, code, keyCode: upper.charCodeAt(0), text: key };
|
|
757
|
+
}
|
|
758
|
+
throw driverError("page-error", `unknown key '${key}' (use a named key like Enter/Tab/ArrowDown or a single character)`);
|
|
737
759
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
760
|
+
function unwrap(obj) {
|
|
761
|
+
if (Object.prototype.hasOwnProperty.call(obj, "value"))
|
|
762
|
+
return obj.value;
|
|
763
|
+
if (obj.unserializableValue !== undefined)
|
|
764
|
+
return obj.unserializableValue;
|
|
765
|
+
return obj.type === "undefined" ? undefined : obj.description ?? null;
|
|
766
|
+
}
|
|
767
|
+
function exceptionMessage(details) {
|
|
768
|
+
const ex = details.exception;
|
|
769
|
+
const fromObject = ex?.description ?? (typeof ex?.value === "string" ? ex.value : ex?.value !== undefined ? JSON.stringify(ex.value) : undefined);
|
|
770
|
+
return fromObject ?? details.text ?? "evaluation threw";
|
|
771
|
+
}
|
|
772
|
+
var INTERACTIVE_ROLES2 = new Set([
|
|
773
|
+
"button",
|
|
774
|
+
"link",
|
|
775
|
+
"textbox",
|
|
776
|
+
"searchbox",
|
|
777
|
+
"checkbox",
|
|
778
|
+
"radio",
|
|
779
|
+
"combobox",
|
|
780
|
+
"listbox",
|
|
781
|
+
"option",
|
|
782
|
+
"menuitem",
|
|
783
|
+
"menuitemcheckbox",
|
|
784
|
+
"menuitemradio",
|
|
785
|
+
"tab",
|
|
786
|
+
"switch",
|
|
787
|
+
"slider",
|
|
788
|
+
"spinbutton",
|
|
789
|
+
"textfield"
|
|
790
|
+
]);
|
|
791
|
+
function axString(v) {
|
|
792
|
+
if (!v || v.value === undefined || v.value === null)
|
|
793
|
+
return;
|
|
794
|
+
const s = String(v.value).trim();
|
|
795
|
+
return s.length ? s : undefined;
|
|
796
|
+
}
|
|
797
|
+
function propValue(node, name) {
|
|
798
|
+
return axString(node.properties?.find((p) => p.name === name)?.value);
|
|
799
|
+
}
|
|
800
|
+
function isInteractive(role) {
|
|
801
|
+
return !!role && (INTERACTIVE_ROLES2.has(role) || INTERACTIVE_ROLES2.has(role.toLowerCase()));
|
|
802
|
+
}
|
|
803
|
+
function axExtras(node) {
|
|
804
|
+
const extras = {};
|
|
805
|
+
const value = axString(node.value);
|
|
806
|
+
if (value)
|
|
807
|
+
extras.value = value;
|
|
808
|
+
const checked = propValue(node, "checked");
|
|
809
|
+
if (checked && checked !== "false")
|
|
810
|
+
extras.checked = checked;
|
|
811
|
+
const expanded = propValue(node, "expanded");
|
|
812
|
+
if (expanded)
|
|
813
|
+
extras.expanded = expanded;
|
|
814
|
+
if (propValue(node, "disabled") === "true")
|
|
815
|
+
extras.disabled = "true";
|
|
816
|
+
const url = propValue(node, "url");
|
|
817
|
+
if (url)
|
|
818
|
+
extras.url = url;
|
|
819
|
+
if (propValue(node, "focused") === "true")
|
|
820
|
+
extras.focused = "true";
|
|
821
|
+
return extras;
|
|
822
|
+
}
|
|
823
|
+
async function resolveElementLocator(conn, loc) {
|
|
824
|
+
if ("uid" in loc) {
|
|
825
|
+
return resolveUid(conn, decodeUid(loc.uid)).catch(() => {
|
|
826
|
+
throw driverError("stale-uid", `uid does not resolve: ${loc.uid}`);
|
|
827
|
+
});
|
|
742
828
|
}
|
|
743
|
-
|
|
744
|
-
const {
|
|
745
|
-
|
|
746
|
-
|
|
829
|
+
if ("css" in loc) {
|
|
830
|
+
const { result, exceptionDetails } = await conn.send("Runtime.evaluate", { expression: `document.querySelector(${JSON.stringify(loc.css)})`, returnByValue: false });
|
|
831
|
+
if (exceptionDetails)
|
|
832
|
+
throw driverError("page-error", `css '${loc.css}' evaluation failed: ${exceptionDetails.text ?? "error"}`);
|
|
833
|
+
if (!result.objectId || result.subtype === "null")
|
|
834
|
+
throw driverError("no-such-element", `css '${loc.css}' matched no element`);
|
|
835
|
+
return { objectId: result.objectId };
|
|
836
|
+
}
|
|
837
|
+
const backendNodeId = await searchLocate(conn, "xpath" in loc ? loc.xpath : loc.text);
|
|
838
|
+
return resolveUid(conn, backendNodeId).catch(() => {
|
|
839
|
+
throw driverError("stale-uid", "text/xpath match no longer resolves");
|
|
747
840
|
});
|
|
748
841
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
842
|
+
async function searchLocate(conn, query) {
|
|
843
|
+
await conn.send("DOM.getDocument", { depth: 0 }).catch(() => {
|
|
844
|
+
return;
|
|
845
|
+
});
|
|
846
|
+
const { searchId, resultCount } = await conn.send("DOM.performSearch", { query });
|
|
847
|
+
try {
|
|
848
|
+
if (!resultCount)
|
|
849
|
+
throw driverError("no-such-element", `locator matched nothing: ${query}`);
|
|
850
|
+
const { nodeIds } = await conn.send("DOM.getSearchResults", { searchId, fromIndex: 0, toIndex: 1 });
|
|
851
|
+
const nodeId = nodeIds[0];
|
|
852
|
+
if (nodeId === undefined)
|
|
853
|
+
throw driverError("no-such-element", `locator matched nothing: ${query}`);
|
|
854
|
+
const { node } = await conn.send("DOM.describeNode", { nodeId });
|
|
855
|
+
return node.backendNodeId;
|
|
856
|
+
} finally {
|
|
857
|
+
await conn.send("DOM.discardSearchResults", { searchId }).catch(() => {
|
|
858
|
+
return;
|
|
859
|
+
});
|
|
860
|
+
}
|
|
756
861
|
}
|
|
757
|
-
function
|
|
758
|
-
|
|
862
|
+
async function backendNodeIdOf(conn, loc) {
|
|
863
|
+
if ("uid" in loc)
|
|
864
|
+
return decodeUid(loc.uid);
|
|
865
|
+
if ("css" in loc) {
|
|
866
|
+
const { objectId } = await resolveElementLocator(conn, loc);
|
|
867
|
+
const { node } = await conn.send("DOM.describeNode", { objectId });
|
|
868
|
+
return node.backendNodeId;
|
|
869
|
+
}
|
|
870
|
+
return searchLocate(conn, "xpath" in loc ? loc.xpath : loc.text);
|
|
759
871
|
}
|
|
760
872
|
async function fullPageClip(conn) {
|
|
761
873
|
const m = await conn.send("Page.getLayoutMetrics");
|
|
762
874
|
const size = m.cssContentSize ?? m.contentSize;
|
|
763
875
|
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 {}
|
|
876
|
+
throw driverError("page-error", "Page.getLayoutMetrics returned no content size");
|
|
877
|
+
return { x: 0, y: 0, width: Math.ceil(size.width), height: Math.ceil(size.height) };
|
|
878
|
+
}
|
|
879
|
+
async function elementClip(conn, loc) {
|
|
880
|
+
const backendNodeId = await backendNodeIdOf(conn, loc);
|
|
881
|
+
await conn.send("DOM.scrollIntoViewIfNeeded", { backendNodeId }).catch(() => {
|
|
882
|
+
return;
|
|
883
|
+
});
|
|
787
884
|
const box = await conn.send("DOM.getBoxModel", { backendNodeId });
|
|
788
885
|
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
886
|
const xs = [q[0], q[2], q[4], q[6]].filter((n) => n != null);
|
|
794
887
|
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);
|
|
888
|
+
if (!xs.length || !ys.length)
|
|
889
|
+
throw driverError("page-error", "DOM.getBoxModel returned an empty content quad");
|
|
890
|
+
const minX = Math.min(...xs), minY = Math.min(...ys);
|
|
891
|
+
const width = Math.ceil(Math.max(...xs) - minX), height = Math.ceil(Math.max(...ys) - minY);
|
|
801
892
|
if (width <= 0 || height <= 0)
|
|
802
|
-
throw
|
|
803
|
-
return { x: minX, y: minY, width, height
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
893
|
+
throw driverError("page-error", "resolved element has zero area");
|
|
894
|
+
return { x: minX, y: minY, width, height };
|
|
895
|
+
}
|
|
896
|
+
var CDP_CAPABILITIES = new Set([
|
|
897
|
+
"trace.performance",
|
|
898
|
+
"heap.snapshot",
|
|
899
|
+
"audit.lighthouse",
|
|
900
|
+
"emulate.cpuThrottling",
|
|
901
|
+
"emulate.mediaFeatures",
|
|
902
|
+
"emulate.deviceMetrics",
|
|
903
|
+
"emulate.networkConditions",
|
|
904
|
+
"screenshot.fullPage",
|
|
905
|
+
"screenshot.element",
|
|
906
|
+
"network.intercept",
|
|
907
|
+
"snapshot.accessibilityTree",
|
|
908
|
+
"input.insertTextAtomic",
|
|
909
|
+
"locate.text",
|
|
910
|
+
"locate.xpath"
|
|
911
|
+
]);
|
|
912
|
+
|
|
913
|
+
class CdpPageDriver {
|
|
914
|
+
conn;
|
|
915
|
+
browser;
|
|
916
|
+
info;
|
|
917
|
+
enabledDomains = new Set;
|
|
918
|
+
released = false;
|
|
919
|
+
constructor(conn, target, browser) {
|
|
920
|
+
this.conn = conn;
|
|
921
|
+
this.browser = browser;
|
|
922
|
+
this.info = { id: target.id, url: target.url, title: target.title, type: target.type };
|
|
923
|
+
}
|
|
924
|
+
async ensureDomain(name) {
|
|
925
|
+
if (this.enabledDomains.has(name))
|
|
926
|
+
return;
|
|
927
|
+
this.enabledDomains.add(name);
|
|
928
|
+
await this.conn.send(`${name}.enable`);
|
|
929
|
+
}
|
|
930
|
+
async ensureDomainSoft(name) {
|
|
931
|
+
if (this.enabledDomains.has(name))
|
|
932
|
+
return;
|
|
933
|
+
this.enabledDomains.add(name);
|
|
934
|
+
await this.conn.send(`${name}.enable`).catch(() => {
|
|
935
|
+
return;
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
async navigate(opts) {
|
|
939
|
+
await this.ensureDomain("Page");
|
|
940
|
+
const reload = opts.reload === true;
|
|
941
|
+
if (!reload && !opts.url)
|
|
942
|
+
throw driverError("page-error", "navigate: 'url' is required unless reload:true");
|
|
943
|
+
const waitUntil = opts.waitUntil ?? "load";
|
|
944
|
+
const milestoneMethod = waitUntil === "load" ? "Page.loadEventFired" : "Page.domContentEventFired";
|
|
945
|
+
const milestone = this.conn.waitFor(milestoneMethod, undefined, opts.timeoutMs).then(() => waitUntil).catch(() => {
|
|
946
|
+
return;
|
|
947
|
+
});
|
|
948
|
+
const stopped = this.conn.waitFor("Page.frameStoppedLoading", undefined, opts.timeoutMs).then(() => "committed").catch(() => {
|
|
949
|
+
return;
|
|
950
|
+
});
|
|
951
|
+
let url, contextId, reloaded;
|
|
952
|
+
if (reload) {
|
|
953
|
+
await this.conn.send("Page.reload", { ignoreCache: opts.ignoreCache === true }, opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {});
|
|
954
|
+
const tree = await this.conn.send("Page.getFrameTree").catch(() => {
|
|
955
|
+
return;
|
|
956
|
+
});
|
|
957
|
+
contextId = tree?.frameTree?.frame?.id ?? "";
|
|
958
|
+
url = tree?.frameTree?.frame?.url ?? opts.url ?? "";
|
|
959
|
+
reloaded = true;
|
|
960
|
+
} else {
|
|
961
|
+
const nav = await this.conn.send("Page.navigate", { url: opts.url }, opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {});
|
|
962
|
+
if (nav.errorText)
|
|
963
|
+
throw driverError("page-error", `navigate: ${nav.errorText} (${opts.url})`);
|
|
964
|
+
contextId = nav.frameId;
|
|
965
|
+
url = opts.url;
|
|
966
|
+
}
|
|
967
|
+
const settled = await Promise.race([milestone, stopped]);
|
|
968
|
+
return { url, contextId, ...reloaded ? { reloaded } : {}, waitedFor: settled ?? "timeout" };
|
|
969
|
+
}
|
|
970
|
+
async waitForText(text, timeoutMs = 15000, pollMs = 250) {
|
|
971
|
+
await this.ensureDomain("Runtime");
|
|
972
|
+
const start = Date.now();
|
|
973
|
+
const expr = `(() => { const b = document.body; return !!b && typeof b.innerText === 'string' && b.innerText.includes(${JSON.stringify(text)}); })()`;
|
|
974
|
+
for (;; ) {
|
|
975
|
+
const { result } = await this.conn.send("Runtime.evaluate", { expression: expr, returnByValue: true });
|
|
976
|
+
if (result?.value === true)
|
|
977
|
+
return { found: true, elapsedMs: Date.now() - start };
|
|
978
|
+
if (Date.now() - start >= timeoutMs)
|
|
979
|
+
throw driverError("timeout", `waitForText: '${text}' not found within ${timeoutMs}ms`);
|
|
980
|
+
await new Promise((r) => setTimeout(r, Math.min(pollMs, Math.max(0, timeoutMs - (Date.now() - start)))));
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
async callFn(objectId, functionDeclaration, args, awaitPromise) {
|
|
984
|
+
const res = await this.conn.send("Runtime.callFunctionOn", { objectId, functionDeclaration, arguments: args.map((value) => ({ value })), returnByValue: true, awaitPromise });
|
|
985
|
+
if (res.exceptionDetails)
|
|
986
|
+
throw driverError("page-error", exceptionMessage(res.exceptionDetails));
|
|
987
|
+
return unwrap(res.result);
|
|
988
|
+
}
|
|
989
|
+
async evaluate(expression, opts) {
|
|
990
|
+
await this.ensureDomain("Runtime");
|
|
991
|
+
const awaitPromise = opts?.awaitPromise ?? true;
|
|
992
|
+
if (opts?.args && opts.args.length > 0) {
|
|
993
|
+
const globalObj = await this.conn.send("Runtime.evaluate", { expression: "globalThis", returnByValue: false });
|
|
994
|
+
const objectId = globalObj.result.objectId;
|
|
995
|
+
if (!objectId)
|
|
996
|
+
throw driverError("page-error", "could not resolve global object for function call");
|
|
997
|
+
return this.callFn(objectId, expression, opts.args, awaitPromise);
|
|
998
|
+
}
|
|
999
|
+
const res = await this.conn.send("Runtime.evaluate", { expression, returnByValue: true, awaitPromise });
|
|
1000
|
+
if (res.exceptionDetails)
|
|
1001
|
+
throw driverError("page-error", exceptionMessage(res.exceptionDetails));
|
|
1002
|
+
return unwrap(res.result);
|
|
1003
|
+
}
|
|
1004
|
+
async callOnElement(loc, functionDeclaration, args = []) {
|
|
1005
|
+
const { objectId } = await resolveElementLocator(this.conn, loc);
|
|
1006
|
+
return this.callFn(objectId, functionDeclaration, args, true);
|
|
1007
|
+
}
|
|
1008
|
+
async snapshot(opts) {
|
|
1009
|
+
const interactiveOnly = opts?.interactiveOnly ?? false;
|
|
1010
|
+
await this.ensureDomain("Accessibility");
|
|
1011
|
+
const { nodes } = await this.conn.send("Accessibility.getFullAXTree");
|
|
1012
|
+
const byId = new Map;
|
|
1013
|
+
for (const n of nodes)
|
|
1014
|
+
byId.set(n.nodeId, n);
|
|
1015
|
+
const roots = nodes.filter((n) => !n.parentId || !byId.has(n.parentId));
|
|
1016
|
+
const out = [];
|
|
1017
|
+
const walk = (node, depth) => {
|
|
1018
|
+
const role = axString(node.role);
|
|
1019
|
+
const name = axString(node.name);
|
|
1020
|
+
const backendNodeId = node.backendDOMNodeId;
|
|
1021
|
+
const emitted = !node.ignored && backendNodeId !== undefined && (interactiveOnly ? isInteractive(role) : !!(role || name));
|
|
1022
|
+
if (emitted) {
|
|
1023
|
+
const extras = axExtras(node);
|
|
1024
|
+
out.push({ uid: encodeUid(backendNodeId), role: role ?? "generic", depth, ...name ? { name } : {}, ...Object.keys(extras).length ? { extras } : {} });
|
|
1025
|
+
}
|
|
1026
|
+
const childDepth = emitted && !interactiveOnly ? depth + 1 : depth;
|
|
1027
|
+
for (const childId of node.childIds ?? []) {
|
|
1028
|
+
const child = byId.get(childId);
|
|
1029
|
+
if (child)
|
|
1030
|
+
walk(child, childDepth);
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
for (const root of roots)
|
|
1034
|
+
walk(root, 0);
|
|
1035
|
+
return out;
|
|
1036
|
+
}
|
|
1037
|
+
async locate(loc) {
|
|
1038
|
+
return encodeUid(await backendNodeIdOf(this.conn, loc));
|
|
1039
|
+
}
|
|
1040
|
+
async resolve(uid) {
|
|
1041
|
+
await resolveUid(this.conn, decodeUid(uid)).catch(() => {
|
|
1042
|
+
throw driverError("stale-uid", `uid does not resolve: ${uid}`);
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
async hover(loc) {
|
|
1046
|
+
const { objectId } = await resolveElementLocator(this.conn, loc);
|
|
1047
|
+
const { x, y } = await centerOf(this.conn, objectId);
|
|
1048
|
+
await this.conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y });
|
|
1049
|
+
return { x, y };
|
|
1050
|
+
}
|
|
1051
|
+
async click(loc, opts) {
|
|
1052
|
+
const button = opts?.button ?? "left";
|
|
1053
|
+
const clickCount = opts?.clickCount ?? 1;
|
|
1054
|
+
const { x, y } = await this.hover(loc);
|
|
1055
|
+
await this.conn.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button, clickCount });
|
|
1056
|
+
await this.conn.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button, clickCount });
|
|
1057
|
+
return { x, y };
|
|
1058
|
+
}
|
|
1059
|
+
async drag(from, to) {
|
|
1060
|
+
const fromPt = await this.hover(from);
|
|
1061
|
+
const { objectId: dstId } = await resolveElementLocator(this.conn, to);
|
|
1062
|
+
const toPt = await centerOf(this.conn, dstId);
|
|
1063
|
+
await this.conn.send("Input.dispatchMouseEvent", { type: "mousePressed", x: fromPt.x, y: fromPt.y, button: "left", clickCount: 1 });
|
|
1064
|
+
await this.conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: (fromPt.x + toPt.x) / 2, y: (fromPt.y + toPt.y) / 2, button: "left" });
|
|
1065
|
+
await this.conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: toPt.x, y: toPt.y, button: "left" });
|
|
1066
|
+
await this.conn.send("Input.dispatchMouseEvent", { type: "mouseReleased", x: toPt.x, y: toPt.y, button: "left", clickCount: 1 });
|
|
1067
|
+
return { from: fromPt, to: toPt };
|
|
1068
|
+
}
|
|
1069
|
+
async setValue(loc, value) {
|
|
1070
|
+
const { objectId } = await resolveElementLocator(this.conn, loc);
|
|
1071
|
+
await setValueOnObject(this.conn, objectId, value);
|
|
1072
|
+
}
|
|
1073
|
+
async typeText(loc, text) {
|
|
1074
|
+
const { objectId } = await resolveElementLocator(this.conn, loc);
|
|
1075
|
+
await focusElement(this.conn, objectId);
|
|
1076
|
+
if (text.length)
|
|
1077
|
+
await this.conn.send("Input.insertText", { text });
|
|
1078
|
+
}
|
|
1079
|
+
async pressKey(press) {
|
|
1080
|
+
const spec = resolveKey(press.key);
|
|
1081
|
+
let modBits = 0;
|
|
1082
|
+
for (const m of press.modifiers ?? []) {
|
|
1083
|
+
const bit = MODIFIER_BITS[m.toLowerCase()];
|
|
1084
|
+
if (bit === undefined)
|
|
1085
|
+
throw driverError("page-error", `unknown modifier '${m}'`);
|
|
1086
|
+
modBits |= bit;
|
|
1087
|
+
}
|
|
1088
|
+
const suppressText = (modBits & ~MODIFIER_BITS.shift) !== 0;
|
|
1089
|
+
const down = { type: "keyDown", key: spec.key, code: spec.code, windowsVirtualKeyCode: spec.keyCode, nativeVirtualKeyCode: spec.keyCode, modifiers: modBits };
|
|
1090
|
+
if (spec.text && !suppressText)
|
|
1091
|
+
down.text = spec.text;
|
|
1092
|
+
await this.conn.send("Input.dispatchKeyEvent", down);
|
|
1093
|
+
await this.conn.send("Input.dispatchKeyEvent", { type: "keyUp", key: spec.key, code: spec.code, windowsVirtualKeyCode: spec.keyCode, nativeVirtualKeyCode: spec.keyCode, modifiers: modBits });
|
|
1094
|
+
}
|
|
1095
|
+
async setFiles(loc, files) {
|
|
1096
|
+
const { objectId } = await resolveElementLocator(this.conn, loc);
|
|
1097
|
+
await this.conn.send("DOM.setFileInputFiles", { files: [...files], objectId });
|
|
1098
|
+
}
|
|
1099
|
+
async screenshot(opts) {
|
|
1100
|
+
await this.ensureDomain("Page");
|
|
1101
|
+
const format = opts?.format ?? "png";
|
|
1102
|
+
const quality = format === "jpeg" ? opts?.quality ?? 80 : undefined;
|
|
813
1103
|
const params = { format, captureBeyondViewport: true };
|
|
814
1104
|
if (quality != null)
|
|
815
1105
|
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
|
|
1106
|
+
if (opts?.clip)
|
|
1107
|
+
params.clip = { ...await elementClip(this.conn, opts.clip), scale: 1 };
|
|
1108
|
+
else if (opts?.fullPage)
|
|
1109
|
+
params.clip = { ...await fullPageClip(this.conn), scale: 1 };
|
|
1110
|
+
const { data } = await this.conn.send("Page.captureScreenshot", params);
|
|
1111
|
+
return { data: new Uint8Array(Buffer.from(data, "base64")), format };
|
|
1112
|
+
}
|
|
1113
|
+
async emulate(opts) {
|
|
1114
|
+
if (opts.clearOverrides) {
|
|
1115
|
+
const c = () => {
|
|
1116
|
+
return;
|
|
876
1117
|
};
|
|
1118
|
+
await this.conn.send("Emulation.clearDeviceMetricsOverride").catch(c);
|
|
1119
|
+
await this.conn.send("Emulation.setUserAgentOverride", { userAgent: "" }).catch(c);
|
|
1120
|
+
await this.conn.send("Emulation.setCPUThrottlingRate", { rate: 1 }).catch(c);
|
|
1121
|
+
await this.conn.send("Emulation.setEmulatedMedia", { media: "", features: [] }).catch(c);
|
|
1122
|
+
await this.ensureDomainSoft("Network");
|
|
1123
|
+
await this.conn.send("Network.emulateNetworkConditions", { offline: false, latency: 0, downloadThroughput: -1, uploadThroughput: -1 }).catch(c);
|
|
1124
|
+
return { applied: [] };
|
|
877
1125
|
}
|
|
878
1126
|
const applied = [];
|
|
879
|
-
if (
|
|
880
|
-
if (
|
|
881
|
-
throw
|
|
1127
|
+
if (opts.width != null || opts.height != null) {
|
|
1128
|
+
if (opts.width == null || opts.height == null) {
|
|
1129
|
+
throw driverError("page-error", "emulate device metrics require both width and height");
|
|
882
1130
|
}
|
|
883
|
-
await conn.send("Emulation.setDeviceMetricsOverride", {
|
|
884
|
-
width:
|
|
885
|
-
height:
|
|
886
|
-
deviceScaleFactor:
|
|
887
|
-
mobile:
|
|
1131
|
+
await this.conn.send("Emulation.setDeviceMetricsOverride", {
|
|
1132
|
+
width: opts.width,
|
|
1133
|
+
height: opts.height,
|
|
1134
|
+
deviceScaleFactor: opts.deviceScaleFactor ?? 0,
|
|
1135
|
+
mobile: opts.mobile ?? false
|
|
888
1136
|
});
|
|
889
1137
|
applied.push("deviceMetrics");
|
|
890
1138
|
}
|
|
891
|
-
if (
|
|
892
|
-
await conn.send("Emulation.setUserAgentOverride", { userAgent:
|
|
1139
|
+
if (opts.userAgent != null) {
|
|
1140
|
+
await this.conn.send("Emulation.setUserAgentOverride", { userAgent: opts.userAgent });
|
|
893
1141
|
applied.push("userAgent");
|
|
894
1142
|
}
|
|
895
|
-
if (
|
|
896
|
-
if (
|
|
897
|
-
throw
|
|
898
|
-
await conn.send("Emulation.setCPUThrottlingRate", { rate:
|
|
1143
|
+
if (opts.cpuThrottlingRate != null) {
|
|
1144
|
+
if (opts.cpuThrottlingRate < 1)
|
|
1145
|
+
throw driverError("page-error", "cpuThrottlingRate must be >= 1");
|
|
1146
|
+
await this.conn.send("Emulation.setCPUThrottlingRate", { rate: opts.cpuThrottlingRate });
|
|
899
1147
|
applied.push("cpuThrottlingRate");
|
|
900
1148
|
}
|
|
901
|
-
if (
|
|
902
|
-
await conn.send("Emulation.setEmulatedMedia", {
|
|
903
|
-
media: args.media ?? "",
|
|
904
|
-
features: args.mediaFeatures ?? []
|
|
905
|
-
});
|
|
1149
|
+
if (opts.media != null || opts.mediaFeatures != null) {
|
|
1150
|
+
await this.conn.send("Emulation.setEmulatedMedia", { media: opts.media ?? "", features: opts.mediaFeatures ?? [] });
|
|
906
1151
|
applied.push("emulatedMedia");
|
|
907
1152
|
}
|
|
908
|
-
if (
|
|
909
|
-
const n =
|
|
910
|
-
await
|
|
911
|
-
|
|
912
|
-
});
|
|
913
|
-
await conn.send("Network.emulateNetworkConditions", {
|
|
1153
|
+
if (opts.networkConditions != null) {
|
|
1154
|
+
const n = opts.networkConditions;
|
|
1155
|
+
await this.ensureDomainSoft("Network");
|
|
1156
|
+
await this.conn.send("Network.emulateNetworkConditions", {
|
|
914
1157
|
offline: n.offline ?? false,
|
|
915
1158
|
latency: n.latency ?? 0,
|
|
916
1159
|
downloadThroughput: n.downloadThroughput ?? -1,
|
|
@@ -919,95 +1162,542 @@ async function emulate(args = {}) {
|
|
|
919
1162
|
});
|
|
920
1163
|
applied.push("networkConditions");
|
|
921
1164
|
}
|
|
922
|
-
|
|
923
|
-
|
|
1165
|
+
return { applied };
|
|
1166
|
+
}
|
|
1167
|
+
static EVENT_MAP = {
|
|
1168
|
+
navigated: { method: "Page.frameNavigated" },
|
|
1169
|
+
console: { method: "Runtime.consoleAPICalled" },
|
|
1170
|
+
"network.request": { method: "Network.requestWillBeSent", domain: "Network" },
|
|
1171
|
+
"network.response": { method: "Network.responseReceived", domain: "Network" },
|
|
1172
|
+
dialog: { method: "Page.javascriptDialogOpening" },
|
|
1173
|
+
"page.closed": { method: "Inspector.detached", domain: "Inspector" }
|
|
1174
|
+
};
|
|
1175
|
+
on(event, handler) {
|
|
1176
|
+
const spec = CdpPageDriver.EVENT_MAP[event];
|
|
1177
|
+
if (spec.domain && !this.enabledDomains.has(spec.domain)) {
|
|
1178
|
+
this.enabledDomains.add(spec.domain);
|
|
1179
|
+
this.conn.send(`${spec.domain}.enable`);
|
|
924
1180
|
}
|
|
925
|
-
return
|
|
1181
|
+
return this.conn.on(spec.method, (params) => handler(params));
|
|
1182
|
+
}
|
|
1183
|
+
async waitForEvent(event, predicate, timeoutMs) {
|
|
1184
|
+
const spec = CdpPageDriver.EVENT_MAP[event];
|
|
1185
|
+
if (spec.domain)
|
|
1186
|
+
await this.ensureDomain(spec.domain);
|
|
1187
|
+
return this.conn.waitFor(spec.method, predicate, timeoutMs);
|
|
1188
|
+
}
|
|
1189
|
+
armDialog(accept, promptText, onHandled) {
|
|
1190
|
+
return this.conn.on("Page.javascriptDialogOpening", (params) => {
|
|
1191
|
+
const p = params;
|
|
1192
|
+
this.conn.send("Page.handleJavaScriptDialog", { accept, ...promptText !== undefined ? { promptText } : {} }).then(() => onHandled({
|
|
1193
|
+
type: p.type,
|
|
1194
|
+
message: p.message,
|
|
1195
|
+
url: p.url,
|
|
1196
|
+
...p.defaultPrompt !== undefined ? { defaultPrompt: p.defaultPrompt } : {},
|
|
1197
|
+
accept,
|
|
1198
|
+
...promptText !== undefined ? { promptText } : {},
|
|
1199
|
+
handled: true
|
|
1200
|
+
})).catch(() => {
|
|
1201
|
+
return;
|
|
1202
|
+
});
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
async handleDialog(accept, promptText, opts) {
|
|
1206
|
+
await this.ensureDomain("Page");
|
|
1207
|
+
if (opts?.autoMs !== undefined) {
|
|
1208
|
+
const handled = [];
|
|
1209
|
+
const off = this.armDialog(accept, promptText, (d) => handled.push(d));
|
|
1210
|
+
await new Promise((resolve) => setTimeout(resolve, opts.autoMs));
|
|
1211
|
+
off();
|
|
1212
|
+
return { handled, count: handled.length };
|
|
1213
|
+
}
|
|
1214
|
+
const timeoutMs = opts?.timeoutMs ?? 15000;
|
|
1215
|
+
return new Promise((resolve, reject) => {
|
|
1216
|
+
const timer = setTimeout(() => {
|
|
1217
|
+
off();
|
|
1218
|
+
reject(driverError("timeout", `handleDialog: no dialog opened within ${timeoutMs}ms`));
|
|
1219
|
+
}, timeoutMs);
|
|
1220
|
+
const off = this.armDialog(accept, promptText, (d) => {
|
|
1221
|
+
clearTimeout(timer);
|
|
1222
|
+
off();
|
|
1223
|
+
resolve(d);
|
|
1224
|
+
});
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
async startBodyCapture() {
|
|
1228
|
+
await this.ensureDomain("Network");
|
|
1229
|
+
return async () => {
|
|
1230
|
+
await this.conn.send("Network.disable").catch(() => {
|
|
1231
|
+
return;
|
|
1232
|
+
});
|
|
1233
|
+
this.enabledDomains.delete("Network");
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
async getResponseBody(requestId) {
|
|
1237
|
+
return this.conn.send("Network.getResponseBody", { requestId });
|
|
1238
|
+
}
|
|
1239
|
+
async intercept(rules) {
|
|
1240
|
+
await this.conn.send("Fetch.enable", { patterns: rules.map((r) => ({ urlPattern: r.urlPattern })) });
|
|
1241
|
+
let chain = Promise.resolve();
|
|
1242
|
+
const off = this.conn.on("Fetch.requestPaused", (params) => {
|
|
1243
|
+
const p = params;
|
|
1244
|
+
chain = chain.then(async () => {
|
|
1245
|
+
const rule = selectRule(rules, p.request.url, p.request.method);
|
|
1246
|
+
if (!rule) {
|
|
1247
|
+
await this.conn.send("Fetch.continueRequest", { requestId: p.requestId });
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
if (rule.delayMs)
|
|
1251
|
+
await new Promise((r) => setTimeout(r, rule.delayMs));
|
|
1252
|
+
const act = effectiveAction(rule, Math.random());
|
|
1253
|
+
if (act === "fulfill")
|
|
1254
|
+
await this.conn.send("Fetch.fulfillRequest", { ...buildFulfillParams(p.requestId, rule) });
|
|
1255
|
+
else if (act === "fail")
|
|
1256
|
+
await this.conn.send("Fetch.failRequest", { requestId: p.requestId, errorReason: rule.errorReason ?? "Failed" });
|
|
1257
|
+
else
|
|
1258
|
+
await this.conn.send("Fetch.continueRequest", { requestId: p.requestId });
|
|
1259
|
+
}).catch(() => {
|
|
1260
|
+
return;
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1263
|
+
return async () => {
|
|
1264
|
+
off();
|
|
1265
|
+
await this.conn.send("Fetch.disable").catch(() => {
|
|
1266
|
+
return;
|
|
1267
|
+
});
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
async release() {
|
|
1271
|
+
if (this.released)
|
|
1272
|
+
return;
|
|
1273
|
+
this.released = true;
|
|
1274
|
+
this.conn.close();
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
async function withBrowserConn(fn) {
|
|
1278
|
+
const conn = await openBrowser();
|
|
1279
|
+
try {
|
|
1280
|
+
return await fn(conn);
|
|
1281
|
+
} finally {
|
|
1282
|
+
conn.close();
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
function noSuchTarget(e) {
|
|
1286
|
+
throw driverError("no-such-target", e instanceof CdpError ? e.message : String(e));
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
class CdpBrowserDriver {
|
|
1290
|
+
scheme = "cdp";
|
|
1291
|
+
lifetime = "per-call";
|
|
1292
|
+
capabilities = CDP_CAPABILITIES;
|
|
1293
|
+
uidStability = "browser-node";
|
|
1294
|
+
snapshotFidelity = "accessibility-tree";
|
|
1295
|
+
async listPages(opts) {
|
|
1296
|
+
const targets = await listTargets();
|
|
1297
|
+
const filtered = opts?.all ? targets : targets.filter((t) => t.type === "page");
|
|
1298
|
+
return filtered.map((t) => ({ id: t.id, url: t.url, title: t.title, type: t.type }));
|
|
1299
|
+
}
|
|
1300
|
+
async newPage(url) {
|
|
1301
|
+
return withBrowserConn(async (conn) => {
|
|
1302
|
+
const { targetId } = await conn.send("Target.createTarget", { url: url ?? "about:blank" });
|
|
1303
|
+
if (!targetId)
|
|
1304
|
+
throw driverError("page-error", "Target.createTarget returned no targetId");
|
|
1305
|
+
return { id: targetId, url: url ?? "about:blank", title: "" };
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
async closePage(id) {
|
|
1309
|
+
return withBrowserConn(async (conn) => {
|
|
1310
|
+
const res = await conn.send("Target.closeTarget", { targetId: id });
|
|
1311
|
+
return { success: res.success ?? true };
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
async activatePage(id) {
|
|
1315
|
+
const target = await resolveTarget(id).catch(noSuchTarget);
|
|
1316
|
+
await withBrowserConn((conn) => conn.send("Target.activateTarget", { targetId: target.id }));
|
|
1317
|
+
return { id: target.id, url: target.url, title: target.title };
|
|
1318
|
+
}
|
|
1319
|
+
async page(selector) {
|
|
1320
|
+
const { conn, target } = await openPage(selector).catch(noSuchTarget);
|
|
1321
|
+
return new CdpPageDriver(conn, target, this);
|
|
1322
|
+
}
|
|
1323
|
+
async dispose() {}
|
|
1324
|
+
}
|
|
1325
|
+
function createCdpDriver() {
|
|
1326
|
+
return new CdpBrowserDriver;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// src/leases-tools.ts
|
|
1330
|
+
class LeaseToolError extends Error {
|
|
1331
|
+
}
|
|
1332
|
+
function backendOf(driver) {
|
|
1333
|
+
return driver.scheme === "bidi" ? "firefox" : "chrome";
|
|
1334
|
+
}
|
|
1335
|
+
async function claimPage(driver, args = {}) {
|
|
1336
|
+
const backend = backendOf(driver);
|
|
1337
|
+
const label = typeof args.label === "string" && args.label.length ? args.label : `pid-${process.pid}`;
|
|
1338
|
+
const pages = await driver.listPages();
|
|
1339
|
+
let targetId = args.targetId;
|
|
1340
|
+
let url = args.url ?? "about:blank";
|
|
1341
|
+
if (targetId === undefined || targetId === "") {
|
|
1342
|
+
const page = await driver.newPage(args.url);
|
|
1343
|
+
targetId = page.id;
|
|
1344
|
+
url = page.url;
|
|
1345
|
+
} else {
|
|
1346
|
+
const existing = pages.find((p) => p.id === targetId);
|
|
1347
|
+
if (!existing)
|
|
1348
|
+
throw new LeaseToolError(`claim_page: no page target with id '${targetId}'`);
|
|
1349
|
+
url = existing.url;
|
|
1350
|
+
}
|
|
1351
|
+
const liveIds = [...pages.map((p) => p.id), targetId];
|
|
1352
|
+
const { record, token } = await claimLease(backend, targetId, {
|
|
1353
|
+
label,
|
|
1354
|
+
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
|
|
1355
|
+
liveIds
|
|
1356
|
+
});
|
|
1357
|
+
return {
|
|
1358
|
+
lease: token,
|
|
1359
|
+
targetId,
|
|
1360
|
+
url,
|
|
1361
|
+
label: record.label,
|
|
1362
|
+
ttlMs: record.ttlMs,
|
|
1363
|
+
expiresAt: record.lastUsedAt + record.ttlMs
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
async function releasePage(_driver, args) {
|
|
1367
|
+
if (typeof args.lease !== "string" || !args.lease.length) {
|
|
1368
|
+
throw new LeaseToolError("release_page requires the 'lease' token returned by claim_page");
|
|
1369
|
+
}
|
|
1370
|
+
return releaseLease(args.lease);
|
|
1371
|
+
}
|
|
1372
|
+
async function listLeasesTool(driver, _args = {}) {
|
|
1373
|
+
const liveIds = (await driver.listPages()).map((p) => p.id);
|
|
1374
|
+
const leases = await listLeases({ liveIds, liveBackend: backendOf(driver) });
|
|
1375
|
+
return { leases, count: leases.length };
|
|
1376
|
+
}
|
|
1377
|
+
var LEASE_TOOLS = {
|
|
1378
|
+
claim_page: claimPage,
|
|
1379
|
+
release_page: releasePage,
|
|
1380
|
+
list_leases: listLeasesTool
|
|
1381
|
+
};
|
|
1382
|
+
|
|
1383
|
+
// src/shared-tools.ts
|
|
1384
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1385
|
+
import { join as join2 } from "node:path";
|
|
1386
|
+
var ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1387
|
+
var STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
1388
|
+
|
|
1389
|
+
class SharedToolError extends Error {
|
|
1390
|
+
}
|
|
1391
|
+
function target3(p) {
|
|
1392
|
+
return { id: p.id, url: p.url, title: p.title };
|
|
1393
|
+
}
|
|
1394
|
+
function backendOf2(driver) {
|
|
1395
|
+
return driver.scheme === "bidi" ? "firefox" : "chrome";
|
|
1396
|
+
}
|
|
1397
|
+
async function withPage2(driver, target, fn) {
|
|
1398
|
+
const page = await driver.page(target);
|
|
1399
|
+
try {
|
|
1400
|
+
return await fn(page);
|
|
1401
|
+
} finally {
|
|
1402
|
+
await page.release();
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
async function resolvePage(driver, selector) {
|
|
1406
|
+
const pages = await driver.listPages();
|
|
1407
|
+
const hit = await pickPage(driver, pages, selector);
|
|
1408
|
+
await assertLeaseOk(backendOf2(driver), hit.id, { url: hit.url, title: hit.title });
|
|
1409
|
+
return hit;
|
|
1410
|
+
}
|
|
1411
|
+
async function pickPage(driver, pages, selector) {
|
|
1412
|
+
if (selector === undefined || selector === "active") {
|
|
1413
|
+
if (!pages.length)
|
|
1414
|
+
throw new SharedToolError("no page targets open");
|
|
1415
|
+
return pages[0];
|
|
1416
|
+
}
|
|
1417
|
+
if (selector.startsWith("index:")) {
|
|
1418
|
+
const p = pages[Number(selector.slice(6))];
|
|
1419
|
+
if (!p)
|
|
1420
|
+
throw new SharedToolError(`no page target at index ${selector.slice(6)} (have ${pages.length})`);
|
|
1421
|
+
return p;
|
|
1422
|
+
}
|
|
1423
|
+
if (selector.startsWith("url:")) {
|
|
1424
|
+
const needle = selector.slice(4);
|
|
1425
|
+
const p = pages.find((x) => x.url.includes(needle));
|
|
1426
|
+
if (!p)
|
|
1427
|
+
throw new SharedToolError(`no page url containing '${needle}'`);
|
|
1428
|
+
return p;
|
|
1429
|
+
}
|
|
1430
|
+
if (selector.startsWith("title:")) {
|
|
1431
|
+
const needle = selector.slice(6);
|
|
1432
|
+
const p = pages.find((x) => x.title.includes(needle));
|
|
1433
|
+
if (!p)
|
|
1434
|
+
throw new SharedToolError(`no page title containing '${needle}'`);
|
|
1435
|
+
return p;
|
|
1436
|
+
}
|
|
1437
|
+
const exactPage = pages.find((x) => x.id === selector);
|
|
1438
|
+
if (exactPage)
|
|
1439
|
+
return exactPage;
|
|
1440
|
+
const all = await driver.listPages({ all: true });
|
|
1441
|
+
const exact = all.find((x) => x.id === selector);
|
|
1442
|
+
if (exact)
|
|
1443
|
+
return exact;
|
|
1444
|
+
throw new SharedToolError(`no target with id '${selector}'`);
|
|
1445
|
+
}
|
|
1446
|
+
function locatorOf(args) {
|
|
1447
|
+
const hasUid = args.uid !== undefined && args.uid !== null && args.uid !== "";
|
|
1448
|
+
const hasSelector = typeof args.selector === "string" && args.selector.length > 0;
|
|
1449
|
+
if (hasUid === hasSelector)
|
|
1450
|
+
throw new SharedToolError("provide exactly one of { uid } or { selector }");
|
|
1451
|
+
return hasUid ? { uid: args.uid } : { css: args.selector };
|
|
1452
|
+
}
|
|
1453
|
+
async function listPages(driver, args = {}) {
|
|
1454
|
+
const pages = await driver.listPages({ all: args.all });
|
|
1455
|
+
return { pages, count: pages.length };
|
|
1456
|
+
}
|
|
1457
|
+
async function newPage(driver, args = {}) {
|
|
1458
|
+
const p = await driver.newPage(args.url);
|
|
1459
|
+
if (args.claim !== true)
|
|
1460
|
+
return { targetId: p.id, url: p.url };
|
|
1461
|
+
const { record, token } = await claimLease(backendOf2(driver), p.id, {
|
|
1462
|
+
label: typeof args.label === "string" && args.label.length ? args.label : `pid-${process.pid}`,
|
|
1463
|
+
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs()
|
|
1464
|
+
});
|
|
1465
|
+
return { targetId: p.id, url: p.url, lease: token, label: record.label, expiresAt: record.lastUsedAt + record.ttlMs };
|
|
1466
|
+
}
|
|
1467
|
+
async function closePage(driver, args) {
|
|
1468
|
+
if (args.target === undefined || args.target === "")
|
|
1469
|
+
throw new SharedToolError("close_page requires an explicit target; refusing to guess which page to close");
|
|
1470
|
+
const p = await resolvePage(driver, args.target);
|
|
1471
|
+
const res = await driver.closePage(p.id);
|
|
1472
|
+
if (!res.success)
|
|
1473
|
+
return { closed: p.id, success: false, leaseReleased: false };
|
|
1474
|
+
const released = await releaseLeaseFor(backendOf2(driver), p.id);
|
|
1475
|
+
return { closed: p.id, success: true, ...released.released ? { leaseReleased: true } : {} };
|
|
1476
|
+
}
|
|
1477
|
+
async function writeSelectedFile(id) {
|
|
1478
|
+
await mkdir2(STATE_DIR, { recursive: true });
|
|
1479
|
+
await writeFile2(join2(STATE_DIR, "selected"), id, "utf8");
|
|
1480
|
+
}
|
|
1481
|
+
async function selectPage(driver, args) {
|
|
1482
|
+
if (args.target === undefined || args.target === "")
|
|
1483
|
+
throw new SharedToolError("select_page requires an explicit target");
|
|
1484
|
+
const p = await resolvePage(driver, args.target);
|
|
1485
|
+
const activated = await driver.activatePage(p.id);
|
|
1486
|
+
await writeSelectedFile(activated.id);
|
|
1487
|
+
return { selected: activated.id };
|
|
1488
|
+
}
|
|
1489
|
+
function toLegacyWaitedFor(w) {
|
|
1490
|
+
if (w === "committed")
|
|
1491
|
+
return "frameStoppedLoading";
|
|
1492
|
+
if (w === "timeout")
|
|
1493
|
+
return "navigate-only";
|
|
1494
|
+
return w;
|
|
1495
|
+
}
|
|
1496
|
+
async function navigatePage(driver, args) {
|
|
1497
|
+
const reload = args.reload === true;
|
|
1498
|
+
if (!reload && (!args.url || typeof args.url !== "string")) {
|
|
1499
|
+
throw new SharedToolError("navigate_page: 'url' is required (or pass reload:true to reload the current page)");
|
|
1500
|
+
}
|
|
1501
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1502
|
+
const r = await page.navigate(args);
|
|
1503
|
+
return {
|
|
1504
|
+
url: r.url,
|
|
1505
|
+
frameId: r.contextId,
|
|
1506
|
+
...r.reloaded ? { reloaded: r.reloaded } : {},
|
|
1507
|
+
waitedFor: toLegacyWaitedFor(r.waitedFor)
|
|
1508
|
+
};
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
async function waitForText(driver, args) {
|
|
1512
|
+
if (typeof args.text !== "string" || args.text.length === 0)
|
|
1513
|
+
throw new SharedToolError("wait_for: 'text' is required");
|
|
1514
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1515
|
+
const r = await page.waitForText(args.text, args.timeoutMs ?? 15000, args.pollMs ?? 250);
|
|
1516
|
+
return { found: true, waitedMs: r.elapsedMs };
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
async function evaluateScript(driver, args) {
|
|
1520
|
+
if (typeof args.expression !== "string" || args.expression.length === 0) {
|
|
1521
|
+
throw new SharedToolError("evaluateScript: 'expression' must be a non-empty string");
|
|
1522
|
+
}
|
|
1523
|
+
return withPage2(driver, args.target, (page) => page.evaluate(args.expression, { args: args.args, awaitPromise: args.awaitPromise ?? true }));
|
|
1524
|
+
}
|
|
1525
|
+
var BARE_TOKEN_EXTRAS = new Set(["disabled", "focused"]);
|
|
1526
|
+
var UNQUOTED_EXTRAS = new Set(["checked", "expanded", "url"]);
|
|
1527
|
+
function renderSnapshotLine(n, scheme) {
|
|
1528
|
+
const indent = " ".repeat(n.depth);
|
|
1529
|
+
const uidText = scheme === "cdp" && n.uid.startsWith("cdp:") ? n.uid.slice(4) : n.uid;
|
|
1530
|
+
const name = n.name !== undefined ? ` ${JSON.stringify(n.name)}` : "";
|
|
1531
|
+
const extrasToks = [];
|
|
1532
|
+
if (n.extras) {
|
|
1533
|
+
for (const [k, v] of Object.entries(n.extras)) {
|
|
1534
|
+
if (BARE_TOKEN_EXTRAS.has(k))
|
|
1535
|
+
extrasToks.push(k);
|
|
1536
|
+
else if (UNQUOTED_EXTRAS.has(k))
|
|
1537
|
+
extrasToks.push(`${k}=${v}`);
|
|
1538
|
+
else
|
|
1539
|
+
extrasToks.push(`${k}=${JSON.stringify(v)}`);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
const extra = extrasToks.length ? ` [${extrasToks.join(" ")}]` : "";
|
|
1543
|
+
return `${indent}[${uidText}] ${n.role}${name}${extra}`;
|
|
1544
|
+
}
|
|
1545
|
+
async function takeSnapshot(driver, args = {}) {
|
|
1546
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1547
|
+
const nodes = await page.snapshot({ interactiveOnly: args.interactiveOnly ?? false });
|
|
1548
|
+
const scheme = page.browser.scheme;
|
|
1549
|
+
return { snapshot: nodes.map((n) => renderSnapshotLine(n, scheme)).join(`
|
|
1550
|
+
`), target: target3(page.info), nodeCount: nodes.length };
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
async function click(driver, args) {
|
|
1554
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1555
|
+
const { x, y } = await page.click(locatorOf(args), { button: args.button ?? "left", clickCount: args.clickCount ?? 1 });
|
|
1556
|
+
return { clicked: true, x, y };
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
async function hover(driver, args) {
|
|
1560
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1561
|
+
const { x, y } = await page.hover(locatorOf(args));
|
|
1562
|
+
return { hovered: true, x, y };
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
async function drag(driver, args) {
|
|
1566
|
+
if (!args.from || !args.to)
|
|
1567
|
+
throw new SharedToolError("drag requires { from } and { to }");
|
|
1568
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1569
|
+
const { from, to } = await page.drag(locatorOf(args.from), locatorOf(args.to));
|
|
1570
|
+
return { dragged: true, from, to };
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
async function fill(driver, args) {
|
|
1574
|
+
if (typeof args.value !== "string")
|
|
1575
|
+
throw new SharedToolError("fill requires a string { value }");
|
|
1576
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1577
|
+
await page.setValue(locatorOf(args), args.value);
|
|
1578
|
+
return { filled: true, value: args.value };
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
async function fillForm(driver, args) {
|
|
1582
|
+
if (!Array.isArray(args.fields) || args.fields.length === 0)
|
|
1583
|
+
throw new SharedToolError("fill_form requires a non-empty { fields } array");
|
|
1584
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1585
|
+
let filled = 0;
|
|
1586
|
+
for (const f of args.fields) {
|
|
1587
|
+
if (typeof f.value !== "string")
|
|
1588
|
+
throw new SharedToolError("each fill_form field requires a string value");
|
|
1589
|
+
await page.setValue(locatorOf(f), f.value);
|
|
1590
|
+
filled++;
|
|
1591
|
+
}
|
|
1592
|
+
return { filled, fields: args.fields.length };
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
async function typeText(driver, args) {
|
|
1596
|
+
if (typeof args.text !== "string")
|
|
1597
|
+
throw new SharedToolError("type_text requires a string { text }");
|
|
1598
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1599
|
+
await page.typeText(locatorOf(args), args.text);
|
|
1600
|
+
return { typed: true, text: args.text };
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
async function pressKey(driver, args) {
|
|
1604
|
+
if (typeof args.key !== "string" || !args.key.length)
|
|
1605
|
+
throw new SharedToolError("press_key requires a non-empty { key }");
|
|
1606
|
+
const modifiers = args.modifiers ?? [];
|
|
1607
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1608
|
+
await page.pressKey({ key: args.key, modifiers });
|
|
1609
|
+
return { pressed: args.key, modifiers };
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
async function uploadFile(driver, args) {
|
|
1613
|
+
const files = Array.isArray(args.files) ? args.files : [args.files];
|
|
1614
|
+
if (!files.length || files.some((f) => typeof f !== "string" || !f.length)) {
|
|
1615
|
+
throw new SharedToolError("upload_file requires { files } as a non-empty path or array of paths");
|
|
1616
|
+
}
|
|
1617
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1618
|
+
await page.setFiles(locatorOf(args), files);
|
|
1619
|
+
return { uploaded: files };
|
|
926
1620
|
});
|
|
927
1621
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
await
|
|
941
|
-
|
|
942
|
-
});
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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
|
|
1622
|
+
function stamp() {
|
|
1623
|
+
return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
|
|
1624
|
+
}
|
|
1625
|
+
async function takeScreenshot(driver, args = {}) {
|
|
1626
|
+
if (args.uid !== undefined && args.uid !== "" && args.selector)
|
|
1627
|
+
throw new SharedToolError("provide exactly one of uid or selector, not both");
|
|
1628
|
+
const format = args.format ?? "png";
|
|
1629
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1630
|
+
const clip = args.uid !== undefined || args.selector ? locatorOf(args) : undefined;
|
|
1631
|
+
const { data, format: outFormat } = await page.screenshot({ format, quality: args.quality, fullPage: args.fullPage, ...clip ? { clip } : {} });
|
|
1632
|
+
if (data.byteLength === 0)
|
|
1633
|
+
throw new SharedToolError("captureScreenshot returned empty data");
|
|
1634
|
+
await mkdir2(ARTIFACT_DIR, { recursive: true });
|
|
1635
|
+
const ext = outFormat === "jpeg" ? "jpg" : "png";
|
|
1636
|
+
const path = args.savePath ?? join2(ARTIFACT_DIR, `screenshot-${page.info.id.slice(0, 8)}-${stamp()}.${ext}`);
|
|
1637
|
+
await writeFile2(path, Buffer.from(data));
|
|
1638
|
+
const result = {
|
|
1639
|
+
path,
|
|
1640
|
+
bytes: data.byteLength,
|
|
1641
|
+
format: outFormat,
|
|
1642
|
+
target: target3(page.info)
|
|
954
1643
|
};
|
|
1644
|
+
if (args.returnBase64)
|
|
1645
|
+
result.base64 = Buffer.from(data).toString("base64");
|
|
1646
|
+
return result;
|
|
955
1647
|
});
|
|
956
1648
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
-
})();
|
|
1649
|
+
async function emulate(driver, args = {}) {
|
|
1650
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1651
|
+
if (args.clearOverrides) {
|
|
1652
|
+
await page.emulate({ clearOverrides: true });
|
|
1653
|
+
return { target: target3(page.info), applied: [], cleared: true };
|
|
1654
|
+
}
|
|
1655
|
+
const { applied } = await page.emulate(args);
|
|
1656
|
+
if (applied.length === 0)
|
|
1657
|
+
throw new SharedToolError("emulate: no overrides specified (pass clearOverrides:true to reset)");
|
|
1658
|
+
return { target: target3(page.info), applied };
|
|
979
1659
|
});
|
|
980
1660
|
}
|
|
981
|
-
async function
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
try {
|
|
985
|
-
await conn.send("Page.enable");
|
|
986
|
-
if (autoMs !== undefined) {
|
|
987
|
-
const handled = [];
|
|
988
|
-
const disarm = armDialogHandler(conn, accept, promptText, (d) => handled.push(d));
|
|
989
|
-
await new Promise((resolve) => setTimeout(resolve, autoMs));
|
|
990
|
-
disarm();
|
|
991
|
-
return { handled, count: handled.length };
|
|
992
|
-
}
|
|
993
|
-
return await new Promise((resolve, reject) => {
|
|
994
|
-
const timer = setTimeout(() => {
|
|
995
|
-
disarm();
|
|
996
|
-
reject(new CdpError(`handle_dialog: no dialog opened within ${timeoutMs}ms`));
|
|
997
|
-
}, timeoutMs);
|
|
998
|
-
const disarm = armDialogHandler(conn, accept, promptText, (d) => {
|
|
999
|
-
clearTimeout(timer);
|
|
1000
|
-
disarm();
|
|
1001
|
-
resolve(d);
|
|
1002
|
-
});
|
|
1003
|
-
});
|
|
1004
|
-
} finally {
|
|
1005
|
-
conn.close();
|
|
1661
|
+
async function resizePage(driver, args) {
|
|
1662
|
+
if (!Number.isFinite(args.width) || !Number.isFinite(args.height) || args.width <= 0 || args.height <= 0) {
|
|
1663
|
+
throw new SharedToolError("resize_page requires positive numeric width and height");
|
|
1006
1664
|
}
|
|
1665
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1666
|
+
await page.emulate({ width: args.width, height: args.height, deviceScaleFactor: args.deviceScaleFactor, mobile: args.mobile });
|
|
1667
|
+
const v = await page.evaluate("({ w: window.innerWidth, h: window.innerHeight })");
|
|
1668
|
+
return { target: target3(page.info), width: args.width, height: args.height, innerWidth: v?.w ?? args.width, innerHeight: v?.h ?? args.height };
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
async function handleDialog(driver, args) {
|
|
1672
|
+
if (args.accept === undefined)
|
|
1673
|
+
throw new SharedToolError("handle_dialog requires { accept }");
|
|
1674
|
+
return withPage2(driver, args.target, (page) => page.handleDialog(args.accept, args.promptText, { timeoutMs: args.timeoutMs, autoMs: args.autoMs }));
|
|
1007
1675
|
}
|
|
1676
|
+
var SHARED_TOOLS = {
|
|
1677
|
+
list_pages: listPages,
|
|
1678
|
+
new_page: newPage,
|
|
1679
|
+
close_page: closePage,
|
|
1680
|
+
select_page: selectPage,
|
|
1681
|
+
navigate_page: navigatePage,
|
|
1682
|
+
wait_for: waitForText,
|
|
1683
|
+
evaluate_script: evaluateScript,
|
|
1684
|
+
take_snapshot: takeSnapshot,
|
|
1685
|
+
click,
|
|
1686
|
+
hover,
|
|
1687
|
+
drag,
|
|
1688
|
+
fill,
|
|
1689
|
+
fill_form: fillForm,
|
|
1690
|
+
type_text: typeText,
|
|
1691
|
+
press_key: pressKey,
|
|
1692
|
+
upload_file: uploadFile,
|
|
1693
|
+
take_screenshot: takeScreenshot,
|
|
1694
|
+
emulate,
|
|
1695
|
+
resize_page: resizePage,
|
|
1696
|
+
handle_dialog: handleDialog
|
|
1697
|
+
};
|
|
1008
1698
|
|
|
1009
1699
|
// src/tools/console.ts
|
|
1010
|
-
import { readFile } from "node:fs/promises";
|
|
1700
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
1011
1701
|
|
|
1012
1702
|
// src/tools/recorder.ts
|
|
1013
1703
|
import { mkdir as mkdir3, appendFile, writeFile as writeFile3, copyFile } from "node:fs/promises";
|
|
@@ -1177,7 +1867,7 @@ function lineToEntry(line, index) {
|
|
|
1177
1867
|
async function readConsoleEntries(file) {
|
|
1178
1868
|
let raw;
|
|
1179
1869
|
try {
|
|
1180
|
-
raw = await
|
|
1870
|
+
raw = await readFile2(file, "utf8");
|
|
1181
1871
|
} catch {
|
|
1182
1872
|
return [];
|
|
1183
1873
|
}
|
|
@@ -1233,7 +1923,7 @@ async function getConsoleMessage(args = {}) {
|
|
|
1233
1923
|
}
|
|
1234
1924
|
|
|
1235
1925
|
// src/tools/network.ts
|
|
1236
|
-
import { readFile as
|
|
1926
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
1237
1927
|
var DEFAULT_CAPTURE_MS2 = 2500;
|
|
1238
1928
|
function asHeaders(v) {
|
|
1239
1929
|
if (!v || typeof v !== "object")
|
|
@@ -1300,7 +1990,7 @@ function correlate(lines) {
|
|
|
1300
1990
|
async function readNetworkRequests(file) {
|
|
1301
1991
|
let raw;
|
|
1302
1992
|
try {
|
|
1303
|
-
raw = await
|
|
1993
|
+
raw = await readFile3(file, "utf8");
|
|
1304
1994
|
} catch {
|
|
1305
1995
|
return [];
|
|
1306
1996
|
}
|
|
@@ -1382,8 +2072,8 @@ async function getNetworkRequest(args = {}) {
|
|
|
1382
2072
|
}
|
|
1383
2073
|
|
|
1384
2074
|
// src/tools/performance.ts
|
|
1385
|
-
import { mkdir as mkdir4, readFile as
|
|
1386
|
-
import { join as
|
|
2075
|
+
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4, stat, unlink as unlink2 } from "node:fs/promises";
|
|
2076
|
+
import { join as join3 } from "node:path";
|
|
1387
2077
|
var ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1388
2078
|
var STATE_DIR2 = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
1389
2079
|
var DEFAULT_CATEGORIES = [
|
|
@@ -1397,11 +2087,11 @@ var liveTraces = new Map;
|
|
|
1397
2087
|
function stamp2() {
|
|
1398
2088
|
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
1399
2089
|
}
|
|
1400
|
-
function
|
|
2090
|
+
function shortId(id) {
|
|
1401
2091
|
return id.slice(0, 8);
|
|
1402
2092
|
}
|
|
1403
2093
|
function stateFilePath() {
|
|
1404
|
-
return
|
|
2094
|
+
return join3(STATE_DIR2, "trace-state.json");
|
|
1405
2095
|
}
|
|
1406
2096
|
function bufferTraceData(conn, sink) {
|
|
1407
2097
|
return conn.on("Tracing.dataCollected", (params) => {
|
|
@@ -1424,7 +2114,7 @@ async function endTrace(conn, sink, timeoutMs = 30000) {
|
|
|
1424
2114
|
}
|
|
1425
2115
|
async function writeTrace(events, targetId) {
|
|
1426
2116
|
await mkdir4(ARTIFACT_DIR3, { recursive: true });
|
|
1427
|
-
const path =
|
|
2117
|
+
const path = join3(ARTIFACT_DIR3, `trace-${shortId(targetId)}-${stamp2()}.json`);
|
|
1428
2118
|
const body = JSON.stringify({ traceEvents: events, metadata: { source: "cdp-toolkit" } });
|
|
1429
2119
|
await writeFile4(path, body, "utf8");
|
|
1430
2120
|
const { size } = await stat(path);
|
|
@@ -1435,7 +2125,7 @@ async function performanceStartTrace(args = {}) {
|
|
|
1435
2125
|
const { conn, target } = await openPage(args.target);
|
|
1436
2126
|
if (liveTraces.has(target.id)) {
|
|
1437
2127
|
conn.close();
|
|
1438
|
-
throw new CdpError(`a trace is already in progress for target ${
|
|
2128
|
+
throw new CdpError(`a trace is already in progress for target ${shortId(target.id)}; call performanceStopTrace first`);
|
|
1439
2129
|
}
|
|
1440
2130
|
const events = [];
|
|
1441
2131
|
bufferTraceData(conn, events);
|
|
@@ -1467,9 +2157,9 @@ async function performanceStopTrace(args = {}) {
|
|
|
1467
2157
|
if (!entry) {
|
|
1468
2158
|
let hint = "";
|
|
1469
2159
|
try {
|
|
1470
|
-
const raw = await
|
|
2160
|
+
const raw = await readFile4(stateFilePath(), "utf8");
|
|
1471
2161
|
const state = JSON.parse(raw);
|
|
1472
|
-
hint = state.pid === process.pid ? ` (state file lists target ${
|
|
2162
|
+
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
2163
|
} catch {}
|
|
1474
2164
|
throw new CdpError(`no in-process trace to stop${hint}. Use performanceTrace for a robust single-call trace.`);
|
|
1475
2165
|
}
|
|
@@ -1479,7 +2169,7 @@ async function performanceStopTrace(args = {}) {
|
|
|
1479
2169
|
} finally {
|
|
1480
2170
|
conn.close();
|
|
1481
2171
|
liveTraces.delete(target.id);
|
|
1482
|
-
await
|
|
2172
|
+
await unlink2(stateFilePath()).catch(() => {});
|
|
1483
2173
|
}
|
|
1484
2174
|
const written = await writeTrace(events, target.id);
|
|
1485
2175
|
const metrics = analyzeEvents(events);
|
|
@@ -1597,7 +2287,7 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
1597
2287
|
}
|
|
1598
2288
|
let raw;
|
|
1599
2289
|
try {
|
|
1600
|
-
raw = await
|
|
2290
|
+
raw = await readFile4(args.tracePath, "utf8");
|
|
1601
2291
|
} catch (err) {
|
|
1602
2292
|
throw new CdpError(`cannot read trace file '${args.tracePath}': ${err.message}`);
|
|
1603
2293
|
}
|
|
@@ -1615,7 +2305,7 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
1615
2305
|
|
|
1616
2306
|
// src/tools/heap.ts
|
|
1617
2307
|
import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
|
|
1618
|
-
import { join as
|
|
2308
|
+
import { join as join4 } from "node:path";
|
|
1619
2309
|
var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1620
2310
|
function stamp3(targetId) {
|
|
1621
2311
|
const short = targetId.slice(0, 8);
|
|
@@ -1641,7 +2331,7 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
1641
2331
|
throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
|
|
1642
2332
|
}
|
|
1643
2333
|
await mkdir5(ARTIFACT_DIR4, { recursive: true });
|
|
1644
|
-
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath :
|
|
2334
|
+
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join4(ARTIFACT_DIR4, args.savePath) : join4(ARTIFACT_DIR4, `take_heapsnapshot-${stamp3(target.id)}.heapsnapshot`);
|
|
1645
2335
|
await writeFile5(path, snapshot, "utf8");
|
|
1646
2336
|
const bytes = Buffer.byteLength(snapshot, "utf8");
|
|
1647
2337
|
return {
|
|
@@ -1655,7 +2345,7 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
1655
2345
|
|
|
1656
2346
|
// src/tools/lighthouse.ts
|
|
1657
2347
|
import { spawn } from "node:child_process";
|
|
1658
|
-
import { mkdir as mkdir6, readFile as
|
|
2348
|
+
import { mkdir as mkdir6, readFile as readFile5, stat as stat2 } from "node:fs/promises";
|
|
1659
2349
|
var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1660
2350
|
function cdpPort() {
|
|
1661
2351
|
const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
@@ -1745,7 +2435,7 @@ async function lighthouseAudit(args) {
|
|
|
1745
2435
|
}
|
|
1746
2436
|
let raw;
|
|
1747
2437
|
try {
|
|
1748
|
-
raw = await
|
|
2438
|
+
raw = await readFile5(outPath, "utf8");
|
|
1749
2439
|
} catch {
|
|
1750
2440
|
raw = undefined;
|
|
1751
2441
|
}
|
|
@@ -1753,7 +2443,7 @@ async function lighthouseAudit(args) {
|
|
|
1753
2443
|
if (res.signal === "SIGKILL") {
|
|
1754
2444
|
throw new CdpError(`lighthouse_audit timed out after ${timeoutMs}ms (no report written). stderr: ${truncate(res.stderr)}`);
|
|
1755
2445
|
}
|
|
1756
|
-
const hint = /not found|could not determine executable|npm ERR|E404|ERR_MODULE/i.test(res.stderr) ? " The Lighthouse CLI could not be fetched/installed via npx
|
|
2446
|
+
const hint = /not found|could not determine executable|npm ERR|E404|ERR_MODULE/i.test(res.stderr) ? " The Lighthouse CLI could not be fetched/installed via npx; ensure network access and that 'lighthouse' is installable." : "";
|
|
1757
2447
|
throw new CdpError(`lighthouse_audit produced no report (exit code ${res.code}).${hint} stderr: ${truncate(res.stderr)}`);
|
|
1758
2448
|
}
|
|
1759
2449
|
let report;
|
|
@@ -1789,205 +2479,32 @@ function truncate(s, max = 600) {
|
|
|
1789
2479
|
return t.length > max ? `${t.slice(0, max)}…` : t;
|
|
1790
2480
|
}
|
|
1791
2481
|
|
|
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
2482
|
// src/index.ts
|
|
2483
|
+
var cdpDriver = createCdpDriver();
|
|
2484
|
+
function onCdp(fn) {
|
|
2485
|
+
return (args) => fn(cdpDriver, args);
|
|
2486
|
+
}
|
|
1970
2487
|
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:
|
|
2488
|
+
list_pages: onCdp(SHARED_TOOLS.list_pages),
|
|
2489
|
+
new_page: onCdp(SHARED_TOOLS.new_page),
|
|
2490
|
+
close_page: onCdp(SHARED_TOOLS.close_page),
|
|
2491
|
+
select_page: onCdp(SHARED_TOOLS.select_page),
|
|
2492
|
+
navigate_page: onCdp(SHARED_TOOLS.navigate_page),
|
|
2493
|
+
wait_for: onCdp(SHARED_TOOLS.wait_for),
|
|
2494
|
+
evaluate_script: onCdp(SHARED_TOOLS.evaluate_script),
|
|
2495
|
+
take_snapshot: onCdp(SHARED_TOOLS.take_snapshot),
|
|
2496
|
+
click: onCdp(SHARED_TOOLS.click),
|
|
2497
|
+
hover: onCdp(SHARED_TOOLS.hover),
|
|
2498
|
+
drag: onCdp(SHARED_TOOLS.drag),
|
|
2499
|
+
fill: onCdp(SHARED_TOOLS.fill),
|
|
2500
|
+
fill_form: onCdp(SHARED_TOOLS.fill_form),
|
|
2501
|
+
type_text: onCdp(SHARED_TOOLS.type_text),
|
|
2502
|
+
press_key: onCdp(SHARED_TOOLS.press_key),
|
|
2503
|
+
upload_file: onCdp(SHARED_TOOLS.upload_file),
|
|
2504
|
+
take_screenshot: onCdp(SHARED_TOOLS.take_screenshot),
|
|
2505
|
+
emulate: onCdp(SHARED_TOOLS.emulate),
|
|
2506
|
+
resize_page: onCdp(SHARED_TOOLS.resize_page),
|
|
2507
|
+
handle_dialog: onCdp(SHARED_TOOLS.handle_dialog),
|
|
1991
2508
|
list_console_messages: listConsoleMessages,
|
|
1992
2509
|
get_console_message: getConsoleMessage,
|
|
1993
2510
|
list_network_requests: listNetworkRequests,
|
|
@@ -2000,7 +2517,10 @@ var TOOLS = {
|
|
|
2000
2517
|
lighthouse_audit: lighthouseAudit,
|
|
2001
2518
|
mock_request: mockRequest,
|
|
2002
2519
|
list_mocks: listMocks,
|
|
2003
|
-
clear_mocks: clearMocks
|
|
2520
|
+
clear_mocks: clearMocks,
|
|
2521
|
+
claim_page: onCdp(LEASE_TOOLS.claim_page),
|
|
2522
|
+
release_page: onCdp(LEASE_TOOLS.release_page),
|
|
2523
|
+
list_leases: onCdp(LEASE_TOOLS.list_leases)
|
|
2004
2524
|
};
|
|
2005
2525
|
var TOOL_NAMES = Object.keys(TOOLS);
|
|
2006
2526
|
export {
|