@xbrowser/cli 1.20.0 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/{browser-Y6B3NS4Q.js → browser-BVRFSUXU.js} +3 -2
- package/dist/{browser-NVC7MTML.js → browser-DSYUAE7H.js} +5 -5
- package/dist/{browser-PBBC7RAV.js → browser-Q67BJTI2.js} +6 -4
- package/dist/{cdp-driver-QYFA4QTP.js → cdp-driver-DC7EIUG5.js} +4 -3
- package/dist/{cdp-driver-Z2PLAO42.js → cdp-driver-T7D6H2RW.js} +368 -510
- package/dist/{cdp-driver-73F5CVH2.js → cdp-driver-WWO2UZDR.js} +4 -3
- package/dist/{chunk-76EKW7YJ.js → chunk-2Q3GQRUP.js} +3 -3
- package/dist/{chunk-UYEJ66VX.js → chunk-4OU37CXP.js} +387 -540
- package/dist/{chunk-H2A5JUK5.js → chunk-75DNUI6M.js} +9 -0
- package/dist/{chunk-FDU5BCPV.js → chunk-7OP2ISEY.js} +378 -540
- package/dist/{chunk-3FWLW7FS.js → chunk-A7NEA4PA.js} +39 -1
- package/dist/chunk-GYB5BOSP.js +602 -0
- package/dist/{chunk-T2KUVO4H.js → chunk-IFKJO2UR.js} +393 -546
- package/dist/{chunk-NODRQGOK.js → chunk-IR6C24P7.js} +97 -0
- package/dist/{chunk-3KZ34AUC.js → chunk-MYH6FIBZ.js} +2 -1
- package/dist/{chunk-75TLPVVZ.js → chunk-T4WTKBWI.js} +2 -2
- package/dist/{chunk-HBHOKZPN.js → chunk-V6KGBU5J.js} +97 -0
- package/dist/{chunk-IWVG4XYZ.js → chunk-VCVDILH6.js} +2 -1
- package/dist/{chunk-A6LPGFAL.js → chunk-WYETQ5C7.js} +1 -1
- package/dist/cli.js +25 -13
- package/dist/daemon-main.js +20 -14
- package/dist/index.d.ts +136 -1
- package/dist/index.js +29 -18
- package/dist/keep-awake-M4KJS4B4.js +29 -0
- package/dist/{launcher-44STYRJC.js → launcher-IK2FKW6T.js} +1 -1
- package/dist/{launcher-EXJHHAUL.js → launcher-LUOMMGZC.js} +1 -1
- package/dist/{proxy-LUR4U5YF.js → proxy-NKCYE76M.js} +2 -2
- package/dist/{session-recorder-SLDBENVF.js → session-recorder-563ISSJK.js} +1 -1
- package/dist/{session-recorder-3BEVWHOK.js → session-recorder-GEZVQBKW.js} +1 -1
- package/dist/session-replayer-XM5L7LFD.js +927 -0
- package/dist/stealth-2AA2FSNN.js +29 -0
- package/dist/stealth-JOXGV34D.js +29 -0
- package/package.json +2 -1
- package/dist/session-replayer-SBYJXQZZ.js +0 -413
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_STEALTH_CONFIG,
|
|
3
|
+
StealthInjector,
|
|
4
|
+
bezierTrajectory,
|
|
5
|
+
buildStealthInitScript,
|
|
6
|
+
cosineEase,
|
|
7
|
+
deriveUaChProfileForBinary,
|
|
8
|
+
landingOffset,
|
|
9
|
+
lookupKey,
|
|
10
|
+
rand,
|
|
11
|
+
sleep,
|
|
12
|
+
typingDelay,
|
|
13
|
+
wheelDelta
|
|
14
|
+
} from "./chunk-GYB5BOSP.js";
|
|
15
|
+
import "./chunk-3RG5ZIWI.js";
|
|
16
|
+
export {
|
|
17
|
+
DEFAULT_STEALTH_CONFIG,
|
|
18
|
+
StealthInjector,
|
|
19
|
+
bezierTrajectory,
|
|
20
|
+
buildStealthInitScript,
|
|
21
|
+
cosineEase,
|
|
22
|
+
deriveUaChProfileForBinary,
|
|
23
|
+
landingOffset,
|
|
24
|
+
lookupKey,
|
|
25
|
+
rand,
|
|
26
|
+
sleep,
|
|
27
|
+
typingDelay,
|
|
28
|
+
wheelDelta
|
|
29
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_STEALTH_CONFIG,
|
|
3
|
+
StealthInjector,
|
|
4
|
+
bezierTrajectory,
|
|
5
|
+
buildStealthInitScript,
|
|
6
|
+
cosineEase,
|
|
7
|
+
deriveUaChProfileForBinary,
|
|
8
|
+
landingOffset,
|
|
9
|
+
lookupKey,
|
|
10
|
+
rand,
|
|
11
|
+
sleep,
|
|
12
|
+
typingDelay,
|
|
13
|
+
wheelDelta
|
|
14
|
+
} from "./chunk-GYB5BOSP.js";
|
|
15
|
+
import "./chunk-KFQGP6VL.js";
|
|
16
|
+
export {
|
|
17
|
+
DEFAULT_STEALTH_CONFIG,
|
|
18
|
+
StealthInjector,
|
|
19
|
+
bezierTrajectory,
|
|
20
|
+
buildStealthInitScript,
|
|
21
|
+
cosineEase,
|
|
22
|
+
deriveUaChProfileForBinary,
|
|
23
|
+
landingOffset,
|
|
24
|
+
lookupKey,
|
|
25
|
+
rand,
|
|
26
|
+
sleep,
|
|
27
|
+
typingDelay,
|
|
28
|
+
wheelDelta
|
|
29
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xbrowser/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "Browser automation CLI for web scraping, headless browsing, SEO analysis, and AI agent workflows. A command-line alternative to Playwright, Puppeteer, and Selenium.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"lint": "eslint src/ bin/ --ext .ts",
|
|
76
76
|
"lint:plugin-contract": "node lint-scripts/check-plugin-contract.mjs && node lint-scripts/check-plugin-status.mjs",
|
|
77
77
|
"lint:plugin-status": "node lint-scripts/check-plugin-status.mjs --update",
|
|
78
|
+
"stealth:check": "vitest run tests/cdp-driver/stealth-health.test.ts tests/cdp-driver/stealth-audit.test.ts tests/cdp-driver/stealth-probe.test.ts",
|
|
78
79
|
"lint:docs": "node lint-scripts/check-docs-consistency.mjs",
|
|
79
80
|
"typecheck": "tsc --noEmit",
|
|
80
81
|
"start": "node dist/cli.js",
|
|
@@ -1,413 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
queryJS
|
|
3
|
-
} from "./chunk-3FWLW7FS.js";
|
|
4
|
-
import "./chunk-KFQGP6VL.js";
|
|
5
|
-
|
|
6
|
-
// src/recorder/session-replayer.ts
|
|
7
|
-
var SessionReplayer = class {
|
|
8
|
-
opts;
|
|
9
|
-
recording = null;
|
|
10
|
-
page = null;
|
|
11
|
-
constructor(opts) {
|
|
12
|
-
this.opts = {
|
|
13
|
-
cdpUrl: opts.cdpUrl,
|
|
14
|
-
page: opts.page,
|
|
15
|
-
stepDelay: opts.stepDelay ?? 500,
|
|
16
|
-
stepTimeout: opts.stepTimeout ?? 1e4,
|
|
17
|
-
onStep: opts.onStep,
|
|
18
|
-
onError: opts.onError
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
/** Load a recording from a file path or parsed JSON */
|
|
22
|
-
async load(source) {
|
|
23
|
-
if (typeof source === "string") {
|
|
24
|
-
const fs = await import("fs");
|
|
25
|
-
const raw = fs.readFileSync(source, "utf8");
|
|
26
|
-
this.recording = JSON.parse(raw);
|
|
27
|
-
} else {
|
|
28
|
-
this.recording = source;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
/** Run the full replay */
|
|
32
|
-
async run() {
|
|
33
|
-
if (!this.recording) throw new Error("No recording loaded. Call load() first.");
|
|
34
|
-
if (this.opts.page) {
|
|
35
|
-
this.page = this.opts.page;
|
|
36
|
-
} else if (this.opts.cdpUrl) {
|
|
37
|
-
const { launch } = await import("./cdp-driver-73F5CVH2.js");
|
|
38
|
-
const { browser } = await launch({ cdpEndpoint: this.opts.cdpUrl });
|
|
39
|
-
let contexts = browser.contexts();
|
|
40
|
-
for (let i = 0; i < 10 && contexts.length === 0; i++) {
|
|
41
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
42
|
-
contexts = browser.contexts();
|
|
43
|
-
}
|
|
44
|
-
const context = contexts[0];
|
|
45
|
-
if (!context) throw new Error("No browser context available");
|
|
46
|
-
const pages = context.pages();
|
|
47
|
-
this.page = pages[0];
|
|
48
|
-
}
|
|
49
|
-
if (!this.page) throw new Error("No page available. Provide cdpUrl or page.");
|
|
50
|
-
const actions = this.dedupAdjacentActions(this.recording.actions);
|
|
51
|
-
let success = 0;
|
|
52
|
-
let failed = 0;
|
|
53
|
-
let skipped = 0;
|
|
54
|
-
for (let i = 0; i < actions.length; i++) {
|
|
55
|
-
const action = actions[i];
|
|
56
|
-
this.opts.onStep?.(action, i, actions.length);
|
|
57
|
-
const pagesBefore = this.listContextPages();
|
|
58
|
-
try {
|
|
59
|
-
if (action.trajectory) {
|
|
60
|
-
await this.replayTrajectory(action.trajectory);
|
|
61
|
-
}
|
|
62
|
-
await this.replayAction(action);
|
|
63
|
-
if (action.type !== "resize" && action.type !== "clipboard" && action.type !== "visibility") {
|
|
64
|
-
try {
|
|
65
|
-
await this.page.waitForLoadState("domcontentloaded", this.opts.stepTimeout);
|
|
66
|
-
} catch {
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
success++;
|
|
70
|
-
if (action.type === "click" || action.type === "cdp-click") {
|
|
71
|
-
const pagesAfter = this.listContextPages();
|
|
72
|
-
if (pagesAfter && pagesBefore && pagesAfter.length > pagesBefore.length) {
|
|
73
|
-
const newest = pagesAfter[pagesAfter.length - 1];
|
|
74
|
-
await newest.bringToFront().catch(() => {
|
|
75
|
-
});
|
|
76
|
-
this.page = newest;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
} catch (e) {
|
|
80
|
-
const err = e instanceof Error ? e : new Error(String(e));
|
|
81
|
-
this.opts.onError?.(action, err, i);
|
|
82
|
-
failed++;
|
|
83
|
-
}
|
|
84
|
-
if (i < actions.length - 1) {
|
|
85
|
-
await new Promise((r) => setTimeout(r, this.opts.stepDelay));
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return { success, failed, skipped };
|
|
89
|
-
}
|
|
90
|
-
/** Replay a single action */
|
|
91
|
-
/** Pages of the replayer page's context (best-effort; null if unavailable). */
|
|
92
|
-
listContextPages() {
|
|
93
|
-
try {
|
|
94
|
-
const ctx = this.page.context?.();
|
|
95
|
-
const pages = ctx?.pages?.();
|
|
96
|
-
return Array.isArray(pages) ? pages : null;
|
|
97
|
-
} catch {
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Replay-time adjacent dedup (rec-duel d02/d03/d05).
|
|
103
|
-
*
|
|
104
|
-
* The recorder can emit both the real action signal AND the injected cdp
|
|
105
|
-
* command action for a single interaction when the signal flush lags behind
|
|
106
|
-
* the recorder-side dedup window (heavy snapshot capture slows polling).
|
|
107
|
-
* Replaying both executes the interaction twice. Filter here: an action is
|
|
108
|
-
* skipped when a nearby (≤15s apart) earlier action matches on
|
|
109
|
-
* type-normalized key (selector + text) AND coordinates agree — either side
|
|
110
|
-
* lacks coords (cdp actions carry none) or both are within 30px. Two real
|
|
111
|
-
* clicks on the same element at different spots (canvas buttons) survive.
|
|
112
|
-
*/
|
|
113
|
-
dedupAdjacentActions(actions) {
|
|
114
|
-
const normType = (t) => t === "cdp-click" ? "click" : t === "cdp-fill" ? "input" : t;
|
|
115
|
-
const generic = (a) => (a.type === "click" || a.type === "dblclick" || a.type === "contextmenu") && (a.element?.selector === "html" || a.element?.selector === "body");
|
|
116
|
-
const keyOf = (a) => `${normType(a.type)}|${a.element?.selector || ""}|${a.element?.text || ""}`;
|
|
117
|
-
const coordsOf = (a) => typeof a.x === "number" && typeof a.y === "number" ? { x: a.x, y: a.y } : null;
|
|
118
|
-
const near = (p, q) => Math.abs(p.x - q.x) <= 30 && Math.abs(p.y - q.y) <= 30;
|
|
119
|
-
const lastKept = /* @__PURE__ */ new Map();
|
|
120
|
-
return actions.filter((a) => {
|
|
121
|
-
if (generic(a)) return false;
|
|
122
|
-
const key = keyOf(a);
|
|
123
|
-
const c = coordsOf(a);
|
|
124
|
-
const recent = lastKept.get(key) || [];
|
|
125
|
-
const dup = recent.some((e) => Math.abs(a.timestamp - e.ts) <= 15e3 && (!c || !e.c || near(c, e.c)));
|
|
126
|
-
if (dup) return false;
|
|
127
|
-
recent.push({ ts: a.timestamp, c });
|
|
128
|
-
lastKept.set(key, recent);
|
|
129
|
-
return true;
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
async replayAction(action) {
|
|
133
|
-
const page = this.page;
|
|
134
|
-
const timeout = this.opts.stepTimeout;
|
|
135
|
-
switch (action.type) {
|
|
136
|
-
// Proactive sensing actions are observations, not user actions — skip
|
|
137
|
-
// them during replay (they don't represent anything the user did).
|
|
138
|
-
case "popup_appear":
|
|
139
|
-
case "discovered_filters":
|
|
140
|
-
return;
|
|
141
|
-
case "navigation":
|
|
142
|
-
await page.goto(action.url, { waitUntil: "load", timeout });
|
|
143
|
-
break;
|
|
144
|
-
case "goto":
|
|
145
|
-
await page.goto(action.url, { waitUntil: "load", timeout });
|
|
146
|
-
break;
|
|
147
|
-
case "click":
|
|
148
|
-
case "cdp-click": {
|
|
149
|
-
const selector = await this.resolveAndWait(action);
|
|
150
|
-
if (typeof action.x === "number" && typeof action.y === "number") {
|
|
151
|
-
const hit = await page.evaluate(`
|
|
152
|
-
(function() {
|
|
153
|
-
const el = ${queryJS(selector)};
|
|
154
|
-
if (!el) return false;
|
|
155
|
-
const r = el.getBoundingClientRect();
|
|
156
|
-
return ${action.x} >= r.x - 2 && ${action.x} <= r.x + r.width + 2
|
|
157
|
-
&& ${action.y} >= r.y - 2 && ${action.y} <= r.y + r.height + 2;
|
|
158
|
-
})()
|
|
159
|
-
`).catch(() => false);
|
|
160
|
-
if (hit) {
|
|
161
|
-
await page.mouse.click(action.x, action.y, { stealth: true });
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
await page.click(selector, { timeout });
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
case "scroll": {
|
|
169
|
-
const [dir, distStr] = (action.value || "down:300").split(":");
|
|
170
|
-
const dist = Number(distStr) || 300;
|
|
171
|
-
const sign = dir === "up" ? -1 : dir === "left" ? 0 : 1;
|
|
172
|
-
const selector = await this.resolveAndWait(action).catch(() => void 0);
|
|
173
|
-
if (selector) {
|
|
174
|
-
await page.evaluate(`
|
|
175
|
-
(function() {
|
|
176
|
-
const el = ${queryJS(selector)};
|
|
177
|
-
if (el) el.scrollTop += ${sign * dist};
|
|
178
|
-
})()
|
|
179
|
-
`).catch(() => {
|
|
180
|
-
});
|
|
181
|
-
} else {
|
|
182
|
-
await page.evaluate(`window.scrollBy(0, ${sign * dist})`).catch(() => {
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
case "input": {
|
|
188
|
-
const selector = await this.resolveAndWait(action);
|
|
189
|
-
await page.fill(selector, action.value ?? "", { timeout });
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
case "cdp-fill": {
|
|
193
|
-
const selector = await this.resolveAndWait(action);
|
|
194
|
-
await page.fill(selector, action.value ?? "", { timeout });
|
|
195
|
-
break;
|
|
196
|
-
}
|
|
197
|
-
case "change": {
|
|
198
|
-
const selector = await this.resolveAndWait(action);
|
|
199
|
-
if (action.value) {
|
|
200
|
-
await page.selectOption(selector, action.value);
|
|
201
|
-
}
|
|
202
|
-
break;
|
|
203
|
-
}
|
|
204
|
-
case "filechooser": {
|
|
205
|
-
const selector = await this.resolveAndWait(action);
|
|
206
|
-
const files = this.resolveFiles(action);
|
|
207
|
-
if (files.length > 0) {
|
|
208
|
-
await page.setInputFiles(selector, files);
|
|
209
|
-
}
|
|
210
|
-
break;
|
|
211
|
-
}
|
|
212
|
-
case "keydown": {
|
|
213
|
-
const key = action.key ?? "";
|
|
214
|
-
if (key === "Enter" || key === "Tab" || key === "Escape") {
|
|
215
|
-
await page.keyboard.press(key);
|
|
216
|
-
} else if (key === "Backspace") {
|
|
217
|
-
await page.keyboard.press("Backspace");
|
|
218
|
-
} else if (key === "Delete") {
|
|
219
|
-
await page.keyboard.press("Delete");
|
|
220
|
-
} else if (key.startsWith("Arrow")) {
|
|
221
|
-
await page.keyboard.press(key);
|
|
222
|
-
} else if (key.includes("+")) {
|
|
223
|
-
await page.keyboard.press(key.replace("Meta", "Meta").replace("Ctrl", "Control"));
|
|
224
|
-
}
|
|
225
|
-
break;
|
|
226
|
-
}
|
|
227
|
-
case "dblclick": {
|
|
228
|
-
const selector = this.resolveSelector(action);
|
|
229
|
-
if (selector) {
|
|
230
|
-
await page.waitForSelector(selector, { state: "visible", timeout });
|
|
231
|
-
await page.dblclick(selector, { timeout });
|
|
232
|
-
} else if (action.x !== void 0 && action.y !== void 0) {
|
|
233
|
-
await page.mouse.dblclick(action.x, action.y);
|
|
234
|
-
}
|
|
235
|
-
break;
|
|
236
|
-
}
|
|
237
|
-
case "contextmenu": {
|
|
238
|
-
const selector = this.resolveSelector(action);
|
|
239
|
-
if (selector) {
|
|
240
|
-
await page.waitForSelector(selector, { state: "visible", timeout });
|
|
241
|
-
await page.click(selector, { button: "right", timeout });
|
|
242
|
-
} else if (action.x !== void 0 && action.y !== void 0) {
|
|
243
|
-
await page.mouse.click(action.x, action.y, { button: "right" });
|
|
244
|
-
}
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
247
|
-
case "hover": {
|
|
248
|
-
const selector = await this.resolveAndWait(action);
|
|
249
|
-
if (selector) {
|
|
250
|
-
await page.hover(selector);
|
|
251
|
-
}
|
|
252
|
-
const firstPopup = action.hoverContext?.appeared?.[0];
|
|
253
|
-
if (firstPopup?.selector) {
|
|
254
|
-
try {
|
|
255
|
-
await page.waitForSelector(firstPopup.selector, {
|
|
256
|
-
state: "visible",
|
|
257
|
-
timeout: 1e3
|
|
258
|
-
});
|
|
259
|
-
} catch {
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
break;
|
|
263
|
-
}
|
|
264
|
-
case "drag": {
|
|
265
|
-
if (action.drag) {
|
|
266
|
-
const { fromX, fromY, toX, toY } = action.drag;
|
|
267
|
-
await page.mouse.move(fromX, fromY);
|
|
268
|
-
await page.mouse.down();
|
|
269
|
-
const steps = 5;
|
|
270
|
-
for (let i = 1; i <= steps; i++) {
|
|
271
|
-
await page.mouse.move(
|
|
272
|
-
fromX + (toX - fromX) * i / steps,
|
|
273
|
-
fromY + (toY - fromY) * i / steps
|
|
274
|
-
);
|
|
275
|
-
await new Promise((r) => setTimeout(r, 30));
|
|
276
|
-
}
|
|
277
|
-
await page.mouse.up();
|
|
278
|
-
}
|
|
279
|
-
break;
|
|
280
|
-
}
|
|
281
|
-
case "resize": {
|
|
282
|
-
break;
|
|
283
|
-
}
|
|
284
|
-
case "clipboard": {
|
|
285
|
-
break;
|
|
286
|
-
}
|
|
287
|
-
case "touch": {
|
|
288
|
-
if (action.touch) {
|
|
289
|
-
const { touchType, touches } = action.touch;
|
|
290
|
-
if (touchType === "start" && touches.length > 0) {
|
|
291
|
-
await page.mouse.move(touches[0].x, touches[0].y);
|
|
292
|
-
await page.mouse.down();
|
|
293
|
-
} else if (touchType === "end" && touches.length > 0) {
|
|
294
|
-
await page.mouse.move(touches[0].x, touches[0].y);
|
|
295
|
-
await page.mouse.up();
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
break;
|
|
299
|
-
}
|
|
300
|
-
case "focus": {
|
|
301
|
-
const selector = await this.resolveAndWait(action);
|
|
302
|
-
if (action.focus?.focusType === "focus") {
|
|
303
|
-
await page.locator(selector).focus();
|
|
304
|
-
}
|
|
305
|
-
break;
|
|
306
|
-
}
|
|
307
|
-
case "visibility": {
|
|
308
|
-
break;
|
|
309
|
-
}
|
|
310
|
-
case "submit": {
|
|
311
|
-
const selector = await this.resolveAndWait(action);
|
|
312
|
-
await page.evaluate((sel) => {
|
|
313
|
-
const form = document.querySelector(sel);
|
|
314
|
-
if (!form) return;
|
|
315
|
-
if (typeof form.requestSubmit === "function") {
|
|
316
|
-
form.requestSubmit();
|
|
317
|
-
} else {
|
|
318
|
-
form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
|
|
319
|
-
}
|
|
320
|
-
}, selector);
|
|
321
|
-
break;
|
|
322
|
-
}
|
|
323
|
-
case "scroll": {
|
|
324
|
-
await page.evaluate(() => {
|
|
325
|
-
window.scrollBy(action.scrollX ?? 0, action.scrollY ?? 0);
|
|
326
|
-
});
|
|
327
|
-
break;
|
|
328
|
-
}
|
|
329
|
-
default:
|
|
330
|
-
break;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
/** Replay a mouse trajectory (smooth movement between actions) */
|
|
334
|
-
async replayTrajectory(trajectory) {
|
|
335
|
-
const page = this.page;
|
|
336
|
-
const { points } = trajectory;
|
|
337
|
-
if (!points || points.length < 2) return;
|
|
338
|
-
for (let i = 0; i < points.length; i++) {
|
|
339
|
-
const { x, y, dt } = points[i];
|
|
340
|
-
if (dt > 0) {
|
|
341
|
-
await new Promise((r) => setTimeout(r, Math.min(dt, 200)));
|
|
342
|
-
}
|
|
343
|
-
await page.mouse.move(x, y);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
/** Resolve the best selector for an action (primary selector only) */
|
|
347
|
-
resolveSelector(action) {
|
|
348
|
-
const el = action.element;
|
|
349
|
-
if (!el) return "";
|
|
350
|
-
if (el.selector) {
|
|
351
|
-
return el.selector;
|
|
352
|
-
}
|
|
353
|
-
if (el.tag) {
|
|
354
|
-
return el.tag;
|
|
355
|
-
}
|
|
356
|
-
return "";
|
|
357
|
-
}
|
|
358
|
-
/**
|
|
359
|
-
* X2: Wait for an element using the best available selector, with
|
|
360
|
-
* confidence-based fallback support.
|
|
361
|
-
*
|
|
362
|
-
* Returns the first matching selector, or throws if none match.
|
|
363
|
-
* Fallback order:
|
|
364
|
-
* 1. Primary CSS selector (always tried first)
|
|
365
|
-
* 2. textFallback selector (used when primary fails — not just for low
|
|
366
|
-
* confidence, since high-confidence selectors from dynamic attributes
|
|
367
|
-
* like data-spm-anchor-id can also fail on replay)
|
|
368
|
-
* 3. Tag-based fallback (last resort)
|
|
369
|
-
*/
|
|
370
|
-
async resolveAndWait(action) {
|
|
371
|
-
const el = action.element;
|
|
372
|
-
if (!el) throw new Error("No element metadata");
|
|
373
|
-
const page = this.page;
|
|
374
|
-
const timeout = this.opts.stepTimeout;
|
|
375
|
-
const candidates = [];
|
|
376
|
-
if (el.selector) candidates.push(el.selector);
|
|
377
|
-
if (el.textFallback?.selector && !candidates.includes(el.textFallback.selector)) {
|
|
378
|
-
candidates.push(el.textFallback.selector);
|
|
379
|
-
}
|
|
380
|
-
if (el.tag && !candidates.includes(el.tag)) {
|
|
381
|
-
candidates.push(el.tag);
|
|
382
|
-
}
|
|
383
|
-
if (candidates.length === 0) throw new Error("No selector available for element");
|
|
384
|
-
for (const sel of candidates) {
|
|
385
|
-
try {
|
|
386
|
-
await page.waitForSelector(sel, { state: "visible", timeout });
|
|
387
|
-
return sel;
|
|
388
|
-
} catch {
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
throw new Error(`Element not found, tried: ${candidates.join(", ")}`);
|
|
392
|
-
}
|
|
393
|
-
/** Resolve file payloads from a filechooser action */
|
|
394
|
-
resolveFiles(action) {
|
|
395
|
-
if (!action.files?.fileData) return [];
|
|
396
|
-
return action.files.fileData.filter((f) => f.dataUrl).map((f) => {
|
|
397
|
-
const match = f.dataUrl.match(/^data:[^;]+;base64,(.+)$/);
|
|
398
|
-
if (!match) return null;
|
|
399
|
-
return {
|
|
400
|
-
name: f.name,
|
|
401
|
-
mimeType: f.type || "application/octet-stream",
|
|
402
|
-
buffer: Buffer.from(match[1], "base64")
|
|
403
|
-
};
|
|
404
|
-
}).filter((f) => f !== null);
|
|
405
|
-
}
|
|
406
|
-
/** Clean up */
|
|
407
|
-
async close() {
|
|
408
|
-
this.page = null;
|
|
409
|
-
}
|
|
410
|
-
};
|
|
411
|
-
export {
|
|
412
|
-
SessionReplayer
|
|
413
|
-
};
|