@xbrowser/cli 1.0.0 → 1.0.3

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