feat(js): add types to humanized method options (#205)

Replace `any` with proper TypeScript types on all humanized method options
(Playwright, Puppeteer, ElementHandle, Frame). Support flat per-call config
overrides alongside existing `human_config` style. Internalize `humanIdle`
duration computation with backward-compatible overloads.

Co-authored-by: Eternal <chasezou09@gmail.com>
This commit is contained in:
Eternal
2026-05-10 23:50:01 +02:00
committed by CloakHQ
parent 114b3c826b
commit 80d9f7c14e
6 changed files with 518 additions and 302 deletions
+83 -34
View File
@@ -319,7 +319,11 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
} }
// ==== goto ==== // ==== goto ====
const humanGoto = async (url: string, options?: any) => { const humanGoto = async (url: string, options?: {
referer?: string;
timeout?: number;
waitUntil?: 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2';
}) => {
const response = await originals.goto(url, options); const response = await originals.goto(url, options);
stealth.invalidate(); stealth.invalidate();
patchFrames(page, cfg, cursor, raw, rawKb, originals, stealth); patchFrames(page, cfg, cursor, raw, rawKb, originals, stealth);
@@ -327,11 +331,18 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
}; };
// ==== click (with clickCount support for dblclick) ==== // ==== click (with clickCount support for dblclick) ====
const humanClickFn = async (selector: string, options?: any) => { const humanClickFn = async (selector: string, options?: Partial<HumanConfig> & ({
button?: 'left' | 'right' | 'middle' | 'back' | 'forward';
clickCount?: number;
count?: number;
delay?: number;
human_config?: Partial<HumanConfig>;
timeout?: number;
})) => {
await ensureCursorInit(); await ensureCursorInit();
const callCfg = mergeConfig(cfg, options?.human_config); const callCfg = mergeConfig(cfg, options?.human_config ?? options);
if (callCfg.idle_between_actions) { if (callCfg.idle_between_actions) {
await humanIdle(raw, rand(callCfg.idle_between_duration[0], callCfg.idle_between_duration[1]), cursor.x, cursor.y, callCfg); await humanIdle(raw, cursor.x, cursor.y, callCfg);
} }
const { box, cursorX, cursorY } = await scrollToElement(page, raw, selector, cursor.x, cursor.y, callCfg, options?.timeout); const { box, cursorX, cursorY } = await scrollToElement(page, raw, selector, cursor.x, cursor.y, callCfg, options?.timeout);
cursor.x = cursorX; cursor.x = cursorX;
@@ -355,11 +366,11 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
}; };
// ==== hover ==== // ==== hover ====
const humanHoverFn = async (selector: string, options?: any) => { const humanHoverFn = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
await ensureCursorInit(); await ensureCursorInit();
const callCfg = mergeConfig(cfg, options?.human_config); const callCfg = mergeConfig(cfg, options?.human_config ?? options);
if (callCfg.idle_between_actions) { if (callCfg.idle_between_actions) {
await humanIdle(raw, rand(callCfg.idle_between_duration[0], callCfg.idle_between_duration[1]), cursor.x, cursor.y, callCfg); await humanIdle(raw, cursor.x, cursor.y, callCfg);
} }
const { box, cursorX, cursorY } = await scrollToElement(page, raw, selector, cursor.x, cursor.y, callCfg, options?.timeout); const { box, cursorX, cursorY } = await scrollToElement(page, raw, selector, cursor.x, cursor.y, callCfg, options?.timeout);
cursor.x = cursorX; cursor.x = cursorX;
@@ -371,8 +382,12 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
}; };
// ==== type ==== // ==== type ====
const humanTypeFn = async (selector: string, text: string, options?: any) => { const humanTypeFn = async (selector: string, text: string, options?: Partial<HumanConfig> & ({
const callCfg = mergeConfig(cfg, options?.human_config); delay?: number;
human_config?: Partial<HumanConfig>;
timeout?: number;
})) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
await sleep(randRange(callCfg.field_switch_delay)); await sleep(randRange(callCfg.field_switch_delay));
await humanClickFn(selector, options); await humanClickFn(selector, options);
await sleep(rand(100, 250)); await sleep(rand(100, 250));
@@ -395,7 +410,7 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
}; };
// ==== tap ==== // ==== tap ====
const humanTapFn = async (selector: string, options?: any) => { const humanTapFn = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
await humanClickFn(selector, options); await humanClickFn(selector, options);
}; };
@@ -413,14 +428,19 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
// ============================================================ // ============================================================
// Mouse patches // Mouse patches
// ============================================================ // ============================================================
page.mouse.move = async (x: number, y: number, options?: any) => { page.mouse.move = async (x: number, y: number, options?: { steps?: number }) => {
await ensureCursorInit(); await ensureCursorInit();
await humanMove(raw, cursor.x, cursor.y, x, y, cfg); await humanMove(raw, cursor.x, cursor.y, x, y, cfg);
cursor.x = x; cursor.x = x;
cursor.y = y; cursor.y = y;
}; };
page.mouse.click = async (x: number, y: number, options?: any) => { page.mouse.click = async (x: number, y: number, options?: {
button?: 'left' | 'right' | 'middle' | 'back' | 'forward';
clickCount?: number;
count?: number;
delay?: number;
}) => {
await ensureCursorInit(); await ensureCursorInit();
await humanMove(raw, cursor.x, cursor.y, x, y, cfg); await humanMove(raw, cursor.x, cursor.y, x, y, cfg);
cursor.x = x; cursor.x = x;
@@ -455,7 +475,7 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
(page.mouse as any).dragAndDrop = async ( (page.mouse as any).dragAndDrop = async (
start: { x: number; y: number }, start: { x: number; y: number },
target: { x: number; y: number }, target: { x: number; y: number },
options?: any, options?: { delay?: number },
) => { ) => {
await ensureCursorInit(); await ensureCursorInit();
await humanMove(raw, cursor.x, cursor.y, start.x, start.y, cfg); await humanMove(raw, cursor.x, cursor.y, start.x, start.y, cfg);
@@ -475,12 +495,12 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
// ============================================================ // ============================================================
// Keyboard patches // Keyboard patches
// ============================================================ // ============================================================
page.keyboard.type = async (text: string, options?: any) => { page.keyboard.type = async (text: string, options?: { delay?: number }) => {
const cdp = await ensureCdp(); const cdp = await ensureCdp();
await humanType(page, rawKb, text, cfg, cdp); await humanType(page, rawKb, text, cfg, cdp);
}; };
page.keyboard.press = async (key: any, options?: any) => { page.keyboard.press = async (key: any, options?: { delay?: number }) => {
await sleep(rand(20, 60)); await sleep(rand(20, 60));
await originals.keyboardDown(key as any); await originals.keyboardDown(key as any);
await sleep(randRange(cfg.key_hold)); await sleep(randRange(cfg.key_hold));
@@ -551,7 +571,11 @@ function patchElementHandle(
return els; return els;
}; };
(page as any).waitForSelector = async (selector: string, options?: any) => { (page as any).waitForSelector = async (selector: string, options?: {
hidden?: boolean;
timeout?: number;
visible?: boolean;
}) => {
const el = await origWaitForSelector(selector, options); const el = await origWaitForSelector(selector, options);
if (el) patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth); if (el) patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth);
return el; return el;
@@ -603,14 +627,18 @@ function patchSingleElementHandle(
return children; return children;
}; };
(el as any).waitForSelector = async (selector: string, options?: any) => { (el as any).waitForSelector = async (selector: string, options?: {
hidden?: boolean;
timeout?: number;
visible?: boolean;
}) => {
const child = await origElWaitForSelector(selector, options); const child = await origElWaitForSelector(selector, options);
if (child) patchSingleElementHandle(child, page, cfg, cursor, raw, rawKb, originals, stealth); if (child) patchSingleElementHandle(child, page, cfg, cursor, raw, rawKb, originals, stealth);
return child; return child;
}; };
// --- Helper: get box and move cursor. Accepts a per-call ``callCfg`` // --- Helper: get box and move cursor. Accepts a per-call ``callCfg``
// so type/fill overrides like ``el.type(text, { human_config: {...} })`` // so type/fill overrides like ``el.type(text, { typing_delay: 30 })``
// carry through to mouse timing for that single call. Also scrolls into // carry through to mouse timing for that single call. Also scrolls into
// view first so off-screen elements work (#129, #172 follow-up). // view first so off-screen elements work (#129, #172 follow-up).
const moveToElement = async (callCfg: HumanConfig = cfg) => { const moveToElement = async (callCfg: HumanConfig = cfg) => {
@@ -633,7 +661,7 @@ function patchSingleElementHandle(
const target = clickTarget(box, isInp, callCfg); const target = clickTarget(box, isInp, callCfg);
if (callCfg.idle_between_actions) { if (callCfg.idle_between_actions) {
await humanIdle(raw, rand(callCfg.idle_between_duration[0], callCfg.idle_between_duration[1]), cursor.x, cursor.y, callCfg); await humanIdle(raw, cursor.x, cursor.y, callCfg);
} }
await humanMove(raw, cursor.x, cursor.y, target.x, target.y, callCfg); await humanMove(raw, cursor.x, cursor.y, target.x, target.y, callCfg);
@@ -643,8 +671,14 @@ function patchSingleElementHandle(
}; };
// --- el.click() --- // --- el.click() ---
(el as any).click = async (options?: any) => { (el as any).click = async (options?: Partial<HumanConfig> & ({
const callCfg = mergeConfig(cfg, options?.human_config); button?: 'left' | 'right' | 'middle' | 'back' | 'forward';
clickCount?: number;
count?: number;
delay?: number;
human_config?: Partial<HumanConfig>;
})) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
const info = await moveToElement(callCfg); const info = await moveToElement(callCfg);
if (!info) return origElClick(options); if (!info) return origElClick(options);
@@ -667,8 +701,8 @@ function patchSingleElementHandle(
}; };
// --- el.type() --- // --- el.type() ---
(el as any).type = async (text: string, options?: any) => { (el as any).type = async (text: string, options?: Partial<HumanConfig> & ({ delay?: number; human_config?: Partial<HumanConfig> })) => {
const callCfg = mergeConfig(cfg, options?.human_config); const callCfg = mergeConfig(cfg, options?.human_config ?? options);
const info = await moveToElement(callCfg); const info = await moveToElement(callCfg);
if (!info) return origElType(text, options); if (!info) return origElType(text, options);
await humanClick(raw, info.isInp, callCfg); await humanClick(raw, info.isInp, callCfg);
@@ -684,8 +718,8 @@ function patchSingleElementHandle(
// page.click(). Only patched when the underlying ElementHandle exposes // page.click(). Only patched when the underlying ElementHandle exposes
// ``scrollIntoView`` (Puppeteer v22+). // ``scrollIntoView`` (Puppeteer v22+).
if (origElScrollIntoView) { if (origElScrollIntoView) {
(el as any).scrollIntoView = async (options?: any) => { (el as any).scrollIntoView = async (options?: Partial<HumanConfig> & { human_config?: Partial<HumanConfig> }) => {
const callCfg = mergeConfig(cfg, options?.human_config); const callCfg = mergeConfig(cfg, options?.human_config ?? options);
await (page as any)._ensureCursorInit(); await (page as any)._ensureCursorInit();
try { try {
const { cursorX, cursorY } = await humanScrollIntoView( const { cursorX, cursorY } = await humanScrollIntoView(
@@ -703,7 +737,7 @@ function patchSingleElementHandle(
// --- el.press() --- // --- el.press() ---
if (origElPress) { if (origElPress) {
(el as any).press = async (key: string, options?: any) => { (el as any).press = async (key: string, options?: { delay?: number }) => {
await sleep(rand(20, 60)); await sleep(rand(20, 60));
await originals.keyboardDown(key as any); await originals.keyboardDown(key as any);
await sleep(randRange(cfg.key_hold)); await sleep(randRange(cfg.key_hold));
@@ -742,7 +776,7 @@ function patchSingleElementHandle(
// --- el.drop() --- // --- el.drop() ---
if (origElDrop) { if (origElDrop) {
(el as any).drop = async (draggable: ElementHandle, options?: any) => { (el as any).drop = async (draggable: ElementHandle, options?: { delay?: number }) => {
const srcBox = await draggable.boundingBox(); const srcBox = await draggable.boundingBox();
const tgtBox = await el.boundingBox(); const tgtBox = await el.boundingBox();
@@ -772,7 +806,7 @@ function patchSingleElementHandle(
// --- el.dragAndDrop() --- // --- el.dragAndDrop() ---
if (origElDragAndDrop) { if (origElDragAndDrop) {
(el as any).dragAndDrop = async (targetEl: ElementHandle, options?: any) => { (el as any).dragAndDrop = async (targetEl: ElementHandle, options?: { delay?: number }) => {
const srcBox = await el.boundingBox(); const srcBox = await el.boundingBox();
const tgtBox = await targetEl.boundingBox(); const tgtBox = await targetEl.boundingBox();
@@ -836,15 +870,26 @@ function patchSingleFrame(
const origFrameSelect = frame.select.bind(frame); const origFrameSelect = frame.select.bind(frame);
(frame as any).click = async (selector: string, options?: any) => { (frame as any).click = async (selector: string, options?: Partial<HumanConfig> & ({
button?: 'left' | 'right' | 'middle' | 'back' | 'forward';
clickCount?: number;
count?: number;
delay?: number;
human_config?: Partial<HumanConfig>;
timeout?: number;
})) => {
await (page as any).click(selector, options); await (page as any).click(selector, options);
}; };
(frame as any).hover = async (selector: string, options?: any) => { (frame as any).hover = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
await (page as any).hover(selector, options); await (page as any).hover(selector, options);
}; };
(frame as any).type = async (selector: string, text: string, options?: any) => { (frame as any).type = async (selector: string, text: string, options?: Partial<HumanConfig> & ({
delay?: number;
human_config?: Partial<HumanConfig>;
timeout?: number;
})) => {
await (page as any).type(selector, text, options); await (page as any).type(selector, text, options);
}; };
@@ -858,7 +903,7 @@ function patchSingleFrame(
await (page as any).focus(selector); await (page as any).focus(selector);
}; };
(frame as any).tap = async (selector: string, options?: any) => { (frame as any).tap = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
await (page as any).click(selector, options); await (page as any).click(selector, options);
}; };
@@ -881,7 +926,11 @@ function patchSingleFrame(
return els; return els;
}; };
(frame as any).waitForSelector = async (selector: string, options?: any) => { (frame as any).waitForSelector = async (selector: string, options?: {
hidden?: boolean;
timeout?: number;
visible?: boolean;
}) => {
const el = await origFrameWaitForSelector(selector, options); const el = await origFrameWaitForSelector(selector, options);
if (el) patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth); if (el) patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth);
return el; return el;
@@ -927,7 +976,7 @@ export function patchBrowser(browser: Browser, cfg: HumanConfig): void {
for (const methodName of ['createBrowserContext', 'createIncognitoBrowserContext'] as const) { for (const methodName of ['createBrowserContext', 'createIncognitoBrowserContext'] as const) {
if (typeof (browser as any)[methodName] === 'function') { if (typeof (browser as any)[methodName] === 'function') {
const origCreateContext = (browser as any)[methodName].bind(browser); const origCreateContext = (browser as any)[methodName].bind(browser);
(browser as any)[methodName] = async (options?: any) => { (browser as any)[methodName] = async (options?: Parameters<typeof origCreateContext>[0]) => {
const context: BrowserContext = await origCreateContext(options); const context: BrowserContext = await origCreateContext(options);
const origCtxNewPage = context.newPage.bind(context); const origCtxNewPage = context.newPage.bind(context);
+107 -27
View File
@@ -125,16 +125,21 @@ export function patchSingleElementHandle(
return children; return children;
}; };
(el as any).waitForSelector = async (selector: string, options?: any) => { (el as any).waitForSelector = async (selector: string, options?: {
const child = await origElWaitForSelector(selector, options); state?: 'attached' | 'detached' | 'visible' | 'hidden';
strict?: boolean;
timeout?: number;
}) => {
const child = await origElWaitForSelector(selector, options ?? {});
if (child) patchSingleElementHandle(child, page, cfg, cursor, raw, rawKb, originals, stealth); if (child) patchSingleElementHandle(child, page, cfg, cursor, raw, rawKb, originals, stealth);
return child; return child;
}; };
// --- Helper: get bounding box and move cursor to element --- // --- Helper: get bounding box and move cursor to element ---
// Accepts a per-call ``callCfg`` so type/fill overrides like // Accepts a per-call ``callCfg`` so type/fill overrides like
// ``el.type(text, { human_config: { typing_delay: 30 } })`` carry through to // ``el.type(text, { human_config: { typing_delay: 30 } })`` or
// mouse movement & idle timing for that single call. // ``el.type(text, { typing_delay: 30 })`` carry through to mouse movement
// & idle timing for that single call.
// Also scrolls the element into view first so off-screen elements work // Also scrolls the element into view first so off-screen elements work
// (#129, #172 follow-up): otherwise boundingBox() returns null and we'd // (#129, #172 follow-up): otherwise boundingBox() returns null and we'd
// silently fall back to the unpatched native method. // silently fall back to the unpatched native method.
@@ -164,7 +169,7 @@ export function patchSingleElementHandle(
const target = clickTarget(box, isInp, callCfg); const target = clickTarget(box, isInp, callCfg);
if (callCfg.idle_between_actions) { if (callCfg.idle_between_actions) {
await humanIdle(raw, rand(callCfg.idle_between_duration[0], callCfg.idle_between_duration[1]), cursor.x, cursor.y, callCfg); await humanIdle(raw, cursor.x, cursor.y, callCfg);
} }
await humanMove(raw, cursor.x, cursor.y, target.x, target.y, callCfg); await humanMove(raw, cursor.x, cursor.y, target.x, target.y, callCfg);
@@ -174,16 +179,37 @@ export function patchSingleElementHandle(
}; };
// --- el.click() --- // --- el.click() ---
(el as any).click = async (options?: any) => { (el as any).click = async (options?: Partial<HumanConfig> & ({
const callCfg = mergeConfig(cfg, options?.human_config); button?: 'left' | 'right' | 'middle';
clickCount?: number;
delay?: number;
force?: boolean;
human_config?: Partial<HumanConfig>;
modifiers?: Array<'Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift'>;
noWaitAfter?: boolean;
position?: { x: number; y: number };
timeout?: number;
trial?: boolean;
})) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
const info = await moveToElement(callCfg); const info = await moveToElement(callCfg);
if (!info) return origElClick(options); if (!info) return origElClick(options);
await humanClick(raw, info.isInp, callCfg); await humanClick(raw, info.isInp, callCfg);
}; };
// --- el.dblclick() --- // --- el.dblclick() ---
(el as any).dblclick = async (options?: any) => { (el as any).dblclick = async (options?: Partial<HumanConfig> & ({
const callCfg = mergeConfig(cfg, options?.human_config); button?: 'left' | 'right' | 'middle';
delay?: number;
force?: boolean;
human_config?: Partial<HumanConfig>;
modifiers?: Array<'Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift'>;
noWaitAfter?: boolean;
position?: { x: number; y: number };
timeout?: number;
trial?: boolean;
})) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
const info = await moveToElement(callCfg); const info = await moveToElement(callCfg);
if (!info) return origElDblclick(options); if (!info) return origElDblclick(options);
await raw.down({ clickCount: 2 }); await raw.down({ clickCount: 2 });
@@ -192,16 +218,28 @@ export function patchSingleElementHandle(
}; };
// --- el.hover() --- // --- el.hover() ---
(el as any).hover = async (options?: any) => { (el as any).hover = async (options?: Partial<HumanConfig> & ({
const callCfg = mergeConfig(cfg, options?.human_config); force?: boolean;
human_config?: Partial<HumanConfig>;
modifiers?: Array<'Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift'>;
position?: { x: number; y: number };
timeout?: number;
trial?: boolean;
})) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
const info = await moveToElement(callCfg); const info = await moveToElement(callCfg);
if (!info) return origElHover(options); if (!info) return origElHover(options);
// Just move — no click // Just move — no click
}; };
// --- el.type() --- // --- el.type() ---
(el as any).type = async (text: string, options?: any) => { (el as any).type = async (text: string, options?: Partial<HumanConfig> & ({
const callCfg = mergeConfig(cfg, options?.human_config); delay?: number;
human_config?: Partial<HumanConfig>;
noWaitAfter?: boolean;
timeout?: number;
})) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
const info = await moveToElement(callCfg); const info = await moveToElement(callCfg);
if (!info) return origElType(text, options); if (!info) return origElType(text, options);
await humanClick(raw, info.isInp, callCfg); await humanClick(raw, info.isInp, callCfg);
@@ -212,8 +250,13 @@ export function patchSingleElementHandle(
}; };
// --- el.fill() --- // --- el.fill() ---
(el as any).fill = async (value: string, options?: any) => { (el as any).fill = async (value: string, options?: Partial<HumanConfig> & ({
const callCfg = mergeConfig(cfg, options?.human_config); force?: boolean;
human_config?: Partial<HumanConfig>;
noWaitAfter?: boolean;
timeout?: number;
})) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
const info = await moveToElement(callCfg); const info = await moveToElement(callCfg);
if (!info) return origElFill(value, options); if (!info) return origElFill(value, options);
await humanClick(raw, info.isInp, callCfg); await humanClick(raw, info.isInp, callCfg);
@@ -229,7 +272,7 @@ export function patchSingleElementHandle(
}; };
// --- el.press() --- // --- el.press() ---
(el as any).press = async (key: string, options?: any) => { (el as any).press = async (key: string, options?: { delay?: number; noWaitAfter?: boolean; timeout?: number }) => {
await sleep(rand(20, 60)); await sleep(rand(20, 60));
await originals.keyboardDown(key); await originals.keyboardDown(key);
await sleep(randRange(cfg.key_hold)); await sleep(randRange(cfg.key_hold));
@@ -237,7 +280,11 @@ export function patchSingleElementHandle(
}; };
// --- el.selectOption() --- // --- el.selectOption() ---
(el as any).selectOption = async (values: any, options?: any) => { (el as any).selectOption = async (values: any, options?: {
force?: boolean;
noWaitAfter?: boolean;
timeout?: number;
}) => {
const info = await moveToElement(); const info = await moveToElement();
if (!info) return origElSelectOption(values, options); if (!info) return origElSelectOption(values, options);
await humanClick(raw, false, cfg); await humanClick(raw, false, cfg);
@@ -246,7 +293,13 @@ export function patchSingleElementHandle(
}; };
// --- el.check() --- // --- el.check() ---
(el as any).check = async (options?: any) => { (el as any).check = async (options?: {
force?: boolean;
noWaitAfter?: boolean;
position?: { x: number; y: number };
timeout?: number;
trial?: boolean;
}) => {
try { try {
const checked = await el.isChecked(); const checked = await el.isChecked();
if (checked) return; // Already checked if (checked) return; // Already checked
@@ -257,7 +310,13 @@ export function patchSingleElementHandle(
}; };
// --- el.uncheck() --- // --- el.uncheck() ---
(el as any).uncheck = async (options?: any) => { (el as any).uncheck = async (options?: {
force?: boolean;
noWaitAfter?: boolean;
position?: { x: number; y: number };
timeout?: number;
trial?: boolean;
}) => {
try { try {
const checked = await el.isChecked(); const checked = await el.isChecked();
if (!checked) return; // Already unchecked if (!checked) return; // Already unchecked
@@ -269,7 +328,13 @@ export function patchSingleElementHandle(
// --- el.setChecked() --- // --- el.setChecked() ---
if (origElSetChecked) { if (origElSetChecked) {
(el as any).setChecked = async (checked: boolean, options?: any) => { (el as any).setChecked = async (checked: boolean, options?: {
force?: boolean;
noWaitAfter?: boolean;
position?: { x: number; y: number };
timeout?: number;
trial?: boolean;
}) => {
try { try {
const current = await el.isChecked(); const current = await el.isChecked();
if (current === checked) return; if (current === checked) return;
@@ -281,7 +346,14 @@ export function patchSingleElementHandle(
} }
// --- el.tap() --- // --- el.tap() ---
(el as any).tap = async (options?: any) => { (el as any).tap = async (options?: {
force?: boolean;
modifiers?: Array<'Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift'>;
noWaitAfter?: boolean;
position?: { x: number; y: number };
timeout?: number;
trial?: boolean;
}) => {
const info = await moveToElement(); const info = await moveToElement();
if (!info) return origElTap(options); if (!info) return origElTap(options);
await humanClick(raw, info.isInp, cfg); await humanClick(raw, info.isInp, cfg);
@@ -302,8 +374,8 @@ export function patchSingleElementHandle(
// wheel sequence used by page.click() etc. Falls back to the native // wheel sequence used by page.click() etc. Falls back to the native
// method if the element is detached or scrolling fails. // method if the element is detached or scrolling fails.
if (origElScrollIntoViewIfNeeded) { if (origElScrollIntoViewIfNeeded) {
(el as any).scrollIntoViewIfNeeded = async (options?: any) => { (el as any).scrollIntoViewIfNeeded = async (options?: Partial<HumanConfig> & ({ human_config?: Partial<HumanConfig>; timeout?: number })) => {
const callCfg = mergeConfig(cfg, options?.human_config); const callCfg = mergeConfig(cfg, options?.human_config ?? options);
const ensureCursorInit = (page as any)._ensureCursorInit; const ensureCursorInit = (page as any)._ensureCursorInit;
if (ensureCursorInit) await ensureCursorInit(); if (ensureCursorInit) await ensureCursorInit();
try { try {
@@ -360,8 +432,12 @@ export function patchPageElementHandles(
// Patch page.waitForSelector() // Patch page.waitForSelector()
if (typeof page.waitForSelector === 'function') { if (typeof page.waitForSelector === 'function') {
const origWaitForSelector = page.waitForSelector.bind(page); const origWaitForSelector = page.waitForSelector.bind(page);
(page as any).waitForSelector = async (selector: string, options?: any) => { (page as any).waitForSelector = async (selector: string, options?: {
const el = await origWaitForSelector(selector, options); state?: 'attached' | 'detached' | 'visible' | 'hidden';
strict?: boolean;
timeout?: number;
}) => {
const el = await origWaitForSelector(selector, options ?? {});
if (el) patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth); if (el) patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth);
return el; return el;
}; };
@@ -408,8 +484,12 @@ export function patchFrameElementHandles(
// Patch frame.waitForSelector() // Patch frame.waitForSelector()
if (typeof frame.waitForSelector === 'function') { if (typeof frame.waitForSelector === 'function') {
const origFrameWaitForSelector = frame.waitForSelector.bind(frame); const origFrameWaitForSelector = frame.waitForSelector.bind(frame);
(frame as any).waitForSelector = async (selector: string, options?: any) => { (frame as any).waitForSelector = async (selector: string, options?: {
const el = await origFrameWaitForSelector(selector, options); state?: 'attached' | 'detached' | 'visible' | 'hidden';
strict?: boolean;
timeout?: number;
}) => {
const el = await origFrameWaitForSelector(selector, options ?? {});
if (el) patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth); if (el) patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth);
return el; return el;
}; };
+76 -55
View File
@@ -295,7 +295,11 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
} }
// --- goto (invalidate isolated world on navigation) --- // --- goto (invalidate isolated world on navigation) ---
const humanGoto = async (url: string, options?: any) => { const humanGoto = async (url: string, options?: {
referer?: string;
timeout?: number;
waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' | 'commit';
}) => {
const response = await originals.goto(url, options); const response = await originals.goto(url, options);
stealth.invalidate(); stealth.invalidate();
patchFrames(page, cfg, cursor, raw, rawKb, originals, stealth); patchFrames(page, cfg, cursor, raw, rawKb, originals, stealth);
@@ -303,11 +307,11 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
}; };
// --- click --- // --- click ---
const humanClickFn = async (selector: string, options?: any) => { const humanClickFn = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
await ensureCursorInit(); await ensureCursorInit();
const callCfg = mergeConfig(cfg, options?.human_config); const callCfg = mergeConfig(cfg, options?.human_config ?? options);
if (callCfg.idle_between_actions) { if (callCfg.idle_between_actions) {
await humanIdle(raw, rand(callCfg.idle_between_duration[0], callCfg.idle_between_duration[1]), cursor.x, cursor.y, callCfg); await humanIdle(raw, cursor.x, cursor.y, callCfg);
} }
const { box, cursorX, cursorY } = await scrollToElement(page, raw, selector, cursor.x, cursor.y, callCfg, options?.timeout); const { box, cursorX, cursorY } = await scrollToElement(page, raw, selector, cursor.x, cursor.y, callCfg, options?.timeout);
cursor.x = cursorX; cursor.x = cursorX;
@@ -321,12 +325,13 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
}; };
// --- dblclick --- // --- dblclick ---
const humanDblclickFn = async (selector: string, options?: any) => { const humanDblclickFn = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
await ensureCursorInit(); await ensureCursorInit();
const callCfg = mergeConfig(cfg, options?.human_config); const callCfg = mergeConfig(cfg, options?.human_config ?? options);
if (callCfg.idle_between_actions) { if (callCfg.idle_between_actions) {
await humanIdle(raw, rand(callCfg.idle_between_duration[0], callCfg.idle_between_duration[1]), cursor.x, cursor.y, callCfg); await humanIdle(raw, cursor.x, cursor.y, callCfg);
} }
const { box, cursorX, cursorY } = await scrollToElement(page, raw, selector, cursor.x, cursor.y, callCfg, options?.timeout); const { box, cursorX, cursorY } = await scrollToElement(page, raw, selector, cursor.x, cursor.y, callCfg, options?.timeout);
cursor.x = cursorX; cursor.x = cursorX;
cursor.y = cursorY; cursor.y = cursorY;
@@ -341,11 +346,11 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
}; };
// --- hover --- // --- hover ---
const humanHoverFn = async (selector: string, options?: any) => { const humanHoverFn = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
await ensureCursorInit(); await ensureCursorInit();
const callCfg = mergeConfig(cfg, options?.human_config); const callCfg = mergeConfig(cfg, options?.human_config ?? options);
if (callCfg.idle_between_actions) { if (callCfg.idle_between_actions) {
await humanIdle(raw, rand(callCfg.idle_between_duration[0], callCfg.idle_between_duration[1]), cursor.x, cursor.y, callCfg); await humanIdle(raw, cursor.x, cursor.y, callCfg);
} }
const { box, cursorX, cursorY } = await scrollToElement(page, raw, selector, cursor.x, cursor.y, callCfg, options?.timeout); const { box, cursorX, cursorY } = await scrollToElement(page, raw, selector, cursor.x, cursor.y, callCfg, options?.timeout);
cursor.x = cursorX; cursor.x = cursorX;
@@ -357,8 +362,8 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
}; };
// --- type --- // --- type ---
const humanTypeFn = async (selector: string, text: string, options?: any) => { const humanTypeFn = async (selector: string, text: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const callCfg = mergeConfig(cfg, options?.human_config); const callCfg = mergeConfig(cfg, options?.human_config ?? options);
await sleep(randRange(callCfg.field_switch_delay)); await sleep(randRange(callCfg.field_switch_delay));
await humanClickFn(selector, options); await humanClickFn(selector, options);
await sleep(rand(100, 250)); await sleep(rand(100, 250));
@@ -367,8 +372,8 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
}; };
// --- fill (clears existing content first) --- // --- fill (clears existing content first) ---
const humanFillFn = async (selector: string, value: string, options?: any) => { const humanFillFn = async (selector: string, value: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const callCfg = mergeConfig(cfg, options?.human_config); const callCfg = mergeConfig(cfg, options?.human_config ?? options);
await sleep(randRange(callCfg.field_switch_delay)); await sleep(randRange(callCfg.field_switch_delay));
await humanClickFn(selector, options); await humanClickFn(selector, options);
await sleep(rand(100, 250)); await sleep(rand(100, 250));
@@ -381,9 +386,9 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
}; };
// --- clear --- // --- clear ---
const humanClearFn = async (selector: string, options?: any) => { const humanClearFn = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
if (!await isSelectorFocused(stealth, page, selector)) { if (!await isSelectorFocused(stealth, page, selector)) {
await humanClickFn(selector); await humanClickFn(selector, options);
} }
await sleep(rand(50, 150)); await sleep(rand(50, 150));
await originals.keyboardPress(SELECT_ALL); await originals.keyboardPress(SELECT_ALL);
@@ -392,46 +397,48 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
}; };
// --- check --- // --- check ---
const humanCheckFn = async (selector: string, options?: any) => { const humanCheckFn = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
if (cfg.idle_between_actions) { const callCfg = mergeConfig(cfg, options?.human_config ?? options);
await humanIdle(raw, rand(cfg.idle_between_duration[0], cfg.idle_between_duration[1]), cursor.x, cursor.y, cfg); if (callCfg.idle_between_actions) {
await humanIdle(raw, cursor.x, cursor.y, callCfg);
} }
const checked = await originals.isChecked(selector).catch(() => false); const checked = await originals.isChecked(selector).catch(() => false);
if (!checked) { if (!checked) {
await humanClickFn(selector); await humanClickFn(selector, options);
} }
}; };
// --- uncheck --- // --- uncheck ---
const humanUncheckFn = async (selector: string, options?: any) => { const humanUncheckFn = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
if (cfg.idle_between_actions) { const callCfg = mergeConfig(cfg, options?.human_config ?? options);
await humanIdle(raw, rand(cfg.idle_between_duration[0], cfg.idle_between_duration[1]), cursor.x, cursor.y, cfg); if (callCfg.idle_between_actions) {
await humanIdle(raw, cursor.x, cursor.y, callCfg);
} }
const checked = await originals.isChecked(selector).catch(() => true); const checked = await originals.isChecked(selector).catch(() => true);
if (checked) { if (checked) {
await humanClickFn(selector); await humanClickFn(selector, options);
} }
}; };
// --- selectOption --- // --- selectOption ---
const humanSelectOptionFn = async (selector: string, values: any, options?: any) => { const humanSelectOptionFn = async (selector: string, values: any, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
await humanHoverFn(selector); await humanHoverFn(selector, options);
await sleep(rand(100, 300)); await sleep(rand(100, 300));
return originals.selectOption(selector, values, options); return originals.selectOption(selector, values, options);
}; };
// --- press (checks focus first — avoids redundant mouse moves) --- // --- press (checks focus first — avoids redundant mouse moves) ---
const humanPressFn = async (selector: string, key: string, options?: any) => { const humanPressFn = async (selector: string, key: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
if (!await isSelectorFocused(stealth, page, selector)) { if (!await isSelectorFocused(stealth, page, selector)) {
await humanClickFn(selector); await humanClickFn(selector, options);
} }
await sleep(rand(50, 150)); await sleep(rand(50, 150));
await originals.keyboardPress(key); await originals.keyboardPress(key);
}; };
// --- pressSequentially --- // --- pressSequentially ---
const humanPressSequentiallyFn = async (selector: string, text: string, options?: any) => { const humanPressSequentiallyFn = async (selector: string, text: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const callCfg = mergeConfig(cfg, options?.human_config); const callCfg = mergeConfig(cfg, options?.human_config ?? options);
if (!await isSelectorFocused(stealth, page, selector)) { if (!await isSelectorFocused(stealth, page, selector)) {
await humanClickFn(selector, options); await humanClickFn(selector, options);
} }
@@ -441,7 +448,7 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
}; };
// --- tap --- // --- tap ---
const humanTapFn = async (selector: string, options?: any) => { const humanTapFn = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
await humanClickFn(selector, options); await humanClickFn(selector, options);
}; };
@@ -461,14 +468,20 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
(page as any).clear = humanClearFn; (page as any).clear = humanClearFn;
// --- mouse patches --- // --- mouse patches ---
page.mouse.move = async (x: number, y: number, options?: any) => { page.mouse.move = async (x: number, y: number, options?: {
steps?: number;
}) => {
await ensureCursorInit(); await ensureCursorInit();
await humanMove(raw, cursor.x, cursor.y, x, y, cfg); await humanMove(raw, cursor.x, cursor.y, x, y, cfg);
cursor.x = x; cursor.x = x;
cursor.y = y; cursor.y = y;
}; };
page.mouse.click = async (x: number, y: number, options?: any) => { page.mouse.click = async (x: number, y: number, options?: {
button?: 'left' | 'right' | 'middle';
clickCount?: number;
delay?: number;
}) => {
await ensureCursorInit(); await ensureCursorInit();
await humanMove(raw, cursor.x, cursor.y, x, y, cfg); await humanMove(raw, cursor.x, cursor.y, x, y, cfg);
cursor.x = x; cursor.x = x;
@@ -477,7 +490,7 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
}; };
// --- keyboard patches --- // --- keyboard patches ---
page.keyboard.type = async (text: string, options?: any) => { page.keyboard.type = async (text: string, options?: { delay?: number }) => {
const cdp = await ensureCdp(); const cdp = await ensureCdp();
await humanType(page, rawKb, text, cfg, cdp); await humanType(page, rawKb, text, cfg, cdp);
}; };
@@ -580,10 +593,10 @@ function patchSingleFrame(
const origFrameTap = (frame as any).tap?.bind(frame); const origFrameTap = (frame as any).tap?.bind(frame);
const origFrameDragAndDrop = frame.dragAndDrop.bind(frame); const origFrameDragAndDrop = frame.dragAndDrop.bind(frame);
const moveToFrameSelector = async (selector: string, options?: any, inputBias = false) => { const moveToFrameSelector = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> }), inputBias = false) => {
const callCfg = mergeConfig(cfg, options?.human_config); const callCfg = mergeConfig(cfg, options?.human_config ?? options);
if (callCfg.idle_between_actions) { if (callCfg.idle_between_actions) {
await humanIdle(raw, rand(callCfg.idle_between_duration[0], callCfg.idle_between_duration[1]), cursor.x, cursor.y, callCfg); await humanIdle(raw, cursor.x, cursor.y, callCfg);
} }
const locator = firstFrameLocator(frame, selector); const locator = firstFrameLocator(frame, selector);
@@ -601,7 +614,7 @@ function patchSingleFrame(
return { callCfg, isInput }; return { callCfg, isInput };
}; };
const frameClick = async (selector: string, options?: any) => { const frameClick = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const moved = await moveToFrameSelector(selector, options); const moved = await moveToFrameSelector(selector, options);
if (!moved) return origFrameClick(selector, options); if (!moved) return origFrameClick(selector, options);
await humanClick(raw, moved.isInput, moved.callCfg); await humanClick(raw, moved.isInput, moved.callCfg);
@@ -609,14 +622,14 @@ function patchSingleFrame(
const getFrameCdp = async () => stealth.getCdpSession().catch(() => null); const getFrameCdp = async () => stealth.getCdpSession().catch(() => null);
const frameHover = async (selector: string, options?: any) => { const frameHover = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const moved = await moveToFrameSelector(selector, options, false); const moved = await moveToFrameSelector(selector, options, false);
if (!moved) return origFrameHover(selector, options); if (!moved) return origFrameHover(selector, options);
}; };
(frame as any).click = frameClick; (frame as any).click = frameClick;
(frame as any).dblclick = async (selector: string, options?: any) => { (frame as any).dblclick = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const moved = await moveToFrameSelector(selector, options); const moved = await moveToFrameSelector(selector, options);
if (!moved) return origFrameDblclick(selector, options); if (!moved) return origFrameDblclick(selector, options);
await raw.down({ clickCount: 2 }); await raw.down({ clickCount: 2 });
@@ -626,8 +639,8 @@ function patchSingleFrame(
(frame as any).hover = frameHover; (frame as any).hover = frameHover;
(frame as any).type = async (selector: string, text: string, options?: any) => { (frame as any).type = async (selector: string, text: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const callCfg = mergeConfig(cfg, options?.human_config); const callCfg = mergeConfig(cfg, options?.human_config ?? options);
await sleep(randRange(callCfg.field_switch_delay)); await sleep(randRange(callCfg.field_switch_delay));
await frameClick(selector, options); await frameClick(selector, options);
await sleep(rand(100, 250)); await sleep(rand(100, 250));
@@ -635,8 +648,8 @@ function patchSingleFrame(
await humanType(page, rawKb, text, callCfg, cdp).catch(() => origFrameType(selector, text, options)); await humanType(page, rawKb, text, callCfg, cdp).catch(() => origFrameType(selector, text, options));
}; };
(frame as any).fill = async (selector: string, value: string, options?: any) => { (frame as any).fill = async (selector: string, value: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const callCfg = mergeConfig(cfg, options?.human_config); const callCfg = mergeConfig(cfg, options?.human_config ?? options);
await sleep(randRange(callCfg.field_switch_delay)); await sleep(randRange(callCfg.field_switch_delay));
await frameClick(selector, options); await frameClick(selector, options);
await sleep(rand(100, 250)); await sleep(rand(100, 250));
@@ -648,23 +661,23 @@ function patchSingleFrame(
await humanType(page, rawKb, value, callCfg, cdp).catch(() => origFrameFill(selector, value, options)); await humanType(page, rawKb, value, callCfg, cdp).catch(() => origFrameFill(selector, value, options));
}; };
(frame as any).check = async (selector: string, options?: any) => { (frame as any).check = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const checked = await firstFrameLocator(frame, selector).isChecked?.().catch(() => false) ?? false; const checked = await firstFrameLocator(frame, selector).isChecked?.().catch(() => false) ?? false;
if (!checked) await frameClick(selector, options).catch(() => origFrameCheck(selector, options)); if (!checked) await frameClick(selector, options).catch(() => origFrameCheck(selector, options));
}; };
(frame as any).uncheck = async (selector: string, options?: any) => { (frame as any).uncheck = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const checked = await firstFrameLocator(frame, selector).isChecked?.().catch(() => true) ?? true; const checked = await firstFrameLocator(frame, selector).isChecked?.().catch(() => true) ?? true;
if (checked) await frameClick(selector, options).catch(() => origFrameUncheck(selector, options)); if (checked) await frameClick(selector, options).catch(() => origFrameUncheck(selector, options));
}; };
(frame as any).selectOption = async (selector: string, values: any, options?: any) => { (frame as any).selectOption = async (selector: string, values: any, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
await frameHover(selector, options); await frameHover(selector, options);
await sleep(rand(100, 300)); await sleep(rand(100, 300));
return origFrameSelectOption(selector, values, options); return origFrameSelectOption(selector, values, options);
}; };
(frame as any).press = async (selector: string, key: string, options?: any) => { (frame as any).press = async (selector: string, key: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
if (!await isFrameSelectorFocused(frame, selector)) { if (!await isFrameSelectorFocused(frame, selector)) {
await frameClick(selector, options); await frameClick(selector, options);
} }
@@ -672,8 +685,8 @@ function patchSingleFrame(
await originals.keyboardPress(key); await originals.keyboardPress(key);
}; };
(frame as any).pressSequentially = async (selector: string, text: string, options?: any) => { (frame as any).pressSequentially = async (selector: string, text: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const callCfg = mergeConfig(cfg, options?.human_config); const callCfg = mergeConfig(cfg, options?.human_config ?? options);
if (!await isFrameSelectorFocused(frame, selector)) { if (!await isFrameSelectorFocused(frame, selector)) {
await frameClick(selector, options); await frameClick(selector, options);
} }
@@ -682,11 +695,11 @@ function patchSingleFrame(
await humanType(page, rawKb, text, callCfg, cdp).catch(() => origFramePressSequentially?.(selector, text, options)); await humanType(page, rawKb, text, callCfg, cdp).catch(() => origFramePressSequentially?.(selector, text, options));
}; };
(frame as any).tap = async (selector: string, options?: any) => { (frame as any).tap = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
await frameClick(selector, options).catch(() => origFrameTap?.(selector, options)); await frameClick(selector, options).catch(() => origFrameTap?.(selector, options));
}; };
(frame as any).clear = async (selector: string, options?: any) => { (frame as any).clear = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
if (!await isFrameSelectorFocused(frame, selector)) { if (!await isFrameSelectorFocused(frame, selector)) {
await frameClick(selector, options); await frameClick(selector, options);
} }
@@ -696,7 +709,15 @@ function patchSingleFrame(
await originals.keyboardPress('Backspace'); await originals.keyboardPress('Backspace');
}; };
(frame as any).dragAndDrop = async (source: string, target: string, options?: any) => { (frame as any).dragAndDrop = async (source: string, target: string, options?: {
force?: boolean;
noWaitAfter?: boolean;
sourcePosition?: { x: number; y: number };
strict?: boolean;
targetPosition?: { x: number; y: number };
timeout?: number;
trial?: boolean;
}) => {
const srcBox = await firstFrameLocator(frame, source).boundingBox({ timeout: options?.timeout ?? 30000 }).catch(() => null); const srcBox = await firstFrameLocator(frame, source).boundingBox({ timeout: options?.timeout ?? 30000 }).catch(() => null);
const tgtBox = await firstFrameLocator(frame, target).boundingBox({ timeout: options?.timeout ?? 30000 }).catch(() => null); const tgtBox = await firstFrameLocator(frame, target).boundingBox({ timeout: options?.timeout ?? 30000 }).catch(() => null);
@@ -767,14 +788,14 @@ export function patchBrowser(browser: Browser, cfg: HumanConfig): void {
} }
const origNewContext = browser.newContext.bind(browser); const origNewContext = browser.newContext.bind(browser);
(browser as any).newContext = async (options?: any) => { (browser as any).newContext = async (options?: Parameters<typeof origNewContext>[0]) => {
const context = await origNewContext(options); const context = await origNewContext(options);
patchContext(context, cfg); patchContext(context, cfg);
return context; return context;
}; };
const origNewPage = browser.newPage.bind(browser); const origNewPage = browser.newPage.bind(browser);
(browser as any).newPage = async (options?: any) => { (browser as any).newPage = async (options?: Parameters<typeof origNewPage>[0]) => {
const page = await origNewPage(options); const page = await origNewPage(options);
if (!(page as any)._original) { if (!(page as any)._original) {
const ctx = page.context(); const ctx = page.context();
+21 -1
View File
@@ -172,13 +172,33 @@ export async function humanClick(
// Human idle / drift // Human idle / drift
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export async function humanIdle( export function humanIdle(
raw: RawMouse,
cx: number,
cy: number,
cfg: HumanConfig,
): Promise<void>;
export function humanIdle(
raw: RawMouse, raw: RawMouse,
seconds: number, seconds: number,
cx: number, cx: number,
cy: number, cy: number,
cfg: HumanConfig, cfg: HumanConfig,
): Promise<void>;
export async function humanIdle(
raw: RawMouse,
secondsOrCx: number,
cxOrCy: number,
cyOrCfg: number | HumanConfig,
maybeCfg?: HumanConfig,
): Promise<void> { ): Promise<void> {
const hasExplicitSeconds = maybeCfg !== undefined;
const seconds = hasExplicitSeconds
? secondsOrCx
: rand((cyOrCfg as HumanConfig).idle_between_duration[0], (cyOrCfg as HumanConfig).idle_between_duration[1]);
const cx = hasExplicitSeconds ? cxOrCy : secondsOrCx;
const cy = hasExplicitSeconds ? (cyOrCfg as number) : cxOrCy;
const cfg = hasExplicitSeconds ? maybeCfg! : (cyOrCfg as HumanConfig);
const endTime = Date.now() + seconds * 1000; const endTime = Date.now() + seconds * 1000;
let x = cx; let x = cx;
let y = cy; let y = cy;
+6 -6
View File
@@ -204,7 +204,7 @@ describe("humanIdle", () => {
up: vi.fn(async () => { }), up: vi.fn(async () => { }),
wheel: vi.fn(async () => { }), wheel: vi.fn(async () => { }),
}; };
await humanIdle(raw, 10, 100, 100, cfg); await humanIdle(raw, 100, 100, cfg);
expect(raw.move).toHaveBeenCalled(); expect(raw.move).toHaveBeenCalled();
}, 15000); }, 15000);
}); });
@@ -1224,10 +1224,10 @@ describe("page.click(selector, { timeout }) forwards timeout to scroll", () => {
// ========================================================================= // =========================================================================
// Per-call human_config override // Per-call human config override
// ========================================================================= // =========================================================================
describe("page.type / page.fill accept per-call human_config override", () => { describe("page.type / page.fill accept per-call human config override", () => {
it("page.type forwards merged config to humanType", async () => { it("page.type forwards nested human_config to humanType", async () => {
const keyboardMod = await import("../src/human/keyboard.js"); const keyboardMod = await import("../src/human/keyboard.js");
const scrollMod = await import("../src/human/scroll.js"); const scrollMod = await import("../src/human/scroll.js");
const { patchPage } = await import("../src/human/index.js"); const { patchPage } = await import("../src/human/index.js");
@@ -1267,7 +1267,7 @@ describe("page.type / page.fill accept per-call human_config override", () => {
scrollSpy.mockRestore(); scrollSpy.mockRestore();
}, 30000); }, 30000);
it("page.fill forwards merged config to humanType", async () => { it("page.fill forwards flat config to humanType", async () => {
const keyboardMod = await import("../src/human/keyboard.js"); const keyboardMod = await import("../src/human/keyboard.js");
const scrollMod = await import("../src/human/scroll.js"); const scrollMod = await import("../src/human/scroll.js");
const { patchPage } = await import("../src/human/index.js"); const { patchPage } = await import("../src/human/index.js");
@@ -1293,7 +1293,7 @@ describe("page.type / page.fill accept per-call human_config override", () => {
patchPage(page as any, cfg, cursor as any); patchPage(page as any, cfg, cursor as any);
await (page as any).fill("#password", "secret", { await (page as any).fill("#password", "secret", {
human_config: { typing_delay: 150 }, typing_delay: 150,
}); });
expect(captured.typing_delay).toBe(150); expect(captured.typing_delay).toBe(150);
+46
View File
@@ -532,6 +532,52 @@ describe("Puppeteer: non-ASCII text avoids CDP shift path", () => {
}); });
// =========================================================================
// Per-call human config override (Puppeteer page-level)
// =========================================================================
describe("Puppeteer: page.type accepts per-call human config override", () => {
it("page.type forwards merged config to humanType", async () => {
const keyboardMod = await import("../src/human-puppeteer/keyboard.js");
const scrollMod = await import("../src/human-puppeteer/scroll.js");
const cfg = resolveConfig("default", {
idle_between_actions: false,
field_switch_delay: [0, 1],
});
expect(cfg.typing_delay).toBe(70);
let captured: any = null;
const typeSpy = vi.spyOn(keyboardMod, "humanType").mockImplementation(
async (_page, _raw, _text, callCfg) => { captured = callCfg; },
);
const scrollSpy = vi.spyOn(scrollMod, "scrollToElement").mockImplementation(
async (_page, _raw, _sel, cx, cy) => ({
box: { x: 100, y: 100, width: 50, height: 30 },
cursorX: cx,
cursorY: cy,
}),
);
const { patchPage } = await import("../src/human-puppeteer/index.js");
const page = buildMockPage();
const cursor = { x: 100, y: 100, initialized: true };
patchPage(page as any, cfg, cursor as any);
await (page as any).type("#email", "hi", {
typing_delay: 30,
mistype_chance: 0,
});
expect(captured.typing_delay).toBe(30);
expect(captured.mistype_chance).toBe(0);
expect(cfg.typing_delay).toBe(70);
typeSpy.mockRestore();
scrollSpy.mockRestore();
});
});
// ========================================================================= // =========================================================================
// patchPage stealth infrastructure (Puppeteer) // patchPage stealth infrastructure (Puppeteer)
// ========================================================================= // =========================================================================