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;
+185 -185
View File
@@ -106,9 +106,9 @@ describe("humanMove", () => {
return { return {
raw: { raw: {
move: vi.fn(async (x: number, y: number) => { moves.push({ x, y }); }), move: vi.fn(async (x: number, y: number) => { moves.push({ x, y }); }),
down: vi.fn(async () => {}), down: vi.fn(async () => { }),
up: vi.fn(async () => {}), up: vi.fn(async () => { }),
wheel: vi.fn(async () => {}), wheel: vi.fn(async () => { }),
}, },
moves, moves,
}; };
@@ -180,10 +180,10 @@ describe("humanClick", () => {
const cfg = resolveConfig("default"); const cfg = resolveConfig("default");
const callOrder: string[] = []; const callOrder: string[] = [];
const raw = { const raw = {
move: vi.fn(async () => {}), move: vi.fn(async () => { }),
down: vi.fn(async () => { callOrder.push("down"); }), down: vi.fn(async () => { callOrder.push("down"); }),
up: vi.fn(async () => { callOrder.push("up"); }), up: vi.fn(async () => { callOrder.push("up"); }),
wheel: vi.fn(async () => {}), wheel: vi.fn(async () => { }),
}; };
await humanClick(raw, false, cfg); await humanClick(raw, false, cfg);
expect(raw.down).toHaveBeenCalledTimes(1); expect(raw.down).toHaveBeenCalledTimes(1);
@@ -199,12 +199,12 @@ describe("humanIdle", () => {
it("calls raw.move at least once during idle", async () => { it("calls raw.move at least once during idle", async () => {
const cfg = resolveConfig("default"); const cfg = resolveConfig("default");
const raw = { const raw = {
move: vi.fn(async () => {}), move: vi.fn(async () => { }),
down: vi.fn(async () => {}), down: vi.fn(async () => { }),
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);
}); });
@@ -265,7 +265,7 @@ describe("patchPage fill", () => {
const cursor = { x: 0, y: 0, initialized: false }; const cursor = { x: 0, y: 0, initialized: false };
patchPage(page as any, cfg, cursor as any); patchPage(page as any, cfg, cursor as any);
try { await (page as any).fill("input#name", "hello"); } catch (_) {} try { await (page as any).fill("input#name", "hello"); } catch (_) { }
const expected = process.platform === "darwin" ? "Meta+a" : "Control+a"; const expected = process.platform === "darwin" ? "Meta+a" : "Control+a";
const wrong = process.platform === "darwin" ? "Control+a" : "Meta+a"; const wrong = process.platform === "darwin" ? "Control+a" : "Meta+a";
@@ -298,7 +298,7 @@ describe("patchPage check/uncheck idle", () => {
const cursor = { x: 100, y: 100, initialized: true }; const cursor = { x: 100, y: 100, initialized: true };
patchPage(page as any, cfg, cursor as any); patchPage(page as any, cfg, cursor as any);
try { await (page as any).check("input#cb"); } catch (_) {} try { await (page as any).check("input#cb"); } catch (_) { }
// humanCheckFn → humanIdle → humanClickFn → humanClick → raw.down // humanCheckFn → humanIdle → humanClickFn → humanClick → raw.down
expect(downCalled).toBe(true); expect(downCalled).toBe(true);
@@ -321,7 +321,7 @@ describe("patchPage check/uncheck idle", () => {
const cursor = { x: 100, y: 100, initialized: true }; const cursor = { x: 100, y: 100, initialized: true };
patchPage(page as any, cfg, cursor as any); patchPage(page as any, cfg, cursor as any);
try { await (page as any).uncheck("input#cb"); } catch (_) {} try { await (page as any).uncheck("input#cb"); } catch (_) { }
expect(downCalled).toBe(true); expect(downCalled).toBe(true);
}, 30000); }, 30000);
@@ -354,7 +354,7 @@ describe("patchPage press focus", () => {
const cursor = { x: 50, y: 50, initialized: true }; const cursor = { x: 50, y: 50, initialized: true };
patchPage(page as any, cfg, cursor as any); patchPage(page as any, cfg, cursor as any);
try { await (page as any).press("input#field", "Enter"); } catch (_) {} try { await (page as any).press("input#field", "Enter"); } catch (_) { }
expect(downCount).toBeGreaterThan(0); expect(downCount).toBeGreaterThan(0);
}); });
@@ -372,7 +372,7 @@ describe("patchPage press focus", () => {
const cursor = { x: 50, y: 50, initialized: true }; const cursor = { x: 50, y: 50, initialized: true };
patchPage(page as any, cfg, cursor as any); patchPage(page as any, cfg, cursor as any);
try { await (page as any).press("input#field", "Enter"); } catch (_) {} try { await (page as any).press("input#field", "Enter"); } catch (_) { }
expect(downCount).toBe(0); expect(downCount).toBe(0);
}); });
@@ -528,7 +528,7 @@ describe("patchBrowser CDP-connected workflow", () => {
pages: vi.fn(() => [page]), pages: vi.fn(() => [page]),
on: vi.fn(), on: vi.fn(),
newPage: vi.fn(async () => buildMockPage()), newPage: vi.fn(async () => buildMockPage()),
addInitScript: vi.fn(async () => {}), addInitScript: vi.fn(async () => { }),
}; };
const browser: any = { const browser: any = {
contexts: vi.fn(() => [context]), contexts: vi.fn(() => [context]),
@@ -557,7 +557,7 @@ describe("patchBrowser CDP-connected workflow", () => {
pages: vi.fn(() => [page]), pages: vi.fn(() => [page]),
on: vi.fn(), on: vi.fn(),
newPage: vi.fn(async () => buildMockPage()), newPage: vi.fn(async () => buildMockPage()),
addInitScript: vi.fn(async () => {}), addInitScript: vi.fn(async () => { }),
}; };
const browser: any = { const browser: any = {
contexts: vi.fn(() => [context]), contexts: vi.fn(() => [context]),
@@ -568,7 +568,7 @@ describe("patchBrowser CDP-connected workflow", () => {
patchBrowser(browser, resolveConfig("default")); patchBrowser(browser, resolveConfig("default"));
// Click through the patched method — should go through humanize path // Click through the patched method — should go through humanize path
try { await (page as any).click("button"); } catch (_) {} try { await (page as any).click("button"); } catch (_) { }
expect(downCalled).toBe(true); expect(downCalled).toBe(true);
}, 30000); }, 30000);
@@ -581,7 +581,7 @@ describe("patchBrowser CDP-connected workflow", () => {
pages: vi.fn(() => [newPage]), pages: vi.fn(() => [newPage]),
on: vi.fn(), on: vi.fn(),
newPage: vi.fn(async () => buildMockPage()), newPage: vi.fn(async () => buildMockPage()),
addInitScript: vi.fn(async () => {}), addInitScript: vi.fn(async () => { }),
}; };
const browser: any = { const browser: any = {
contexts: vi.fn(() => []), contexts: vi.fn(() => []),
@@ -605,27 +605,27 @@ describe("patchBrowser CDP-connected workflow", () => {
function buildMockPage(overrides: Record<string, any> = {}): any { function buildMockPage(overrides: Record<string, any> = {}): any {
const mainFrameObj = overrides.mainFrameReturn ?? { const mainFrameObj = overrides.mainFrameReturn ?? {
childFrames: vi.fn(() => []), childFrames: vi.fn(() => []),
click: vi.fn(async () => {}), click: vi.fn(async () => { }),
dblclick: vi.fn(async () => {}), dblclick: vi.fn(async () => { }),
hover: vi.fn(async () => {}), hover: vi.fn(async () => { }),
type: vi.fn(async () => {}), type: vi.fn(async () => { }),
fill: vi.fn(async () => {}), fill: vi.fn(async () => { }),
check: vi.fn(async () => {}), check: vi.fn(async () => { }),
uncheck: vi.fn(async () => {}), uncheck: vi.fn(async () => { }),
selectOption: vi.fn(async () => {}), selectOption: vi.fn(async () => { }),
press: vi.fn(async () => {}), press: vi.fn(async () => { }),
clear: vi.fn(async () => {}), clear: vi.fn(async () => { }),
dragAndDrop: vi.fn(async () => {}), dragAndDrop: vi.fn(async () => { }),
locator: vi.fn(() => ({ locator: vi.fn(() => ({
boundingBox: vi.fn(async () => ({ x: 0, y: 0, width: 100, height: 30 })), boundingBox: vi.fn(async () => ({ x: 0, y: 0, width: 100, height: 30 })),
first: vi.fn(function(this: any) { return this; }), first: vi.fn(function (this: any) { return this; }),
})), })),
}; };
const makeLocator = () => { const makeLocator = () => {
const loc: any = { const loc: any = {
boundingBox: vi.fn(async () => ({ x: 100, y: 100, width: 200, height: 30 })), boundingBox: vi.fn(async () => ({ x: 100, y: 100, width: 200, height: 30 })),
scrollIntoViewIfNeeded: vi.fn(async () => {}), scrollIntoViewIfNeeded: vi.fn(async () => { }),
isChecked: overrides.isChecked ?? vi.fn(async () => false), isChecked: overrides.isChecked ?? vi.fn(async () => false),
}; };
loc.first = vi.fn(() => loc); loc.first = vi.fn(() => loc);
@@ -634,33 +634,33 @@ function buildMockPage(overrides: Record<string, any> = {}): any {
const page: any = { const page: any = {
evaluate: overrides.evaluate ?? vi.fn(async () => false), evaluate: overrides.evaluate ?? vi.fn(async () => false),
addInitScript: vi.fn(async () => {}), addInitScript: vi.fn(async () => { }),
mouse: { mouse: {
move: vi.fn(async () => {}), move: vi.fn(async () => { }),
down: vi.fn(async () => {}), down: vi.fn(async () => { }),
up: vi.fn(async () => {}), up: vi.fn(async () => { }),
click: vi.fn(async () => {}), click: vi.fn(async () => { }),
dblclick: vi.fn(async () => {}), dblclick: vi.fn(async () => { }),
wheel: vi.fn(async () => {}), wheel: vi.fn(async () => { }),
}, },
keyboard: { keyboard: {
press: overrides.keyboardPress press: overrides.keyboardPress
? vi.fn(overrides.keyboardPress) ? vi.fn(overrides.keyboardPress)
: vi.fn(async () => {}), : vi.fn(async () => { }),
type: vi.fn(async () => {}), type: vi.fn(async () => { }),
down: vi.fn(async () => {}), down: vi.fn(async () => { }),
up: vi.fn(async () => {}), up: vi.fn(async () => { }),
insertText: vi.fn(async () => {}), insertText: vi.fn(async () => { }),
}, },
click: vi.fn(async () => {}), click: vi.fn(async () => { }),
dblclick: vi.fn(async () => {}), dblclick: vi.fn(async () => { }),
hover: vi.fn(async () => {}), hover: vi.fn(async () => { }),
type: vi.fn(async () => {}), type: vi.fn(async () => { }),
fill: vi.fn(async () => {}), fill: vi.fn(async () => { }),
check: vi.fn(async () => {}), check: vi.fn(async () => { }),
uncheck: vi.fn(async () => {}), uncheck: vi.fn(async () => { }),
selectOption: vi.fn(async () => {}), selectOption: vi.fn(async () => { }),
press: vi.fn(async () => {}), press: vi.fn(async () => { }),
goto: vi.fn(async () => ({})), goto: vi.fn(async () => ({})),
isChecked: overrides.isChecked ?? vi.fn(async () => false), isChecked: overrides.isChecked ?? vi.fn(async () => false),
locator: vi.fn(() => makeLocator()), locator: vi.fn(() => makeLocator()),
@@ -669,10 +669,10 @@ function buildMockPage(overrides: Record<string, any> = {}): any {
frames: vi.fn(() => []), frames: vi.fn(() => []),
context: vi.fn(() => ({ context: vi.fn(() => ({
pages: vi.fn(() => []), pages: vi.fn(() => []),
addInitScript: vi.fn(async () => {}), addInitScript: vi.fn(async () => { }),
})), })),
url: vi.fn(() => "about:blank"), url: vi.fn(() => "about:blank"),
waitForTimeout: vi.fn(async () => {}), waitForTimeout: vi.fn(async () => { }),
}; };
return page; return page;
} }
@@ -686,8 +686,8 @@ describe("humanType non-ASCII", () => {
const insertedChars: string[] = []; const insertedChars: string[] = [];
const raw = { const raw = {
down: vi.fn(async (k: string) => { downKeys.push(k); }), down: vi.fn(async (k: string) => { downKeys.push(k); }),
up: vi.fn(async () => {}), up: vi.fn(async () => { }),
type: vi.fn(async () => {}), type: vi.fn(async () => { }),
insertText: vi.fn(async (t: string) => { insertedChars.push(t); }), insertText: vi.fn(async (t: string) => { insertedChars.push(t); }),
}; };
return { raw, downKeys, insertedChars }; return { raw, downKeys, insertedChars };
@@ -759,18 +759,18 @@ describe("humanType non-ASCII", () => {
function buildMockElementHandle(overrides: Record<string, any> = {}): any { function buildMockElementHandle(overrides: Record<string, any> = {}): any {
const el: any = { const el: any = {
click: vi.fn(async () => {}), click: vi.fn(async () => { }),
dblclick: vi.fn(async () => {}), dblclick: vi.fn(async () => { }),
hover: vi.fn(async () => {}), hover: vi.fn(async () => { }),
type: vi.fn(async () => {}), type: vi.fn(async () => { }),
fill: vi.fn(async () => {}), fill: vi.fn(async () => { }),
press: vi.fn(async () => {}), press: vi.fn(async () => { }),
selectOption: vi.fn(async () => {}), selectOption: vi.fn(async () => { }),
check: vi.fn(async () => {}), check: vi.fn(async () => { }),
uncheck: vi.fn(async () => {}), uncheck: vi.fn(async () => { }),
setChecked: vi.fn(async () => {}), setChecked: vi.fn(async () => { }),
tap: vi.fn(async () => {}), tap: vi.fn(async () => { }),
focus: vi.fn(async () => {}), focus: vi.fn(async () => { }),
boundingBox: overrides.boundingBox ?? vi.fn(async () => ({ x: 100, y: 100, width: 200, height: 30 })), boundingBox: overrides.boundingBox ?? vi.fn(async () => ({ x: 100, y: 100, width: 200, height: 30 })),
evaluate: overrides.evaluate ?? vi.fn(async () => false), evaluate: overrides.evaluate ?? vi.fn(async () => false),
isChecked: overrides.isChecked ?? vi.fn(async () => false), isChecked: overrides.isChecked ?? vi.fn(async () => false),
@@ -788,21 +788,21 @@ describe("patchSingleElementHandle", () => {
const cfg = resolveConfig("default"); const cfg = resolveConfig("default");
const cursor = { x: 100, y: 100, initialized: true }; const cursor = { x: 100, y: 100, initialized: true };
const raw = { const raw = {
move: vi.fn(async () => {}), move: vi.fn(async () => { }),
down: vi.fn(async () => {}), down: vi.fn(async () => { }),
up: vi.fn(async () => {}), up: vi.fn(async () => { }),
wheel: vi.fn(async () => {}), wheel: vi.fn(async () => { }),
}; };
const rawKb = { const rawKb = {
down: vi.fn(async () => {}), down: vi.fn(async () => { }),
up: vi.fn(async () => {}), up: vi.fn(async () => { }),
type: vi.fn(async () => {}), type: vi.fn(async () => { }),
insertText: vi.fn(async () => {}), insertText: vi.fn(async () => { }),
}; };
const originals = { const originals = {
keyboardPress: vi.fn(async () => {}), keyboardPress: vi.fn(async () => { }),
keyboardDown: vi.fn(async () => {}), keyboardDown: vi.fn(async () => { }),
keyboardUp: vi.fn(async () => {}), keyboardUp: vi.fn(async () => { }),
}; };
const el = buildMockElementHandle(); const el = buildMockElementHandle();
@@ -825,23 +825,23 @@ describe("patchSingleElementHandle", () => {
move: vi.fn(async () => { moveCount++; }), move: vi.fn(async () => { moveCount++; }),
down: vi.fn(async () => { downCalled = true; }), down: vi.fn(async () => { downCalled = true; }),
up: vi.fn(async () => { upCalled = true; }), up: vi.fn(async () => { upCalled = true; }),
wheel: vi.fn(async () => {}), wheel: vi.fn(async () => { }),
}; };
const rawKb = { const rawKb = {
down: vi.fn(async () => {}), down: vi.fn(async () => { }),
up: vi.fn(async () => {}), up: vi.fn(async () => { }),
type: vi.fn(async () => {}), type: vi.fn(async () => { }),
insertText: vi.fn(async () => {}), insertText: vi.fn(async () => { }),
}; };
const originals = { const originals = {
keyboardPress: vi.fn(async () => {}), keyboardPress: vi.fn(async () => { }),
keyboardDown: vi.fn(async () => {}), keyboardDown: vi.fn(async () => { }),
keyboardUp: vi.fn(async () => {}), keyboardUp: vi.fn(async () => { }),
}; };
const el = buildMockElementHandle(); const el = buildMockElementHandle();
const page = buildMockPage(); const page = buildMockPage();
(page as any)._ensureCursorInit = vi.fn(async () => {}); (page as any)._ensureCursorInit = vi.fn(async () => { });
patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null); patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null);
@@ -859,22 +859,22 @@ describe("patchSingleElementHandle", () => {
let downCalled = false; let downCalled = false;
const raw = { const raw = {
move: vi.fn(async () => {}), move: vi.fn(async () => { }),
down: vi.fn(async () => { downCalled = true; }), down: vi.fn(async () => { downCalled = true; }),
up: vi.fn(async () => {}), up: vi.fn(async () => { }),
wheel: vi.fn(async () => {}), wheel: vi.fn(async () => { }),
}; };
const rawKb = { const rawKb = {
down: vi.fn(async () => {}), down: vi.fn(async () => { }),
up: vi.fn(async () => {}), up: vi.fn(async () => { }),
type: vi.fn(async () => {}), type: vi.fn(async () => { }),
insertText: vi.fn(async () => {}), insertText: vi.fn(async () => { }),
}; };
const originals = { keyboardPress: vi.fn(async () => {}), keyboardDown: vi.fn(async () => {}), keyboardUp: vi.fn(async () => {}) }; const originals = { keyboardPress: vi.fn(async () => { }), keyboardDown: vi.fn(async () => { }), keyboardUp: vi.fn(async () => { }) };
const el = buildMockElementHandle(); const el = buildMockElementHandle();
const page = buildMockPage(); const page = buildMockPage();
(page as any)._ensureCursorInit = vi.fn(async () => {}); (page as any)._ensureCursorInit = vi.fn(async () => { });
patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null); patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null);
@@ -890,22 +890,22 @@ describe("patchSingleElementHandle", () => {
const cursor = { x: 50, y: 50, initialized: true }; const cursor = { x: 50, y: 50, initialized: true };
const raw = { const raw = {
move: vi.fn(async () => {}), move: vi.fn(async () => { }),
down: vi.fn(async () => {}), down: vi.fn(async () => { }),
up: vi.fn(async () => {}), up: vi.fn(async () => { }),
wheel: vi.fn(async () => {}), wheel: vi.fn(async () => { }),
}; };
const rawKb = { const rawKb = {
down: vi.fn(async () => {}), down: vi.fn(async () => { }),
up: vi.fn(async () => {}), up: vi.fn(async () => { }),
type: vi.fn(async () => {}), type: vi.fn(async () => { }),
insertText: vi.fn(async () => {}), insertText: vi.fn(async () => { }),
}; };
const originals = { keyboardPress: vi.fn(async () => {}), keyboardDown: vi.fn(async () => {}), keyboardUp: vi.fn(async () => {}) }; const originals = { keyboardPress: vi.fn(async () => { }), keyboardDown: vi.fn(async () => { }), keyboardUp: vi.fn(async () => { }) };
const el = buildMockElementHandle({ evaluate: vi.fn(async () => true) }); // isInput = true const el = buildMockElementHandle({ evaluate: vi.fn(async () => true) }); // isInput = true
const page = buildMockPage(); const page = buildMockPage();
(page as any)._ensureCursorInit = vi.fn(async () => {}); (page as any)._ensureCursorInit = vi.fn(async () => { });
patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null); patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null);
@@ -923,26 +923,26 @@ describe("patchSingleElementHandle", () => {
const pressedKeys: string[] = []; const pressedKeys: string[] = [];
const raw = { const raw = {
move: vi.fn(async () => {}), move: vi.fn(async () => { }),
down: vi.fn(async () => {}), down: vi.fn(async () => { }),
up: vi.fn(async () => {}), up: vi.fn(async () => { }),
wheel: vi.fn(async () => {}), wheel: vi.fn(async () => { }),
}; };
const rawKb = { const rawKb = {
down: vi.fn(async () => {}), down: vi.fn(async () => { }),
up: vi.fn(async () => {}), up: vi.fn(async () => { }),
type: vi.fn(async () => {}), type: vi.fn(async () => { }),
insertText: vi.fn(async () => {}), insertText: vi.fn(async () => { }),
}; };
const originals = { const originals = {
keyboardPress: vi.fn(async (key: string) => { pressedKeys.push(key); }), keyboardPress: vi.fn(async (key: string) => { pressedKeys.push(key); }),
keyboardDown: vi.fn(async () => {}), keyboardDown: vi.fn(async () => { }),
keyboardUp: vi.fn(async () => {}), keyboardUp: vi.fn(async () => { }),
}; };
const el = buildMockElementHandle({ evaluate: vi.fn(async () => true) }); const el = buildMockElementHandle({ evaluate: vi.fn(async () => true) });
const page = buildMockPage(); const page = buildMockPage();
(page as any)._ensureCursorInit = vi.fn(async () => {}); (page as any)._ensureCursorInit = vi.fn(async () => { });
patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null); patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null);
@@ -957,9 +957,9 @@ describe("patchSingleElementHandle", () => {
const { patchSingleElementHandle } = await import("../src/human/elementhandle.js"); const { patchSingleElementHandle } = await import("../src/human/elementhandle.js");
const cfg = resolveConfig("default"); const cfg = resolveConfig("default");
const cursor = { x: 0, y: 0, initialized: false }; const cursor = { x: 0, y: 0, initialized: false };
const raw = { move: vi.fn(async () => {}), down: vi.fn(async () => {}), up: vi.fn(async () => {}), wheel: vi.fn(async () => {}) }; const raw = { move: vi.fn(async () => { }), down: vi.fn(async () => { }), up: vi.fn(async () => { }), wheel: vi.fn(async () => { }) };
const rawKb = { down: vi.fn(async () => {}), up: vi.fn(async () => {}), type: vi.fn(async () => {}), insertText: vi.fn(async () => {}) }; const rawKb = { down: vi.fn(async () => { }), up: vi.fn(async () => { }), type: vi.fn(async () => { }), insertText: vi.fn(async () => { }) };
const originals = { keyboardPress: vi.fn(async () => {}), keyboardDown: vi.fn(async () => {}), keyboardUp: vi.fn(async () => {}) }; const originals = { keyboardPress: vi.fn(async () => { }), keyboardDown: vi.fn(async () => { }), keyboardUp: vi.fn(async () => { }) };
const el = buildMockElementHandle(); const el = buildMockElementHandle();
const page = buildMockPage(); const page = buildMockPage();
@@ -976,9 +976,9 @@ describe("patchSingleElementHandle", () => {
const { patchSingleElementHandle } = await import("../src/human/elementhandle.js"); const { patchSingleElementHandle } = await import("../src/human/elementhandle.js");
const cfg = resolveConfig("default"); const cfg = resolveConfig("default");
const cursor = { x: 0, y: 0, initialized: false }; const cursor = { x: 0, y: 0, initialized: false };
const raw = { move: vi.fn(async () => {}), down: vi.fn(async () => {}), up: vi.fn(async () => {}), wheel: vi.fn(async () => {}) }; const raw = { move: vi.fn(async () => { }), down: vi.fn(async () => { }), up: vi.fn(async () => { }), wheel: vi.fn(async () => { }) };
const rawKb = { down: vi.fn(async () => {}), up: vi.fn(async () => {}), type: vi.fn(async () => {}), insertText: vi.fn(async () => {}) }; const rawKb = { down: vi.fn(async () => { }), up: vi.fn(async () => { }), type: vi.fn(async () => { }), insertText: vi.fn(async () => { }) };
const originals = { keyboardPress: vi.fn(async () => {}), keyboardDown: vi.fn(async () => {}), keyboardUp: vi.fn(async () => {}) }; const originals = { keyboardPress: vi.fn(async () => { }), keyboardDown: vi.fn(async () => { }), keyboardUp: vi.fn(async () => { }) };
const child = buildMockElementHandle(); const child = buildMockElementHandle();
const el = buildMockElementHandle(); const el = buildMockElementHandle();
@@ -998,9 +998,9 @@ describe("patchPageElementHandles", () => {
const { patchPageElementHandles } = await import("../src/human/elementhandle.js"); const { patchPageElementHandles } = await import("../src/human/elementhandle.js");
const cfg = resolveConfig("default"); const cfg = resolveConfig("default");
const cursor = { x: 0, y: 0, initialized: false }; const cursor = { x: 0, y: 0, initialized: false };
const raw = { move: vi.fn(async () => {}), down: vi.fn(async () => {}), up: vi.fn(async () => {}), wheel: vi.fn(async () => {}) }; const raw = { move: vi.fn(async () => { }), down: vi.fn(async () => { }), up: vi.fn(async () => { }), wheel: vi.fn(async () => { }) };
const rawKb = { down: vi.fn(async () => {}), up: vi.fn(async () => {}), type: vi.fn(async () => {}), insertText: vi.fn(async () => {}) }; const rawKb = { down: vi.fn(async () => { }), up: vi.fn(async () => { }), type: vi.fn(async () => { }), insertText: vi.fn(async () => { }) };
const originals = { keyboardPress: vi.fn(async () => {}), keyboardDown: vi.fn(async () => {}), keyboardUp: vi.fn(async () => {}) }; const originals = { keyboardPress: vi.fn(async () => { }), keyboardDown: vi.fn(async () => { }), keyboardUp: vi.fn(async () => { }) };
const el = buildMockElementHandle(); const el = buildMockElementHandle();
const page = buildMockPage(); const page = buildMockPage();
@@ -1018,9 +1018,9 @@ describe("patchPageElementHandles", () => {
const { patchPageElementHandles } = await import("../src/human/elementhandle.js"); const { patchPageElementHandles } = await import("../src/human/elementhandle.js");
const cfg = resolveConfig("default"); const cfg = resolveConfig("default");
const cursor = { x: 0, y: 0, initialized: false }; const cursor = { x: 0, y: 0, initialized: false };
const raw = { move: vi.fn(async () => {}), down: vi.fn(async () => {}), up: vi.fn(async () => {}), wheel: vi.fn(async () => {}) }; const raw = { move: vi.fn(async () => { }), down: vi.fn(async () => { }), up: vi.fn(async () => { }), wheel: vi.fn(async () => { }) };
const rawKb = { down: vi.fn(async () => {}), up: vi.fn(async () => {}), type: vi.fn(async () => {}), insertText: vi.fn(async () => {}) }; const rawKb = { down: vi.fn(async () => { }), up: vi.fn(async () => { }), type: vi.fn(async () => { }), insertText: vi.fn(async () => { }) };
const originals = { keyboardPress: vi.fn(async () => {}), keyboardDown: vi.fn(async () => {}), keyboardUp: vi.fn(async () => {}) }; const originals = { keyboardPress: vi.fn(async () => { }), keyboardDown: vi.fn(async () => { }), keyboardUp: vi.fn(async () => { }) };
const el1 = buildMockElementHandle(); const el1 = buildMockElementHandle();
const el2 = buildMockElementHandle(); const el2 = buildMockElementHandle();
@@ -1040,9 +1040,9 @@ describe("patchPageElementHandles", () => {
const { patchPageElementHandles } = await import("../src/human/elementhandle.js"); const { patchPageElementHandles } = await import("../src/human/elementhandle.js");
const cfg = resolveConfig("default"); const cfg = resolveConfig("default");
const cursor = { x: 0, y: 0, initialized: false }; const cursor = { x: 0, y: 0, initialized: false };
const raw = { move: vi.fn(async () => {}), down: vi.fn(async () => {}), up: vi.fn(async () => {}), wheel: vi.fn(async () => {}) }; const raw = { move: vi.fn(async () => { }), down: vi.fn(async () => { }), up: vi.fn(async () => { }), wheel: vi.fn(async () => { }) };
const rawKb = { down: vi.fn(async () => {}), up: vi.fn(async () => {}), type: vi.fn(async () => {}), insertText: vi.fn(async () => {}) }; const rawKb = { down: vi.fn(async () => { }), up: vi.fn(async () => { }), type: vi.fn(async () => { }), insertText: vi.fn(async () => { }) };
const originals = { keyboardPress: vi.fn(async () => {}), keyboardDown: vi.fn(async () => {}), keyboardUp: vi.fn(async () => {}) }; const originals = { keyboardPress: vi.fn(async () => { }), keyboardDown: vi.fn(async () => { }), keyboardUp: vi.fn(async () => { }) };
const el = buildMockElementHandle(); const el = buildMockElementHandle();
const page = buildMockPage(); const page = buildMockPage();
@@ -1060,9 +1060,9 @@ describe("patchPageElementHandles", () => {
const { patchPageElementHandles } = await import("../src/human/elementhandle.js"); const { patchPageElementHandles } = await import("../src/human/elementhandle.js");
const cfg = resolveConfig("default"); const cfg = resolveConfig("default");
const cursor = { x: 0, y: 0, initialized: false }; const cursor = { x: 0, y: 0, initialized: false };
const raw = { move: vi.fn(async () => {}), down: vi.fn(async () => {}), up: vi.fn(async () => {}), wheel: vi.fn(async () => {}) }; const raw = { move: vi.fn(async () => { }), down: vi.fn(async () => { }), up: vi.fn(async () => { }), wheel: vi.fn(async () => { }) };
const rawKb = { down: vi.fn(async () => {}), up: vi.fn(async () => {}), type: vi.fn(async () => {}), insertText: vi.fn(async () => {}) }; const rawKb = { down: vi.fn(async () => { }), up: vi.fn(async () => { }), type: vi.fn(async () => { }), insertText: vi.fn(async () => { }) };
const originals = { keyboardPress: vi.fn(async () => {}), keyboardDown: vi.fn(async () => {}), keyboardUp: vi.fn(async () => {}) }; const originals = { keyboardPress: vi.fn(async () => { }), keyboardDown: vi.fn(async () => { }), keyboardUp: vi.fn(async () => { }) };
const page = buildMockPage(); const page = buildMockPage();
(page as any).$ = vi.fn(async () => null); (page as any).$ = vi.fn(async () => null);
@@ -1106,19 +1106,19 @@ function buildMockFrame(): any {
locator.first = vi.fn(() => locator); locator.first = vi.fn(() => locator);
return { return {
click: vi.fn(async () => {}), click: vi.fn(async () => { }),
dblclick: vi.fn(async () => {}), dblclick: vi.fn(async () => { }),
hover: vi.fn(async () => {}), hover: vi.fn(async () => { }),
type: vi.fn(async () => {}), type: vi.fn(async () => { }),
fill: vi.fn(async () => {}), fill: vi.fn(async () => { }),
check: vi.fn(async () => {}), check: vi.fn(async () => { }),
uncheck: vi.fn(async () => {}), uncheck: vi.fn(async () => { }),
selectOption: vi.fn(async () => {}), selectOption: vi.fn(async () => { }),
press: vi.fn(async () => {}), press: vi.fn(async () => { }),
pressSequentially: vi.fn(async () => {}), pressSequentially: vi.fn(async () => { }),
tap: vi.fn(async () => {}), tap: vi.fn(async () => { }),
clear: vi.fn(async () => {}), clear: vi.fn(async () => { }),
dragAndDrop: vi.fn(async () => {}), dragAndDrop: vi.fn(async () => { }),
locator: vi.fn(() => locator), locator: vi.fn(() => locator),
childFrames: vi.fn(() => []), childFrames: vi.fn(() => []),
}; };
@@ -1169,10 +1169,10 @@ describe("page.click(selector, { timeout }) forwards timeout to scroll", () => {
locator: vi.fn(() => ({ first: () => ({ boundingBox }) })), locator: vi.fn(() => ({ first: () => ({ boundingBox }) })),
}; };
const raw = { const raw = {
move: vi.fn(async () => {}), move: vi.fn(async () => { }),
down: vi.fn(async () => {}), down: vi.fn(async () => { }),
up: vi.fn(async () => {}), up: vi.fn(async () => { }),
wheel: vi.fn(async () => {}), wheel: vi.fn(async () => { }),
}; };
await scrollToElement(page, raw, "#x", 0, 0, cfg, 5000); await scrollToElement(page, raw, "#x", 0, 0, cfg, 5000);
@@ -1189,10 +1189,10 @@ describe("page.click(selector, { timeout }) forwards timeout to scroll", () => {
locator: vi.fn(() => ({ first: () => ({ boundingBox }) })), locator: vi.fn(() => ({ first: () => ({ boundingBox }) })),
}; };
const raw = { const raw = {
move: vi.fn(async () => {}), move: vi.fn(async () => { }),
down: vi.fn(async () => {}), down: vi.fn(async () => { }),
up: vi.fn(async () => {}), up: vi.fn(async () => { }),
wheel: vi.fn(async () => {}), wheel: vi.fn(async () => { }),
}; };
await scrollToElement(page, raw, "#x", 0, 0, cfg); await scrollToElement(page, raw, "#x", 0, 0, cfg);
@@ -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);
@@ -1313,13 +1313,13 @@ describe("page.type / page.fill accept per-call human_config override", () => {
async (_page, _raw, _text, callCfg) => { captured = callCfg; }, async (_page, _raw, _text, callCfg) => { captured = callCfg; },
); );
const raw = { move: vi.fn(async () => {}), down: vi.fn(async () => {}), up: vi.fn(async () => {}), wheel: vi.fn(async () => {}) }; const raw = { move: vi.fn(async () => { }), down: vi.fn(async () => { }), up: vi.fn(async () => { }), wheel: vi.fn(async () => { }) };
const rawKb = { down: vi.fn(async () => {}), up: vi.fn(async () => {}), type: vi.fn(async () => {}), insertText: vi.fn(async () => {}) }; const rawKb = { down: vi.fn(async () => { }), up: vi.fn(async () => { }), type: vi.fn(async () => { }), insertText: vi.fn(async () => { }) };
const originals = { keyboardPress: vi.fn(async () => {}), keyboardDown: vi.fn(async () => {}), keyboardUp: vi.fn(async () => {}) }; const originals = { keyboardPress: vi.fn(async () => { }), keyboardDown: vi.fn(async () => { }), keyboardUp: vi.fn(async () => { }) };
const el = buildMockElementHandle({ evaluate: vi.fn(async () => true) }); const el = buildMockElementHandle({ evaluate: vi.fn(async () => true) });
const page = buildMockPage(); const page = buildMockPage();
(page as any)._ensureCursorInit = vi.fn(async () => {}); (page as any)._ensureCursorInit = vi.fn(async () => { });
patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null); patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null);
@@ -1341,10 +1341,10 @@ describe("humanScrollIntoView", () => {
const page: any = { viewportSize: () => ({ width: 1280, height: 720 }) }; const page: any = { viewportSize: () => ({ width: 1280, height: 720 }) };
const raw = { const raw = {
move: vi.fn(async () => {}), move: vi.fn(async () => { }),
down: vi.fn(async () => {}), down: vi.fn(async () => { }),
up: vi.fn(async () => {}), up: vi.fn(async () => { }),
wheel: vi.fn(async () => {}), wheel: vi.fn(async () => { }),
}; };
// Box centered in viewport — squarely in scroll_target_zone // Box centered in viewport — squarely in scroll_target_zone
const inViewBox = { x: 200, y: 300, width: 50, height: 30 }; const inViewBox = { x: 200, y: 300, width: 50, height: 30 };
@@ -1366,10 +1366,10 @@ describe("humanScrollIntoView", () => {
const page: any = { viewportSize: () => ({ width: 1280, height: 720 }) }; const page: any = { viewportSize: () => ({ width: 1280, height: 720 }) };
const raw = { const raw = {
move: vi.fn(async () => {}), move: vi.fn(async () => { }),
down: vi.fn(async () => {}), down: vi.fn(async () => { }),
up: vi.fn(async () => {}), up: vi.fn(async () => { }),
wheel: vi.fn(async () => {}), wheel: vi.fn(async () => { }),
}; };
const boxes = [ const boxes = [
@@ -1403,14 +1403,14 @@ describe("el.scrollIntoViewIfNeeded humanization", () => {
const cfg = resolveConfig("default", { idle_between_actions: false }); const cfg = resolveConfig("default", { idle_between_actions: false });
const cursor = { x: 50, y: 50, initialized: true }; const cursor = { x: 50, y: 50, initialized: true };
const raw = { move: vi.fn(async () => {}), down: vi.fn(async () => {}), up: vi.fn(async () => {}), wheel: vi.fn(async () => {}) }; const raw = { move: vi.fn(async () => { }), down: vi.fn(async () => { }), up: vi.fn(async () => { }), wheel: vi.fn(async () => { }) };
const rawKb = { down: vi.fn(async () => {}), up: vi.fn(async () => {}), type: vi.fn(async () => {}), insertText: vi.fn(async () => {}) }; const rawKb = { down: vi.fn(async () => { }), up: vi.fn(async () => { }), type: vi.fn(async () => { }), insertText: vi.fn(async () => { }) };
const originals = { keyboardPress: vi.fn(async () => {}), keyboardDown: vi.fn(async () => {}), keyboardUp: vi.fn(async () => {}) }; const originals = { keyboardPress: vi.fn(async () => { }), keyboardDown: vi.fn(async () => { }), keyboardUp: vi.fn(async () => { }) };
const el = buildMockElementHandle(); const el = buildMockElementHandle();
el.scrollIntoViewIfNeeded = vi.fn(async () => {}); el.scrollIntoViewIfNeeded = vi.fn(async () => { });
const page = buildMockPage(); const page = buildMockPage();
(page as any)._ensureCursorInit = vi.fn(async () => {}); (page as any)._ensureCursorInit = vi.fn(async () => { });
patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null); patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null);
await el.scrollIntoViewIfNeeded(); await el.scrollIntoViewIfNeeded();
@@ -1429,15 +1429,15 @@ describe("el.scrollIntoViewIfNeeded humanization", () => {
const cfg = resolveConfig("default", { idle_between_actions: false }); const cfg = resolveConfig("default", { idle_between_actions: false });
const cursor = { x: 50, y: 50, initialized: true }; const cursor = { x: 50, y: 50, initialized: true };
const raw = { move: vi.fn(async () => {}), down: vi.fn(async () => {}), up: vi.fn(async () => {}), wheel: vi.fn(async () => {}) }; const raw = { move: vi.fn(async () => { }), down: vi.fn(async () => { }), up: vi.fn(async () => { }), wheel: vi.fn(async () => { }) };
const rawKb = { down: vi.fn(async () => {}), up: vi.fn(async () => {}), type: vi.fn(async () => {}), insertText: vi.fn(async () => {}) }; const rawKb = { down: vi.fn(async () => { }), up: vi.fn(async () => { }), type: vi.fn(async () => { }), insertText: vi.fn(async () => { }) };
const originals = { keyboardPress: vi.fn(async () => {}), keyboardDown: vi.fn(async () => {}), keyboardUp: vi.fn(async () => {}) }; const originals = { keyboardPress: vi.fn(async () => { }), keyboardDown: vi.fn(async () => { }), keyboardUp: vi.fn(async () => { }) };
const nativeFallback = vi.fn(async () => {}); const nativeFallback = vi.fn(async () => { });
const el = buildMockElementHandle(); const el = buildMockElementHandle();
el.scrollIntoViewIfNeeded = nativeFallback; el.scrollIntoViewIfNeeded = nativeFallback;
const page = buildMockPage(); const page = buildMockPage();
(page as any)._ensureCursorInit = vi.fn(async () => {}); (page as any)._ensureCursorInit = vi.fn(async () => { });
patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null); patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null);
await el.scrollIntoViewIfNeeded(); await el.scrollIntoViewIfNeeded();
+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)
// ========================================================================= // =========================================================================