@xbrowser/cli 0.16.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{browser-R7B255ML.js → browser-53KUFEEM.js} +5 -1
- package/dist/{browser-I2HJZ7IP.js → browser-DSVV4GHS.js} +2 -2
- package/dist/{browser-GWBH6OJK.js → browser-GURRY444.js} +3 -1
- package/dist/cdp-driver-MNPR3HZH.js +2537 -0
- package/dist/cdp-driver-SSXUGXP6.js +47 -0
- package/dist/{chunk-2ONMTDLK.js → chunk-2BQZIT3S.js} +2535 -50
- package/dist/{chunk-KDYXFLAC.js → chunk-2MFXKN32.js} +2 -2
- package/dist/chunk-42RPMJ76.js +2530 -0
- package/dist/{chunk-F3ZWFCJJ.js → chunk-E4O5ZU3H.js} +2535 -50
- package/dist/{chunk-ATFTAKMN.js → chunk-IDVD44ED.js} +20 -0
- package/dist/chunk-T4J4C2NZ.js +250 -0
- package/dist/{chunk-RS6YYWTK.js → chunk-YKOHDEFV.js} +73 -38
- package/dist/cli.js +1054 -140
- package/dist/{convert-4DUWZIKH.js → convert-EGFYNICZ.js} +2 -0
- package/dist/{daemon-client-GX2UYIW4.js → daemon-client-3VM7VU7O.js} +22 -0
- package/dist/{daemon-client-3IJD6X4B.js → daemon-client-YAVQ343A.js} +7 -1
- package/dist/daemon-main.js +984 -114
- package/dist/{extract-EGRXZSSK.js → extract-L2IW3IUB.js} +2 -0
- package/dist/{filter-OLAE26HN.js → filter-HC4RA7JY.js} +2 -0
- package/dist/index.d.ts +581 -41
- package/dist/index.js +1100 -182
- package/dist/launcher-KA7J32K5.js +19 -0
- package/dist/{network-store-YAF5OIBH.js → network-store-66A2RATI.js} +1 -0
- package/dist/{session-recorder-XET3DNML.js → session-recorder-MA75PKTQ.js} +1 -1
- package/package.json +2 -3
- package/dist/daemon-client-XWSSQBEA.js +0 -58
- package/dist/network-store-2S5HATEV.js +0 -194
- package/dist/parse-action-dsl-DRSPBALP.js +0 -72
- package/dist/screenshot-MB6R7RSS.js +0 -26
- package/dist/session-recorder-ILSSV2UC.js +0 -6
|
@@ -1,12 +1,2464 @@
|
|
|
1
|
+
import {
|
|
2
|
+
connectToCDP,
|
|
3
|
+
launchChrome
|
|
4
|
+
} from "./chunk-T4J4C2NZ.js";
|
|
5
|
+
import {
|
|
6
|
+
__require
|
|
7
|
+
} from "./chunk-3RG5ZIWI.js";
|
|
8
|
+
|
|
1
9
|
// src/browser.ts
|
|
2
10
|
import { randomUUID } from "crypto";
|
|
3
11
|
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "fs";
|
|
4
12
|
import { join } from "path";
|
|
5
13
|
import { homedir } from "os";
|
|
6
|
-
|
|
14
|
+
|
|
15
|
+
// src/cdp-driver/browser.ts
|
|
16
|
+
import { EventEmitter as EventEmitter3 } from "events";
|
|
17
|
+
|
|
18
|
+
// src/cdp-driver/context.ts
|
|
19
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
20
|
+
|
|
21
|
+
// src/cdp-driver/page.ts
|
|
22
|
+
import { EventEmitter } from "events";
|
|
23
|
+
|
|
24
|
+
// src/cdp-driver/mouse.ts
|
|
25
|
+
var XBMouseImpl = class {
|
|
26
|
+
conn;
|
|
27
|
+
sessionId;
|
|
28
|
+
_x = 0;
|
|
29
|
+
_y = 0;
|
|
30
|
+
_button = "none";
|
|
31
|
+
constructor(conn, sessionId) {
|
|
32
|
+
this.conn = conn;
|
|
33
|
+
this.sessionId = sessionId;
|
|
34
|
+
}
|
|
35
|
+
/** Current cursor X position */
|
|
36
|
+
get x() {
|
|
37
|
+
return this._x;
|
|
38
|
+
}
|
|
39
|
+
/** Current cursor Y position */
|
|
40
|
+
get y() {
|
|
41
|
+
return this._y;
|
|
42
|
+
}
|
|
43
|
+
async click(x, y, opts = {}) {
|
|
44
|
+
const button = opts.button ?? "left";
|
|
45
|
+
const clickCount = opts.clickCount ?? 1;
|
|
46
|
+
const delay = opts.delay ?? 0;
|
|
47
|
+
await this.move(x, y);
|
|
48
|
+
await this.down({ button });
|
|
49
|
+
if (delay > 0) {
|
|
50
|
+
await sleep(delay);
|
|
51
|
+
}
|
|
52
|
+
await this.up({ button });
|
|
53
|
+
for (let i = 1; i < clickCount; i++) {
|
|
54
|
+
if (delay > 0) await sleep(delay);
|
|
55
|
+
await this.down({ button });
|
|
56
|
+
if (delay > 0) await sleep(delay);
|
|
57
|
+
await this.up({ button });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async dblclick(x, y, opts = {}) {
|
|
61
|
+
await this.click(x, y, { clickCount: 2, button: opts.button });
|
|
62
|
+
}
|
|
63
|
+
async down(opts = {}) {
|
|
64
|
+
const button = opts.button ?? "left";
|
|
65
|
+
this._button = button;
|
|
66
|
+
await this.send("Input.dispatchMouseEvent", {
|
|
67
|
+
type: "mousePressed",
|
|
68
|
+
x: this._x,
|
|
69
|
+
y: this._y,
|
|
70
|
+
button,
|
|
71
|
+
clickCount: 1
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
async up(opts = {}) {
|
|
75
|
+
const button = opts.button ?? "left";
|
|
76
|
+
this._button = "none";
|
|
77
|
+
await this.send("Input.dispatchMouseEvent", {
|
|
78
|
+
type: "mouseReleased",
|
|
79
|
+
x: this._x,
|
|
80
|
+
y: this._y,
|
|
81
|
+
button,
|
|
82
|
+
clickCount: 1
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async move(x, y, opts = {}) {
|
|
86
|
+
const steps = Math.max(1, opts.steps ?? 1);
|
|
87
|
+
const fromX = this._x;
|
|
88
|
+
const fromY = this._y;
|
|
89
|
+
const dx = x - fromX;
|
|
90
|
+
const dy = y - fromY;
|
|
91
|
+
for (let i = 1; i <= steps; i++) {
|
|
92
|
+
const t = i / steps;
|
|
93
|
+
this._x = fromX + dx * t;
|
|
94
|
+
this._y = fromY + dy * t;
|
|
95
|
+
await this.send("Input.dispatchMouseEvent", {
|
|
96
|
+
type: "mouseMoved",
|
|
97
|
+
x: this._x,
|
|
98
|
+
y: this._y,
|
|
99
|
+
button: this._button
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
this._x = x;
|
|
103
|
+
this._y = y;
|
|
104
|
+
}
|
|
105
|
+
async wheel(deltaX, deltaY) {
|
|
106
|
+
await this.send("Input.dispatchMouseEvent", {
|
|
107
|
+
type: "mouseWheel",
|
|
108
|
+
x: this._x,
|
|
109
|
+
y: this._y,
|
|
110
|
+
deltaX,
|
|
111
|
+
deltaY
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
async send(method, params) {
|
|
115
|
+
await this.conn.send(method, params, this.sessionId);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
function sleep(ms) {
|
|
119
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/cdp-driver/keyboard.ts
|
|
123
|
+
var XBKeyboardImpl = class {
|
|
124
|
+
conn;
|
|
125
|
+
sessionId;
|
|
126
|
+
constructor(conn, sessionId) {
|
|
127
|
+
this.conn = conn;
|
|
128
|
+
this.sessionId = sessionId;
|
|
129
|
+
}
|
|
130
|
+
async press(key, opts = {}) {
|
|
131
|
+
const delay = opts.delay ?? 0;
|
|
132
|
+
const mapping = resolveKeyMapping(key);
|
|
133
|
+
const downParams = {
|
|
134
|
+
type: "rawKeyDown",
|
|
135
|
+
key: mapping.key,
|
|
136
|
+
code: mapping.code
|
|
137
|
+
};
|
|
138
|
+
if (mapping.text) {
|
|
139
|
+
downParams.text = mapping.text;
|
|
140
|
+
downParams.unmodifiedText = mapping.text;
|
|
141
|
+
}
|
|
142
|
+
if (mapping.keyCode) {
|
|
143
|
+
downParams.windowsVirtualKeyCode = mapping.keyCode;
|
|
144
|
+
}
|
|
145
|
+
await this.dispatchKeyEvent(downParams);
|
|
146
|
+
if (mapping.text) {
|
|
147
|
+
await this.dispatchKeyEvent({
|
|
148
|
+
type: "char",
|
|
149
|
+
text: mapping.text
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (delay > 0) await sleep2(delay);
|
|
153
|
+
const upParams = {
|
|
154
|
+
type: "keyUp",
|
|
155
|
+
key: mapping.key,
|
|
156
|
+
code: mapping.code
|
|
157
|
+
};
|
|
158
|
+
if (mapping.keyCode) {
|
|
159
|
+
upParams.windowsVirtualKeyCode = mapping.keyCode;
|
|
160
|
+
}
|
|
161
|
+
await this.dispatchKeyEvent(upParams);
|
|
162
|
+
}
|
|
163
|
+
async down(key) {
|
|
164
|
+
const mapping = resolveKeyMapping(key);
|
|
165
|
+
const params = {
|
|
166
|
+
type: "rawKeyDown",
|
|
167
|
+
key: mapping.key,
|
|
168
|
+
code: mapping.code
|
|
169
|
+
};
|
|
170
|
+
if (mapping.text) {
|
|
171
|
+
params.text = mapping.text;
|
|
172
|
+
params.unmodifiedText = mapping.text;
|
|
173
|
+
}
|
|
174
|
+
if (mapping.keyCode) {
|
|
175
|
+
params.windowsVirtualKeyCode = mapping.keyCode;
|
|
176
|
+
}
|
|
177
|
+
await this.dispatchKeyEvent(params);
|
|
178
|
+
}
|
|
179
|
+
async up(key) {
|
|
180
|
+
const mapping = resolveKeyMapping(key);
|
|
181
|
+
const params = {
|
|
182
|
+
type: "keyUp",
|
|
183
|
+
key: mapping.key,
|
|
184
|
+
code: mapping.code
|
|
185
|
+
};
|
|
186
|
+
if (mapping.keyCode) {
|
|
187
|
+
params.windowsVirtualKeyCode = mapping.keyCode;
|
|
188
|
+
}
|
|
189
|
+
await this.dispatchKeyEvent(params);
|
|
190
|
+
}
|
|
191
|
+
async type(text, opts = {}) {
|
|
192
|
+
const delay = opts.delay ?? 0;
|
|
193
|
+
for (const char of text) {
|
|
194
|
+
if (delay > 0) await sleep2(delay);
|
|
195
|
+
const mapping = resolveKeyMapping(char);
|
|
196
|
+
const downParams = {
|
|
197
|
+
type: "rawKeyDown",
|
|
198
|
+
key: mapping.key,
|
|
199
|
+
code: mapping.code
|
|
200
|
+
};
|
|
201
|
+
if (mapping.text) {
|
|
202
|
+
downParams.text = mapping.text;
|
|
203
|
+
downParams.unmodifiedText = mapping.text;
|
|
204
|
+
}
|
|
205
|
+
if (mapping.keyCode) {
|
|
206
|
+
downParams.windowsVirtualKeyCode = mapping.keyCode;
|
|
207
|
+
}
|
|
208
|
+
await this.dispatchKeyEvent(downParams);
|
|
209
|
+
if (mapping.text) {
|
|
210
|
+
await this.dispatchKeyEvent({
|
|
211
|
+
type: "char",
|
|
212
|
+
text: mapping.text
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
await this.dispatchKeyEvent({
|
|
216
|
+
type: "keyUp",
|
|
217
|
+
key: mapping.key,
|
|
218
|
+
code: mapping.code,
|
|
219
|
+
...mapping.keyCode ? { windowsVirtualKeyCode: mapping.keyCode } : {}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async insertText(text) {
|
|
224
|
+
await this.conn.send(
|
|
225
|
+
"Input.insertText",
|
|
226
|
+
{ text },
|
|
227
|
+
this.sessionId
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
async dispatchKeyEvent(params) {
|
|
231
|
+
await this.conn.send("Input.dispatchKeyEvent", params, this.sessionId);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
function resolveKeyMapping(key) {
|
|
235
|
+
if (KEY_MAP[key]) return KEY_MAP[key];
|
|
236
|
+
if (key.length === 1) {
|
|
237
|
+
const lower = key.toLowerCase();
|
|
238
|
+
if (lower >= "a" && lower <= "z") {
|
|
239
|
+
const code = `Key${lower.toUpperCase()}`;
|
|
240
|
+
const keyCode = lower.charCodeAt(0) - 32;
|
|
241
|
+
return { key, code, text: key, keyCode };
|
|
242
|
+
}
|
|
243
|
+
if (key >= "0" && key <= "9") {
|
|
244
|
+
const code = `Digit${key}`;
|
|
245
|
+
const keyCode = key.charCodeAt(0);
|
|
246
|
+
return { key, code, text: key, keyCode };
|
|
247
|
+
}
|
|
248
|
+
return { key, code: key, text: key };
|
|
249
|
+
}
|
|
250
|
+
return { key, code: key };
|
|
251
|
+
}
|
|
252
|
+
var KEY_MAP = {
|
|
253
|
+
Enter: { key: "Enter", code: "Enter", text: "\r" },
|
|
254
|
+
Tab: { key: "Tab", code: "Tab", text: " " },
|
|
255
|
+
Escape: { key: "Escape", code: "Escape" },
|
|
256
|
+
Backspace: { key: "Backspace", code: "Backspace" },
|
|
257
|
+
Delete: { key: "Delete", code: "Delete" },
|
|
258
|
+
Space: { key: " ", code: "Space", text: " " },
|
|
259
|
+
ArrowUp: { key: "ArrowUp", code: "ArrowUp" },
|
|
260
|
+
ArrowDown: { key: "ArrowDown", code: "ArrowDown" },
|
|
261
|
+
ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft" },
|
|
262
|
+
ArrowRight: { key: "ArrowRight", code: "ArrowRight" },
|
|
263
|
+
Home: { key: "Home", code: "Home" },
|
|
264
|
+
End: { key: "End", code: "End" },
|
|
265
|
+
PageUp: { key: "PageUp", code: "PageUp" },
|
|
266
|
+
PageDown: { key: "PageDown", code: "PageDown" },
|
|
267
|
+
Control: { key: "Control", code: "ControlLeft" },
|
|
268
|
+
Shift: { key: "Shift", code: "ShiftLeft" },
|
|
269
|
+
Alt: { key: "Alt", code: "AltLeft" },
|
|
270
|
+
Meta: { key: "Meta", code: "MetaLeft" },
|
|
271
|
+
F1: { key: "F1", code: "F1" },
|
|
272
|
+
F2: { key: "F2", code: "F2" },
|
|
273
|
+
F3: { key: "F3", code: "F3" },
|
|
274
|
+
F4: { key: "F4", code: "F4" },
|
|
275
|
+
F5: { key: "F5", code: "F5" },
|
|
276
|
+
F6: { key: "F6", code: "F6" },
|
|
277
|
+
F7: { key: "F7", code: "F7" },
|
|
278
|
+
F8: { key: "F8", code: "F8" },
|
|
279
|
+
F9: { key: "F9", code: "F9" },
|
|
280
|
+
F10: { key: "F10", code: "F10" },
|
|
281
|
+
F11: { key: "F11", code: "F11" },
|
|
282
|
+
F12: { key: "F12", code: "F12" }
|
|
283
|
+
};
|
|
284
|
+
function sleep2(ms) {
|
|
285
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/cdp-driver/actionability.ts
|
|
289
|
+
async function waitForActionable(page, selector, opts = {}) {
|
|
290
|
+
const timeout = opts.timeout ?? 3e4;
|
|
291
|
+
if (opts.force) {
|
|
292
|
+
const nodeId = await page.querySelector(selector);
|
|
293
|
+
if (!nodeId) throw new Error(`Element not found: ${selector}`);
|
|
294
|
+
const rect = await page.getBoxModel(nodeId);
|
|
295
|
+
if (!rect) throw new Error(`Element has no box: ${selector}`);
|
|
296
|
+
return { nodeId, rect };
|
|
297
|
+
}
|
|
298
|
+
const deadline = Date.now() + timeout;
|
|
299
|
+
while (Date.now() < deadline) {
|
|
300
|
+
const result = await checkActionable(page, selector);
|
|
301
|
+
if (result.ok && result.rect) {
|
|
302
|
+
const nodeId = await page.querySelector(selector);
|
|
303
|
+
if (nodeId) return { nodeId, rect: result.rect };
|
|
304
|
+
}
|
|
305
|
+
await page.waitForTimeout(50);
|
|
306
|
+
}
|
|
307
|
+
throw new Error(`Actionability timeout: element '${selector}' not ready after ${timeout}ms`);
|
|
308
|
+
}
|
|
309
|
+
async function checkActionable(page, selector) {
|
|
310
|
+
const result = await page.evaluate(`
|
|
311
|
+
(function() {
|
|
312
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
313
|
+
if (!el) return { ok: false, reason: 'not_found' };
|
|
314
|
+
|
|
315
|
+
// Check attached to DOM
|
|
316
|
+
if (!el.isConnected) return { ok: false, reason: 'detached' };
|
|
317
|
+
|
|
318
|
+
// Check visibility
|
|
319
|
+
const style = window.getComputedStyle(el);
|
|
320
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
|
|
321
|
+
return { ok: false, reason: 'invisible' };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Check non-zero size
|
|
325
|
+
const rect = el.getBoundingClientRect();
|
|
326
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
327
|
+
return { ok: false, reason: 'zero_size' };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Check enabled (for form elements)
|
|
331
|
+
if (el.disabled) return { ok: false, reason: 'disabled' };
|
|
332
|
+
if (el.tagName === 'OPTION' && el.closest('select')?.disabled) {
|
|
333
|
+
return { ok: false, reason: 'parent_disabled' };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Check not covered by another element at center
|
|
337
|
+
const cx = rect.x + rect.width / 2;
|
|
338
|
+
const cy = rect.y + rect.height / 2;
|
|
339
|
+
const topEl = document.elementFromPoint(cx, cy);
|
|
340
|
+
if (topEl && topEl !== el && !el.contains(topEl) && !topEl.contains(el)) {
|
|
341
|
+
return { ok: false, reason: 'covered', rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
ok: true,
|
|
346
|
+
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
347
|
+
};
|
|
348
|
+
})()
|
|
349
|
+
`);
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
async function scrollIntoView(page, selector) {
|
|
353
|
+
await page.evaluate(`
|
|
354
|
+
(function() {
|
|
355
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
356
|
+
if (el) el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
|
|
357
|
+
})()
|
|
358
|
+
`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// src/cdp-driver/locator.ts
|
|
362
|
+
var XBLocatorImpl = class _XBLocatorImpl {
|
|
363
|
+
page;
|
|
364
|
+
selector;
|
|
365
|
+
constructor(page, selector) {
|
|
366
|
+
this.page = page;
|
|
367
|
+
this.selector = selector;
|
|
368
|
+
}
|
|
369
|
+
// ── Actions ─────────────────────────────────────────────────
|
|
370
|
+
async click(opts = {}) {
|
|
371
|
+
const { rect } = await waitForActionable(this.page, this.selector, opts);
|
|
372
|
+
await scrollIntoView(this.page, this.selector);
|
|
373
|
+
const updatedRect = await this.page.evaluate(`
|
|
374
|
+
(function() {
|
|
375
|
+
const el = document.querySelector(${JSON.stringify(this.selector)});
|
|
376
|
+
if (!el) return null;
|
|
377
|
+
const rect = el.getBoundingClientRect();
|
|
378
|
+
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
379
|
+
})()
|
|
380
|
+
`);
|
|
381
|
+
const finalRect = updatedRect ?? rect;
|
|
382
|
+
const cx = finalRect.x + finalRect.width / 2;
|
|
383
|
+
const cy = finalRect.y + finalRect.height / 2;
|
|
384
|
+
await this.page.mouse.click(cx, cy, {
|
|
385
|
+
button: opts.button ?? "left",
|
|
386
|
+
clickCount: opts.clickCount ?? 1,
|
|
387
|
+
delay: opts.delay
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
async fill(value, opts = {}) {
|
|
391
|
+
await waitForActionable(this.page, this.selector, opts);
|
|
392
|
+
await scrollIntoView(this.page, this.selector);
|
|
393
|
+
await this.page.evaluate(`
|
|
394
|
+
(function() {
|
|
395
|
+
const el = document.querySelector(${JSON.stringify(this.selector)});
|
|
396
|
+
if (!el) throw new Error('Element not found: ${this.selector.replace(/'/g, "\\'")}');
|
|
397
|
+
el.focus();
|
|
398
|
+
el.value = '';
|
|
399
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
400
|
+
})()
|
|
401
|
+
`);
|
|
402
|
+
await this.page.keyboard.insertText(value);
|
|
403
|
+
await this.page.evaluate(`
|
|
404
|
+
(function() {
|
|
405
|
+
const el = document.querySelector(${JSON.stringify(this.selector)});
|
|
406
|
+
if (el) {
|
|
407
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
408
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
409
|
+
}
|
|
410
|
+
})()
|
|
411
|
+
`);
|
|
412
|
+
}
|
|
413
|
+
async press(key, opts = {}) {
|
|
414
|
+
await waitForActionable(this.page, this.selector, { timeout: opts.timeout });
|
|
415
|
+
await scrollIntoView(this.page, this.selector);
|
|
416
|
+
await this.page.evaluate(`
|
|
417
|
+
(function() {
|
|
418
|
+
const el = document.querySelector(${JSON.stringify(this.selector)});
|
|
419
|
+
if (el) el.focus();
|
|
420
|
+
})()
|
|
421
|
+
`);
|
|
422
|
+
await this.page.keyboard.press(key);
|
|
423
|
+
}
|
|
424
|
+
async pressSequentially(text, opts = {}) {
|
|
425
|
+
await waitForActionable(this.page, this.selector, { timeout: opts.timeout });
|
|
426
|
+
await scrollIntoView(this.page, this.selector);
|
|
427
|
+
await this.page.evaluate(`
|
|
428
|
+
(function() {
|
|
429
|
+
const el = document.querySelector(${JSON.stringify(this.selector)});
|
|
430
|
+
if (el) el.focus();
|
|
431
|
+
})()
|
|
432
|
+
`);
|
|
433
|
+
await this.page.keyboard.type(text, { delay: opts.delay });
|
|
434
|
+
}
|
|
435
|
+
async hover(opts = {}) {
|
|
436
|
+
if (!opts.force) {
|
|
437
|
+
const { rect } = await waitForActionable(this.page, this.selector, { timeout: opts.timeout });
|
|
438
|
+
await scrollIntoView(this.page, this.selector);
|
|
439
|
+
const cx = rect.x + rect.width / 2;
|
|
440
|
+
const cy = rect.y + rect.height / 2;
|
|
441
|
+
await this.page.mouse.move(cx, cy);
|
|
442
|
+
} else {
|
|
443
|
+
await scrollIntoView(this.page, this.selector);
|
|
444
|
+
const rect = await this.page.evaluate(`
|
|
445
|
+
(function() {
|
|
446
|
+
const el = document.querySelector(${JSON.stringify(this.selector)});
|
|
447
|
+
if (!el) return { x: 0, y: 0, width: 0, height: 0 };
|
|
448
|
+
const r = el.getBoundingClientRect();
|
|
449
|
+
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
450
|
+
})()
|
|
451
|
+
`);
|
|
452
|
+
const cx = rect.x + rect.width / 2;
|
|
453
|
+
const cy = rect.y + rect.height / 2;
|
|
454
|
+
await this.page.mouse.move(cx, cy);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
async type(text, opts = {}) {
|
|
458
|
+
await this.pressSequentially(text, opts);
|
|
459
|
+
}
|
|
460
|
+
async check(opts = {}) {
|
|
461
|
+
await waitForActionable(this.page, this.selector, { timeout: opts.timeout });
|
|
462
|
+
const isChecked = await this.page.evaluate(`
|
|
463
|
+
(function() {
|
|
464
|
+
const el = document.querySelector(${JSON.stringify(this.selector)});
|
|
465
|
+
return el?.checked === true;
|
|
466
|
+
})()
|
|
467
|
+
`);
|
|
468
|
+
if (!isChecked) {
|
|
469
|
+
await this.click({ timeout: opts.timeout });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async uncheck(opts = {}) {
|
|
473
|
+
await waitForActionable(this.page, this.selector, { timeout: opts.timeout });
|
|
474
|
+
const isChecked = await this.page.evaluate(`
|
|
475
|
+
(function() {
|
|
476
|
+
const el = document.querySelector(${JSON.stringify(this.selector)});
|
|
477
|
+
return el?.checked === true;
|
|
478
|
+
})()
|
|
479
|
+
`);
|
|
480
|
+
if (isChecked) {
|
|
481
|
+
await this.click({ timeout: opts.timeout });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async selectOption(value) {
|
|
485
|
+
await waitForActionable(this.page, this.selector);
|
|
486
|
+
const values = Array.isArray(value) ? value : [value];
|
|
487
|
+
const selected = await this.page.evaluate(`
|
|
488
|
+
(function() {
|
|
489
|
+
const el = document.querySelector(${JSON.stringify(this.selector)});
|
|
490
|
+
if (!el || el.tagName !== 'SELECT') throw new Error('Not a select element');
|
|
491
|
+
|
|
492
|
+
const values = ${JSON.stringify(values)};
|
|
493
|
+
const selectedValues = [];
|
|
494
|
+
|
|
495
|
+
for (const opt of el.options) {
|
|
496
|
+
for (const v of values) {
|
|
497
|
+
if (typeof v === 'object') {
|
|
498
|
+
if (v.label && opt.label === v.label) { opt.selected = true; selectedValues.push(opt.value); }
|
|
499
|
+
else if (v.value && opt.value === v.value) { opt.selected = true; selectedValues.push(opt.value); }
|
|
500
|
+
else if (v.index !== undefined && opt.index === v.index) { opt.selected = true; selectedValues.push(opt.value); }
|
|
501
|
+
} else if (opt.value === v || opt.label === v) {
|
|
502
|
+
opt.selected = true;
|
|
503
|
+
selectedValues.push(opt.value);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
509
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
510
|
+
return selectedValues;
|
|
511
|
+
})()
|
|
512
|
+
`);
|
|
513
|
+
return selected;
|
|
514
|
+
}
|
|
515
|
+
async screenshot(opts = {}) {
|
|
516
|
+
await waitForActionable(this.page, this.selector);
|
|
517
|
+
const nodeId = await this.page.querySelector(this.selector);
|
|
518
|
+
if (!nodeId) throw new Error(`Element not found: ${this.selector}`);
|
|
519
|
+
const box = await this.page.getBoxModel(nodeId);
|
|
520
|
+
if (!box) throw new Error("Element has no box");
|
|
521
|
+
return this.page.screenshot({
|
|
522
|
+
...opts,
|
|
523
|
+
clip: { x: box.x, y: box.y, width: box.width, height: box.height }
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
// ── State checks ────────────────────────────────────────────
|
|
527
|
+
async waitFor(opts = {}) {
|
|
528
|
+
await this.page.waitForSelector(this.selector, opts);
|
|
529
|
+
}
|
|
530
|
+
async count() {
|
|
531
|
+
return this.page.evaluate(`
|
|
532
|
+
document.querySelectorAll(${JSON.stringify(this.selector)}).length
|
|
533
|
+
`);
|
|
534
|
+
}
|
|
535
|
+
async isVisible() {
|
|
536
|
+
try {
|
|
537
|
+
const result = await this.page.evaluate(`
|
|
538
|
+
(function() {
|
|
539
|
+
const el = document.querySelector(${JSON.stringify(this.selector)});
|
|
540
|
+
if (!el) return false;
|
|
541
|
+
if (!el.isConnected) return false;
|
|
542
|
+
const style = window.getComputedStyle(el);
|
|
543
|
+
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
|
544
|
+
const rect = el.getBoundingClientRect();
|
|
545
|
+
return rect.width > 0 && rect.height > 0;
|
|
546
|
+
})()
|
|
547
|
+
`);
|
|
548
|
+
return Boolean(result);
|
|
549
|
+
} catch {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
async isHidden() {
|
|
554
|
+
return !await this.isVisible();
|
|
555
|
+
}
|
|
556
|
+
async isEnabled() {
|
|
557
|
+
return this.page.evaluate(`
|
|
558
|
+
(function() {
|
|
559
|
+
const el = document.querySelector(${JSON.stringify(this.selector)});
|
|
560
|
+
if (!el) return false;
|
|
561
|
+
return !el.disabled;
|
|
562
|
+
})()
|
|
563
|
+
`);
|
|
564
|
+
}
|
|
565
|
+
async isDisabled() {
|
|
566
|
+
return !await this.isEnabled();
|
|
567
|
+
}
|
|
568
|
+
async boundingBox() {
|
|
569
|
+
const nodeId = await this.page.querySelector(this.selector);
|
|
570
|
+
if (!nodeId) return null;
|
|
571
|
+
return this.page.getBoxModel(nodeId);
|
|
572
|
+
}
|
|
573
|
+
// ── Text/HTML ───────────────────────────────────────────────
|
|
574
|
+
async textContent() {
|
|
575
|
+
return this.page.evaluate(`
|
|
576
|
+
(function() {
|
|
577
|
+
const el = document.querySelector(${JSON.stringify(this.selector)});
|
|
578
|
+
return el?.textContent ?? null;
|
|
579
|
+
})()
|
|
580
|
+
`);
|
|
581
|
+
}
|
|
582
|
+
async innerText() {
|
|
583
|
+
return this.page.evaluate(`
|
|
584
|
+
(function() {
|
|
585
|
+
const el = document.querySelector(${JSON.stringify(this.selector)});
|
|
586
|
+
if (!el) throw new Error('Element not found');
|
|
587
|
+
return el.innerText;
|
|
588
|
+
})()
|
|
589
|
+
`);
|
|
590
|
+
}
|
|
591
|
+
async innerHTML() {
|
|
592
|
+
return this.page.evaluate(`
|
|
593
|
+
(function() {
|
|
594
|
+
const el = document.querySelector(${JSON.stringify(this.selector)});
|
|
595
|
+
if (!el) throw new Error('Element not found');
|
|
596
|
+
return el.innerHTML;
|
|
597
|
+
})()
|
|
598
|
+
`);
|
|
599
|
+
}
|
|
600
|
+
async getAttribute(name) {
|
|
601
|
+
return this.page.evaluate(`
|
|
602
|
+
(function() {
|
|
603
|
+
const el = document.querySelector(${JSON.stringify(this.selector)});
|
|
604
|
+
return el?.getAttribute(${JSON.stringify(name)}) ?? null;
|
|
605
|
+
})()
|
|
606
|
+
`);
|
|
607
|
+
}
|
|
608
|
+
// ── Evaluate ────────────────────────────────────────────────
|
|
609
|
+
async evaluate(fn, ...args) {
|
|
610
|
+
const fnBody = typeof fn === "function" ? fn.toString() : fn;
|
|
611
|
+
return this.page.evaluate(
|
|
612
|
+
`(function(sel, fnStr, ...evalArgs) {
|
|
613
|
+
const el = document.querySelector(sel);
|
|
614
|
+
if (!el) throw new Error('No element found for selector: ' + sel);
|
|
615
|
+
const fn = new Function('return ' + fnStr)();
|
|
616
|
+
return fn(el, ...evalArgs);
|
|
617
|
+
})(${JSON.stringify(this.selector)}, ${JSON.stringify(fnBody)}${args.length > 0 ? ", " + args.map((a) => JSON.stringify(a)).join(", ") : ""})`
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
async ariaSnapshot() {
|
|
621
|
+
const result = await this.page._cdpSend(
|
|
622
|
+
"Accessibility.getFullAXTree"
|
|
623
|
+
);
|
|
624
|
+
return result.nodes.map((n) => `${n.role?.value}: ${n.name?.value ?? ""}`).join("\n");
|
|
625
|
+
}
|
|
626
|
+
// ── Filtering ───────────────────────────────────────────────
|
|
627
|
+
first() {
|
|
628
|
+
return new FilteredLocator(this.page, this.selector, 0);
|
|
629
|
+
}
|
|
630
|
+
last() {
|
|
631
|
+
return new FilteredLocator(this.page, this.selector, -1);
|
|
632
|
+
}
|
|
633
|
+
nth(index) {
|
|
634
|
+
return new FilteredLocator(this.page, this.selector, index);
|
|
635
|
+
}
|
|
636
|
+
filter(opts) {
|
|
637
|
+
if (opts.visible) {
|
|
638
|
+
return new VisibleFilteredLocator(this.page, this.selector);
|
|
639
|
+
}
|
|
640
|
+
return new _XBLocatorImpl(this.page, this.selector);
|
|
641
|
+
}
|
|
642
|
+
async all() {
|
|
643
|
+
const n = await this.page.evaluate(`
|
|
644
|
+
document.querySelectorAll(${JSON.stringify(this.selector)}).length
|
|
645
|
+
`);
|
|
646
|
+
const locators = [];
|
|
647
|
+
for (let i = 0; i < n; i++) {
|
|
648
|
+
locators.push(new FilteredLocator(this.page, this.selector, i));
|
|
649
|
+
}
|
|
650
|
+
return locators;
|
|
651
|
+
}
|
|
652
|
+
async focus() {
|
|
653
|
+
await this.page.evaluate(`
|
|
654
|
+
(function() {
|
|
655
|
+
const el = document.querySelector(${JSON.stringify(this.selector)});
|
|
656
|
+
if (el) el.focus();
|
|
657
|
+
})()
|
|
658
|
+
`);
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
var FilteredLocator = class extends XBLocatorImpl {
|
|
662
|
+
constructor(page, selector, index) {
|
|
663
|
+
const indexedSelector = index === -1 ? `${selector}:last-of-type` : `${selector}:nth-of-type(${index + 1})`;
|
|
664
|
+
super(page, indexedSelector);
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
var VisibleFilteredLocator = class extends XBLocatorImpl {
|
|
668
|
+
async _withVisibleTag(fn) {
|
|
669
|
+
const tag = `data-xb-vt-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
|
|
670
|
+
const found = await this.page.evaluate(`
|
|
671
|
+
(function() {
|
|
672
|
+
const els = document.querySelectorAll(${JSON.stringify(this.selector)});
|
|
673
|
+
for (const el of els) {
|
|
674
|
+
if (!el.isConnected) continue;
|
|
675
|
+
const style = window.getComputedStyle(el);
|
|
676
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') continue;
|
|
677
|
+
const rect = el.getBoundingClientRect();
|
|
678
|
+
if (rect.width === 0 || rect.height === 0) continue;
|
|
679
|
+
el.setAttribute(${JSON.stringify(tag)}, '');
|
|
680
|
+
return true;
|
|
681
|
+
}
|
|
682
|
+
return false;
|
|
683
|
+
})()
|
|
684
|
+
`);
|
|
685
|
+
if (!found) throw new Error(`No visible element found for: ${this.selector}`);
|
|
686
|
+
try {
|
|
687
|
+
return await fn(`[${tag}]`);
|
|
688
|
+
} finally {
|
|
689
|
+
await this.page.evaluate(`
|
|
690
|
+
document.querySelectorAll(${JSON.stringify(`[${tag}]`)}).forEach(el => el.removeAttribute(${JSON.stringify(tag)}))
|
|
691
|
+
`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
async click(opts) {
|
|
695
|
+
return this._withVisibleTag((tagSel) => new XBLocatorImpl(this.page, tagSel).click(opts));
|
|
696
|
+
}
|
|
697
|
+
async fill(value, opts) {
|
|
698
|
+
return this._withVisibleTag((tagSel) => new XBLocatorImpl(this.page, tagSel).fill(value, opts));
|
|
699
|
+
}
|
|
700
|
+
async press(key, opts) {
|
|
701
|
+
return this._withVisibleTag((tagSel) => new XBLocatorImpl(this.page, tagSel).press(key, opts));
|
|
702
|
+
}
|
|
703
|
+
async hover(opts) {
|
|
704
|
+
return this._withVisibleTag((tagSel) => new XBLocatorImpl(this.page, tagSel).hover(opts));
|
|
705
|
+
}
|
|
706
|
+
async count() {
|
|
707
|
+
return this.page.evaluate(`
|
|
708
|
+
(function() {
|
|
709
|
+
let count = 0;
|
|
710
|
+
const els = document.querySelectorAll(${JSON.stringify(this.selector)});
|
|
711
|
+
for (const el of els) {
|
|
712
|
+
if (!el.isConnected) continue;
|
|
713
|
+
const style = window.getComputedStyle(el);
|
|
714
|
+
if (style.display === 'none' || style.visibility === 'hidden') continue;
|
|
715
|
+
const rect = el.getBoundingClientRect();
|
|
716
|
+
if (rect.width === 0 || rect.height === 0) continue;
|
|
717
|
+
count++;
|
|
718
|
+
}
|
|
719
|
+
return count;
|
|
720
|
+
})()
|
|
721
|
+
`);
|
|
722
|
+
}
|
|
723
|
+
async isVisible() {
|
|
724
|
+
try {
|
|
725
|
+
const result = await this.page.evaluate(`
|
|
726
|
+
(function() {
|
|
727
|
+
const els = document.querySelectorAll(${JSON.stringify(this.selector)});
|
|
728
|
+
for (const el of els) {
|
|
729
|
+
if (!el.isConnected) continue;
|
|
730
|
+
const style = window.getComputedStyle(el);
|
|
731
|
+
if (style.display === 'none' || style.visibility === 'hidden') continue;
|
|
732
|
+
const rect = el.getBoundingClientRect();
|
|
733
|
+
if (rect.width > 0 && rect.height > 0) return true;
|
|
734
|
+
}
|
|
735
|
+
return false;
|
|
736
|
+
})()
|
|
737
|
+
`);
|
|
738
|
+
return result;
|
|
739
|
+
} catch {
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
async textContent() {
|
|
744
|
+
return this._withVisibleTag((tagSel) => new XBLocatorImpl(this.page, tagSel).textContent());
|
|
745
|
+
}
|
|
746
|
+
async innerText() {
|
|
747
|
+
return this._withVisibleTag((tagSel) => new XBLocatorImpl(this.page, tagSel).innerText());
|
|
748
|
+
}
|
|
749
|
+
async waitFor(opts) {
|
|
750
|
+
const deadline = Date.now() + (opts?.timeout ?? 3e4);
|
|
751
|
+
while (Date.now() < deadline) {
|
|
752
|
+
if (await this.isVisible()) return;
|
|
753
|
+
await this.page.waitForTimeout(50);
|
|
754
|
+
}
|
|
755
|
+
throw new Error(`Timeout waiting for visible element: ${this.selector}`);
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
// src/cdp-driver/element-handle.ts
|
|
760
|
+
var XBElementHandleImpl = class {
|
|
761
|
+
page;
|
|
762
|
+
nodeId;
|
|
763
|
+
disposed = false;
|
|
764
|
+
constructor(page, nodeId) {
|
|
765
|
+
this.page = page;
|
|
766
|
+
this.nodeId = nodeId;
|
|
767
|
+
}
|
|
768
|
+
get _nodeId() {
|
|
769
|
+
return this.nodeId;
|
|
770
|
+
}
|
|
771
|
+
async click(opts = {}) {
|
|
772
|
+
if (this.disposed) throw new Error("Element handle disposed");
|
|
773
|
+
const box = await this.boundingBox();
|
|
774
|
+
if (!box) throw new Error("Element has no box");
|
|
775
|
+
await this.scrollIntoViewIfNeeded();
|
|
776
|
+
const cx = box.x + box.width / 2;
|
|
777
|
+
const cy = box.y + box.height / 2;
|
|
778
|
+
await this.page.mouse.click(cx, cy, {
|
|
779
|
+
button: opts.button ?? "left",
|
|
780
|
+
clickCount: opts.clickCount ?? 1,
|
|
781
|
+
delay: opts.delay
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
async fill(value, _opts = {}) {
|
|
785
|
+
if (this.disposed) throw new Error("Element handle disposed");
|
|
786
|
+
const objectId = await this.page.resolveNode(this.nodeId);
|
|
787
|
+
await this.page.callFunctionOn(
|
|
788
|
+
objectId,
|
|
789
|
+
`function(value) {
|
|
790
|
+
this.focus();
|
|
791
|
+
this.value = '';
|
|
792
|
+
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
793
|
+
}`,
|
|
794
|
+
[value]
|
|
795
|
+
);
|
|
796
|
+
await this.page.keyboard.insertText(value);
|
|
797
|
+
await this.page.callFunctionOn(
|
|
798
|
+
objectId,
|
|
799
|
+
`function() {
|
|
800
|
+
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
801
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
802
|
+
}`
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
async hover() {
|
|
806
|
+
const box = await this.boundingBox();
|
|
807
|
+
if (!box) throw new Error("Element has no box");
|
|
808
|
+
await this.scrollIntoViewIfNeeded();
|
|
809
|
+
await this.page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
|
810
|
+
}
|
|
811
|
+
async press(key) {
|
|
812
|
+
const objectId = await this.page.resolveNode(this.nodeId);
|
|
813
|
+
await this.page.callFunctionOn(objectId, "function() { this.focus(); }");
|
|
814
|
+
await this.page.keyboard.press(key);
|
|
815
|
+
}
|
|
816
|
+
async screenshot(opts = {}) {
|
|
817
|
+
const box = await this.boundingBox();
|
|
818
|
+
if (!box) throw new Error("Element has no box");
|
|
819
|
+
return this.page.screenshot({
|
|
820
|
+
...opts,
|
|
821
|
+
clip: box
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
async boundingBox() {
|
|
825
|
+
return this.page.getBoxModel(this.nodeId);
|
|
826
|
+
}
|
|
827
|
+
async isVisible() {
|
|
828
|
+
try {
|
|
829
|
+
const objectId = await this.page.resolveNode(this.nodeId);
|
|
830
|
+
const result = await this.page.callFunctionOn(
|
|
831
|
+
objectId,
|
|
832
|
+
`function() {
|
|
833
|
+
if (!this.isConnected) return false;
|
|
834
|
+
const style = window.getComputedStyle(this);
|
|
835
|
+
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
|
836
|
+
const rect = this.getBoundingClientRect();
|
|
837
|
+
return rect.width > 0 && rect.height > 0;
|
|
838
|
+
}`
|
|
839
|
+
);
|
|
840
|
+
return Boolean(result);
|
|
841
|
+
} catch {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
async isEnabled() {
|
|
846
|
+
const objectId = await this.page.resolveNode(this.nodeId);
|
|
847
|
+
return this.page.callFunctionOn(objectId, "function() { return !this.disabled; }");
|
|
848
|
+
}
|
|
849
|
+
async textContent() {
|
|
850
|
+
const objectId = await this.page.resolveNode(this.nodeId);
|
|
851
|
+
return this.page.callFunctionOn(objectId, "function() { return this.textContent; }");
|
|
852
|
+
}
|
|
853
|
+
async innerText() {
|
|
854
|
+
const objectId = await this.page.resolveNode(this.nodeId);
|
|
855
|
+
return this.page.callFunctionOn(objectId, "function() { return this.innerText; }");
|
|
856
|
+
}
|
|
857
|
+
async innerHTML() {
|
|
858
|
+
const objectId = await this.page.resolveNode(this.nodeId);
|
|
859
|
+
return this.page.callFunctionOn(objectId, "function() { return this.innerHTML; }");
|
|
860
|
+
}
|
|
861
|
+
async getAttribute(name) {
|
|
862
|
+
const objectId = await this.page.resolveNode(this.nodeId);
|
|
863
|
+
return this.page.callFunctionOn(objectId, `function() { return this.getAttribute(${JSON.stringify(name)}); }`);
|
|
864
|
+
}
|
|
865
|
+
async scrollIntoViewIfNeeded() {
|
|
866
|
+
if (this.disposed) return;
|
|
867
|
+
const objectId = await this.page.resolveNode(this.nodeId);
|
|
868
|
+
await this.page.callFunctionOn(
|
|
869
|
+
objectId,
|
|
870
|
+
'function() { this.scrollIntoView({ block: "center", inline: "center", behavior: "instant" }); }'
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
dispose() {
|
|
874
|
+
this.disposed = true;
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
// src/cdp-driver/page-helpers.ts
|
|
879
|
+
function globToRegex(glob) {
|
|
880
|
+
let pattern = glob.replace(/[\\^${}()|[\]+]/g, "\\$&").replace(/\./g, "\\.").replace(/\*\*/g, "{{DOUBLESTAR}}").replace(/\*/g, "[^/]*").replace(/{{DOUBLESTAR}}/g, ".*").replace(/\?/g, ".");
|
|
881
|
+
if (!pattern.startsWith("http") && !pattern.startsWith("\\.")) {
|
|
882
|
+
pattern = ".*" + pattern;
|
|
883
|
+
}
|
|
884
|
+
return new RegExp("^" + pattern + "$", "i");
|
|
885
|
+
}
|
|
886
|
+
function matchGlob(pattern, url) {
|
|
887
|
+
return globToRegex(pattern).test(url);
|
|
888
|
+
}
|
|
889
|
+
function createResponsePredicate(urlOrPredicate) {
|
|
890
|
+
if (typeof urlOrPredicate === "function") {
|
|
891
|
+
return urlOrPredicate;
|
|
892
|
+
}
|
|
893
|
+
if (urlOrPredicate instanceof RegExp) {
|
|
894
|
+
return (resp) => urlOrPredicate.test(resp.url());
|
|
895
|
+
}
|
|
896
|
+
const regex = globToRegex(urlOrPredicate);
|
|
897
|
+
return (resp) => regex.test(resp.url());
|
|
898
|
+
}
|
|
899
|
+
function createRequestPredicate(urlOrPredicate) {
|
|
900
|
+
if (typeof urlOrPredicate === "function") {
|
|
901
|
+
return urlOrPredicate;
|
|
902
|
+
}
|
|
903
|
+
if (urlOrPredicate instanceof RegExp) {
|
|
904
|
+
return (req) => urlOrPredicate.test(req.url());
|
|
905
|
+
}
|
|
906
|
+
const regex = globToRegex(urlOrPredicate);
|
|
907
|
+
return (req) => regex.test(req.url());
|
|
908
|
+
}
|
|
909
|
+
function createXBResponse(data, conn, sessionId, requestData) {
|
|
910
|
+
const request = requestData ? createXBRequest(null, requestData) : createXBRequest(null, { requestId: data.requestId, url: data.url, method: "GET", headers: {}, postData: null, resourceType: "other" });
|
|
911
|
+
return {
|
|
912
|
+
status: () => data.status,
|
|
913
|
+
statusText: () => "",
|
|
914
|
+
url: () => data.url,
|
|
915
|
+
headers: () => data.headers,
|
|
916
|
+
ok: () => data.status >= 200 && data.status < 300,
|
|
917
|
+
body: async () => {
|
|
918
|
+
if (!conn) throw new Error("Response body not available");
|
|
919
|
+
try {
|
|
920
|
+
const resp = await conn.send(
|
|
921
|
+
"Network.getResponseBody",
|
|
922
|
+
{ requestId: data.requestId },
|
|
923
|
+
sessionId
|
|
924
|
+
);
|
|
925
|
+
return Buffer.from(resp.body, resp.base64Encoded ? "base64" : "utf8");
|
|
926
|
+
} catch {
|
|
927
|
+
throw new Error("Response body not available");
|
|
928
|
+
}
|
|
929
|
+
},
|
|
930
|
+
text: async () => {
|
|
931
|
+
if (!conn) throw new Error("Response body not available");
|
|
932
|
+
try {
|
|
933
|
+
const resp = await conn.send(
|
|
934
|
+
"Network.getResponseBody",
|
|
935
|
+
{ requestId: data.requestId },
|
|
936
|
+
sessionId
|
|
937
|
+
);
|
|
938
|
+
return resp.base64Encoded ? Buffer.from(resp.body, "base64").toString("utf8") : resp.body;
|
|
939
|
+
} catch {
|
|
940
|
+
throw new Error("Response body not available");
|
|
941
|
+
}
|
|
942
|
+
},
|
|
943
|
+
json: async () => {
|
|
944
|
+
const text = await (async () => {
|
|
945
|
+
if (!conn) throw new Error("Response body not available");
|
|
946
|
+
try {
|
|
947
|
+
const resp = await conn.send(
|
|
948
|
+
"Network.getResponseBody",
|
|
949
|
+
{ requestId: data.requestId },
|
|
950
|
+
sessionId
|
|
951
|
+
);
|
|
952
|
+
return resp.base64Encoded ? Buffer.from(resp.body, "base64").toString("utf8") : resp.body;
|
|
953
|
+
} catch {
|
|
954
|
+
throw new Error("Response body not available");
|
|
955
|
+
}
|
|
956
|
+
})();
|
|
957
|
+
return JSON.parse(text);
|
|
958
|
+
},
|
|
959
|
+
request: () => request
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
function createXBRequest(page, data) {
|
|
963
|
+
return {
|
|
964
|
+
url: () => data.url,
|
|
965
|
+
method: () => data.method,
|
|
966
|
+
headers: () => data.headers,
|
|
967
|
+
postData: () => data.postData,
|
|
968
|
+
resourceType: () => data.resourceType,
|
|
969
|
+
response: async () => {
|
|
970
|
+
if (!page?._networkResponses) return null;
|
|
971
|
+
const resp = page._networkResponses.get(data.requestId);
|
|
972
|
+
if (!resp) return null;
|
|
973
|
+
return createXBResponse(resp, page._connection, page.sessionId, data);
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
function createXBRouteFetch(conn, sessionId, params) {
|
|
978
|
+
const request = createXBRequest(null, {
|
|
979
|
+
requestId: params.requestId,
|
|
980
|
+
url: params.request.url,
|
|
981
|
+
method: params.request.method,
|
|
982
|
+
headers: params.request.headers,
|
|
983
|
+
postData: params.request.postData ?? null,
|
|
984
|
+
resourceType: params.resourceType
|
|
985
|
+
});
|
|
986
|
+
return {
|
|
987
|
+
request: () => request,
|
|
988
|
+
abort: async (errorCode) => {
|
|
989
|
+
await conn.send("Fetch.failRequest", {
|
|
990
|
+
requestId: params.requestId,
|
|
991
|
+
errorReason: errorCode || "Failed"
|
|
992
|
+
}, sessionId);
|
|
993
|
+
},
|
|
994
|
+
continue: async (opts) => {
|
|
995
|
+
await conn.send("Fetch.continueRequest", {
|
|
996
|
+
requestId: params.requestId,
|
|
997
|
+
url: opts?.url,
|
|
998
|
+
method: opts?.method,
|
|
999
|
+
headers: opts?.headers ? Object.entries(opts.headers).map(([k, v]) => ({ name: k, value: v })) : void 0,
|
|
1000
|
+
postData: opts?.postData ? Buffer.from(opts.postData).toString("base64") : void 0
|
|
1001
|
+
}, sessionId);
|
|
1002
|
+
},
|
|
1003
|
+
fulfill: async (opts) => {
|
|
1004
|
+
const bodyStr = typeof opts.body === "string" ? opts.body : opts.body ? opts.body.toString("utf8") : "";
|
|
1005
|
+
const bodyBytes = Buffer.from(bodyStr, "utf8");
|
|
1006
|
+
const headers = { ...opts.headers };
|
|
1007
|
+
if (opts.contentType) headers["content-type"] = opts.contentType;
|
|
1008
|
+
headers["access-control-allow-origin"] = "*";
|
|
1009
|
+
await conn.send("Fetch.fulfillRequest", {
|
|
1010
|
+
requestId: params.requestId,
|
|
1011
|
+
responseCode: opts.status ?? 200,
|
|
1012
|
+
responseHeaders: Object.entries(headers).map(([k, v]) => ({ name: k, value: v })),
|
|
1013
|
+
body: bodyBytes.toString("base64")
|
|
1014
|
+
}, sessionId);
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// src/cdp-driver/page.ts
|
|
1020
|
+
var XBPageImpl = class _XBPageImpl {
|
|
1021
|
+
conn;
|
|
1022
|
+
_emitter = new EventEmitter();
|
|
1023
|
+
_subscriptions = [];
|
|
1024
|
+
sessionId;
|
|
1025
|
+
_targetId;
|
|
1026
|
+
_contextImpl;
|
|
1027
|
+
_browserImpl;
|
|
1028
|
+
_closed = false;
|
|
1029
|
+
_url = "about:blank";
|
|
1030
|
+
_title = "";
|
|
1031
|
+
_viewportSize = null;
|
|
1032
|
+
_loadState = {
|
|
1033
|
+
loadFired: true,
|
|
1034
|
+
domContentFired: true,
|
|
1035
|
+
networkIdle: true
|
|
1036
|
+
};
|
|
1037
|
+
mouse;
|
|
1038
|
+
keyboard;
|
|
1039
|
+
// Network tracking for waitForLoadState('networkidle')
|
|
1040
|
+
inflightRequests = /* @__PURE__ */ new Set();
|
|
1041
|
+
networkIdleResolve = null;
|
|
1042
|
+
networkIdleTimer = null;
|
|
1043
|
+
static NETWORK_IDLE_MS = 500;
|
|
1044
|
+
constructor(conn, sessionId, targetId, context, browser) {
|
|
1045
|
+
this.conn = conn;
|
|
1046
|
+
this.sessionId = sessionId;
|
|
1047
|
+
this._targetId = targetId;
|
|
1048
|
+
this._contextImpl = context;
|
|
1049
|
+
this._browserImpl = browser;
|
|
1050
|
+
this.mouse = new XBMouseImpl(conn, sessionId);
|
|
1051
|
+
this.keyboard = new XBKeyboardImpl(conn, sessionId);
|
|
1052
|
+
}
|
|
1053
|
+
_emit(event, ...args) {
|
|
1054
|
+
this._emitter.emit(event, ...args);
|
|
1055
|
+
}
|
|
1056
|
+
/** Internal initialization — must be called after construction */
|
|
1057
|
+
async _init() {
|
|
1058
|
+
await this.conn.send("Page.enable", void 0, this.sessionId);
|
|
1059
|
+
await this.conn.send("Runtime.enable", void 0, this.sessionId);
|
|
1060
|
+
await this.conn.send("Network.enable", void 0, this.sessionId);
|
|
1061
|
+
this.setupPageEvents();
|
|
1062
|
+
this.setupNetworkEvents();
|
|
1063
|
+
this.setupConsoleEvents();
|
|
1064
|
+
await this.conn.send("Runtime.runIfWaitingForDebugger", void 0, this.sessionId).catch(() => {
|
|
1065
|
+
});
|
|
1066
|
+
try {
|
|
1067
|
+
const info = await this.conn.send(
|
|
1068
|
+
"Target.getTargetInfo",
|
|
1069
|
+
{ targetId: this._targetId }
|
|
1070
|
+
);
|
|
1071
|
+
this._url = info.url;
|
|
1072
|
+
this._title = info.title;
|
|
1073
|
+
} catch {
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
get _connection() {
|
|
1077
|
+
return this.conn;
|
|
1078
|
+
}
|
|
1079
|
+
// ── Navigation ──────────────────────────────────────────────
|
|
1080
|
+
async goto(url, opts = {}) {
|
|
1081
|
+
if (this._closed) throw new Error("Page is closed");
|
|
1082
|
+
const waitUntil = opts.waitUntil ?? "load";
|
|
1083
|
+
const timeout = opts.timeout ?? 3e4;
|
|
1084
|
+
this._loadState = { loadFired: false, domContentFired: false, networkIdle: false };
|
|
1085
|
+
const result = await this.conn.send(
|
|
1086
|
+
"Page.navigate",
|
|
1087
|
+
{ url, referrer: opts.referer },
|
|
1088
|
+
this.sessionId
|
|
1089
|
+
);
|
|
1090
|
+
if (result.errorText) {
|
|
1091
|
+
throw new Error(`Navigation failed: ${result.errorText}`);
|
|
1092
|
+
}
|
|
1093
|
+
await this.waitForLoadState(waitUntil, timeout);
|
|
1094
|
+
this._url = url;
|
|
1095
|
+
const statusCode = 200;
|
|
1096
|
+
const finalUrl = url;
|
|
1097
|
+
const headers = {};
|
|
1098
|
+
return {
|
|
1099
|
+
status: () => statusCode,
|
|
1100
|
+
ok: () => statusCode >= 200 && statusCode < 300,
|
|
1101
|
+
url: () => finalUrl,
|
|
1102
|
+
headers: () => headers
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
async goBack(opts = {}) {
|
|
1106
|
+
await this.evaluate("() => history.back()");
|
|
1107
|
+
await this.waitForLoadState(opts.waitUntil ?? "load", opts.timeout);
|
|
1108
|
+
}
|
|
1109
|
+
async goForward(opts = {}) {
|
|
1110
|
+
await this.evaluate("() => history.forward()");
|
|
1111
|
+
await this.waitForLoadState(opts.waitUntil ?? "load", opts.timeout);
|
|
1112
|
+
}
|
|
1113
|
+
async reload(opts = {}) {
|
|
1114
|
+
this._loadState = { loadFired: false, domContentFired: false, networkIdle: false };
|
|
1115
|
+
await this.conn.send("Page.reload", void 0, this.sessionId);
|
|
1116
|
+
await this.waitForLoadState(opts.waitUntil ?? "load", opts.timeout);
|
|
1117
|
+
}
|
|
1118
|
+
async waitForLoadState(state = "load", timeout = 3e4) {
|
|
1119
|
+
if (this._closed) throw new Error("Page is closed");
|
|
1120
|
+
const checkState = () => {
|
|
1121
|
+
switch (state) {
|
|
1122
|
+
case "domcontentloaded":
|
|
1123
|
+
return this._loadState.domContentFired;
|
|
1124
|
+
case "load":
|
|
1125
|
+
return this._loadState.loadFired;
|
|
1126
|
+
case "networkidle":
|
|
1127
|
+
return this._loadState.networkIdle;
|
|
1128
|
+
case "commit":
|
|
1129
|
+
return this._loadState.domContentFired;
|
|
1130
|
+
default:
|
|
1131
|
+
return true;
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
if (checkState()) return;
|
|
1135
|
+
return new Promise((resolve, reject) => {
|
|
1136
|
+
const timer = setTimeout(() => {
|
|
1137
|
+
reject(new Error(`waitForLoadState('${state}') timeout after ${timeout}ms`));
|
|
1138
|
+
}, timeout);
|
|
1139
|
+
const check = () => {
|
|
1140
|
+
if (checkState()) {
|
|
1141
|
+
clearTimeout(timer);
|
|
1142
|
+
resolve();
|
|
1143
|
+
} else {
|
|
1144
|
+
setTimeout(check, 50);
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
check();
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
async waitForTimeout(ms) {
|
|
1151
|
+
await new Promise((resolve) => {
|
|
1152
|
+
const timer = setTimeout(resolve, ms);
|
|
1153
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
async waitForSelector(selector, opts = {}) {
|
|
1157
|
+
const state = opts.state ?? "visible";
|
|
1158
|
+
const timeout = opts.timeout ?? 3e4;
|
|
1159
|
+
const deadline = Date.now() + timeout;
|
|
1160
|
+
while (Date.now() < deadline) {
|
|
1161
|
+
const exists = await this.evaluate(
|
|
1162
|
+
`(function() { const el = document.querySelector(${JSON.stringify(selector)}); return !!el; })()`
|
|
1163
|
+
);
|
|
1164
|
+
if (state === "attached" && exists) return;
|
|
1165
|
+
if (state === "detached" && !exists) return;
|
|
1166
|
+
if (state === "visible" && exists) {
|
|
1167
|
+
const visible = await this.evaluate(
|
|
1168
|
+
`(function() { const el = document.querySelector(${JSON.stringify(selector)}); if (!el) return false; const rect = el.getBoundingClientRect(); const style = window.getComputedStyle(el); return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none'; })()`
|
|
1169
|
+
);
|
|
1170
|
+
if (visible) return;
|
|
1171
|
+
}
|
|
1172
|
+
if (state === "hidden") {
|
|
1173
|
+
if (!exists) return;
|
|
1174
|
+
const visible = await this.evaluate(
|
|
1175
|
+
`(function() { const el = document.querySelector(${JSON.stringify(selector)}); if (!el) return false; const rect = el.getBoundingClientRect(); const style = window.getComputedStyle(el); return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none'; })()`
|
|
1176
|
+
);
|
|
1177
|
+
if (!visible) return;
|
|
1178
|
+
}
|
|
1179
|
+
await this.waitForTimeout(100);
|
|
1180
|
+
}
|
|
1181
|
+
throw new Error(`waitForSelector('${selector}', state='${state}') timeout after ${timeout}ms`);
|
|
1182
|
+
}
|
|
1183
|
+
async waitForFunction(fn, opts = {}, ...args) {
|
|
1184
|
+
const timeout = opts.timeout ?? 3e4;
|
|
1185
|
+
const polling = opts.polling ?? 100;
|
|
1186
|
+
const deadline = Date.now() + timeout;
|
|
1187
|
+
const fnBody = typeof fn === "function" ? fn.toString() : fn;
|
|
1188
|
+
let lastError = null;
|
|
1189
|
+
while (Date.now() < deadline) {
|
|
1190
|
+
try {
|
|
1191
|
+
const result = await this.evaluate(
|
|
1192
|
+
`(function(fnStr, ...evalArgs) { const fn = new Function('return ' + fnStr); return fn(...evalArgs); })(${JSON.stringify(fnBody)}${args.length > 0 ? ", " + args.map((a) => JSON.stringify(a)).join(", ") : ""})`
|
|
1193
|
+
);
|
|
1194
|
+
if (result) return result;
|
|
1195
|
+
} catch (err) {
|
|
1196
|
+
lastError = err;
|
|
1197
|
+
}
|
|
1198
|
+
const pollMs = typeof polling === "number" ? polling : 16;
|
|
1199
|
+
await this.waitForTimeout(pollMs);
|
|
1200
|
+
}
|
|
1201
|
+
const detail = lastError ? `
|
|
1202
|
+
Last error: ${lastError.message}` : "";
|
|
1203
|
+
throw new Error(`waitForFunction timeout after ${timeout}ms${detail}`);
|
|
1204
|
+
}
|
|
1205
|
+
url() {
|
|
1206
|
+
return this._url;
|
|
1207
|
+
}
|
|
1208
|
+
async title() {
|
|
1209
|
+
this._title = await this.evaluate("document.title");
|
|
1210
|
+
return this._title;
|
|
1211
|
+
}
|
|
1212
|
+
async content() {
|
|
1213
|
+
return this.evaluate("document.documentElement.outerHTML");
|
|
1214
|
+
}
|
|
1215
|
+
// ── Evaluation ──────────────────────────────────────────────
|
|
1216
|
+
async evaluate(fn, ...args) {
|
|
1217
|
+
if (this._closed) throw new Error("Page is closed");
|
|
1218
|
+
let expression;
|
|
1219
|
+
if (typeof fn === "string") {
|
|
1220
|
+
expression = fn;
|
|
1221
|
+
} else {
|
|
1222
|
+
const argStr = args.length > 0 ? `...${JSON.stringify(args)}` : "";
|
|
1223
|
+
expression = `(()=>{const __fn=(${fn.toString()});return __fn(${argStr});})()`;
|
|
1224
|
+
}
|
|
1225
|
+
const result = await this.conn.send("Runtime.evaluate", {
|
|
1226
|
+
expression,
|
|
1227
|
+
returnByValue: true,
|
|
1228
|
+
awaitPromise: true
|
|
1229
|
+
}, this.sessionId);
|
|
1230
|
+
if (result.exceptionDetails) {
|
|
1231
|
+
const detail = result.exceptionDetails.exception?.description ?? result.exceptionDetails.exception?.value ?? result.exceptionDetails.text;
|
|
1232
|
+
throw new Error(`${detail}`);
|
|
1233
|
+
}
|
|
1234
|
+
return result.result?.value;
|
|
1235
|
+
}
|
|
1236
|
+
async $eval(selector, fn, ...args) {
|
|
1237
|
+
const fnBody = typeof fn === "function" ? fn.toString() : fn;
|
|
1238
|
+
return this.evaluate(
|
|
1239
|
+
`(function(sel, fnStr, ...evalArgs) {
|
|
1240
|
+
const el = document.querySelector(sel);
|
|
1241
|
+
if (!el) throw new Error('No element found for selector: ' + sel);
|
|
1242
|
+
const fn = new Function('return ' + fnStr)();
|
|
1243
|
+
return fn(el, ...evalArgs);
|
|
1244
|
+
})(${JSON.stringify(selector)}, ${JSON.stringify(fnBody)}${args.length > 0 ? ", " + args.map((a) => JSON.stringify(a)).join(", ") : ""})`
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
async $$eval(selector, fn, ...args) {
|
|
1248
|
+
const fnBody = typeof fn === "function" ? fn.toString() : fn;
|
|
1249
|
+
return this.evaluate(
|
|
1250
|
+
`(function(sel, fnStr, ...evalArgs) {
|
|
1251
|
+
const els = Array.from(document.querySelectorAll(sel));
|
|
1252
|
+
const fn = new Function('return ' + fnStr)();
|
|
1253
|
+
return fn(els, ...evalArgs);
|
|
1254
|
+
})(${JSON.stringify(selector)}, ${JSON.stringify(fnBody)}${args.length > 0 ? ", " + args.map((a) => JSON.stringify(a)).join(", ") : ""})`
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
// ── Locator ─────────────────────────────────────────────────
|
|
1258
|
+
locator(selector) {
|
|
1259
|
+
return new XBLocatorImpl(this, selector);
|
|
1260
|
+
}
|
|
1261
|
+
getByText(text, opts) {
|
|
1262
|
+
const escaped = text.replace(/'/g, "\\'");
|
|
1263
|
+
if (opts?.exact) {
|
|
1264
|
+
return this.locator(`xpath=//*[normalize-space(text())='${escaped}']`);
|
|
1265
|
+
}
|
|
1266
|
+
return this.locator(`xpath=//*[contains(text(),'${escaped}')]`);
|
|
1267
|
+
}
|
|
1268
|
+
getByRole(role, opts) {
|
|
1269
|
+
let sel = `[role="${role}"]`;
|
|
1270
|
+
if (opts?.name) {
|
|
1271
|
+
sel += opts.exact ? `[aria-label="${opts.name}"]` : `[aria-label*="${opts.name}"]`;
|
|
1272
|
+
}
|
|
1273
|
+
return this.locator(sel);
|
|
1274
|
+
}
|
|
1275
|
+
getByLabel(label, opts) {
|
|
1276
|
+
const escaped = label.replace(/'/g, "\\'");
|
|
1277
|
+
const sel = opts?.exact ? `xpath=//*[@aria-label='${escaped}' or @id=//label[normalize-space(text())='${escaped}']/@for]` : `xpath=//*[contains(@aria-label,'${escaped}') or @id=//label[contains(text(),'${escaped}')]/@for]`;
|
|
1278
|
+
return this.locator(sel);
|
|
1279
|
+
}
|
|
1280
|
+
getByPlaceholder(text, opts) {
|
|
1281
|
+
return this.locator(
|
|
1282
|
+
opts?.exact ? `[placeholder="${text}"]` : `[placeholder*="${text}"]`
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
getByTestId(id) {
|
|
1286
|
+
return this.locator(`[data-testid="${id}"]`);
|
|
1287
|
+
}
|
|
1288
|
+
getByAltText(text, opts) {
|
|
1289
|
+
return this.locator(opts?.exact ? `[alt="${text}"]` : `[alt*="${text}"]`);
|
|
1290
|
+
}
|
|
1291
|
+
getByTitle(title, opts) {
|
|
1292
|
+
return this.locator(
|
|
1293
|
+
opts?.exact ? `[title="${title}"]` : `[title*="${title}"]`
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
// ── Interaction shortcuts ───────────────────────────────────
|
|
1297
|
+
async click(selector, opts = {}) {
|
|
1298
|
+
await this.locator(selector).click(opts);
|
|
1299
|
+
}
|
|
1300
|
+
async dblclick(selector, opts = {}) {
|
|
1301
|
+
await this.locator(selector).click({ ...opts, clickCount: 2 });
|
|
1302
|
+
}
|
|
1303
|
+
async fill(selector, value, opts = {}) {
|
|
1304
|
+
await this.locator(selector).fill(value, opts);
|
|
1305
|
+
}
|
|
1306
|
+
async press(selector, key, opts) {
|
|
1307
|
+
await this.locator(selector).press(key, opts);
|
|
1308
|
+
}
|
|
1309
|
+
async hover(selector, opts) {
|
|
1310
|
+
await this.locator(selector).hover(opts);
|
|
1311
|
+
}
|
|
1312
|
+
async type(selector, text, opts = {}) {
|
|
1313
|
+
await this.locator(selector).type(text, opts);
|
|
1314
|
+
}
|
|
1315
|
+
async check(selector, opts) {
|
|
1316
|
+
await this.locator(selector).check(opts);
|
|
1317
|
+
}
|
|
1318
|
+
async uncheck(selector, opts) {
|
|
1319
|
+
await this.locator(selector).uncheck(opts);
|
|
1320
|
+
}
|
|
1321
|
+
async selectOption(selector, value) {
|
|
1322
|
+
return this.locator(selector).selectOption(value);
|
|
1323
|
+
}
|
|
1324
|
+
// ── Convenience selectors ───────────────────────────────────
|
|
1325
|
+
async textContent(selector) {
|
|
1326
|
+
return this.evaluate(
|
|
1327
|
+
`(function() { const el = document.querySelector(${JSON.stringify(selector)}); return el?.textContent ?? null; })()`
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
async innerText(selector) {
|
|
1331
|
+
return this.evaluate(
|
|
1332
|
+
`(function() { const el = document.querySelector(${JSON.stringify(selector)}); if (!el) throw new Error('Element not found'); return el.innerText; })()`
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
async innerHTML(selector) {
|
|
1336
|
+
return this.evaluate(
|
|
1337
|
+
`(function() { const el = document.querySelector(${JSON.stringify(selector)}); if (!el) throw new Error('Element not found'); return el.innerHTML; })()`
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
async getAttribute(selector, name) {
|
|
1341
|
+
return this.evaluate(
|
|
1342
|
+
`(function() { const el = document.querySelector(${JSON.stringify(selector)}); return el?.getAttribute(${JSON.stringify(name)}) ?? null; })()`
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
// ── Query ───────────────────────────────────────────────────
|
|
1346
|
+
async $(selector) {
|
|
1347
|
+
const nodeId = await this.querySelector(selector);
|
|
1348
|
+
if (!nodeId) return null;
|
|
1349
|
+
return new XBElementHandleImpl(this, nodeId);
|
|
1350
|
+
}
|
|
1351
|
+
async $$(selector) {
|
|
1352
|
+
const nodeIds = await this.querySelectorAll(selector);
|
|
1353
|
+
return nodeIds.map((id) => new XBElementHandleImpl(this, id));
|
|
1354
|
+
}
|
|
1355
|
+
// ── Screen ──────────────────────────────────────────────────
|
|
1356
|
+
async screenshot(opts = {}) {
|
|
1357
|
+
const params = {
|
|
1358
|
+
format: opts.type ?? "png"
|
|
1359
|
+
};
|
|
1360
|
+
if (opts.quality !== void 0 && (opts.type === "jpeg" || !opts.type && opts.quality)) {
|
|
1361
|
+
params.quality = opts.quality;
|
|
1362
|
+
}
|
|
1363
|
+
if (opts.fullPage) {
|
|
1364
|
+
params.captureBeyondViewport = true;
|
|
1365
|
+
}
|
|
1366
|
+
if (opts.clip) {
|
|
1367
|
+
const clip = {
|
|
1368
|
+
x: Math.round(opts.clip.x),
|
|
1369
|
+
y: Math.round(opts.clip.y),
|
|
1370
|
+
width: Math.round(Math.max(1, opts.clip.width)),
|
|
1371
|
+
height: Math.round(Math.max(1, opts.clip.height)),
|
|
1372
|
+
scale: 1
|
|
1373
|
+
};
|
|
1374
|
+
params.clip = clip;
|
|
1375
|
+
}
|
|
1376
|
+
if (opts.omitBackground) {
|
|
1377
|
+
params.omitBackground = true;
|
|
1378
|
+
}
|
|
1379
|
+
const result = await this.conn.send(
|
|
1380
|
+
"Page.captureScreenshot",
|
|
1381
|
+
params,
|
|
1382
|
+
this.sessionId
|
|
1383
|
+
);
|
|
1384
|
+
return Buffer.from(result.data, "base64");
|
|
1385
|
+
}
|
|
1386
|
+
async pdf(opts = {}) {
|
|
1387
|
+
const params = {};
|
|
1388
|
+
if (opts.landscape !== void 0) params.landscape = opts.landscape;
|
|
1389
|
+
if (opts.printBackground !== void 0) params.printBackground = opts.printBackground;
|
|
1390
|
+
if (opts.scale !== void 0) params.scale = opts.scale;
|
|
1391
|
+
if (opts.format) params.paperFormat = opts.format;
|
|
1392
|
+
if (opts.preferCSSPageSize !== void 0) params.preferCSSPageSize = opts.preferCSSPageSize;
|
|
1393
|
+
if (opts.margin) {
|
|
1394
|
+
if (opts.margin.top) params.marginTop = parseFloat(opts.margin.top);
|
|
1395
|
+
if (opts.margin.bottom) params.marginBottom = parseFloat(opts.margin.bottom);
|
|
1396
|
+
if (opts.margin.left) params.marginLeft = parseFloat(opts.margin.left);
|
|
1397
|
+
if (opts.margin.right) params.marginRight = parseFloat(opts.margin.right);
|
|
1398
|
+
}
|
|
1399
|
+
const result = await this.conn.send("Page.printToPDF", params, this.sessionId);
|
|
1400
|
+
return Buffer.from(result.data, "base64");
|
|
1401
|
+
}
|
|
1402
|
+
viewportSize() {
|
|
1403
|
+
return this._viewportSize ?? null;
|
|
1404
|
+
}
|
|
1405
|
+
async setViewportSize(size) {
|
|
1406
|
+
await this.conn.send(
|
|
1407
|
+
"Emulation.setDeviceMetricsOverride",
|
|
1408
|
+
{
|
|
1409
|
+
width: size.width,
|
|
1410
|
+
height: size.height,
|
|
1411
|
+
deviceScaleFactor: 1,
|
|
1412
|
+
mobile: false
|
|
1413
|
+
},
|
|
1414
|
+
this.sessionId
|
|
1415
|
+
);
|
|
1416
|
+
this._viewportSize = size;
|
|
1417
|
+
}
|
|
1418
|
+
// ── Scripts ─────────────────────────────────────────────────
|
|
1419
|
+
async addInitScript(script) {
|
|
1420
|
+
await this.conn.send(
|
|
1421
|
+
"Page.addScriptToEvaluateOnNewDocument",
|
|
1422
|
+
{ source: script },
|
|
1423
|
+
this.sessionId
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
/** Internal: set user agent */
|
|
1427
|
+
async _setUserAgent(userAgent) {
|
|
1428
|
+
await this.conn.send(
|
|
1429
|
+
"Network.setUserAgentOverride",
|
|
1430
|
+
{ userAgent },
|
|
1431
|
+
this.sessionId
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
/** Internal: set extra HTTP headers */
|
|
1435
|
+
async _setExtraHTTPHeaders(headers) {
|
|
1436
|
+
await this.setExtraHTTPHeaders(headers);
|
|
1437
|
+
}
|
|
1438
|
+
async bringToFront() {
|
|
1439
|
+
await this.conn.send("Page.bringToFront", void 0, this.sessionId);
|
|
1440
|
+
}
|
|
1441
|
+
async setExtraHTTPHeaders(headers) {
|
|
1442
|
+
await this.conn.send("Network.setExtraHTTPHeaders", { headers }, this.sessionId);
|
|
1443
|
+
}
|
|
1444
|
+
// ── Events ──────────────────────────────────────────────────
|
|
1445
|
+
on(event, handler) {
|
|
1446
|
+
this._emitter.on(event, handler);
|
|
1447
|
+
}
|
|
1448
|
+
off(event, handler) {
|
|
1449
|
+
this._emitter.off(event, handler);
|
|
1450
|
+
}
|
|
1451
|
+
// ── Lifecycle ───────────────────────────────────────────────
|
|
1452
|
+
async close() {
|
|
1453
|
+
if (this._closed) return;
|
|
1454
|
+
this._closed = true;
|
|
1455
|
+
if (this.networkIdleTimer) {
|
|
1456
|
+
clearTimeout(this.networkIdleTimer);
|
|
1457
|
+
this.networkIdleTimer = null;
|
|
1458
|
+
}
|
|
1459
|
+
for (const unsub of this._subscriptions) {
|
|
1460
|
+
unsub();
|
|
1461
|
+
}
|
|
1462
|
+
this._subscriptions = [];
|
|
1463
|
+
await this._browserImpl._closeTarget(this._targetId).catch(() => {
|
|
1464
|
+
});
|
|
1465
|
+
await this._browserImpl._detachFromTarget(this.sessionId).catch(() => {
|
|
1466
|
+
});
|
|
1467
|
+
this._emitter.emit("close");
|
|
1468
|
+
}
|
|
1469
|
+
isClosed() {
|
|
1470
|
+
return this._closed;
|
|
1471
|
+
}
|
|
1472
|
+
context() {
|
|
1473
|
+
return this._contextImpl;
|
|
1474
|
+
}
|
|
1475
|
+
browser() {
|
|
1476
|
+
return this._browserImpl;
|
|
1477
|
+
}
|
|
1478
|
+
mainFrame() {
|
|
1479
|
+
return {
|
|
1480
|
+
url: () => this._url,
|
|
1481
|
+
name: () => "",
|
|
1482
|
+
isDetached: () => this._closed,
|
|
1483
|
+
page: () => this,
|
|
1484
|
+
evaluate: (fn, ...args) => this.evaluate(fn, ...args),
|
|
1485
|
+
$: (sel) => this.$(sel),
|
|
1486
|
+
$$: (sel) => this.$$(sel)
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
frames() {
|
|
1490
|
+
return [this.mainFrame()];
|
|
1491
|
+
}
|
|
1492
|
+
// ── CDP helpers exposed for locator/element ─────────────────
|
|
1493
|
+
/** Query a single element, returns CDP nodeId or 0 if not found */
|
|
1494
|
+
async querySelector(selector) {
|
|
1495
|
+
const doc = await this.conn.send(
|
|
1496
|
+
"DOM.getDocument",
|
|
1497
|
+
{ depth: 0 },
|
|
1498
|
+
this.sessionId
|
|
1499
|
+
);
|
|
1500
|
+
const result = await this.conn.send(
|
|
1501
|
+
"DOM.querySelector",
|
|
1502
|
+
{ nodeId: doc.root.nodeId, selector },
|
|
1503
|
+
this.sessionId
|
|
1504
|
+
);
|
|
1505
|
+
return result.nodeId;
|
|
1506
|
+
}
|
|
1507
|
+
/** Query all matching elements, returns array of CDP nodeIds */
|
|
1508
|
+
async querySelectorAll(selector) {
|
|
1509
|
+
const doc = await this.conn.send(
|
|
1510
|
+
"DOM.getDocument",
|
|
1511
|
+
{ depth: 0 },
|
|
1512
|
+
this.sessionId
|
|
1513
|
+
);
|
|
1514
|
+
const result = await this.conn.send(
|
|
1515
|
+
"DOM.querySelectorAll",
|
|
1516
|
+
{ nodeId: doc.root.nodeId, selector },
|
|
1517
|
+
this.sessionId
|
|
1518
|
+
);
|
|
1519
|
+
return result.nodeIds ?? [];
|
|
1520
|
+
}
|
|
1521
|
+
/** Resolve a CDP nodeId to a RemoteObject for evaluate */
|
|
1522
|
+
async resolveNode(nodeId) {
|
|
1523
|
+
const result = await this.conn.send(
|
|
1524
|
+
"DOM.resolveNode",
|
|
1525
|
+
{ nodeId },
|
|
1526
|
+
this.sessionId
|
|
1527
|
+
);
|
|
1528
|
+
return result.object.objectId;
|
|
1529
|
+
}
|
|
1530
|
+
/** Get the box model for a nodeId */
|
|
1531
|
+
async getBoxModel(nodeId) {
|
|
1532
|
+
try {
|
|
1533
|
+
const result = await this.conn.send("DOM.getBoxModel", { nodeId }, this.sessionId);
|
|
1534
|
+
const c = result.model?.content;
|
|
1535
|
+
if (!c || c.length < 8) return null;
|
|
1536
|
+
const x1 = c[0];
|
|
1537
|
+
const y1 = c[1];
|
|
1538
|
+
const x2 = c[4];
|
|
1539
|
+
const y2 = c[5];
|
|
1540
|
+
return {
|
|
1541
|
+
x: Math.min(x1, x2),
|
|
1542
|
+
y: Math.min(y1, y2),
|
|
1543
|
+
width: Math.abs(x2 - x1),
|
|
1544
|
+
height: Math.abs(y2 - y1)
|
|
1545
|
+
};
|
|
1546
|
+
} catch {
|
|
1547
|
+
return null;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
/** Call a function on a RemoteObject */
|
|
1551
|
+
async callFunctionOn(objectId, functionDeclaration, args = []) {
|
|
1552
|
+
const result = await this.conn.send("Runtime.callFunctionOn", {
|
|
1553
|
+
objectId,
|
|
1554
|
+
functionDeclaration,
|
|
1555
|
+
arguments: args.map((a) => ({ value: a })),
|
|
1556
|
+
returnByValue: true
|
|
1557
|
+
}, this.sessionId);
|
|
1558
|
+
if (result.exceptionDetails) {
|
|
1559
|
+
throw new Error(`CallFunctionOn error: ${result.exceptionDetails.text}`);
|
|
1560
|
+
}
|
|
1561
|
+
return result.result?.value;
|
|
1562
|
+
}
|
|
1563
|
+
/** Send a CDP command scoped to this page's session */
|
|
1564
|
+
async _cdpSend(method, params) {
|
|
1565
|
+
return this.conn.send(method, params, this.sessionId);
|
|
1566
|
+
}
|
|
1567
|
+
/** Subscribe to a CDP event on this page's session. Returns unsubscribe function. */
|
|
1568
|
+
_subscribe(event, handler) {
|
|
1569
|
+
return this.conn.subscribe(event, this.sessionId, handler);
|
|
1570
|
+
}
|
|
1571
|
+
// ── Private: Event Setup ────────────────────────────────────
|
|
1572
|
+
setupPageEvents() {
|
|
1573
|
+
this._subscriptions.push(
|
|
1574
|
+
this.conn.subscribe("Page.frameNavigated", this.sessionId, (params) => {
|
|
1575
|
+
const p = params;
|
|
1576
|
+
if (p.frame) {
|
|
1577
|
+
this._url = p.frame.url;
|
|
1578
|
+
}
|
|
1579
|
+
this._emit("framenavigated", this.mainFrame());
|
|
1580
|
+
})
|
|
1581
|
+
);
|
|
1582
|
+
this._subscriptions.push(
|
|
1583
|
+
this.conn.subscribe("Page.loadEventFired", this.sessionId, () => {
|
|
1584
|
+
this._loadState.loadFired = true;
|
|
1585
|
+
})
|
|
1586
|
+
);
|
|
1587
|
+
this._subscriptions.push(
|
|
1588
|
+
this.conn.subscribe("Page.domContentEventFired", this.sessionId, () => {
|
|
1589
|
+
this._loadState.domContentFired = true;
|
|
1590
|
+
})
|
|
1591
|
+
);
|
|
1592
|
+
this._subscriptions.push(
|
|
1593
|
+
this.conn.subscribe("Page.javascriptDialogOpening", this.sessionId, (params) => {
|
|
1594
|
+
const p = params;
|
|
1595
|
+
const dialog = {
|
|
1596
|
+
type: p.type,
|
|
1597
|
+
message: () => p.message,
|
|
1598
|
+
defaultValue: () => p.defaultValue,
|
|
1599
|
+
accept: async (text) => {
|
|
1600
|
+
await this.conn.send("Page.handleJavaScriptDialog", {
|
|
1601
|
+
accept: true,
|
|
1602
|
+
promptText: text
|
|
1603
|
+
}, this.sessionId);
|
|
1604
|
+
},
|
|
1605
|
+
dismiss: async () => {
|
|
1606
|
+
await this.conn.send("Page.handleJavaScriptDialog", {
|
|
1607
|
+
accept: false
|
|
1608
|
+
}, this.sessionId);
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
this._emit("dialog", dialog);
|
|
1612
|
+
})
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
setupNetworkEvents() {
|
|
1616
|
+
this._subscriptions.push(
|
|
1617
|
+
this.conn.subscribe("Network.requestWillBeSent", this.sessionId, (params) => {
|
|
1618
|
+
const p = params;
|
|
1619
|
+
this.inflightRequests.add(p.requestId);
|
|
1620
|
+
this._storeNetworkRequest(p.requestId, {
|
|
1621
|
+
url: p.request.url,
|
|
1622
|
+
method: p.request.method,
|
|
1623
|
+
headers: p.request.headers,
|
|
1624
|
+
postData: p.request.postData ?? null,
|
|
1625
|
+
resourceType: p.type
|
|
1626
|
+
});
|
|
1627
|
+
this._emit("request", p);
|
|
1628
|
+
this.checkNetworkIdle();
|
|
1629
|
+
})
|
|
1630
|
+
);
|
|
1631
|
+
this._subscriptions.push(
|
|
1632
|
+
this.conn.subscribe("Network.responseReceived", this.sessionId, (params) => {
|
|
1633
|
+
const p = params;
|
|
1634
|
+
this._storeNetworkResponse(p.requestId, {
|
|
1635
|
+
status: p.response.status,
|
|
1636
|
+
url: p.response.url,
|
|
1637
|
+
headers: p.response.headers
|
|
1638
|
+
});
|
|
1639
|
+
this._emit("response", p);
|
|
1640
|
+
})
|
|
1641
|
+
);
|
|
1642
|
+
this._subscriptions.push(
|
|
1643
|
+
this.conn.subscribe("Network.loadingFinished", this.sessionId, (params) => {
|
|
1644
|
+
const p = params;
|
|
1645
|
+
this.inflightRequests.delete(p.requestId);
|
|
1646
|
+
this._emit("requestfinished", p);
|
|
1647
|
+
this.checkNetworkIdle();
|
|
1648
|
+
})
|
|
1649
|
+
);
|
|
1650
|
+
this._subscriptions.push(
|
|
1651
|
+
this.conn.subscribe("Network.loadingFailed", this.sessionId, (params) => {
|
|
1652
|
+
const p = params;
|
|
1653
|
+
this.inflightRequests.delete(p.requestId);
|
|
1654
|
+
this.checkNetworkIdle();
|
|
1655
|
+
})
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
setupConsoleEvents() {
|
|
1659
|
+
this._subscriptions.push(
|
|
1660
|
+
this.conn.subscribe("Runtime.consoleAPICalled", this.sessionId, (params) => {
|
|
1661
|
+
const p = params;
|
|
1662
|
+
const text = p.args.map((a) => {
|
|
1663
|
+
if (a.value !== void 0) return String(a.value);
|
|
1664
|
+
return a.description ?? "";
|
|
1665
|
+
}).join(" ");
|
|
1666
|
+
const location = p.stackTrace?.callFrames?.[0] ? {
|
|
1667
|
+
url: p.stackTrace.callFrames[0].url,
|
|
1668
|
+
lineNumber: p.stackTrace.callFrames[0].lineNumber,
|
|
1669
|
+
columnNumber: p.stackTrace.callFrames[0].columnNumber
|
|
1670
|
+
} : { url: "", lineNumber: 0, columnNumber: 0 };
|
|
1671
|
+
const msg = {
|
|
1672
|
+
type: () => p.type,
|
|
1673
|
+
text: () => text,
|
|
1674
|
+
location: () => location
|
|
1675
|
+
};
|
|
1676
|
+
this._emit("console", msg);
|
|
1677
|
+
})
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
checkNetworkIdle() {
|
|
1681
|
+
if (this.inflightRequests.size === 0) {
|
|
1682
|
+
if (this.networkIdleTimer) clearTimeout(this.networkIdleTimer);
|
|
1683
|
+
this.networkIdleTimer = setTimeout(() => {
|
|
1684
|
+
if (this.inflightRequests.size === 0) {
|
|
1685
|
+
this._loadState.networkIdle = true;
|
|
1686
|
+
if (this.networkIdleResolve) {
|
|
1687
|
+
this.networkIdleResolve();
|
|
1688
|
+
this.networkIdleResolve = null;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}, _XBPageImpl.NETWORK_IDLE_MS);
|
|
1692
|
+
} else {
|
|
1693
|
+
if (this.networkIdleTimer) {
|
|
1694
|
+
clearTimeout(this.networkIdleTimer);
|
|
1695
|
+
this.networkIdleTimer = null;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
// ── Network Data Store (for waitForResponse/waitForRequest) ──
|
|
1700
|
+
_networkResponses = /* @__PURE__ */ new Map();
|
|
1701
|
+
_networkRequests = /* @__PURE__ */ new Map();
|
|
1702
|
+
_routeHandlers = [];
|
|
1703
|
+
_interceptionEnabled = false;
|
|
1704
|
+
/** Store network data — called by browser.ts installNetworkCapture or internal event handlers */
|
|
1705
|
+
_storeNetworkRequest(requestId, data) {
|
|
1706
|
+
this._networkRequests.set(requestId, { requestId, ...data });
|
|
1707
|
+
}
|
|
1708
|
+
_storeNetworkResponse(requestId, data) {
|
|
1709
|
+
this._networkResponses.set(requestId, { requestId, ...data });
|
|
1710
|
+
}
|
|
1711
|
+
// ── waitForResponse ─────────────────────────────────────────
|
|
1712
|
+
async waitForResponse(urlOrPredicate, opts = {}) {
|
|
1713
|
+
const timeout = opts.timeout ?? 3e4;
|
|
1714
|
+
const predicate = createResponsePredicate(urlOrPredicate);
|
|
1715
|
+
for (const [, data] of this._networkResponses) {
|
|
1716
|
+
const response = createXBResponse(data, this.conn, this.sessionId);
|
|
1717
|
+
if (predicate(response)) return response;
|
|
1718
|
+
}
|
|
1719
|
+
return new Promise((resolve, reject) => {
|
|
1720
|
+
const timer = setTimeout(() => {
|
|
1721
|
+
this._emitter.removeListener("response", handler);
|
|
1722
|
+
reject(new Error(`waitForResponse timed out after ${timeout}ms`));
|
|
1723
|
+
}, timeout);
|
|
1724
|
+
const handler = (params) => {
|
|
1725
|
+
const p = params;
|
|
1726
|
+
const data = {
|
|
1727
|
+
requestId: p.requestId,
|
|
1728
|
+
status: p.response.status,
|
|
1729
|
+
url: p.response.url,
|
|
1730
|
+
headers: p.response.headers
|
|
1731
|
+
};
|
|
1732
|
+
const response = createXBResponse(data, this.conn, this.sessionId);
|
|
1733
|
+
if (predicate(response)) {
|
|
1734
|
+
clearTimeout(timer);
|
|
1735
|
+
this._emitter.removeListener("response", handler);
|
|
1736
|
+
resolve(response);
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1739
|
+
this._emitter.on("response", handler);
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
// ── waitForRequest ──────────────────────────────────────────
|
|
1743
|
+
async waitForRequest(urlOrPredicate, opts = {}) {
|
|
1744
|
+
const timeout = opts.timeout ?? 3e4;
|
|
1745
|
+
const predicate = createRequestPredicate(urlOrPredicate);
|
|
1746
|
+
for (const [, data] of this._networkRequests) {
|
|
1747
|
+
const request = createXBRequest(this, data);
|
|
1748
|
+
if (predicate(request)) return request;
|
|
1749
|
+
}
|
|
1750
|
+
return new Promise((resolve, reject) => {
|
|
1751
|
+
const timer = setTimeout(() => {
|
|
1752
|
+
this._emitter.removeListener("request", handler);
|
|
1753
|
+
reject(new Error(`waitForRequest timed out after ${timeout}ms`));
|
|
1754
|
+
}, timeout);
|
|
1755
|
+
const handler = (params) => {
|
|
1756
|
+
const p = params;
|
|
1757
|
+
const data = {
|
|
1758
|
+
requestId: p.requestId,
|
|
1759
|
+
url: p.request.url,
|
|
1760
|
+
method: p.request.method,
|
|
1761
|
+
headers: p.request.headers,
|
|
1762
|
+
postData: p.request.postData ?? null,
|
|
1763
|
+
resourceType: p.type
|
|
1764
|
+
};
|
|
1765
|
+
const request = createXBRequest(this, data);
|
|
1766
|
+
if (predicate(request)) {
|
|
1767
|
+
clearTimeout(timer);
|
|
1768
|
+
this._emitter.removeListener("request", handler);
|
|
1769
|
+
resolve(request);
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
this._emitter.on("request", handler);
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
// ── waitForURL ──────────────────────────────────────────────
|
|
1776
|
+
async waitForURL(url, opts = {}) {
|
|
1777
|
+
const timeout = opts.timeout ?? 3e4;
|
|
1778
|
+
const checkFn = typeof url === "function" ? url : typeof url === "string" ? (current) => matchGlob(url, current) : (current) => url.test(current);
|
|
1779
|
+
if (checkFn(this._url)) return;
|
|
1780
|
+
return new Promise((resolve, reject) => {
|
|
1781
|
+
const timer = setTimeout(() => {
|
|
1782
|
+
this._emitter.removeListener("framenavigated", handler);
|
|
1783
|
+
reject(new Error(`waitForURL timed out after ${timeout}ms`));
|
|
1784
|
+
}, timeout);
|
|
1785
|
+
const handler = () => {
|
|
1786
|
+
if (checkFn(this._url)) {
|
|
1787
|
+
clearTimeout(timer);
|
|
1788
|
+
this._emitter.removeListener("framenavigated", handler);
|
|
1789
|
+
resolve();
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
this._emitter.on("framenavigated", handler);
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
// ── route / unroute ─────────────────────────────────────────
|
|
1796
|
+
async route(url, handler) {
|
|
1797
|
+
const regex = typeof url === "string" ? globToRegex(url) : url;
|
|
1798
|
+
this._routeHandlers.push({ pattern: String(url), regex, handler });
|
|
1799
|
+
if (!this._interceptionEnabled) {
|
|
1800
|
+
this._interceptionEnabled = true;
|
|
1801
|
+
await this.conn.send("Fetch.enable", {
|
|
1802
|
+
patterns: [{ urlPattern: "*", requestStage: "Request" }]
|
|
1803
|
+
}, this.sessionId);
|
|
1804
|
+
this._subscriptions.push(
|
|
1805
|
+
this.conn.subscribe("Fetch.requestPaused", this.sessionId, (params) => {
|
|
1806
|
+
this._handleRequestPaused(params);
|
|
1807
|
+
})
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
async unroute(url, handler) {
|
|
1812
|
+
const regex = typeof url === "string" ? globToRegex(url) : url;
|
|
1813
|
+
this._routeHandlers = this._routeHandlers.filter(
|
|
1814
|
+
(h) => !(regex.source === h.regex.source && (!handler || h.handler === handler))
|
|
1815
|
+
);
|
|
1816
|
+
if (this._routeHandlers.length === 0 && this._interceptionEnabled) {
|
|
1817
|
+
this._interceptionEnabled = false;
|
|
1818
|
+
await this.conn.send("Fetch.disable", void 0, this.sessionId).catch(() => {
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
async _handleRequestPaused(params) {
|
|
1823
|
+
const requestUrl = params.request.url;
|
|
1824
|
+
for (const { regex, handler } of this._routeHandlers) {
|
|
1825
|
+
if (regex.test(requestUrl)) {
|
|
1826
|
+
const route = createXBRouteFetch(this.conn, this.sessionId, params);
|
|
1827
|
+
try {
|
|
1828
|
+
await handler(route);
|
|
1829
|
+
} catch {
|
|
1830
|
+
await this.conn.send("Fetch.continueRequest", {
|
|
1831
|
+
requestId: params.requestId
|
|
1832
|
+
}, this.sessionId).catch(() => {
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
await this.conn.send("Fetch.continueRequest", {
|
|
1839
|
+
requestId: params.requestId
|
|
1840
|
+
}, this.sessionId).catch(() => {
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
// ── setInputFiles ───────────────────────────────────────────
|
|
1844
|
+
async setInputFiles(selector, files) {
|
|
1845
|
+
const fileArr = Array.isArray(files) ? files : [files];
|
|
1846
|
+
const fileList = fileArr.map((f) => ({
|
|
1847
|
+
name: f.name,
|
|
1848
|
+
type: f.mimeType,
|
|
1849
|
+
dataBase64: f.buffer.toString("base64")
|
|
1850
|
+
}));
|
|
1851
|
+
await this.evaluate(`
|
|
1852
|
+
(function() {
|
|
1853
|
+
var selector = ${JSON.stringify(selector)};
|
|
1854
|
+
var input = document.querySelector(selector);
|
|
1855
|
+
if (!input) throw new Error('Element not found: ' + selector);
|
|
1856
|
+
|
|
1857
|
+
var fileList = ${JSON.stringify(fileList)};
|
|
1858
|
+
var dt = new DataTransfer();
|
|
1859
|
+
|
|
1860
|
+
fileList.forEach(function(f) {
|
|
1861
|
+
var binary = atob(f.dataBase64);
|
|
1862
|
+
var bytes = new Uint8Array(binary.length);
|
|
1863
|
+
for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
1864
|
+
var blob = new Blob([bytes], { type: f.type });
|
|
1865
|
+
var file = new File([blob], f.name, { type: f.type });
|
|
1866
|
+
dt.items.add(file);
|
|
1867
|
+
});
|
|
1868
|
+
|
|
1869
|
+
input.files = dt.files;
|
|
1870
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1871
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1872
|
+
})()
|
|
1873
|
+
`);
|
|
1874
|
+
}
|
|
1875
|
+
// ── dragAndDrop ─────────────────────────────────────────────
|
|
1876
|
+
async dragAndDrop(source, target) {
|
|
1877
|
+
const sourceRect = await this.evaluate(`
|
|
1878
|
+
(function() {
|
|
1879
|
+
const el = document.querySelector(${JSON.stringify(source)});
|
|
1880
|
+
if (!el) throw new Error('Source not found: ${source}');
|
|
1881
|
+
el.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
1882
|
+
const r = el.getBoundingClientRect();
|
|
1883
|
+
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
1884
|
+
})()
|
|
1885
|
+
`);
|
|
1886
|
+
const targetRect = await this.evaluate(`
|
|
1887
|
+
(function() {
|
|
1888
|
+
const el = document.querySelector(${JSON.stringify(target)});
|
|
1889
|
+
if (!el) throw new Error('Target not found: ${target}');
|
|
1890
|
+
el.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
1891
|
+
const r = el.getBoundingClientRect();
|
|
1892
|
+
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
1893
|
+
})()
|
|
1894
|
+
`);
|
|
1895
|
+
const sx = sourceRect.x + sourceRect.width / 2;
|
|
1896
|
+
const sy = sourceRect.y + sourceRect.height / 2;
|
|
1897
|
+
const tx = targetRect.x + targetRect.width / 2;
|
|
1898
|
+
const ty = targetRect.y + targetRect.height / 2;
|
|
1899
|
+
try {
|
|
1900
|
+
await this.conn.send("Input.dispatchDragEvent", {
|
|
1901
|
+
type: "dragStart",
|
|
1902
|
+
x: sx,
|
|
1903
|
+
y: sy,
|
|
1904
|
+
data: { items: [], dragOperations: ["copy", "move", "link"] }
|
|
1905
|
+
}, this.sessionId);
|
|
1906
|
+
await this.conn.send("Input.dispatchDragEvent", {
|
|
1907
|
+
type: "dragOver",
|
|
1908
|
+
x: tx,
|
|
1909
|
+
y: ty,
|
|
1910
|
+
data: { items: [], dragOperations: ["copy", "move", "link"] }
|
|
1911
|
+
}, this.sessionId);
|
|
1912
|
+
await this.conn.send("Input.dispatchDragEvent", {
|
|
1913
|
+
type: "drop",
|
|
1914
|
+
x: tx,
|
|
1915
|
+
y: ty,
|
|
1916
|
+
data: { items: [], dragOperations: ["copy", "move", "link"] }
|
|
1917
|
+
}, this.sessionId);
|
|
1918
|
+
await this.conn.send("Input.dispatchDragEvent", {
|
|
1919
|
+
type: "dragCancel",
|
|
1920
|
+
x: sx,
|
|
1921
|
+
y: sy,
|
|
1922
|
+
data: { items: [], dragOperations: ["copy", "move", "link"] }
|
|
1923
|
+
}, this.sessionId);
|
|
1924
|
+
} catch {
|
|
1925
|
+
await this.mouse.move(sx, sy);
|
|
1926
|
+
await this.mouse.down();
|
|
1927
|
+
await this.mouse.move(tx, ty, { steps: 10 });
|
|
1928
|
+
await this.mouse.up();
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
// ── setOfflineMode ──────────────────────────────────────────
|
|
1932
|
+
async setOfflineMode(offline) {
|
|
1933
|
+
await this.conn.send("Network.emulateNetworkConditions", {
|
|
1934
|
+
offline,
|
|
1935
|
+
latency: 0,
|
|
1936
|
+
downloadThroughput: offline ? 0 : -1,
|
|
1937
|
+
uploadThroughput: offline ? 0 : -1
|
|
1938
|
+
}, this.sessionId);
|
|
1939
|
+
}
|
|
1940
|
+
};
|
|
1941
|
+
|
|
1942
|
+
// src/cdp-driver/cdp-session.ts
|
|
1943
|
+
var XBCDPSessionImpl = class {
|
|
1944
|
+
conn;
|
|
1945
|
+
sessionId;
|
|
1946
|
+
constructor(conn, sessionId) {
|
|
1947
|
+
this.conn = conn;
|
|
1948
|
+
this.sessionId = sessionId;
|
|
1949
|
+
}
|
|
1950
|
+
async send(method, params) {
|
|
1951
|
+
return this.conn.send(method, params, this.sessionId);
|
|
1952
|
+
}
|
|
1953
|
+
on(event, handler) {
|
|
1954
|
+
this.conn.on(event, (params, sid) => {
|
|
1955
|
+
if (sid === this.sessionId || !this.sessionId && !sid) {
|
|
1956
|
+
handler(params);
|
|
1957
|
+
}
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
off(event, handler) {
|
|
1961
|
+
this.conn.off(event, handler);
|
|
1962
|
+
}
|
|
1963
|
+
async detach() {
|
|
1964
|
+
if (!this.sessionId) return;
|
|
1965
|
+
}
|
|
1966
|
+
};
|
|
1967
|
+
|
|
1968
|
+
// src/cdp-driver/context.ts
|
|
1969
|
+
var XBContextImpl = class {
|
|
1970
|
+
conn;
|
|
1971
|
+
_emitter = new EventEmitter2();
|
|
1972
|
+
_browser;
|
|
1973
|
+
contextId;
|
|
1974
|
+
_pages = [];
|
|
1975
|
+
closed = false;
|
|
1976
|
+
options;
|
|
1977
|
+
targetAttachedHandler = null;
|
|
1978
|
+
_initScripts = [];
|
|
1979
|
+
constructor(conn, contextId, browser, opts) {
|
|
1980
|
+
this.conn = conn;
|
|
1981
|
+
this.contextId = contextId;
|
|
1982
|
+
this._browser = browser;
|
|
1983
|
+
this.options = opts;
|
|
1984
|
+
this.setupAutoAttach();
|
|
1985
|
+
}
|
|
1986
|
+
async newPage() {
|
|
1987
|
+
if (this.closed) throw new Error("Context is closed");
|
|
1988
|
+
const { targetId } = await this._browser._createTarget(this.contextId);
|
|
1989
|
+
const sessionId = await this._browser._attachToTarget(targetId);
|
|
1990
|
+
const page = new XBPageImpl(this.conn, sessionId, targetId, this, this._browser);
|
|
1991
|
+
await page._init();
|
|
1992
|
+
this._pages.push(page);
|
|
1993
|
+
if (this.options.viewport) {
|
|
1994
|
+
await page.setViewportSize(this.options.viewport).catch(() => {
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
if (this.options.userAgent) {
|
|
1998
|
+
await page._setUserAgent(this.options.userAgent);
|
|
1999
|
+
}
|
|
2000
|
+
if (this.options.extraHTTPHeaders) {
|
|
2001
|
+
await page._setExtraHTTPHeaders(this.options.extraHTTPHeaders);
|
|
2002
|
+
}
|
|
2003
|
+
for (const script of this._initScripts) {
|
|
2004
|
+
await page.addInitScript(script);
|
|
2005
|
+
}
|
|
2006
|
+
return page;
|
|
2007
|
+
}
|
|
2008
|
+
pages() {
|
|
2009
|
+
return [...this._pages];
|
|
2010
|
+
}
|
|
2011
|
+
browser() {
|
|
2012
|
+
return this._browser;
|
|
2013
|
+
}
|
|
2014
|
+
async close() {
|
|
2015
|
+
if (this.closed) return;
|
|
2016
|
+
this.closed = true;
|
|
2017
|
+
for (const page of this._pages) {
|
|
2018
|
+
await page.close().catch(() => {
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
this._pages = [];
|
|
2022
|
+
if (this.targetAttachedHandler) {
|
|
2023
|
+
this.conn.off("Target.attachedToTarget", this.targetAttachedHandler);
|
|
2024
|
+
this.targetAttachedHandler = null;
|
|
2025
|
+
}
|
|
2026
|
+
if (this.contextId && this.contextId !== "default") {
|
|
2027
|
+
await this.conn.send("Target.disposeBrowserContext", {
|
|
2028
|
+
browserContextId: this.contextId
|
|
2029
|
+
}).catch(() => {
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
this._browser._removeContext(this.contextId);
|
|
2033
|
+
}
|
|
2034
|
+
async newCDPSession(_page) {
|
|
2035
|
+
return new XBCDPSessionImpl(this.conn);
|
|
2036
|
+
}
|
|
2037
|
+
async addInitScript(script) {
|
|
2038
|
+
this._initScripts.push(script);
|
|
2039
|
+
for (const page of this._pages) {
|
|
2040
|
+
await page.addInitScript(script);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
// ── Cookies ─────────────────────────────────────────────────
|
|
2044
|
+
async cookies(urls) {
|
|
2045
|
+
const urlList = typeof urls === "string" ? [urls] : urls;
|
|
2046
|
+
const result = await this.conn.send("Network.getCookies", urlList ? { urls: urlList } : void 0);
|
|
2047
|
+
return result.cookies;
|
|
2048
|
+
}
|
|
2049
|
+
async addCookies(cookies) {
|
|
2050
|
+
const cdpCookies = cookies.map((c) => ({
|
|
2051
|
+
name: c.name,
|
|
2052
|
+
value: c.value,
|
|
2053
|
+
domain: c.domain,
|
|
2054
|
+
path: c.path || "/",
|
|
2055
|
+
expires: c.expires,
|
|
2056
|
+
httpOnly: c.httpOnly,
|
|
2057
|
+
secure: c.secure,
|
|
2058
|
+
sameSite: c.sameSite
|
|
2059
|
+
}));
|
|
2060
|
+
await this.conn.send("Network.setCookies", { cookies: cdpCookies });
|
|
2061
|
+
}
|
|
2062
|
+
async clearCookies() {
|
|
2063
|
+
await this.conn.send("Network.clearBrowserCookies");
|
|
2064
|
+
}
|
|
2065
|
+
on(event, handler) {
|
|
2066
|
+
this._emitter.on(event, handler);
|
|
2067
|
+
}
|
|
2068
|
+
off(event, handler) {
|
|
2069
|
+
this._emitter.off(event, handler);
|
|
2070
|
+
}
|
|
2071
|
+
// ── Private ─────────────────────────────────────────────────
|
|
2072
|
+
setupAutoAttach() {
|
|
2073
|
+
this.targetAttachedHandler = (paramsRaw) => {
|
|
2074
|
+
const params = paramsRaw;
|
|
2075
|
+
if (this.contextId !== "default" && params.targetInfo.browserContextId !== this.contextId) return;
|
|
2076
|
+
if (params.targetInfo.type !== "page") return;
|
|
2077
|
+
const exists = this._pages.some(
|
|
2078
|
+
(p) => p._targetId === params.targetInfo.targetId
|
|
2079
|
+
);
|
|
2080
|
+
if (exists) return;
|
|
2081
|
+
const page = new XBPageImpl(
|
|
2082
|
+
this.conn,
|
|
2083
|
+
params.sessionId,
|
|
2084
|
+
params.targetInfo.targetId,
|
|
2085
|
+
this,
|
|
2086
|
+
this._browser
|
|
2087
|
+
);
|
|
2088
|
+
this.conn.send("Runtime.runIfWaitingForDebugger", void 0, params.sessionId).catch(() => {
|
|
2089
|
+
});
|
|
2090
|
+
page._init().then(async () => {
|
|
2091
|
+
for (const script of this._initScripts) {
|
|
2092
|
+
await page.addInitScript(script).catch(() => {
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
this._pages.push(page);
|
|
2096
|
+
this._emitter.emit("page", page);
|
|
2097
|
+
});
|
|
2098
|
+
};
|
|
2099
|
+
this.conn.on("Target.attachedToTarget", this.targetAttachedHandler);
|
|
2100
|
+
}
|
|
2101
|
+
};
|
|
2102
|
+
|
|
2103
|
+
// src/cdp-driver/browser.ts
|
|
2104
|
+
var XBBrowserImpl = class {
|
|
2105
|
+
conn;
|
|
2106
|
+
_emitter = new EventEmitter3();
|
|
2107
|
+
_contexts = /* @__PURE__ */ new Map();
|
|
2108
|
+
_disconnected = false;
|
|
2109
|
+
childProcess = null;
|
|
2110
|
+
tmpDir;
|
|
2111
|
+
_exitHandler = null;
|
|
2112
|
+
constructor(conn, childProcess, tmpDir) {
|
|
2113
|
+
this.conn = conn;
|
|
2114
|
+
this.childProcess = childProcess ?? null;
|
|
2115
|
+
this.tmpDir = tmpDir;
|
|
2116
|
+
conn.on("disconnect", () => {
|
|
2117
|
+
this._disconnected = true;
|
|
2118
|
+
this._emitter.emit("disconnected");
|
|
2119
|
+
});
|
|
2120
|
+
if (this.childProcess) {
|
|
2121
|
+
this._exitHandler = () => {
|
|
2122
|
+
try {
|
|
2123
|
+
if (this.childProcess?.exitCode === null) {
|
|
2124
|
+
this.childProcess.kill("SIGKILL");
|
|
2125
|
+
}
|
|
2126
|
+
} catch {
|
|
2127
|
+
}
|
|
2128
|
+
if (this.tmpDir) {
|
|
2129
|
+
try {
|
|
2130
|
+
const { rmSync } = __require("fs");
|
|
2131
|
+
rmSync(this.tmpDir, { recursive: true, force: true });
|
|
2132
|
+
} catch {
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
};
|
|
2136
|
+
process.on("exit", this._exitHandler);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
get disconnected() {
|
|
2140
|
+
return this._disconnected;
|
|
2141
|
+
}
|
|
2142
|
+
/** The underlying CDP connection (for advanced use) */
|
|
2143
|
+
get connection() {
|
|
2144
|
+
return this.conn;
|
|
2145
|
+
}
|
|
2146
|
+
async close() {
|
|
2147
|
+
if (this._disconnected) return;
|
|
2148
|
+
this._disconnected = true;
|
|
2149
|
+
for (const [, info] of this._contexts) {
|
|
2150
|
+
await info.context.close().catch(() => {
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
this._contexts.clear();
|
|
2154
|
+
if (this._exitHandler) {
|
|
2155
|
+
process.removeListener("exit", this._exitHandler);
|
|
2156
|
+
this._exitHandler = null;
|
|
2157
|
+
}
|
|
2158
|
+
if (this.childProcess) {
|
|
2159
|
+
const { killChrome: killChrome2 } = await import("./launcher-KA7J32K5.js");
|
|
2160
|
+
await killChrome2(this.childProcess, this.tmpDir);
|
|
2161
|
+
}
|
|
2162
|
+
await this.conn.close();
|
|
2163
|
+
this._emitter.emit("disconnected");
|
|
2164
|
+
}
|
|
2165
|
+
async newContext(opts = {}) {
|
|
2166
|
+
if (this._disconnected) {
|
|
2167
|
+
throw new Error("Browser is disconnected");
|
|
2168
|
+
}
|
|
2169
|
+
let contextId = "default";
|
|
2170
|
+
try {
|
|
2171
|
+
const result = await this.conn.send(
|
|
2172
|
+
"Target.createBrowserContext",
|
|
2173
|
+
{ disposeOnDetach: true },
|
|
2174
|
+
void 0,
|
|
2175
|
+
1e4
|
|
2176
|
+
// 10s timeout instead of default 30s
|
|
2177
|
+
);
|
|
2178
|
+
contextId = result.browserContextId;
|
|
2179
|
+
} catch {
|
|
2180
|
+
}
|
|
2181
|
+
const context = new XBContextImpl(this.conn, contextId, this, opts);
|
|
2182
|
+
context.on("page", (page) => {
|
|
2183
|
+
this._emitter.emit("page", page);
|
|
2184
|
+
});
|
|
2185
|
+
this._contexts.set(contextId, {
|
|
2186
|
+
contextId,
|
|
2187
|
+
context
|
|
2188
|
+
});
|
|
2189
|
+
return context;
|
|
2190
|
+
}
|
|
2191
|
+
contexts() {
|
|
2192
|
+
return Array.from(this._contexts.values()).map((info) => info.context);
|
|
2193
|
+
}
|
|
2194
|
+
on(event, handler) {
|
|
2195
|
+
this._emitter.on(event, handler);
|
|
2196
|
+
}
|
|
2197
|
+
off(event, handler) {
|
|
2198
|
+
this._emitter.off(event, handler);
|
|
2199
|
+
}
|
|
2200
|
+
/** Called by context.close() to remove from registry */
|
|
2201
|
+
_removeContext(contextId) {
|
|
2202
|
+
this._contexts.delete(contextId);
|
|
2203
|
+
}
|
|
2204
|
+
// ── CDP helpers exposed for context/page ────────────────────
|
|
2205
|
+
/** Attach to a target and get a session ID for flat protocol */
|
|
2206
|
+
async _attachToTarget(targetId) {
|
|
2207
|
+
const result = await this.conn.send(
|
|
2208
|
+
"Target.attachToTarget",
|
|
2209
|
+
{ targetId, flatten: true }
|
|
2210
|
+
);
|
|
2211
|
+
return result.sessionId;
|
|
2212
|
+
}
|
|
2213
|
+
/** Detach from a target session */
|
|
2214
|
+
async _detachFromTarget(sessionId) {
|
|
2215
|
+
await this.conn.send("Target.detachFromTarget", { sessionId });
|
|
2216
|
+
}
|
|
2217
|
+
/** Create a new page target within a browser context */
|
|
2218
|
+
async _createTarget(contextId, url = "about:blank") {
|
|
2219
|
+
const params = { url };
|
|
2220
|
+
if (contextId && contextId !== "default") {
|
|
2221
|
+
params.browserContextId = contextId;
|
|
2222
|
+
}
|
|
2223
|
+
return this.conn.send("Target.createTarget", params);
|
|
2224
|
+
}
|
|
2225
|
+
/** Close a target */
|
|
2226
|
+
async _closeTarget(targetId) {
|
|
2227
|
+
await this.conn.send("Target.closeTarget", { targetId });
|
|
2228
|
+
}
|
|
2229
|
+
/** Enable auto-attach for new targets */
|
|
2230
|
+
async _enableAutoAttach() {
|
|
2231
|
+
await this.conn.send("Target.setAutoAttach", {
|
|
2232
|
+
autoAttach: true,
|
|
2233
|
+
waitForDebuggerOnStart: true,
|
|
2234
|
+
flatten: true
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
};
|
|
2238
|
+
|
|
2239
|
+
// src/cdp-driver/connection.ts
|
|
2240
|
+
import { EventEmitter as EventEmitter4 } from "events";
|
|
2241
|
+
import { WebSocket } from "ws";
|
|
2242
|
+
var CDPConnection = class extends EventEmitter4 {
|
|
2243
|
+
ws;
|
|
2244
|
+
nextId = 1;
|
|
2245
|
+
pending = /* @__PURE__ */ new Map();
|
|
2246
|
+
closed = false;
|
|
2247
|
+
closeReason = null;
|
|
2248
|
+
/** Default session ID for flat session protocol (Target.attachToTarget) */
|
|
2249
|
+
defaultSessionId;
|
|
2250
|
+
constructor(wsOrUrl, sessionId) {
|
|
2251
|
+
super();
|
|
2252
|
+
this.defaultSessionId = sessionId;
|
|
2253
|
+
if (typeof wsOrUrl === "string") {
|
|
2254
|
+
this.ws = new WebSocket(wsOrUrl);
|
|
2255
|
+
} else {
|
|
2256
|
+
this.ws = wsOrUrl;
|
|
2257
|
+
}
|
|
2258
|
+
this.bindWebSocket();
|
|
2259
|
+
}
|
|
2260
|
+
/** Wait for the connection to be fully open */
|
|
2261
|
+
async ready() {
|
|
2262
|
+
if (this.ws.readyState === WebSocket.OPEN) return;
|
|
2263
|
+
if (this.ws.readyState === WebSocket.CLOSED || this.ws.readyState === WebSocket.CLOSING) {
|
|
2264
|
+
throw new Error(`WebSocket already closed: ${this.closeReason ?? "unknown"}`);
|
|
2265
|
+
}
|
|
2266
|
+
return new Promise((resolve, reject) => {
|
|
2267
|
+
const onOpen = () => {
|
|
2268
|
+
this.ws.off("error", onError);
|
|
2269
|
+
resolve();
|
|
2270
|
+
};
|
|
2271
|
+
const onError = (err) => {
|
|
2272
|
+
this.ws.off("open", onOpen);
|
|
2273
|
+
reject(err);
|
|
2274
|
+
};
|
|
2275
|
+
this.ws.once("open", onOpen);
|
|
2276
|
+
this.ws.once("error", onError);
|
|
2277
|
+
});
|
|
2278
|
+
}
|
|
2279
|
+
/** Is the underlying WebSocket alive? */
|
|
2280
|
+
get isOpen() {
|
|
2281
|
+
return !this.closed && this.ws.readyState === WebSocket.OPEN;
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Send a CDP command and await its response.
|
|
2285
|
+
*
|
|
2286
|
+
* @param method — CDP domain.method (e.g. "Page.navigate")
|
|
2287
|
+
* @param params — method parameters
|
|
2288
|
+
* @param sessionId — optional flat session ID for sub-targets
|
|
2289
|
+
* @param timeoutMs — response timeout (default: 30s)
|
|
2290
|
+
* @returns the `result` field from the CDP response
|
|
2291
|
+
*/
|
|
2292
|
+
async send(method, params, sessionId, timeoutMs = 3e4) {
|
|
2293
|
+
if (this.closed) {
|
|
2294
|
+
throw new Error(`CDP connection closed: ${this.closeReason ?? "unknown"}`);
|
|
2295
|
+
}
|
|
2296
|
+
if (!this.isOpen) {
|
|
2297
|
+
throw new Error(`CDP connection not open (state: ${this.ws.readyState})`);
|
|
2298
|
+
}
|
|
2299
|
+
const id = this.nextId++;
|
|
2300
|
+
const sid = sessionId ?? this.defaultSessionId;
|
|
2301
|
+
const message = { id, method };
|
|
2302
|
+
if (params !== void 0) message.params = params;
|
|
2303
|
+
if (sid !== void 0) message.sessionId = sid;
|
|
2304
|
+
return new Promise((resolve, reject) => {
|
|
2305
|
+
const timeout = setTimeout(() => {
|
|
2306
|
+
this.pending.delete(id);
|
|
2307
|
+
reject(new Error(`CDP timeout: ${method} (${timeoutMs}ms)`));
|
|
2308
|
+
}, timeoutMs);
|
|
2309
|
+
this.pending.set(id, {
|
|
2310
|
+
resolve: (v) => {
|
|
2311
|
+
clearTimeout(timeout);
|
|
2312
|
+
this.pending.delete(id);
|
|
2313
|
+
resolve(v);
|
|
2314
|
+
},
|
|
2315
|
+
reject: (err) => {
|
|
2316
|
+
clearTimeout(timeout);
|
|
2317
|
+
this.pending.delete(id);
|
|
2318
|
+
reject(err);
|
|
2319
|
+
},
|
|
2320
|
+
method,
|
|
2321
|
+
timeout
|
|
2322
|
+
});
|
|
2323
|
+
const data = JSON.stringify(message);
|
|
2324
|
+
try {
|
|
2325
|
+
this.ws.send(data);
|
|
2326
|
+
} catch (err) {
|
|
2327
|
+
clearTimeout(timeout);
|
|
2328
|
+
this.pending.delete(id);
|
|
2329
|
+
reject(new Error(`CDP send failed: ${method} \u2014 ${err instanceof Error ? err.message : String(err)}`));
|
|
2330
|
+
}
|
|
2331
|
+
});
|
|
2332
|
+
}
|
|
2333
|
+
/**
|
|
2334
|
+
* Subscribe to a CDP event.
|
|
2335
|
+
*
|
|
2336
|
+
* @param event — full event name (e.g. "Page.frameNavigated")
|
|
2337
|
+
* @param handler — called with the event params
|
|
2338
|
+
* @param sessionId — optional session filter
|
|
2339
|
+
*/
|
|
2340
|
+
on(event, handler) {
|
|
2341
|
+
return super.on(event, handler);
|
|
2342
|
+
}
|
|
2343
|
+
once(event, handler) {
|
|
2344
|
+
return super.once(event, handler);
|
|
2345
|
+
}
|
|
2346
|
+
/** Remove an event listener */
|
|
2347
|
+
off(event, handler) {
|
|
2348
|
+
super.off(event, handler);
|
|
2349
|
+
return this;
|
|
2350
|
+
}
|
|
2351
|
+
/**
|
|
2352
|
+
* Subscribe to a CDP event for a specific session.
|
|
2353
|
+
* Returns an unsubscribe function.
|
|
2354
|
+
*/
|
|
2355
|
+
subscribe(event, sessionId, handler) {
|
|
2356
|
+
const wrapper = (params, sid) => {
|
|
2357
|
+
if (sid === sessionId || !sessionId && !sid) handler(params);
|
|
2358
|
+
};
|
|
2359
|
+
this.on(event, wrapper);
|
|
2360
|
+
return () => this.off(event, wrapper);
|
|
2361
|
+
}
|
|
2362
|
+
/** Close the WebSocket */
|
|
2363
|
+
async close() {
|
|
2364
|
+
if (this.closed) return;
|
|
2365
|
+
this.closed = true;
|
|
2366
|
+
this.closeReason = "closed by caller";
|
|
2367
|
+
for (const [id, pending] of this.pending) {
|
|
2368
|
+
clearTimeout(pending.timeout);
|
|
2369
|
+
pending.reject(new Error(`Connection closed: ${pending.method}`));
|
|
2370
|
+
this.pending.delete(id);
|
|
2371
|
+
}
|
|
2372
|
+
if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
|
|
2373
|
+
this.ws.close(1e3, "normal closure");
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
/** Set the default session ID for flat protocol */
|
|
2377
|
+
setDefaultSessionId(sid) {
|
|
2378
|
+
this.defaultSessionId = sid;
|
|
2379
|
+
}
|
|
2380
|
+
// ── Private ─────────────────────────────────────────────────
|
|
2381
|
+
bindWebSocket() {
|
|
2382
|
+
this.ws.on("message", (raw) => {
|
|
2383
|
+
let msg;
|
|
2384
|
+
try {
|
|
2385
|
+
msg = JSON.parse(raw.toString());
|
|
2386
|
+
} catch {
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
2389
|
+
if (msg.id !== void 0) {
|
|
2390
|
+
const pending = this.pending.get(msg.id);
|
|
2391
|
+
if (!pending) return;
|
|
2392
|
+
if (msg.error) {
|
|
2393
|
+
pending.reject(new CDPProtocolError(msg.error.code, msg.error.message, pending.method));
|
|
2394
|
+
} else {
|
|
2395
|
+
pending.resolve(msg.result ?? {});
|
|
2396
|
+
}
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
if (msg.method) {
|
|
2400
|
+
this.emit(msg.method, msg.params ?? {}, msg.sessionId);
|
|
2401
|
+
this.emit("*", msg.method, msg.params ?? {}, msg.sessionId);
|
|
2402
|
+
}
|
|
2403
|
+
});
|
|
2404
|
+
this.ws.on("close", (code, reason) => {
|
|
2405
|
+
if (this.closed) return;
|
|
2406
|
+
this.closed = true;
|
|
2407
|
+
this.closeReason = `WebSocket closed: ${code} ${reason?.toString() ?? ""}`.trim();
|
|
2408
|
+
for (const [id, pending] of this.pending) {
|
|
2409
|
+
clearTimeout(pending.timeout);
|
|
2410
|
+
pending.reject(new Error(`Connection closed: ${pending.method}`));
|
|
2411
|
+
this.pending.delete(id);
|
|
2412
|
+
}
|
|
2413
|
+
this.emit("disconnect");
|
|
2414
|
+
});
|
|
2415
|
+
this.ws.on("error", (err) => {
|
|
2416
|
+
if (this.closed) return;
|
|
2417
|
+
this.emit("ws-error", err);
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
};
|
|
2421
|
+
var CDPProtocolError = class extends Error {
|
|
2422
|
+
code;
|
|
2423
|
+
method;
|
|
2424
|
+
data;
|
|
2425
|
+
constructor(code, message, method, data) {
|
|
2426
|
+
super(`CDP error [${code}] in ${method}: ${message}`);
|
|
2427
|
+
this.name = "CDPProtocolError";
|
|
2428
|
+
this.code = code;
|
|
2429
|
+
this.method = method;
|
|
2430
|
+
this.data = data;
|
|
2431
|
+
}
|
|
2432
|
+
};
|
|
2433
|
+
|
|
2434
|
+
// src/cdp-driver/index.ts
|
|
2435
|
+
async function launch(options = {}) {
|
|
2436
|
+
let wsEndpoint;
|
|
2437
|
+
let childProcess;
|
|
2438
|
+
let tmpDir;
|
|
2439
|
+
if (options.cdpEndpoint) {
|
|
2440
|
+
wsEndpoint = await connectToCDP(options.cdpEndpoint);
|
|
2441
|
+
} else {
|
|
2442
|
+
const result = await launchChrome({
|
|
2443
|
+
executablePath: options.executablePath,
|
|
2444
|
+
headless: options.headless,
|
|
2445
|
+
args: options.args,
|
|
2446
|
+
userDataDir: options.userDataDir,
|
|
2447
|
+
timeout: options.timeout,
|
|
2448
|
+
env: options.env
|
|
2449
|
+
});
|
|
2450
|
+
wsEndpoint = result.wsEndpoint;
|
|
2451
|
+
childProcess = result.process;
|
|
2452
|
+
tmpDir = result.tmpDir;
|
|
2453
|
+
}
|
|
2454
|
+
const conn = new CDPConnection(wsEndpoint);
|
|
2455
|
+
await conn.ready();
|
|
2456
|
+
const browser = new XBBrowserImpl(conn, childProcess, tmpDir);
|
|
2457
|
+
return { browser, wsEndpoint };
|
|
2458
|
+
}
|
|
7
2459
|
|
|
8
2460
|
// src/cdp-interceptor/proxy.ts
|
|
9
|
-
import { WebSocketServer, WebSocket } from "ws";
|
|
2461
|
+
import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
|
|
10
2462
|
|
|
11
2463
|
// src/cdp-interceptor/rules/shared.ts
|
|
12
2464
|
var PLAYWRIGHT_INTERNAL_MARKERS = [
|
|
@@ -1243,9 +3695,9 @@ var CDPInterceptorProxy = class {
|
|
|
1243
3695
|
let browserWs = null;
|
|
1244
3696
|
let isAlive = true;
|
|
1245
3697
|
const pendingMessages = [];
|
|
1246
|
-
browserWs = new
|
|
3698
|
+
browserWs = new WebSocket2(this.config.cdpEndpoint);
|
|
1247
3699
|
clientWs.on("message", (raw) => {
|
|
1248
|
-
if (browserWs && browserWs.readyState ===
|
|
3700
|
+
if (browserWs && browserWs.readyState === WebSocket2.OPEN) {
|
|
1249
3701
|
this.handleClientMessage(clientWs, browserWs, raw);
|
|
1250
3702
|
} else {
|
|
1251
3703
|
pendingMessages.push(raw);
|
|
@@ -1261,7 +3713,7 @@ var CDPInterceptorProxy = class {
|
|
|
1261
3713
|
this.logger.info("Browser WebSocket error", { error: String(err) });
|
|
1262
3714
|
});
|
|
1263
3715
|
browserWs.on("close", (code, reason) => {
|
|
1264
|
-
if (isAlive && clientWs.readyState ===
|
|
3716
|
+
if (isAlive && clientWs.readyState === WebSocket2.OPEN) {
|
|
1265
3717
|
this.logger.info("Browser WS closed, closing client", { code, reason: String(reason) });
|
|
1266
3718
|
clientWs.close();
|
|
1267
3719
|
}
|
|
@@ -1271,14 +3723,14 @@ var CDPInterceptorProxy = class {
|
|
|
1271
3723
|
});
|
|
1272
3724
|
const cleanup = () => {
|
|
1273
3725
|
isAlive = false;
|
|
1274
|
-
if (browserWs && browserWs.readyState ===
|
|
3726
|
+
if (browserWs && browserWs.readyState === WebSocket2.OPEN) {
|
|
1275
3727
|
browserWs.close();
|
|
1276
3728
|
}
|
|
1277
3729
|
};
|
|
1278
3730
|
clientWs.on("close", cleanup);
|
|
1279
3731
|
clientWs.on("error", cleanup);
|
|
1280
3732
|
browserWs.on("close", () => {
|
|
1281
|
-
if (isAlive && clientWs.readyState ===
|
|
3733
|
+
if (isAlive && clientWs.readyState === WebSocket2.OPEN) {
|
|
1282
3734
|
clientWs.close();
|
|
1283
3735
|
}
|
|
1284
3736
|
});
|
|
@@ -1501,7 +3953,7 @@ process.on("exit", () => {
|
|
|
1501
3953
|
}
|
|
1502
3954
|
sessions.clear();
|
|
1503
3955
|
});
|
|
1504
|
-
async function
|
|
3956
|
+
async function getCDPTargets2(cdpEndpoint) {
|
|
1505
3957
|
try {
|
|
1506
3958
|
const ep = String(cdpEndpoint);
|
|
1507
3959
|
let host = "localhost";
|
|
@@ -1521,7 +3973,7 @@ async function getCDPTargets(cdpEndpoint) {
|
|
|
1521
3973
|
}
|
|
1522
3974
|
}
|
|
1523
3975
|
async function findTargetPage(cdpEndpoint, target) {
|
|
1524
|
-
const targets = await
|
|
3976
|
+
const targets = await getCDPTargets2(cdpEndpoint);
|
|
1525
3977
|
const pages = targets.filter((t) => t.url && !t.url.startsWith("about:blank") && !t.url.startsWith("chrome://"));
|
|
1526
3978
|
const byId = pages.find((t) => t.id === target);
|
|
1527
3979
|
if (byId) return { pageId: byId.id, wsUrl: byId.webSocketDebuggerUrl, title: byId.title, url: byId.url };
|
|
@@ -1556,15 +4008,18 @@ async function createBrowser(options) {
|
|
|
1556
4008
|
const realEndpoint = await resolveCDPEndpoint(options.cdpEndpoint);
|
|
1557
4009
|
if (options.intercept) {
|
|
1558
4010
|
const config = typeof options.intercept === "object" ? { ...options.intercept, cdpEndpoint: realEndpoint } : { cdpEndpoint: realEndpoint };
|
|
1559
|
-
|
|
1560
|
-
const proxyPort = await
|
|
4011
|
+
_sharedCdpProxy = new CDPInterceptorProxy(config);
|
|
4012
|
+
const proxyPort = await _sharedCdpProxy.start();
|
|
1561
4013
|
console.error(`[CDP Interceptor] Proxy running on ws://localhost:${proxyPort}, forwarding to ${realEndpoint}`);
|
|
1562
|
-
|
|
4014
|
+
const { browser: browser3 } = await launch({ cdpEndpoint: `ws://localhost:${proxyPort}` });
|
|
4015
|
+
return browser3;
|
|
1563
4016
|
}
|
|
1564
|
-
|
|
4017
|
+
const { browser: browser2 } = await launch({ cdpEndpoint: realEndpoint });
|
|
4018
|
+
return browser2;
|
|
1565
4019
|
}
|
|
1566
4020
|
const executablePath = options?.executablePath || process.env.XBROWSER_CHROMIUM_PATH || discoverChromiumPath();
|
|
1567
|
-
|
|
4021
|
+
const { browser } = await launch({ executablePath, headless: options?.headless ?? true });
|
|
4022
|
+
return browser;
|
|
1568
4023
|
}
|
|
1569
4024
|
async function getBrowser(options) {
|
|
1570
4025
|
if (_sharedBrowser) return _sharedBrowser;
|
|
@@ -1656,7 +4111,7 @@ async function findOrRestoreSession(name, cdpEndpoint) {
|
|
|
1656
4111
|
}
|
|
1657
4112
|
page = page || fallbackPage;
|
|
1658
4113
|
if (!page) {
|
|
1659
|
-
const targets = await
|
|
4114
|
+
const targets = await getCDPTargets2(ep);
|
|
1660
4115
|
const matchTarget = targets.find(
|
|
1661
4116
|
(t) => t.url && t.url !== "about:blank" && !t.url.startsWith("chrome://") && (targetHostname ? t.url.includes(targetHostname) : true)
|
|
1662
4117
|
);
|
|
@@ -1715,7 +4170,7 @@ async function findOrRestoreSession(name, cdpEndpoint) {
|
|
|
1715
4170
|
async function createEphemeralContext(options) {
|
|
1716
4171
|
if (options?.cdpEndpoint) {
|
|
1717
4172
|
const endpoint = await resolveCDPEndpoint(options.cdpEndpoint);
|
|
1718
|
-
const b2 = await
|
|
4173
|
+
const { browser: b2 } = await launch({ cdpEndpoint: endpoint });
|
|
1719
4174
|
const contexts = b2.contexts();
|
|
1720
4175
|
const ctx = contexts[0] || await b2.newContext();
|
|
1721
4176
|
const page2 = await ctx.newPage();
|
|
@@ -1754,34 +4209,55 @@ function getAllSessions() {
|
|
|
1754
4209
|
}
|
|
1755
4210
|
async function installNetworkCapture(page, sessionName) {
|
|
1756
4211
|
if (process.env.XBROWSER_DAEMON_WORKER !== "1") return;
|
|
1757
|
-
const { networkStore } = await import("./network-store-
|
|
1758
|
-
|
|
4212
|
+
const { networkStore } = await import("./network-store-BN6QEZ7R.js");
|
|
4213
|
+
const requestData = /* @__PURE__ */ new Map();
|
|
4214
|
+
const responseMeta = /* @__PURE__ */ new Map();
|
|
4215
|
+
const xbPage = page;
|
|
4216
|
+
xbPage.on("request", (params) => {
|
|
1759
4217
|
try {
|
|
1760
|
-
const
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
4218
|
+
const p = params;
|
|
4219
|
+
requestData.set(p.requestId, {
|
|
4220
|
+
method: p.request.method,
|
|
4221
|
+
headers: p.request.headers,
|
|
4222
|
+
postData: p.request.postData ?? null,
|
|
4223
|
+
resourceType: p.type
|
|
4224
|
+
});
|
|
4225
|
+
} catch {
|
|
4226
|
+
}
|
|
4227
|
+
});
|
|
4228
|
+
xbPage.on("response", (params) => {
|
|
4229
|
+
try {
|
|
4230
|
+
const p = params;
|
|
4231
|
+
responseMeta.set(p.requestId, {
|
|
4232
|
+
status: p.response.status,
|
|
4233
|
+
url: p.response.url,
|
|
4234
|
+
headers: p.response.headers,
|
|
4235
|
+
mimeType: p.response.mimeType,
|
|
4236
|
+
type: p.type
|
|
4237
|
+
});
|
|
4238
|
+
} catch {
|
|
4239
|
+
}
|
|
4240
|
+
});
|
|
4241
|
+
xbPage.on("requestfinished", async (params) => {
|
|
4242
|
+
try {
|
|
4243
|
+
const p = params;
|
|
4244
|
+
const meta = responseMeta.get(p.requestId);
|
|
4245
|
+
if (!meta) return;
|
|
4246
|
+
const req = requestData.get(p.requestId);
|
|
4247
|
+
const method = req?.method ?? "GET";
|
|
4248
|
+
const contentType = meta.headers["content-type"] || meta.headers["Content-Type"] || "";
|
|
4249
|
+
const resourceType = req?.resourceType ?? meta.type;
|
|
4250
|
+
const requestHeaders = req?.headers ?? {};
|
|
1771
4251
|
let requestBody = void 0;
|
|
1772
|
-
const method = request.method();
|
|
1773
4252
|
const isPostLike = ["POST", "PATCH", "PUT"].includes(method);
|
|
1774
4253
|
if (isPostLike && requestHeaders["content-type"]?.includes("application/json")) {
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
requestBody = postData;
|
|
1782
|
-
}
|
|
4254
|
+
const postData = req?.postData;
|
|
4255
|
+
if (postData) {
|
|
4256
|
+
try {
|
|
4257
|
+
requestBody = JSON.parse(postData);
|
|
4258
|
+
} catch {
|
|
4259
|
+
requestBody = postData;
|
|
1783
4260
|
}
|
|
1784
|
-
} catch {
|
|
1785
4261
|
}
|
|
1786
4262
|
}
|
|
1787
4263
|
let responseBody = void 0;
|
|
@@ -1789,7 +4265,11 @@ async function installNetworkCapture(page, sessionName) {
|
|
|
1789
4265
|
const isJsonish = contentType.includes("json") || contentType.includes("javascript") || contentType.includes("text/");
|
|
1790
4266
|
if (isJsonish) {
|
|
1791
4267
|
try {
|
|
1792
|
-
const
|
|
4268
|
+
const bodyResult = await xbPage._cdpSend(
|
|
4269
|
+
"Network.getResponseBody",
|
|
4270
|
+
{ requestId: p.requestId }
|
|
4271
|
+
);
|
|
4272
|
+
const text = bodyResult.body ?? "";
|
|
1793
4273
|
size = text.length;
|
|
1794
4274
|
if (size <= 10240) {
|
|
1795
4275
|
try {
|
|
@@ -1802,8 +4282,11 @@ async function installNetworkCapture(page, sessionName) {
|
|
|
1802
4282
|
}
|
|
1803
4283
|
} else {
|
|
1804
4284
|
try {
|
|
1805
|
-
const
|
|
1806
|
-
|
|
4285
|
+
const bodyResult = await xbPage._cdpSend(
|
|
4286
|
+
"Network.getResponseBody",
|
|
4287
|
+
{ requestId: p.requestId }
|
|
4288
|
+
);
|
|
4289
|
+
size = bodyResult.body?.length ?? 0;
|
|
1807
4290
|
} catch {
|
|
1808
4291
|
size = 0;
|
|
1809
4292
|
}
|
|
@@ -1811,17 +4294,19 @@ async function installNetworkCapture(page, sessionName) {
|
|
|
1811
4294
|
networkStore.add(sessionName, {
|
|
1812
4295
|
timestamp: Date.now(),
|
|
1813
4296
|
method,
|
|
1814
|
-
url,
|
|
1815
|
-
path: new URL(url).pathname,
|
|
1816
|
-
status:
|
|
4297
|
+
url: meta.url,
|
|
4298
|
+
path: new URL(meta.url).pathname,
|
|
4299
|
+
status: meta.status,
|
|
1817
4300
|
contentType,
|
|
1818
4301
|
size,
|
|
1819
|
-
headers,
|
|
4302
|
+
headers: meta.headers,
|
|
1820
4303
|
body: responseBody,
|
|
1821
4304
|
requestHeaders,
|
|
1822
4305
|
requestBody,
|
|
1823
|
-
resourceType
|
|
4306
|
+
resourceType
|
|
1824
4307
|
});
|
|
4308
|
+
requestData.delete(p.requestId);
|
|
4309
|
+
responseMeta.delete(p.requestId);
|
|
1825
4310
|
} catch {
|
|
1826
4311
|
}
|
|
1827
4312
|
});
|
|
@@ -1857,7 +4342,7 @@ async function createSession(name, url, options) {
|
|
|
1857
4342
|
if (targetPage) break;
|
|
1858
4343
|
}
|
|
1859
4344
|
if (!targetPage && options?.cdpEndpoint) {
|
|
1860
|
-
const targets = await
|
|
4345
|
+
const targets = await getCDPTargets2(options.cdpEndpoint);
|
|
1861
4346
|
const matchTarget = targets.find(
|
|
1862
4347
|
(t) => t.url && t.url !== "about:blank" && !t.url.startsWith("chrome://") && (url ? t.url.includes(new URL(url).hostname) : true)
|
|
1863
4348
|
);
|
|
@@ -1928,13 +4413,13 @@ async function closeSessionByName(name) {
|
|
|
1928
4413
|
} catch {
|
|
1929
4414
|
}
|
|
1930
4415
|
try {
|
|
1931
|
-
const { networkStore, commandLogStore } = await import("./network-store-
|
|
4416
|
+
const { networkStore, commandLogStore } = await import("./network-store-BN6QEZ7R.js");
|
|
1932
4417
|
networkStore.clear(session.name);
|
|
1933
4418
|
commandLogStore.clear(session.name);
|
|
1934
4419
|
} catch {
|
|
1935
4420
|
}
|
|
1936
4421
|
try {
|
|
1937
|
-
const { SessionRecorder } = await import("./session-recorder-
|
|
4422
|
+
const { SessionRecorder } = await import("./session-recorder-MA75PKTQ.js");
|
|
1938
4423
|
SessionRecorder.cleanup(session.name);
|
|
1939
4424
|
} catch {
|
|
1940
4425
|
}
|