@humanjs/playwright 0.4.0 → 0.5.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
@@ -63,6 +63,73 @@ async function dispatchKey(page, key) {
63
63
  await page.keyboard.insertText(key);
64
64
  }
65
65
  }
66
+ async function executePaste(target, value, ctx) {
67
+ if (value.length === 0) return { characters: 0 };
68
+ const locator = typeof target === "string" ? ctx.page.locator(target) : target;
69
+ await locator.focus();
70
+ await ctx.page.keyboard.insertText(value);
71
+ return { characters: value.length };
72
+ }
73
+ async function executePress(key, ctx) {
74
+ const dispatched = resolveChord(key);
75
+ await ctx.page.keyboard.press(dispatched);
76
+ return { dispatched };
77
+ }
78
+ function resolveChord(key) {
79
+ const parts = key.split("+").map((p) => p.trim()).filter((p) => p.length > 0);
80
+ if (parts.length === 0) {
81
+ throw new Error(`Invalid key: ${JSON.stringify(key)} \u2014 empty or only separators`);
82
+ }
83
+ const keyToken = parts[parts.length - 1];
84
+ if (keyToken === void 0) {
85
+ throw new Error(`Invalid key: ${JSON.stringify(key)} \u2014 missing key`);
86
+ }
87
+ const modifierTokens = parts.slice(0, -1);
88
+ const modifiers = [];
89
+ for (const token of modifierTokens) {
90
+ const resolved = resolveModifier(token);
91
+ if (resolved === null) {
92
+ throw new Error(
93
+ `Invalid key modifier: ${JSON.stringify(token)} in ${JSON.stringify(key)}. Use one of: Mod/CmdOrCtrl/CommandOrControl, Cmd/Command/Meta/Win/Super, Ctrl/Control, Alt/Option/Opt, Shift.`
94
+ );
95
+ }
96
+ modifiers.push(resolved);
97
+ }
98
+ return [...modifiers, normalizeKey(keyToken)].join("+");
99
+ }
100
+ function resolveModifier(token) {
101
+ const lower = token.toLowerCase();
102
+ switch (lower) {
103
+ case "mod":
104
+ case "cmdorctrl":
105
+ case "commandorcontrol":
106
+ return isMac() ? "Meta" : "Control";
107
+ case "cmd":
108
+ case "command":
109
+ case "meta":
110
+ case "win":
111
+ case "super":
112
+ return "Meta";
113
+ case "ctrl":
114
+ case "control":
115
+ return "Control";
116
+ case "alt":
117
+ case "option":
118
+ case "opt":
119
+ return "Alt";
120
+ case "shift":
121
+ return "Shift";
122
+ default:
123
+ return null;
124
+ }
125
+ }
126
+ function normalizeKey(key) {
127
+ if (key.length === 1) return key.toUpperCase();
128
+ return key.charAt(0).toUpperCase() + key.slice(1);
129
+ }
130
+ function isMac() {
131
+ return process.platform === "darwin";
132
+ }
66
133
 
67
134
  // src/internal/mouse-walk.ts
68
135
  async function walkMouseAlongPath(page, path, durationMs) {
@@ -77,31 +144,191 @@ async function walkMouseAlongPath(page, path, durationMs) {
77
144
  }
78
145
  }
79
146
  }
