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