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