147
+ var RESERVED_TARGETS = /* @__PURE__ */ new Set(["natural", "end", "top"]);
148
+ async function executeScroll(target, ctx, options = {}) {
149
+ const { page, personality, rng, speed } = ctx;
150
+ const speedFactor = speedModeFactor(speed);
151
+ const axis = options.axis ?? "y";
152
+ const container = resolveWithin(options.within, ctx);
153
+ const geom = container ? await readContainerGeometry(container, axis) : await readWindowGeometry(page, axis);
154
+ if (!geom) {
155
+ return { from: 0, to: 0, distance: 0, durationMs: 0 };
156
+ }
157
+ const from = geom.current;
158
+ const targetPos = await resolveTarget(target, ctx, geom, container, axis, options.block);
159
+ const to = clamp(targetPos, 0, Math.max(0, geom.total - geom.viewport));
160
+ const distance = to - from;
161
+ if (distance === 0) {
162
+ return { from, to, distance: 0, durationMs: 0 };
163
+ }
164
+ if (speed === "instant") {
165
+ if (container) {
166
+ await container.evaluate(
167
+ (el, args) => {
168
+ const a = args;
169
+ if (a.axis === "x") el.scrollTo(a.pos, el.scrollTop);
170
+ else el.scrollTo(el.scrollLeft, a.pos);
171
+ },
172
+ { axis, pos: to }
173
+ );
174
+ } else {
175
+ await page.evaluate(
176
+ (args) => {
177
+ if (args.axis === "x") window.scrollTo(args.pos, window.scrollY);
178
+ else window.scrollTo(window.scrollX, args.pos);
179
+ },
180
+ { axis, pos: to }
181
+ );
182
+ }
183
+ return { from, to, distance, durationMs: 0 };
184
+ }
185
+ const segments = planScroll(from, to, personality.scroll, rng, {
186
+ forceOvershoot: options.overshoot,
187
+ withPauses: options.withPauses,
188
+ personalitySpeed: personality.speed,
189
+ speedFactor
190
+ });
191
+ if (container && geom.hover) {
192
+ await page.mouse.move(geom.hover.x, geom.hover.y);
193
+ }
194
+ const startedAt = Date.now();
195
+ await walkSegments(page, segments, axis, container);
196
+ const durationMs = Date.now() - startedAt;
197
+ return { from, to, distance, durationMs };
198
+ }
199
+ function resolveWithin(within, ctx) {
200
+ if (!within) return null;
201
+ return typeof within === "string" ? ctx.page.locator(within) : within;
202
+ }
203
+ async function readWindowGeometry(page, axis) {
204
+ const g = await page.evaluate((a) => {
205
+ if (a === "x") {
206
+ return {
207
+ current: window.scrollX,
208
+ viewport: window.innerWidth,
209
+ total: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0)
210
+ };
211
+ }
212
+ return {
213
+ current: window.scrollY,
214
+ viewport: window.innerHeight,
215
+ total: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0)
216
+ };
217
+ }, axis);
218
+ return { current: g.current, viewport: g.viewport, total: g.total };
219
+ }
220
+ async function readContainerGeometry(container, axis) {
221
+ return container.evaluate((el, a) => {
222
+ const rect = el.getBoundingClientRect();
223
+ const isX = a === "x";
224
+ return {
225
+ current: isX ? el.scrollLeft : el.scrollTop,
226
+ viewport: isX ? el.clientWidth : el.clientHeight,
227
+ total: isX ? el.scrollWidth : el.scrollHeight,
228
+ hover: {
229
+ x: rect.left + rect.width / 2,
230
+ y: rect.top + rect.height / 2
231
+ }
232
+ };
233
+ }, axis).catch(() => null);
234
+ }
235
+ async function resolveTarget(target, ctx, geom, container, axis, block = "start") {
236
+ if (target === void 0 || target === "natural") return geom.current + geom.viewport;
237
+ if (target === "end") return geom.total;
238
+ if (target === "top") return 0;
239
+ if (typeof target === "object" && "by" in target) return geom.current + target.by;
240
+ if (typeof target === "object" && "to" in target) return target.to;
241
+ const elementLocator = typeof target === "string" && !RESERVED_TARGETS.has(target) ? ctx.page.locator(target) : typeof target === "string" ? null : target;
242
+ if (!elementLocator) return geom.current + geom.viewport;
243
+ return container ? resolveElementWithinContainer(elementLocator, container, geom, axis, block) : resolveElementInWindow(elementLocator, geom, axis, block);
244
+ }
245
+ async function resolveElementInWindow(elementLocator, geom, axis, block) {
246
+ const rect = await elementLocator.boundingBox().catch(() => null);
247
+ if (!rect) return geom.current;
248
+ const relStart = axis === "x" ? rect.x : rect.y;
249
+ const length = axis === "x" ? rect.width : rect.height;
250
+ const absoluteStart = geom.current + relStart;
251
+ const absoluteEnd = absoluteStart + length;
252
+ if (block === "start") return absoluteStart;
253
+ if (block === "end") return absoluteEnd - geom.viewport;
254
+ if (block === "nearest") {
255
+ if (relStart >= 0 && relStart + length <= geom.viewport) return geom.current;
256
+ if (relStart < 0) return absoluteStart;
257
+ return absoluteEnd - geom.viewport;
258
+ }
259
+ return absoluteStart - (geom.viewport - length) / 2;
260
+ }
261
+ async function resolveElementWithinContainer(elementLocator, container, geom, axis, block) {
262
+ const rects = await container.evaluate(
263
+ (containerEl, args) => {
264
+ const elementEl = args.sel ? document.querySelector(args.sel) : null;
265
+ const targetEl = elementEl ?? containerEl.querySelector(":scope > *");
266
+ if (!targetEl) return null;
267
+ const cRect = containerEl.getBoundingClientRect();
268
+ const eRect = targetEl.getBoundingClientRect();
269
+ return args.axis === "x" ? { relStart: eRect.left - cRect.left, length: eRect.width } : { relStart: eRect.top - cRect.top, length: eRect.height };
270
+ },
271
+ { sel: await locatorSelector(elementLocator), axis }
272
+ ).catch(() => null);
273
+ if (!rects) return geom.current;
274
+ const offsetStart = rects.relStart + geom.current;
275
+ const offsetEnd = offsetStart + rects.length;
276
+ if (block === "start") return offsetStart;
277
+ if (block === "end") return offsetEnd - geom.viewport;
278
+ if (block === "nearest") {
279
+ if (rects.relStart >= 0 && rects.relStart + rects.length <= geom.viewport) {
280
+ return geom.current;
281
+ }
282
+ if (rects.relStart < 0) return offsetStart;
283
+ return offsetEnd - geom.viewport;
284
+ }
285
+ return offsetStart - (geom.viewport - rects.length) / 2;
286
+ }
287
+ async function locatorSelector(locator) {
288
+ const s = locator.toString?.();
289
+ if (typeof s !== "string") return null;
290
+ const match = /locator\(['"](.+?)['"]/.exec(s);
291
+ if (!match) return null;
292
+ const raw = match[1] ?? "";
293
+ const eq = raw.indexOf("=");
294
+ return eq > 0 && /^[a-z]+$/.test(raw.slice(0, eq)) ? raw.slice(eq + 1) : raw;
295
+ }
296
+ async function walkSegments(page, segments, axis, container) {
297
+ for (const segment of segments) {
298
+ if (segment.delayBeforeMs > 0) await sleep(segment.delayBeforeMs);
299
+ if (segment.delta === 0) continue;
300
+ if (container) {
301
+ await container.evaluate(
302
+ (el, args) => {
303
+ const a = args;
304
+ if (a.axis === "x") el.scrollLeft += a.delta;
305
+ else el.scrollTop += a.delta;
306
+ },
307
+ { axis, delta: segment.delta }
308
+ );
309
+ } else if (axis === "x") {
310
+ await page.mouse.wheel(segment.delta, 0);
311
+ } else {
312
+ await page.mouse.wheel(0, segment.delta);
313
+ }
314
+ }
315
+ }
316
+ function clamp(value, min, max) {
317
+ return value < min ? min : value > max ? max : value;
318
+ }
80
319
 
81
320
  // src/mouse/index.ts
82
- async function executeClick(target, ctx) {
321
+ async function executeClick(target, ctx, options = {}) {
322
+ const button = options.button ?? "left";
83
323
  const locator = typeof target === "string" ? ctx.page.locator(target) : target;
84
324
  if (ctx.speed === "instant") {
85
- const box2 = await locator.boundingBox();
86
- await locator.click();
87
- const center = box2 ? { x: box2.x + box2.width / 2, y: box2.y + box2.height / 2 } : ctx.getMousePosition();
325
+ const box = await locator.boundingBox();
326
+ await locator.click({ button });
327
+ const center = box ? { x: box.x + box.width / 2, y: box.y + box.height / 2 } : ctx.getMousePosition();
88
328
  ctx.setMousePosition(center);
89
329
  return { target: center };
90
330
  }
91
- const box = await locator.boundingBox();
92
- if (!box) {
93
- throw new Error(
94
- `Cannot click: element not found or has no bounding box (target: ${describeTarget(target)})`
95
- );
96
- }
97
- const targetPoint = pickClickPoint(box, ctx.rng);
98
- const startPoint = ctx.getMousePosition();
99
- const rawPath = bezierPath(startPoint, targetPoint, ctx.rng, {
100
- curvature: ctx.personality.mouse.curvature
101
- });
102
- const path = humanizePath(rawPath, ctx.rng);
103
- const travelMs = computeTravelTime(path, ctx.personality, ctx.speed, ctx.rng);
104
- await walkMouseAlongPath(ctx.page, path, travelMs);
331
+ const targetPoint = await moveToTarget(target, ctx, "click");
105
332
  const preClickMs = computeDwellTime(
106
333
  ctx.personality.dwell.preClickMs,
107
334
  ctx.personality.dwell.preClickJitter,
@@ -111,7 +338,7 @@ async function executeClick(target, ctx) {
111
338
  );
112
339
  if (preClickMs > 0) await sleep(preClickMs);
113
340
  ctx.setMousePosition(targetPoint);
114
- await ctx.page.mouse.click(targetPoint.x, targetPoint.y);
341
+ await ctx.page.mouse.click(targetPoint.x, targetPoint.y, { button });
115
342
  const postActionMs = computeDwellTime(
116
343
  ctx.personality.dwell.postActionMs,
117
344
  ctx.personality.dwell.postActionJitter,
@@ -122,11 +349,249 @@ async function executeClick(target, ctx) {
122
349
  if (postActionMs > 0) await sleep(postActionMs);
123
350
  return { target: targetPoint };
124
351
  }
125
- function pickClickPoint(box, rng) {
352
+ async function executeHover(target, ctx) {
353
+ if (ctx.speed === "instant") {
354
+ const box = await readBoxWithAutoScroll(target, ctx, "hover");
355
+ const center = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
356
+ await ctx.page.mouse.move(center.x, center.y);
357
+ ctx.setMousePosition(center);
358
+ return { target: center };
359
+ }
360
+ const targetPoint = await moveToTarget(target, ctx, "hover");
361
+ const dwellMs = computeDwellTime(
362
+ ctx.personality.dwell.preClickMs,
363
+ ctx.personality.dwell.preClickJitter,
364
+ ctx.personality,
365
+ ctx.speed,
366
+ ctx.rng
367
+ );
368
+ if (dwellMs > 0) await sleep(dwellMs);
369
+ ctx.setMousePosition(targetPoint);
370
+ return { target: targetPoint };
371
+ }
372
+ async function executeDrag(from, to, ctx) {
373
+ const scrollYBeforeResolve = await readScrollY(ctx.page);
374
+ let { point: fromPoint, box: fromBox } = await resolveTargetPointAndBox(from, ctx, "drag");
375
+ let { point: toPoint, box: toBox } = await resolveTargetPointAndBox(to, ctx, "drag");
376
+ const resolveScrollDelta = await readScrollY(ctx.page) - scrollYBeforeResolve;
377
+ if (resolveScrollDelta !== 0) {
378
+ if (isPoint(from)) fromPoint = { x: fromPoint.x, y: fromPoint.y - resolveScrollDelta };
379
+ if (isPoint(to)) toPoint = { x: toPoint.x, y: toPoint.y - resolveScrollDelta };
380
+ }
381
+ if (ctx.speed === "instant") {
382
+ await ctx.page.mouse.move(fromPoint.x, fromPoint.y);
383
+ await ctx.page.mouse.down();
384
+ await ctx.page.mouse.move(toPoint.x, toPoint.y);
385
+ await ctx.page.mouse.up();
386
+ ctx.setMousePosition(toPoint);
387
+ return { from: fromPoint, to: toPoint };
388
+ }
389
+ if (fromBox && toBox) {
390
+ const scrollDelta = computeCurveScrollDelta(
391
+ fromPoint,
392
+ toPoint,
393
+ ctx.page.viewportSize(),
394
+ ctx.personality.mouse.curvature
395
+ );
396
+ if (scrollDelta !== 0) {
397
+ await executeScroll({ by: scrollDelta }, ctx, {});
398
+ const refreshedFrom = await resolveTargetPointAndBox(from, ctx, "drag");
399
+ fromPoint = refreshedFrom.point;
400
+ fromBox = refreshedFrom.box;
401
+ const refreshedTo = await resolveTargetPointAndBox(to, ctx, "drag");
402
+ toPoint = refreshedTo.point;
403
+ toBox = refreshedTo.box;
404
+ }
405
+ }
406
+ await maybeMisclickBeat(ctx, fromBox, fromPoint);
407
+ await walkBezierTo(fromPoint, ctx);
408
+ ctx.setMousePosition(fromPoint);
409
+ const preDragMs = computeDwellTime(
410
+ ctx.personality.dwell.preClickMs,
411
+ ctx.personality.dwell.preClickJitter,
412
+ ctx.personality,
413
+ ctx.speed,
414
+ ctx.rng
415
+ );
416
+ if (preDragMs > 0) await sleep(preDragMs);
417
+ await ctx.page.mouse.down();
418
+ await maybeMisclickBeat(ctx, toBox, toPoint);
419
+ await walkBezierTo(toPoint, ctx);
420
+ await ctx.page.mouse.up();
421
+ ctx.setMousePosition(toPoint);
422
+ const postActionMs = computeDwellTime(
423
+ ctx.personality.dwell.postActionMs,
424
+ ctx.personality.dwell.postActionJitter,
425
+ ctx.personality,
426
+ ctx.speed,
427
+ ctx.rng
428
+ );
429
+ if (postActionMs > 0) await sleep(postActionMs);
430
+ return { from: fromPoint, to: toPoint };
431
+ }
432
+ async function executeMove(target, ctx) {
433
+ const point = await resolveTargetPoint(target, ctx, "move");
434
+ if (ctx.speed === "instant") {
435
+ await ctx.page.mouse.move(point.x, point.y);
436
+ ctx.setMousePosition(point);
437
+ return { target: point };
438
+ }
439
+ await walkBezierTo(point, ctx);
440
+ ctx.setMousePosition(point);
441
+ return { target: point };
442
+ }
443
+ async function moveToTarget(target, ctx, action) {
444
+ const box = await readBoxWithAutoScroll(target, ctx, action);
445
+ const targetPoint = pickClickPoint(box, ctx.rng, ctx.personality.mouse.clickSpread);
446
+ if (action === "click") await maybeMisclickBeat(ctx, box, targetPoint);
447
+ await walkBezierTo(targetPoint, ctx);
448
+ return targetPoint;
449
+ }
450
+ async function walkBezierTo(to, ctx) {
451
+ const startPoint = ctx.getMousePosition();
452
+ const rawPath = bezierPath(startPoint, to, ctx.rng, {
453
+ curvature: ctx.personality.mouse.curvature
454
+ });
455
+ const path = humanizePath(rawPath, ctx.rng);
456
+ const travelMs = computeTravelTime(path, ctx.personality, ctx.speed, ctx.rng);
457
+ await walkMouseAlongPath(ctx.page, path, travelMs);
458
+ }
459
+ async function resolveTargetPoint(target, ctx, action) {
460
+ if (isPoint(target)) return target;
461
+ return resolveLocatorPoint(target, ctx, action);
462
+ }
463
+ async function resolveTargetPointAndBox(target, ctx, action) {
464
+ if (isPoint(target)) return { point: target, box: null };
465
+ const box = await readBoxWithAutoScroll(target, ctx, action);
466
+ const point = pickClickPoint(box, ctx.rng, ctx.personality.mouse.clickSpread);
467
+ return { point, box };
468
+ }
469
+ async function resolveLocatorPoint(target, ctx, action) {
470
+ const box = await readBoxWithAutoScroll(target, ctx, action);
471
+ return pickClickPoint(box, ctx.rng, ctx.personality.mouse.clickSpread);
472
+ }
473
+ async function readBoxWithAutoScroll(target, ctx, action) {
474
+ const locator = typeof target === "string" ? ctx.page.locator(target) : target;
475
+ let box = await locator.boundingBox();
476
+ if (!box) {
477
+ throw new Error(
478
+ `Cannot ${action}: element not found or has no bounding box (target: ${describeTarget(target)})`
479
+ );
480
+ }
481
+ const viewport = ctx.page.viewportSize();
482
+ if (viewport && !isBoxCenterInViewport(box, viewport)) {
483
+ if (ctx.speed === "instant") {
484
+ await locator.scrollIntoViewIfNeeded();
485
+ } else {
486
+ await executeScroll(locator, ctx, { block: "center" });
487
+ }
488
+ box = await locator.boundingBox();
489
+ if (!box) {
490
+ throw new Error(
491
+ `Cannot ${action}: element disappeared after scrolling into view (target: ${describeTarget(target)})`
492
+ );
493
+ }
494
+ }
495
+ return box;
496
+ }
497
+ function isBoxCenterInViewport(box, viewport) {
498
+ const cx = box.x + box.width / 2;
499
+ const cy = box.y + box.height / 2;
500
+ return cx >= 0 && cx <= viewport.width && cy >= 0 && cy <= viewport.height;
501
+ }
502
+ async function readScrollY(page) {
503
+ if (typeof page.evaluate !== "function") return 0;
504
+ return page.evaluate(() => window.scrollY);
505
+ }
506
+ var CURVE_VIEWPORT_MARGIN = 20;
507
+ function computeCurveScrollDelta(from, to, viewport, curvature) {
508
+ if (!viewport) return 0;
509
+ const distance = Math.hypot(to.x - from.x, to.y - from.y);
510
+ const perpendicularExtent = distance * curvature;
511
+ const minY = Math.min(from.y, to.y) - perpendicularExtent;
512
+ const maxY = Math.max(from.y, to.y) + perpendicularExtent;
513
+ const topOverflow = -minY + CURVE_VIEWPORT_MARGIN;
514
+ const bottomOverflow = maxY + CURVE_VIEWPORT_MARGIN - viewport.height;
515
+ if (bottomOverflow > 0 && bottomOverflow >= topOverflow) return bottomOverflow;
516
+ if (topOverflow > 0) return -topOverflow;
517
+ return 0;
518
+ }
519
+ function isPoint(target) {
520
+ return typeof target === "object" && target !== null && !("boundingBox" in target) && typeof target.x === "number" && typeof target.y === "number";
521
+ }
522
+ var MISCLICK_OFFSET_MIN = 5;
523
+ var MISCLICK_OFFSET_MAX = 15;
524
+ async function maybeMisclickBeat(ctx, box, targetPoint) {
525
+ if (!ctx.rng.chance(ctx.personality.mouse.misclickProbability)) return;
526
+ if (cursorAlreadyOnTarget(ctx.getMousePosition(), box, targetPoint)) return;
527
+ const viewport = ctx.page.viewportSize();
528
+ const misclickPoint = box ? pickMisclickOutsideBox(box, ctx.rng, viewport) : pickMisclickAroundPoint(targetPoint, ctx.rng, viewport);
529
+ if (misclickPoint === null) return;
530
+ await walkBezierTo(misclickPoint, ctx);
531
+ ctx.setMousePosition(misclickPoint);
532
+ const realizeMs = computeDwellTime(
533
+ ctx.personality.dwell.preClickMs,
534
+ ctx.personality.dwell.preClickJitter,
535
+ ctx.personality,
536
+ ctx.speed,
537
+ ctx.rng
538
+ );
539
+ if (realizeMs > 0) await sleep(realizeMs);
540
+ }
541
+ function cursorAlreadyOnTarget(current, box, targetPoint) {
542
+ if (box) {
543
+ return current.x >= box.x && current.x <= box.x + box.width && current.y >= box.y && current.y <= box.y + box.height;
544
+ }
545
+ const dx = current.x - targetPoint.x;
546
+ const dy = current.y - targetPoint.y;
547
+ return Math.hypot(dx, dy) < MISCLICK_OFFSET_MIN;
548
+ }
549
+ function pickMisclickOutsideBox(box, rng, viewport) {
550
+ const edge = rng.nextInt(0, 4);
551
+ const offset = rng.nextFloat(MISCLICK_OFFSET_MIN, MISCLICK_OFFSET_MAX);
552
+ const along = rng.nextFloat(0.2, 0.8);
553
+ let x;
554
+ let y;
555
+ if (edge === 0) {
556
+ x = box.x + box.width * along;
557
+ y = box.y - offset;
558
+ } else if (edge === 1) {
559
+ x = box.x + box.width + offset;
560
+ y = box.y + box.height * along;
561
+ } else if (edge === 2) {
562
+ x = box.x + box.width * along;
563
+ y = box.y + box.height + offset;
564
+ } else {
565
+ x = box.x - offset;
566
+ y = box.y + box.height * along;
567
+ }
568
+ if (viewport) {
569
+ x = clamp2(x, 0, viewport.width - 1);
570
+ y = clamp2(y, 0, viewport.height - 1);
571
+ }
572
+ const insideBox = x >= box.x && x <= box.x + box.width && y >= box.y && y <= box.y + box.height;
573
+ if (insideBox) return null;
574
+ return { x, y };
575
+ }
576
+ function pickMisclickAroundPoint(target, rng, viewport) {
577
+ const angle = rng.nextFloat(0, Math.PI * 2);
578
+ const distance = rng.nextFloat(MISCLICK_OFFSET_MIN, MISCLICK_OFFSET_MAX);
579
+ let x = target.x + Math.cos(angle) * distance;
580
+ let y = target.y + Math.sin(angle) * distance;
581
+ if (viewport) {
582
+ x = clamp2(x, 0, viewport.width - 1);
583
+ y = clamp2(y, 0, viewport.height - 1);
584
+ }
585
+ if (x === target.x && y === target.y) return null;
586
+ return { x, y };
587
+ }
588
+ function pickClickPoint(box, rng, clickSpread) {
126
589
  const cx = box.x + box.width / 2;
127
590
  const cy = box.y + box.height / 2;
128
- const x = clamp(cx + rng.nextGaussian(0, box.width / 8), box.x, box.x + box.width);
129
- const y = clamp(cy + rng.nextGaussian(0, box.height / 8), box.y, box.y + box.height);
591
+ const sigmaX = box.width * clickSpread;
592
+ const sigmaY = box.height * clickSpread;
593
+ const x = clamp2(cx + rng.nextGaussian(0, sigmaX), box.x, box.x + box.width);
594
+ const y = clamp2(cy + rng.nextGaussian(0, sigmaY), box.y, box.y + box.height);
130
595
  return { x, y };
131
596
  }
132
597
  function computeTravelTime(path, personality, speed, rng) {
@@ -144,9 +609,10 @@ function computeTravelTime(path, personality, speed, rng) {
144
609
  return Math.max(0, total);
145
610
  }
146
611
  function describeTarget(target) {
612
+ if (isPoint(target)) return `point(${target.x}, ${target.y})`;
147
613
  return typeof target === "string" ? target : target.toString?.() ?? "locator";
148
614
  }
149
- function clamp(value, min, max) {
615
+ function clamp2(value, min, max) {
150
616
  return value < min ? min : value > max ? max : value;
151
617
  }
152
618
  async function executeRead(target, ctx, options = {}) {
@@ -630,178 +1096,6 @@ async function startCapture(page, options = {}) {
630
1096
  }
631
1097
  };
632
1098
  }
633
- var RESERVED_TARGETS = /* @__PURE__ */ new Set(["natural", "end", "top"]);
634
- async function executeScroll(target, ctx, options = {}) {
635
- const { page, personality, rng, speed } = ctx;
636
- const speedFactor = speedModeFactor(speed);
637
- const axis = options.axis ?? "y";
638
- const container = resolveWithin(options.within, ctx);
639
- const geom = container ? await readContainerGeometry(container, axis) : await readWindowGeometry(page, axis);
640
- if (!geom) {
641
- return { from: 0, to: 0, distance: 0, durationMs: 0 };
642
- }
643
- const from = geom.current;
644
- const targetPos = await resolveTarget(target, ctx, geom, container, axis, options.block);
645
- const to = clamp2(targetPos, 0, Math.max(0, geom.total - geom.viewport));
646
- const distance = to - from;
647
- if (distance === 0) {
648
- return { from, to, distance: 0, durationMs: 0 };
649
- }
650
- if (speed === "instant") {
651
- if (container) {
652
- await container.evaluate(
653
- (el, args) => {
654
- const a = args;
655
- if (a.axis === "x") el.scrollTo(a.pos, el.scrollTop);
656
- else el.scrollTo(el.scrollLeft, a.pos);
657
- },
658
- { axis, pos: to }
659
- );
660
- } else {
661
- await page.evaluate(
662
- (args) => {
663
- if (args.axis === "x") window.scrollTo(args.pos, window.scrollY);
664
- else window.scrollTo(window.scrollX, args.pos);
665
- },
666
- { axis, pos: to }
667
- );
668
- }
669
- return { from, to, distance, durationMs: 0 };
670
- }
671
- const segments = planScroll(from, to, personality.scroll, rng, {
672
- forceOvershoot: options.overshoot,
673
- withPauses: options.withPauses,
674
- personalitySpeed: personality.speed,
675
- speedFactor
676
- });
677
- if (container && geom.hover) {
678
- await page.mouse.move(geom.hover.x, geom.hover.y);
679
- }
680
- const startedAt = Date.now();
681
- await walkSegments(page, segments, axis, container);
682
- const durationMs = Date.now() - startedAt;
683
- return { from, to, distance, durationMs };
684
- }
685
- function resolveWithin(within, ctx) {
686
- if (!within) return null;
687
- return typeof within === "string" ? ctx.page.locator(within) : within;
688
- }
689
- async function readWindowGeometry(page, axis) {
690
- const g = await page.evaluate((a) => {
691
- if (a === "x") {
692
- return {
693
- current: window.scrollX,
694
- viewport: window.innerWidth,
695
- total: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0)
696
- };
697
- }
698
- return {
699
- current: window.scrollY,
700
- viewport: window.innerHeight,
701
- total: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0)
702
- };
703
- }, axis);
704
- return { current: g.current, viewport: g.viewport, total: g.total };
705
- }
706
- async function readContainerGeometry(container, axis) {
707
- return container.evaluate((el, a) => {
708
- const rect = el.getBoundingClientRect();
709
- const isX = a === "x";
710
- return {
711
- current: isX ? el.scrollLeft : el.scrollTop,
712
- viewport: isX ? el.clientWidth : el.clientHeight,
713
- total: isX ? el.scrollWidth : el.scrollHeight,
714
- hover: {
715
- x: rect.left + rect.width / 2,
716
- y: rect.top + rect.height / 2
717
- }
718
- };
719
- }, axis).catch(() => null);
720
- }
721
- async function resolveTarget(target, ctx, geom, container, axis, block = "start") {
722
- if (target === void 0 || target === "natural") return geom.current + geom.viewport;
723
- if (target === "end") return geom.total;
724
- if (target === "top") return 0;
725
- if (typeof target === "object" && "by" in target) return geom.current + target.by;
726
- if (typeof target === "object" && "to" in target) return target.to;
727
- const elementLocator = typeof target === "string" && !RESERVED_TARGETS.has(target) ? ctx.page.locator(target) : typeof target === "string" ? null : target;
728
- if (!elementLocator) return geom.current + geom.viewport;
729
- return container ? resolveElementWithinContainer(elementLocator, container, geom, axis, block) : resolveElementInWindow(elementLocator, geom, axis, block);
730
- }
731
- async function resolveElementInWindow(elementLocator, geom, axis, block) {
732
- const rect = await elementLocator.boundingBox().catch(() => null);
733
- if (!rect) return geom.current;
734
- const relStart = axis === "x" ? rect.x : rect.y;
735
- const length = axis === "x" ? rect.width : rect.height;
736
- const absoluteStart = geom.current + relStart;
737
- const absoluteEnd = absoluteStart + length;
738
- if (block === "start") return absoluteStart;
739
- if (block === "end") return absoluteEnd - geom.viewport;
740
- if (block === "nearest") {
741
- if (relStart >= 0 && relStart + length <= geom.viewport) return geom.current;
742
- if (relStart < 0) return absoluteStart;
743
- return absoluteEnd - geom.viewport;
744
- }
745
- return absoluteStart - (geom.viewport - length) / 2;
746
- }
747
- async function resolveElementWithinContainer(elementLocator, container, geom, axis, block) {
748
- const rects = await container.evaluate(
749
- (containerEl, args) => {
750
- const elementEl = args.sel ? document.querySelector(args.sel) : null;
751
- const targetEl = elementEl ?? containerEl.querySelector(":scope > *");
752
- if (!targetEl) return null;
753
- const cRect = containerEl.getBoundingClientRect();
754
- const eRect = targetEl.getBoundingClientRect();
755
- return args.axis === "x" ? { relStart: eRect.left - cRect.left, length: eRect.width } : { relStart: eRect.top - cRect.top, length: eRect.height };
756
- },
757
- { sel: await locatorSelector(elementLocator), axis }
758
- ).catch(() => null);
759
- if (!rects) return geom.current;
760
- const offsetStart = rects.relStart + geom.current;
761
- const offsetEnd = offsetStart + rects.length;
762
- if (block === "start") return offsetStart;
763
- if (block === "end") return offsetEnd - geom.viewport;
764
- if (block === "nearest") {
765
- if (rects.relStart >= 0 && rects.relStart + rects.length <= geom.viewport) {
766
- return geom.current;
767
- }
768
- if (rects.relStart < 0) return offsetStart;
769
- return offsetEnd - geom.viewport;
770
- }
771
- return offsetStart - (geom.viewport - rects.length) / 2;
772
- }
773
- async function locatorSelector(locator) {
774
- const s = locator.toString?.();
775
- if (typeof s !== "string") return null;
776
- const match = /locator\(['"](.+?)['"]/.exec(s);
777
- if (!match) return null;
778
- const raw = match[1] ?? "";
779
- const eq = raw.indexOf("=");
780
- return eq > 0 && /^[a-z]+$/.test(raw.slice(0, eq)) ? raw.slice(eq + 1) : raw;
781
- }
782
- async function walkSegments(page, segments, axis, container) {
783
- for (const segment of segments) {
784
- if (segment.delayBeforeMs > 0) await sleep(segment.delayBeforeMs);
785
- if (segment.delta === 0) continue;
786
- if (container) {
787
- await container.evaluate(
788
- (el, args) => {
789
- const a = args;
790
- if (a.axis === "x") el.scrollLeft += a.delta;
791
- else el.scrollTop += a.delta;
792
- },
793
- { axis, delta: segment.delta }
794
- );
795
- } else if (axis === "x") {
796
- await page.mouse.wheel(segment.delta, 0);
797
- } else {
798
- await page.mouse.wheel(0, segment.delta);
799
- }
800
- }
801
- }
802
- function clamp2(value, min, max) {
803
- return value < min ? min : value > max ? max : value;
804
- }
805
1099
 
806
1100
  // src/mouse-helper/index.ts
807
1101
  var CURSOR_PATH = "M 0 0 L 16 6 L 8 9.5 L 5 19 Z";
@@ -966,6 +1260,23 @@ async function createHuman(page, options = {}) {
966
1260
  }
967
1261
  }
968
1262
  let lastMousePosition = options.initialMousePosition ?? { x: 0, y: 0 };
1263
+ const mouseCtx = () => ({
1264
+ page,
1265
+ personality,
1266
+ rng,
1267
+ speed,
1268
+ getMousePosition: () => lastMousePosition,
1269
+ setMousePosition: (point) => {
1270
+ lastMousePosition = point;
1271
+ }
1272
+ });
1273
+ const describeMouseTarget = (target) => {
1274
+ if (typeof target === "string") return target;
1275
+ if ("x" in target && "y" in target && typeof target.x === "number") {
1276
+ return `point(${target.x}, ${target.y})`;
1277
+ }
1278
+ return target.toString?.() ?? "locator";
1279
+ };
969
1280
  return {
970
1281
  personality,
971
1282
  speed,
@@ -975,29 +1286,65 @@ async function createHuman(page, options = {}) {
975
1286
  });
976
1287
  },
977
1288
  async click(target) {
978
- const description = typeof target === "string" ? target : target.toString?.() ?? "locator";
1289
+ const description = describeMouseTarget(target);
979
1290
  await performAction({ type: "click", params: { target: description } }, async () => {
980
- await executeClick(target, {
981
- page,
982
- personality,
983
- rng,
984
- speed,
985
- getMousePosition: () => lastMousePosition,
986
- setMousePosition: (point) => {
987
- lastMousePosition = point;
988
- }
989
- });
1291
+ await executeClick(target, mouseCtx());
1292
+ });
1293
+ },
1294
+ async rightClick(target) {
1295
+ const description = describeMouseTarget(target);
1296
+ await performAction({ type: "rightClick", params: { target: description } }, async () => {
1297
+ await executeClick(target, mouseCtx(), { button: "right" });
1298
+ });
1299
+ },
1300
+ async hover(target) {
1301
+ const description = describeMouseTarget(target);
1302
+ await performAction({ type: "hover", params: { target: description } }, async () => {
1303
+ await executeHover(target, mouseCtx());
1304
+ });
1305
+ },
1306
+ async move(target) {
1307
+ const description = describeMouseTarget(target);
1308
+ await performAction({ type: "move", params: { target: description } }, async () => {
1309
+ await executeMove(target, mouseCtx());
1310
+ });
1311
+ },
1312
+ async drag(from, to) {
1313
+ const fromDesc = describeMouseTarget(from);
1314
+ const toDesc = describeMouseTarget(to);
1315
+ await performAction({ type: "drag", params: { from: fromDesc, to: toDesc } }, async () => {
1316
+ await executeDrag(from, to, mouseCtx());
990
1317
  });
991
1318
  },
992
1319
  async type(target, value) {
993
- const description = typeof target === "string" ? target : target.toString?.() ?? "locator";
1320
+ const description = describeMouseTarget(target);
994
1321
  await performAction(
995
1322
  { type: "type", params: { target: description, length: value.length } },
996
1323
  async () => {
1324
+ if (speed !== "instant" && value.length > 0) {
1325
+ await executeClick(target, mouseCtx());
1326
+ }
997
1327
  await executeType(target, value, { page, personality, rng, speed });
998
1328
  }
999
1329
  );
1000
1330
  },
1331
+ async paste(target, value) {
1332
+ const description = describeMouseTarget(target);
1333
+ await performAction(
1334
+ { type: "paste", params: { target: description, length: value.length } },
1335
+ async () => {
1336
+ if (speed !== "instant" && value.length > 0) {
1337
+ await executeClick(target, mouseCtx());
1338
+ }
1339
+ await executePaste(target, value, { page, personality, rng, speed });
1340
+ }
1341
+ );
1342
+ },
1343
+ async press(key) {
1344
+ await performAction({ type: "press", params: { key } }, async () => {
1345
+ await executePress(key, { page, personality, rng, speed });
1346
+ });
1347
+ },
1001
1348
  async read(target, options2) {
1002
1349
  const description = describeReadTarget(target);
1003
1350
  return performAction(