@humanjs/playwright 0.7.0 → 0.9.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
@@ -1,1844 +1,104 @@
1
1
  'use strict';
2
2
 
3
- var core = require('@humanjs/core');
4
- var child_process = require('child_process');
5
- var fs = require('fs');
6
- var promises = require('fs/promises');
7
- var path = require('path');
8
- var ffmpegStatic = require('ffmpeg-static');
9
- var os = require('os');
10
- var playwright = require('playwright');
3
+ var chunk3X36PFTS_cjs = require('./chunk-3X36PFTS.cjs');
11
4
 
12
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
13
5
 
14
- var ffmpegStatic__default = /*#__PURE__*/_interopDefault(ffmpegStatic);
15
6
 
16
- // src/index.ts
17
-
18
- // src/internal/timing.ts
19
- function sleep(ms) {
20
- return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
21
- }
22
- function speedModeFactor(speed) {
23
- switch (speed) {
24
- case "fast":
25
- return 0.5;
26
- case "instant":
27
- return 0;
28
- default:
29
- return 1;
30
- }
31
- }
32
- function computeDwellTime(meanMs, jitter, personality, speed, rng) {
33
- if (meanMs <= 0) return 0;
34
- const jitterMag = meanMs * jitter;
35
- const offset = rng.nextFloat(-jitterMag, jitterMag);
36
- return Math.max(0, (meanMs + offset) * personality.speed * speedModeFactor(speed));
37
- }
38
-
39
- // src/keyboard/index.ts
40
- async function executeType(target, value, ctx) {
41
- const locator = typeof target === "string" ? ctx.page.locator(target) : target;
42
- if (value.length === 0) {
43
- return { characters: 0, typos: 0, corrections: 0 };
44
- }
45
- if (ctx.speed === "instant") {
46
- await locator.pressSequentially(value, { delay: 0 });
47
- return { characters: value.length, typos: 0, corrections: 0 };
48
- }
49
- await locator.focus();
50
- const plan = core.planTypeKeystrokes(value, ctx.personality.typing, ctx.rng, {
51
- personalitySpeed: ctx.personality.speed,
52
- speedFactor: speedModeFactor(ctx.speed)
53
- });
54
- let typos = 0;
55
- let corrections = 0;
56
- for (const step of plan) {
57
- if (step.delayBeforeMs > 0) await sleep(step.delayBeforeMs);
58
- await dispatchKey(ctx.page, step.key);
59
- if (step.isTypo) typos++;
60
- if (step.isCorrection) corrections++;
61
- }
62
- return { characters: value.length, typos, corrections };
63
- }
64
- async function dispatchKey(page, key) {
65
- if (key.length > 1 || key.charCodeAt(0) < 128) {
66
- await page.keyboard.press(key);
67
- } else {
68
- await page.keyboard.insertText(key);
69
- }
70
- }
71
- async function executePaste(target, value, ctx) {
72
- if (value.length === 0) return { characters: 0 };
73
- const locator = typeof target === "string" ? ctx.page.locator(target) : target;
74
- await locator.focus();
75
- await ctx.page.keyboard.insertText(value);
76
- return { characters: value.length };
77
- }
78
- async function executePress(key, ctx) {
79
- const dispatched = resolveChord(key);
80
- await ctx.page.keyboard.press(dispatched);
81
- return { dispatched };
82
- }
83
- function resolveChord(key) {
84
- const parts = key.split("+").map((p) => p.trim()).filter((p) => p.length > 0);
85
- if (parts.length === 0) {
86
- throw new Error(`Invalid key: ${JSON.stringify(key)} \u2014 empty or only separators`);
87
- }
88
- const keyToken = parts[parts.length - 1];
89
- if (keyToken === void 0) {
90
- throw new Error(`Invalid key: ${JSON.stringify(key)} \u2014 missing key`);
91
- }
92
- const modifierTokens = parts.slice(0, -1);
93
- const modifiers = [];
94
- for (const token of modifierTokens) {
95
- const resolved = resolveModifier(token);
96
- if (resolved === null) {
97
- throw new Error(
98
- `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.`
99
- );
100
- }
101
- modifiers.push(resolved);
102
- }
103
- return [...modifiers, normalizeKey(keyToken)].join("+");
104
- }
105
- function resolveModifier(token) {
106
- const lower = token.toLowerCase();
107
- switch (lower) {
108
- case "mod":
109
- case "cmdorctrl":
110
- case "commandorcontrol":
111
- return isMac() ? "Meta" : "Control";
112
- case "cmd":
113
- case "command":
114
- case "meta":
115
- case "win":
116
- case "super":
117
- return "Meta";
118
- case "ctrl":
119
- case "control":
120
- return "Control";
121
- case "alt":
122
- case "option":
123
- case "opt":
124
- return "Alt";
125
- case "shift":
126
- return "Shift";
127
- default:
128
- return null;
129
- }
130
- }
131
- function normalizeKey(key) {
132
- if (key.length === 1) return key.toUpperCase();
133
- return key.charAt(0).toUpperCase() + key.slice(1);
134
- }
135
- function isMac() {
136
- return process.platform === "darwin";
137
- }
138
-
139
- // src/internal/mouse-walk.ts
140
- async function walkMouseAlongPath(page, path, durationMs) {
141
- if (path.length === 0) return;
142
- const stepDelayMs = path.length > 1 && durationMs > 0 ? durationMs / (path.length - 1) : 0;
143
- for (let i = 0; i < path.length; i++) {
144
- const point = path[i];
145
- if (!point) continue;
146
- await page.mouse.move(point.x, point.y);
147
- if (i < path.length - 1 && stepDelayMs > 0) {
148
- await sleep(stepDelayMs);
149
- }
150
- }
151
- }
152
- var RESERVED_TARGETS = /* @__PURE__ */ new Set(["natural", "end", "top"]);
153
- async function executeScroll(target, ctx, options = {}) {
154
- const { page, personality, rng, speed } = ctx;
155
- const speedFactor = speedModeFactor(speed);
156
- const axis = options.axis ?? "y";
157
- const container = resolveWithin(options.within, ctx);
158
- const geom = container ? await readContainerGeometry(container, axis) : await readWindowGeometry(page, axis);
159
- if (!geom) {
160
- return { from: 0, to: 0, distance: 0, durationMs: 0 };
161
- }
162
- const from = geom.current;
163
- const targetPos = await resolveTarget(target, ctx, geom, container, axis, options.block);
164
- const to = clamp(targetPos, 0, Math.max(0, geom.total - geom.viewport));
165
- const distance = to - from;
166
- if (distance === 0) {
167
- return { from, to, distance: 0, durationMs: 0 };
168
- }
169
- if (speed === "instant") {
170
- if (container) {
171
- await container.evaluate(
172
- (el, args) => {
173
- const a = args;
174
- if (a.axis === "x") el.scrollTo(a.pos, el.scrollTop);
175
- else el.scrollTo(el.scrollLeft, a.pos);
176
- },
177
- { axis, pos: to }
178
- );
179
- } else {
180
- await page.evaluate(
181
- (args) => {
182
- if (args.axis === "x") window.scrollTo(args.pos, window.scrollY);
183
- else window.scrollTo(window.scrollX, args.pos);
184
- },
185
- { axis, pos: to }
186
- );
187
- }
188
- return { from, to, distance, durationMs: 0 };
189
- }
190
- const segments = core.planScroll(from, to, personality.scroll, rng, {
191
- forceOvershoot: options.overshoot,
192
- withPauses: options.withPauses,
193
- personalitySpeed: personality.speed,
194
- speedFactor
195
- });
196
- if (container && geom.hover) {
197
- await page.mouse.move(geom.hover.x, geom.hover.y);
198
- }
199
- const startedAt = Date.now();
200
- await walkSegments(page, segments, axis, container);
201
- const durationMs = Date.now() - startedAt;
202
- return { from, to, distance, durationMs };
203
- }
204
- function resolveWithin(within, ctx) {
205
- if (!within) return null;
206
- return typeof within === "string" ? ctx.page.locator(within) : within;
207
- }
208
- async function readWindowGeometry(page, axis) {
209
- const g = await page.evaluate((a) => {
210
- if (a === "x") {
211
- return {
212
- current: window.scrollX,
213
- viewport: window.innerWidth,
214
- total: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0)
215
- };
216
- }
217
- return {
218
- current: window.scrollY,
219
- viewport: window.innerHeight,
220
- total: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0)
221
- };
222
- }, axis);
223
- return { current: g.current, viewport: g.viewport, total: g.total };
224
- }
225
- async function readContainerGeometry(container, axis) {
226
- return container.evaluate((el, a) => {
227
- const rect = el.getBoundingClientRect();
228
- const isX = a === "x";
229
- return {
230
- current: isX ? el.scrollLeft : el.scrollTop,
231
- viewport: isX ? el.clientWidth : el.clientHeight,
232
- total: isX ? el.scrollWidth : el.scrollHeight,
233
- hover: {
234
- x: rect.left + rect.width / 2,
235
- y: rect.top + rect.height / 2
236
- }
237
- };
238
- }, axis).catch(() => null);
239
- }
240
- async function resolveTarget(target, ctx, geom, container, axis, block = "start") {
241
- if (target === void 0 || target === "natural") return geom.current + geom.viewport;
242
- if (target === "end") return geom.total;
243
- if (target === "top") return 0;
244
- if (typeof target === "object" && "by" in target) return geom.current + target.by;
245
- if (typeof target === "object" && "to" in target) return target.to;
246
- const elementLocator = typeof target === "string" && !RESERVED_TARGETS.has(target) ? ctx.page.locator(target) : typeof target === "string" ? null : target;
247
- if (!elementLocator) return geom.current + geom.viewport;
248
- return container ? resolveElementWithinContainer(elementLocator, container, geom, axis, block) : resolveElementInWindow(elementLocator, geom, axis, block);
249
- }
250
- async function resolveElementInWindow(elementLocator, geom, axis, block) {
251
- const rect = await elementLocator.boundingBox().catch(() => null);
252
- if (!rect) return geom.current;
253
- const relStart = axis === "x" ? rect.x : rect.y;
254
- const length = axis === "x" ? rect.width : rect.height;
255
- const absoluteStart = geom.current + relStart;
256
- const absoluteEnd = absoluteStart + length;
257
- if (block === "start") return absoluteStart;
258
- if (block === "end") return absoluteEnd - geom.viewport;
259
- if (block === "nearest") {
260
- if (relStart >= 0 && relStart + length <= geom.viewport) return geom.current;
261
- if (relStart < 0) return absoluteStart;
262
- return absoluteEnd - geom.viewport;
263
- }
264
- return absoluteStart - (geom.viewport - length) / 2;
265
- }
266
- async function resolveElementWithinContainer(elementLocator, container, geom, axis, block) {
267
- const rects = await container.evaluate(
268
- (containerEl, args) => {
269
- const elementEl = args.sel ? document.querySelector(args.sel) : null;
270
- const targetEl = elementEl ?? containerEl.querySelector(":scope > *");
271
- if (!targetEl) return null;
272
- const cRect = containerEl.getBoundingClientRect();
273
- const eRect = targetEl.getBoundingClientRect();
274
- return args.axis === "x" ? { relStart: eRect.left - cRect.left, length: eRect.width } : { relStart: eRect.top - cRect.top, length: eRect.height };
275
- },
276
- { sel: await locatorSelector(elementLocator), axis }
277
- ).catch(() => null);
278
- if (!rects) return geom.current;
279
- const offsetStart = rects.relStart + geom.current;
280
- const offsetEnd = offsetStart + rects.length;
281
- if (block === "start") return offsetStart;
282
- if (block === "end") return offsetEnd - geom.viewport;
283
- if (block === "nearest") {
284
- if (rects.relStart >= 0 && rects.relStart + rects.length <= geom.viewport) {
285
- return geom.current;
286
- }
287
- if (rects.relStart < 0) return offsetStart;
288
- return offsetEnd - geom.viewport;
289
- }
290
- return offsetStart - (geom.viewport - rects.length) / 2;
291
- }
292
- async function locatorSelector(locator) {
293
- const s = locator.toString?.();
294
- if (typeof s !== "string") return null;
295
- const match = /locator\(['"](.+?)['"]/.exec(s);
296
- if (!match) return null;
297
- const raw = match[1] ?? "";
298
- const eq = raw.indexOf("=");
299
- return eq > 0 && /^[a-z]+$/.test(raw.slice(0, eq)) ? raw.slice(eq + 1) : raw;
300
- }
301
- async function walkSegments(page, segments, axis, container) {
302
- for (const segment of segments) {
303
- if (segment.delayBeforeMs > 0) await sleep(segment.delayBeforeMs);
304
- if (segment.delta === 0) continue;
305
- if (container) {
306
- await container.evaluate(
307
- (el, args) => {
308
- const a = args;
309
- if (a.axis === "x") el.scrollLeft += a.delta;
310
- else el.scrollTop += a.delta;
311
- },
312
- { axis, delta: segment.delta }
313
- );
314
- } else if (axis === "x") {
315
- await page.mouse.wheel(segment.delta, 0);
316
- } else {
317
- await page.mouse.wheel(0, segment.delta);
318
- }
319
- }
320
- }
321
- function clamp(value, min, max) {
322
- return value < min ? min : value > max ? max : value;
323
- }
324
-
325
- // src/mouse/index.ts
326
- async function executeClick(target, ctx, options = {}) {
327
- const button = options.button ?? "left";
328
- if (ctx.speed === "instant") {
329
- if (isPoint(target)) {
330
- await ctx.page.mouse.click(target.x, target.y, { button });
331
- ctx.setMousePosition(target);
332
- return { target };
333
- }
334
- const locator = typeof target === "string" ? ctx.page.locator(target) : target;
335
- const box2 = await locator.boundingBox();
336
- await locator.click({ button });
337
- const center = box2 ? { x: box2.x + box2.width / 2, y: box2.y + box2.height / 2 } : ctx.getMousePosition();
338
- ctx.setMousePosition(center);
339
- return { target: center };
340
- }
341
- const { point: targetPoint, box } = await resolveTargetPointAndBox(target, ctx, "click");
342
- await maybeMisclickBeat(ctx, box, targetPoint);
343
- await walkBezierTo(targetPoint, ctx);
344
- const preClickMs = computeDwellTime(
345
- ctx.personality.dwell.preClickMs,
346
- ctx.personality.dwell.preClickJitter,
347
- ctx.personality,
348
- ctx.speed,
349
- ctx.rng
350
- );
351
- if (preClickMs > 0) await sleep(preClickMs);
352
- ctx.setMousePosition(targetPoint);
353
- await ctx.page.mouse.click(targetPoint.x, targetPoint.y, { button });
354
- const postActionMs = computeDwellTime(
355
- ctx.personality.dwell.postActionMs,
356
- ctx.personality.dwell.postActionJitter,
357
- ctx.personality,
358
- ctx.speed,
359
- ctx.rng
360
- );
361
- if (postActionMs > 0) await sleep(postActionMs);
362
- return { target: targetPoint };
363
- }
364
- async function executeHover(target, ctx) {
365
- if (ctx.speed === "instant") {
366
- const box = await readBoxWithAutoScroll(target, ctx, "hover");
367
- const center = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
368
- await ctx.page.mouse.move(center.x, center.y);
369
- ctx.setMousePosition(center);
370
- return { target: center };
371
- }
372
- const targetPoint = await moveToTarget(target, ctx);
373
- const dwellMs = computeDwellTime(
374
- ctx.personality.dwell.preClickMs,
375
- ctx.personality.dwell.preClickJitter,
376
- ctx.personality,
377
- ctx.speed,
378
- ctx.rng
379
- );
380
- if (dwellMs > 0) await sleep(dwellMs);
381
- ctx.setMousePosition(targetPoint);
382
- return { target: targetPoint };
383
- }
384
- async function executeDrag(from, to, ctx) {
385
- const scrollYBeforeResolve = await readScrollY(ctx.page);
386
- let { point: fromPoint, box: fromBox } = await resolveTargetPointAndBox(from, ctx, "drag");
387
- let { point: toPoint, box: toBox } = await resolveTargetPointAndBox(to, ctx, "drag");
388
- const resolveScrollDelta = await readScrollY(ctx.page) - scrollYBeforeResolve;
389
- if (resolveScrollDelta !== 0) {
390
- if (isPoint(from)) fromPoint = { x: fromPoint.x, y: fromPoint.y - resolveScrollDelta };
391
- if (isPoint(to)) toPoint = { x: toPoint.x, y: toPoint.y - resolveScrollDelta };
392
- }
393
- if (ctx.speed === "instant") {
394
- await ctx.page.mouse.move(fromPoint.x, fromPoint.y);
395
- await ctx.page.mouse.down();
396
- await ctx.page.mouse.move(toPoint.x, toPoint.y);
397
- await ctx.page.mouse.up();
398
- ctx.setMousePosition(toPoint);
399
- return { from: fromPoint, to: toPoint };
400
- }
401
- if (fromBox && toBox) {
402
- const scrollDelta = computeCurveScrollDelta(
403
- fromPoint,
404
- toPoint,
405
- ctx.page.viewportSize(),
406
- ctx.personality.mouse.curvature
407
- );
408
- if (scrollDelta !== 0) {
409
- await executeScroll({ by: scrollDelta }, ctx, {});
410
- const refreshedFrom = await resolveTargetPointAndBox(from, ctx, "drag");
411
- fromPoint = refreshedFrom.point;
412
- fromBox = refreshedFrom.box;
413
- const refreshedTo = await resolveTargetPointAndBox(to, ctx, "drag");
414
- toPoint = refreshedTo.point;
415
- toBox = refreshedTo.box;
416
- }
417
- }
418
- await maybeMisclickBeat(ctx, fromBox, fromPoint);
419
- await walkBezierTo(fromPoint, ctx);
420
- ctx.setMousePosition(fromPoint);
421
- const preDragMs = computeDwellTime(
422
- ctx.personality.dwell.preClickMs,
423
- ctx.personality.dwell.preClickJitter,
424
- ctx.personality,
425
- ctx.speed,
426
- ctx.rng
427
- );
428
- if (preDragMs > 0) await sleep(preDragMs);
429
- await ctx.page.mouse.down();
430
- await maybeMisclickBeat(ctx, toBox, toPoint);
431
- await walkBezierTo(toPoint, ctx);
432
- await ctx.page.mouse.up();
433
- ctx.setMousePosition(toPoint);
434
- const postActionMs = computeDwellTime(
435
- ctx.personality.dwell.postActionMs,
436
- ctx.personality.dwell.postActionJitter,
437
- ctx.personality,
438
- ctx.speed,
439
- ctx.rng
440
- );
441
- if (postActionMs > 0) await sleep(postActionMs);
442
- return { from: fromPoint, to: toPoint };
443
- }
444
- async function executeMove(target, ctx) {
445
- const point = await resolveTargetPoint(target, ctx, "move");
446
- if (ctx.speed === "instant") {
447
- await ctx.page.mouse.move(point.x, point.y);
448
- ctx.setMousePosition(point);
449
- return { target: point };
450
- }
451
- await walkBezierTo(point, ctx);
452
- ctx.setMousePosition(point);
453
- return { target: point };
454
- }
455
- async function moveToTarget(target, ctx) {
456
- const box = await readBoxWithAutoScroll(target, ctx, "hover");
457
- const targetPoint = pickClickPoint(box, ctx.rng, ctx.personality.mouse.clickSpread);
458
- await walkBezierTo(targetPoint, ctx);
459
- return targetPoint;
460
- }
461
- async function walkBezierTo(to, ctx) {
462
- const startPoint = ctx.getMousePosition();
463
- const rawPath = core.bezierPath(startPoint, to, ctx.rng, {
464
- curvature: ctx.personality.mouse.curvature
465
- });
466
- const path = core.humanizePath(rawPath, ctx.rng);
467
- const travelMs = computeTravelTime(path, ctx.personality, ctx.speed, ctx.rng);
468
- await walkMouseAlongPath(ctx.page, path, travelMs);
469
- }
470
- async function resolveTargetPoint(target, ctx, action) {
471
- if (isPoint(target)) return target;
472
- return resolveLocatorPoint(target, ctx, action);
473
- }
474
- async function resolveTargetPointAndBox(target, ctx, action) {
475
- if (isPoint(target)) return { point: target, box: null };
476
- const box = await readBoxWithAutoScroll(target, ctx, action);
477
- const point = pickClickPoint(box, ctx.rng, ctx.personality.mouse.clickSpread);
478
- return { point, box };
479
- }
480
- async function resolveLocatorPoint(target, ctx, action) {
481
- const box = await readBoxWithAutoScroll(target, ctx, action);
482
- return pickClickPoint(box, ctx.rng, ctx.personality.mouse.clickSpread);
483
- }
484
- async function readBoxWithAutoScroll(target, ctx, action) {
485
- const locator = typeof target === "string" ? ctx.page.locator(target) : target;
486
- let box = await locator.boundingBox();
487
- if (!box) {
488
- throw new Error(
489
- `Cannot ${action}: element not found or has no bounding box (target: ${describeTarget(target)})`
490
- );
491
- }
492
- const viewport = ctx.page.viewportSize();
493
- if (viewport && !isBoxCenterInViewport(box, viewport)) {
494
- if (ctx.speed === "instant") {
495
- await locator.scrollIntoViewIfNeeded();
496
- } else {
497
- await executeScroll(locator, ctx, { block: "center" });
498
- }
499
- box = await locator.boundingBox();
500
- if (!box) {
501
- throw new Error(
502
- `Cannot ${action}: element disappeared after scrolling into view (target: ${describeTarget(target)})`
503
- );
504
- }
505
- }
506
- return box;
507
- }
508
- function isBoxCenterInViewport(box, viewport) {
509
- const cx = box.x + box.width / 2;
510
- const cy = box.y + box.height / 2;
511
- return cx >= 0 && cx <= viewport.width && cy >= 0 && cy <= viewport.height;
512
- }
513
- async function readScrollY(page) {
514
- if (typeof page.evaluate !== "function") return 0;
515
- return page.evaluate(() => window.scrollY);
516
- }
517
- var CURVE_VIEWPORT_MARGIN = 20;
518
- function computeCurveScrollDelta(from, to, viewport, curvature) {
519
- if (!viewport) return 0;
520
- const distance = Math.hypot(to.x - from.x, to.y - from.y);
521
- const perpendicularExtent = distance * curvature;
522
- const minY = Math.min(from.y, to.y) - perpendicularExtent;
523
- const maxY = Math.max(from.y, to.y) + perpendicularExtent;
524
- const topOverflow = -minY + CURVE_VIEWPORT_MARGIN;
525
- const bottomOverflow = maxY + CURVE_VIEWPORT_MARGIN - viewport.height;
526
- if (bottomOverflow > 0 && bottomOverflow >= topOverflow) return bottomOverflow;
527
- if (topOverflow > 0) return -topOverflow;
528
- return 0;
529
- }
530
- function isPoint(target) {
531
- return typeof target === "object" && target !== null && !("boundingBox" in target) && typeof target.x === "number" && typeof target.y === "number";
532
- }
533
- var MISCLICK_OFFSET_MIN = 5;
534
- var MISCLICK_OFFSET_MAX = 15;
535
- async function maybeMisclickBeat(ctx, box, targetPoint) {
536
- if (!ctx.rng.chance(ctx.personality.mouse.misclickProbability)) return;
537
- if (cursorAlreadyOnTarget(ctx.getMousePosition(), box, targetPoint)) return;
538
- const viewport = ctx.page.viewportSize();
539
- const misclickPoint = box ? pickMisclickOutsideBox(box, ctx.rng, viewport) : pickMisclickAroundPoint(targetPoint, ctx.rng, viewport);
540
- if (misclickPoint === null) return;
541
- await walkBezierTo(misclickPoint, ctx);
542
- ctx.setMousePosition(misclickPoint);
543
- const realizeMs = computeDwellTime(
544
- ctx.personality.dwell.preClickMs,
545
- ctx.personality.dwell.preClickJitter,
546
- ctx.personality,
547
- ctx.speed,
548
- ctx.rng
549
- );
550
- if (realizeMs > 0) await sleep(realizeMs);
551
- }
552
- function cursorAlreadyOnTarget(current, box, targetPoint) {
553
- if (box) {
554
- return current.x >= box.x && current.x <= box.x + box.width && current.y >= box.y && current.y <= box.y + box.height;
555
- }
556
- const dx = current.x - targetPoint.x;
557
- const dy = current.y - targetPoint.y;
558
- return Math.hypot(dx, dy) < MISCLICK_OFFSET_MIN;
559
- }
560
- function pickMisclickOutsideBox(box, rng, viewport) {
561
- const edge = rng.nextInt(0, 4);
562
- const offset = rng.nextFloat(MISCLICK_OFFSET_MIN, MISCLICK_OFFSET_MAX);
563
- const along = rng.nextFloat(0.2, 0.8);
564
- let x;
565
- let y;
566
- if (edge === 0) {
567
- x = box.x + box.width * along;
568
- y = box.y - offset;
569
- } else if (edge === 1) {
570
- x = box.x + box.width + offset;
571
- y = box.y + box.height * along;
572
- } else if (edge === 2) {
573
- x = box.x + box.width * along;
574
- y = box.y + box.height + offset;
575
- } else {
576
- x = box.x - offset;
577
- y = box.y + box.height * along;
578
- }
579
- if (viewport) {
580
- x = clamp2(x, 0, viewport.width - 1);
581
- y = clamp2(y, 0, viewport.height - 1);
582
- }
583
- const insideBox = x >= box.x && x <= box.x + box.width && y >= box.y && y <= box.y + box.height;
584
- if (insideBox) return null;
585
- return { x, y };
586
- }
587
- function pickMisclickAroundPoint(target, rng, viewport) {
588
- const angle = rng.nextFloat(0, Math.PI * 2);
589
- const distance = rng.nextFloat(MISCLICK_OFFSET_MIN, MISCLICK_OFFSET_MAX);
590
- let x = target.x + Math.cos(angle) * distance;
591
- let y = target.y + Math.sin(angle) * distance;
592
- if (viewport) {
593
- x = clamp2(x, 0, viewport.width - 1);
594
- y = clamp2(y, 0, viewport.height - 1);
595
- }
596
- if (x === target.x && y === target.y) return null;
597
- return { x, y };
598
- }
599
- function pickClickPoint(box, rng, clickSpread) {
600
- const cx = box.x + box.width / 2;
601
- const cy = box.y + box.height / 2;
602
- const sigmaX = box.width * clickSpread;
603
- const sigmaY = box.height * clickSpread;
604
- const x = clamp2(cx + rng.nextGaussian(0, sigmaX), box.x, box.x + box.width);
605
- const y = clamp2(cy + rng.nextGaussian(0, sigmaY), box.y, box.y + box.height);
606
- return { x, y };
607
- }
608
- function computeTravelTime(path, personality, speed, rng) {
609
- let distance = 0;
610
- for (let i = 1; i < path.length; i++) {
611
- const prev = path[i - 1];
612
- const curr = path[i];
613
- if (!prev || !curr) continue;
614
- distance += Math.hypot(curr.x - prev.x, curr.y - prev.y);
615
- }
616
- const baseTime = distance / 1e3 * personality.mouse.travelTimeMs;
617
- const jitterMag = baseTime * personality.mouse.travelTimeJitter;
618
- const jitter = rng.nextFloat(-jitterMag, jitterMag);
619
- const total = (baseTime + jitter) * personality.speed * speedModeFactor(speed);
620
- return Math.max(0, total);
621
- }
622
- function describeTarget(target) {
623
- if (isPoint(target)) return `point(${target.x}, ${target.y})`;
624
- return typeof target === "string" ? target : target.toString?.() ?? "locator";
625
- }
626
- function clamp2(value, min, max) {
627
- return value < min ? min : value > max ? max : value;
628
- }
629
- async function executeRead(target, ctx, options = {}) {
630
- let words = 0;
631
- let locator;
632
- if (typeof target === "string") {
633
- locator = ctx.page.locator(target);
634
- } else if ("words" in target) {
635
- words = target.words;
636
- } else if ("text" in target) {
637
- words = core.countWords(target.text);
638
- } else {
639
- locator = target;
640
- }
641
- let autoDetectedKind;
642
- if (locator) {
643
- if (options.scrollIntoView) {
644
- await locator.scrollIntoViewIfNeeded();
645
- }
646
- const text = await locator.innerText().catch(() => "");
647
- words = core.countWords(text);
648
- if (options.kind === void 0) {
649
- autoDetectedKind = await detectKindFromTag(locator);
650
- }
651
- }
652
- const kind = options.kind ?? autoDetectedKind ?? "prose";
653
- const durationMs = core.computeReadingDwellMs(words, ctx.personality.reading, ctx.rng, {
654
- kind,
655
- wpmMultiplier: options.wpmMultiplier,
656
- personalitySpeed: ctx.personality.speed,
657
- speedFactor: speedModeFactor(ctx.speed)
658
- });
659
- const withMotion = options.withMotion ?? true;
660
- if (withMotion && locator && durationMs > 0) {
661
- const box = await locator.boundingBox().catch(() => null);
662
- if (box) {
663
- const lineRects = await getLineRects(locator).catch(() => []);
664
- const path = core.planReadingScan(box, ctx.rng, {
665
- start: ctx.getMousePosition(),
666
- lineRects: lineRects.length > 0 ? lineRects : void 0
667
- });
668
- await walkMouseAlongPath(ctx.page, path, durationMs);
669
- const final = path[path.length - 1];
670
- if (final) ctx.setMousePosition(final);
671
- return { words, durationMs, kind };
672
- }
673
- }
674
- if (durationMs > 0) await sleep(durationMs);
675
- return { words, durationMs, kind };
676
- }
677
- async function getLineRects(locator) {
678
- const result = await locator.evaluate((el) => {
679
- const walker = el.ownerDocument.createTreeWalker(el, 4);
680
- const rects = [];
681
- let node = walker.nextNode();
682
- while (node) {
683
- const text = node.textContent ?? "";
684
- if (text.trim().length > 0) {
685
- const range = el.ownerDocument.createRange();
686
- range.selectNodeContents(node);
687
- for (const r of Array.from(range.getClientRects())) {
688
- if (r.width > 0 && r.height > 0) {
689
- rects.push({ x: r.x, y: r.y, width: r.width, height: r.height });
690
- }
691
- }
692
- }
693
- node = walker.nextNode();
694
- }
695
- rects.sort((a, b) => a.y - b.y || a.x - b.x);
696
- const merged = [];
697
- for (const r of rects) {
698
- const last = merged[merged.length - 1];
699
- if (last && Math.abs(last.y - r.y) < 1 && r.x - (last.x + last.width) < 6) {
700
- const right = Math.max(last.x + last.width, r.x + r.width);
701
- const bottom = Math.max(last.y + last.height, r.y + r.height);
702
- last.width = right - last.x;
703
- last.height = bottom - last.y;
704
- } else {
705
- merged.push({ ...r });
706
- }
707
- }
708
- return merged;
709
- });
710
- if (!Array.isArray(result)) return [];
711
- return result.filter(
712
- (r) => r != null && typeof r === "object" && typeof r.x === "number" && typeof r.y === "number" && typeof r.width === "number" && typeof r.height === "number"
713
- );
714
- }
715
- async function detectKindFromTag(locator) {
716
- const tag = await locator.evaluate((el) => el.tagName?.toLowerCase() ?? "").catch(() => "");
717
- if (tag === "pre" || tag === "code") return "code";
718
- return void 0;
719
- }
720
-
721
- // src/recording/codegen.ts
722
- var POINT_RE = /^point\((-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)\)$/;
723
- var POINT_COMMENT = " // raw coordinate \u2014 replace with a locator for a stable selector";
724
- var UNCAPTURED_COMMENT = " // input not captured (masked or captureInputs disabled) \u2014 fill in (e.g. process.env.X)";
725
- function q(value) {
726
- return `'${String(value ?? "").replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r")}'`;
727
- }
728
- function targetArg(desc) {
729
- const s = String(desc ?? "");
730
- const m = s.match(POINT_RE);
731
- if (m) return { code: `{ x: ${m[1]}, y: ${m[2]} }`, isPoint: true };
732
- return { code: q(s), isPoint: false };
733
- }
734
- function createHumanOptions(timeline, ciSpeed = false) {
735
- const parts = [` personality: ${q(timeline.personality)},`];
736
- if (timeline.seed !== null) parts.push(` seed: ${q(timeline.seed)},`);
737
- parts.push(
738
- ciSpeed ? ` speed: process.env.CI ? 'instant' : ${q(timeline.speed)},` : ` speed: ${q(timeline.speed)},`
739
- );
740
- return `{
741
- ${parts.join("\n")}
742
- }`;
743
- }
744
- function emitScroll(target) {
745
- const s = String(target ?? "natural");
746
- if (s === "natural") return " await human.scroll('natural');";
747
- const by = s.match(/^by:(-?\d+(?:\.\d+)?)$/);
748
- if (by) return ` await human.scroll({ by: ${by[1]} });`;
749
- const to = s.match(/^to:(-?\d+(?:\.\d+)?)$/);
750
- if (to) return ` await human.scroll({ to: ${to[1]} });`;
751
- return ` await human.scroll(${q(s)});`;
752
- }
753
- function emitAction(e, opts = {}) {
754
- const p = e.params;
755
- switch (e.type) {
756
- case "goto": {
757
- const url = String(p.url ?? "");
758
- if (opts.baseOrigin && url.startsWith(opts.baseOrigin)) {
759
- return ` await human.goto(${q(url.slice(opts.baseOrigin.length) || "/")});`;
760
- }
761
- return ` await human.goto(${q(url)});`;
762
- }
763
- case "click":
764
- case "rightClick":
765
- case "hover":
766
- case "move": {
767
- const { code, isPoint: isPoint2 } = targetArg(p.target);
768
- return ` await human.${e.type}(${code});${isPoint2 ? POINT_COMMENT : ""}`;
769
- }
770
- case "drag": {
771
- const from = targetArg(p.from);
772
- const to = targetArg(p.to);
773
- const comment = from.isPoint || to.isPoint ? POINT_COMMENT : "";
774
- return ` await human.drag(${from.code}, ${to.code});${comment}`;
775
- }
776
- case "type":
777
- case "paste": {
778
- const { code, isPoint: isPoint2 } = targetArg(p.target);
779
- if (e.inputValue === void 0) {
780
- return ` await human.${e.type}(${code}, '');${UNCAPTURED_COMMENT}`;
781
- }
782
- const call = ` await human.${e.type}(${code}, ${q(e.inputValue)});`;
783
- if (opts.asserts && !isPoint2) {
784
- return `${call}
785
- await expect(page.locator(${code})).toHaveValue(${q(e.inputValue)});`;
786
- }
787
- return call;
788
- }
789
- case "press":
790
- return ` await human.press(${q(p.key)});`;
791
- case "scroll":
792
- return emitScroll(p.target);
793
- case "read": {
794
- const desc = String(p.target ?? "");
795
- if (/^\d+ words$/.test(desc) || /^text:\d+ chars$/.test(desc)) {
796
- return ` // human.read(...) \u2014 ${desc}; original target not captured`;
797
- }
798
- const call = ` await human.read(${q(desc)});`;
799
- if (opts.asserts) return `${call}
800
- await expect(page.locator(${q(desc)})).toBeVisible();`;
801
- return call;
802
- }
803
- case "sleep":
804
- return ` await sleep(${Number(p.ms) || 0});`;
805
- case "reload":
806
- return " await human.reload();";
807
- case "goBack":
808
- return " await human.goBack();";
809
- case "goForward":
810
- return " await human.goForward();";
811
- default:
812
- return ` // unsupported action: ${e.type}`;
813
- }
814
- }
815
- function needsSleepImport(timeline) {
816
- return timeline.events.some((e) => e.type === "sleep");
817
- }
818
- function generateHumanJS(timeline) {
819
- const imports = needsSleepImport(timeline) ? "import { chromium, createHuman, sleep } from '@humanjs/playwright';" : "import { chromium, createHuman } from '@humanjs/playwright';";
820
- const body = timeline.events.map((e) => emitAction(e)).join("\n");
821
- return `${imports}
822
-
823
- async function main() {
824
- const browser = await chromium.launch({ headless: false });
825
- const page = await browser.newPage();
826
- const human = await createHuman(page, ${createHumanOptions(timeline)});
827
-
828
- ${body}
829
-
830
- await browser.close();
831
- }
832
-
833
- main();
834
- `;
835
- }
836
- var NAV_TYPES = /* @__PURE__ */ new Set(["goto", "reload", "goBack", "goForward"]);
837
- function sharedGotoOrigin(events) {
838
- let origin;
839
- for (const e of events) {
840
- if (e.type !== "goto") continue;
841
- try {
842
- const o = new URL(String(e.params.url ?? "")).origin;
843
- if (origin === void 0) origin = o;
844
- else if (origin !== o) return void 0;
845
- } catch {
846
- return void 0;
847
- }
848
- }
849
- return origin;
850
- }
851
- function indentLines(block, pad) {
852
- return block.split("\n").map((line) => line.length > 0 ? pad + line : line).join("\n");
853
- }
854
- function stepLabel(event, index, baseOrigin) {
855
- switch (event.type) {
856
- case "goto": {
857
- const url = String(event.params.url ?? "");
858
- const path = baseOrigin && url.startsWith(baseOrigin) ? url.slice(baseOrigin.length) || "/" : url;
859
- return `go to ${path}`;
860
- }
861
- case "reload":
862
- return "reload";
863
- case "goBack":
864
- return "go back";
865
- case "goForward":
866
- return "go forward";
867
- default:
868
- return `step ${index + 1}`;
869
- }
870
- }
871
- function emitSteps(events, opts) {
872
- const groups = [];
873
- for (const e of events) {
874
- const last = groups[groups.length - 1];
875
- if (last === void 0 || NAV_TYPES.has(e.type)) groups.push([e]);
876
- else last.push(e);
877
- }
878
- return groups.map((group, i) => {
879
- const [first] = group;
880
- const label = first ? stepLabel(first, i, opts.baseOrigin) : `step ${i + 1}`;
881
- const inner = group.map((e) => indentLines(emitAction(e, opts), " ")).join("\n");
882
- return ` await test.step(${q(label)}, async () => {
883
- ${inner}
884
- });`;
885
- }).join("\n\n");
886
- }
887
- function generatePlaywrightTest(timeline, options = {}) {
888
- const events = options.keepSleeps ? timeline.events : timeline.events.filter((e) => e.type !== "sleep");
889
- const baseOrigin = options.baseUrl ? sharedGotoOrigin(events) : void 0;
890
- const emitOpts = { asserts: true, baseOrigin };
891
- const body = options.steps ? emitSteps(events, emitOpts) : events.map((e) => emitAction(e, emitOpts)).join("\n");
892
- const needsSleep = events.some((e) => e.type === "sleep");
893
- const hasAsserts = body.includes("await expect(");
894
- const testImport = hasAsserts ? "import { expect, test } from '@playwright/test';" : "import { test } from '@playwright/test';";
895
- const humanImport = needsSleep ? "import { createHuman, sleep } from '@humanjs/playwright';" : "import { createHuman } from '@humanjs/playwright';";
896
- const title = options.title ?? timeline.name ?? "recorded session";
897
- const baseUrlNote = baseOrigin ? ` // Set use.baseURL = ${q(baseOrigin)} in playwright.config.ts for these relative paths.
898
-
899
- ` : "";
900
- const todo = [
901
- hasAsserts ? " // TODO: add assertions for the outcome of this flow, e.g.:" : " // TODO: assert the outcome \u2014 import { expect } from '@playwright/test', e.g.:",
902
- " // await expect(page).toHaveURL(/dashboard/);",
903
- " // await expect(page.getByText('Welcome back')).toBeVisible();"
904
- ].join("\n");
905
- return `${testImport}
906
- ${humanImport}
907
-
908
- test(${q(title)}, async ({ page }) => {
909
- const human = await createHuman(page, ${createHumanOptions(timeline, true)});
910
-
911
- ${baseUrlNote}${body}
912
-
913
- ${todo}
7
+ Object.defineProperty(exports, "Recording", {
8
+ enumerable: true,
9
+ get: function () { return chunk3X36PFTS_cjs.Recording; }
914
10
  });
915
- `;
916
- }
917
-
918
- // src/recording/index.ts
919
- var pendingFrameCleanups = /* @__PURE__ */ new Set();
920
- var exitHandlerInstalled = false;
921
- function ensureExitHandler() {
922
- if (exitHandlerInstalled) return;
923
- exitHandlerInstalled = true;
924
- process.on("exit", () => {
925
- for (const dir of pendingFrameCleanups) {
926
- try {
927
- fs.rmSync(dir, { recursive: true, force: true });
928
- } catch {
929
- }
930
- }
931
- pendingFrameCleanups.clear();
932
- });
933
- }
934
- var FFMPEG_PATH = ffmpegStatic__default.default;
935
- var QUALITY_PRESETS = {
936
- fast: {
937
- captureFormat: "jpeg",
938
- captureJpegQuality: 85,
939
- captureFps: 24,
940
- crf: 23,
941
- preset: "fast"
942
- },
943
- standard: {
944
- captureFormat: "jpeg",
945
- captureJpegQuality: 90,
946
- captureFps: 30,
947
- crf: 20,
948
- preset: "fast"
949
- },
950
- high: {
951
- captureFormat: "jpeg",
952
- captureJpegQuality: 95,
953
- captureFps: 30,
954
- crf: 18,
955
- preset: "slow",
956
- // 'animation' suits screen content (large solid regions, sharp edges)
957
- // better than 'film' which is tuned for live-action grain.
958
- tune: "animation"
959
- },
960
- lossless: {
961
- // PNG capture for perceptually lossless source frames. Temp files are
962
- // 10-20× larger than JPEG; output mp4 still benefits from the extra
963
- // headroom (no JPEG artifacts to preserve).
964
- captureFormat: "png",
965
- captureJpegQuality: 100,
966
- captureFps: 30,
967
- crf: 12,
968
- preset: "veryslow",
969
- tune: "animation"
970
- }
971
- };
972
- function getCaptureSettingsForQuality(quality) {
973
- const preset = QUALITY_PRESETS[quality];
974
- return {
975
- format: preset.captureFormat,
976
- quality: preset.captureJpegQuality,
977
- fps: preset.captureFps
978
- };
979
- }
980
- var Recording = class {
981
- #capture;
982
- #windowStartMs;
983
- #windowEndMs;
984
- #timelineSource;
985
- // Frames live on disk until `dispose()` is called. Exporters
986
- // (`toVideo`, `toGif`) are repeatable and interleavable — they read the
987
- // same frame source, they don't consume it.
988
- #disposed = false;
989
- constructor(capture, windowStartMs, windowEndMs, timelineSource) {
990
- this.#capture = capture;
991
- this.#windowStartMs = windowStartMs;
992
- this.#windowEndMs = windowEndMs;
993
- this.#timelineSource = timelineSource;
994
- if (capture !== null) {
995
- pendingFrameCleanups.add(capture.dir);
996
- ensureExitHandler();
997
- }
998
- }
999
- /** Wall-clock duration of the recorded window. */
1000
- get durationMs() {
1001
- return this.#windowEndMs - this.#windowStartMs;
1002
- }
1003
- /** True if frames were captured during this recording. */
1004
- get hasVideo() {
1005
- return this.#capture !== null;
1006
- }
1007
- /**
1008
- * The structured action timeline of this recording — same data that
1009
- * `toTimeline()` writes to disk.
1010
- */
1011
- get timeline() {
1012
- return {
1013
- version: 1,
1014
- ...this.#timelineSource.name !== void 0 ? { name: this.#timelineSource.name } : {},
1015
- personality: this.#timelineSource.personality,
1016
- seed: this.#timelineSource.seed,
1017
- speed: this.#timelineSource.speed,
1018
- durationMs: this.durationMs,
1019
- events: this.#timelineSource.events
1020
- };
1021
- }
1022
- /**
1023
- * Assembles the captured frames into a video at `outputPath`. The output
1024
- * format is inferred from the extension — `.mp4` (H.264, re-encoded
1025
- * with the configured quality) or `.webm` (VP9).
1026
- *
1027
- * Repeatable and interleavable with `toGif()` — the frame source is read,
1028
- * not consumed. Frames live until you call `dispose()` (or `await using`
1029
- * goes out of scope, or the process exits and the OS reaps `tmpdir`).
1030
- *
1031
- * @returns the resolved output path.
1032
- */
1033
- async toVideo(outputPath, options = {}) {
1034
- if (this.#disposed) {
1035
- throw new Error("Recording.toVideo() called after dispose() \u2014 the source frames are gone.");
1036
- }
1037
- if (this.#capture === null) {
1038
- throw new Error(
1039
- "Recording.toVideo() requires video capture, which was disabled for this recording. Call `human.record(cb)` (default captures video) or pass `output` to @humanjs/recorder's `record()`. `toTimeline()` and `.timeline` work without capture."
1040
- );
1041
- }
1042
- const preset = QUALITY_PRESETS[options.quality ?? "high"];
1043
- const crf = options.crf ?? preset.crf;
1044
- const ffmpegPreset = options.preset ?? preset.preset;
1045
- const tune = options.tune ?? preset.tune;
1046
- const { dir, frames, startedAtMs, stoppedAtMs } = this.#capture;
1047
- if (frames.length === 0) {
1048
- throw new Error(
1049
- "No frames were captured. The recording window may have been too short, or the page may not have rendered any frames before the callback completed."
1050
- );
1051
- }
1052
- await promises.mkdir(path.dirname(outputPath), { recursive: true });
1053
- const ext = path.extname(outputPath).toLowerCase();
1054
- if (ext !== ".mp4" && ext !== ".webm") {
1055
- throw new Error(`Unsupported output extension: ${ext || "(none)"}. Use .mp4 or .webm.`);
1056
- }
1057
- const concatPath = `${dir}/concat.txt`;
1058
- const concatBody = buildConcatFile(frames, stoppedAtMs - startedAtMs);
1059
- await promises.writeFile(concatPath, concatBody, "utf8");
1060
- const args = ["-y", "-f", "concat", "-safe", "0", "-i", concatPath, "-vsync", "vfr"];
1061
- if (ext === ".mp4") {
1062
- args.push(
1063
- "-c:v",
1064
- "libx264",
1065
- "-pix_fmt",
1066
- "yuv420p",
1067
- "-crf",
1068
- String(crf),
1069
- "-preset",
1070
- ffmpegPreset
1071
- );
1072
- if (tune) args.push("-tune", tune);
1073
- args.push("-movflags", "+faststart");
1074
- } else {
1075
- args.push(
1076
- "-c:v",
1077
- "libvpx-vp9",
1078
- "-pix_fmt",
1079
- "yuv420p",
1080
- "-crf",
1081
- String(crf),
1082
- "-b:v",
1083
- "0",
1084
- "-deadline",
1085
- ffmpegPreset === "fast" || ffmpegPreset === "veryfast" ? "realtime" : "good"
1086
- );
1087
- }
1088
- args.push(outputPath);
1089
- await runFfmpeg(args);
1090
- return outputPath;
1091
- }
1092
- /**
1093
- * Assembles the captured frames into an animated GIF at `outputPath`.
1094
- * Optimized for embedding in READMEs, PRs, Slack, and docs — uses a
1095
- * per-recording palette (`palettegen` + `paletteuse`) with Bayer dithering
1096
- * so gradients stay smooth without exploding the file size.
1097
- *
1098
- * Repeatable and interleavable with `toVideo()` — call them in any order,
1099
- * any number of times. Frames live until you call `dispose()`.
1100
- *
1101
- * @returns the resolved output path.
1102
- */
1103
- async toGif(outputPath, options = {}) {
1104
- if (this.#disposed) {
1105
- throw new Error("Recording.toGif() called after dispose() \u2014 the source frames are gone.");
1106
- }
1107
- if (this.#capture === null) {
1108
- throw new Error(
1109
- "Recording.toGif() requires video capture, which was disabled for this recording. Call `human.record(cb)` (default captures video) or pass `output` to @humanjs/recorder's `record()`. `toTimeline()` and `.timeline` work without capture."
1110
- );
1111
- }
1112
- const fps = options.fps ?? 15;
1113
- const width = options.width;
1114
- const { dir, frames, startedAtMs, stoppedAtMs } = this.#capture;
1115
- if (frames.length === 0) {
1116
- throw new Error(
1117
- "No frames were captured. The recording window may have been too short, or the page may not have rendered any frames before the callback completed."
1118
- );
1119
- }
1120
- await promises.mkdir(path.dirname(outputPath), { recursive: true });
1121
- const ext = path.extname(outputPath).toLowerCase();
1122
- if (ext !== ".gif") {
1123
- throw new Error(`Unsupported output extension: ${ext || "(none)"}. Use .gif.`);
1124
- }
1125
- const concatPath = `${dir}/concat.txt`;
1126
- const concatBody = buildConcatFile(frames, stoppedAtMs - startedAtMs);
1127
- await promises.writeFile(concatPath, concatBody, "utf8");
1128
- const filterSteps = [`fps=${fps}`];
1129
- if (width !== void 0) {
1130
- filterSteps.push(`scale=${width}:-1:flags=lanczos`);
1131
- }
1132
- const preFilter = filterSteps.join(",");
1133
- const filterComplex = `${preFilter},split [a][b]; [a] palettegen=stats_mode=diff [p]; [b][p] paletteuse=dither=bayer:bayer_scale=5`;
1134
- const args = [
1135
- "-y",
1136
- "-f",
1137
- "concat",
1138
- "-safe",
1139
- "0",
1140
- "-i",
1141
- concatPath,
1142
- "-filter_complex",
1143
- filterComplex,
1144
- "-loop",
1145
- "0",
1146
- outputPath
1147
- ];
1148
- await runFfmpeg(args);
1149
- return outputPath;
1150
- }
1151
- /**
1152
- * Writes the structured action timeline to `outputPath` as JSON.
1153
- * Independent of `toVideo()` / `toGif()` — call before, after, in between,
1154
- * or instead. Safe to call multiple times. Unaffected by `dispose()`
1155
- * (the timeline lives in memory, not in the captured-frames temp dir).
1156
- *
1157
- * @returns the resolved output path.
1158
- */
1159
- async toTimeline(outputPath) {
1160
- await promises.mkdir(path.dirname(outputPath), { recursive: true });
1161
- await promises.writeFile(outputPath, `${JSON.stringify(this.timeline, null, 2)}
1162
- `, "utf8");
1163
- return outputPath;
1164
- }
1165
- /**
1166
- * Generates a standalone, runnable HumanJS script from the timeline and
1167
- * writes it to `outputPath`. String selectors round-trip verbatim; typed
1168
- * values are included when `captureInputs` was on (passwords masked).
1169
- *
1170
- * Independent of frame capture — works on timeline-only recordings and is
1171
- * unaffected by `dispose()`.
1172
- *
1173
- * @returns the resolved output path.
1174
- */
1175
- async toHumanJS(outputPath) {
1176
- await promises.mkdir(path.dirname(outputPath), { recursive: true });
1177
- await promises.writeFile(outputPath, generateHumanJS(this.timeline), "utf8");
1178
- return outputPath;
1179
- }
1180
- /**
1181
- * Generates a `@playwright/test` spec from the timeline — a humanized test
1182
- * (uses `createHuman` + `human.*`), not raw Playwright — and writes it to
1183
- * `outputPath`. Runs instant in CI / recorded speed locally, drops timing
1184
- * `sleep()`s (pass `{ keepSleeps: true }` to keep them), and derives the
1185
- * assertions it safely can.
1186
- *
1187
- * Independent of frame capture — works on timeline-only recordings and is
1188
- * unaffected by `dispose()`.
1189
- *
1190
- * @returns the resolved output path.
1191
- */
1192
- async toPlaywright(outputPath, options) {
1193
- await promises.mkdir(path.dirname(outputPath), { recursive: true });
1194
- await promises.writeFile(outputPath, generatePlaywrightTest(this.timeline, options), "utf8");
1195
- return outputPath;
1196
- }
1197
- /**
1198
- * Releases the captured-frames temp directory. After this call, `toVideo()`
1199
- * and `toGif()` throw — but `toTimeline()` and the in-memory `timeline`
1200
- * still work because those don't depend on the frames.
1201
- *
1202
- * **Optional.** A process-exit handler also sweeps any un-disposed frame
1203
- * dirs, so casual scripts can skip this entirely. Call it explicitly when
1204
- * you want to release frames proactively (long-running services, batch
1205
- * jobs, or anywhere you want predictable disk usage).
1206
- *
1207
- * Idempotent. Safe to call on a Recording that never had a capture
1208
- * (timeline-only mode) — no-op there.
1209
- *
1210
- * Also wired to `Symbol.asyncDispose`, so the explicit-resource-management
1211
- * `await using` syntax (TypeScript ≥ 5.2 / Node ≥ 20.4) works:
1212
- *
1213
- * ```ts
1214
- * await using rec = await human.record(fn);
1215
- * await rec.toVideo('demo.mp4');
1216
- * await rec.toGif('demo.gif');
1217
- * // frames cleaned up automatically when `rec` goes out of scope
1218
- * ```
1219
- */
1220
- async dispose() {
1221
- if (this.#disposed) return;
1222
- if (this.#capture !== null) {
1223
- await this.#capture.cleanup();
1224
- pendingFrameCleanups.delete(this.#capture.dir);
1225
- }
1226
- this.#disposed = true;
1227
- }
1228
- async [Symbol.asyncDispose]() {
1229
- await this.dispose();
1230
- }
1231
- };
1232
- function buildConcatFile(frames, totalMs) {
1233
- const lines = [];
1234
- for (let i = 0; i < frames.length; i++) {
1235
- const frame = frames[i];
1236
- const next = frames[i + 1];
1237
- const nextTMs = next ? next.tMs : totalMs;
1238
- const durationS = Math.max(1e-3, (nextTMs - frame.tMs) / 1e3);
1239
- lines.push(`file '${frame.path.replaceAll("'", "'\\''")}'`);
1240
- lines.push(`duration ${durationS.toFixed(6)}`);
1241
- }
1242
- const last = frames[frames.length - 1];
1243
- if (last) {
1244
- lines.push(`file '${last.path.replaceAll("'", "'\\''")}'`);
1245
- }
1246
- return `${lines.join("\n")}
1247
- `;
1248
- }
1249
- function runFfmpeg(args) {
1250
- if (!FFMPEG_PATH) {
1251
- return Promise.reject(
1252
- new Error(
1253
- "ffmpeg-static did not bundle a binary for this platform. Install system ffmpeg and set FFMPEG_PATH, or run on a supported platform."
1254
- )
1255
- );
1256
- }
1257
- return new Promise((resolve, reject) => {
1258
- const proc = child_process.spawn(FFMPEG_PATH, [...args]);
1259
- let stderr = "";
1260
- proc.stderr?.on("data", (chunk) => {
1261
- stderr += chunk.toString();
1262
- });
1263
- proc.on("error", reject);
1264
- proc.on("close", (code) => {
1265
- if (code === 0) resolve();
1266
- else reject(new Error(`ffmpeg exited with code ${code}
1267
- ${stderr.trim()}`));
1268
- });
1269
- });
1270
- }
1271
- async function startCapture(page, options = {}) {
1272
- const format = options.format ?? "jpeg";
1273
- const quality = options.quality ?? 95;
1274
- const fps = Math.max(1, Math.min(60, options.fps ?? 30));
1275
- const intervalMs = 1e3 / fps;
1276
- const dir = await promises.mkdtemp(path.join(os.tmpdir(), "humanjs-capture-"));
1277
- const frames = [];
1278
- const ext = format === "png" ? "png" : "jpg";
1279
- let stopped = false;
1280
- let frameIndex = 0;
1281
- const writes = [];
1282
- const startedAtMs = Date.now();
1283
- const captureLoop = async () => {
1284
- while (!stopped) {
1285
- const loopStart = Date.now();
1286
- try {
1287
- const buf = await page.screenshot({
1288
- type: format,
1289
- quality: format === "jpeg" ? quality : void 0
1290
- });
1291
- if (stopped) return;
1292
- const idx = frameIndex++;
1293
- const path$1 = path.join(dir, `frame_${String(idx).padStart(6, "0")}.${ext}`);
1294
- const tMs = loopStart - startedAtMs;
1295
- writes.push(
1296
- promises.writeFile(path$1, buf).then(
1297
- () => {
1298
- frames.push({ path: path$1, tMs });
1299
- },
1300
- (err) => {
1301
- console.warn(`humanjs capture: write failed for frame ${idx}:`, err);
1302
- }
1303
- )
1304
- );
1305
- } catch (err) {
1306
- if (stopped) return;
1307
- console.warn("humanjs capture: screenshot failed, stopping loop:", err);
1308
- stopped = true;
1309
- return;
1310
- }
1311
- const elapsed = Date.now() - loopStart;
1312
- const wait = intervalMs - elapsed;
1313
- if (wait > 0) await core.sleep(wait);
1314
- }
1315
- };
1316
- const loopPromise = captureLoop();
1317
- const finish = async () => {
1318
- stopped = true;
1319
- await loopPromise;
1320
- await Promise.allSettled(writes);
1321
- };
1322
- return {
1323
- async stop() {
1324
- await finish();
1325
- const stoppedAtMs = Date.now();
1326
- return {
1327
- dir,
1328
- frames: [...frames].sort((a, b) => a.tMs - b.tMs),
1329
- startedAtMs,
1330
- stoppedAtMs,
1331
- format,
1332
- fps,
1333
- cleanup: () => promises.rm(dir, { recursive: true, force: true }).then(() => void 0)
1334
- };
1335
- },
1336
- async abort() {
1337
- await finish();
1338
- await promises.rm(dir, { recursive: true, force: true }).catch(() => void 0);
1339
- }
1340
- };
1341
- }
1342
-
1343
- // src/mouse-helper/index.ts
1344
- var CURSOR_PATH = "M 0 0 L 16 6 L 8 9.5 L 5 19 Z";
1345
- var INSTALLED_FLAG = /* @__PURE__ */ Symbol.for("@humanjs/playwright:mouse-helper:installed");
1346
- async function installMouseHelper(target, options = {}) {
1347
- const tagged = target;
1348
- if (tagged[INSTALLED_FLAG]) return;
1349
- tagged[INSTALLED_FLAG] = true;
1350
- const config = {
1351
- color: options.color ?? "#f5a55c",
1352
- stroke: "#020203",
1353
- size: options.size ?? 22,
1354
- showClicks: options.showClicks ?? true,
1355
- haloOpacity: options.haloOpacity ?? 0.18,
1356
- path: CURSOR_PATH
1357
- };
1358
- await target.addInitScript(installScript, config);
1359
- const attachPageHooks = (page) => {
1360
- page.on("domcontentloaded", () => {
1361
- page.evaluate(installScript, config).catch(() => void 0);
1362
- });
1363
- };
1364
- const pages = "pages" in target ? target.pages() : [target];
1365
- for (const page of pages) attachPageHooks(page);
1366
- if ("on" in target && "newPage" in target) {
1367
- target.on("page", attachPageHooks);
1368
- }
1369
- await Promise.all(
1370
- pages.map((page) => page.evaluate(installScript, config).catch(() => void 0))
1371
- );
1372
- }
1373
- function installScript(config) {
1374
- if (document.querySelector("[data-humanjs-cursor]")) return;
1375
- const attach = () => {
1376
- const cursor = document.createElement("div");
1377
- cursor.setAttribute("aria-hidden", "true");
1378
- cursor.setAttribute("data-humanjs-cursor", "true");
1379
- cursor.style.cssText = [
1380
- "position: fixed",
1381
- "left: 0",
1382
- "top: 0",
1383
- `width: ${config.size}px`,
1384
- `height: ${config.size + 4}px`,
1385
- "pointer-events: none",
1386
- "z-index: 2147483647",
1387
- // Start visible at (0, 0) so the cursor is on screen from the moment
1388
- // the page loads — without this the helper looks like nothing happened
1389
- // until the first mousemove arrives.
1390
- "opacity: 1",
1391
- "transform: translate(0px, 0px)",
1392
- // CSS interpolates between successive `mousemove` updates so the
1393
- // cursor reads as continuous motion instead of discrete hops. Slightly
1394
- // longer than the path-walker's typical step interval (~30–80ms) so
1395
- // each tween is still settling when the next move lands → no pauses.
1396
- "transition: transform 110ms ease-out, opacity 0.18s ease-out",
1397
- "will-change: transform"
1398
- ].join("; ");
1399
- const haloRadius = Math.round(config.size * 0.6);
1400
- cursor.innerHTML = `
1401
- <svg width="${config.size}" height="${config.size + 4}" viewBox="0 0 22 24" style="overflow: visible;">
1402
- <circle cx="0" cy="0" r="${haloRadius}" fill="${config.color}" opacity="${config.haloOpacity}" />
1403
- <path d="${config.path}" fill="${config.color}" stroke="${config.stroke}" stroke-width="0.7" stroke-linejoin="round" />
1404
- </svg>
1405
- `;
1406
- document.body.appendChild(cursor);
1407
- let lastX = 0;
1408
- let lastY = 0;
1409
- const onMove = (e) => {
1410
- lastX = e.clientX;
1411
- lastY = e.clientY;
1412
- cursor.style.transform = `translate(${lastX}px, ${lastY}px)`;
1413
- cursor.style.opacity = "1";
1414
- };
1415
- window.addEventListener("mousemove", onMove, { capture: true, passive: true });
1416
- document.addEventListener("mousemove", onMove, { capture: true, passive: true });
1417
- document.addEventListener(
1418
- "mouseleave",
1419
- () => {
1420
- cursor.style.opacity = "0";
1421
- },
1422
- { capture: true, passive: true }
1423
- );
1424
- if (config.showClicks) {
1425
- const styleEl = document.createElement("style");
1426
- 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; } }";
1427
- document.head.appendChild(styleEl);
1428
- window.addEventListener(
1429
- "mousedown",
1430
- () => {
1431
- const ripple = document.createElement("div");
1432
- ripple.style.cssText = [
1433
- "position: fixed",
1434
- `left: ${lastX}px`,
1435
- `top: ${lastY}px`,
1436
- "width: 28px",
1437
- "height: 28px",
1438
- "border-radius: 50%",
1439
- `border: 1.5px solid ${config.color}`,
1440
- "pointer-events: none",
1441
- "z-index: 2147483646",
1442
- "animation: humanjs-ripple 0.45s ease-out forwards"
1443
- ].join("; ");
1444
- document.body.appendChild(ripple);
1445
- window.setTimeout(() => ripple.remove(), 500);
1446
- },
1447
- { capture: true, passive: true }
1448
- );
1449
- }
1450
- };
1451
- if (document.body) attach();
1452
- else document.addEventListener("DOMContentLoaded", attach, { once: true });
1453
- }
1454
-
1455
- // src/index.ts
1456
- async function createHuman(page, options = {}) {
1457
- const personality = core.resolvePersonality(options.personality ?? "careful");
1458
- const rng = core.createRng(options.seed);
1459
- const speed = options.speed ?? "human";
1460
- const plugins = options.plugins ?? [];
1461
- const context = { personality, rng };
1462
- for (const plugin of plugins) {
1463
- await plugin.install?.(context);
1464
- }
1465
- let hasRecorded = false;
1466
- let activeRecordingEvents = null;
1467
- let activeRecordingStartMs = 0;
1468
- let activeRecordingCaptureInputs = false;
1469
- async function performAction(action, actionFn, recordMeta) {
1470
- for (const plugin of plugins) {
1471
- await plugin.beforeAction?.(action);
1472
- }
1473
- const startedAt = Date.now();
1474
- try {
1475
- const value = await actionFn();
1476
- const durationMs = Date.now() - startedAt;
1477
- const result = { type: action.type, durationMs };
1478
- if (activeRecordingEvents !== null && action.type !== "record") {
1479
- activeRecordingEvents.push({
1480
- type: action.type,
1481
- params: action.params ?? {},
1482
- tMs: startedAt - activeRecordingStartMs,
1483
- durationMs,
1484
- ...recordMeta?.inputValue !== void 0 ? { inputValue: recordMeta.inputValue } : {}
1485
- });
1486
- }
1487
- for (const plugin of plugins) {
1488
- await plugin.afterAction?.(action, result);
1489
- }
1490
- return value;
1491
- } catch (error) {
1492
- if (activeRecordingEvents !== null && action.type !== "record") {
1493
- activeRecordingEvents.push({
1494
- type: action.type,
1495
- params: action.params ?? {},
1496
- tMs: startedAt - activeRecordingStartMs,
1497
- durationMs: Date.now() - startedAt,
1498
- error: error instanceof Error ? error.message : String(error),
1499
- ...recordMeta?.inputValue !== void 0 ? { inputValue: recordMeta.inputValue } : {}
1500
- });
1501
- }
1502
- for (const plugin of plugins) {
1503
- await plugin.onError?.(action, error);
1504
- }
1505
- throw error;
1506
- }
1507
- }
1508
- let lastMousePosition = options.initialMousePosition ?? { x: 0, y: 0 };
1509
- const mouseCtx = () => ({
1510
- page,
1511
- personality,
1512
- rng,
1513
- speed,
1514
- getMousePosition: () => lastMousePosition,
1515
- setMousePosition: (point) => {
1516
- lastMousePosition = point;
1517
- }
1518
- });
1519
- const describeMouseTarget = (target) => {
1520
- if (typeof target === "string") return target;
1521
- if ("x" in target && "y" in target && typeof target.x === "number") {
1522
- return `point(${target.x}, ${target.y})`;
1523
- }
1524
- return target.toString?.() ?? "locator";
1525
- };
1526
- const isPointTarget = (target) => typeof target !== "string" && "x" in target && "y" in target && typeof target.x === "number";
1527
- const captureInputValue = async (target, value) => {
1528
- if (activeRecordingEvents === null || !activeRecordingCaptureInputs) return void 0;
1529
- const locator = typeof target === "string" ? page.locator(target) : isPointTarget(target) ? null : target;
1530
- if (locator !== null) {
1531
- try {
1532
- const fieldType = await locator.first().getAttribute("type", { timeout: 1e3 });
1533
- if (fieldType?.toLowerCase() === "password") {
1534
- return void 0;
1535
- }
1536
- } catch {
1537
- }
1538
- }
1539
- return value;
1540
- };
1541
- return {
1542
- personality,
1543
- speed,
1544
- async goto(url) {
1545
- await performAction({ type: "goto", params: { url } }, async () => {
1546
- await page.goto(url);
1547
- });
1548
- },
1549
- async click(target) {
1550
- const description = describeMouseTarget(target);
1551
- await performAction({ type: "click", params: { target: description } }, async () => {
1552
- await executeClick(target, mouseCtx());
1553
- });
1554
- },
1555
- async rightClick(target) {
1556
- const description = describeMouseTarget(target);
1557
- await performAction({ type: "rightClick", params: { target: description } }, async () => {
1558
- await executeClick(target, mouseCtx(), { button: "right" });
1559
- });
1560
- },
1561
- async hover(target) {
1562
- const description = describeMouseTarget(target);
1563
- await performAction({ type: "hover", params: { target: description } }, async () => {
1564
- await executeHover(target, mouseCtx());
1565
- });
1566
- },
1567
- async move(target) {
1568
- const description = describeMouseTarget(target);
1569
- await performAction({ type: "move", params: { target: description } }, async () => {
1570
- await executeMove(target, mouseCtx());
1571
- });
1572
- },
1573
- async drag(from, to) {
1574
- const fromDesc = describeMouseTarget(from);
1575
- const toDesc = describeMouseTarget(to);
1576
- await performAction({ type: "drag", params: { from: fromDesc, to: toDesc } }, async () => {
1577
- await executeDrag(from, to, mouseCtx());
1578
- });
1579
- },
1580
- async type(target, value) {
1581
- const description = describeMouseTarget(target);
1582
- const inputValue = await captureInputValue(target, value);
1583
- await performAction(
1584
- { type: "type", params: { target: description, length: value.length } },
1585
- async () => {
1586
- if (speed !== "instant" && value.length > 0) {
1587
- await executeClick(target, mouseCtx());
1588
- }
1589
- await executeType(target, value, { page, personality, rng, speed });
1590
- },
1591
- inputValue !== void 0 ? { inputValue } : void 0
1592
- );
1593
- },
1594
- async paste(target, value) {
1595
- const description = describeMouseTarget(target);
1596
- const inputValue = await captureInputValue(target, value);
1597
- await performAction(
1598
- { type: "paste", params: { target: description, length: value.length } },
1599
- async () => {
1600
- if (speed !== "instant" && value.length > 0) {
1601
- await executeClick(target, mouseCtx());
1602
- }
1603
- await executePaste(target, value, { page, personality, rng, speed });
1604
- },
1605
- inputValue !== void 0 ? { inputValue } : void 0
1606
- );
1607
- },
1608
- async press(key) {
1609
- await performAction({ type: "press", params: { key } }, async () => {
1610
- await executePress(key, { page, personality, rng, speed });
1611
- });
1612
- },
1613
- async read(target, options2) {
1614
- const description = describeReadTarget(target);
1615
- return performAction(
1616
- {
1617
- type: "read",
1618
- params: {
1619
- target: description,
1620
- kind: options2?.kind
1621
- }
1622
- },
1623
- () => executeRead(
1624
- target,
1625
- {
1626
- page,
1627
- personality,
1628
- rng,
1629
- speed,
1630
- // Read shares the session's tracked cursor position so an eye
1631
- // scan starts from where the last click left off, and the next
1632
- // click starts from where the scan ended.
1633
- getMousePosition: () => lastMousePosition,
1634
- setMousePosition: (point) => {
1635
- lastMousePosition = point;
1636
- }
1637
- },
1638
- options2
1639
- )
1640
- );
1641
- },
1642
- async scroll(target, options2) {
1643
- const description = describeScrollTarget(target);
1644
- return performAction(
1645
- {
1646
- type: "scroll",
1647
- params: { target: description }
1648
- },
1649
- () => executeScroll(target, { page, personality, rng, speed }, options2)
1650
- );
1651
- },
1652
- async sleep(ms) {
1653
- await performAction({ type: "sleep", params: { ms } }, () => core.sleep(ms));
1654
- },
1655
- async record(optionsOrFn, maybeFn) {
1656
- const [recordOptions, fn] = typeof optionsOrFn === "function" ? [{}, optionsOrFn] : [optionsOrFn, maybeFn];
1657
- if (hasRecorded) {
1658
- throw new Error(
1659
- "human.record() can only be called once per session. Create a new browser context (and a new human session) to record a separate clip."
1660
- );
1661
- }
1662
- hasRecorded = true;
1663
- const captureEnabled = recordOptions.video !== false;
1664
- const captureQuality = recordOptions.quality ?? "high";
1665
- let captureSession = null;
1666
- if (captureEnabled) {
1667
- const { format, quality, fps } = getCaptureSettingsForQuality(captureQuality);
1668
- captureSession = await startCapture(page, { format, quality, fps });
1669
- }
1670
- const events = [];
1671
- const windowStartMs = Date.now();
1672
- activeRecordingEvents = events;
1673
- activeRecordingStartMs = windowStartMs;
1674
- activeRecordingCaptureInputs = recordOptions.captureInputs !== false;
1675
- let windowEndMs = windowStartMs;
1676
- try {
1677
- await performAction({ type: "record", params: {} }, async () => {
1678
- try {
1679
- await fn();
1680
- } finally {
1681
- windowEndMs = Date.now();
1682
- }
1683
- });
1684
- } catch (error) {
1685
- if (captureSession) await captureSession.abort();
1686
- throw error;
1687
- } finally {
1688
- activeRecordingEvents = null;
1689
- activeRecordingCaptureInputs = false;
1690
- }
1691
- const captureResult = captureSession ? await captureSession.stop() : null;
1692
- return new Recording(captureResult, windowStartMs, windowEndMs, {
1693
- name: recordOptions.name,
1694
- personality: personality.name,
1695
- seed: options.seed === void 0 ? null : String(options.seed),
1696
- speed,
1697
- events
1698
- });
1699
- },
1700
- // ────────────────────────────────────────────────────────────────────
1701
- // Thin re-exports of common Playwright `Page` methods. See the `Human`
1702
- // interface for the rationale; implementations forward unchanged.
1703
- // ────────────────────────────────────────────────────────────────────
1704
- screenshot(opts) {
1705
- return page.screenshot(opts);
1706
- },
1707
- pageText() {
1708
- return page.innerText("body");
1709
- },
1710
- content() {
1711
- return page.content();
1712
- },
1713
- url() {
1714
- return page.url();
1715
- },
1716
- title() {
1717
- return page.title();
1718
- },
1719
- async reload(opts) {
1720
- await performAction({ type: "reload", params: {} }, async () => {
1721
- await page.reload(opts);
1722
- });
1723
- },
1724
- async goBack(opts) {
1725
- await performAction({ type: "goBack", params: {} }, async () => {
1726
- await page.goBack(opts);
1727
- });
1728
- },
1729
- async goForward(opts) {
1730
- await performAction({ type: "goForward", params: {} }, async () => {
1731
- await page.goForward(opts);
1732
- });
1733
- },
1734
- waitForLoadState(state, opts) {
1735
- return page.waitForLoadState(state, opts);
1736
- },
1737
- waitForURL(url, opts) {
1738
- return page.waitForURL(url, opts);
1739
- },
1740
- setViewportSize(size) {
1741
- return page.setViewportSize(size);
1742
- },
1743
- pdf(opts) {
1744
- return page.pdf(opts);
1745
- }
1746
- };
1747
- }
1748
- function describeScrollTarget(target) {
1749
- if (target === void 0) return "natural";
1750
- if (typeof target === "string") return target;
1751
- if ("by" in target) return `by:${target.by}`;
1752
- if ("to" in target) return `to:${target.to}`;
1753
- return target.toString?.() ?? "locator";
1754
- }
1755
- function describeReadTarget(target) {
1756
- if (typeof target === "string") return target;
1757
- if ("words" in target && typeof target.words === "number") return `${target.words} words`;
1758
- if ("text" in target && typeof target.text === "string") {
1759
- return `text:${target.text.length} chars`;
1760
- }
1761
- return target.toString?.() ?? "locator";
1762
- }
1763
-
1764
11
  Object.defineProperty(exports, "applyMicroJitter", {
1765
12
  enumerable: true,
1766
- get: function () { return core.applyMicroJitter; }
13
+ get: function () { return chunk3X36PFTS_cjs.applyMicroJitter; }
1767
14
  });
1768
15
  Object.defineProperty(exports, "applyVelocityProfile", {
1769
16
  enumerable: true,
1770
- get: function () { return core.applyVelocityProfile; }
17
+ get: function () { return chunk3X36PFTS_cjs.applyVelocityProfile; }
1771
18
  });
1772
19
  Object.defineProperty(exports, "bezierPath", {
1773
20
  enumerable: true,
1774
- get: function () { return core.bezierPath; }
21
+ get: function () { return chunk3X36PFTS_cjs.bezierPath; }
1775
22
  });
1776
23
  Object.defineProperty(exports, "blend", {
1777
24
  enumerable: true,
1778
- get: function () { return core.blend; }
25
+ get: function () { return chunk3X36PFTS_cjs.blend; }
1779
26
  });
1780
27
  Object.defineProperty(exports, "careful", {
1781
28
  enumerable: true,
1782
- get: function () { return core.careful; }
29
+ get: function () { return chunk3X36PFTS_cjs.careful; }
30
+ });
31
+ Object.defineProperty(exports, "chromium", {
32
+ enumerable: true,
33
+ get: function () { return chunk3X36PFTS_cjs.chromium; }
1783
34
  });
1784
35
  Object.defineProperty(exports, "computeReadingDwellMs", {
1785
36
  enumerable: true,
1786
- get: function () { return core.computeReadingDwellMs; }
37
+ get: function () { return chunk3X36PFTS_cjs.computeReadingDwellMs; }
1787
38
  });
1788
39
  Object.defineProperty(exports, "countWords", {
1789
40
  enumerable: true,
1790
- get: function () { return core.countWords; }
41
+ get: function () { return chunk3X36PFTS_cjs.countWords; }
42
+ });
43
+ Object.defineProperty(exports, "createHuman", {
44
+ enumerable: true,
45
+ get: function () { return chunk3X36PFTS_cjs.createHuman; }
1791
46
  });
1792
47
  Object.defineProperty(exports, "createRng", {
1793
48
  enumerable: true,
1794
- get: function () { return core.createRng; }
49
+ get: function () { return chunk3X36PFTS_cjs.createRng; }
1795
50
  });
1796
51
  Object.defineProperty(exports, "distracted", {
1797
52
  enumerable: true,
1798
- get: function () { return core.distracted; }
53
+ get: function () { return chunk3X36PFTS_cjs.distracted; }
1799
54
  });
1800
55
  Object.defineProperty(exports, "fast", {
1801
56
  enumerable: true,
1802
- get: function () { return core.fast; }
57
+ get: function () { return chunk3X36PFTS_cjs.fast; }
58
+ });
59
+ Object.defineProperty(exports, "firefox", {
60
+ enumerable: true,
61
+ get: function () { return chunk3X36PFTS_cjs.firefox; }
62
+ });
63
+ Object.defineProperty(exports, "generateHumanJS", {
64
+ enumerable: true,
65
+ get: function () { return chunk3X36PFTS_cjs.generateHumanJS; }
66
+ });
67
+ Object.defineProperty(exports, "generatePlaywrightTest", {
68
+ enumerable: true,
69
+ get: function () { return chunk3X36PFTS_cjs.generatePlaywrightTest; }
1803
70
  });
1804
71
  Object.defineProperty(exports, "humanizePath", {
1805
72
  enumerable: true,
1806
- get: function () { return core.humanizePath; }
73
+ get: function () { return chunk3X36PFTS_cjs.humanizePath; }
74
+ });
75
+ Object.defineProperty(exports, "installMouseHelper", {
76
+ enumerable: true,
77
+ get: function () { return chunk3X36PFTS_cjs.installMouseHelper; }
1807
78
  });
1808
79
  Object.defineProperty(exports, "planScroll", {
1809
80
  enumerable: true,
1810
- get: function () { return core.planScroll; }
81
+ get: function () { return chunk3X36PFTS_cjs.planScroll; }
1811
82
  });
1812
83
  Object.defineProperty(exports, "planTypeKeystrokes", {
1813
84
  enumerable: true,
1814
- get: function () { return core.planTypeKeystrokes; }
85
+ get: function () { return chunk3X36PFTS_cjs.planTypeKeystrokes; }
1815
86
  });
1816
87
  Object.defineProperty(exports, "precise", {
1817
88
  enumerable: true,
1818
- get: function () { return core.precise; }
89
+ get: function () { return chunk3X36PFTS_cjs.precise; }
1819
90
  });
1820
91
  Object.defineProperty(exports, "resolvePersonality", {
1821
92
  enumerable: true,
1822
- get: function () { return core.resolvePersonality; }
93
+ get: function () { return chunk3X36PFTS_cjs.resolvePersonality; }
1823
94
  });
1824
95
  Object.defineProperty(exports, "sleep", {
1825
96
  enumerable: true,
1826
- get: function () { return core.sleep; }
1827
- });
1828
- Object.defineProperty(exports, "chromium", {
1829
- enumerable: true,
1830
- get: function () { return playwright.chromium; }
1831
- });
1832
- Object.defineProperty(exports, "firefox", {
1833
- enumerable: true,
1834
- get: function () { return playwright.firefox; }
97
+ get: function () { return chunk3X36PFTS_cjs.sleep; }
1835
98
  });
1836
99
  Object.defineProperty(exports, "webkit", {
1837
100
  enumerable: true,
1838
- get: function () { return playwright.webkit; }
101
+ get: function () { return chunk3X36PFTS_cjs.webkit; }
1839
102
  });
1840
- exports.Recording = Recording;
1841
- exports.createHuman = createHuman;
1842
- exports.installMouseHelper = installMouseHelper;
1843
103
  //# sourceMappingURL=index.cjs.map
1844
104
  //# sourceMappingURL=index.cjs.map