arn-browser 0.0.4 → 0.0.5

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.
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Bezier curve calculator and human-like trajectory generator
3
+ * Ported from Python HumanCursor library
4
+ */
5
+
6
+ import { easeOutQuad } from './tweening.js';
7
+
8
+ /**
9
+ * Calculates the binomial coefficient "n choose k"
10
+ * @param {number} n
11
+ * @param {number} k
12
+ * @returns {number}
13
+ */
14
+ function binomial(n, k) {
15
+ let result = 1;
16
+ for (let i = 1; i <= k; i++) {
17
+ result = result * (n - i + 1) / i;
18
+ }
19
+ return result;
20
+ }
21
+
22
+ /**
23
+ * Calculate the i-th component of a Bernstein polynomial of degree n
24
+ * @param {number} x - Point on curve (0 to 1)
25
+ * @param {number} i - Index
26
+ * @param {number} n - Degree
27
+ * @returns {number}
28
+ */
29
+ function bernsteinPolynomialPoint(x, i, n) {
30
+ return binomial(n, i) * Math.pow(x, i) * Math.pow(1 - x, n - i);
31
+ }
32
+
33
+ /**
34
+ * Returns a function that calculates a point on the Bezier curve
35
+ * @param {Array<[number, number]>} points - Control points
36
+ * @returns {function(number): [number, number]}
37
+ */
38
+ function bernsteinPolynomial(points) {
39
+ return function (t) {
40
+ const n = points.length - 1;
41
+ let x = 0;
42
+ let y = 0;
43
+ for (let i = 0; i <= n; i++) {
44
+ const bern = bernsteinPolynomialPoint(t, i, n);
45
+ x += points[i][0] * bern;
46
+ y += points[i][1] * bern;
47
+ }
48
+ return [x, y];
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Calculate n points on a Bezier curve
54
+ * @param {number} n - Number of points to generate
55
+ * @param {Array<[number, number]>} points - Control points
56
+ * @returns {Array<[number, number]>}
57
+ */
58
+ function calculatePointsInCurve(n, points) {
59
+ const curvePoints = [];
60
+ const getBezierPoint = bernsteinPolynomial(points);
61
+ for (let i = 0; i < n; i++) {
62
+ const t = i / (n - 1);
63
+ curvePoints.push(getBezierPoint(t));
64
+ }
65
+ return curvePoints;
66
+ }
67
+
68
+ /**
69
+ * Check if a value is numeric
70
+ * @param {*} val
71
+ * @returns {boolean}
72
+ */
73
+ function isNumeric(val) {
74
+ return typeof val === 'number' && !isNaN(val);
75
+ }
76
+
77
+ /**
78
+ * Check if list contains valid points
79
+ * @param {Array} list
80
+ * @returns {boolean}
81
+ */
82
+ function isListOfPoints(list) {
83
+ if (!Array.isArray(list)) return false;
84
+ return list.every(p =>
85
+ Array.isArray(p) &&
86
+ p.length === 2 &&
87
+ isNumeric(p[0]) &&
88
+ isNumeric(p[1])
89
+ );
90
+ }
91
+
92
+ /**
93
+ * Random integer between min and max (inclusive)
94
+ * @param {number} min
95
+ * @param {number} max
96
+ * @returns {number}
97
+ */
98
+ function randInt(min, max) {
99
+ return Math.floor(Math.random() * (max - min + 1)) + min;
100
+ }
101
+
102
+ /**
103
+ * Random float with normal distribution
104
+ * @param {number} mean
105
+ * @param {number} stdDev
106
+ * @returns {number}
107
+ */
108
+ function randomNormal(mean, stdDev) {
109
+ // Box-Muller transform
110
+ const u1 = Math.random();
111
+ const u2 = Math.random();
112
+ const z0 = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
113
+ return z0 * stdDev + mean;
114
+ }
115
+
116
+ /**
117
+ * Generates human-like mouse trajectory using Bezier curves
118
+ */
119
+ export class HumanizeMouseTrajectory {
120
+ /**
121
+ * @param {[number, number]} fromPoint - Starting point [x, y]
122
+ * @param {[number, number]} toPoint - Ending point [x, y]
123
+ * @param {Object} options - Curve generation options
124
+ */
125
+ constructor(fromPoint, toPoint, options = {}) {
126
+ this.fromPoint = fromPoint;
127
+ this.toPoint = toPoint;
128
+ this.points = this.generateCurve(options);
129
+ }
130
+
131
+ /**
132
+ * Generates the curve based on options
133
+ * @param {Object} options
134
+ * @returns {Array<[number, number]>}
135
+ */
136
+ generateCurve(options) {
137
+ const offsetBoundaryX = options.offsetBoundaryX ?? 80;
138
+ const offsetBoundaryY = options.offsetBoundaryY ?? 80;
139
+ const leftBoundary = (options.leftBoundary ?? Math.min(this.fromPoint[0], this.toPoint[0])) - offsetBoundaryX;
140
+ const rightBoundary = (options.rightBoundary ?? Math.max(this.fromPoint[0], this.toPoint[0])) + offsetBoundaryX;
141
+ const downBoundary = (options.downBoundary ?? Math.min(this.fromPoint[1], this.toPoint[1])) - offsetBoundaryY;
142
+ const upBoundary = (options.upBoundary ?? Math.max(this.fromPoint[1], this.toPoint[1])) + offsetBoundaryY;
143
+ const knotsCount = options.knotsCount ?? 2;
144
+ const distortionMean = options.distortionMean ?? 1;
145
+ const distortionStdDev = options.distortionStdDev ?? 1;
146
+ const distortionFrequency = options.distortionFrequency ?? 0.5;
147
+ const tween = options.tweening ?? easeOutQuad;
148
+ const targetPoints = options.targetPoints ?? 100;
149
+
150
+ const internalKnots = this.generateInternalKnots(
151
+ leftBoundary, rightBoundary, downBoundary, upBoundary, knotsCount
152
+ );
153
+ let points = this.generatePoints(internalKnots);
154
+ points = this.distortPoints(points, distortionMean, distortionStdDev, distortionFrequency);
155
+ points = this.tweenPoints(points, tween, targetPoints);
156
+ return points;
157
+ }
158
+
159
+ /**
160
+ * Generates random internal knots for the curve
161
+ * @param {number} lBoundary
162
+ * @param {number} rBoundary
163
+ * @param {number} dBoundary
164
+ * @param {number} uBoundary
165
+ * @param {number} knotsCount
166
+ * @returns {Array<[number, number]>}
167
+ */
168
+ generateInternalKnots(lBoundary, rBoundary, dBoundary, uBoundary, knotsCount) {
169
+ if (knotsCount < 0) knotsCount = 0;
170
+ if (lBoundary > rBoundary) {
171
+ throw new Error('leftBoundary must be less than or equal to rightBoundary');
172
+ }
173
+ if (dBoundary > uBoundary) {
174
+ throw new Error('downBoundary must be less than or equal to upperBoundary');
175
+ }
176
+
177
+ const knots = [];
178
+ for (let i = 0; i < knotsCount; i++) {
179
+ const x = randInt(Math.floor(lBoundary), Math.floor(rBoundary));
180
+ const y = randInt(Math.floor(dBoundary), Math.floor(uBoundary));
181
+ knots.push([x, y]);
182
+ }
183
+ return knots;
184
+ }
185
+
186
+ /**
187
+ * Generates points from Bezier curve
188
+ * @param {Array<[number, number]>} knots
189
+ * @returns {Array<[number, number]>}
190
+ */
191
+ generatePoints(knots) {
192
+ const midPtsCnt = Math.max(
193
+ Math.abs(this.fromPoint[0] - this.toPoint[0]),
194
+ Math.abs(this.fromPoint[1] - this.toPoint[1]),
195
+ 2
196
+ );
197
+ const allKnots = [this.fromPoint, ...knots, this.toPoint];
198
+ return calculatePointsInCurve(Math.floor(midPtsCnt), allKnots);
199
+ }
200
+
201
+ /**
202
+ * Distorts points to add human-like imperfections
203
+ * @param {Array<[number, number]>} points
204
+ * @param {number} distortionMean
205
+ * @param {number} distortionStdDev
206
+ * @param {number} distortionFrequency
207
+ * @returns {Array<[number, number]>}
208
+ */
209
+ distortPoints(points, distortionMean, distortionStdDev, distortionFrequency) {
210
+ if (distortionFrequency < 0 || distortionFrequency > 1) {
211
+ throw new Error('distortionFrequency must be in range [0, 1]');
212
+ }
213
+
214
+ const distorted = [points[0]];
215
+ for (let i = 1; i < points.length - 1; i++) {
216
+ const [x, y] = points[i];
217
+ const delta = Math.random() < distortionFrequency
218
+ ? randomNormal(distortionMean, distortionStdDev)
219
+ : 0;
220
+ distorted.push([x, y + delta]);
221
+ }
222
+ distorted.push(points[points.length - 1]);
223
+ return distorted;
224
+ }
225
+
226
+ /**
227
+ * Applies tweening to points
228
+ * @param {Array<[number, number]>} points
229
+ * @param {function(number): number} tween
230
+ * @param {number} targetPoints
231
+ * @returns {Array<[number, number]>}
232
+ */
233
+ tweenPoints(points, tween, targetPoints) {
234
+ if (targetPoints < 2) {
235
+ throw new Error('targetPoints must be >= 2');
236
+ }
237
+
238
+ const result = [];
239
+ for (let i = 0; i < targetPoints; i++) {
240
+ const t = i / (targetPoints - 1);
241
+ const index = Math.floor(tween(t) * (points.length - 1));
242
+ result.push(points[index]);
243
+ }
244
+ return result;
245
+ }
246
+ }
247
+
248
+ export { calculatePointsInCurve, bernsteinPolynomial };
@@ -0,0 +1,154 @@
1
+ /**
2
+ * TypeScript declarations for human-cursor-playwright
3
+ */
4
+
5
+ import type { Page, Locator, Response, PageScreenshotOptions, WaitForURLOptions } from 'playwright';
6
+
7
+ // ============= Tweening Functions =============
8
+
9
+ export type EasingFunction = (n: number) => number;
10
+
11
+ export function linear(n: number): number;
12
+ export function easeOutQuad(n: number): number;
13
+ export function easeInQuad(n: number): number;
14
+ export function easeInOutQuad(n: number): number;
15
+ export function easeOutCubic(n: number): number;
16
+ export function easeInCubic(n: number): number;
17
+ export function easeInOutCubic(n: number): number;
18
+ export function easeOutQuart(n: number): number;
19
+ export function easeInQuart(n: number): number;
20
+ export function easeInOutQuart(n: number): number;
21
+ export function easeOutQuint(n: number): number;
22
+ export function easeInQuint(n: number): number;
23
+ export function easeInOutQuint(n: number): number;
24
+ export function easeOutSine(n: number): number;
25
+ export function easeInSine(n: number): number;
26
+ export function easeInOutSine(n: number): number;
27
+ export function easeOutExpo(n: number): number;
28
+ export function easeInExpo(n: number): number;
29
+ export function easeInOutExpo(n: number): number;
30
+ export function easeOutCirc(n: number): number;
31
+ export function easeInCirc(n: number): number;
32
+ export function easeInOutCirc(n: number): number;
33
+
34
+ export const easingFunctions: Record<string, EasingFunction>;
35
+
36
+ // ============= Bezier Curve =============
37
+
38
+ export interface CurveOptions {
39
+ offsetBoundaryX?: number;
40
+ offsetBoundaryY?: number;
41
+ knotsCount?: number;
42
+ distortionMean?: number;
43
+ distortionStdDev?: number;
44
+ distortionFrequency?: number;
45
+ tweening?: EasingFunction;
46
+ targetPoints?: number;
47
+ }
48
+
49
+ export class HumanizeMouseTrajectory {
50
+ fromPoint: [number, number];
51
+ toPoint: [number, number];
52
+ points: Array<[number, number]>;
53
+
54
+ constructor(fromPoint: [number, number], toPoint: [number, number], options?: CurveOptions);
55
+ }
56
+
57
+ // ============= HumanLocator =============
58
+
59
+ export interface HumanClickOptions {
60
+ clickCount?: number;
61
+ button?: 'left' | 'right' | 'middle';
62
+ delay?: number;
63
+ position?: { x: number; y: number };
64
+ steady?: boolean;
65
+ moveSpeed?: number;
66
+ }
67
+
68
+ export interface HumanFillOptions extends HumanClickOptions {
69
+ minDelay?: number;
70
+ maxDelay?: number;
71
+ }
72
+
73
+ /**
74
+ * Locator with human-like cursor movement
75
+ * All Playwright Locator methods work, with click/fill/type using human cursor
76
+ */
77
+ export interface HumanLocator extends Locator {
78
+ click(options?: HumanClickOptions): Promise<void>;
79
+ dblclick(options?: HumanClickOptions): Promise<void>;
80
+ fill(value: string, options?: HumanFillOptions): Promise<void>;
81
+ type(text: string, options?: HumanFillOptions): Promise<void>;
82
+ hover(options?: HumanClickOptions): Promise<void>;
83
+ press(key: string, options?: HumanClickOptions): Promise<void>;
84
+
85
+ // Chainable methods return HumanLocator
86
+ filter(options: { has?: Locator; hasNot?: Locator; hasText?: string | RegExp }): HumanLocator;
87
+ first(): HumanLocator;
88
+ last(): HumanLocator;
89
+ nth(index: number): HumanLocator;
90
+ locator(selector: string): HumanLocator;
91
+ getByText(text: string | RegExp, options?: { exact?: boolean }): HumanLocator;
92
+ getByRole(role: string, options?: { name?: string | RegExp; exact?: boolean }): HumanLocator;
93
+ getByLabel(text: string | RegExp, options?: { exact?: boolean }): HumanLocator;
94
+ getByPlaceholder(text: string | RegExp, options?: { exact?: boolean }): HumanLocator;
95
+ getByTestId(testId: string | RegExp): HumanLocator;
96
+ }
97
+
98
+ // ============= HumanCursor Options =============
99
+
100
+ export interface CreateCursorOptions {
101
+ /** Enable/disable human-like cursor movement (default: true) */
102
+ humanize?: boolean;
103
+ /** Maximum movement time in seconds (default: 1.5) */
104
+ maxTime?: number;
105
+ /** Minimum movement time in seconds (default: 0.5) */
106
+ minTime?: number;
107
+ /** Auto-show cursor indicator after goto (default: true) */
108
+ showCursor?: boolean;
109
+ }
110
+
111
+ // ============= HumanPage =============
112
+
113
+ /**
114
+ * Page with human-like cursor movement
115
+ * All Playwright Page methods work, with locator methods returning HumanLocators
116
+ */
117
+ export interface HumanPage extends Page {
118
+ // Override locator methods to return HumanLocator
119
+ locator(selector: string): HumanLocator;
120
+ getByText(text: string | RegExp, options?: { exact?: boolean }): HumanLocator;
121
+ getByRole(role: string, options?: { name?: string | RegExp; exact?: boolean }): HumanLocator;
122
+ getByLabel(text: string | RegExp, options?: { exact?: boolean }): HumanLocator;
123
+ getByPlaceholder(text: string | RegExp, options?: { exact?: boolean }): HumanLocator;
124
+ getByAltText(text: string | RegExp, options?: { exact?: boolean }): HumanLocator;
125
+ getByTitle(text: string | RegExp, options?: { exact?: boolean }): HumanLocator;
126
+ getByTestId(testId: string | RegExp): HumanLocator;
127
+
128
+ // Additional cursor utilities
129
+ showCursor(): Promise<void>;
130
+ originCoordinates: [number, number];
131
+ setPosition(x: number, y: number): void;
132
+ }
133
+
134
+ // ============= Main Export =============
135
+
136
+ /**
137
+ * Create a cursor that acts as a drop-in replacement for Page
138
+ * All Page methods work, but locator actions (click, fill, type, hover) use human cursor
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * const cursor = createCursor(page, { showCursor: true });
143
+ * await cursor.goto('https://example.com');
144
+ * await cursor.locator('button').click(); // Uses human cursor!
145
+ * ```
146
+ */
147
+ export function createCursor(page: Page, options?: CreateCursorOptions): HumanPage;
148
+
149
+ /**
150
+ * @deprecated Use createCursor() instead
151
+ */
152
+ export class HumanCursor {
153
+ constructor(page: Page, options?: CreateCursorOptions);
154
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * human-cursor-playwright
3
+ * Human-like cursor movements for Playwright
4
+ */
5
+
6
+ export { HumanCursor, createCursor } from './HumanCursor.js';
7
+ export { HumanizeMouseTrajectory, calculatePointsInCurve, bernsteinPolynomial } from './bezier.js';
8
+ export * from './tweening.js';
9
+ export { generateRandomCurveParameters, calculateAbsoluteOffset } from './randomizer.js';
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Random curve parameter generation
3
+ * Ported from Python HumanCursor library
4
+ */
5
+
6
+ import * as tweening from './tweening.js';
7
+
8
+ /**
9
+ * Random integer between min and max (inclusive)
10
+ * @param {number} min
11
+ * @param {number} max
12
+ * @returns {number}
13
+ */
14
+ function randInt(min, max) {
15
+ return Math.floor(Math.random() * (max - min + 1)) + min;
16
+ }
17
+
18
+ /**
19
+ * Randomly select from array
20
+ * @template T
21
+ * @param {T[]} arr
22
+ * @returns {T}
23
+ */
24
+ function randomChoice(arr) {
25
+ return arr[Math.floor(Math.random() * arr.length)];
26
+ }
27
+
28
+ /**
29
+ * Weighted random selection
30
+ * @template T
31
+ * @param {T[]} choices
32
+ * @param {number[]} weights
33
+ * @returns {T}
34
+ */
35
+ function weightedChoice(choices, weights) {
36
+ const totalWeight = weights.reduce((a, b) => a + b, 0);
37
+ let random = Math.random() * totalWeight;
38
+ for (let i = 0; i < choices.length; i++) {
39
+ random -= weights[i];
40
+ if (random <= 0) return choices[i];
41
+ }
42
+ return choices[choices.length - 1];
43
+ }
44
+
45
+ /**
46
+ * Calculate exact pixel offsets from relative position values
47
+ * @param {{ width: number, height: number }} size - Element dimensions
48
+ * @param {[number, number]} relativePosition - [x, y] percentages (0 to 1)
49
+ * @returns {[number, number]} Absolute pixel offsets
50
+ */
51
+ export function calculateAbsoluteOffset(size, relativePosition) {
52
+ const xFinal = size.width * relativePosition[0];
53
+ const yFinal = size.height * relativePosition[1];
54
+ return [Math.floor(xFinal), Math.floor(yFinal)];
55
+ }
56
+
57
+ /**
58
+ * List of easing functions for random selection
59
+ */
60
+ const tweenOptions = [
61
+ tweening.easeOutExpo,
62
+ tweening.easeInOutQuint,
63
+ tweening.easeInOutSine,
64
+ tweening.easeInOutQuart,
65
+ tweening.easeInOutExpo,
66
+ tweening.easeInOutCubic,
67
+ tweening.easeInOutCirc,
68
+ tweening.linear,
69
+ tweening.easeOutSine,
70
+ tweening.easeOutQuart,
71
+ tweening.easeOutQuint,
72
+ tweening.easeOutCubic,
73
+ tweening.easeOutCirc,
74
+ ];
75
+
76
+ /**
77
+ * Generate random parameters for human-like curve
78
+ * @param {{ width: number, height: number }} viewportSize - Browser viewport size
79
+ * @param {[number, number]} preOrigin - Starting point
80
+ * @param {[number, number]} postDestination - Ending point
81
+ * @returns {Object} Curve parameters
82
+ */
83
+ export function generateRandomCurveParameters(viewportSize, preOrigin, postDestination) {
84
+ const viewportWidth = viewportSize.width;
85
+ const viewportHeight = viewportSize.height;
86
+
87
+ const minWidth = viewportWidth * 0.15;
88
+ const maxWidth = viewportWidth * 0.85;
89
+ const minHeight = viewportHeight * 0.15;
90
+ const maxHeight = viewportHeight * 0.85;
91
+
92
+ const tween = randomChoice(tweenOptions);
93
+
94
+ let offsetBoundaryX = randomChoice(
95
+ weightedChoice(
96
+ [[20, 45], [45, 75], [75, 100]],
97
+ [0.2, 0.65, 0.15]
98
+ ).map(v => randInt(v, v))
99
+ ) || randInt(45, 75);
100
+
101
+ let offsetBoundaryY = randomChoice(
102
+ weightedChoice(
103
+ [[20, 45], [45, 75], [75, 100]],
104
+ [0.2, 0.65, 0.15]
105
+ ).map(v => randInt(v, v))
106
+ ) || randInt(45, 75);
107
+
108
+ let knotsCount = weightedChoice(
109
+ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
110
+ [0.15, 0.36, 0.17, 0.12, 0.08, 0.04, 0.03, 0.02, 0.015, 0.005]
111
+ );
112
+
113
+ const distortionMean = randInt(80, 110) / 100;
114
+ const distortionStdDev = randInt(85, 110) / 100;
115
+ const distortionFrequency = randInt(25, 70) / 100;
116
+
117
+ const targetPoints = randomChoice(
118
+ weightedChoice(
119
+ [[35, 45], [45, 60], [60, 80]],
120
+ [0.53, 0.32, 0.15]
121
+ ).map(range => randInt(range[0], range[1]))
122
+ ) || randInt(45, 60);
123
+
124
+ // Reduce curve complexity if near viewport edges
125
+ const isOriginNearEdge =
126
+ preOrigin[0] < minWidth || preOrigin[0] > maxWidth ||
127
+ preOrigin[1] < minHeight || preOrigin[1] > maxHeight;
128
+
129
+ const isDestNearEdge =
130
+ postDestination[0] < minWidth || postDestination[0] > maxWidth ||
131
+ postDestination[1] < minHeight || postDestination[1] > maxHeight;
132
+
133
+ if (isOriginNearEdge || isDestNearEdge) {
134
+ offsetBoundaryX = 1;
135
+ offsetBoundaryY = 1;
136
+ knotsCount = 1;
137
+ }
138
+
139
+ return {
140
+ offsetBoundaryX,
141
+ offsetBoundaryY,
142
+ knotsCount,
143
+ distortionMean,
144
+ distortionStdDev,
145
+ distortionFrequency,
146
+ tween,
147
+ targetPoints
148
+ };
149
+ }