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