@humanjs/playwright 0.1.0 → 0.2.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 +133 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -3
- package/dist/index.d.ts +21 -3
- package/dist/index.js +119 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -3,6 +3,107 @@
|
|
|
3
3
|
var core = require('@humanjs/core');
|
|
4
4
|
|
|
5
5
|
// src/index.ts
|
|
6
|
+
async function executeClick(target, ctx) {
|
|
7
|
+
const locator = typeof target === "string" ? ctx.page.locator(target) : target;
|
|
8
|
+
if (ctx.speed === "instant") {
|
|
9
|
+
const box2 = await locator.boundingBox();
|
|
10
|
+
await locator.click();
|
|
11
|
+
const center = box2 ? { x: box2.x + box2.width / 2, y: box2.y + box2.height / 2 } : ctx.getMousePosition();
|
|
12
|
+
ctx.setMousePosition(center);
|
|
13
|
+
return { target: center };
|
|
14
|
+
}
|
|
15
|
+
const box = await locator.boundingBox();
|
|
16
|
+
if (!box) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Cannot click: element not found or has no bounding box (target: ${describeTarget(target)})`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
const targetPoint = pickClickPoint(box, ctx.rng);
|
|
22
|
+
const startPoint = ctx.getMousePosition();
|
|
23
|
+
const rawPath = core.bezierPath(startPoint, targetPoint, ctx.rng, {
|
|
24
|
+
curvature: ctx.personality.mouse.curvature
|
|
25
|
+
});
|
|
26
|
+
const path = core.humanizePath(rawPath, ctx.rng);
|
|
27
|
+
await walkMouseAlongPath(ctx.page, path, ctx.personality, ctx.rng, ctx.speed);
|
|
28
|
+
const preClickMs = computeDwellTime(
|
|
29
|
+
ctx.personality.dwell.preClickMs,
|
|
30
|
+
ctx.personality.dwell.preClickJitter,
|
|
31
|
+
ctx.personality,
|
|
32
|
+
ctx.speed,
|
|
33
|
+
ctx.rng
|
|
34
|
+
);
|
|
35
|
+
if (preClickMs > 0) await sleep(preClickMs);
|
|
36
|
+
ctx.setMousePosition(targetPoint);
|
|
37
|
+
await ctx.page.mouse.click(targetPoint.x, targetPoint.y);
|
|
38
|
+
const postActionMs = computeDwellTime(
|
|
39
|
+
ctx.personality.dwell.postActionMs,
|
|
40
|
+
ctx.personality.dwell.postActionJitter,
|
|
41
|
+
ctx.personality,
|
|
42
|
+
ctx.speed,
|
|
43
|
+
ctx.rng
|
|
44
|
+
);
|
|
45
|
+
if (postActionMs > 0) await sleep(postActionMs);
|
|
46
|
+
return { target: targetPoint };
|
|
47
|
+
}
|
|
48
|
+
function pickClickPoint(box, rng) {
|
|
49
|
+
const cx = box.x + box.width / 2;
|
|
50
|
+
const cy = box.y + box.height / 2;
|
|
51
|
+
const x = clamp(cx + rng.nextGaussian(0, box.width / 8), box.x, box.x + box.width);
|
|
52
|
+
const y = clamp(cy + rng.nextGaussian(0, box.height / 8), box.y, box.y + box.height);
|
|
53
|
+
return { x, y };
|
|
54
|
+
}
|
|
55
|
+
async function walkMouseAlongPath(page, path, personality, rng, speed) {
|
|
56
|
+
if (path.length === 0) return;
|
|
57
|
+
const totalTimeMs = computeTravelTime(path, personality, speed, rng);
|
|
58
|
+
const stepDelayMs = path.length > 1 ? totalTimeMs / (path.length - 1) : 0;
|
|
59
|
+
for (let i = 0; i < path.length; i++) {
|
|
60
|
+
const point = path[i];
|
|
61
|
+
if (!point) continue;
|
|
62
|
+
await page.mouse.move(point.x, point.y);
|
|
63
|
+
if (i < path.length - 1 && stepDelayMs > 0) {
|
|
64
|
+
await sleep(stepDelayMs);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function computeTravelTime(path, personality, speed, rng) {
|
|
69
|
+
let distance = 0;
|
|
70
|
+
for (let i = 1; i < path.length; i++) {
|
|
71
|
+
const prev = path[i - 1];
|
|
72
|
+
const curr = path[i];
|
|
73
|
+
if (!prev || !curr) continue;
|
|
74
|
+
distance += Math.hypot(curr.x - prev.x, curr.y - prev.y);
|
|
75
|
+
}
|
|
76
|
+
const baseTime = distance / 1e3 * personality.mouse.travelTimeMs;
|
|
77
|
+
const jitterMag = baseTime * personality.mouse.travelTimeJitter;
|
|
78
|
+
const jitter = rng.nextFloat(-jitterMag, jitterMag);
|
|
79
|
+
const total = (baseTime + jitter) * personality.speed * speedModeFactor(speed);
|
|
80
|
+
return Math.max(0, total);
|
|
81
|
+
}
|
|
82
|
+
function computeDwellTime(meanMs, jitter, personality, speed, rng) {
|
|
83
|
+
if (meanMs <= 0) return 0;
|
|
84
|
+
const jitterMag = meanMs * jitter;
|
|
85
|
+
const offset = rng.nextFloat(-jitterMag, jitterMag);
|
|
86
|
+
return Math.max(0, (meanMs + offset) * personality.speed * speedModeFactor(speed));
|
|
87
|
+
}
|
|
88
|
+
function speedModeFactor(speed) {
|
|
89
|
+
switch (speed) {
|
|
90
|
+
case "fast":
|
|
91
|
+
return 0.5;
|
|
92
|
+
case "instant":
|
|
93
|
+
return 0;
|
|
94
|
+
default:
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function describeTarget(target) {
|
|
99
|
+
return typeof target === "string" ? target : target.toString?.() ?? "locator";
|
|
100
|
+
}
|
|
101
|
+
function clamp(value, min, max) {
|
|
102
|
+
return value < min ? min : value > max ? max : value;
|
|
103
|
+
}
|
|
104
|
+
function sleep(ms) {
|
|
105
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
106
|
+
}
|
|
6
107
|
async function createHuman(page, options = {}) {
|
|
7
108
|
const personality = core.resolvePersonality(options.personality ?? "careful");
|
|
8
109
|
const rng = core.createRng(options.seed);
|
|
@@ -34,6 +135,7 @@ async function createHuman(page, options = {}) {
|
|
|
34
135
|
throw error;
|
|
35
136
|
}
|
|
36
137
|
}
|
|
138
|
+
let lastMousePosition = options.initialMousePosition ?? { x: 0, y: 0 };
|
|
37
139
|
return {
|
|
38
140
|
personality,
|
|
39
141
|
speed,
|
|
@@ -41,10 +143,37 @@ async function createHuman(page, options = {}) {
|
|
|
41
143
|
await performAction({ type: "goto", params: { url } }, async () => {
|
|
42
144
|
await page.goto(url);
|
|
43
145
|
});
|
|
146
|
+
},
|
|
147
|
+
async click(target) {
|
|
148
|
+
const description = typeof target === "string" ? target : target.toString?.() ?? "locator";
|
|
149
|
+
await performAction({ type: "click", params: { target: description } }, async () => {
|
|
150
|
+
await executeClick(target, {
|
|
151
|
+
page,
|
|
152
|
+
personality,
|
|
153
|
+
rng,
|
|
154
|
+
speed,
|
|
155
|
+
getMousePosition: () => lastMousePosition,
|
|
156
|
+
setMousePosition: (point) => {
|
|
157
|
+
lastMousePosition = point;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
44
161
|
}
|
|
45
162
|
};
|
|
46
163
|
}
|
|
47
164
|
|
|
165
|
+
Object.defineProperty(exports, "applyMicroJitter", {
|
|
166
|
+
enumerable: true,
|
|
167
|
+
get: function () { return core.applyMicroJitter; }
|
|
168
|
+
});
|
|
169
|
+
Object.defineProperty(exports, "applyVelocityProfile", {
|
|
170
|
+
enumerable: true,
|
|
171
|
+
get: function () { return core.applyVelocityProfile; }
|
|
172
|
+
});
|
|
173
|
+
Object.defineProperty(exports, "bezierPath", {
|
|
174
|
+
enumerable: true,
|
|
175
|
+
get: function () { return core.bezierPath; }
|
|
176
|
+
});
|
|
48
177
|
Object.defineProperty(exports, "blend", {
|
|
49
178
|
enumerable: true,
|
|
50
179
|
get: function () { return core.blend; }
|
|
@@ -65,6 +194,10 @@ Object.defineProperty(exports, "fast", {
|
|
|
65
194
|
enumerable: true,
|
|
66
195
|
get: function () { return core.fast; }
|
|
67
196
|
});
|
|
197
|
+
Object.defineProperty(exports, "humanizePath", {
|
|
198
|
+
enumerable: true,
|
|
199
|
+
get: function () { return core.humanizePath; }
|
|
200
|
+
});
|
|
68
201
|
Object.defineProperty(exports, "precise", {
|
|
69
202
|
enumerable: true,
|
|
70
203
|
get: function () { return core.precise; }
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["resolvePersonality","createRng"],"mappings":";;;;;AA4FA,eAAsB,WAAA,CAAY,IAAA,EAAY,OAAA,GAA8B,EAAC,EAAmB;AAC9F,EAAA,MAAM,WAAA,GAAcA,uBAAA,CAAmB,OAAA,CAAQ,WAAA,IAAe,SAAS,CAAA;AACvE,EAAA,MAAM,GAAA,GAAMC,cAAA,CAAU,OAAA,CAAQ,IAAI,CAAA;AAClC,EAAA,MAAM,KAAA,GAAQ,QAAQ,KAAA,IAAS,OAAA;AAC/B,EAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,IAAW,EAAC;AAEpC,EAAA,MAAM,OAAA,GAAyB,EAAE,WAAA,EAAa,GAAA,EAAI;AAClD,EAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,IAAA,MAAM,MAAA,CAAO,UAAU,OAAO,CAAA;AAAA,EAChC;AAEA,EAAA,eAAe,aAAA,CAAiB,QAAqB,QAAA,EAAwC;AAC3F,IAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,MAAA,MAAM,MAAA,CAAO,eAAe,MAAM,CAAA;AAAA,IACpC;AACA,IAAA,MAAM,SAAA,GAAY,KAAK,GAAA,EAAI;AAC3B,IAAA,IAAI;AACF,MAAA,MAAM,KAAA,GAAQ,MAAM,QAAA,EAAS;AAC7B,MAAA,MAAM,MAAA,GAAuB;AAAA,QAC3B,MAAM,MAAA,CAAO,IAAA;AAAA,QACb,UAAA,EAAY,IAAA,CAAK,GAAA,EAAI,GAAI;AAAA,OAC3B;AACA,MAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,QAAA,MAAM,MAAA,CAAO,WAAA,GAAc,MAAA,EAAQ,MAAM,CAAA;AAAA,MAC3C;AACA,MAAA,OAAO,KAAA;AAAA,IACT,SAAS,KAAA,EAAO;AACd,MAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,QAAA,MAAM,MAAA,CAAO,OAAA,GAAU,MAAA,EAAQ,KAAK,CAAA;AAAA,MACtC;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,WAAA;AAAA,IACA,KAAA;AAAA,IACA,MAAM,KAAK,GAAA,EAAK;AACd,MAAA,MAAM,aAAA,CAAc,EAAE,IAAA,EAAM,MAAA,EAAQ,QAAQ,EAAE,GAAA,EAAI,EAAE,EAAG,YAAY;AACjE,QAAA,MAAM,IAAA,CAAK,KAAK,GAAG,CAAA;AAAA,MACrB,CAAC,CAAA;AAAA,IACH;AAAA,GACF;AACF","file":"index.cjs","sourcesContent":["import {\n type ActionResult,\n createRng,\n type HumanAction,\n type HumanPlugin,\n type Personality,\n type PersonalityConfig,\n type PluginContext,\n resolvePersonality,\n} from '@humanjs/core';\nimport type { Page } from 'playwright';\n\nexport type {\n ActionResult,\n ActionType,\n DwellProfile,\n HumanAction,\n HumanPlugin,\n KnownActionType,\n MouseProfile,\n Personality,\n PersonalityConfig,\n PersonalityExtension,\n PluginContext,\n PresetName,\n ReadingProfile,\n Rng,\n TypingProfile,\n} from '@humanjs/core';\n// Re-exports of the public core API so consumers have one import surface.\nexport {\n blend,\n careful,\n createRng,\n distracted,\n fast,\n precise,\n resolvePersonality,\n} from '@humanjs/core';\n\n/**\n * How fast the humanized session runs.\n * - `'human'` — full humanization (default)\n * - `'fast'` — humanized but accelerated\n * - `'instant'` — bypass all humanization, straight Playwright\n */\nexport type Speed = 'fast' | 'human' | 'instant';\n\n/** Options for {@link createHuman}. */\nexport interface CreateHumanOptions {\n /** Personality preset, extension, or fully built personality. Defaults to `'careful'`. */\n readonly personality?: PersonalityConfig;\n /** Seed for the session's PRNG. Same seed produces identical trajectories. */\n readonly seed?: number | string;\n /** Speed mode. Defaults to `'human'`. */\n readonly speed?: Speed;\n /** Plugins installed on this session, invoked in registration order. */\n readonly plugins?: readonly HumanPlugin[];\n}\n\n/** A humanized Playwright session bound to a single `Page`. */\nexport interface Human {\n /** The resolved personality this session is using. */\n readonly personality: Personality;\n /** The speed mode this session was created with. */\n readonly speed: Speed;\n /**\n * Navigate to `url`. Plugins observe the action via `'goto'`; the underlying\n * `page.goto(url)` is awaited unchanged.\n */\n goto(url: string): Promise<void>;\n}\n\n/**\n * Creates a humanized session bound to a Playwright `Page`.\n *\n * @example\n * ```ts\n * import { chromium } from 'playwright';\n * import { createHuman } from '@humanjs/playwright';\n *\n * const browser = await chromium.launch();\n * const page = await browser.newPage();\n *\n * const human = await createHuman(page, {\n * personality: 'careful',\n * seed: 'session-42',\n * });\n *\n * await human.goto('https://example.com');\n * ```\n */\nexport async function createHuman(page: Page, options: CreateHumanOptions = {}): Promise<Human> {\n const personality = resolvePersonality(options.personality ?? 'careful');\n const rng = createRng(options.seed);\n const speed = options.speed ?? 'human';\n const plugins = options.plugins ?? [];\n\n const context: PluginContext = { personality, rng };\n for (const plugin of plugins) {\n await plugin.install?.(context);\n }\n\n async function performAction<T>(action: HumanAction, actionFn: () => Promise<T>): Promise<T> {\n for (const plugin of plugins) {\n await plugin.beforeAction?.(action);\n }\n const startedAt = Date.now();\n try {\n const value = await actionFn();\n const result: ActionResult = {\n type: action.type,\n durationMs: Date.now() - startedAt,\n };\n for (const plugin of plugins) {\n await plugin.afterAction?.(action, result);\n }\n return value;\n } catch (error) {\n for (const plugin of plugins) {\n await plugin.onError?.(action, error);\n }\n throw error;\n }\n }\n\n return {\n personality,\n speed,\n async goto(url) {\n await performAction({ type: 'goto', params: { url } }, async () => {\n await page.goto(url);\n });\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/mouse/index.ts","../src/index.ts"],"names":["box","bezierPath","humanizePath","resolvePersonality","createRng"],"mappings":"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","file":"index.cjs","sourcesContent":["import { bezierPath, humanizePath, type Personality, type Point, type Rng } from '@humanjs/core';\nimport type { Locator, Page } from 'playwright';\nimport type { Speed } from '../index';\n\n/** Runtime dependencies for a humanized mouse action. */\nexport interface MouseContext {\n readonly page: Page;\n readonly personality: Personality;\n readonly rng: Rng;\n readonly speed: Speed;\n /** Last known mouse position — used as the path start. */\n readonly getMousePosition: () => Point;\n /** Updates the last known mouse position after a move/click. */\n readonly setMousePosition: (point: Point) => void;\n}\n\n/** Result of a click action, returned to the caller for observability. */\nexport interface ClickResult {\n /** Coordinates the click landed at. */\n readonly target: Point;\n}\n\n/**\n * Executes a humanized click on a target locator.\n *\n * Steps:\n * 1. Resolve the target's bounding box.\n * 2. Pick a point inside it — Gaussian-centered so we never click dead-center,\n * which is itself a bot signal.\n * 3. Generate a Bezier path from the current mouse position to the target.\n * 4. Apply velocity profile + micro-jitter via `humanizePath`.\n * 5. Walk the mouse along the path with timing scaled by personality + speed.\n * 6. Click at the target coordinates.\n *\n * In `speed: 'instant'`, all humanization is bypassed and Playwright's\n * native `locator.click()` is used directly.\n */\nexport async function executeClick(\n target: Locator | string,\n ctx: MouseContext,\n): Promise<ClickResult> {\n const locator = typeof target === 'string' ? ctx.page.locator(target) : target;\n\n if (ctx.speed === 'instant') {\n // Read the bounding box BEFORE the click — the click may navigate away\n // or remove the element, after which `boundingBox()` returns null.\n const box = await locator.boundingBox();\n await locator.click();\n const center = box\n ? { x: box.x + box.width / 2, y: box.y + box.height / 2 }\n : ctx.getMousePosition();\n ctx.setMousePosition(center);\n return { target: center };\n }\n\n const box = await locator.boundingBox();\n if (!box) {\n throw new Error(\n `Cannot click: element not found or has no bounding box (target: ${describeTarget(target)})`,\n );\n }\n\n const targetPoint = pickClickPoint(box, ctx.rng);\n const startPoint = ctx.getMousePosition();\n\n const rawPath = bezierPath(startPoint, targetPoint, ctx.rng, {\n curvature: ctx.personality.mouse.curvature,\n });\n const path = humanizePath(rawPath, ctx.rng);\n\n await walkMouseAlongPath(ctx.page, path, ctx.personality, ctx.rng, ctx.speed);\n\n // Hover dwell — a real user briefly settles on the target before clicking.\n const preClickMs = computeDwellTime(\n ctx.personality.dwell.preClickMs,\n ctx.personality.dwell.preClickJitter,\n ctx.personality,\n ctx.speed,\n ctx.rng,\n );\n if (preClickMs > 0) await sleep(preClickMs);\n\n // Commit the new position BEFORE the click side-effect. If the click throws\n // (page closed, target removed mid-flight), the next action still starts\n // from the correct mouse position.\n ctx.setMousePosition(targetPoint);\n await ctx.page.mouse.click(targetPoint.x, targetPoint.y);\n\n // Post-action dwell — a beat after the click before the next action.\n const postActionMs = computeDwellTime(\n ctx.personality.dwell.postActionMs,\n ctx.personality.dwell.postActionJitter,\n ctx.personality,\n ctx.speed,\n ctx.rng,\n );\n if (postActionMs > 0) await sleep(postActionMs);\n\n return { target: targetPoint };\n}\n\n/** Bounding box returned by Playwright's `Locator.boundingBox()`. */\ninterface BoundingBox {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\n/**\n * Picks a click point inside the bounding box. Gaussian-centered so the\n * majority of clicks land near the visual center but with natural spread.\n * Clamped to never fall outside the element.\n */\nfunction pickClickPoint(box: BoundingBox, rng: Rng): Point {\n const cx = box.x + box.width / 2;\n const cy = box.y + box.height / 2;\n // Spread is 1/8 of each dimension — most clicks fall within the middle\n // half of the element, but we never click dead-center deterministically.\n const x = clamp(cx + rng.nextGaussian(0, box.width / 8), box.x, box.x + box.width);\n const y = clamp(cy + rng.nextGaussian(0, box.height / 8), box.y, box.y + box.height);\n return { x, y };\n}\n\n/**\n * Walks the mouse along the path with timing scaled by personality + speed.\n * Sleeps between moves so the path is visible to observers (and to whatever\n * timing-based detector is watching).\n */\nasync function walkMouseAlongPath(\n page: Page,\n path: readonly Point[],\n personality: Personality,\n rng: Rng,\n speed: Speed,\n): Promise<void> {\n if (path.length === 0) return;\n const totalTimeMs = computeTravelTime(path, personality, speed, rng);\n const stepDelayMs = path.length > 1 ? totalTimeMs / (path.length - 1) : 0;\n\n for (let i = 0; i < path.length; i++) {\n const point = path[i];\n if (!point) continue;\n await page.mouse.move(point.x, point.y);\n if (i < path.length - 1 && stepDelayMs > 0) {\n await sleep(stepDelayMs);\n }\n }\n}\n\n/**\n * Travel time in ms for the given path.\n *\n * base = (totalDistance / 1000) * Personality.mouse.travelTimeMs\n * jitter = base * Personality.mouse.travelTimeJitter * rand[-1, 1]\n * total = (base + jitter) * Personality.speed * speedModeFactor\n */\nfunction computeTravelTime(\n path: readonly Point[],\n personality: Personality,\n speed: Speed,\n rng: Rng,\n): number {\n let distance = 0;\n for (let i = 1; i < path.length; i++) {\n const prev = path[i - 1];\n const curr = path[i];\n if (!prev || !curr) continue;\n distance += Math.hypot(curr.x - prev.x, curr.y - prev.y);\n }\n\n const baseTime = (distance / 1000) * personality.mouse.travelTimeMs;\n const jitterMag = baseTime * personality.mouse.travelTimeJitter;\n const jitter = rng.nextFloat(-jitterMag, jitterMag);\n const total = (baseTime + jitter) * personality.speed * speedModeFactor(speed);\n return Math.max(0, total);\n}\n\n/**\n * Dwell time in ms for a single pause.\n *\n * base = meanMs\n * jitter = base * jitterFraction * rand[-1, 1]\n * total = (base + jitter) * Personality.speed * speedModeFactor\n *\n * Returns 0 for zero/negative inputs so callers can skip the sleep entirely.\n */\nfunction computeDwellTime(\n meanMs: number,\n jitter: number,\n personality: Personality,\n speed: Speed,\n rng: Rng,\n): number {\n if (meanMs <= 0) return 0;\n const jitterMag = meanMs * jitter;\n const offset = rng.nextFloat(-jitterMag, jitterMag);\n return Math.max(0, (meanMs + offset) * personality.speed * speedModeFactor(speed));\n}\n\nfunction speedModeFactor(speed: Speed): number {\n switch (speed) {\n case 'fast':\n return 0.5;\n case 'instant':\n return 0;\n default:\n return 1;\n }\n}\n\nfunction describeTarget(target: Locator | string): string {\n return typeof target === 'string' ? target : (target.toString?.() ?? 'locator');\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return value < min ? min : value > max ? max : value;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import {\n type ActionResult,\n createRng,\n type HumanAction,\n type HumanPlugin,\n type Personality,\n type PersonalityConfig,\n type PluginContext,\n type Point,\n resolvePersonality,\n} from '@humanjs/core';\nimport type { Locator, Page } from 'playwright';\nimport { executeClick } from './mouse';\n\nexport type {\n ActionResult,\n ActionType,\n BezierPathOptions,\n DwellProfile,\n HumanAction,\n HumanizePathOptions,\n HumanPlugin,\n KnownActionType,\n MouseProfile,\n Personality,\n PersonalityConfig,\n PersonalityExtension,\n PluginContext,\n Point,\n PresetName,\n ReadingProfile,\n Rng,\n TypingProfile,\n} from '@humanjs/core';\n// Re-exports of the public core API so consumers have one import surface.\nexport {\n applyMicroJitter,\n applyVelocityProfile,\n bezierPath,\n blend,\n careful,\n createRng,\n distracted,\n fast,\n humanizePath,\n precise,\n resolvePersonality,\n} from '@humanjs/core';\n\n/**\n * How fast the humanized session runs.\n * - `'human'` — full humanization (default)\n * - `'fast'` — humanized but accelerated\n * - `'instant'` — bypass all humanization, straight Playwright\n */\nexport type Speed = 'fast' | 'human' | 'instant';\n\n/** Options for {@link createHuman}. */\nexport interface CreateHumanOptions {\n /** Personality preset, extension, or fully built personality. Defaults to `'careful'`. */\n readonly personality?: PersonalityConfig;\n /** Seed for the session's PRNG. Same seed produces identical trajectories. */\n readonly seed?: number | string;\n /** Speed mode. Defaults to `'human'`. */\n readonly speed?: Speed;\n /** Plugins installed on this session, invoked in registration order. */\n readonly plugins?: readonly HumanPlugin[];\n /**\n * Starting cursor position used as the origin of the first humanized path.\n * Defaults to `{ x: 0, y: 0 }`. Set this if you've already moved the cursor\n * (e.g. via `page.mouse.move`) before creating the session, so the first\n * click's path starts from the correct location.\n */\n readonly initialMousePosition?: Point;\n}\n\n/** A humanized Playwright session bound to a single `Page`. */\nexport interface Human {\n /** The resolved personality this session is using. */\n readonly personality: Personality;\n /** The speed mode this session was created with. */\n readonly speed: Speed;\n /**\n * Navigate to `url`. Plugins observe the action via `'goto'`; the underlying\n * `page.goto(url)` is awaited unchanged.\n */\n goto(url: string): Promise<void>;\n /**\n * Move the mouse along a humanized Bezier path to `target` and click.\n *\n * `target` accepts either a Playwright-compatible selector string (e.g.\n * `'button:has-text(\"Buy now\")'`) or a built `Locator`. The click point\n * inside the element is Gaussian-distributed around the center.\n *\n * In `speed: 'instant'`, all humanization is skipped and Playwright's\n * native `locator.click()` is used directly.\n */\n click(target: Locator | string): Promise<void>;\n}\n\n/**\n * Creates a humanized session bound to a Playwright `Page`.\n *\n * @example\n * ```ts\n * import { chromium } from 'playwright';\n * import { createHuman } from '@humanjs/playwright';\n *\n * const browser = await chromium.launch();\n * const page = await browser.newPage();\n *\n * const human = await createHuman(page, {\n * personality: 'careful',\n * seed: 'session-42',\n * });\n *\n * await human.goto('https://example.com');\n * ```\n */\nexport async function createHuman(page: Page, options: CreateHumanOptions = {}): Promise<Human> {\n const personality = resolvePersonality(options.personality ?? 'careful');\n const rng = createRng(options.seed);\n const speed = options.speed ?? 'human';\n const plugins = options.plugins ?? [];\n\n const context: PluginContext = { personality, rng };\n for (const plugin of plugins) {\n await plugin.install?.(context);\n }\n\n async function performAction<T>(action: HumanAction, actionFn: () => Promise<T>): Promise<T> {\n for (const plugin of plugins) {\n await plugin.beforeAction?.(action);\n }\n const startedAt = Date.now();\n try {\n const value = await actionFn();\n const result: ActionResult = {\n type: action.type,\n durationMs: Date.now() - startedAt,\n };\n for (const plugin of plugins) {\n await plugin.afterAction?.(action, result);\n }\n return value;\n } catch (error) {\n for (const plugin of plugins) {\n await plugin.onError?.(action, error);\n }\n throw error;\n }\n }\n\n let lastMousePosition: Point = options.initialMousePosition ?? { x: 0, y: 0 };\n\n return {\n personality,\n speed,\n async goto(url) {\n await performAction({ type: 'goto', params: { url } }, async () => {\n await page.goto(url);\n });\n },\n async click(target) {\n const description = typeof target === 'string' ? target : (target.toString?.() ?? 'locator');\n await performAction({ type: 'click', params: { target: description } }, async () => {\n await executeClick(target, {\n page,\n personality,\n rng,\n speed,\n getMousePosition: () => lastMousePosition,\n setMousePosition: (point) => {\n lastMousePosition = point;\n },\n });\n });\n },\n };\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { PersonalityConfig, HumanPlugin, Personality } from '@humanjs/core';
|
|
2
|
-
export { ActionResult, ActionType, DwellProfile, HumanAction, HumanPlugin, KnownActionType, MouseProfile, Personality, PersonalityConfig, PersonalityExtension, PluginContext, PresetName, ReadingProfile, Rng, TypingProfile, blend, careful, createRng, distracted, fast, precise, resolvePersonality } from '@humanjs/core';
|
|
3
|
-
import { Page } from 'playwright';
|
|
1
|
+
import { PersonalityConfig, HumanPlugin, Point, Personality } from '@humanjs/core';
|
|
2
|
+
export { ActionResult, ActionType, BezierPathOptions, DwellProfile, HumanAction, HumanPlugin, HumanizePathOptions, KnownActionType, MouseProfile, Personality, PersonalityConfig, PersonalityExtension, PluginContext, Point, PresetName, ReadingProfile, Rng, TypingProfile, applyMicroJitter, applyVelocityProfile, bezierPath, blend, careful, createRng, distracted, fast, humanizePath, precise, resolvePersonality } from '@humanjs/core';
|
|
3
|
+
import { Locator, Page } from 'playwright';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* How fast the humanized session runs.
|
|
@@ -19,6 +19,13 @@ interface CreateHumanOptions {
|
|
|
19
19
|
readonly speed?: Speed;
|
|
20
20
|
/** Plugins installed on this session, invoked in registration order. */
|
|
21
21
|
readonly plugins?: readonly HumanPlugin[];
|
|
22
|
+
/**
|
|
23
|
+
* Starting cursor position used as the origin of the first humanized path.
|
|
24
|
+
* Defaults to `{ x: 0, y: 0 }`. Set this if you've already moved the cursor
|
|
25
|
+
* (e.g. via `page.mouse.move`) before creating the session, so the first
|
|
26
|
+
* click's path starts from the correct location.
|
|
27
|
+
*/
|
|
28
|
+
readonly initialMousePosition?: Point;
|
|
22
29
|
}
|
|
23
30
|
/** A humanized Playwright session bound to a single `Page`. */
|
|
24
31
|
interface Human {
|
|
@@ -31,6 +38,17 @@ interface Human {
|
|
|
31
38
|
* `page.goto(url)` is awaited unchanged.
|
|
32
39
|
*/
|
|
33
40
|
goto(url: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Move the mouse along a humanized Bezier path to `target` and click.
|
|
43
|
+
*
|
|
44
|
+
* `target` accepts either a Playwright-compatible selector string (e.g.
|
|
45
|
+
* `'button:has-text("Buy now")'`) or a built `Locator`. The click point
|
|
46
|
+
* inside the element is Gaussian-distributed around the center.
|
|
47
|
+
*
|
|
48
|
+
* In `speed: 'instant'`, all humanization is skipped and Playwright's
|
|
49
|
+
* native `locator.click()` is used directly.
|
|
50
|
+
*/
|
|
51
|
+
click(target: Locator | string): Promise<void>;
|
|
34
52
|
}
|
|
35
53
|
/**
|
|
36
54
|
* Creates a humanized session bound to a Playwright `Page`.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { PersonalityConfig, HumanPlugin, Personality } from '@humanjs/core';
|
|
2
|
-
export { ActionResult, ActionType, DwellProfile, HumanAction, HumanPlugin, KnownActionType, MouseProfile, Personality, PersonalityConfig, PersonalityExtension, PluginContext, PresetName, ReadingProfile, Rng, TypingProfile, blend, careful, createRng, distracted, fast, precise, resolvePersonality } from '@humanjs/core';
|
|
3
|
-
import { Page } from 'playwright';
|
|
1
|
+
import { PersonalityConfig, HumanPlugin, Point, Personality } from '@humanjs/core';
|
|
2
|
+
export { ActionResult, ActionType, BezierPathOptions, DwellProfile, HumanAction, HumanPlugin, HumanizePathOptions, KnownActionType, MouseProfile, Personality, PersonalityConfig, PersonalityExtension, PluginContext, Point, PresetName, ReadingProfile, Rng, TypingProfile, applyMicroJitter, applyVelocityProfile, bezierPath, blend, careful, createRng, distracted, fast, humanizePath, precise, resolvePersonality } from '@humanjs/core';
|
|
3
|
+
import { Locator, Page } from 'playwright';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* How fast the humanized session runs.
|
|
@@ -19,6 +19,13 @@ interface CreateHumanOptions {
|
|
|
19
19
|
readonly speed?: Speed;
|
|
20
20
|
/** Plugins installed on this session, invoked in registration order. */
|
|
21
21
|
readonly plugins?: readonly HumanPlugin[];
|
|
22
|
+
/**
|
|
23
|
+
* Starting cursor position used as the origin of the first humanized path.
|
|
24
|
+
* Defaults to `{ x: 0, y: 0 }`. Set this if you've already moved the cursor
|
|
25
|
+
* (e.g. via `page.mouse.move`) before creating the session, so the first
|
|
26
|
+
* click's path starts from the correct location.
|
|
27
|
+
*/
|
|
28
|
+
readonly initialMousePosition?: Point;
|
|
22
29
|
}
|
|
23
30
|
/** A humanized Playwright session bound to a single `Page`. */
|
|
24
31
|
interface Human {
|
|
@@ -31,6 +38,17 @@ interface Human {
|
|
|
31
38
|
* `page.goto(url)` is awaited unchanged.
|
|
32
39
|
*/
|
|
33
40
|
goto(url: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Move the mouse along a humanized Bezier path to `target` and click.
|
|
43
|
+
*
|
|
44
|
+
* `target` accepts either a Playwright-compatible selector string (e.g.
|
|
45
|
+
* `'button:has-text("Buy now")'`) or a built `Locator`. The click point
|
|
46
|
+
* inside the element is Gaussian-distributed around the center.
|
|
47
|
+
*
|
|
48
|
+
* In `speed: 'instant'`, all humanization is skipped and Playwright's
|
|
49
|
+
* native `locator.click()` is used directly.
|
|
50
|
+
*/
|
|
51
|
+
click(target: Locator | string): Promise<void>;
|
|
34
52
|
}
|
|
35
53
|
/**
|
|
36
54
|
* Creates a humanized session bound to a Playwright `Page`.
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,108 @@
|
|
|
1
|
-
import { resolvePersonality, createRng } from '@humanjs/core';
|
|
2
|
-
export { blend, careful, createRng, distracted, fast, precise, resolvePersonality } from '@humanjs/core';
|
|
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';
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
5
|
+
async function executeClick(target, ctx) {
|
|
6
|
+
const locator = typeof target === "string" ? ctx.page.locator(target) : target;
|
|
7
|
+
if (ctx.speed === "instant") {
|
|
8
|
+
const box2 = await locator.boundingBox();
|
|
9
|
+
await locator.click();
|
|
10
|
+
const center = box2 ? { x: box2.x + box2.width / 2, y: box2.y + box2.height / 2 } : ctx.getMousePosition();
|
|
11
|
+
ctx.setMousePosition(center);
|
|
12
|
+
return { target: center };
|
|
13
|
+
}
|
|
14
|
+
const box = await locator.boundingBox();
|
|
15
|
+
if (!box) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Cannot click: element not found or has no bounding box (target: ${describeTarget(target)})`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
const targetPoint = pickClickPoint(box, ctx.rng);
|
|
21
|
+
const startPoint = ctx.getMousePosition();
|
|
22
|
+
const rawPath = bezierPath(startPoint, targetPoint, ctx.rng, {
|
|
23
|
+
curvature: ctx.personality.mouse.curvature
|
|
24
|
+
});
|
|
25
|
+
const path = humanizePath(rawPath, ctx.rng);
|
|
26
|
+
await walkMouseAlongPath(ctx.page, path, ctx.personality, ctx.rng, ctx.speed);
|
|
27
|
+
const preClickMs = computeDwellTime(
|
|
28
|
+
ctx.personality.dwell.preClickMs,
|
|
29
|
+
ctx.personality.dwell.preClickJitter,
|
|
30
|
+
ctx.personality,
|
|
31
|
+
ctx.speed,
|
|
32
|
+
ctx.rng
|
|
33
|
+
);
|
|
34
|
+
if (preClickMs > 0) await sleep(preClickMs);
|
|
35
|
+
ctx.setMousePosition(targetPoint);
|
|
36
|
+
await ctx.page.mouse.click(targetPoint.x, targetPoint.y);
|
|
37
|
+
const postActionMs = computeDwellTime(
|
|
38
|
+
ctx.personality.dwell.postActionMs,
|
|
39
|
+
ctx.personality.dwell.postActionJitter,
|
|
40
|
+
ctx.personality,
|
|
41
|
+
ctx.speed,
|
|
42
|
+
ctx.rng
|
|
43
|
+
);
|
|
44
|
+
if (postActionMs > 0) await sleep(postActionMs);
|
|
45
|
+
return { target: targetPoint };
|
|
46
|
+
}
|
|
47
|
+
function pickClickPoint(box, rng) {
|
|
48
|
+
const cx = box.x + box.width / 2;
|
|
49
|
+
const cy = box.y + box.height / 2;
|
|
50
|
+
const x = clamp(cx + rng.nextGaussian(0, box.width / 8), box.x, box.x + box.width);
|
|
51
|
+
const y = clamp(cy + rng.nextGaussian(0, box.height / 8), box.y, box.y + box.height);
|
|
52
|
+
return { x, y };
|
|
53
|
+
}
|
|
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
|
+
function computeTravelTime(path, personality, speed, rng) {
|
|
68
|
+
let distance = 0;
|
|
69
|
+
for (let i = 1; i < path.length; i++) {
|
|
70
|
+
const prev = path[i - 1];
|
|
71
|
+
const curr = path[i];
|
|
72
|
+
if (!prev || !curr) continue;
|
|
73
|
+
distance += Math.hypot(curr.x - prev.x, curr.y - prev.y);
|
|
74
|
+
}
|
|
75
|
+
const baseTime = distance / 1e3 * personality.mouse.travelTimeMs;
|
|
76
|
+
const jitterMag = baseTime * personality.mouse.travelTimeJitter;
|
|
77
|
+
const jitter = rng.nextFloat(-jitterMag, jitterMag);
|
|
78
|
+
const total = (baseTime + jitter) * personality.speed * speedModeFactor(speed);
|
|
79
|
+
return Math.max(0, total);
|
|
80
|
+
}
|
|
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
|
+
function describeTarget(target) {
|
|
98
|
+
return typeof target === "string" ? target : target.toString?.() ?? "locator";
|
|
99
|
+
}
|
|
100
|
+
function clamp(value, min, max) {
|
|
101
|
+
return value < min ? min : value > max ? max : value;
|
|
102
|
+
}
|
|
103
|
+
function sleep(ms) {
|
|
104
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
105
|
+
}
|
|
5
106
|
async function createHuman(page, options = {}) {
|
|
6
107
|
const personality = resolvePersonality(options.personality ?? "careful");
|
|
7
108
|
const rng = createRng(options.seed);
|
|
@@ -33,6 +134,7 @@ async function createHuman(page, options = {}) {
|
|
|
33
134
|
throw error;
|
|
34
135
|
}
|
|
35
136
|
}
|
|
137
|
+
let lastMousePosition = options.initialMousePosition ?? { x: 0, y: 0 };
|
|
36
138
|
return {
|
|
37
139
|
personality,
|
|
38
140
|
speed,
|
|
@@ -40,6 +142,21 @@ async function createHuman(page, options = {}) {
|
|
|
40
142
|
await performAction({ type: "goto", params: { url } }, async () => {
|
|
41
143
|
await page.goto(url);
|
|
42
144
|
});
|
|
145
|
+
},
|
|
146
|
+
async click(target) {
|
|
147
|
+
const description = typeof target === "string" ? target : target.toString?.() ?? "locator";
|
|
148
|
+
await performAction({ type: "click", params: { target: description } }, async () => {
|
|
149
|
+
await executeClick(target, {
|
|
150
|
+
page,
|
|
151
|
+
personality,
|
|
152
|
+
rng,
|
|
153
|
+
speed,
|
|
154
|
+
getMousePosition: () => lastMousePosition,
|
|
155
|
+
setMousePosition: (point) => {
|
|
156
|
+
lastMousePosition = point;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
43
160
|
}
|
|
44
161
|
};
|
|
45
162
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;;AA4FA,eAAsB,WAAA,CAAY,IAAA,EAAY,OAAA,GAA8B,EAAC,EAAmB;AAC9F,EAAA,MAAM,WAAA,GAAc,kBAAA,CAAmB,OAAA,CAAQ,WAAA,IAAe,SAAS,CAAA;AACvE,EAAA,MAAM,GAAA,GAAM,SAAA,CAAU,OAAA,CAAQ,IAAI,CAAA;AAClC,EAAA,MAAM,KAAA,GAAQ,QAAQ,KAAA,IAAS,OAAA;AAC/B,EAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,IAAW,EAAC;AAEpC,EAAA,MAAM,OAAA,GAAyB,EAAE,WAAA,EAAa,GAAA,EAAI;AAClD,EAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,IAAA,MAAM,MAAA,CAAO,UAAU,OAAO,CAAA;AAAA,EAChC;AAEA,EAAA,eAAe,aAAA,CAAiB,QAAqB,QAAA,EAAwC;AAC3F,IAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,MAAA,MAAM,MAAA,CAAO,eAAe,MAAM,CAAA;AAAA,IACpC;AACA,IAAA,MAAM,SAAA,GAAY,KAAK,GAAA,EAAI;AAC3B,IAAA,IAAI;AACF,MAAA,MAAM,KAAA,GAAQ,MAAM,QAAA,EAAS;AAC7B,MAAA,MAAM,MAAA,GAAuB;AAAA,QAC3B,MAAM,MAAA,CAAO,IAAA;AAAA,QACb,UAAA,EAAY,IAAA,CAAK,GAAA,EAAI,GAAI;AAAA,OAC3B;AACA,MAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,QAAA,MAAM,MAAA,CAAO,WAAA,GAAc,MAAA,EAAQ,MAAM,CAAA;AAAA,MAC3C;AACA,MAAA,OAAO,KAAA;AAAA,IACT,SAAS,KAAA,EAAO;AACd,MAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,QAAA,MAAM,MAAA,CAAO,OAAA,GAAU,MAAA,EAAQ,KAAK,CAAA;AAAA,MACtC;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,WAAA;AAAA,IACA,KAAA;AAAA,IACA,MAAM,KAAK,GAAA,EAAK;AACd,MAAA,MAAM,aAAA,CAAc,EAAE,IAAA,EAAM,MAAA,EAAQ,QAAQ,EAAE,GAAA,EAAI,EAAE,EAAG,YAAY;AACjE,QAAA,MAAM,IAAA,CAAK,KAAK,GAAG,CAAA;AAAA,MACrB,CAAC,CAAA;AAAA,IACH;AAAA,GACF;AACF","file":"index.js","sourcesContent":["import {\n type ActionResult,\n createRng,\n type HumanAction,\n type HumanPlugin,\n type Personality,\n type PersonalityConfig,\n type PluginContext,\n resolvePersonality,\n} from '@humanjs/core';\nimport type { Page } from 'playwright';\n\nexport type {\n ActionResult,\n ActionType,\n DwellProfile,\n HumanAction,\n HumanPlugin,\n KnownActionType,\n MouseProfile,\n Personality,\n PersonalityConfig,\n PersonalityExtension,\n PluginContext,\n PresetName,\n ReadingProfile,\n Rng,\n TypingProfile,\n} from '@humanjs/core';\n// Re-exports of the public core API so consumers have one import surface.\nexport {\n blend,\n careful,\n createRng,\n distracted,\n fast,\n precise,\n resolvePersonality,\n} from '@humanjs/core';\n\n/**\n * How fast the humanized session runs.\n * - `'human'` — full humanization (default)\n * - `'fast'` — humanized but accelerated\n * - `'instant'` — bypass all humanization, straight Playwright\n */\nexport type Speed = 'fast' | 'human' | 'instant';\n\n/** Options for {@link createHuman}. */\nexport interface CreateHumanOptions {\n /** Personality preset, extension, or fully built personality. Defaults to `'careful'`. */\n readonly personality?: PersonalityConfig;\n /** Seed for the session's PRNG. Same seed produces identical trajectories. */\n readonly seed?: number | string;\n /** Speed mode. Defaults to `'human'`. */\n readonly speed?: Speed;\n /** Plugins installed on this session, invoked in registration order. */\n readonly plugins?: readonly HumanPlugin[];\n}\n\n/** A humanized Playwright session bound to a single `Page`. */\nexport interface Human {\n /** The resolved personality this session is using. */\n readonly personality: Personality;\n /** The speed mode this session was created with. */\n readonly speed: Speed;\n /**\n * Navigate to `url`. Plugins observe the action via `'goto'`; the underlying\n * `page.goto(url)` is awaited unchanged.\n */\n goto(url: string): Promise<void>;\n}\n\n/**\n * Creates a humanized session bound to a Playwright `Page`.\n *\n * @example\n * ```ts\n * import { chromium } from 'playwright';\n * import { createHuman } from '@humanjs/playwright';\n *\n * const browser = await chromium.launch();\n * const page = await browser.newPage();\n *\n * const human = await createHuman(page, {\n * personality: 'careful',\n * seed: 'session-42',\n * });\n *\n * await human.goto('https://example.com');\n * ```\n */\nexport async function createHuman(page: Page, options: CreateHumanOptions = {}): Promise<Human> {\n const personality = resolvePersonality(options.personality ?? 'careful');\n const rng = createRng(options.seed);\n const speed = options.speed ?? 'human';\n const plugins = options.plugins ?? [];\n\n const context: PluginContext = { personality, rng };\n for (const plugin of plugins) {\n await plugin.install?.(context);\n }\n\n async function performAction<T>(action: HumanAction, actionFn: () => Promise<T>): Promise<T> {\n for (const plugin of plugins) {\n await plugin.beforeAction?.(action);\n }\n const startedAt = Date.now();\n try {\n const value = await actionFn();\n const result: ActionResult = {\n type: action.type,\n durationMs: Date.now() - startedAt,\n };\n for (const plugin of plugins) {\n await plugin.afterAction?.(action, result);\n }\n return value;\n } catch (error) {\n for (const plugin of plugins) {\n await plugin.onError?.(action, error);\n }\n throw error;\n }\n }\n\n return {\n personality,\n speed,\n async goto(url) {\n await performAction({ type: 'goto', params: { url } }, async () => {\n await page.goto(url);\n });\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/mouse/index.ts","../src/index.ts"],"names":["box"],"mappings":"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","file":"index.js","sourcesContent":["import { bezierPath, humanizePath, type Personality, type Point, type Rng } from '@humanjs/core';\nimport type { Locator, Page } from 'playwright';\nimport type { Speed } from '../index';\n\n/** Runtime dependencies for a humanized mouse action. */\nexport interface MouseContext {\n readonly page: Page;\n readonly personality: Personality;\n readonly rng: Rng;\n readonly speed: Speed;\n /** Last known mouse position — used as the path start. */\n readonly getMousePosition: () => Point;\n /** Updates the last known mouse position after a move/click. */\n readonly setMousePosition: (point: Point) => void;\n}\n\n/** Result of a click action, returned to the caller for observability. */\nexport interface ClickResult {\n /** Coordinates the click landed at. */\n readonly target: Point;\n}\n\n/**\n * Executes a humanized click on a target locator.\n *\n * Steps:\n * 1. Resolve the target's bounding box.\n * 2. Pick a point inside it — Gaussian-centered so we never click dead-center,\n * which is itself a bot signal.\n * 3. Generate a Bezier path from the current mouse position to the target.\n * 4. Apply velocity profile + micro-jitter via `humanizePath`.\n * 5. Walk the mouse along the path with timing scaled by personality + speed.\n * 6. Click at the target coordinates.\n *\n * In `speed: 'instant'`, all humanization is bypassed and Playwright's\n * native `locator.click()` is used directly.\n */\nexport async function executeClick(\n target: Locator | string,\n ctx: MouseContext,\n): Promise<ClickResult> {\n const locator = typeof target === 'string' ? ctx.page.locator(target) : target;\n\n if (ctx.speed === 'instant') {\n // Read the bounding box BEFORE the click — the click may navigate away\n // or remove the element, after which `boundingBox()` returns null.\n const box = await locator.boundingBox();\n await locator.click();\n const center = box\n ? { x: box.x + box.width / 2, y: box.y + box.height / 2 }\n : ctx.getMousePosition();\n ctx.setMousePosition(center);\n return { target: center };\n }\n\n const box = await locator.boundingBox();\n if (!box) {\n throw new Error(\n `Cannot click: element not found or has no bounding box (target: ${describeTarget(target)})`,\n );\n }\n\n const targetPoint = pickClickPoint(box, ctx.rng);\n const startPoint = ctx.getMousePosition();\n\n const rawPath = bezierPath(startPoint, targetPoint, ctx.rng, {\n curvature: ctx.personality.mouse.curvature,\n });\n const path = humanizePath(rawPath, ctx.rng);\n\n await walkMouseAlongPath(ctx.page, path, ctx.personality, ctx.rng, ctx.speed);\n\n // Hover dwell — a real user briefly settles on the target before clicking.\n const preClickMs = computeDwellTime(\n ctx.personality.dwell.preClickMs,\n ctx.personality.dwell.preClickJitter,\n ctx.personality,\n ctx.speed,\n ctx.rng,\n );\n if (preClickMs > 0) await sleep(preClickMs);\n\n // Commit the new position BEFORE the click side-effect. If the click throws\n // (page closed, target removed mid-flight), the next action still starts\n // from the correct mouse position.\n ctx.setMousePosition(targetPoint);\n await ctx.page.mouse.click(targetPoint.x, targetPoint.y);\n\n // Post-action dwell — a beat after the click before the next action.\n const postActionMs = computeDwellTime(\n ctx.personality.dwell.postActionMs,\n ctx.personality.dwell.postActionJitter,\n ctx.personality,\n ctx.speed,\n ctx.rng,\n );\n if (postActionMs > 0) await sleep(postActionMs);\n\n return { target: targetPoint };\n}\n\n/** Bounding box returned by Playwright's `Locator.boundingBox()`. */\ninterface BoundingBox {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\n/**\n * Picks a click point inside the bounding box. Gaussian-centered so the\n * majority of clicks land near the visual center but with natural spread.\n * Clamped to never fall outside the element.\n */\nfunction pickClickPoint(box: BoundingBox, rng: Rng): Point {\n const cx = box.x + box.width / 2;\n const cy = box.y + box.height / 2;\n // Spread is 1/8 of each dimension — most clicks fall within the middle\n // half of the element, but we never click dead-center deterministically.\n const x = clamp(cx + rng.nextGaussian(0, box.width / 8), box.x, box.x + box.width);\n const y = clamp(cy + rng.nextGaussian(0, box.height / 8), box.y, box.y + box.height);\n return { x, y };\n}\n\n/**\n * Walks the mouse along the path with timing scaled by personality + speed.\n * Sleeps between moves so the path is visible to observers (and to whatever\n * timing-based detector is watching).\n */\nasync function walkMouseAlongPath(\n page: Page,\n path: readonly Point[],\n personality: Personality,\n rng: Rng,\n speed: Speed,\n): Promise<void> {\n if (path.length === 0) return;\n const totalTimeMs = computeTravelTime(path, personality, speed, rng);\n const stepDelayMs = path.length > 1 ? totalTimeMs / (path.length - 1) : 0;\n\n for (let i = 0; i < path.length; i++) {\n const point = path[i];\n if (!point) continue;\n await page.mouse.move(point.x, point.y);\n if (i < path.length - 1 && stepDelayMs > 0) {\n await sleep(stepDelayMs);\n }\n }\n}\n\n/**\n * Travel time in ms for the given path.\n *\n * base = (totalDistance / 1000) * Personality.mouse.travelTimeMs\n * jitter = base * Personality.mouse.travelTimeJitter * rand[-1, 1]\n * total = (base + jitter) * Personality.speed * speedModeFactor\n */\nfunction computeTravelTime(\n path: readonly Point[],\n personality: Personality,\n speed: Speed,\n rng: Rng,\n): number {\n let distance = 0;\n for (let i = 1; i < path.length; i++) {\n const prev = path[i - 1];\n const curr = path[i];\n if (!prev || !curr) continue;\n distance += Math.hypot(curr.x - prev.x, curr.y - prev.y);\n }\n\n const baseTime = (distance / 1000) * personality.mouse.travelTimeMs;\n const jitterMag = baseTime * personality.mouse.travelTimeJitter;\n const jitter = rng.nextFloat(-jitterMag, jitterMag);\n const total = (baseTime + jitter) * personality.speed * speedModeFactor(speed);\n return Math.max(0, total);\n}\n\n/**\n * Dwell time in ms for a single pause.\n *\n * base = meanMs\n * jitter = base * jitterFraction * rand[-1, 1]\n * total = (base + jitter) * Personality.speed * speedModeFactor\n *\n * Returns 0 for zero/negative inputs so callers can skip the sleep entirely.\n */\nfunction computeDwellTime(\n meanMs: number,\n jitter: number,\n personality: Personality,\n speed: Speed,\n rng: Rng,\n): number {\n if (meanMs <= 0) return 0;\n const jitterMag = meanMs * jitter;\n const offset = rng.nextFloat(-jitterMag, jitterMag);\n return Math.max(0, (meanMs + offset) * personality.speed * speedModeFactor(speed));\n}\n\nfunction speedModeFactor(speed: Speed): number {\n switch (speed) {\n case 'fast':\n return 0.5;\n case 'instant':\n return 0;\n default:\n return 1;\n }\n}\n\nfunction describeTarget(target: Locator | string): string {\n return typeof target === 'string' ? target : (target.toString?.() ?? 'locator');\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return value < min ? min : value > max ? max : value;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import {\n type ActionResult,\n createRng,\n type HumanAction,\n type HumanPlugin,\n type Personality,\n type PersonalityConfig,\n type PluginContext,\n type Point,\n resolvePersonality,\n} from '@humanjs/core';\nimport type { Locator, Page } from 'playwright';\nimport { executeClick } from './mouse';\n\nexport type {\n ActionResult,\n ActionType,\n BezierPathOptions,\n DwellProfile,\n HumanAction,\n HumanizePathOptions,\n HumanPlugin,\n KnownActionType,\n MouseProfile,\n Personality,\n PersonalityConfig,\n PersonalityExtension,\n PluginContext,\n Point,\n PresetName,\n ReadingProfile,\n Rng,\n TypingProfile,\n} from '@humanjs/core';\n// Re-exports of the public core API so consumers have one import surface.\nexport {\n applyMicroJitter,\n applyVelocityProfile,\n bezierPath,\n blend,\n careful,\n createRng,\n distracted,\n fast,\n humanizePath,\n precise,\n resolvePersonality,\n} from '@humanjs/core';\n\n/**\n * How fast the humanized session runs.\n * - `'human'` — full humanization (default)\n * - `'fast'` — humanized but accelerated\n * - `'instant'` — bypass all humanization, straight Playwright\n */\nexport type Speed = 'fast' | 'human' | 'instant';\n\n/** Options for {@link createHuman}. */\nexport interface CreateHumanOptions {\n /** Personality preset, extension, or fully built personality. Defaults to `'careful'`. */\n readonly personality?: PersonalityConfig;\n /** Seed for the session's PRNG. Same seed produces identical trajectories. */\n readonly seed?: number | string;\n /** Speed mode. Defaults to `'human'`. */\n readonly speed?: Speed;\n /** Plugins installed on this session, invoked in registration order. */\n readonly plugins?: readonly HumanPlugin[];\n /**\n * Starting cursor position used as the origin of the first humanized path.\n * Defaults to `{ x: 0, y: 0 }`. Set this if you've already moved the cursor\n * (e.g. via `page.mouse.move`) before creating the session, so the first\n * click's path starts from the correct location.\n */\n readonly initialMousePosition?: Point;\n}\n\n/** A humanized Playwright session bound to a single `Page`. */\nexport interface Human {\n /** The resolved personality this session is using. */\n readonly personality: Personality;\n /** The speed mode this session was created with. */\n readonly speed: Speed;\n /**\n * Navigate to `url`. Plugins observe the action via `'goto'`; the underlying\n * `page.goto(url)` is awaited unchanged.\n */\n goto(url: string): Promise<void>;\n /**\n * Move the mouse along a humanized Bezier path to `target` and click.\n *\n * `target` accepts either a Playwright-compatible selector string (e.g.\n * `'button:has-text(\"Buy now\")'`) or a built `Locator`. The click point\n * inside the element is Gaussian-distributed around the center.\n *\n * In `speed: 'instant'`, all humanization is skipped and Playwright's\n * native `locator.click()` is used directly.\n */\n click(target: Locator | string): Promise<void>;\n}\n\n/**\n * Creates a humanized session bound to a Playwright `Page`.\n *\n * @example\n * ```ts\n * import { chromium } from 'playwright';\n * import { createHuman } from '@humanjs/playwright';\n *\n * const browser = await chromium.launch();\n * const page = await browser.newPage();\n *\n * const human = await createHuman(page, {\n * personality: 'careful',\n * seed: 'session-42',\n * });\n *\n * await human.goto('https://example.com');\n * ```\n */\nexport async function createHuman(page: Page, options: CreateHumanOptions = {}): Promise<Human> {\n const personality = resolvePersonality(options.personality ?? 'careful');\n const rng = createRng(options.seed);\n const speed = options.speed ?? 'human';\n const plugins = options.plugins ?? [];\n\n const context: PluginContext = { personality, rng };\n for (const plugin of plugins) {\n await plugin.install?.(context);\n }\n\n async function performAction<T>(action: HumanAction, actionFn: () => Promise<T>): Promise<T> {\n for (const plugin of plugins) {\n await plugin.beforeAction?.(action);\n }\n const startedAt = Date.now();\n try {\n const value = await actionFn();\n const result: ActionResult = {\n type: action.type,\n durationMs: Date.now() - startedAt,\n };\n for (const plugin of plugins) {\n await plugin.afterAction?.(action, result);\n }\n return value;\n } catch (error) {\n for (const plugin of plugins) {\n await plugin.onError?.(action, error);\n }\n throw error;\n }\n }\n\n let lastMousePosition: Point = options.initialMousePosition ?? { x: 0, y: 0 };\n\n return {\n personality,\n speed,\n async goto(url) {\n await performAction({ type: 'goto', params: { url } }, async () => {\n await page.goto(url);\n });\n },\n async click(target) {\n const description = typeof target === 'string' ? target : (target.toString?.() ?? 'locator');\n await performAction({ type: 'click', params: { target: description } }, async () => {\n await executeClick(target, {\n page,\n personality,\n rng,\n speed,\n getMousePosition: () => lastMousePosition,\n setMousePosition: (point) => {\n lastMousePosition = point;\n },\n });\n });\n },\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanjs/playwright",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Humanize Playwright sessions for AI agents, QA tests, and demos.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"humanjs",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"node": ">=20"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@humanjs/core": "0.
|
|
49
|
+
"@humanjs/core": "0.2.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"playwright": ">=1.40.0"
|