@humanjs/playwright 0.1.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,5 +1,511 @@
1
- import { resolvePersonality, createRng } from '@humanjs/core';
2
- export { blend, careful, createRng, distracted, fast, 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
+
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
75
+ async function executeClick(target, ctx) {
76
+ const locator = typeof target === "string" ? ctx.page.locator(target) : target;
77
+ if (ctx.speed === "instant") {
78
+ const box2 = await locator.boundingBox();
79
+ await locator.click();
80
+ const center = box2 ? { x: box2.x + box2.width / 2, y: box2.y + box2.height / 2 } : ctx.getMousePosition();
81
+ ctx.setMousePosition(center);
82
+ return { target: center };
83
+ }
84
+ const box = await locator.boundingBox();
85
+ if (!box) {
86
+ throw new Error(
87
+ `Cannot click: element not found or has no bounding box (target: ${describeTarget(target)})`
88
+ );
89
+ }
90
+ const targetPoint = pickClickPoint(box, ctx.rng);
91
+ const startPoint = ctx.getMousePosition();
92
+ const rawPath = bezierPath(startPoint, targetPoint, ctx.rng, {
93
+ curvature: ctx.personality.mouse.curvature
94
+ });
95
+ const path = humanizePath(rawPath, ctx.rng);
96
+ const travelMs = computeTravelTime(path, ctx.personality, ctx.speed, ctx.rng);
97
+ await walkMouseAlongPath(ctx.page, path, travelMs);
98
+ const preClickMs = computeDwellTime(
99
+ ctx.personality.dwell.preClickMs,
100
+ ctx.personality.dwell.preClickJitter,
101
+ ctx.personality,
102
+ ctx.speed,
103
+ ctx.rng
104
+ );
105
+ if (preClickMs > 0) await sleep(preClickMs);
106
+ ctx.setMousePosition(targetPoint);
107
+ await ctx.page.mouse.click(targetPoint.x, targetPoint.y);
108
+ const postActionMs = computeDwellTime(
109
+ ctx.personality.dwell.postActionMs,
110
+ ctx.personality.dwell.postActionJitter,
111
+ ctx.personality,
112
+ ctx.speed,
113
+ ctx.rng
114
+ );
115
+ if (postActionMs > 0) await sleep(postActionMs);
116
+ return { target: targetPoint };
117
+ }
118
+ function pickClickPoint(box, rng) {
119
+ const cx = box.x + box.width / 2;
120
+ const cy = box.y + box.height / 2;
121
+ const x = clamp(cx + rng.nextGaussian(0, box.width / 8), box.x, box.x + box.width);
122
+ const y = clamp(cy + rng.nextGaussian(0, box.height / 8), box.y, box.y + box.height);
123
+ return { x, y };
124
+ }
125
+ function computeTravelTime(path, personality, speed, rng) {
126
+ let distance = 0;
127
+ for (let i = 1; i < path.length; i++) {
128
+ const prev = path[i - 1];
129
+ const curr = path[i];
130
+ if (!prev || !curr) continue;
131
+ distance += Math.hypot(curr.x - prev.x, curr.y - prev.y);
132
+ }
133
+ const baseTime = distance / 1e3 * personality.mouse.travelTimeMs;
134
+ const jitterMag = baseTime * personality.mouse.travelTimeJitter;
135
+ const jitter = rng.nextFloat(-jitterMag, jitterMag);
136
+ const total = (baseTime + jitter) * personality.speed * speedModeFactor(speed);
137
+ return Math.max(0, total);
138
+ }
139
+ function describeTarget(target) {
140
+ return typeof target === "string" ? target : target.toString?.() ?? "locator";
141
+ }
142
+ function clamp(value, min, max) {
143
+ return value < min ? min : value > max ? max : value;
144
+ }
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 });
508
+ }
3
509
 
4
510
  // src/index.ts
5
511
  async function createHuman(page, options = {}) {
@@ -33,6 +539,7 @@ async function createHuman(page, options = {}) {
33
539
  throw error;
34
540
  }
35
541
  }
542
+ let lastMousePosition = options.initialMousePosition ?? { x: 0, y: 0 };
36
543
  return {
37
544
  personality,
38
545
  speed,
@@ -40,10 +547,88 @@ async function createHuman(page, options = {}) {
40
547
  await performAction({ type: "goto", params: { url } }, async () => {
41
548
  await page.goto(url);
42
549
  });
550
+ },
551
+ async click(target) {
552
+ const description = typeof target === "string" ? target : target.toString?.() ?? "locator";
553
+ await performAction({ type: "click", params: { target: description } }, async () => {
554
+ await executeClick(target, {
555
+ page,
556
+ personality,
557
+ rng,
558
+ speed,
559
+ getMousePosition: () => lastMousePosition,
560
+ setMousePosition: (point) => {
561
+ lastMousePosition = point;
562
+ }
563
+ });
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
+ );
43
613
  }
44
614
  };
45
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
+ }
46
631
 
47
- export { createHuman };
632
+ export { createHuman, installMouseHelper };
48
633
  //# sourceMappingURL=index.js.map
49
634
  //# sourceMappingURL=index.js.map