@humanjs/playwright 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,77 @@
1
- import { resolvePersonality, createRng, bezierPath, humanizePath } from '@humanjs/core';
2
- export { applyMicroJitter, applyVelocityProfile, bezierPath, blend, careful, createRng, distracted, fast, humanizePath, precise, resolvePersonality } from '@humanjs/core';
1
+ import { resolvePersonality, createRng, planScroll, countWords, computeReadingDwellMs, planReadingScan, planTypeKeystrokes, bezierPath, humanizePath } from '@humanjs/core';
2
+ export { applyMicroJitter, applyVelocityProfile, bezierPath, blend, careful, computeReadingDwellMs, countWords, createRng, distracted, fast, humanizePath, planScroll, planTypeKeystrokes, precise, resolvePersonality } from '@humanjs/core';
3
3
 
4
4
  // src/index.ts
5
+
6
+ // src/internal/timing.ts
7
+ function sleep(ms) {
8
+ return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
9
+ }
10
+ function speedModeFactor(speed) {
11
+ switch (speed) {
12
+ case "fast":
13
+ return 0.5;
14
+ case "instant":
15
+ return 0;
16
+ default:
17
+ return 1;
18
+ }
19
+ }
20
+ function computeDwellTime(meanMs, jitter, personality, speed, rng) {
21
+ if (meanMs <= 0) return 0;
22
+ const jitterMag = meanMs * jitter;
23
+ const offset = rng.nextFloat(-jitterMag, jitterMag);
24
+ return Math.max(0, (meanMs + offset) * personality.speed * speedModeFactor(speed));
25
+ }
26
+
27
+ // src/keyboard/index.ts
28
+ async function executeType(target, value, ctx) {
29
+ const locator = typeof target === "string" ? ctx.page.locator(target) : target;
30
+ if (value.length === 0) {
31
+ return { characters: 0, typos: 0, corrections: 0 };
32
+ }
33
+ if (ctx.speed === "instant") {
34
+ await locator.pressSequentially(value, { delay: 0 });
35
+ return { characters: value.length, typos: 0, corrections: 0 };
36
+ }
37
+ await locator.focus();
38
+ const plan = planTypeKeystrokes(value, ctx.personality.typing, ctx.rng, {
39
+ personalitySpeed: ctx.personality.speed,
40
+ speedFactor: speedModeFactor(ctx.speed)
41
+ });
42
+ let typos = 0;
43
+ let corrections = 0;
44
+ for (const step of plan) {
45
+ if (step.delayBeforeMs > 0) await sleep(step.delayBeforeMs);
46
+ await dispatchKey(ctx.page, step.key);
47
+ if (step.isTypo) typos++;
48
+ if (step.isCorrection) corrections++;
49
+ }
50
+ return { characters: value.length, typos, corrections };
51
+ }
52
+ async function dispatchKey(page, key) {
53
+ if (key.length > 1 || key.charCodeAt(0) < 128) {
54
+ await page.keyboard.press(key);
55
+ } else {
56
+ await page.keyboard.insertText(key);
57
+ }
58
+ }
59
+
60
+ // src/internal/mouse-walk.ts
61
+ async function walkMouseAlongPath(page, path, durationMs) {
62
+ if (path.length === 0) return;
63
+ const stepDelayMs = path.length > 1 && durationMs > 0 ? durationMs / (path.length - 1) : 0;
64
+ for (let i = 0; i < path.length; i++) {
65
+ const point = path[i];
66
+ if (!point) continue;
67
+ await page.mouse.move(point.x, point.y);
68
+ if (i < path.length - 1 && stepDelayMs > 0) {
69
+ await sleep(stepDelayMs);
70
+ }
71
+ }
72
+ }
73
+
74
+ // src/mouse/index.ts
5
75
  async function executeClick(target, ctx) {
6
76
  const locator = typeof target === "string" ? ctx.page.locator(target) : target;
7
77
  if (ctx.speed === "instant") {
@@ -23,7 +93,8 @@ async function executeClick(target, ctx) {
23
93
  curvature: ctx.personality.mouse.curvature
24
94
  });
25
95
  const path = humanizePath(rawPath, ctx.rng);
26
- await walkMouseAlongPath(ctx.page, path, ctx.personality, ctx.rng, ctx.speed);
96
+ const travelMs = computeTravelTime(path, ctx.personality, ctx.speed, ctx.rng);
97
+ await walkMouseAlongPath(ctx.page, path, travelMs);
27
98
  const preClickMs = computeDwellTime(
28
99
  ctx.personality.dwell.preClickMs,
29
100
  ctx.personality.dwell.preClickJitter,
@@ -51,19 +122,6 @@ function pickClickPoint(box, rng) {
51
122
  const y = clamp(cy + rng.nextGaussian(0, box.height / 8), box.y, box.y + box.height);
52
123
  return { x, y };
53
124
  }
54
- async function walkMouseAlongPath(page, path, personality, rng, speed) {
55
- if (path.length === 0) return;
56
- const totalTimeMs = computeTravelTime(path, personality, speed, rng);
57
- const stepDelayMs = path.length > 1 ? totalTimeMs / (path.length - 1) : 0;
58
- for (let i = 0; i < path.length; i++) {
59
- const point = path[i];
60
- if (!point) continue;
61
- await page.mouse.move(point.x, point.y);
62
- if (i < path.length - 1 && stepDelayMs > 0) {
63
- await sleep(stepDelayMs);
64
- }
65
- }
66
- }
67
125
  function computeTravelTime(path, personality, speed, rng) {
68
126
  let distance = 0;
69
127
  for (let i = 1; i < path.length; i++) {
@@ -78,31 +136,378 @@ function computeTravelTime(path, personality, speed, rng) {
78
136
  const total = (baseTime + jitter) * personality.speed * speedModeFactor(speed);
79
137
  return Math.max(0, total);
80
138
  }
81
- function computeDwellTime(meanMs, jitter, personality, speed, rng) {
82
- if (meanMs <= 0) return 0;
83
- const jitterMag = meanMs * jitter;
84
- const offset = rng.nextFloat(-jitterMag, jitterMag);
85
- return Math.max(0, (meanMs + offset) * personality.speed * speedModeFactor(speed));
86
- }
87
- function speedModeFactor(speed) {
88
- switch (speed) {
89
- case "fast":
90
- return 0.5;
91
- case "instant":
92
- return 0;
93
- default:
94
- return 1;
95
- }
96
- }
97
139
  function describeTarget(target) {
98
140
  return typeof target === "string" ? target : target.toString?.() ?? "locator";
99
141
  }
100
142
  function clamp(value, min, max) {
101
143
  return value < min ? min : value > max ? max : value;
102
144
  }
103
- function sleep(ms) {
104
- return new Promise((resolve) => setTimeout(resolve, ms));
145
+ async function executeRead(target, ctx, options = {}) {
146
+ let words = 0;
147
+ let locator;
148
+ if (typeof target === "string") {
149
+ locator = ctx.page.locator(target);
150
+ } else if ("words" in target) {
151
+ words = target.words;
152
+ } else if ("text" in target) {
153
+ words = countWords(target.text);
154
+ } else {
155
+ locator = target;
156
+ }
157
+ let autoDetectedKind;
158
+ if (locator) {
159
+ if (options.scrollIntoView) {
160
+ await locator.scrollIntoViewIfNeeded();
161
+ }
162
+ const text = await locator.innerText().catch(() => "");
163
+ words = countWords(text);
164
+ if (options.kind === void 0) {
165
+ autoDetectedKind = await detectKindFromTag(locator);
166
+ }
167
+ }
168
+ const kind = options.kind ?? autoDetectedKind ?? "prose";
169
+ const durationMs = computeReadingDwellMs(words, ctx.personality.reading, ctx.rng, {
170
+ kind,
171
+ wpmMultiplier: options.wpmMultiplier,
172
+ personalitySpeed: ctx.personality.speed,
173
+ speedFactor: speedModeFactor(ctx.speed)
174
+ });
175
+ if (options.withMotion && locator && durationMs > 0) {
176
+ const box = await locator.boundingBox().catch(() => null);
177
+ if (box) {
178
+ const lineRects = await getLineRects(locator).catch(() => []);
179
+ const path = planReadingScan(box, ctx.rng, {
180
+ start: ctx.getMousePosition(),
181
+ lineRects: lineRects.length > 0 ? lineRects : void 0
182
+ });
183
+ await walkMouseAlongPath(ctx.page, path, durationMs);
184
+ const final = path[path.length - 1];
185
+ if (final) ctx.setMousePosition(final);
186
+ return { words, durationMs, kind };
187
+ }
188
+ }
189
+ if (durationMs > 0) await sleep(durationMs);
190
+ return { words, durationMs, kind };
191
+ }
192
+ async function getLineRects(locator) {
193
+ const result = await locator.evaluate((el) => {
194
+ const walker = el.ownerDocument.createTreeWalker(el, 4);
195
+ const rects = [];
196
+ let node = walker.nextNode();
197
+ while (node) {
198
+ const text = node.textContent ?? "";
199
+ if (text.trim().length > 0) {
200
+ const range = el.ownerDocument.createRange();
201
+ range.selectNodeContents(node);
202
+ for (const r of Array.from(range.getClientRects())) {
203
+ if (r.width > 0 && r.height > 0) {
204
+ rects.push({ x: r.x, y: r.y, width: r.width, height: r.height });
205
+ }
206
+ }
207
+ }
208
+ node = walker.nextNode();
209
+ }
210
+ rects.sort((a, b) => a.y - b.y || a.x - b.x);
211
+ const merged = [];
212
+ for (const r of rects) {
213
+ const last = merged[merged.length - 1];
214
+ if (last && Math.abs(last.y - r.y) < 1 && r.x - (last.x + last.width) < 6) {
215
+ const right = Math.max(last.x + last.width, r.x + r.width);
216
+ const bottom = Math.max(last.y + last.height, r.y + r.height);
217
+ last.width = right - last.x;
218
+ last.height = bottom - last.y;
219
+ } else {
220
+ merged.push({ ...r });
221
+ }
222
+ }
223
+ return merged;
224
+ });
225
+ if (!Array.isArray(result)) return [];
226
+ return result.filter(
227
+ (r) => r != null && typeof r === "object" && typeof r.x === "number" && typeof r.y === "number" && typeof r.width === "number" && typeof r.height === "number"
228
+ );
229
+ }
230
+ async function detectKindFromTag(locator) {
231
+ const tag = await locator.evaluate((el) => el.tagName?.toLowerCase() ?? "").catch(() => "");
232
+ if (tag === "pre" || tag === "code") return "code";
233
+ return void 0;
234
+ }
235
+ var RESERVED_TARGETS = /* @__PURE__ */ new Set(["natural", "end", "top"]);
236
+ async function executeScroll(target, ctx, options = {}) {
237
+ const { page, personality, rng, speed } = ctx;
238
+ const speedFactor = speedModeFactor(speed);
239
+ const axis = options.axis ?? "y";
240
+ const container = resolveWithin(options.within, ctx);
241
+ const geom = container ? await readContainerGeometry(container, axis) : await readWindowGeometry(page, axis);
242
+ if (!geom) {
243
+ return { from: 0, to: 0, distance: 0, durationMs: 0 };
244
+ }
245
+ const from = geom.current;
246
+ const targetPos = await resolveTarget(target, ctx, geom, container, axis, options.block);
247
+ const to = clamp2(targetPos, 0, Math.max(0, geom.total - geom.viewport));
248
+ const distance = to - from;
249
+ if (distance === 0) {
250
+ return { from, to, distance: 0, durationMs: 0 };
251
+ }
252
+ if (speed === "instant") {
253
+ if (container) {
254
+ await container.evaluate(
255
+ (el, args) => {
256
+ const a = args;
257
+ if (a.axis === "x") el.scrollTo(a.pos, el.scrollTop);
258
+ else el.scrollTo(el.scrollLeft, a.pos);
259
+ },
260
+ { axis, pos: to }
261
+ );
262
+ } else {
263
+ await page.evaluate(
264
+ (args) => {
265
+ if (args.axis === "x") window.scrollTo(args.pos, window.scrollY);
266
+ else window.scrollTo(window.scrollX, args.pos);
267
+ },
268
+ { axis, pos: to }
269
+ );
270
+ }
271
+ return { from, to, distance, durationMs: 0 };
272
+ }
273
+ const segments = planScroll(from, to, personality.scroll, rng, {
274
+ forceOvershoot: options.overshoot,
275
+ withPauses: options.withPauses,
276
+ personalitySpeed: personality.speed,
277
+ speedFactor
278
+ });
279
+ if (container && geom.hover) {
280
+ await page.mouse.move(geom.hover.x, geom.hover.y);
281
+ }
282
+ const startedAt = Date.now();
283
+ await walkSegments(page, segments, axis, container);
284
+ const durationMs = Date.now() - startedAt;
285
+ return { from, to, distance, durationMs };
286
+ }
287
+ function resolveWithin(within, ctx) {
288
+ if (!within) return null;
289
+ return typeof within === "string" ? ctx.page.locator(within) : within;
290
+ }
291
+ async function readWindowGeometry(page, axis) {
292
+ const g = await page.evaluate((a) => {
293
+ if (a === "x") {
294
+ return {
295
+ current: window.scrollX,
296
+ viewport: window.innerWidth,
297
+ total: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0)
298
+ };
299
+ }
300
+ return {
301
+ current: window.scrollY,
302
+ viewport: window.innerHeight,
303
+ total: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0)
304
+ };
305
+ }, axis);
306
+ return { current: g.current, viewport: g.viewport, total: g.total };
307
+ }
308
+ async function readContainerGeometry(container, axis) {
309
+ return container.evaluate((el, a) => {
310
+ const rect = el.getBoundingClientRect();
311
+ const isX = a === "x";
312
+ return {
313
+ current: isX ? el.scrollLeft : el.scrollTop,
314
+ viewport: isX ? el.clientWidth : el.clientHeight,
315
+ total: isX ? el.scrollWidth : el.scrollHeight,
316
+ hover: {
317
+ x: rect.left + rect.width / 2,
318
+ y: rect.top + rect.height / 2
319
+ }
320
+ };
321
+ }, axis).catch(() => null);
322
+ }
323
+ async function resolveTarget(target, ctx, geom, container, axis, block = "start") {
324
+ if (target === void 0 || target === "natural") return geom.current + geom.viewport;
325
+ if (target === "end") return geom.total;
326
+ if (target === "top") return 0;
327
+ if (typeof target === "object" && "by" in target) return geom.current + target.by;
328
+ if (typeof target === "object" && "to" in target) return target.to;
329
+ const elementLocator = typeof target === "string" && !RESERVED_TARGETS.has(target) ? ctx.page.locator(target) : typeof target === "string" ? null : target;
330
+ if (!elementLocator) return geom.current + geom.viewport;
331
+ return container ? resolveElementWithinContainer(elementLocator, container, geom, axis, block) : resolveElementInWindow(elementLocator, geom, axis, block);
332
+ }
333
+ async function resolveElementInWindow(elementLocator, geom, axis, block) {
334
+ const rect = await elementLocator.boundingBox().catch(() => null);
335
+ if (!rect) return geom.current;
336
+ const relStart = axis === "x" ? rect.x : rect.y;
337
+ const length = axis === "x" ? rect.width : rect.height;
338
+ const absoluteStart = geom.current + relStart;
339
+ const absoluteEnd = absoluteStart + length;
340
+ if (block === "start") return absoluteStart;
341
+ if (block === "end") return absoluteEnd - geom.viewport;
342
+ if (block === "nearest") {
343
+ if (relStart >= 0 && relStart + length <= geom.viewport) return geom.current;
344
+ if (relStart < 0) return absoluteStart;
345
+ return absoluteEnd - geom.viewport;
346
+ }
347
+ return absoluteStart - (geom.viewport - length) / 2;
348
+ }
349
+ async function resolveElementWithinContainer(elementLocator, container, geom, axis, block) {
350
+ const rects = await container.evaluate(
351
+ (containerEl, args) => {
352
+ const elementEl = args.sel ? document.querySelector(args.sel) : null;
353
+ const targetEl = elementEl ?? containerEl.querySelector(":scope > *");
354
+ if (!targetEl) return null;
355
+ const cRect = containerEl.getBoundingClientRect();
356
+ const eRect = targetEl.getBoundingClientRect();
357
+ return args.axis === "x" ? { relStart: eRect.left - cRect.left, length: eRect.width } : { relStart: eRect.top - cRect.top, length: eRect.height };
358
+ },
359
+ { sel: await locatorSelector(elementLocator), axis }
360
+ ).catch(() => null);
361
+ if (!rects) return geom.current;
362
+ const offsetStart = rects.relStart + geom.current;
363
+ const offsetEnd = offsetStart + rects.length;
364
+ if (block === "start") return offsetStart;
365
+ if (block === "end") return offsetEnd - geom.viewport;
366
+ if (block === "nearest") {
367
+ if (rects.relStart >= 0 && rects.relStart + rects.length <= geom.viewport) {
368
+ return geom.current;
369
+ }
370
+ if (rects.relStart < 0) return offsetStart;
371
+ return offsetEnd - geom.viewport;
372
+ }
373
+ return offsetStart - (geom.viewport - rects.length) / 2;
374
+ }
375
+ async function locatorSelector(locator) {
376
+ const s = locator.toString?.();
377
+ if (typeof s !== "string") return null;
378
+ const match = /locator\(['"](.+?)['"]/.exec(s);
379
+ if (!match) return null;
380
+ const raw = match[1] ?? "";
381
+ const eq = raw.indexOf("=");
382
+ return eq > 0 && /^[a-z]+$/.test(raw.slice(0, eq)) ? raw.slice(eq + 1) : raw;
383
+ }
384
+ async function walkSegments(page, segments, axis, container) {
385
+ for (const segment of segments) {
386
+ if (segment.delayBeforeMs > 0) await sleep(segment.delayBeforeMs);
387
+ if (segment.delta === 0) continue;
388
+ if (container) {
389
+ await container.evaluate(
390
+ (el, args) => {
391
+ const a = args;
392
+ if (a.axis === "x") el.scrollLeft += a.delta;
393
+ else el.scrollTop += a.delta;
394
+ },
395
+ { axis, delta: segment.delta }
396
+ );
397
+ } else if (axis === "x") {
398
+ await page.mouse.wheel(segment.delta, 0);
399
+ } else {
400
+ await page.mouse.wheel(0, segment.delta);
401
+ }
402
+ }
403
+ }
404
+ function clamp2(value, min, max) {
405
+ return value < min ? min : value > max ? max : value;
406
+ }
407
+
408
+ // src/mouse-helper/index.ts
409
+ var CURSOR_PATH = "M 0 0 L 16 6 L 8 9.5 L 5 19 Z";
410
+ async function installMouseHelper(target, options = {}) {
411
+ const config = {
412
+ color: options.color ?? "#f5a55c",
413
+ stroke: "#020203",
414
+ size: options.size ?? 22,
415
+ showClicks: options.showClicks ?? true,
416
+ haloOpacity: options.haloOpacity ?? 0.18,
417
+ path: CURSOR_PATH
418
+ };
419
+ await target.addInitScript(installScript, config);
420
+ const pages = "pages" in target ? target.pages() : [target];
421
+ await Promise.all(
422
+ pages.map((page) => page.evaluate(installScript, config).catch(() => void 0))
423
+ );
424
+ }
425
+ function installScript(config) {
426
+ const guardKey = "__humanjsMouseHelperInstalled";
427
+ const w = window;
428
+ if (w[guardKey]) return;
429
+ w[guardKey] = true;
430
+ const attach = () => {
431
+ const cursor = document.createElement("div");
432
+ cursor.setAttribute("aria-hidden", "true");
433
+ cursor.setAttribute("data-humanjs-cursor", "true");
434
+ cursor.style.cssText = [
435
+ "position: fixed",
436
+ "left: 0",
437
+ "top: 0",
438
+ `width: ${config.size}px`,
439
+ `height: ${config.size + 4}px`,
440
+ "pointer-events: none",
441
+ "z-index: 2147483647",
442
+ // Start visible at (0, 0) so the cursor is on screen from the moment
443
+ // the page loads — without this the helper looks like nothing happened
444
+ // until the first mousemove arrives.
445
+ "opacity: 1",
446
+ "transform: translate(0px, 0px)",
447
+ // CSS interpolates between successive `mousemove` updates so the
448
+ // cursor reads as continuous motion instead of discrete hops. Slightly
449
+ // longer than the path-walker's typical step interval (~30–80ms) so
450
+ // each tween is still settling when the next move lands → no pauses.
451
+ "transition: transform 110ms ease-out, opacity 0.18s ease-out",
452
+ "will-change: transform"
453
+ ].join("; ");
454
+ const haloRadius = Math.round(config.size * 0.6);
455
+ cursor.innerHTML = `
456
+ <svg width="${config.size}" height="${config.size + 4}" viewBox="0 0 22 24" style="overflow: visible;">
457
+ <circle cx="0" cy="0" r="${haloRadius}" fill="${config.color}" opacity="${config.haloOpacity}" />
458
+ <path d="${config.path}" fill="${config.color}" stroke="${config.stroke}" stroke-width="0.7" stroke-linejoin="round" />
459
+ </svg>
460
+ `;
461
+ document.body.appendChild(cursor);
462
+ let lastX = 0;
463
+ let lastY = 0;
464
+ const onMove = (e) => {
465
+ lastX = e.clientX;
466
+ lastY = e.clientY;
467
+ cursor.style.transform = `translate(${lastX}px, ${lastY}px)`;
468
+ cursor.style.opacity = "1";
469
+ };
470
+ window.addEventListener("mousemove", onMove, { capture: true, passive: true });
471
+ document.addEventListener("mousemove", onMove, { capture: true, passive: true });
472
+ document.addEventListener(
473
+ "mouseleave",
474
+ () => {
475
+ cursor.style.opacity = "0";
476
+ },
477
+ { capture: true, passive: true }
478
+ );
479
+ if (config.showClicks) {
480
+ const styleEl = document.createElement("style");
481
+ styleEl.textContent = "@keyframes humanjs-ripple { 0% { transform: translate(-50%, -50%) scale(0.4); opacity: 0.9; } 100% { transform: translate(-50%, -50%) scale(2); opacity: 0; } }";
482
+ document.head.appendChild(styleEl);
483
+ window.addEventListener(
484
+ "mousedown",
485
+ () => {
486
+ const ripple = document.createElement("div");
487
+ ripple.style.cssText = [
488
+ "position: fixed",
489
+ `left: ${lastX}px`,
490
+ `top: ${lastY}px`,
491
+ "width: 28px",
492
+ "height: 28px",
493
+ "border-radius: 50%",
494
+ `border: 1.5px solid ${config.color}`,
495
+ "pointer-events: none",
496
+ "z-index: 2147483646",
497
+ "animation: humanjs-ripple 0.45s ease-out forwards"
498
+ ].join("; ");
499
+ document.body.appendChild(ripple);
500
+ window.setTimeout(() => ripple.remove(), 500);
501
+ },
502
+ { capture: true, passive: true }
503
+ );
504
+ }
505
+ };
506
+ if (document.body) attach();
507
+ else document.addEventListener("DOMContentLoaded", attach, { once: true });
105
508
  }
509
+
510
+ // src/index.ts
106
511
  async function createHuman(page, options = {}) {
107
512
  const personality = resolvePersonality(options.personality ?? "careful");
108
513
  const rng = createRng(options.seed);
@@ -157,10 +562,73 @@ async function createHuman(page, options = {}) {
157
562
  }
158
563
  });
159
564
  });
565
+ },
566
+ async type(target, value) {
567
+ const description = typeof target === "string" ? target : target.toString?.() ?? "locator";
568
+ await performAction(
569
+ { type: "type", params: { target: description, length: value.length } },
570
+ async () => {
571
+ await executeType(target, value, { page, personality, rng, speed });
572
+ }
573
+ );
574
+ },
575
+ async read(target, options2) {
576
+ const description = describeReadTarget(target);
577
+ return performAction(
578
+ {
579
+ type: "read",
580
+ params: {
581
+ target: description,
582
+ kind: options2?.kind
583
+ }
584
+ },
585
+ () => executeRead(
586
+ target,
587
+ {
588
+ page,
589
+ personality,
590
+ rng,
591
+ speed,
592
+ // Read shares the session's tracked cursor position so an eye
593
+ // scan starts from where the last click left off, and the next
594
+ // click starts from where the scan ended.
595
+ getMousePosition: () => lastMousePosition,
596
+ setMousePosition: (point) => {
597
+ lastMousePosition = point;
598
+ }
599
+ },
600
+ options2
601
+ )
602
+ );
603
+ },
604
+ async scroll(target, options2) {
605
+ const description = describeScrollTarget(target);
606
+ return performAction(
607
+ {
608
+ type: "scroll",
609
+ params: { target: description }
610
+ },
611
+ () => executeScroll(target, { page, personality, rng, speed }, options2)
612
+ );
160
613
  }
161
614
  };
162
615
  }
616
+ function describeScrollTarget(target) {
617
+ if (target === void 0) return "natural";
618
+ if (typeof target === "string") return target;
619
+ if ("by" in target) return `by:${target.by}`;
620
+ if ("to" in target) return `to:${target.to}`;
621
+ return target.toString?.() ?? "locator";
622
+ }
623
+ function describeReadTarget(target) {
624
+ if (typeof target === "string") return target;
625
+ if ("words" in target && typeof target.words === "number") return `${target.words} words`;
626
+ if ("text" in target && typeof target.text === "string") {
627
+ return `text:${target.text.length} chars`;
628
+ }
629
+ return target.toString?.() ?? "locator";
630
+ }
163
631
 
164
- export { createHuman };
632
+ export { createHuman, installMouseHelper };
165
633
  //# sourceMappingURL=index.js.map
166
634
  //# sourceMappingURL=index.js.map