@xbrowser/cli 1.0.0 → 1.0.3
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/README.md +17 -26
- package/dist/{browser-DSVV4GHS.js → browser-5CTOA2WS.js} +4 -3
- package/dist/{browser-53KUFEEM.js → browser-ITLZZDHJ.js} +5 -5
- package/dist/{browser-GURRY444.js → browser-IUJXXNBT.js} +6 -3
- package/dist/{cdp-driver-MNPR3HZH.js → cdp-driver-4X3DK6PS.js} +339 -59
- package/dist/{cdp-driver-SSXUGXP6.js → cdp-driver-D6WMSMWX.js} +4 -3
- package/dist/chunk-2SVQTI2O.js +2794 -0
- package/dist/{chunk-IDVD44ED.js → chunk-6WOSXSCQ.js} +23 -7
- package/dist/{chunk-ZZ2TFWIV.js → chunk-ABXMBNQ6.js} +1 -1
- package/dist/{chunk-2MFXKN32.js → chunk-ACFE6PKF.js} +1013 -119
- package/dist/chunk-AMI64BSD.js +268 -0
- package/dist/{chunk-E4O5ZU3H.js → chunk-DKWR54XQ.js} +412 -98
- package/dist/{chunk-DTJRVA76.js → chunk-ETCO4SNK.js} +2 -2
- package/dist/chunk-GDKLH7ZY.js +8 -0
- package/dist/chunk-KFQGP6VL.js +33 -0
- package/dist/{chunk-2BQZIT3S.js → chunk-LRBSUKUZ.js} +85 -2497
- package/dist/{chunk-ITKPSIP7.js → chunk-MDAPTB7C.js} +6 -25
- package/dist/{chunk-42RPMJ76.js → chunk-N2JFPWMI.js} +342 -60
- package/dist/chunk-OZKD3W4X.js +417 -0
- package/dist/{chunk-T4J4C2NZ.js → chunk-TNEN6VQ2.js} +17 -4
- package/dist/{chunk-YKOHDEFV.js → chunk-TWWOIJM7.js} +74 -38
- package/dist/chunk-WJRE55TN.js +83 -0
- package/dist/cli.js +1558 -1122
- package/dist/{convert-EGFYNICZ.js → convert-LB3GJTLR.js} +3 -3
- package/dist/{convert-EKQVHKB4.js → convert-R3XXYKC6.js} +2 -2
- package/dist/{daemon-client-YAVQ343A.js → daemon-client-3JOKX2L2.js} +3 -2
- package/dist/{daemon-client-3VM7VU7O.js → daemon-client-DIEHGP5B.js} +28 -74
- package/dist/daemon-main.js +2296 -1722
- package/dist/{extract-JUOQQX4V.js → extract-2ZFW2MX7.js} +1 -1
- package/dist/{extract-L2IW3IUB.js → extract-BSYBM4MR.js} +1 -1
- package/dist/{filter-HC4RA7JY.js → filter-KCFO4RSV.js} +1 -1
- package/dist/{filter-VID2GGZ7.js → filter-T7DSZ2X7.js} +1 -1
- package/dist/{human-interaction-W753RVJB.js → human-interaction-UKAS5ZXV.js} +2 -2
- package/dist/index.d.ts +166 -109
- package/dist/index.js +2668 -1742
- package/dist/launcher-L2JNDB2H.js +20 -0
- package/dist/{launcher-KA7J32K5.js → launcher-OZXJQPNG.js} +1 -1
- package/dist/{network-store-66A2RATI.js → network-store-XGZ25FFC.js} +1 -1
- package/dist/{network-store-BN6QEZ7R.js → network-store-YVDNUREI.js} +1 -1
- package/dist/{parse-action-dsl-T3DYC33D.js → parse-action-dsl-UM333TL2.js} +1 -1
- package/dist/{proxy-WKGUCH2C.js → proxy-C6CK3UH5.js} +2 -2
- package/dist/session-recorder-RTDGURIJ.js +8 -0
- package/dist/session-recorder-YI7YYM36.js +7 -0
- package/dist/session-replayer-MY27H4DX.js +276 -0
- package/dist/site-knowledge-SYC6VCDB.js +23 -0
- package/package.json +5 -4
- package/dist/screenshot-CWAWMXVA.js +0 -28
- package/dist/session-recorder-MA75PKTQ.js +0 -7
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from "./chunk-T4J4C2NZ.js";
|
|
2
|
+
launch
|
|
3
|
+
} from "./chunk-N2JFPWMI.js";
|
|
5
4
|
import {
|
|
6
|
-
|
|
7
|
-
} from "./chunk-
|
|
5
|
+
errMsg
|
|
6
|
+
} from "./chunk-GDKLH7ZY.js";
|
|
8
7
|
|
|
9
8
|
// src/browser.ts
|
|
10
9
|
import { randomUUID } from "crypto";
|
|
@@ -12,2453 +11,8 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFile
|
|
|
12
11
|
import { join } from "path";
|
|
13
12
|
import { homedir } from "os";
|
|
14
13
|
|
|
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
|
-
}
|
|
2459
|
-
|
|
2460
14
|
// src/cdp-interceptor/proxy.ts
|
|
2461
|
-
import { WebSocketServer, WebSocket
|
|
15
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
2462
16
|
|
|
2463
17
|
// src/cdp-interceptor/rules/shared.ts
|
|
2464
18
|
var PLAYWRIGHT_INTERNAL_MARKERS = [
|
|
@@ -3695,9 +1249,9 @@ var CDPInterceptorProxy = class {
|
|
|
3695
1249
|
let browserWs = null;
|
|
3696
1250
|
let isAlive = true;
|
|
3697
1251
|
const pendingMessages = [];
|
|
3698
|
-
browserWs = new
|
|
1252
|
+
browserWs = new WebSocket(this.config.cdpEndpoint);
|
|
3699
1253
|
clientWs.on("message", (raw) => {
|
|
3700
|
-
if (browserWs && browserWs.readyState ===
|
|
1254
|
+
if (browserWs && browserWs.readyState === WebSocket.OPEN) {
|
|
3701
1255
|
this.handleClientMessage(clientWs, browserWs, raw);
|
|
3702
1256
|
} else {
|
|
3703
1257
|
pendingMessages.push(raw);
|
|
@@ -3713,7 +1267,7 @@ var CDPInterceptorProxy = class {
|
|
|
3713
1267
|
this.logger.info("Browser WebSocket error", { error: String(err) });
|
|
3714
1268
|
});
|
|
3715
1269
|
browserWs.on("close", (code, reason) => {
|
|
3716
|
-
if (isAlive && clientWs.readyState ===
|
|
1270
|
+
if (isAlive && clientWs.readyState === WebSocket.OPEN) {
|
|
3717
1271
|
this.logger.info("Browser WS closed, closing client", { code, reason: String(reason) });
|
|
3718
1272
|
clientWs.close();
|
|
3719
1273
|
}
|
|
@@ -3723,14 +1277,14 @@ var CDPInterceptorProxy = class {
|
|
|
3723
1277
|
});
|
|
3724
1278
|
const cleanup = () => {
|
|
3725
1279
|
isAlive = false;
|
|
3726
|
-
if (browserWs && browserWs.readyState ===
|
|
1280
|
+
if (browserWs && browserWs.readyState === WebSocket.OPEN) {
|
|
3727
1281
|
browserWs.close();
|
|
3728
1282
|
}
|
|
3729
1283
|
};
|
|
3730
1284
|
clientWs.on("close", cleanup);
|
|
3731
1285
|
clientWs.on("error", cleanup);
|
|
3732
1286
|
browserWs.on("close", () => {
|
|
3733
|
-
if (isAlive && clientWs.readyState ===
|
|
1287
|
+
if (isAlive && clientWs.readyState === WebSocket.OPEN) {
|
|
3734
1288
|
clientWs.close();
|
|
3735
1289
|
}
|
|
3736
1290
|
});
|
|
@@ -3749,7 +1303,7 @@ var CDPInterceptorProxy = class {
|
|
|
3749
1303
|
const ctx = {
|
|
3750
1304
|
method: request.method,
|
|
3751
1305
|
params: request.params ?? {},
|
|
3752
|
-
sessionId: makeCompoundId(browserWs._cdpSession, request.sessionId),
|
|
1306
|
+
sessionId: makeCompoundId("_cdpSession" in browserWs ? browserWs._cdpSession : void 0, request.sessionId),
|
|
3753
1307
|
direction: "client\u2192browser"
|
|
3754
1308
|
};
|
|
3755
1309
|
const decision = this.engine.evaluate(ctx);
|
|
@@ -3879,6 +1433,7 @@ async function resolveCDPEndpoint(raw) {
|
|
|
3879
1433
|
}
|
|
3880
1434
|
|
|
3881
1435
|
// src/browser.ts
|
|
1436
|
+
import { SessionStore } from "@dyyz1993/xcli-core";
|
|
3882
1437
|
function logSessionEvent(event, details) {
|
|
3883
1438
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").substring(0, 19);
|
|
3884
1439
|
const pid = process.pid;
|
|
@@ -3891,7 +1446,7 @@ function sessionFile(name) {
|
|
|
3891
1446
|
function ensureSessionDir() {
|
|
3892
1447
|
mkdirSync(SESSION_DIR, { recursive: true });
|
|
3893
1448
|
}
|
|
3894
|
-
var sessions =
|
|
1449
|
+
var sessions = new SessionStore();
|
|
3895
1450
|
var _sharedBrowser = null;
|
|
3896
1451
|
var _sharedCdpProxy = null;
|
|
3897
1452
|
var IDLE_TIMEOUT_MS = (process.env.XBROWSER_IDLE_TIMEOUT ? parseInt(process.env.XBROWSER_IDLE_TIMEOUT, 10) : 30) * 60 * 1e3;
|
|
@@ -3902,7 +1457,7 @@ function resetIdleTimer() {
|
|
|
3902
1457
|
const now = Date.now();
|
|
3903
1458
|
let allIdle = true;
|
|
3904
1459
|
const idleSessions = [];
|
|
3905
|
-
for (const
|
|
1460
|
+
for (const s of sessions) {
|
|
3906
1461
|
if (now - s.lastActivityAt < IDLE_TIMEOUT_MS) {
|
|
3907
1462
|
allIdle = false;
|
|
3908
1463
|
} else {
|
|
@@ -3925,7 +1480,7 @@ function touchSession(id) {
|
|
|
3925
1480
|
resetIdleTimer();
|
|
3926
1481
|
}
|
|
3927
1482
|
process.on("exit", () => {
|
|
3928
|
-
for (const session of sessions.
|
|
1483
|
+
for (const session of sessions.list()) {
|
|
3929
1484
|
if (session.isCDP) {
|
|
3930
1485
|
logSessionEvent("process_exit", `Session "${session.name}": CDP connection (not closing external browser).`);
|
|
3931
1486
|
} else {
|
|
@@ -3953,7 +1508,7 @@ process.on("exit", () => {
|
|
|
3953
1508
|
}
|
|
3954
1509
|
sessions.clear();
|
|
3955
1510
|
});
|
|
3956
|
-
async function
|
|
1511
|
+
async function getCDPTargets(cdpEndpoint) {
|
|
3957
1512
|
try {
|
|
3958
1513
|
const ep = String(cdpEndpoint);
|
|
3959
1514
|
let host = "localhost";
|
|
@@ -3973,7 +1528,7 @@ async function getCDPTargets2(cdpEndpoint) {
|
|
|
3973
1528
|
}
|
|
3974
1529
|
}
|
|
3975
1530
|
async function findTargetPage(cdpEndpoint, target) {
|
|
3976
|
-
const targets = await
|
|
1531
|
+
const targets = await getCDPTargets(cdpEndpoint);
|
|
3977
1532
|
const pages = targets.filter((t) => t.url && !t.url.startsWith("about:blank") && !t.url.startsWith("chrome://"));
|
|
3978
1533
|
const byId = pages.find((t) => t.id === target);
|
|
3979
1534
|
if (byId) return { pageId: byId.id, wsUrl: byId.webSocketDebuggerUrl, title: byId.title, url: byId.url };
|
|
@@ -4015,6 +1570,9 @@ async function createBrowser(options) {
|
|
|
4015
1570
|
return browser3;
|
|
4016
1571
|
}
|
|
4017
1572
|
const { browser: browser2 } = await launch({ cdpEndpoint: realEndpoint });
|
|
1573
|
+
await browser2.discoverContexts().catch((err) => {
|
|
1574
|
+
console.error(`[browser] discoverContexts failed: ${errMsg(err)}`);
|
|
1575
|
+
});
|
|
4018
1576
|
return browser2;
|
|
4019
1577
|
}
|
|
4020
1578
|
const executablePath = options?.executablePath || process.env.XBROWSER_CHROMIUM_PATH || discoverChromiumPath();
|
|
@@ -4029,10 +1587,7 @@ async function getBrowser(options) {
|
|
|
4029
1587
|
return _sharedBrowser;
|
|
4030
1588
|
}
|
|
4031
1589
|
function findSession(name) {
|
|
4032
|
-
|
|
4033
|
-
if (session.name === name) return session;
|
|
4034
|
-
}
|
|
4035
|
-
return void 0;
|
|
1590
|
+
return sessions.find(name);
|
|
4036
1591
|
}
|
|
4037
1592
|
function getSessionById(id) {
|
|
4038
1593
|
return sessions.get(id);
|
|
@@ -4111,7 +1666,7 @@ async function findOrRestoreSession(name, cdpEndpoint) {
|
|
|
4111
1666
|
}
|
|
4112
1667
|
page = page || fallbackPage;
|
|
4113
1668
|
if (!page) {
|
|
4114
|
-
const targets = await
|
|
1669
|
+
const targets = await getCDPTargets(ep);
|
|
4115
1670
|
const matchTarget = targets.find(
|
|
4116
1671
|
(t) => t.url && t.url !== "about:blank" && !t.url.startsWith("chrome://") && (targetHostname ? t.url.includes(targetHostname) : true)
|
|
4117
1672
|
);
|
|
@@ -4136,9 +1691,14 @@ async function findOrRestoreSession(name, cdpEndpoint) {
|
|
|
4136
1691
|
return void 0;
|
|
4137
1692
|
}
|
|
4138
1693
|
const targetUrl = meta.conversationUrl || meta.url;
|
|
4139
|
-
if (targetUrl && page.url() !== targetUrl
|
|
4140
|
-
|
|
4141
|
-
|
|
1694
|
+
if (targetUrl && page.url() !== targetUrl) {
|
|
1695
|
+
try {
|
|
1696
|
+
if (!page.url().includes(new URL(targetUrl).hostname)) {
|
|
1697
|
+
await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 3e4 }).catch(() => {
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
} catch {
|
|
1701
|
+
}
|
|
4142
1702
|
}
|
|
4143
1703
|
const session = {
|
|
4144
1704
|
id: meta.id || randomUUID(),
|
|
@@ -4151,18 +1711,18 @@ async function findOrRestoreSession(name, cdpEndpoint) {
|
|
|
4151
1711
|
isCDP: true,
|
|
4152
1712
|
cdpEndpoint: ep
|
|
4153
1713
|
};
|
|
4154
|
-
for (const
|
|
1714
|
+
for (const existingSession of sessions.list()) {
|
|
4155
1715
|
if (existingSession.name === name) {
|
|
4156
|
-
logSessionEvent("remove_stale", `Removing stale session name="${name}" id="${
|
|
4157
|
-
sessions.
|
|
1716
|
+
logSessionEvent("remove_stale", `Removing stale session name="${name}" id="${existingSession.id}" during restore`);
|
|
1717
|
+
sessions.removeById(existingSession.id);
|
|
4158
1718
|
}
|
|
4159
1719
|
}
|
|
4160
|
-
sessions.set(session
|
|
1720
|
+
sessions.set(session);
|
|
4161
1721
|
resetIdleTimer();
|
|
4162
1722
|
await installNetworkCapture(page, name);
|
|
4163
1723
|
return session;
|
|
4164
1724
|
} catch (e) {
|
|
4165
|
-
console.error(`[Session Restore] Failed for "${name}":`, e
|
|
1725
|
+
console.error(`[Session Restore] Failed for "${name}":`, errMsg(e));
|
|
4166
1726
|
deleteSessionDiskMeta(name);
|
|
4167
1727
|
return void 0;
|
|
4168
1728
|
}
|
|
@@ -4173,7 +1733,12 @@ async function createEphemeralContext(options) {
|
|
|
4173
1733
|
const { browser: b2 } = await launch({ cdpEndpoint: endpoint });
|
|
4174
1734
|
const contexts = b2.contexts();
|
|
4175
1735
|
const ctx = contexts[0] || await b2.newContext();
|
|
4176
|
-
const
|
|
1736
|
+
const allPages = ctx.pages();
|
|
1737
|
+
const existingPages = allPages.filter((p) => {
|
|
1738
|
+
const url = p.url();
|
|
1739
|
+
return url !== "about:blank" && !url.startsWith("chrome://");
|
|
1740
|
+
});
|
|
1741
|
+
const page2 = existingPages.length > 0 ? existingPages[0] : allPages.length > 0 ? allPages[0] : await ctx.newPage();
|
|
4177
1742
|
resetIdleTimer();
|
|
4178
1743
|
ephemeralConnections.set(page2, b2);
|
|
4179
1744
|
return { context: ctx, page: page2 };
|
|
@@ -4205,11 +1770,11 @@ async function closeEphemeralContext(context) {
|
|
|
4205
1770
|
}
|
|
4206
1771
|
}
|
|
4207
1772
|
function getAllSessions() {
|
|
4208
|
-
return
|
|
1773
|
+
return sessions.list();
|
|
4209
1774
|
}
|
|
4210
1775
|
async function installNetworkCapture(page, sessionName) {
|
|
4211
1776
|
if (process.env.XBROWSER_DAEMON_WORKER !== "1") return;
|
|
4212
|
-
const { networkStore } = await import("./network-store-
|
|
1777
|
+
const { networkStore } = await import("./network-store-XGZ25FFC.js");
|
|
4213
1778
|
const requestData = /* @__PURE__ */ new Map();
|
|
4214
1779
|
const responseMeta = /* @__PURE__ */ new Map();
|
|
4215
1780
|
const xbPage = page;
|
|
@@ -4330,19 +1895,41 @@ async function createSession(name, url, options) {
|
|
|
4330
1895
|
}
|
|
4331
1896
|
context = contexts[0] || await b.newContext();
|
|
4332
1897
|
let targetPage = null;
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
1898
|
+
const targetHostname = url ? (() => {
|
|
1899
|
+
try {
|
|
1900
|
+
return new URL(url).hostname;
|
|
1901
|
+
} catch {
|
|
1902
|
+
return "";
|
|
1903
|
+
}
|
|
1904
|
+
})() : "";
|
|
1905
|
+
if (targetHostname) {
|
|
1906
|
+
for (const ctx of contexts) {
|
|
1907
|
+
const pages = ctx.pages();
|
|
1908
|
+
for (const p of pages) {
|
|
1909
|
+
const pUrl = p.url();
|
|
1910
|
+
if (pUrl && pUrl !== "about:blank" && !pUrl.startsWith("chrome://") && pUrl.includes(targetHostname)) {
|
|
1911
|
+
targetPage = p;
|
|
1912
|
+
break;
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
if (targetPage) break;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
if (!targetPage) {
|
|
1919
|
+
for (const ctx of contexts) {
|
|
1920
|
+
const pages = ctx.pages();
|
|
1921
|
+
for (const p of pages) {
|
|
1922
|
+
const pUrl = p.url();
|
|
1923
|
+
if (pUrl && pUrl !== "about:blank" && !pUrl.startsWith("chrome://")) {
|
|
1924
|
+
targetPage = p;
|
|
1925
|
+
break;
|
|
1926
|
+
}
|
|
4340
1927
|
}
|
|
1928
|
+
if (targetPage) break;
|
|
4341
1929
|
}
|
|
4342
|
-
if (targetPage) break;
|
|
4343
1930
|
}
|
|
4344
1931
|
if (!targetPage && options?.cdpEndpoint) {
|
|
4345
|
-
const targets = await
|
|
1932
|
+
const targets = await getCDPTargets(options.cdpEndpoint);
|
|
4346
1933
|
const matchTarget = targets.find(
|
|
4347
1934
|
(t) => t.url && t.url !== "about:blank" && !t.url.startsWith("chrome://") && (url ? t.url.includes(new URL(url).hostname) : true)
|
|
4348
1935
|
);
|
|
@@ -4380,14 +1967,14 @@ async function createSession(name, url, options) {
|
|
|
4380
1967
|
isCDP,
|
|
4381
1968
|
cdpEndpoint: options?.cdpEndpoint
|
|
4382
1969
|
};
|
|
4383
|
-
sessions.set(session
|
|
1970
|
+
sessions.set(session);
|
|
4384
1971
|
logSessionEvent("create_session", `name="${name}" id="${session.id}" url="${url || "(no url)"}" isCDP=${isCDP} cdpEndpoint=${options?.cdpEndpoint || "(none)"}`);
|
|
4385
1972
|
resetIdleTimer();
|
|
4386
1973
|
await installNetworkCapture(page, name);
|
|
4387
1974
|
return session;
|
|
4388
1975
|
}
|
|
4389
1976
|
async function closeSessionByName(name) {
|
|
4390
|
-
for (const
|
|
1977
|
+
for (const session of sessions) {
|
|
4391
1978
|
if (session.name === name || session.id === name) {
|
|
4392
1979
|
logSessionEvent("close_session", `name="${session.name}" id="${session.id}" url="${session.page.url()}"`);
|
|
4393
1980
|
if (session.isCDP) {
|
|
@@ -4406,20 +1993,20 @@ async function closeSessionByName(name) {
|
|
|
4406
1993
|
});
|
|
4407
1994
|
}
|
|
4408
1995
|
}
|
|
4409
|
-
sessions.
|
|
1996
|
+
sessions.removeById(session.id);
|
|
4410
1997
|
const file2 = sessionFile(session.name);
|
|
4411
1998
|
try {
|
|
4412
1999
|
unlinkSync(file2);
|
|
4413
2000
|
} catch {
|
|
4414
2001
|
}
|
|
4415
2002
|
try {
|
|
4416
|
-
const { networkStore, commandLogStore } = await import("./network-store-
|
|
2003
|
+
const { networkStore, commandLogStore } = await import("./network-store-XGZ25FFC.js");
|
|
4417
2004
|
networkStore.clear(session.name);
|
|
4418
2005
|
commandLogStore.clear(session.name);
|
|
4419
2006
|
} catch {
|
|
4420
2007
|
}
|
|
4421
2008
|
try {
|
|
4422
|
-
const { SessionRecorder } = await import("./session-recorder-
|
|
2009
|
+
const { SessionRecorder } = await import("./session-recorder-YI7YYM36.js");
|
|
4423
2010
|
SessionRecorder.cleanup(session.name);
|
|
4424
2011
|
} catch {
|
|
4425
2012
|
}
|
|
@@ -4434,9 +2021,9 @@ async function closeSessionByName(name) {
|
|
|
4434
2021
|
return false;
|
|
4435
2022
|
}
|
|
4436
2023
|
async function closeAllSessions() {
|
|
4437
|
-
const names =
|
|
2024
|
+
const names = sessions.list().map((s) => `${s.name}(${s.page.url()})`).join(", ");
|
|
4438
2025
|
if (names) logSessionEvent("close_all_sessions", `Closing ${sessions.size} sessions: ${names}`);
|
|
4439
|
-
for (const
|
|
2026
|
+
for (const session of sessions.list()) {
|
|
4440
2027
|
try {
|
|
4441
2028
|
if (!session.isCDP) {
|
|
4442
2029
|
await session.context.close();
|
|
@@ -4445,9 +2032,9 @@ async function closeAllSessions() {
|
|
|
4445
2032
|
await session.browser.close().catch(() => {
|
|
4446
2033
|
});
|
|
4447
2034
|
}
|
|
4448
|
-
sessions.
|
|
2035
|
+
sessions.removeById(session.id);
|
|
4449
2036
|
} catch {
|
|
4450
|
-
sessions.
|
|
2037
|
+
sessions.removeById(session.id);
|
|
4451
2038
|
}
|
|
4452
2039
|
}
|
|
4453
2040
|
}
|
|
@@ -4485,7 +2072,7 @@ async function ensureProcessCanExit() {
|
|
|
4485
2072
|
clearTimeout(idleTimer);
|
|
4486
2073
|
idleTimer = null;
|
|
4487
2074
|
}
|
|
4488
|
-
for (const session of sessions.
|
|
2075
|
+
for (const session of sessions.list()) {
|
|
4489
2076
|
if (session.browser) {
|
|
4490
2077
|
if (session.isCDP) {
|
|
4491
2078
|
await session.browser.close().catch(() => {
|
|
@@ -4511,6 +2098,7 @@ async function ensureProcessCanExit() {
|
|
|
4511
2098
|
|
|
4512
2099
|
export {
|
|
4513
2100
|
createRuleEngine,
|
|
2101
|
+
resolveCDPEndpoint,
|
|
4514
2102
|
touchSession,
|
|
4515
2103
|
findTargetPage,
|
|
4516
2104
|
resolveLaunchOpts,
|