@humanjs/playwright 0.3.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/README.md +200 -7
- package/dist/index.cjs +1022 -183
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +406 -31
- package/dist/index.d.ts +406 -31
- package/dist/index.js +1004 -186
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
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');
|
|
11
|
+
|
|
12
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
|
+
|
|
14
|
+
var ffmpegStatic__default = /*#__PURE__*/_interopDefault(ffmpegStatic);
|
|
4
15
|
|
|
5
16
|
// src/index.ts
|
|
6
17
|
|
|
@@ -57,6 +68,73 @@ async function dispatchKey(page, key) {
|
|
|
57
68
|
await page.keyboard.insertText(key);
|
|
58
69
|
}
|
|
59
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
|
+
}
|
|
60
138
|
|
|
61
139
|
// src/internal/mouse-walk.ts
|
|
62
140
|
async function walkMouseAlongPath(page, path, durationMs) {
|
|
@@ -71,168 +149,6 @@ async function walkMouseAlongPath(page, path, durationMs) {
|
|
|
71
149
|
}
|
|
72
150
|
}
|
|
73
151
|
}
|
|
74
|
-
|
|
75
|
-
// src/mouse/index.ts
|
|
76
|
-
async function executeClick(target, ctx) {
|
|
77
|
-
const locator = typeof target === "string" ? ctx.page.locator(target) : target;
|
|
78
|
-
if (ctx.speed === "instant") {
|
|
79
|
-
const box2 = await locator.boundingBox();
|
|
80
|
-
await locator.click();
|
|
81
|
-
const center = box2 ? { x: box2.x + box2.width / 2, y: box2.y + box2.height / 2 } : ctx.getMousePosition();
|
|
82
|
-
ctx.setMousePosition(center);
|
|
83
|
-
return { target: center };
|
|
84
|
-
}
|
|
85
|
-
const box = await locator.boundingBox();
|
|
86
|
-
if (!box) {
|
|
87
|
-
throw new Error(
|
|
88
|
-
`Cannot click: element not found or has no bounding box (target: ${describeTarget(target)})`
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
const targetPoint = pickClickPoint(box, ctx.rng);
|
|
92
|
-
const startPoint = ctx.getMousePosition();
|
|
93
|
-
const rawPath = core.bezierPath(startPoint, targetPoint, ctx.rng, {
|
|
94
|
-
curvature: ctx.personality.mouse.curvature
|
|
95
|
-
});
|
|
96
|
-
const path = core.humanizePath(rawPath, ctx.rng);
|
|
97
|
-
const travelMs = computeTravelTime(path, ctx.personality, ctx.speed, ctx.rng);
|
|
98
|
-
await walkMouseAlongPath(ctx.page, path, travelMs);
|
|
99
|
-
const preClickMs = computeDwellTime(
|
|
100
|
-
ctx.personality.dwell.preClickMs,
|
|
101
|
-
ctx.personality.dwell.preClickJitter,
|
|
102
|
-
ctx.personality,
|
|
103
|
-
ctx.speed,
|
|
104
|
-
ctx.rng
|
|
105
|
-
);
|
|
106
|
-
if (preClickMs > 0) await sleep(preClickMs);
|
|
107
|
-
ctx.setMousePosition(targetPoint);
|
|
108
|
-
await ctx.page.mouse.click(targetPoint.x, targetPoint.y);
|
|
109
|
-
const postActionMs = computeDwellTime(
|
|
110
|
-
ctx.personality.dwell.postActionMs,
|
|
111
|
-
ctx.personality.dwell.postActionJitter,
|
|
112
|
-
ctx.personality,
|
|
113
|
-
ctx.speed,
|
|
114
|
-
ctx.rng
|
|
115
|
-
);
|
|
116
|
-
if (postActionMs > 0) await sleep(postActionMs);
|
|
117
|
-
return { target: targetPoint };
|
|
118
|
-
}
|
|
119
|
-
function pickClickPoint(box, rng) {
|
|
120
|
-
const cx = box.x + box.width / 2;
|
|
121
|
-
const cy = box.y + box.height / 2;
|
|
122
|
-
const x = clamp(cx + rng.nextGaussian(0, box.width / 8), box.x, box.x + box.width);
|
|
123
|
-
const y = clamp(cy + rng.nextGaussian(0, box.height / 8), box.y, box.y + box.height);
|
|
124
|
-
return { x, y };
|
|
125
|
-
}
|
|
126
|
-
function computeTravelTime(path, personality, speed, rng) {
|
|
127
|
-
let distance = 0;
|
|
128
|
-
for (let i = 1; i < path.length; i++) {
|
|
129
|
-
const prev = path[i - 1];
|
|
130
|
-
const curr = path[i];
|
|
131
|
-
if (!prev || !curr) continue;
|
|
132
|
-
distance += Math.hypot(curr.x - prev.x, curr.y - prev.y);
|
|
133
|
-
}
|
|
134
|
-
const baseTime = distance / 1e3 * personality.mouse.travelTimeMs;
|
|
135
|
-
const jitterMag = baseTime * personality.mouse.travelTimeJitter;
|
|
136
|
-
const jitter = rng.nextFloat(-jitterMag, jitterMag);
|
|
137
|
-
const total = (baseTime + jitter) * personality.speed * speedModeFactor(speed);
|
|
138
|
-
return Math.max(0, total);
|
|
139
|
-
}
|
|
140
|
-
function describeTarget(target) {
|
|
141
|
-
return typeof target === "string" ? target : target.toString?.() ?? "locator";
|
|
142
|
-
}
|
|
143
|
-
function clamp(value, min, max) {
|
|
144
|
-
return value < min ? min : value > max ? max : value;
|
|
145
|
-
}
|
|
146
|
-
async function executeRead(target, ctx, options = {}) {
|
|
147
|
-
let words = 0;
|
|
148
|
-
let locator;
|
|
149
|
-
if (typeof target === "string") {
|
|
150
|
-
locator = ctx.page.locator(target);
|
|
151
|
-
} else if ("words" in target) {
|
|
152
|
-
words = target.words;
|
|
153
|
-
} else if ("text" in target) {
|
|
154
|
-
words = core.countWords(target.text);
|
|
155
|
-
} else {
|
|
156
|
-
locator = target;
|
|
157
|
-
}
|
|
158
|
-
let autoDetectedKind;
|
|
159
|
-
if (locator) {
|
|
160
|
-
if (options.scrollIntoView) {
|
|
161
|
-
await locator.scrollIntoViewIfNeeded();
|
|
162
|
-
}
|
|
163
|
-
const text = await locator.innerText().catch(() => "");
|
|
164
|
-
words = core.countWords(text);
|
|
165
|
-
if (options.kind === void 0) {
|
|
166
|
-
autoDetectedKind = await detectKindFromTag(locator);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
const kind = options.kind ?? autoDetectedKind ?? "prose";
|
|
170
|
-
const durationMs = core.computeReadingDwellMs(words, ctx.personality.reading, ctx.rng, {
|
|
171
|
-
kind,
|
|
172
|
-
wpmMultiplier: options.wpmMultiplier,
|
|
173
|
-
personalitySpeed: ctx.personality.speed,
|
|
174
|
-
speedFactor: speedModeFactor(ctx.speed)
|
|
175
|
-
});
|
|
176
|
-
if (options.withMotion && locator && durationMs > 0) {
|
|
177
|
-
const box = await locator.boundingBox().catch(() => null);
|
|
178
|
-
if (box) {
|
|
179
|
-
const lineRects = await getLineRects(locator).catch(() => []);
|
|
180
|
-
const path = core.planReadingScan(box, ctx.rng, {
|
|
181
|
-
start: ctx.getMousePosition(),
|
|
182
|
-
lineRects: lineRects.length > 0 ? lineRects : void 0
|
|
183
|
-
});
|
|
184
|
-
await walkMouseAlongPath(ctx.page, path, durationMs);
|
|
185
|
-
const final = path[path.length - 1];
|
|
186
|
-
if (final) ctx.setMousePosition(final);
|
|
187
|
-
return { words, durationMs, kind };
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
if (durationMs > 0) await sleep(durationMs);
|
|
191
|
-
return { words, durationMs, kind };
|
|
192
|
-
}
|
|
193
|
-
async function getLineRects(locator) {
|
|
194
|
-
const result = await locator.evaluate((el) => {
|
|
195
|
-
const walker = el.ownerDocument.createTreeWalker(el, 4);
|
|
196
|
-
const rects = [];
|
|
197
|
-
let node = walker.nextNode();
|
|
198
|
-
while (node) {
|
|
199
|
-
const text = node.textContent ?? "";
|
|
200
|
-
if (text.trim().length > 0) {
|
|
201
|
-
const range = el.ownerDocument.createRange();
|
|
202
|
-
range.selectNodeContents(node);
|
|
203
|
-
for (const r of Array.from(range.getClientRects())) {
|
|
204
|
-
if (r.width > 0 && r.height > 0) {
|
|
205
|
-
rects.push({ x: r.x, y: r.y, width: r.width, height: r.height });
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
node = walker.nextNode();
|
|
210
|
-
}
|
|
211
|
-
rects.sort((a, b) => a.y - b.y || a.x - b.x);
|
|
212
|
-
const merged = [];
|
|
213
|
-
for (const r of rects) {
|
|
214
|
-
const last = merged[merged.length - 1];
|
|
215
|
-
if (last && Math.abs(last.y - r.y) < 1 && r.x - (last.x + last.width) < 6) {
|
|
216
|
-
const right = Math.max(last.x + last.width, r.x + r.width);
|
|
217
|
-
const bottom = Math.max(last.y + last.height, r.y + r.height);
|
|
218
|
-
last.width = right - last.x;
|
|
219
|
-
last.height = bottom - last.y;
|
|
220
|
-
} else {
|
|
221
|
-
merged.push({ ...r });
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return merged;
|
|
225
|
-
});
|
|
226
|
-
if (!Array.isArray(result)) return [];
|
|
227
|
-
return result.filter(
|
|
228
|
-
(r) => r != null && typeof r === "object" && typeof r.x === "number" && typeof r.y === "number" && typeof r.width === "number" && typeof r.height === "number"
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
async function detectKindFromTag(locator) {
|
|
232
|
-
const tag = await locator.evaluate((el) => el.tagName?.toLowerCase() ?? "").catch(() => "");
|
|
233
|
-
if (tag === "pre" || tag === "code") return "code";
|
|
234
|
-
return void 0;
|
|
235
|
-
}
|
|
236
152
|
var RESERVED_TARGETS = /* @__PURE__ */ new Set(["natural", "end", "top"]);
|
|
237
153
|
async function executeScroll(target, ctx, options = {}) {
|
|
238
154
|
const { page, personality, rng, speed } = ctx;
|
|
@@ -245,7 +161,7 @@ async function executeScroll(target, ctx, options = {}) {
|
|
|
245
161
|
}
|
|
246
162
|
const from = geom.current;
|
|
247
163
|
const targetPos = await resolveTarget(target, ctx, geom, container, axis, options.block);
|
|
248
|
-
const to =
|
|
164
|
+
const to = clamp(targetPos, 0, Math.max(0, geom.total - geom.viewport));
|
|
249
165
|
const distance = to - from;
|
|
250
166
|
if (distance === 0) {
|
|
251
167
|
return { from, to, distance: 0, durationMs: 0 };
|
|
@@ -402,13 +318,797 @@ async function walkSegments(page, segments, axis, container) {
|
|
|
402
318
|
}
|
|
403
319
|
}
|
|
404
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
|
+
const locator = typeof target === "string" ? ctx.page.locator(target) : target;
|
|
329
|
+
if (ctx.speed === "instant") {
|
|
330
|
+
const box = await locator.boundingBox();
|
|
331
|
+
await locator.click({ button });
|
|
332
|
+
const center = box ? { x: box.x + box.width / 2, y: box.y + box.height / 2 } : ctx.getMousePosition();
|
|
333
|
+
ctx.setMousePosition(center);
|
|
334
|
+
return { target: center };
|
|
335
|
+
}
|
|
336
|
+
const targetPoint = await moveToTarget(target, ctx, "click");
|
|
337
|
+
const preClickMs = computeDwellTime(
|
|
338
|
+
ctx.personality.dwell.preClickMs,
|
|
339
|
+
ctx.personality.dwell.preClickJitter,
|
|
340
|
+
ctx.personality,
|
|
341
|
+
ctx.speed,
|
|
342
|
+
ctx.rng
|
|
343
|
+
);
|
|
344
|
+
if (preClickMs > 0) await sleep(preClickMs);
|
|
345
|
+
ctx.setMousePosition(targetPoint);
|
|
346
|
+
await ctx.page.mouse.click(targetPoint.x, targetPoint.y, { button });
|
|
347
|
+
const postActionMs = computeDwellTime(
|
|
348
|
+
ctx.personality.dwell.postActionMs,
|
|
349
|
+
ctx.personality.dwell.postActionJitter,
|
|
350
|
+
ctx.personality,
|
|
351
|
+
ctx.speed,
|
|
352
|
+
ctx.rng
|
|
353
|
+
);
|
|
354
|
+
if (postActionMs > 0) await sleep(postActionMs);
|
|
355
|
+
return { target: targetPoint };
|
|
356
|
+
}
|
|
357
|
+
async function executeHover(target, ctx) {
|
|
358
|
+
if (ctx.speed === "instant") {
|
|
359
|
+
const box = await readBoxWithAutoScroll(target, ctx, "hover");
|
|
360
|
+
const center = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
361
|
+
await ctx.page.mouse.move(center.x, center.y);
|
|
362
|
+
ctx.setMousePosition(center);
|
|
363
|
+
return { target: center };
|
|
364
|
+
}
|
|
365
|
+
const targetPoint = await moveToTarget(target, ctx, "hover");
|
|
366
|
+
const dwellMs = computeDwellTime(
|
|
367
|
+
ctx.personality.dwell.preClickMs,
|
|
368
|
+
ctx.personality.dwell.preClickJitter,
|
|
369
|
+
ctx.personality,
|
|
370
|
+
ctx.speed,
|
|
371
|
+
ctx.rng
|
|
372
|
+
);
|
|
373
|
+
if (dwellMs > 0) await sleep(dwellMs);
|
|
374
|
+
ctx.setMousePosition(targetPoint);
|
|
375
|
+
return { target: targetPoint };
|
|
376
|
+
}
|
|
377
|
+
async function executeDrag(from, to, ctx) {
|
|
378
|
+
const scrollYBeforeResolve = await readScrollY(ctx.page);
|
|
379
|
+
let { point: fromPoint, box: fromBox } = await resolveTargetPointAndBox(from, ctx, "drag");
|
|
380
|
+
let { point: toPoint, box: toBox } = await resolveTargetPointAndBox(to, ctx, "drag");
|
|
381
|
+
const resolveScrollDelta = await readScrollY(ctx.page) - scrollYBeforeResolve;
|
|
382
|
+
if (resolveScrollDelta !== 0) {
|
|
383
|
+
if (isPoint(from)) fromPoint = { x: fromPoint.x, y: fromPoint.y - resolveScrollDelta };
|
|
384
|
+
if (isPoint(to)) toPoint = { x: toPoint.x, y: toPoint.y - resolveScrollDelta };
|
|
385
|
+
}
|
|
386
|
+
if (ctx.speed === "instant") {
|
|
387
|
+
await ctx.page.mouse.move(fromPoint.x, fromPoint.y);
|
|
388
|
+
await ctx.page.mouse.down();
|
|
389
|
+
await ctx.page.mouse.move(toPoint.x, toPoint.y);
|
|
390
|
+
await ctx.page.mouse.up();
|
|
391
|
+
ctx.setMousePosition(toPoint);
|
|
392
|
+
return { from: fromPoint, to: toPoint };
|
|
393
|
+
}
|
|
394
|
+
if (fromBox && toBox) {
|
|
395
|
+
const scrollDelta = computeCurveScrollDelta(
|
|
396
|
+
fromPoint,
|
|
397
|
+
toPoint,
|
|
398
|
+
ctx.page.viewportSize(),
|
|
399
|
+
ctx.personality.mouse.curvature
|
|
400
|
+
);
|
|
401
|
+
if (scrollDelta !== 0) {
|
|
402
|
+
await executeScroll({ by: scrollDelta }, ctx, {});
|
|
403
|
+
const refreshedFrom = await resolveTargetPointAndBox(from, ctx, "drag");
|
|
404
|
+
fromPoint = refreshedFrom.point;
|
|
405
|
+
fromBox = refreshedFrom.box;
|
|
406
|
+
const refreshedTo = await resolveTargetPointAndBox(to, ctx, "drag");
|
|
407
|
+
toPoint = refreshedTo.point;
|
|
408
|
+
toBox = refreshedTo.box;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
await maybeMisclickBeat(ctx, fromBox, fromPoint);
|
|
412
|
+
await walkBezierTo(fromPoint, ctx);
|
|
413
|
+
ctx.setMousePosition(fromPoint);
|
|
414
|
+
const preDragMs = computeDwellTime(
|
|
415
|
+
ctx.personality.dwell.preClickMs,
|
|
416
|
+
ctx.personality.dwell.preClickJitter,
|
|
417
|
+
ctx.personality,
|
|
418
|
+
ctx.speed,
|
|
419
|
+
ctx.rng
|
|
420
|
+
);
|
|
421
|
+
if (preDragMs > 0) await sleep(preDragMs);
|
|
422
|
+
await ctx.page.mouse.down();
|
|
423
|
+
await maybeMisclickBeat(ctx, toBox, toPoint);
|
|
424
|
+
await walkBezierTo(toPoint, ctx);
|
|
425
|
+
await ctx.page.mouse.up();
|
|
426
|
+
ctx.setMousePosition(toPoint);
|
|
427
|
+
const postActionMs = computeDwellTime(
|
|
428
|
+
ctx.personality.dwell.postActionMs,
|
|
429
|
+
ctx.personality.dwell.postActionJitter,
|
|
430
|
+
ctx.personality,
|
|
431
|
+
ctx.speed,
|
|
432
|
+
ctx.rng
|
|
433
|
+
);
|
|
434
|
+
if (postActionMs > 0) await sleep(postActionMs);
|
|
435
|
+
return { from: fromPoint, to: toPoint };
|
|
436
|
+
}
|
|
437
|
+
async function executeMove(target, ctx) {
|
|
438
|
+
const point = await resolveTargetPoint(target, ctx, "move");
|
|
439
|
+
if (ctx.speed === "instant") {
|
|
440
|
+
await ctx.page.mouse.move(point.x, point.y);
|
|
441
|
+
ctx.setMousePosition(point);
|
|
442
|
+
return { target: point };
|
|
443
|
+
}
|
|
444
|
+
await walkBezierTo(point, ctx);
|
|
445
|
+
ctx.setMousePosition(point);
|
|
446
|
+
return { target: point };
|
|
447
|
+
}
|
|
448
|
+
async function moveToTarget(target, ctx, action) {
|
|
449
|
+
const box = await readBoxWithAutoScroll(target, ctx, action);
|
|
450
|
+
const targetPoint = pickClickPoint(box, ctx.rng, ctx.personality.mouse.clickSpread);
|
|
451
|
+
if (action === "click") await maybeMisclickBeat(ctx, box, targetPoint);
|
|
452
|
+
await walkBezierTo(targetPoint, ctx);
|
|
453
|
+
return targetPoint;
|
|
454
|
+
}
|
|
455
|
+
async function walkBezierTo(to, ctx) {
|
|
456
|
+
const startPoint = ctx.getMousePosition();
|
|
457
|
+
const rawPath = core.bezierPath(startPoint, to, ctx.rng, {
|
|
458
|
+
curvature: ctx.personality.mouse.curvature
|
|
459
|
+
});
|
|
460
|
+
const path = core.humanizePath(rawPath, ctx.rng);
|
|
461
|
+
const travelMs = computeTravelTime(path, ctx.personality, ctx.speed, ctx.rng);
|
|
462
|
+
await walkMouseAlongPath(ctx.page, path, travelMs);
|
|
463
|
+
}
|
|
464
|
+
async function resolveTargetPoint(target, ctx, action) {
|
|
465
|
+
if (isPoint(target)) return target;
|
|
466
|
+
return resolveLocatorPoint(target, ctx, action);
|
|
467
|
+
}
|
|
468
|
+
async function resolveTargetPointAndBox(target, ctx, action) {
|
|
469
|
+
if (isPoint(target)) return { point: target, box: null };
|
|
470
|
+
const box = await readBoxWithAutoScroll(target, ctx, action);
|
|
471
|
+
const point = pickClickPoint(box, ctx.rng, ctx.personality.mouse.clickSpread);
|
|
472
|
+
return { point, box };
|
|
473
|
+
}
|
|
474
|
+
async function resolveLocatorPoint(target, ctx, action) {
|
|
475
|
+
const box = await readBoxWithAutoScroll(target, ctx, action);
|
|
476
|
+
return pickClickPoint(box, ctx.rng, ctx.personality.mouse.clickSpread);
|
|
477
|
+
}
|
|
478
|
+
async function readBoxWithAutoScroll(target, ctx, action) {
|
|
479
|
+
const locator = typeof target === "string" ? ctx.page.locator(target) : target;
|
|
480
|
+
let box = await locator.boundingBox();
|
|
481
|
+
if (!box) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
`Cannot ${action}: element not found or has no bounding box (target: ${describeTarget(target)})`
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
const viewport = ctx.page.viewportSize();
|
|
487
|
+
if (viewport && !isBoxCenterInViewport(box, viewport)) {
|
|
488
|
+
if (ctx.speed === "instant") {
|
|
489
|
+
await locator.scrollIntoViewIfNeeded();
|
|
490
|
+
} else {
|
|
491
|
+
await executeScroll(locator, ctx, { block: "center" });
|
|
492
|
+
}
|
|
493
|
+
box = await locator.boundingBox();
|
|
494
|
+
if (!box) {
|
|
495
|
+
throw new Error(
|
|
496
|
+
`Cannot ${action}: element disappeared after scrolling into view (target: ${describeTarget(target)})`
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return box;
|
|
501
|
+
}
|
|
502
|
+
function isBoxCenterInViewport(box, viewport) {
|
|
503
|
+
const cx = box.x + box.width / 2;
|
|
504
|
+
const cy = box.y + box.height / 2;
|
|
505
|
+
return cx >= 0 && cx <= viewport.width && cy >= 0 && cy <= viewport.height;
|
|
506
|
+
}
|
|
507
|
+
async function readScrollY(page) {
|
|
508
|
+
if (typeof page.evaluate !== "function") return 0;
|
|
509
|
+
return page.evaluate(() => window.scrollY);
|
|
510
|
+
}
|
|
511
|
+
var CURVE_VIEWPORT_MARGIN = 20;
|
|
512
|
+
function computeCurveScrollDelta(from, to, viewport, curvature) {
|
|
513
|
+
if (!viewport) return 0;
|
|
514
|
+
const distance = Math.hypot(to.x - from.x, to.y - from.y);
|
|
515
|
+
const perpendicularExtent = distance * curvature;
|
|
516
|
+
const minY = Math.min(from.y, to.y) - perpendicularExtent;
|
|
517
|
+
const maxY = Math.max(from.y, to.y) + perpendicularExtent;
|
|
518
|
+
const topOverflow = -minY + CURVE_VIEWPORT_MARGIN;
|
|
519
|
+
const bottomOverflow = maxY + CURVE_VIEWPORT_MARGIN - viewport.height;
|
|
520
|
+
if (bottomOverflow > 0 && bottomOverflow >= topOverflow) return bottomOverflow;
|
|
521
|
+
if (topOverflow > 0) return -topOverflow;
|
|
522
|
+
return 0;
|
|
523
|
+
}
|
|
524
|
+
function isPoint(target) {
|
|
525
|
+
return typeof target === "object" && target !== null && !("boundingBox" in target) && typeof target.x === "number" && typeof target.y === "number";
|
|
526
|
+
}
|
|
527
|
+
var MISCLICK_OFFSET_MIN = 5;
|
|
528
|
+
var MISCLICK_OFFSET_MAX = 15;
|
|
529
|
+
async function maybeMisclickBeat(ctx, box, targetPoint) {
|
|
530
|
+
if (!ctx.rng.chance(ctx.personality.mouse.misclickProbability)) return;
|
|
531
|
+
if (cursorAlreadyOnTarget(ctx.getMousePosition(), box, targetPoint)) return;
|
|
532
|
+
const viewport = ctx.page.viewportSize();
|
|
533
|
+
const misclickPoint = box ? pickMisclickOutsideBox(box, ctx.rng, viewport) : pickMisclickAroundPoint(targetPoint, ctx.rng, viewport);
|
|
534
|
+
if (misclickPoint === null) return;
|
|
535
|
+
await walkBezierTo(misclickPoint, ctx);
|
|
536
|
+
ctx.setMousePosition(misclickPoint);
|
|
537
|
+
const realizeMs = computeDwellTime(
|
|
538
|
+
ctx.personality.dwell.preClickMs,
|
|
539
|
+
ctx.personality.dwell.preClickJitter,
|
|
540
|
+
ctx.personality,
|
|
541
|
+
ctx.speed,
|
|
542
|
+
ctx.rng
|
|
543
|
+
);
|
|
544
|
+
if (realizeMs > 0) await sleep(realizeMs);
|
|
545
|
+
}
|
|
546
|
+
function cursorAlreadyOnTarget(current, box, targetPoint) {
|
|
547
|
+
if (box) {
|
|
548
|
+
return current.x >= box.x && current.x <= box.x + box.width && current.y >= box.y && current.y <= box.y + box.height;
|
|
549
|
+
}
|
|
550
|
+
const dx = current.x - targetPoint.x;
|
|
551
|
+
const dy = current.y - targetPoint.y;
|
|
552
|
+
return Math.hypot(dx, dy) < MISCLICK_OFFSET_MIN;
|
|
553
|
+
}
|
|
554
|
+
function pickMisclickOutsideBox(box, rng, viewport) {
|
|
555
|
+
const edge = rng.nextInt(0, 4);
|
|
556
|
+
const offset = rng.nextFloat(MISCLICK_OFFSET_MIN, MISCLICK_OFFSET_MAX);
|
|
557
|
+
const along = rng.nextFloat(0.2, 0.8);
|
|
558
|
+
let x;
|
|
559
|
+
let y;
|
|
560
|
+
if (edge === 0) {
|
|
561
|
+
x = box.x + box.width * along;
|
|
562
|
+
y = box.y - offset;
|
|
563
|
+
} else if (edge === 1) {
|
|
564
|
+
x = box.x + box.width + offset;
|
|
565
|
+
y = box.y + box.height * along;
|
|
566
|
+
} else if (edge === 2) {
|
|
567
|
+
x = box.x + box.width * along;
|
|
568
|
+
y = box.y + box.height + offset;
|
|
569
|
+
} else {
|
|
570
|
+
x = box.x - offset;
|
|
571
|
+
y = box.y + box.height * along;
|
|
572
|
+
}
|
|
573
|
+
if (viewport) {
|
|
574
|
+
x = clamp2(x, 0, viewport.width - 1);
|
|
575
|
+
y = clamp2(y, 0, viewport.height - 1);
|
|
576
|
+
}
|
|
577
|
+
const insideBox = x >= box.x && x <= box.x + box.width && y >= box.y && y <= box.y + box.height;
|
|
578
|
+
if (insideBox) return null;
|
|
579
|
+
return { x, y };
|
|
580
|
+
}
|
|
581
|
+
function pickMisclickAroundPoint(target, rng, viewport) {
|
|
582
|
+
const angle = rng.nextFloat(0, Math.PI * 2);
|
|
583
|
+
const distance = rng.nextFloat(MISCLICK_OFFSET_MIN, MISCLICK_OFFSET_MAX);
|
|
584
|
+
let x = target.x + Math.cos(angle) * distance;
|
|
585
|
+
let y = target.y + Math.sin(angle) * distance;
|
|
586
|
+
if (viewport) {
|
|
587
|
+
x = clamp2(x, 0, viewport.width - 1);
|
|
588
|
+
y = clamp2(y, 0, viewport.height - 1);
|
|
589
|
+
}
|
|
590
|
+
if (x === target.x && y === target.y) return null;
|
|
591
|
+
return { x, y };
|
|
592
|
+
}
|
|
593
|
+
function pickClickPoint(box, rng, clickSpread) {
|
|
594
|
+
const cx = box.x + box.width / 2;
|
|
595
|
+
const cy = box.y + box.height / 2;
|
|
596
|
+
const sigmaX = box.width * clickSpread;
|
|
597
|
+
const sigmaY = box.height * clickSpread;
|
|
598
|
+
const x = clamp2(cx + rng.nextGaussian(0, sigmaX), box.x, box.x + box.width);
|
|
599
|
+
const y = clamp2(cy + rng.nextGaussian(0, sigmaY), box.y, box.y + box.height);
|
|
600
|
+
return { x, y };
|
|
601
|
+
}
|
|
602
|
+
function computeTravelTime(path, personality, speed, rng) {
|
|
603
|
+
let distance = 0;
|
|
604
|
+
for (let i = 1; i < path.length; i++) {
|
|
605
|
+
const prev = path[i - 1];
|
|
606
|
+
const curr = path[i];
|
|
607
|
+
if (!prev || !curr) continue;
|
|
608
|
+
distance += Math.hypot(curr.x - prev.x, curr.y - prev.y);
|
|
609
|
+
}
|
|
610
|
+
const baseTime = distance / 1e3 * personality.mouse.travelTimeMs;
|
|
611
|
+
const jitterMag = baseTime * personality.mouse.travelTimeJitter;
|
|
612
|
+
const jitter = rng.nextFloat(-jitterMag, jitterMag);
|
|
613
|
+
const total = (baseTime + jitter) * personality.speed * speedModeFactor(speed);
|
|
614
|
+
return Math.max(0, total);
|
|
615
|
+
}
|
|
616
|
+
function describeTarget(target) {
|
|
617
|
+
if (isPoint(target)) return `point(${target.x}, ${target.y})`;
|
|
618
|
+
return typeof target === "string" ? target : target.toString?.() ?? "locator";
|
|
619
|
+
}
|
|
405
620
|
function clamp2(value, min, max) {
|
|
406
621
|
return value < min ? min : value > max ? max : value;
|
|
407
622
|
}
|
|
623
|
+
async function executeRead(target, ctx, options = {}) {
|
|
624
|
+
let words = 0;
|
|
625
|
+
let locator;
|
|
626
|
+
if (typeof target === "string") {
|
|
627
|
+
locator = ctx.page.locator(target);
|
|
628
|
+
} else if ("words" in target) {
|
|
629
|
+
words = target.words;
|
|
630
|
+
} else if ("text" in target) {
|
|
631
|
+
words = core.countWords(target.text);
|
|
632
|
+
} else {
|
|
633
|
+
locator = target;
|
|
634
|
+
}
|
|
635
|
+
let autoDetectedKind;
|
|
636
|
+
if (locator) {
|
|
637
|
+
if (options.scrollIntoView) {
|
|
638
|
+
await locator.scrollIntoViewIfNeeded();
|
|
639
|
+
}
|
|
640
|
+
const text = await locator.innerText().catch(() => "");
|
|
641
|
+
words = core.countWords(text);
|
|
642
|
+
if (options.kind === void 0) {
|
|
643
|
+
autoDetectedKind = await detectKindFromTag(locator);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const kind = options.kind ?? autoDetectedKind ?? "prose";
|
|
647
|
+
const durationMs = core.computeReadingDwellMs(words, ctx.personality.reading, ctx.rng, {
|
|
648
|
+
kind,
|
|
649
|
+
wpmMultiplier: options.wpmMultiplier,
|
|
650
|
+
personalitySpeed: ctx.personality.speed,
|
|
651
|
+
speedFactor: speedModeFactor(ctx.speed)
|
|
652
|
+
});
|
|
653
|
+
const withMotion = options.withMotion ?? true;
|
|
654
|
+
if (withMotion && locator && durationMs > 0) {
|
|
655
|
+
const box = await locator.boundingBox().catch(() => null);
|
|
656
|
+
if (box) {
|
|
657
|
+
const lineRects = await getLineRects(locator).catch(() => []);
|
|
658
|
+
const path = core.planReadingScan(box, ctx.rng, {
|
|
659
|
+
start: ctx.getMousePosition(),
|
|
660
|
+
lineRects: lineRects.length > 0 ? lineRects : void 0
|
|
661
|
+
});
|
|
662
|
+
await walkMouseAlongPath(ctx.page, path, durationMs);
|
|
663
|
+
const final = path[path.length - 1];
|
|
664
|
+
if (final) ctx.setMousePosition(final);
|
|
665
|
+
return { words, durationMs, kind };
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (durationMs > 0) await sleep(durationMs);
|
|
669
|
+
return { words, durationMs, kind };
|
|
670
|
+
}
|
|
671
|
+
async function getLineRects(locator) {
|
|
672
|
+
const result = await locator.evaluate((el) => {
|
|
673
|
+
const walker = el.ownerDocument.createTreeWalker(el, 4);
|
|
674
|
+
const rects = [];
|
|
675
|
+
let node = walker.nextNode();
|
|
676
|
+
while (node) {
|
|
677
|
+
const text = node.textContent ?? "";
|
|
678
|
+
if (text.trim().length > 0) {
|
|
679
|
+
const range = el.ownerDocument.createRange();
|
|
680
|
+
range.selectNodeContents(node);
|
|
681
|
+
for (const r of Array.from(range.getClientRects())) {
|
|
682
|
+
if (r.width > 0 && r.height > 0) {
|
|
683
|
+
rects.push({ x: r.x, y: r.y, width: r.width, height: r.height });
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
node = walker.nextNode();
|
|
688
|
+
}
|
|
689
|
+
rects.sort((a, b) => a.y - b.y || a.x - b.x);
|
|
690
|
+
const merged = [];
|
|
691
|
+
for (const r of rects) {
|
|
692
|
+
const last = merged[merged.length - 1];
|
|
693
|
+
if (last && Math.abs(last.y - r.y) < 1 && r.x - (last.x + last.width) < 6) {
|
|
694
|
+
const right = Math.max(last.x + last.width, r.x + r.width);
|
|
695
|
+
const bottom = Math.max(last.y + last.height, r.y + r.height);
|
|
696
|
+
last.width = right - last.x;
|
|
697
|
+
last.height = bottom - last.y;
|
|
698
|
+
} else {
|
|
699
|
+
merged.push({ ...r });
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return merged;
|
|
703
|
+
});
|
|
704
|
+
if (!Array.isArray(result)) return [];
|
|
705
|
+
return result.filter(
|
|
706
|
+
(r) => r != null && typeof r === "object" && typeof r.x === "number" && typeof r.y === "number" && typeof r.width === "number" && typeof r.height === "number"
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
async function detectKindFromTag(locator) {
|
|
710
|
+
const tag = await locator.evaluate((el) => el.tagName?.toLowerCase() ?? "").catch(() => "");
|
|
711
|
+
if (tag === "pre" || tag === "code") return "code";
|
|
712
|
+
return void 0;
|
|
713
|
+
}
|
|
714
|
+
var pendingFrameCleanups = /* @__PURE__ */ new Set();
|
|
715
|
+
var exitHandlerInstalled = false;
|
|
716
|
+
function ensureExitHandler() {
|
|
717
|
+
if (exitHandlerInstalled) return;
|
|
718
|
+
exitHandlerInstalled = true;
|
|
719
|
+
process.on("exit", () => {
|
|
720
|
+
for (const dir of pendingFrameCleanups) {
|
|
721
|
+
try {
|
|
722
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
723
|
+
} catch {
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
pendingFrameCleanups.clear();
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
var FFMPEG_PATH = ffmpegStatic__default.default;
|
|
730
|
+
var QUALITY_PRESETS = {
|
|
731
|
+
fast: {
|
|
732
|
+
captureFormat: "jpeg",
|
|
733
|
+
captureJpegQuality: 85,
|
|
734
|
+
captureFps: 24,
|
|
735
|
+
crf: 23,
|
|
736
|
+
preset: "fast"
|
|
737
|
+
},
|
|
738
|
+
standard: {
|
|
739
|
+
captureFormat: "jpeg",
|
|
740
|
+
captureJpegQuality: 90,
|
|
741
|
+
captureFps: 30,
|
|
742
|
+
crf: 20,
|
|
743
|
+
preset: "fast"
|
|
744
|
+
},
|
|
745
|
+
high: {
|
|
746
|
+
captureFormat: "jpeg",
|
|
747
|
+
captureJpegQuality: 95,
|
|
748
|
+
captureFps: 30,
|
|
749
|
+
crf: 18,
|
|
750
|
+
preset: "slow",
|
|
751
|
+
// 'animation' suits screen content (large solid regions, sharp edges)
|
|
752
|
+
// better than 'film' which is tuned for live-action grain.
|
|
753
|
+
tune: "animation"
|
|
754
|
+
},
|
|
755
|
+
lossless: {
|
|
756
|
+
// PNG capture for perceptually lossless source frames. Temp files are
|
|
757
|
+
// 10-20× larger than JPEG; output mp4 still benefits from the extra
|
|
758
|
+
// headroom (no JPEG artifacts to preserve).
|
|
759
|
+
captureFormat: "png",
|
|
760
|
+
captureJpegQuality: 100,
|
|
761
|
+
captureFps: 30,
|
|
762
|
+
crf: 12,
|
|
763
|
+
preset: "veryslow",
|
|
764
|
+
tune: "animation"
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
function getCaptureSettingsForQuality(quality) {
|
|
768
|
+
const preset = QUALITY_PRESETS[quality];
|
|
769
|
+
return {
|
|
770
|
+
format: preset.captureFormat,
|
|
771
|
+
quality: preset.captureJpegQuality,
|
|
772
|
+
fps: preset.captureFps
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
var Recording = class {
|
|
776
|
+
#capture;
|
|
777
|
+
#windowStartMs;
|
|
778
|
+
#windowEndMs;
|
|
779
|
+
#timelineSource;
|
|
780
|
+
// Frames live on disk until `dispose()` is called. Exporters
|
|
781
|
+
// (`toVideo`, `toGif`) are repeatable and interleavable — they read the
|
|
782
|
+
// same frame source, they don't consume it.
|
|
783
|
+
#disposed = false;
|
|
784
|
+
constructor(capture, windowStartMs, windowEndMs, timelineSource) {
|
|
785
|
+
this.#capture = capture;
|
|
786
|
+
this.#windowStartMs = windowStartMs;
|
|
787
|
+
this.#windowEndMs = windowEndMs;
|
|
788
|
+
this.#timelineSource = timelineSource;
|
|
789
|
+
if (capture !== null) {
|
|
790
|
+
pendingFrameCleanups.add(capture.dir);
|
|
791
|
+
ensureExitHandler();
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
/** Wall-clock duration of the recorded window. */
|
|
795
|
+
get durationMs() {
|
|
796
|
+
return this.#windowEndMs - this.#windowStartMs;
|
|
797
|
+
}
|
|
798
|
+
/** True if frames were captured during this recording. */
|
|
799
|
+
get hasVideo() {
|
|
800
|
+
return this.#capture !== null;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* The structured action timeline of this recording — same data that
|
|
804
|
+
* `toTimeline()` writes to disk.
|
|
805
|
+
*/
|
|
806
|
+
get timeline() {
|
|
807
|
+
return {
|
|
808
|
+
version: 1,
|
|
809
|
+
personality: this.#timelineSource.personality,
|
|
810
|
+
seed: this.#timelineSource.seed,
|
|
811
|
+
speed: this.#timelineSource.speed,
|
|
812
|
+
durationMs: this.durationMs,
|
|
813
|
+
events: this.#timelineSource.events
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Assembles the captured frames into a video at `outputPath`. The output
|
|
818
|
+
* format is inferred from the extension — `.mp4` (H.264, re-encoded
|
|
819
|
+
* with the configured quality) or `.webm` (VP9).
|
|
820
|
+
*
|
|
821
|
+
* Repeatable and interleavable with `toGif()` — the frame source is read,
|
|
822
|
+
* not consumed. Frames live until you call `dispose()` (or `await using`
|
|
823
|
+
* goes out of scope, or the process exits and the OS reaps `tmpdir`).
|
|
824
|
+
*
|
|
825
|
+
* @returns the resolved output path.
|
|
826
|
+
*/
|
|
827
|
+
async toVideo(outputPath, options = {}) {
|
|
828
|
+
if (this.#disposed) {
|
|
829
|
+
throw new Error("Recording.toVideo() called after dispose() \u2014 the source frames are gone.");
|
|
830
|
+
}
|
|
831
|
+
if (this.#capture === null) {
|
|
832
|
+
throw new Error(
|
|
833
|
+
"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."
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
const preset = QUALITY_PRESETS[options.quality ?? "high"];
|
|
837
|
+
const crf = options.crf ?? preset.crf;
|
|
838
|
+
const ffmpegPreset = options.preset ?? preset.preset;
|
|
839
|
+
const tune = options.tune ?? preset.tune;
|
|
840
|
+
const { dir, frames, startedAtMs, stoppedAtMs } = this.#capture;
|
|
841
|
+
if (frames.length === 0) {
|
|
842
|
+
throw new Error(
|
|
843
|
+
"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."
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
await promises.mkdir(path.dirname(outputPath), { recursive: true });
|
|
847
|
+
const ext = path.extname(outputPath).toLowerCase();
|
|
848
|
+
if (ext !== ".mp4" && ext !== ".webm") {
|
|
849
|
+
throw new Error(`Unsupported output extension: ${ext || "(none)"}. Use .mp4 or .webm.`);
|
|
850
|
+
}
|
|
851
|
+
const concatPath = `${dir}/concat.txt`;
|
|
852
|
+
const concatBody = buildConcatFile(frames, stoppedAtMs - startedAtMs);
|
|
853
|
+
await promises.writeFile(concatPath, concatBody, "utf8");
|
|
854
|
+
const args = ["-y", "-f", "concat", "-safe", "0", "-i", concatPath, "-vsync", "vfr"];
|
|
855
|
+
if (ext === ".mp4") {
|
|
856
|
+
args.push(
|
|
857
|
+
"-c:v",
|
|
858
|
+
"libx264",
|
|
859
|
+
"-pix_fmt",
|
|
860
|
+
"yuv420p",
|
|
861
|
+
"-crf",
|
|
862
|
+
String(crf),
|
|
863
|
+
"-preset",
|
|
864
|
+
ffmpegPreset
|
|
865
|
+
);
|
|
866
|
+
if (tune) args.push("-tune", tune);
|
|
867
|
+
args.push("-movflags", "+faststart");
|
|
868
|
+
} else {
|
|
869
|
+
args.push(
|
|
870
|
+
"-c:v",
|
|
871
|
+
"libvpx-vp9",
|
|
872
|
+
"-pix_fmt",
|
|
873
|
+
"yuv420p",
|
|
874
|
+
"-crf",
|
|
875
|
+
String(crf),
|
|
876
|
+
"-b:v",
|
|
877
|
+
"0",
|
|
878
|
+
"-deadline",
|
|
879
|
+
ffmpegPreset === "fast" || ffmpegPreset === "veryfast" ? "realtime" : "good"
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
args.push(outputPath);
|
|
883
|
+
await runFfmpeg(args);
|
|
884
|
+
return outputPath;
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Assembles the captured frames into an animated GIF at `outputPath`.
|
|
888
|
+
* Optimized for embedding in READMEs, PRs, Slack, and docs — uses a
|
|
889
|
+
* per-recording palette (`palettegen` + `paletteuse`) with Bayer dithering
|
|
890
|
+
* so gradients stay smooth without exploding the file size.
|
|
891
|
+
*
|
|
892
|
+
* Repeatable and interleavable with `toVideo()` — call them in any order,
|
|
893
|
+
* any number of times. Frames live until you call `dispose()`.
|
|
894
|
+
*
|
|
895
|
+
* @returns the resolved output path.
|
|
896
|
+
*/
|
|
897
|
+
async toGif(outputPath, options = {}) {
|
|
898
|
+
if (this.#disposed) {
|
|
899
|
+
throw new Error("Recording.toGif() called after dispose() \u2014 the source frames are gone.");
|
|
900
|
+
}
|
|
901
|
+
if (this.#capture === null) {
|
|
902
|
+
throw new Error(
|
|
903
|
+
"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."
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
const fps = options.fps ?? 15;
|
|
907
|
+
const width = options.width;
|
|
908
|
+
const { dir, frames, startedAtMs, stoppedAtMs } = this.#capture;
|
|
909
|
+
if (frames.length === 0) {
|
|
910
|
+
throw new Error(
|
|
911
|
+
"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."
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
await promises.mkdir(path.dirname(outputPath), { recursive: true });
|
|
915
|
+
const ext = path.extname(outputPath).toLowerCase();
|
|
916
|
+
if (ext !== ".gif") {
|
|
917
|
+
throw new Error(`Unsupported output extension: ${ext || "(none)"}. Use .gif.`);
|
|
918
|
+
}
|
|
919
|
+
const concatPath = `${dir}/concat.txt`;
|
|
920
|
+
const concatBody = buildConcatFile(frames, stoppedAtMs - startedAtMs);
|
|
921
|
+
await promises.writeFile(concatPath, concatBody, "utf8");
|
|
922
|
+
const filterSteps = [`fps=${fps}`];
|
|
923
|
+
if (width !== void 0) {
|
|
924
|
+
filterSteps.push(`scale=${width}:-1:flags=lanczos`);
|
|
925
|
+
}
|
|
926
|
+
const preFilter = filterSteps.join(",");
|
|
927
|
+
const filterComplex = `${preFilter},split [a][b]; [a] palettegen=stats_mode=diff [p]; [b][p] paletteuse=dither=bayer:bayer_scale=5`;
|
|
928
|
+
const args = [
|
|
929
|
+
"-y",
|
|
930
|
+
"-f",
|
|
931
|
+
"concat",
|
|
932
|
+
"-safe",
|
|
933
|
+
"0",
|
|
934
|
+
"-i",
|
|
935
|
+
concatPath,
|
|
936
|
+
"-filter_complex",
|
|
937
|
+
filterComplex,
|
|
938
|
+
"-loop",
|
|
939
|
+
"0",
|
|
940
|
+
outputPath
|
|
941
|
+
];
|
|
942
|
+
await runFfmpeg(args);
|
|
943
|
+
return outputPath;
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Writes the structured action timeline to `outputPath` as JSON.
|
|
947
|
+
* Independent of `toVideo()` / `toGif()` — call before, after, in between,
|
|
948
|
+
* or instead. Safe to call multiple times. Unaffected by `dispose()`
|
|
949
|
+
* (the timeline lives in memory, not in the captured-frames temp dir).
|
|
950
|
+
*
|
|
951
|
+
* @returns the resolved output path.
|
|
952
|
+
*/
|
|
953
|
+
async toTimeline(outputPath) {
|
|
954
|
+
await promises.mkdir(path.dirname(outputPath), { recursive: true });
|
|
955
|
+
await promises.writeFile(outputPath, `${JSON.stringify(this.timeline, null, 2)}
|
|
956
|
+
`, "utf8");
|
|
957
|
+
return outputPath;
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Releases the captured-frames temp directory. After this call, `toVideo()`
|
|
961
|
+
* and `toGif()` throw — but `toTimeline()` and the in-memory `timeline`
|
|
962
|
+
* still work because those don't depend on the frames.
|
|
963
|
+
*
|
|
964
|
+
* **Optional.** A process-exit handler also sweeps any un-disposed frame
|
|
965
|
+
* dirs, so casual scripts can skip this entirely. Call it explicitly when
|
|
966
|
+
* you want to release frames proactively (long-running services, batch
|
|
967
|
+
* jobs, or anywhere you want predictable disk usage).
|
|
968
|
+
*
|
|
969
|
+
* Idempotent. Safe to call on a Recording that never had a capture
|
|
970
|
+
* (timeline-only mode) — no-op there.
|
|
971
|
+
*
|
|
972
|
+
* Also wired to `Symbol.asyncDispose`, so the explicit-resource-management
|
|
973
|
+
* `await using` syntax (TypeScript ≥ 5.2 / Node ≥ 20.4) works:
|
|
974
|
+
*
|
|
975
|
+
* ```ts
|
|
976
|
+
* await using rec = await human.record(fn);
|
|
977
|
+
* await rec.toVideo('demo.mp4');
|
|
978
|
+
* await rec.toGif('demo.gif');
|
|
979
|
+
* // frames cleaned up automatically when `rec` goes out of scope
|
|
980
|
+
* ```
|
|
981
|
+
*/
|
|
982
|
+
async dispose() {
|
|
983
|
+
if (this.#disposed) return;
|
|
984
|
+
if (this.#capture !== null) {
|
|
985
|
+
await this.#capture.cleanup();
|
|
986
|
+
pendingFrameCleanups.delete(this.#capture.dir);
|
|
987
|
+
}
|
|
988
|
+
this.#disposed = true;
|
|
989
|
+
}
|
|
990
|
+
async [Symbol.asyncDispose]() {
|
|
991
|
+
await this.dispose();
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
function buildConcatFile(frames, totalMs) {
|
|
995
|
+
const lines = [];
|
|
996
|
+
for (let i = 0; i < frames.length; i++) {
|
|
997
|
+
const frame = frames[i];
|
|
998
|
+
const next = frames[i + 1];
|
|
999
|
+
const nextTMs = next ? next.tMs : totalMs;
|
|
1000
|
+
const durationS = Math.max(1e-3, (nextTMs - frame.tMs) / 1e3);
|
|
1001
|
+
lines.push(`file '${frame.path.replaceAll("'", "'\\''")}'`);
|
|
1002
|
+
lines.push(`duration ${durationS.toFixed(6)}`);
|
|
1003
|
+
}
|
|
1004
|
+
const last = frames[frames.length - 1];
|
|
1005
|
+
if (last) {
|
|
1006
|
+
lines.push(`file '${last.path.replaceAll("'", "'\\''")}'`);
|
|
1007
|
+
}
|
|
1008
|
+
return `${lines.join("\n")}
|
|
1009
|
+
`;
|
|
1010
|
+
}
|
|
1011
|
+
function runFfmpeg(args) {
|
|
1012
|
+
if (!FFMPEG_PATH) {
|
|
1013
|
+
return Promise.reject(
|
|
1014
|
+
new Error(
|
|
1015
|
+
"ffmpeg-static did not bundle a binary for this platform. Install system ffmpeg and set FFMPEG_PATH, or run on a supported platform."
|
|
1016
|
+
)
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
return new Promise((resolve, reject) => {
|
|
1020
|
+
const proc = child_process.spawn(FFMPEG_PATH, [...args]);
|
|
1021
|
+
let stderr = "";
|
|
1022
|
+
proc.stderr?.on("data", (chunk) => {
|
|
1023
|
+
stderr += chunk.toString();
|
|
1024
|
+
});
|
|
1025
|
+
proc.on("error", reject);
|
|
1026
|
+
proc.on("close", (code) => {
|
|
1027
|
+
if (code === 0) resolve();
|
|
1028
|
+
else reject(new Error(`ffmpeg exited with code ${code}
|
|
1029
|
+
${stderr.trim()}`));
|
|
1030
|
+
});
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
async function startCapture(page, options = {}) {
|
|
1034
|
+
const format = options.format ?? "jpeg";
|
|
1035
|
+
const quality = options.quality ?? 95;
|
|
1036
|
+
const fps = Math.max(1, Math.min(60, options.fps ?? 30));
|
|
1037
|
+
const intervalMs = 1e3 / fps;
|
|
1038
|
+
const dir = await promises.mkdtemp(path.join(os.tmpdir(), "humanjs-capture-"));
|
|
1039
|
+
const frames = [];
|
|
1040
|
+
const ext = format === "png" ? "png" : "jpg";
|
|
1041
|
+
let stopped = false;
|
|
1042
|
+
let frameIndex = 0;
|
|
1043
|
+
const writes = [];
|
|
1044
|
+
const startedAtMs = Date.now();
|
|
1045
|
+
const captureLoop = async () => {
|
|
1046
|
+
while (!stopped) {
|
|
1047
|
+
const loopStart = Date.now();
|
|
1048
|
+
try {
|
|
1049
|
+
const buf = await page.screenshot({
|
|
1050
|
+
type: format,
|
|
1051
|
+
quality: format === "jpeg" ? quality : void 0
|
|
1052
|
+
});
|
|
1053
|
+
if (stopped) return;
|
|
1054
|
+
const idx = frameIndex++;
|
|
1055
|
+
const path$1 = path.join(dir, `frame_${String(idx).padStart(6, "0")}.${ext}`);
|
|
1056
|
+
const tMs = loopStart - startedAtMs;
|
|
1057
|
+
writes.push(
|
|
1058
|
+
promises.writeFile(path$1, buf).then(
|
|
1059
|
+
() => {
|
|
1060
|
+
frames.push({ path: path$1, tMs });
|
|
1061
|
+
},
|
|
1062
|
+
(err) => {
|
|
1063
|
+
console.warn(`humanjs capture: write failed for frame ${idx}:`, err);
|
|
1064
|
+
}
|
|
1065
|
+
)
|
|
1066
|
+
);
|
|
1067
|
+
} catch (err) {
|
|
1068
|
+
if (stopped) return;
|
|
1069
|
+
console.warn("humanjs capture: screenshot failed, stopping loop:", err);
|
|
1070
|
+
stopped = true;
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
const elapsed = Date.now() - loopStart;
|
|
1074
|
+
const wait = intervalMs - elapsed;
|
|
1075
|
+
if (wait > 0) await core.sleep(wait);
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
const loopPromise = captureLoop();
|
|
1079
|
+
const finish = async () => {
|
|
1080
|
+
stopped = true;
|
|
1081
|
+
await loopPromise;
|
|
1082
|
+
await Promise.allSettled(writes);
|
|
1083
|
+
};
|
|
1084
|
+
return {
|
|
1085
|
+
async stop() {
|
|
1086
|
+
await finish();
|
|
1087
|
+
const stoppedAtMs = Date.now();
|
|
1088
|
+
return {
|
|
1089
|
+
dir,
|
|
1090
|
+
frames: [...frames].sort((a, b) => a.tMs - b.tMs),
|
|
1091
|
+
startedAtMs,
|
|
1092
|
+
stoppedAtMs,
|
|
1093
|
+
format,
|
|
1094
|
+
fps,
|
|
1095
|
+
cleanup: () => promises.rm(dir, { recursive: true, force: true }).then(() => void 0)
|
|
1096
|
+
};
|
|
1097
|
+
},
|
|
1098
|
+
async abort() {
|
|
1099
|
+
await finish();
|
|
1100
|
+
await promises.rm(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
408
1104
|
|
|
409
1105
|
// src/mouse-helper/index.ts
|
|
410
1106
|
var CURSOR_PATH = "M 0 0 L 16 6 L 8 9.5 L 5 19 Z";
|
|
1107
|
+
var INSTALLED_FLAG = /* @__PURE__ */ Symbol.for("@humanjs/playwright:mouse-helper:installed");
|
|
411
1108
|
async function installMouseHelper(target, options = {}) {
|
|
1109
|
+
const tagged = target;
|
|
1110
|
+
if (tagged[INSTALLED_FLAG]) return;
|
|
1111
|
+
tagged[INSTALLED_FLAG] = true;
|
|
412
1112
|
const config = {
|
|
413
1113
|
color: options.color ?? "#f5a55c",
|
|
414
1114
|
stroke: "#020203",
|
|
@@ -418,16 +1118,22 @@ async function installMouseHelper(target, options = {}) {
|
|
|
418
1118
|
path: CURSOR_PATH
|
|
419
1119
|
};
|
|
420
1120
|
await target.addInitScript(installScript, config);
|
|
1121
|
+
const attachPageHooks = (page) => {
|
|
1122
|
+
page.on("domcontentloaded", () => {
|
|
1123
|
+
page.evaluate(installScript, config).catch(() => void 0);
|
|
1124
|
+
});
|
|
1125
|
+
};
|
|
421
1126
|
const pages = "pages" in target ? target.pages() : [target];
|
|
1127
|
+
for (const page of pages) attachPageHooks(page);
|
|
1128
|
+
if ("on" in target && "newPage" in target) {
|
|
1129
|
+
target.on("page", attachPageHooks);
|
|
1130
|
+
}
|
|
422
1131
|
await Promise.all(
|
|
423
1132
|
pages.map((page) => page.evaluate(installScript, config).catch(() => void 0))
|
|
424
1133
|
);
|
|
425
1134
|
}
|
|
426
1135
|
function installScript(config) {
|
|
427
|
-
|
|
428
|
-
const w = window;
|
|
429
|
-
if (w[guardKey]) return;
|
|
430
|
-
w[guardKey] = true;
|
|
1136
|
+
if (document.querySelector("[data-humanjs-cursor]")) return;
|
|
431
1137
|
const attach = () => {
|
|
432
1138
|
const cursor = document.createElement("div");
|
|
433
1139
|
cursor.setAttribute("aria-hidden", "true");
|
|
@@ -518,6 +1224,9 @@ async function createHuman(page, options = {}) {
|
|
|
518
1224
|
for (const plugin of plugins) {
|
|
519
1225
|
await plugin.install?.(context);
|
|
520
1226
|
}
|
|
1227
|
+
let hasRecorded = false;
|
|
1228
|
+
let activeRecordingEvents = null;
|
|
1229
|
+
let activeRecordingStartMs = 0;
|
|
521
1230
|
async function performAction(action, actionFn) {
|
|
522
1231
|
for (const plugin of plugins) {
|
|
523
1232
|
await plugin.beforeAction?.(action);
|
|
@@ -525,15 +1234,30 @@ async function createHuman(page, options = {}) {
|
|
|
525
1234
|
const startedAt = Date.now();
|
|
526
1235
|
try {
|
|
527
1236
|
const value = await actionFn();
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
1237
|
+
const durationMs = Date.now() - startedAt;
|
|
1238
|
+
const result = { type: action.type, durationMs };
|
|
1239
|
+
if (activeRecordingEvents !== null && action.type !== "record") {
|
|
1240
|
+
activeRecordingEvents.push({
|
|
1241
|
+
type: action.type,
|
|
1242
|
+
params: action.params ?? {},
|
|
1243
|
+
tMs: startedAt - activeRecordingStartMs,
|
|
1244
|
+
durationMs
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
532
1247
|
for (const plugin of plugins) {
|
|
533
1248
|
await plugin.afterAction?.(action, result);
|
|
534
1249
|
}
|
|
535
1250
|
return value;
|
|
536
1251
|
} catch (error) {
|
|
1252
|
+
if (activeRecordingEvents !== null && action.type !== "record") {
|
|
1253
|
+
activeRecordingEvents.push({
|
|
1254
|
+
type: action.type,
|
|
1255
|
+
params: action.params ?? {},
|
|
1256
|
+
tMs: startedAt - activeRecordingStartMs,
|
|
1257
|
+
durationMs: Date.now() - startedAt,
|
|
1258
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
537
1261
|
for (const plugin of plugins) {
|
|
538
1262
|
await plugin.onError?.(action, error);
|
|
539
1263
|
}
|
|
@@ -541,6 +1265,23 @@ async function createHuman(page, options = {}) {
|
|
|
541
1265
|
}
|
|
542
1266
|
}
|
|
543
1267
|
let lastMousePosition = options.initialMousePosition ?? { x: 0, y: 0 };
|
|
1268
|
+
const mouseCtx = () => ({
|
|
1269
|
+
page,
|
|
1270
|
+
personality,
|
|
1271
|
+
rng,
|
|
1272
|
+
speed,
|
|
1273
|
+
getMousePosition: () => lastMousePosition,
|
|
1274
|
+
setMousePosition: (point) => {
|
|
1275
|
+
lastMousePosition = point;
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
const describeMouseTarget = (target) => {
|
|
1279
|
+
if (typeof target === "string") return target;
|
|
1280
|
+
if ("x" in target && "y" in target && typeof target.x === "number") {
|
|
1281
|
+
return `point(${target.x}, ${target.y})`;
|
|
1282
|
+
}
|
|
1283
|
+
return target.toString?.() ?? "locator";
|
|
1284
|
+
};
|
|
544
1285
|
return {
|
|
545
1286
|
personality,
|
|
546
1287
|
speed,
|
|
@@ -550,29 +1291,65 @@ async function createHuman(page, options = {}) {
|
|
|
550
1291
|
});
|
|
551
1292
|
},
|
|
552
1293
|
async click(target) {
|
|
553
|
-
const description =
|
|
1294
|
+
const description = describeMouseTarget(target);
|
|
554
1295
|
await performAction({ type: "click", params: { target: description } }, async () => {
|
|
555
|
-
await executeClick(target,
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
1296
|
+
await executeClick(target, mouseCtx());
|
|
1297
|
+
});
|
|
1298
|
+
},
|
|
1299
|
+
async rightClick(target) {
|
|
1300
|
+
const description = describeMouseTarget(target);
|
|
1301
|
+
await performAction({ type: "rightClick", params: { target: description } }, async () => {
|
|
1302
|
+
await executeClick(target, mouseCtx(), { button: "right" });
|
|
1303
|
+
});
|
|
1304
|
+
},
|
|
1305
|
+
async hover(target) {
|
|
1306
|
+
const description = describeMouseTarget(target);
|
|
1307
|
+
await performAction({ type: "hover", params: { target: description } }, async () => {
|
|
1308
|
+
await executeHover(target, mouseCtx());
|
|
1309
|
+
});
|
|
1310
|
+
},
|
|
1311
|
+
async move(target) {
|
|
1312
|
+
const description = describeMouseTarget(target);
|
|
1313
|
+
await performAction({ type: "move", params: { target: description } }, async () => {
|
|
1314
|
+
await executeMove(target, mouseCtx());
|
|
1315
|
+
});
|
|
1316
|
+
},
|
|
1317
|
+
async drag(from, to) {
|
|
1318
|
+
const fromDesc = describeMouseTarget(from);
|
|
1319
|
+
const toDesc = describeMouseTarget(to);
|
|
1320
|
+
await performAction({ type: "drag", params: { from: fromDesc, to: toDesc } }, async () => {
|
|
1321
|
+
await executeDrag(from, to, mouseCtx());
|
|
565
1322
|
});
|
|
566
1323
|
},
|
|
567
1324
|
async type(target, value) {
|
|
568
|
-
const description =
|
|
1325
|
+
const description = describeMouseTarget(target);
|
|
569
1326
|
await performAction(
|
|
570
1327
|
{ type: "type", params: { target: description, length: value.length } },
|
|
571
1328
|
async () => {
|
|
1329
|
+
if (speed !== "instant" && value.length > 0) {
|
|
1330
|
+
await executeClick(target, mouseCtx());
|
|
1331
|
+
}
|
|
572
1332
|
await executeType(target, value, { page, personality, rng, speed });
|
|
573
1333
|
}
|
|
574
1334
|
);
|
|
575
1335
|
},
|
|
1336
|
+
async paste(target, value) {
|
|
1337
|
+
const description = describeMouseTarget(target);
|
|
1338
|
+
await performAction(
|
|
1339
|
+
{ type: "paste", params: { target: description, length: value.length } },
|
|
1340
|
+
async () => {
|
|
1341
|
+
if (speed !== "instant" && value.length > 0) {
|
|
1342
|
+
await executeClick(target, mouseCtx());
|
|
1343
|
+
}
|
|
1344
|
+
await executePaste(target, value, { page, personality, rng, speed });
|
|
1345
|
+
}
|
|
1346
|
+
);
|
|
1347
|
+
},
|
|
1348
|
+
async press(key) {
|
|
1349
|
+
await performAction({ type: "press", params: { key } }, async () => {
|
|
1350
|
+
await executePress(key, { page, personality, rng, speed });
|
|
1351
|
+
});
|
|
1352
|
+
},
|
|
576
1353
|
async read(target, options2) {
|
|
577
1354
|
const description = describeReadTarget(target);
|
|
578
1355
|
return performAction(
|
|
@@ -611,6 +1388,51 @@ async function createHuman(page, options = {}) {
|
|
|
611
1388
|
},
|
|
612
1389
|
() => executeScroll(target, { page, personality, rng, speed }, options2)
|
|
613
1390
|
);
|
|
1391
|
+
},
|
|
1392
|
+
async sleep(ms) {
|
|
1393
|
+
await performAction({ type: "sleep", params: { ms } }, () => core.sleep(ms));
|
|
1394
|
+
},
|
|
1395
|
+
async record(optionsOrFn, maybeFn) {
|
|
1396
|
+
const [recordOptions, fn] = typeof optionsOrFn === "function" ? [{}, optionsOrFn] : [optionsOrFn, maybeFn];
|
|
1397
|
+
if (hasRecorded) {
|
|
1398
|
+
throw new Error(
|
|
1399
|
+
"human.record() can only be called once per session. Create a new browser context (and a new human session) to record a separate clip."
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
hasRecorded = true;
|
|
1403
|
+
const captureEnabled = recordOptions.video !== false;
|
|
1404
|
+
const captureQuality = recordOptions.quality ?? "high";
|
|
1405
|
+
let captureSession = null;
|
|
1406
|
+
if (captureEnabled) {
|
|
1407
|
+
const { format, quality, fps } = getCaptureSettingsForQuality(captureQuality);
|
|
1408
|
+
captureSession = await startCapture(page, { format, quality, fps });
|
|
1409
|
+
}
|
|
1410
|
+
const events = [];
|
|
1411
|
+
const windowStartMs = Date.now();
|
|
1412
|
+
activeRecordingEvents = events;
|
|
1413
|
+
activeRecordingStartMs = windowStartMs;
|
|
1414
|
+
let windowEndMs = windowStartMs;
|
|
1415
|
+
try {
|
|
1416
|
+
await performAction({ type: "record", params: {} }, async () => {
|
|
1417
|
+
try {
|
|
1418
|
+
await fn();
|
|
1419
|
+
} finally {
|
|
1420
|
+
windowEndMs = Date.now();
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
} catch (error) {
|
|
1424
|
+
if (captureSession) await captureSession.abort();
|
|
1425
|
+
throw error;
|
|
1426
|
+
} finally {
|
|
1427
|
+
activeRecordingEvents = null;
|
|
1428
|
+
}
|
|
1429
|
+
const captureResult = captureSession ? await captureSession.stop() : null;
|
|
1430
|
+
return new Recording(captureResult, windowStartMs, windowEndMs, {
|
|
1431
|
+
personality: personality.name,
|
|
1432
|
+
seed: options.seed === void 0 ? null : String(options.seed),
|
|
1433
|
+
speed,
|
|
1434
|
+
events
|
|
1435
|
+
});
|
|
614
1436
|
}
|
|
615
1437
|
};
|
|
616
1438
|
}
|
|
@@ -690,6 +1512,23 @@ Object.defineProperty(exports, "resolvePersonality", {
|
|
|
690
1512
|
enumerable: true,
|
|
691
1513
|
get: function () { return core.resolvePersonality; }
|
|
692
1514
|
});
|
|
1515
|
+
Object.defineProperty(exports, "sleep", {
|
|
1516
|
+
enumerable: true,
|
|
1517
|
+
get: function () { return core.sleep; }
|
|
1518
|
+
});
|
|
1519
|
+
Object.defineProperty(exports, "chromium", {
|
|
1520
|
+
enumerable: true,
|
|
1521
|
+
get: function () { return playwright.chromium; }
|
|
1522
|
+
});
|
|
1523
|
+
Object.defineProperty(exports, "firefox", {
|
|
1524
|
+
enumerable: true,
|
|
1525
|
+
get: function () { return playwright.firefox; }
|
|
1526
|
+
});
|
|
1527
|
+
Object.defineProperty(exports, "webkit", {
|
|
1528
|
+
enumerable: true,
|
|
1529
|
+
get: function () { return playwright.webkit; }
|
|
1530
|
+
});
|
|
1531
|
+
exports.Recording = Recording;
|
|
693
1532
|
exports.createHuman = createHuman;
|
|
694
1533
|
exports.installMouseHelper = installMouseHelper;
|
|
695
1534
|
//# sourceMappingURL=index.cjs.map
|