mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
feat: Add Puppeteer humanize support and fix Playwright humanize gaps (#129)
- Add full Puppeteer humanize implementation (page, frame, element handle patching) - Fix critical Playwright gaps: page.pressSequentially, page.tap, page.clear - Fix frame-level patching: frame.pressSequentially, frame.tap - Add comprehensive stealth tests for Puppeteer - Update SLOW test suite to use correct humanize: true API - Add 4 new tests validating fixed Playwright methods
This commit is contained in:
@@ -0,0 +1,913 @@
|
|||||||
|
/**
|
||||||
|
* Human-like behavioral layer for cloakbrowser — Puppeteer edition.
|
||||||
|
*
|
||||||
|
* Mirrors Playwright humanize architecture, adapted for Puppeteer API.
|
||||||
|
*
|
||||||
|
* Patches ALL native Puppeteer interaction surfaces:
|
||||||
|
*
|
||||||
|
* PAGE-LEVEL:
|
||||||
|
* click (with clickCount support for dblclick), hover, type,
|
||||||
|
* select, focus, tap, goto
|
||||||
|
*
|
||||||
|
* MOUSE:
|
||||||
|
* move, click (with clickCount support for dblclick), wheel,
|
||||||
|
* dragAndDrop
|
||||||
|
*
|
||||||
|
* KEYBOARD:
|
||||||
|
* type, down, up, press, sendCharacter
|
||||||
|
*
|
||||||
|
* FRAME-LEVEL:
|
||||||
|
* click, hover, type, select, focus, tap
|
||||||
|
* + $, $$, waitForSelector (return patched ElementHandles)
|
||||||
|
*
|
||||||
|
* ELEMENTHANDLE-LEVEL (Puppeteer-specific, no Playwright equivalent):
|
||||||
|
* click (with clickCount), hover, type, press, tap, select,
|
||||||
|
* focus, drop, dragAndDrop
|
||||||
|
* + $, $$, waitForSelector (nested elements are also patched)
|
||||||
|
*
|
||||||
|
* BROWSER-LEVEL:
|
||||||
|
* newPage, createBrowserContext / createIncognitoBrowserContext,
|
||||||
|
* targetcreated event
|
||||||
|
*
|
||||||
|
* Stealth-aware:
|
||||||
|
* - isInputElement / isSelectorFocused use CDP Isolated Worlds
|
||||||
|
* - Shift symbol typing uses CDP Input.dispatchKeyEvent (isTrusted=true)
|
||||||
|
* - ElementHandle isInput check uses CDP DOM.describeNode (no JS execution)
|
||||||
|
* - Falls back to page.evaluate only when CDP session is unavailable
|
||||||
|
*
|
||||||
|
* Puppeteer-specific adaptations:
|
||||||
|
* - page.createCDPSession() instead of context.newCDPSession(page)
|
||||||
|
* - page.viewport() instead of page.viewportSize()
|
||||||
|
* - page.$(selector) instead of page.locator(selector)
|
||||||
|
* - keyboard.sendCharacter() mapped via RawKeyboard.insertText
|
||||||
|
* - mouse.wheel({deltaX, deltaY}) object form adapted to (dx, dy)
|
||||||
|
* - page.select() instead of page.selectOption()
|
||||||
|
* - ElementHandle prototype patching (Puppeteer-only)
|
||||||
|
* - No page.dblclick() — Puppeteer uses click({clickCount:2})
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Browser, Page, Frame, CDPSession, ElementHandle, BrowserContext } from 'puppeteer-core';
|
||||||
|
import type { HumanConfig } from '../human/config.js';
|
||||||
|
import { resolveConfig, rand, randRange, sleep } from '../human/config.js';
|
||||||
|
import { RawMouse, RawKeyboard, humanMove, humanClick, clickTarget, humanIdle } from '../human/mouse.js';
|
||||||
|
import { humanType } from './keyboard.js';
|
||||||
|
import { scrollToElement, smoothWheel } from './scroll.js';
|
||||||
|
|
||||||
|
export type { HumanConfig } from '../human/config.js';
|
||||||
|
export { resolveConfig } from '../human/config.js';
|
||||||
|
export { humanMove, humanClick, clickTarget, humanIdle } from '../human/mouse.js';
|
||||||
|
export { humanType } from './keyboard.js';
|
||||||
|
export { scrollToElement } from './scroll.js';
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// CDP Isolated World — stealth DOM evaluation (Puppeteer version)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
class StealthEval {
|
||||||
|
private cdp: CDPSession | null = null;
|
||||||
|
private contextId: number | null = null;
|
||||||
|
private page: Page;
|
||||||
|
|
||||||
|
constructor(page: Page) {
|
||||||
|
this.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureCdp(): Promise<CDPSession> {
|
||||||
|
if (!this.cdp) {
|
||||||
|
this.cdp = await this.page.createCDPSession();
|
||||||
|
}
|
||||||
|
return this.cdp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createWorld(): Promise<number> {
|
||||||
|
const cdp = await this.ensureCdp();
|
||||||
|
const tree = await cdp.send('Page.getFrameTree');
|
||||||
|
const frameId = (tree as any).frameTree.frame.id;
|
||||||
|
const result = await cdp.send('Page.createIsolatedWorld', {
|
||||||
|
frameId,
|
||||||
|
worldName: '',
|
||||||
|
grantUniveralAccess: true,
|
||||||
|
});
|
||||||
|
const ctxId = (result as any).executionContextId;
|
||||||
|
this.contextId = ctxId;
|
||||||
|
return ctxId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async evaluate(expression: string): Promise<any> {
|
||||||
|
if (this.contextId === null) {
|
||||||
|
await this.createWorld();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < 2; attempt++) {
|
||||||
|
try {
|
||||||
|
const cdp = await this.ensureCdp();
|
||||||
|
const result = await cdp.send('Runtime.evaluate', {
|
||||||
|
expression,
|
||||||
|
contextId: this.contextId!,
|
||||||
|
returnByValue: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if ((result as any).exceptionDetails) {
|
||||||
|
if (attempt === 0) {
|
||||||
|
await this.createWorld();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (result as any).result?.value;
|
||||||
|
} catch {
|
||||||
|
if (attempt === 0) {
|
||||||
|
this.contextId = null;
|
||||||
|
try { await this.createWorld(); } catch { return undefined; }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidate(): void {
|
||||||
|
this.contextId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCdpSession(): Promise<CDPSession> {
|
||||||
|
return this.ensureCdp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Cursor state
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
class CursorState {
|
||||||
|
x = 0;
|
||||||
|
y = 0;
|
||||||
|
initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Stealth DOM queries
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
async function isInputElement(
|
||||||
|
stealth: StealthEval | null,
|
||||||
|
page: Page,
|
||||||
|
selector: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (stealth) {
|
||||||
|
try {
|
||||||
|
const escaped = JSON.stringify(selector);
|
||||||
|
const result = await stealth.evaluate(`
|
||||||
|
(() => {
|
||||||
|
const el = document.querySelector(${escaped});
|
||||||
|
if (!el) return false;
|
||||||
|
const tag = el.tagName.toLowerCase();
|
||||||
|
return tag === 'input' || tag === 'textarea'
|
||||||
|
|| el.getAttribute('contenteditable') === 'true';
|
||||||
|
})()
|
||||||
|
`);
|
||||||
|
return !!result;
|
||||||
|
} catch { /* fallthrough */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
return page.evaluate((sel: string) => {
|
||||||
|
const el = document.querySelector(sel);
|
||||||
|
if (!el) return false;
|
||||||
|
const tag = el.tagName.toLowerCase();
|
||||||
|
return tag === 'input' || tag === 'textarea'
|
||||||
|
|| el.getAttribute('contenteditable') === 'true';
|
||||||
|
}, selector).catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isSelectorFocused(
|
||||||
|
stealth: StealthEval | null,
|
||||||
|
page: Page,
|
||||||
|
selector: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (stealth) {
|
||||||
|
try {
|
||||||
|
const escaped = JSON.stringify(selector);
|
||||||
|
const result = await stealth.evaluate(`
|
||||||
|
(() => {
|
||||||
|
const el = document.querySelector(${escaped});
|
||||||
|
return el === document.activeElement;
|
||||||
|
})()
|
||||||
|
`);
|
||||||
|
return !!result;
|
||||||
|
} catch { /* fallthrough */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
return page.evaluate((sel: string) => {
|
||||||
|
const el = document.querySelector(sel);
|
||||||
|
return el === document.activeElement;
|
||||||
|
}, selector).catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Stealth ElementHandle input check — uses CDP DOM.describeNode
|
||||||
|
// instead of el.evaluate() to avoid main-world JS execution.
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
async function isInputElementHandle(
|
||||||
|
stealth: StealthEval | null,
|
||||||
|
el: ElementHandle,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (stealth) {
|
||||||
|
try {
|
||||||
|
const cdp = await stealth.getCdpSession();
|
||||||
|
const remoteObject = (el as any).remoteObject?.();
|
||||||
|
if (remoteObject?.objectId) {
|
||||||
|
const { node } = await cdp.send('DOM.describeNode', {
|
||||||
|
objectId: remoteObject.objectId,
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
const tag = (node?.nodeName || '').toLowerCase();
|
||||||
|
if (tag === 'input' || tag === 'textarea') return true;
|
||||||
|
|
||||||
|
const attrs: string[] = node?.attributes || [];
|
||||||
|
for (let i = 0; i < attrs.length; i += 2) {
|
||||||
|
if (attrs[i] === 'contenteditable' && attrs[i + 1] === 'true') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch { /* fallthrough to el.evaluate */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
return el.evaluate((node: any) => {
|
||||||
|
const tag = node.tagName?.toLowerCase();
|
||||||
|
return tag === 'input' || tag === 'textarea'
|
||||||
|
|| node.getAttribute?.('contenteditable') === 'true';
|
||||||
|
}).catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Page-level patching
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
|
||||||
|
const originals = {
|
||||||
|
click: page.click.bind(page),
|
||||||
|
hover: page.hover.bind(page),
|
||||||
|
type: page.type.bind(page),
|
||||||
|
select: page.select.bind(page),
|
||||||
|
focus: page.focus.bind(page),
|
||||||
|
goto: page.goto.bind(page),
|
||||||
|
tap: page.tap.bind(page),
|
||||||
|
|
||||||
|
mouseMove: page.mouse.move.bind(page.mouse),
|
||||||
|
mouseClick: page.mouse.click.bind(page.mouse),
|
||||||
|
mouseDown: page.mouse.down.bind(page.mouse),
|
||||||
|
mouseUp: page.mouse.up.bind(page.mouse),
|
||||||
|
mouseWheel: (page.mouse as any).wheel?.bind(page.mouse),
|
||||||
|
mouseDragAndDrop: (page.mouse as any).dragAndDrop?.bind(page.mouse),
|
||||||
|
|
||||||
|
keyboardType: page.keyboard.type.bind(page.keyboard),
|
||||||
|
keyboardDown: page.keyboard.down.bind(page.keyboard) as (key: string) => Promise<void>,
|
||||||
|
keyboardUp: page.keyboard.up.bind(page.keyboard) as (key: string) => Promise<void>,
|
||||||
|
keyboardPress: page.keyboard.press.bind(page.keyboard),
|
||||||
|
keyboardSendCharacter: page.keyboard.sendCharacter.bind(page.keyboard),
|
||||||
|
};
|
||||||
|
|
||||||
|
(page as any)._original = originals;
|
||||||
|
(page as any)._humanCfg = cfg;
|
||||||
|
|
||||||
|
const stealth = new StealthEval(page);
|
||||||
|
(page as any)._stealth = stealth;
|
||||||
|
|
||||||
|
let cdpSession: CDPSession | null = null;
|
||||||
|
const ensureCdp = async (): Promise<CDPSession | null> => {
|
||||||
|
if (!cdpSession) {
|
||||||
|
try { cdpSession = await stealth.getCdpSession(); } catch {}
|
||||||
|
}
|
||||||
|
return cdpSession;
|
||||||
|
};
|
||||||
|
|
||||||
|
const raw: RawMouse = {
|
||||||
|
move: originals.mouseMove,
|
||||||
|
down: originals.mouseDown,
|
||||||
|
up: originals.mouseUp,
|
||||||
|
wheel: async (deltaX: number, deltaY: number) => {
|
||||||
|
if (originals.mouseWheel) {
|
||||||
|
await originals.mouseWheel({ deltaX, deltaY });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const rawKb: RawKeyboard = {
|
||||||
|
down: originals.keyboardDown,
|
||||||
|
up: originals.keyboardUp,
|
||||||
|
type: originals.keyboardType,
|
||||||
|
insertText: originals.keyboardSendCharacter,
|
||||||
|
};
|
||||||
|
|
||||||
|
async function ensureCursorInit(): Promise<void> {
|
||||||
|
if (!cursor.initialized) {
|
||||||
|
cursor.x = rand(cfg.initial_cursor_x[0], cfg.initial_cursor_x[1]);
|
||||||
|
cursor.y = rand(cfg.initial_cursor_y[0], cfg.initial_cursor_y[1]);
|
||||||
|
await originals.mouseMove(cursor.x, cursor.y);
|
||||||
|
cursor.initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==== goto ====
|
||||||
|
const humanGoto = async (url: string, options?: any) => {
|
||||||
|
const response = await originals.goto(url, options);
|
||||||
|
stealth.invalidate();
|
||||||
|
patchFrames(page, cfg, cursor, raw, rawKb, originals, stealth);
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==== click (with clickCount support for dblclick) ====
|
||||||
|
const humanClickFn = async (selector: string, options?: any) => {
|
||||||
|
await ensureCursorInit();
|
||||||
|
if (cfg.idle_between_actions) {
|
||||||
|
await humanIdle(raw, rand(cfg.idle_between_duration[0], cfg.idle_between_duration[1]), cursor.x, cursor.y, cfg);
|
||||||
|
}
|
||||||
|
const { box, cursorX, cursorY } = await scrollToElement(page, raw, selector, cursor.x, cursor.y, cfg);
|
||||||
|
cursor.x = cursorX;
|
||||||
|
cursor.y = cursorY;
|
||||||
|
const isInput = await isInputElement(stealth, page, selector);
|
||||||
|
const target = clickTarget(box, isInput, cfg);
|
||||||
|
await humanMove(raw, cursor.x, cursor.y, target.x, target.y, cfg);
|
||||||
|
cursor.x = target.x;
|
||||||
|
cursor.y = target.y;
|
||||||
|
|
||||||
|
const clickCount = options?.clickCount ?? options?.count ?? 1;
|
||||||
|
if (clickCount >= 2) {
|
||||||
|
await humanClick(raw, isInput, cfg);
|
||||||
|
await sleep(rand(40, 90));
|
||||||
|
await raw.down({ clickCount: 2 });
|
||||||
|
await sleep(rand(30, 60));
|
||||||
|
await raw.up({ clickCount: 2 });
|
||||||
|
} else {
|
||||||
|
await humanClick(raw, isInput, cfg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==== hover ====
|
||||||
|
const humanHoverFn = async (selector: string, options?: any) => {
|
||||||
|
await ensureCursorInit();
|
||||||
|
if (cfg.idle_between_actions) {
|
||||||
|
await humanIdle(raw, rand(cfg.idle_between_duration[0], cfg.idle_between_duration[1]), cursor.x, cursor.y, cfg);
|
||||||
|
}
|
||||||
|
const { box, cursorX, cursorY } = await scrollToElement(page, raw, selector, cursor.x, cursor.y, cfg);
|
||||||
|
cursor.x = cursorX;
|
||||||
|
cursor.y = cursorY;
|
||||||
|
const target = clickTarget(box, false, cfg);
|
||||||
|
await humanMove(raw, cursor.x, cursor.y, target.x, target.y, cfg);
|
||||||
|
cursor.x = target.x;
|
||||||
|
cursor.y = target.y;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==== type ====
|
||||||
|
const humanTypeFn = async (selector: string, text: string, options?: any) => {
|
||||||
|
await sleep(randRange(cfg.field_switch_delay));
|
||||||
|
await humanClickFn(selector);
|
||||||
|
await sleep(rand(100, 250));
|
||||||
|
const cdp = await ensureCdp();
|
||||||
|
await humanType(page, rawKb, text, cfg, cdp);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==== select ====
|
||||||
|
const humanSelectFn = async (selector: string, ...values: string[]) => {
|
||||||
|
await humanHoverFn(selector);
|
||||||
|
await sleep(rand(100, 300));
|
||||||
|
return originals.select(selector, ...values);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==== focus ====
|
||||||
|
const humanFocusFn = async (selector: string) => {
|
||||||
|
if (!await isSelectorFocused(stealth, page, selector)) {
|
||||||
|
await humanClickFn(selector);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==== tap ====
|
||||||
|
const humanTapFn = async (selector: string, options?: any) => {
|
||||||
|
await humanClickFn(selector, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Assign page-level patches
|
||||||
|
// ============================================================
|
||||||
|
(page as any).goto = humanGoto;
|
||||||
|
(page as any).click = humanClickFn;
|
||||||
|
(page as any).hover = humanHoverFn;
|
||||||
|
(page as any).type = humanTypeFn;
|
||||||
|
(page as any).select = humanSelectFn;
|
||||||
|
(page as any).focus = humanFocusFn;
|
||||||
|
(page as any).tap = humanTapFn;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Mouse patches
|
||||||
|
// ============================================================
|
||||||
|
page.mouse.move = async (x: number, y: number, options?: any) => {
|
||||||
|
await ensureCursorInit();
|
||||||
|
await humanMove(raw, cursor.x, cursor.y, x, y, cfg);
|
||||||
|
cursor.x = x;
|
||||||
|
cursor.y = y;
|
||||||
|
};
|
||||||
|
|
||||||
|
page.mouse.click = async (x: number, y: number, options?: any) => {
|
||||||
|
await ensureCursorInit();
|
||||||
|
await humanMove(raw, cursor.x, cursor.y, x, y, cfg);
|
||||||
|
cursor.x = x;
|
||||||
|
cursor.y = y;
|
||||||
|
|
||||||
|
const clickCount = options?.clickCount ?? options?.count ?? 1;
|
||||||
|
if (clickCount >= 2) {
|
||||||
|
await humanClick(raw, false, cfg);
|
||||||
|
await sleep(rand(40, 90));
|
||||||
|
await raw.down({ clickCount: 2 });
|
||||||
|
await sleep(rand(30, 60));
|
||||||
|
await raw.up({ clickCount: 2 });
|
||||||
|
} else {
|
||||||
|
await humanClick(raw, false, cfg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (originals.mouseWheel) {
|
||||||
|
(page.mouse as any).wheel = async (options?: { deltaX?: number; deltaY?: number }) => {
|
||||||
|
const dx = options?.deltaX ?? 0;
|
||||||
|
const dy = options?.deltaY ?? 0;
|
||||||
|
if (Math.abs(dy) > 0) {
|
||||||
|
await smoothWheel(raw, dy, cfg, 'y');
|
||||||
|
}
|
||||||
|
if (Math.abs(dx) > 0) {
|
||||||
|
await smoothWheel(raw, dx, cfg, 'x');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originals.mouseDragAndDrop) {
|
||||||
|
(page.mouse as any).dragAndDrop = async (
|
||||||
|
start: { x: number; y: number },
|
||||||
|
target: { x: number; y: number },
|
||||||
|
options?: any,
|
||||||
|
) => {
|
||||||
|
await ensureCursorInit();
|
||||||
|
await humanMove(raw, cursor.x, cursor.y, start.x, start.y, cfg);
|
||||||
|
cursor.x = start.x;
|
||||||
|
cursor.y = start.y;
|
||||||
|
await sleep(rand(100, 200));
|
||||||
|
await originals.mouseDown();
|
||||||
|
await sleep(rand(80, 150));
|
||||||
|
await humanMove(raw, cursor.x, cursor.y, target.x, target.y, cfg);
|
||||||
|
cursor.x = target.x;
|
||||||
|
cursor.y = target.y;
|
||||||
|
await sleep(rand(80, 150));
|
||||||
|
await originals.mouseUp();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Keyboard patches
|
||||||
|
// ============================================================
|
||||||
|
page.keyboard.type = async (text: string, options?: any) => {
|
||||||
|
const cdp = await ensureCdp();
|
||||||
|
await humanType(page, rawKb, text, cfg, cdp);
|
||||||
|
};
|
||||||
|
|
||||||
|
page.keyboard.press = async (key: any, options?: any) => {
|
||||||
|
await sleep(rand(20, 60));
|
||||||
|
await originals.keyboardDown(key as any);
|
||||||
|
await sleep(randRange(cfg.key_hold));
|
||||||
|
await originals.keyboardUp(key as any);
|
||||||
|
};
|
||||||
|
|
||||||
|
page.keyboard.down = async (key: any) => {
|
||||||
|
await sleep(rand(10, 30));
|
||||||
|
await originals.keyboardDown(key as any);
|
||||||
|
};
|
||||||
|
|
||||||
|
page.keyboard.up = async (key: any) => {
|
||||||
|
await sleep(rand(10, 30));
|
||||||
|
await originals.keyboardUp(key as any);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Store helpers for frame/element patching
|
||||||
|
// ============================================================
|
||||||
|
(page as any)._humanCursor = cursor;
|
||||||
|
(page as any)._humanRaw = raw;
|
||||||
|
(page as any)._humanRawKb = rawKb;
|
||||||
|
(page as any)._ensureCursorInit = ensureCursorInit;
|
||||||
|
|
||||||
|
// Initialize cursor
|
||||||
|
cursor.x = rand(cfg.initial_cursor_x[0], cfg.initial_cursor_x[1]);
|
||||||
|
cursor.y = rand(cfg.initial_cursor_y[0], cfg.initial_cursor_y[1]);
|
||||||
|
originals.mouseMove(cursor.x, cursor.y).then(() => {
|
||||||
|
cursor.initialized = true;
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
|
// Patch frames
|
||||||
|
patchFrames(page, cfg, cursor, raw, rawKb, originals, stealth);
|
||||||
|
|
||||||
|
// Patch ElementHandle selectors
|
||||||
|
patchElementHandle(page, cfg, cursor, raw, rawKb, originals, stealth);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ElementHandle patching — PUPPETEER-SPECIFIC
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
function patchElementHandle(
|
||||||
|
page: Page,
|
||||||
|
cfg: HumanConfig,
|
||||||
|
cursor: CursorState,
|
||||||
|
raw: RawMouse,
|
||||||
|
rawKb: RawKeyboard,
|
||||||
|
originals: any,
|
||||||
|
stealth: StealthEval,
|
||||||
|
): void {
|
||||||
|
const orig$ = page.$.bind(page);
|
||||||
|
const orig$$ = page.$$.bind(page);
|
||||||
|
const origWaitForSelector = page.waitForSelector.bind(page);
|
||||||
|
|
||||||
|
(page as any).$ = async (selector: string) => {
|
||||||
|
const el = await orig$(selector);
|
||||||
|
if (el) patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth);
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
|
||||||
|
(page as any).$$ = async (selector: string) => {
|
||||||
|
const els = await orig$$(selector);
|
||||||
|
for (const el of els) {
|
||||||
|
patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth);
|
||||||
|
}
|
||||||
|
return els;
|
||||||
|
};
|
||||||
|
|
||||||
|
(page as any).waitForSelector = async (selector: string, options?: any) => {
|
||||||
|
const el = await origWaitForSelector(selector, options);
|
||||||
|
if (el) patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth);
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchSingleElementHandle(
|
||||||
|
el: ElementHandle,
|
||||||
|
page: Page,
|
||||||
|
cfg: HumanConfig,
|
||||||
|
cursor: CursorState,
|
||||||
|
raw: RawMouse,
|
||||||
|
rawKb: RawKeyboard,
|
||||||
|
originals: any,
|
||||||
|
stealth: StealthEval,
|
||||||
|
): void {
|
||||||
|
if ((el as any)._humanPatched) return;
|
||||||
|
(el as any)._humanPatched = true;
|
||||||
|
|
||||||
|
const origElClick = el.click.bind(el);
|
||||||
|
const origElHover = el.hover.bind(el);
|
||||||
|
const origElType = el.type.bind(el);
|
||||||
|
const origElPress = (el as any).press?.bind(el);
|
||||||
|
const origElTap = (el as any).tap?.bind(el);
|
||||||
|
const origElFocus = (el as any).focus?.bind(el);
|
||||||
|
const origElDragAndDrop = (el as any).dragAndDrop?.bind(el);
|
||||||
|
const origElSelect = (el as any).select?.bind(el);
|
||||||
|
const origElDrop = (el as any).drop?.bind(el);
|
||||||
|
|
||||||
|
// --- Nested selectors ---
|
||||||
|
const origEl$ = el.$.bind(el);
|
||||||
|
const origEl$$ = el.$$.bind(el);
|
||||||
|
const origElWaitForSelector = el.waitForSelector.bind(el);
|
||||||
|
|
||||||
|
(el as any).$ = async (selector: string) => {
|
||||||
|
const child = await origEl$(selector);
|
||||||
|
if (child) patchSingleElementHandle(child, page, cfg, cursor, raw, rawKb, originals, stealth);
|
||||||
|
return child;
|
||||||
|
};
|
||||||
|
|
||||||
|
(el as any).$$ = async (selector: string) => {
|
||||||
|
const children = await origEl$$(selector);
|
||||||
|
for (const child of children) {
|
||||||
|
patchSingleElementHandle(child, page, cfg, cursor, raw, rawKb, originals, stealth);
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
};
|
||||||
|
|
||||||
|
(el as any).waitForSelector = async (selector: string, options?: any) => {
|
||||||
|
const child = await origElWaitForSelector(selector, options);
|
||||||
|
if (child) patchSingleElementHandle(child, page, cfg, cursor, raw, rawKb, originals, stealth);
|
||||||
|
return child;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Helper: get box and move cursor ---
|
||||||
|
const moveToElement = async () => {
|
||||||
|
await (page as any)._ensureCursorInit();
|
||||||
|
const box = await el.boundingBox();
|
||||||
|
if (!box) return null;
|
||||||
|
|
||||||
|
const isInp = await isInputElementHandle(stealth, el);
|
||||||
|
const target = clickTarget(box, isInp, cfg);
|
||||||
|
|
||||||
|
if (cfg.idle_between_actions) {
|
||||||
|
await humanIdle(raw, rand(cfg.idle_between_duration[0], cfg.idle_between_duration[1]), cursor.x, cursor.y, cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
await humanMove(raw, cursor.x, cursor.y, target.x, target.y, cfg);
|
||||||
|
cursor.x = target.x;
|
||||||
|
cursor.y = target.y;
|
||||||
|
return { box, isInp };
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- el.click() ---
|
||||||
|
(el as any).click = async (options?: any) => {
|
||||||
|
const info = await moveToElement();
|
||||||
|
if (!info) return origElClick(options);
|
||||||
|
|
||||||
|
const clickCount = options?.clickCount ?? options?.count ?? 1;
|
||||||
|
if (clickCount >= 2) {
|
||||||
|
await humanClick(raw, info.isInp, cfg);
|
||||||
|
await sleep(rand(40, 90));
|
||||||
|
await raw.down({ clickCount: 2 });
|
||||||
|
await sleep(rand(30, 60));
|
||||||
|
await raw.up({ clickCount: 2 });
|
||||||
|
} else {
|
||||||
|
await humanClick(raw, info.isInp, cfg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- el.hover() ---
|
||||||
|
(el as any).hover = async () => {
|
||||||
|
const info = await moveToElement();
|
||||||
|
if (!info) return origElHover();
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- el.type() ---
|
||||||
|
(el as any).type = async (text: string, options?: any) => {
|
||||||
|
const info = await moveToElement();
|
||||||
|
if (!info) return origElType(text, options);
|
||||||
|
await humanClick(raw, info.isInp, cfg);
|
||||||
|
await sleep(rand(100, 250));
|
||||||
|
const cdp = await stealth.getCdpSession().catch(() => null);
|
||||||
|
await humanType(page, rawKb, text, cfg, cdp);
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- el.press() ---
|
||||||
|
if (origElPress) {
|
||||||
|
(el as any).press = async (key: string, options?: any) => {
|
||||||
|
await sleep(rand(20, 60));
|
||||||
|
await originals.keyboardDown(key as any);
|
||||||
|
await sleep(randRange(cfg.key_hold));
|
||||||
|
await originals.keyboardUp(key as any);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- el.tap() ---
|
||||||
|
if (origElTap) {
|
||||||
|
(el as any).tap = async () => {
|
||||||
|
const info = await moveToElement();
|
||||||
|
if (!info) return origElTap();
|
||||||
|
await humanClick(raw, info.isInp, cfg);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- el.focus() ---
|
||||||
|
if (origElFocus) {
|
||||||
|
(el as any).focus = async () => {
|
||||||
|
const info = await moveToElement();
|
||||||
|
if (!info) return origElFocus();
|
||||||
|
await humanClick(raw, info.isInp, cfg);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- el.select() ---
|
||||||
|
if (origElSelect) {
|
||||||
|
(el as any).select = async (...values: string[]) => {
|
||||||
|
const info = await moveToElement();
|
||||||
|
if (!info) return origElSelect(...values);
|
||||||
|
await humanClick(raw, false, cfg);
|
||||||
|
await sleep(rand(100, 300));
|
||||||
|
return origElSelect(...values);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- el.drop() ---
|
||||||
|
if (origElDrop) {
|
||||||
|
(el as any).drop = async (draggable: ElementHandle, options?: any) => {
|
||||||
|
const srcBox = await draggable.boundingBox();
|
||||||
|
const tgtBox = await el.boundingBox();
|
||||||
|
|
||||||
|
if (srcBox && tgtBox) {
|
||||||
|
const sx = srcBox.x + srcBox.width / 2;
|
||||||
|
const sy = srcBox.y + srcBox.height / 2;
|
||||||
|
const tx = tgtBox.x + tgtBox.width / 2;
|
||||||
|
const ty = tgtBox.y + tgtBox.height / 2;
|
||||||
|
|
||||||
|
await (page as any)._ensureCursorInit();
|
||||||
|
await humanMove(raw, cursor.x, cursor.y, sx, sy, cfg);
|
||||||
|
cursor.x = sx;
|
||||||
|
cursor.y = sy;
|
||||||
|
await sleep(rand(100, 200));
|
||||||
|
await originals.mouseDown();
|
||||||
|
await sleep(rand(80, 150));
|
||||||
|
await humanMove(raw, cursor.x, cursor.y, tx, ty, cfg);
|
||||||
|
cursor.x = tx;
|
||||||
|
cursor.y = ty;
|
||||||
|
await sleep(rand(80, 150));
|
||||||
|
await originals.mouseUp();
|
||||||
|
} else {
|
||||||
|
return origElDrop(draggable, options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- el.dragAndDrop() ---
|
||||||
|
if (origElDragAndDrop) {
|
||||||
|
(el as any).dragAndDrop = async (targetEl: ElementHandle, options?: any) => {
|
||||||
|
const srcBox = await el.boundingBox();
|
||||||
|
const tgtBox = await targetEl.boundingBox();
|
||||||
|
|
||||||
|
if (srcBox && tgtBox) {
|
||||||
|
const sx = srcBox.x + srcBox.width / 2;
|
||||||
|
const sy = srcBox.y + srcBox.height / 2;
|
||||||
|
const tx = tgtBox.x + tgtBox.width / 2;
|
||||||
|
const ty = tgtBox.y + tgtBox.height / 2;
|
||||||
|
|
||||||
|
await (page as any)._ensureCursorInit();
|
||||||
|
await humanMove(raw, cursor.x, cursor.y, sx, sy, cfg);
|
||||||
|
cursor.x = sx;
|
||||||
|
cursor.y = sy;
|
||||||
|
await sleep(rand(100, 200));
|
||||||
|
await originals.mouseDown();
|
||||||
|
await sleep(rand(80, 150));
|
||||||
|
await humanMove(raw, cursor.x, cursor.y, tx, ty, cfg);
|
||||||
|
cursor.x = tx;
|
||||||
|
cursor.y = ty;
|
||||||
|
await sleep(rand(80, 150));
|
||||||
|
await originals.mouseUp();
|
||||||
|
} else {
|
||||||
|
return origElDragAndDrop(targetEl, options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Frame-level patching — native Puppeteer Frame methods only
|
||||||
|
// Puppeteer Frame has: click, hover, type, select, focus, tap
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
function patchFrames(
|
||||||
|
page: Page,
|
||||||
|
cfg: HumanConfig,
|
||||||
|
cursor: CursorState,
|
||||||
|
raw: RawMouse,
|
||||||
|
rawKb: RawKeyboard,
|
||||||
|
originals: any,
|
||||||
|
stealth: StealthEval,
|
||||||
|
): void {
|
||||||
|
for (const frame of iterFrames(page)) {
|
||||||
|
patchSingleFrame(frame, page, cfg, cursor, raw, rawKb, originals, stealth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchSingleFrame(
|
||||||
|
frame: Frame,
|
||||||
|
page: Page,
|
||||||
|
cfg: HumanConfig,
|
||||||
|
cursor: CursorState,
|
||||||
|
raw: RawMouse,
|
||||||
|
rawKb: RawKeyboard,
|
||||||
|
originals: any,
|
||||||
|
stealth: StealthEval,
|
||||||
|
): void {
|
||||||
|
if ((frame as any)._humanPatched) return;
|
||||||
|
(frame as any)._humanPatched = true;
|
||||||
|
|
||||||
|
const origFrameSelect = frame.select.bind(frame);
|
||||||
|
|
||||||
|
(frame as any).click = async (selector: string, options?: any) => {
|
||||||
|
await (page as any).click(selector, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
(frame as any).hover = async (selector: string, options?: any) => {
|
||||||
|
await (page as any).hover(selector, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
(frame as any).type = async (selector: string, text: string, options?: any) => {
|
||||||
|
await (page as any).type(selector, text, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
(frame as any).select = async (selector: string, ...values: string[]) => {
|
||||||
|
await (page as any).hover(selector);
|
||||||
|
await sleep(rand(100, 300));
|
||||||
|
return origFrameSelect(selector, ...values);
|
||||||
|
};
|
||||||
|
|
||||||
|
(frame as any).focus = async (selector: string) => {
|
||||||
|
await (page as any).focus(selector);
|
||||||
|
};
|
||||||
|
|
||||||
|
(frame as any).tap = async (selector: string, options?: any) => {
|
||||||
|
await (page as any).click(selector, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Patch frame.$() to return patched ElementHandles
|
||||||
|
const origFrame$ = frame.$.bind(frame);
|
||||||
|
const origFrame$$ = frame.$$.bind(frame);
|
||||||
|
const origFrameWaitForSelector = frame.waitForSelector.bind(frame);
|
||||||
|
|
||||||
|
(frame as any).$ = async (selector: string) => {
|
||||||
|
const el = await origFrame$(selector);
|
||||||
|
if (el) patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth);
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
|
||||||
|
(frame as any).$$ = async (selector: string) => {
|
||||||
|
const els = await origFrame$$(selector);
|
||||||
|
for (const el of els) {
|
||||||
|
patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth);
|
||||||
|
}
|
||||||
|
return els;
|
||||||
|
};
|
||||||
|
|
||||||
|
(frame as any).waitForSelector = async (selector: string, options?: any) => {
|
||||||
|
const el = await origFrameWaitForSelector(selector, options);
|
||||||
|
if (el) patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth);
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function* iterFrames(page: Page): Generator<Frame> {
|
||||||
|
try {
|
||||||
|
const mainFrame = page.mainFrame();
|
||||||
|
yield mainFrame;
|
||||||
|
for (const child of mainFrame.childFrames()) {
|
||||||
|
yield child;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Browser-level patching
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export function patchBrowser(browser: Browser, cfg: HumanConfig): void {
|
||||||
|
browser.pages().then(pages => {
|
||||||
|
for (const page of pages) {
|
||||||
|
if (!(page as any)._original) {
|
||||||
|
patchPage(page, cfg, new CursorState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
|
const origNewPage = browser.newPage.bind(browser);
|
||||||
|
(browser as any).newPage = async () => {
|
||||||
|
const page = await origNewPage();
|
||||||
|
if (!(page as any)._original) {
|
||||||
|
patchPage(page, cfg, new CursorState());
|
||||||
|
}
|
||||||
|
return page;
|
||||||
|
};
|
||||||
|
|
||||||
|
// v21: createIncognitoBrowserContext
|
||||||
|
// v22+: createBrowserContext (renamed in puppeteer/puppeteer#11834)
|
||||||
|
for (const methodName of ['createBrowserContext', 'createIncognitoBrowserContext'] as const) {
|
||||||
|
if (typeof (browser as any)[methodName] === 'function') {
|
||||||
|
const origCreateContext = (browser as any)[methodName].bind(browser);
|
||||||
|
(browser as any)[methodName] = async (options?: any) => {
|
||||||
|
const context: BrowserContext = await origCreateContext(options);
|
||||||
|
|
||||||
|
const origCtxNewPage = context.newPage.bind(context);
|
||||||
|
(context as any).newPage = async () => {
|
||||||
|
const page = await origCtxNewPage();
|
||||||
|
if (!(page as any)._original) {
|
||||||
|
patchPage(page, cfg, new CursorState());
|
||||||
|
}
|
||||||
|
return page;
|
||||||
|
};
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
browser.on('targetcreated', async (target: any) => {
|
||||||
|
try {
|
||||||
|
if (target.type() === 'page') {
|
||||||
|
const page = await target.page();
|
||||||
|
if (page && !(page as any)._original) {
|
||||||
|
patchPage(page, cfg, new CursorState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { patchPage };
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
/**
|
||||||
|
* cloakbrowser-human — Human-like keyboard input.
|
||||||
|
* Adapted for Puppeteer API.
|
||||||
|
*
|
||||||
|
* Changes from Playwright version:
|
||||||
|
* - Uses puppeteer-core Page/CDPSession types
|
||||||
|
* - keyboard.sendCharacter() mapped via RawKeyboard.insertText adapter
|
||||||
|
* - CDPSession obtained via page.createCDPSession()
|
||||||
|
*
|
||||||
|
* Stealth-aware: shift symbols use CDP Input.dispatchKeyEvent (isTrusted=true).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Page, CDPSession } from 'puppeteer-core';
|
||||||
|
import { RawKeyboard } from '../human/mouse.js';
|
||||||
|
import type { HumanConfig } from '../human/config.js';
|
||||||
|
import { rand, randRange, sleep } from '../human/config.js';
|
||||||
|
|
||||||
|
const SHIFT_SYMBOLS = new Set([
|
||||||
|
'@', '#', '!', '$', '%', '^', '&', '*', '(', ')',
|
||||||
|
'_', '+', '{', '}', '|', ':', '"', '<', '>', '?', '~',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const NEARBY_KEYS: Record<string, string> = {
|
||||||
|
a: 'sqwz', b: 'vghn', c: 'xdfv', d: 'sfecx', e: 'wrsdf',
|
||||||
|
f: 'dgrtcv', g: 'fhtyb', h: 'gjybn', i: 'ujko', j: 'hkunm',
|
||||||
|
k: 'jloi', l: 'kop', m: 'njk', n: 'bhjm', o: 'iklp',
|
||||||
|
p: 'ol', q: 'wa', r: 'edft', s: 'awedxz', t: 'rfgy',
|
||||||
|
u: 'yhji', v: 'cfgb', w: 'qase', x: 'zsdc', y: 'tghu',
|
||||||
|
z: 'asx',
|
||||||
|
'1': '2q', '2': '13qw', '3': '24we', '4': '35er', '5': '46rt',
|
||||||
|
'6': '57ty', '7': '68yu', '8': '79ui', '9': '80io', '0': '9p',
|
||||||
|
};
|
||||||
|
|
||||||
|
const SHIFT_SYMBOL_CODES: Record<string, string> = {
|
||||||
|
'!': 'Digit1', '@': 'Digit2', '#': 'Digit3', '$': 'Digit4',
|
||||||
|
'%': 'Digit5', '^': 'Digit6', '&': 'Digit7', '*': 'Digit8',
|
||||||
|
'(': 'Digit9', ')': 'Digit0', '_': 'Minus', '+': 'Equal',
|
||||||
|
'{': 'BracketLeft', '}': 'BracketRight', '|': 'Backslash',
|
||||||
|
':': 'Semicolon', '"': 'Quote', '<': 'Comma', '>': 'Period',
|
||||||
|
'?': 'Slash', '~': 'Backquote',
|
||||||
|
};
|
||||||
|
|
||||||
|
const SHIFT_SYMBOL_KEYCODES: Record<string, number> = {
|
||||||
|
'!': 49, '@': 50, '#': 51, '$': 52, '%': 53,
|
||||||
|
'^': 54, '&': 55, '*': 56, '(': 57, ')': 48,
|
||||||
|
'_': 189, '+': 187, '{': 219, '}': 221, '|': 220,
|
||||||
|
':': 186, '"': 222, '<': 188, '>': 190, '?': 191,
|
||||||
|
'~': 192,
|
||||||
|
};
|
||||||
|
|
||||||
|
function isAscii(ch: string): boolean {
|
||||||
|
const code = ch.codePointAt(0);
|
||||||
|
return code !== undefined && code < 128;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNearbyKey(ch: string): string {
|
||||||
|
const lower = ch.toLowerCase();
|
||||||
|
if (lower in NEARBY_KEYS) {
|
||||||
|
const neighbors = NEARBY_KEYS[lower];
|
||||||
|
const wrong = neighbors[Math.floor(Math.random() * neighbors.length)];
|
||||||
|
return ch === ch.toUpperCase() && ch !== ch.toLowerCase() ? wrong.toUpperCase() : wrong;
|
||||||
|
}
|
||||||
|
return ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUpperCase(ch: string): boolean {
|
||||||
|
return ch.length === 1 && ch >= 'A' && ch <= 'Z';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function humanType(
|
||||||
|
page: Page,
|
||||||
|
raw: RawKeyboard,
|
||||||
|
text: string,
|
||||||
|
cfg: HumanConfig,
|
||||||
|
cdpSession?: CDPSession | null,
|
||||||
|
): Promise<void> {
|
||||||
|
const chars = [...text];
|
||||||
|
|
||||||
|
for (let i = 0; i < chars.length; i++) {
|
||||||
|
const ch = chars[i];
|
||||||
|
|
||||||
|
// Non-ASCII → sendCharacter via insertText adapter
|
||||||
|
if (!isAscii(ch)) {
|
||||||
|
await sleep(randRange(cfg.key_hold));
|
||||||
|
await raw.insertText(ch);
|
||||||
|
if (i < chars.length - 1) await interCharDelay(cfg);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mistype
|
||||||
|
if (Math.random() < cfg.mistype_chance && /^[a-zA-Z0-9]$/.test(ch)) {
|
||||||
|
const wrong = getNearbyKey(ch);
|
||||||
|
await typeNormalChar(raw, wrong, cfg);
|
||||||
|
await sleep(randRange(cfg.mistype_delay_notice));
|
||||||
|
await raw.down('Backspace');
|
||||||
|
await sleep(randRange(cfg.key_hold));
|
||||||
|
await raw.up('Backspace');
|
||||||
|
await sleep(randRange(cfg.mistype_delay_correct));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUpperCase(ch)) {
|
||||||
|
await typeShiftedChar(raw, ch, cfg);
|
||||||
|
} else if (SHIFT_SYMBOLS.has(ch)) {
|
||||||
|
await typeShiftSymbol(page, raw, ch, cfg, cdpSession);
|
||||||
|
} else {
|
||||||
|
await typeNormalChar(raw, ch, cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i < chars.length - 1) await interCharDelay(cfg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function typeNormalChar(raw: RawKeyboard, ch: string, cfg: HumanConfig): Promise<void> {
|
||||||
|
await raw.down(ch);
|
||||||
|
await sleep(randRange(cfg.key_hold));
|
||||||
|
await raw.up(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function typeShiftedChar(raw: RawKeyboard, ch: string, cfg: HumanConfig): Promise<void> {
|
||||||
|
await raw.down('Shift');
|
||||||
|
await sleep(randRange(cfg.shift_down_delay));
|
||||||
|
await raw.down(ch);
|
||||||
|
await sleep(randRange(cfg.key_hold));
|
||||||
|
await raw.up(ch);
|
||||||
|
await sleep(randRange(cfg.shift_up_delay));
|
||||||
|
await raw.up('Shift');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function typeShiftSymbol(
|
||||||
|
page: Page,
|
||||||
|
raw: RawKeyboard,
|
||||||
|
ch: string,
|
||||||
|
cfg: HumanConfig,
|
||||||
|
cdpSession?: CDPSession | null,
|
||||||
|
): Promise<void> {
|
||||||
|
if (cdpSession) {
|
||||||
|
const code = SHIFT_SYMBOL_CODES[ch] || '';
|
||||||
|
const keyCode = SHIFT_SYMBOL_KEYCODES[ch] || 0;
|
||||||
|
|
||||||
|
await raw.down('Shift');
|
||||||
|
await sleep(randRange(cfg.shift_down_delay));
|
||||||
|
|
||||||
|
await cdpSession.send('Input.dispatchKeyEvent', {
|
||||||
|
type: 'keyDown',
|
||||||
|
modifiers: 8,
|
||||||
|
key: ch,
|
||||||
|
code,
|
||||||
|
windowsVirtualKeyCode: keyCode,
|
||||||
|
text: ch,
|
||||||
|
unmodifiedText: ch,
|
||||||
|
});
|
||||||
|
await sleep(randRange(cfg.key_hold));
|
||||||
|
|
||||||
|
await cdpSession.send('Input.dispatchKeyEvent', {
|
||||||
|
type: 'keyUp',
|
||||||
|
modifiers: 8,
|
||||||
|
key: ch,
|
||||||
|
code,
|
||||||
|
windowsVirtualKeyCode: keyCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
await sleep(randRange(cfg.shift_up_delay));
|
||||||
|
await raw.up('Shift');
|
||||||
|
} else {
|
||||||
|
await raw.down('Shift');
|
||||||
|
await sleep(randRange(cfg.shift_down_delay));
|
||||||
|
await raw.insertText(ch);
|
||||||
|
await page.evaluate((key: string) => {
|
||||||
|
const el = document.activeElement;
|
||||||
|
if (el) {
|
||||||
|
el.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
|
||||||
|
el.dispatchEvent(new KeyboardEvent('keyup', { key, bubbles: true }));
|
||||||
|
}
|
||||||
|
}, ch);
|
||||||
|
await sleep(randRange(cfg.shift_up_delay));
|
||||||
|
await raw.up('Shift');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function interCharDelay(cfg: HumanConfig): Promise<void> {
|
||||||
|
if (Math.random() < cfg.typing_pause_chance) {
|
||||||
|
await sleep(randRange(cfg.typing_pause_range));
|
||||||
|
} else {
|
||||||
|
const delay = cfg.typing_delay + (Math.random() - 0.5) * 2 * cfg.typing_delay_spread;
|
||||||
|
await sleep(Math.max(10, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
/**
|
||||||
|
* cloakbrowser-human — Human-like scrolling via mouse wheel events.
|
||||||
|
* Adapted for Puppeteer API.
|
||||||
|
*
|
||||||
|
* Changes from Playwright version:
|
||||||
|
* - page.viewport() instead of page.viewportSize()
|
||||||
|
* - page.$(selector) + el.boundingBox() instead of page.locator().boundingBox()
|
||||||
|
* - No timeout parameter on boundingBox()
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Page } from 'puppeteer-core';
|
||||||
|
import type { HumanConfig } from '../human/config.js';
|
||||||
|
import { rand, randRange, randIntRange, sleep } from '../human/config.js';
|
||||||
|
import { RawMouse, humanMove } from '../human/mouse.js';
|
||||||
|
|
||||||
|
interface ElementBounds {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInViewport(
|
||||||
|
bounds: ElementBounds,
|
||||||
|
viewportHeight: number,
|
||||||
|
cfg: HumanConfig,
|
||||||
|
): boolean {
|
||||||
|
const topEdge = bounds.y;
|
||||||
|
const bottomEdge = bounds.y + bounds.height;
|
||||||
|
const zoneTop = viewportHeight * cfg.scroll_target_zone[0];
|
||||||
|
const zoneBottom = viewportHeight * cfg.scroll_target_zone[1];
|
||||||
|
return topEdge >= zoneTop && bottomEdge <= zoneBottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function smoothWheel(
|
||||||
|
raw: RawMouse,
|
||||||
|
delta: number,
|
||||||
|
cfg: HumanConfig,
|
||||||
|
axis: 'x' | 'y' = 'y',
|
||||||
|
): Promise<void> {
|
||||||
|
const absD = Math.abs(delta);
|
||||||
|
const sign = delta > 0 ? 1 : -1;
|
||||||
|
let sent = 0;
|
||||||
|
while (sent < absD) {
|
||||||
|
const stepSize = rand(20, 40);
|
||||||
|
const chunk = Math.min(stepSize, absD - sent);
|
||||||
|
const d = Math.round(chunk) * sign;
|
||||||
|
if (axis === 'x') {
|
||||||
|
await raw.wheel(d, 0);
|
||||||
|
} else {
|
||||||
|
await raw.wheel(0, d);
|
||||||
|
}
|
||||||
|
sent += chunk;
|
||||||
|
await sleep(rand(8, 20));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getElementBox(page: Page, selector: string): Promise<ElementBounds | null> {
|
||||||
|
try {
|
||||||
|
const el = await page.$(selector);
|
||||||
|
if (!el) return null;
|
||||||
|
const box = await el.boundingBox();
|
||||||
|
if (!box) return null;
|
||||||
|
return { x: box.x, y: box.y, width: box.width, height: box.height };
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function scrollToElement(
|
||||||
|
page: Page,
|
||||||
|
raw: RawMouse,
|
||||||
|
selector: string,
|
||||||
|
cursorX: number,
|
||||||
|
cursorY: number,
|
||||||
|
cfg: HumanConfig,
|
||||||
|
): Promise<{ box: ElementBounds; cursorX: number; cursorY: number }> {
|
||||||
|
const viewport = page.viewport();
|
||||||
|
if (!viewport) throw new Error('Viewport size not available');
|
||||||
|
|
||||||
|
let box = await getElementBox(page, selector);
|
||||||
|
if (!box) {
|
||||||
|
await sleep(200);
|
||||||
|
box = await getElementBox(page, selector);
|
||||||
|
if (!box) throw new Error(`Element not found: ${selector}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInViewport(box, viewport.height, cfg)) {
|
||||||
|
return { box, cursorX, cursorY };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move cursor into scroll area
|
||||||
|
const scrollAreaX = Math.round(viewport.width * rand(0.3, 0.7));
|
||||||
|
const scrollAreaY = Math.round(viewport.height * rand(0.3, 0.7));
|
||||||
|
await humanMove(raw, cursorX, cursorY, scrollAreaX, scrollAreaY, cfg);
|
||||||
|
cursorX = scrollAreaX;
|
||||||
|
cursorY = scrollAreaY;
|
||||||
|
await sleep(randRange(cfg.scroll_pre_move_delay));
|
||||||
|
|
||||||
|
// Calculate scroll distance
|
||||||
|
const targetY = viewport.height * rand(cfg.scroll_target_zone[0], cfg.scroll_target_zone[1]);
|
||||||
|
const elementCenter = box.y + box.height / 2;
|
||||||
|
const distanceToScroll = elementCenter - targetY;
|
||||||
|
|
||||||
|
const direction = distanceToScroll > 0 ? 1 : -1;
|
||||||
|
const absDistance = Math.abs(distanceToScroll);
|
||||||
|
const avgDelta = (cfg.scroll_delta_base[0] + cfg.scroll_delta_base[1]) / 2;
|
||||||
|
const totalClicks = Math.max(3, Math.ceil(absDistance / avgDelta));
|
||||||
|
const accelSteps = randIntRange(cfg.scroll_accel_steps);
|
||||||
|
const decelSteps = randIntRange(cfg.scroll_decel_steps);
|
||||||
|
|
||||||
|
let scrolled = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < totalClicks; i++) {
|
||||||
|
let delta: number;
|
||||||
|
let pause: number;
|
||||||
|
|
||||||
|
if (i < accelSteps) {
|
||||||
|
delta = rand(80, 100);
|
||||||
|
pause = randRange(cfg.scroll_pause_slow);
|
||||||
|
} else if (i >= totalClicks - decelSteps) {
|
||||||
|
delta = rand(60, 90);
|
||||||
|
pause = randRange(cfg.scroll_pause_slow);
|
||||||
|
} else {
|
||||||
|
delta = randRange(cfg.scroll_delta_base);
|
||||||
|
pause = randRange(cfg.scroll_pause_fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
delta *= 1 + (Math.random() - 0.5) * 2 * cfg.scroll_delta_variance;
|
||||||
|
delta = Math.round(delta) * direction;
|
||||||
|
|
||||||
|
await smoothWheel(raw, delta, cfg);
|
||||||
|
scrolled += Math.abs(delta);
|
||||||
|
await sleep(pause);
|
||||||
|
|
||||||
|
if (i % 3 === 2 || i === totalClicks - 1) {
|
||||||
|
box = await getElementBox(page, selector);
|
||||||
|
if (box && isInViewport(box, viewport.height, cfg)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrolled >= absDistance * 1.1) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional overshoot + correction
|
||||||
|
if (Math.random() < cfg.scroll_overshoot_chance) {
|
||||||
|
const overshootPx = Math.round(randRange(cfg.scroll_overshoot_px)) * direction;
|
||||||
|
await smoothWheel(raw, overshootPx, cfg);
|
||||||
|
await sleep(randRange(cfg.scroll_settle_delay));
|
||||||
|
|
||||||
|
const corrections = randIntRange([1, 2]);
|
||||||
|
for (let c = 0; c < corrections; c++) {
|
||||||
|
const corrDelta = Math.round(rand(40, 80)) * -direction;
|
||||||
|
await smoothWheel(raw, corrDelta, cfg);
|
||||||
|
await sleep(rand(100, 250));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(randRange(cfg.scroll_settle_delay));
|
||||||
|
|
||||||
|
box = await getElementBox(page, selector);
|
||||||
|
if (!box) throw new Error(`Element lost after scrolling: ${selector}`);
|
||||||
|
|
||||||
|
return { box, cursorX, cursorY };
|
||||||
|
}
|
||||||
+13
-2
@@ -440,6 +440,9 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
|
|||||||
(page as any).uncheck = humanUncheckFn;
|
(page as any).uncheck = humanUncheckFn;
|
||||||
(page as any).selectOption = humanSelectOptionFn;
|
(page as any).selectOption = humanSelectOptionFn;
|
||||||
(page as any).press = humanPressFn;
|
(page as any).press = humanPressFn;
|
||||||
|
(page as any).pressSequentially = humanPressSequentiallyFn;
|
||||||
|
(page as any).tap = humanTapFn;
|
||||||
|
(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?: any) => {
|
||||||
@@ -494,8 +497,8 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Patch Frame methods so Locator-based calls go through humanization.
|
* Patch Frame methods so Locator-based calls go through humanization.
|
||||||
* All 11 methods patched: click, dblclick, hover, type, fill, check, uncheck,
|
* All 13 methods patched: click, dblclick, hover, type, fill, check, uncheck,
|
||||||
* selectOption, press, clear, dragAndDrop.
|
* selectOption, press, pressSequentially, tap, clear, dragAndDrop.
|
||||||
*/
|
*/
|
||||||
function patchFrames(
|
function patchFrames(
|
||||||
page: Page,
|
page: Page,
|
||||||
@@ -563,6 +566,14 @@ function patchSingleFrame(
|
|||||||
await (page as any).press(selector, key, options);
|
await (page as any).press(selector, key, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
(frame as any).pressSequentially = async (selector: string, text: string, options?: any) => {
|
||||||
|
await (page as any).pressSequentially(selector, text, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
(frame as any).tap = async (selector: string, options?: any) => {
|
||||||
|
await (page as any).tap(selector, options);
|
||||||
|
};
|
||||||
|
|
||||||
(frame as any).clear = async (selector: string, options?: any) => {
|
(frame as any).clear = async (selector: string, options?: any) => {
|
||||||
if (!await isSelectorFocused(stealth, page, selector)) {
|
if (!await isSelectorFocused(stealth, page, selector)) {
|
||||||
await (page as any).click(selector);
|
await (page as any).click(selector);
|
||||||
|
|||||||
+21
-10
@@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Puppeteer launch wrapper for cloakbrowser.
|
* Puppeteer launch wrapper for cloakbrowser.
|
||||||
* Alternative to the Playwright wrapper for users who prefer Puppeteer.
|
* NOW WITH HUMANIZE SUPPORT — humanize: true enables human-like
|
||||||
|
* mouse curves, keyboard timing, and scroll patterns (same as Playwright).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Browser } from "puppeteer-core";
|
import type { Browser } from "puppeteer-core";
|
||||||
@@ -17,11 +18,12 @@ import { maybeResolveGeoip, resolveWebrtcArgs } from "./geoip.js";
|
|||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* import { launch } from 'cloakbrowser/puppeteer';
|
* import { launch } from 'cloakbrowser/puppeteer';
|
||||||
* const browser = await launch();
|
* * // With humanize — human-like mouse, keyboard, scroll
|
||||||
|
* const browser = await launch({ humanize: true });
|
||||||
* const page = await browser.newPage();
|
* const page = await browser.newPage();
|
||||||
* await page.goto('https://bot.incolumitas.com');
|
* await page.goto('[https://example.com](https://example.com)');
|
||||||
* console.log(await page.title());
|
* await page.click('#login'); // Bézier curve mouse movement
|
||||||
* await browser.close();
|
* await page.type('#email', 'user@example.com'); // Per-character timing
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export async function launch(options: LaunchOptions = {}): Promise<Browser> {
|
export async function launch(options: LaunchOptions = {}): Promise<Browser> {
|
||||||
@@ -30,6 +32,7 @@ export async function launch(options: LaunchOptions = {}): Promise<Browser> {
|
|||||||
const binaryPath = process.env.CLOAKBROWSER_BINARY_PATH || (await ensureBinary());
|
const binaryPath = process.env.CLOAKBROWSER_BINARY_PATH || (await ensureBinary());
|
||||||
const { exitIp, ...resolved } = (await maybeResolveGeoip(options)) ?? {};
|
const { exitIp, ...resolved } = (await maybeResolveGeoip(options)) ?? {};
|
||||||
let resolvedArgs = (await resolveWebrtcArgs(options)) ?? options.args;
|
let resolvedArgs = (await resolveWebrtcArgs(options)) ?? options.args;
|
||||||
|
|
||||||
if (exitIp && !(resolvedArgs ?? []).some(a => a.startsWith("--fingerprint-webrtc-ip"))) {
|
if (exitIp && !(resolvedArgs ?? []).some(a => a.startsWith("--fingerprint-webrtc-ip"))) {
|
||||||
resolvedArgs = [...(resolvedArgs ?? []), `--fingerprint-webrtc-ip=${exitIp}`];
|
resolvedArgs = [...(resolvedArgs ?? []), `--fingerprint-webrtc-ip=${exitIp}`];
|
||||||
}
|
}
|
||||||
@@ -82,10 +85,18 @@ export async function launch(options: LaunchOptions = {}): Promise<Browser> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Human-like behavioral patching — FULL coverage, same as Playwright.
|
||||||
|
// This enables Bézier mouse movements, organic typing rhythms, and
|
||||||
|
// natural scrolling to bypass advanced anti-bot detection.
|
||||||
|
if (options.humanize) {
|
||||||
|
const { patchBrowser } = await import('./human-puppeteer/index.js');
|
||||||
|
const { resolveConfig } = await import('./human/config.js');
|
||||||
|
const cfg = resolveConfig(
|
||||||
|
(options.humanPreset as any) ?? 'default',
|
||||||
|
options.humanConfig as any,
|
||||||
|
);
|
||||||
|
patchBrowser(browser, cfg);
|
||||||
|
}
|
||||||
|
|
||||||
return browser;
|
return browser;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Internal
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+103
-4
@@ -831,6 +831,101 @@ describe("frame patching with stealth", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Page-level: pressSequentially, tap, clear are patched
|
||||||
|
// =========================================================================
|
||||||
|
describe("page-level pressSequentially, tap, clear patches", () => {
|
||||||
|
it("page.pressSequentially is replaced after patchPage", async () => {
|
||||||
|
const { patchPage } = await import("../src/human/index.js");
|
||||||
|
|
||||||
|
const page = buildMockPage();
|
||||||
|
const originalPressSeq = page.pressSequentially ?? (() => {});
|
||||||
|
const cfg = resolveConfig("default");
|
||||||
|
const cursor = { x: 0, y: 0, initialized: false };
|
||||||
|
patchPage(page as any, cfg, cursor as any);
|
||||||
|
|
||||||
|
expect(typeof (page as any).pressSequentially).toBe("function");
|
||||||
|
expect((page as any).pressSequentially).not.toBe(originalPressSeq);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("page.tap is replaced after patchPage", async () => {
|
||||||
|
const { patchPage } = await import("../src/human/index.js");
|
||||||
|
|
||||||
|
const page = buildMockPage();
|
||||||
|
const originalTap = page.tap ?? (() => {});
|
||||||
|
const cfg = resolveConfig("default");
|
||||||
|
const cursor = { x: 0, y: 0, initialized: false };
|
||||||
|
patchPage(page as any, cfg, cursor as any);
|
||||||
|
|
||||||
|
expect(typeof (page as any).tap).toBe("function");
|
||||||
|
expect((page as any).tap).not.toBe(originalTap);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("page.clear is replaced after patchPage", async () => {
|
||||||
|
const { patchPage } = await import("../src/human/index.js");
|
||||||
|
|
||||||
|
const page = buildMockPage();
|
||||||
|
const originalClear = page.clear ?? (() => {});
|
||||||
|
const cfg = resolveConfig("default");
|
||||||
|
const cursor = { x: 0, y: 0, initialized: false };
|
||||||
|
patchPage(page as any, cfg, cursor as any);
|
||||||
|
|
||||||
|
expect(typeof (page as any).clear).toBe("function");
|
||||||
|
expect((page as any).clear).not.toBe(originalClear);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Frame-level: pressSequentially, tap are patched
|
||||||
|
// =========================================================================
|
||||||
|
describe("frame-level pressSequentially, tap patches", () => {
|
||||||
|
it("child frame has pressSequentially patched", async () => {
|
||||||
|
const { patchPage } = await import("../src/human/index.js");
|
||||||
|
|
||||||
|
const childFrame: any = {
|
||||||
|
click: vi.fn(async () => {}),
|
||||||
|
dblclick: vi.fn(async () => {}),
|
||||||
|
hover: vi.fn(async () => {}),
|
||||||
|
type: vi.fn(async () => {}),
|
||||||
|
fill: vi.fn(async () => {}),
|
||||||
|
check: vi.fn(async () => {}),
|
||||||
|
uncheck: vi.fn(async () => {}),
|
||||||
|
selectOption: vi.fn(async () => {}),
|
||||||
|
press: vi.fn(async () => {}),
|
||||||
|
pressSequentially: vi.fn(async () => {}),
|
||||||
|
tap: vi.fn(async () => {}),
|
||||||
|
clear: vi.fn(async () => {}),
|
||||||
|
dragAndDrop: vi.fn(async () => {}),
|
||||||
|
locator: vi.fn(() => ({
|
||||||
|
boundingBox: vi.fn(async () => ({ x: 0, y: 0, width: 100, height: 30 })),
|
||||||
|
})),
|
||||||
|
childFrames: vi.fn(() => []),
|
||||||
|
};
|
||||||
|
|
||||||
|
const origPressSeq = childFrame.pressSequentially;
|
||||||
|
const origTap = childFrame.tap;
|
||||||
|
|
||||||
|
const mainFrame = {
|
||||||
|
...childFrame,
|
||||||
|
childFrames: vi.fn(() => [childFrame]),
|
||||||
|
};
|
||||||
|
|
||||||
|
const page = buildMockPage({ mainFrameReturn: mainFrame });
|
||||||
|
const cfg = resolveConfig("default");
|
||||||
|
const cursor = { x: 0, y: 0, initialized: false };
|
||||||
|
patchPage(page as any, cfg, cursor as any);
|
||||||
|
|
||||||
|
expect((childFrame as any)._humanPatched).toBe(true);
|
||||||
|
// pressSequentially and tap should be replaced with humanized versions
|
||||||
|
expect(childFrame.pressSequentially).not.toBe(origPressSeq);
|
||||||
|
expect(childFrame.tap).not.toBe(origTap);
|
||||||
|
expect(typeof childFrame.pressSequentially).toBe("function");
|
||||||
|
expect(typeof childFrame.tap).toBe("function");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Non-ASCII text does NOT go through CDP shift symbol path
|
// Non-ASCII text does NOT go through CDP shift symbol path
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -898,7 +993,8 @@ describeIfSlow("stealth browser: no evaluate leak on click", () => {
|
|||||||
it("click() does not trigger querySelector from evaluate context", async () => {
|
it("click() does not trigger querySelector from evaluate context", async () => {
|
||||||
const { launch } = await import("../src/index.js");
|
const { launch } = await import("../src/index.js");
|
||||||
|
|
||||||
const browser = await launch({ headless: true, args: ['--humanize'] });
|
const browser = await launch({ headless: true, humanize: true });
|
||||||
|
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
|
|
||||||
await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' });
|
await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' });
|
||||||
@@ -932,7 +1028,8 @@ describeIfSlow("stealth browser: shift symbols isTrusted=true", () => {
|
|||||||
it("'!' produces isTrusted=true keydown, not isTrusted=false", async () => {
|
it("'!' produces isTrusted=true keydown, not isTrusted=false", async () => {
|
||||||
const { launch } = await import("../src/index.js");
|
const { launch } = await import("../src/index.js");
|
||||||
|
|
||||||
const browser = await launch({ headless: true, args: ['--humanize'] });
|
const browser = await launch({ headless: true, humanize: true });
|
||||||
|
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
|
|
||||||
await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' });
|
await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' });
|
||||||
@@ -972,7 +1069,8 @@ describeIfSlow("stealth browser: navigation invalidation", () => {
|
|||||||
it("click works after navigation (isolated world re-created)", async () => {
|
it("click works after navigation (isolated world re-created)", async () => {
|
||||||
const { launch } = await import("../src/index.js");
|
const { launch } = await import("../src/index.js");
|
||||||
|
|
||||||
const browser = await launch({ headless: true, args: ['--humanize'] });
|
const browser = await launch({ headless: true, humanize: true });
|
||||||
|
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
|
|
||||||
expect((page as any)._stealth).toBeDefined();
|
expect((page as any)._stealth).toBeDefined();
|
||||||
@@ -1003,7 +1101,8 @@ describeIfSlow("stealth browser: full form no evaluate leak", () => {
|
|||||||
it("form with shift symbols has zero evaluate leaks and zero untrusted events", async () => {
|
it("form with shift symbols has zero evaluate leaks and zero untrusted events", async () => {
|
||||||
const { launch } = await import("../src/index.js");
|
const { launch } = await import("../src/index.js");
|
||||||
|
|
||||||
const browser = await launch({ headless: true, args: ['--humanize'] });
|
const browser = await launch({ headless: true, humanize: true });
|
||||||
|
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
|
|
||||||
await page.goto(
|
await page.goto(
|
||||||
|
|||||||
Reference in New Issue
Block a user