/** * Unit tests for stealth / anti-detection fixes — PUPPETEER EDITION. * * Covers: * - StealthEval — CDP isolated-world lifecycle (evaluate, invalidate, retry) * - isInputElement / isSelectorFocused — stealth DOM queries with fallback * - typeShiftSymbol — CDP Input.dispatchKeyEvent path vs evaluate fallback * - humanType integration — shift symbols routed via CDP * - Navigation invalidation (goto → stealth.invalidate) * - patchPage stealth infrastructure wiring * - SHIFT_SYMBOL_CODES / SHIFT_SYMBOL_KEYCODES completeness * - focus() — human-like click instead of programmatic CDP focus * - uncheck() — fallback behavior matches Playwright (assume checked on error) * - mouse.wheel() — smooth scroll via smoothWheel * - mouse.dragAndDrop() — Bézier drag between coordinates * - ElementHandle patching — click, hover, type, press, tap, focus, select * - Frame patching — delegates to page-level humanized methods * - Browser-level patching — newPage, createBrowserContext, targetcreated * * All tests are fast, mock-based, and do NOT require a browser. */ import { describe, it, expect, vi, beforeEach } from "vitest"; import { resolveConfig, rand, randRange, sleep } from "../src/human/config.js"; import { humanType } from "../src/human-puppeteer/keyboard.js"; import { humanMove, humanClick, clickTarget, humanIdle } from "../src/human/mouse.js"; // ========================================================================= // Helper: build mock page / raw objects (Puppeteer-style) // ========================================================================= function buildMockPage(overrides: Record = {}): any { const mainFrameObj = overrides.mainFrameReturn ?? { childFrames: vi.fn(() => []), click: vi.fn(async () => {}), hover: vi.fn(async () => {}), type: vi.fn(async () => {}), fill: vi.fn(async () => {}), check: vi.fn(async () => {}), uncheck: vi.fn(async () => {}), select: vi.fn(async () => []), press: vi.fn(async () => {}), clear: vi.fn(async () => {}), focus: vi.fn(async () => {}), tap: vi.fn(async () => {}), dragAndDrop: vi.fn(async () => {}), $: vi.fn(async () => null), $$: vi.fn(async () => []), waitForSelector: vi.fn(async () => null), }; const page: any = { evaluate: overrides.evaluate ?? vi.fn(async () => false), mouse: { move: vi.fn(async () => {}), down: vi.fn(async () => {}), up: vi.fn(async () => {}), click: vi.fn(async () => {}), wheel: vi.fn(async () => {}), dragAndDrop: overrides.mouseDragAndDrop ?? vi.fn(async () => {}), }, keyboard: { press: overrides.keyboardPress ? vi.fn(overrides.keyboardPress) : vi.fn(async () => {}), type: vi.fn(async () => {}), down: vi.fn(async () => {}), up: vi.fn(async () => {}), sendCharacter: vi.fn(async () => {}), }, click: vi.fn(async () => {}), hover: vi.fn(async () => {}), type: vi.fn(async () => {}), fill: vi.fn(async () => {}), check: vi.fn(async () => {}), uncheck: vi.fn(async () => {}), select: vi.fn(async () => []), press: vi.fn(async () => {}), focus: vi.fn(async () => {}), goto: vi.fn(async () => ({})), tap: vi.fn(async () => {}), clear: vi.fn(async () => {}), $: overrides.$ ?? vi.fn(async () => null), $$: overrides.$$ ?? vi.fn(async () => []), waitForSelector: overrides.waitForSelector ?? vi.fn(async () => null), // Puppeteer-specific: viewport() returns object (not viewportSize()) viewport: vi.fn(() => ({ width: 1280, height: 720 })), mainFrame: vi.fn(() => mainFrameObj), frames: vi.fn(() => []), // Puppeteer-specific: createCDPSession on page, not context createCDPSession: vi.fn(async () => buildMockCDP()), url: vi.fn(() => "about:blank"), }; return page; } function buildMockCDP(overrides: Record = {}): any { return { send: overrides.send ?? vi.fn(async (method: string, params?: any) => { if (method === "Page.getFrameTree") { return { frameTree: { frame: { id: "F1" } } }; } if (method === "Page.createIsolatedWorld") { return { executionContextId: 42 }; } if (method === "Runtime.evaluate") { return { result: { value: false } }; } return {}; }), }; } function buildRawKeyboard() { const downKeys: string[] = []; const upKeys: string[] = []; const insertedChars: string[] = []; const raw = { down: vi.fn(async (k: string) => { downKeys.push(k); }), up: vi.fn(async (k: string) => { upKeys.push(k); }), type: vi.fn(async () => {}), insertText: vi.fn(async (t: string) => { insertedChars.push(t); }), }; return { raw, downKeys, upKeys, insertedChars }; } function buildMockElementHandle(overrides: Record = {}): any { return { click: vi.fn(async () => {}), hover: vi.fn(async () => {}), type: vi.fn(async () => {}), press: vi.fn(async () => {}), tap: vi.fn(async () => {}), focus: vi.fn(async () => {}), select: vi.fn(async () => []), drop: vi.fn(async () => {}), dragAndDrop: vi.fn(async () => {}), boundingBox: overrides.boundingBox ?? vi.fn(async () => ({ x: 100, y: 200, width: 120, height: 30 })), evaluate: overrides.evaluate ?? vi.fn(async () => false), $: vi.fn(async () => null), $$: vi.fn(async () => []), waitForSelector: vi.fn(async () => null), }; } function buildMockBrowser(pages: any[] = []): any { const browser: any = { pages: vi.fn(async () => pages), newPage: vi.fn(async () => buildMockPage()), createBrowserContext: vi.fn(async () => ({ newPage: vi.fn(async () => buildMockPage()), })), on: vi.fn(), close: vi.fn(async () => {}), }; return browser; } // ========================================================================= // SHIFT_SYMBOL_CODES / SHIFT_SYMBOL_KEYCODES completeness // ========================================================================= describe("Puppeteer: SHIFT_SYMBOL maps completeness", () => { it("every shift symbol has a code and keycode entry", async () => { const cfg = resolveConfig("default", { mistype_chance: 0 }); const SHIFT_SYMBOLS = ['@', '#', '!', '$', '%', '^', '&', '*', '(', ')', '_', '+', '{', '}', '|', ':', '"', '<', '>', '?', '~']; for (const sym of SHIFT_SYMBOLS) { const { raw } = buildRawKeyboard(); const page = buildMockPage(); const mockCdp = { send: vi.fn(async () => ({})), }; await humanType(page, raw, sym, cfg, mockCdp as any); const cdpCalls = mockCdp.send.mock.calls; const keyEvents = cdpCalls.filter( (c: any[]) => c[0] === "Input.dispatchKeyEvent" ); expect(keyEvents.length).toBe(2); expect(page.evaluate).not.toHaveBeenCalled(); } }); it("all shift symbol keyDown events have correct structure", async () => { const cfg = resolveConfig("default", { mistype_chance: 0 }); const { raw } = buildRawKeyboard(); const page = buildMockPage(); const cdpCalls: Array<[string, any]> = []; const mockCdp = { send: vi.fn(async (method: string, params: any) => { cdpCalls.push([method, params]); return {}; }), }; await humanType(page, raw, "!", cfg, mockCdp as any); const keyDown = cdpCalls.find( ([m, p]) => m === "Input.dispatchKeyEvent" && p.type === "keyDown" ); expect(keyDown).toBeDefined(); const params = keyDown![1]; expect(params.key).toBe("!"); expect(params.modifiers).toBe(8); expect(typeof params.code).toBe("string"); expect(params.code.length).toBeGreaterThan(0); expect(typeof params.windowsVirtualKeyCode).toBe("number"); expect(params.windowsVirtualKeyCode).toBeGreaterThan(0); expect(params.text).toBe("!"); expect(params.unmodifiedText).toBe("!"); }); it("keyUp event has no text/unmodifiedText fields", async () => { const cfg = resolveConfig("default", { mistype_chance: 0 }); const { raw } = buildRawKeyboard(); const page = buildMockPage(); const cdpCalls: Array<[string, any]> = []; const mockCdp = { send: vi.fn(async (method: string, params: any) => { cdpCalls.push([method, params]); return {}; }), }; await humanType(page, raw, "!", cfg, mockCdp as any); const keyUp = cdpCalls.find( ([m, p]) => m === "Input.dispatchKeyEvent" && p.type === "keyUp" ); expect(keyUp).toBeDefined(); const params = keyUp![1]; expect(params.text).toBeUndefined(); expect(params.unmodifiedText).toBeUndefined(); }); it("digit shift symbols have correct keycodes (49-57, 48)", async () => { const digitSymbols = ['!', '@', '#', '$', '%', '^', '&', '*', '(', ')']; const expectedKeycodes = [49, 50, 51, 52, 53, 54, 55, 56, 57, 48]; const cfg = resolveConfig("default", { mistype_chance: 0 }); for (let i = 0; i < digitSymbols.length; i++) { const { raw } = buildRawKeyboard(); const page = buildMockPage(); const cdpCalls: Array<[string, any]> = []; const mockCdp = { send: vi.fn(async (method: string, params: any) => { cdpCalls.push([method, params]); return {}; }), }; await humanType(page, raw, digitSymbols[i], cfg, mockCdp as any); const keyDown = cdpCalls.find( ([m, p]) => m === "Input.dispatchKeyEvent" && p.type === "keyDown" ); expect(keyDown).toBeDefined(); expect(keyDown![1].windowsVirtualKeyCode).toBe(expectedKeycodes[i]); } }); }); // ========================================================================= // typeShiftSymbol — CDP path vs fallback (Puppeteer keyboard.ts) // ========================================================================= describe("Puppeteer: typeShiftSymbol CDP vs fallback", () => { it("uses CDP path when cdpSession is provided (no page.evaluate)", async () => { const cfg = resolveConfig("default", { mistype_chance: 0 }); const { raw } = buildRawKeyboard(); const page = buildMockPage(); const mockCdp = { send: vi.fn(async () => ({})) }; await humanType(page, raw, "@", cfg, mockCdp as any); expect(page.evaluate).not.toHaveBeenCalled(); expect(mockCdp.send).toHaveBeenCalled(); }); it("CDP path does NOT call raw.insertText for shift symbols", async () => { const cfg = resolveConfig("default", { mistype_chance: 0 }); const { raw, insertedChars } = buildRawKeyboard(); const page = buildMockPage(); const mockCdp = { send: vi.fn(async () => ({})) }; await humanType(page, raw, "#", cfg, mockCdp as any); expect(insertedChars.length).toBe(0); }); it("falls back to page.evaluate when no cdpSession", async () => { const cfg = resolveConfig("default", { mistype_chance: 0 }); const { raw, insertedChars } = buildRawKeyboard(); const page = buildMockPage(); await humanType(page, raw, "$", cfg, null); expect(page.evaluate).toHaveBeenCalled(); expect(insertedChars).toContain("$"); }); it("fallback path calls raw.insertText before page.evaluate", async () => { const cfg = resolveConfig("default", { mistype_chance: 0 }); const callOrder: string[] = []; const raw = { down: vi.fn(async () => { callOrder.push("raw.down"); }), up: vi.fn(async () => { callOrder.push("raw.up"); }), type: vi.fn(async () => {}), insertText: vi.fn(async () => { callOrder.push("raw.insertText"); }), }; const page = buildMockPage({ evaluate: vi.fn(async () => { callOrder.push("page.evaluate"); }), }); await humanType(page, raw, "%", cfg, null); const insertIdx = callOrder.indexOf("raw.insertText"); const evalIdx = callOrder.indexOf("page.evaluate"); expect(insertIdx).toBeLessThan(evalIdx); }); it("Shift is held during CDP key events", async () => { const cfg = resolveConfig("default", { mistype_chance: 0 }); const callOrder: string[] = []; const raw = { down: vi.fn(async (k: string) => { callOrder.push(`raw.down(${k})`); }), up: vi.fn(async (k: string) => { callOrder.push(`raw.up(${k})`); }), type: vi.fn(async () => {}), insertText: vi.fn(async () => {}), }; const page = buildMockPage(); const mockCdp = { send: vi.fn(async (method: string, params: any) => { callOrder.push(`cdp.${params.type || method}`); return {}; }), }; await humanType(page, raw, "!", cfg, mockCdp as any); const shiftDownIdx = callOrder.indexOf("raw.down(Shift)"); const keyDownIdx = callOrder.indexOf("cdp.keyDown"); const keyUpIdx = callOrder.indexOf("cdp.keyUp"); const shiftUpIdx = callOrder.indexOf("raw.up(Shift)"); expect(shiftDownIdx).toBeLessThan(keyDownIdx); expect(keyDownIdx).toBeLessThan(keyUpIdx); expect(keyUpIdx).toBeLessThan(shiftUpIdx); }); }); // ========================================================================= // humanType integration — mixed text with CDP (Puppeteer keyboard.ts) // ========================================================================= describe("Puppeteer: humanType mixed text with CDP", () => { it("normal chars use raw.down/up, shift symbols use CDP", async () => { const cfg = resolveConfig("default", { mistype_chance: 0 }); const { raw, downKeys } = buildRawKeyboard(); const page = buildMockPage(); const cdpCalls: Array<[string, any]> = []; const mockCdp = { send: vi.fn(async (method: string, params: any) => { cdpCalls.push([method, params]); return {}; }), }; await humanType(page, raw, "a!", cfg, mockCdp as any); expect(downKeys).toContain("a"); const keyEvents = cdpCalls.filter( ([m]) => m === "Input.dispatchKeyEvent" ); expect(keyEvents.length).toBe(2); expect(page.evaluate).not.toHaveBeenCalled(); }); it("text without shift symbols does not call CDP", async () => { const cfg = resolveConfig("default", { mistype_chance: 0 }); const { raw } = buildRawKeyboard(); const page = buildMockPage(); const mockCdp = { send: vi.fn(async () => ({})) }; await humanType(page, raw, "hello", cfg, mockCdp as any); expect(mockCdp.send).not.toHaveBeenCalled(); expect(page.evaluate).not.toHaveBeenCalled(); }); it("multiple shift symbols all go through CDP", async () => { const cfg = resolveConfig("default", { mistype_chance: 0 }); const { raw } = buildRawKeyboard(); const page = buildMockPage(); const cdpCalls: Array<[string, any]> = []; const mockCdp = { send: vi.fn(async (method: string, params: any) => { cdpCalls.push([method, params]); return {}; }), }; await humanType(page, raw, "!@#", cfg, mockCdp as any); expect(page.evaluate).not.toHaveBeenCalled(); const keyEvents = cdpCalls.filter( ([m]) => m === "Input.dispatchKeyEvent" ); expect(keyEvents.length).toBe(6); }); it("'Hello World!' — no page.evaluate leak", async () => { const cfg = resolveConfig("default", { mistype_chance: 0 }); const { raw } = buildRawKeyboard(); const page = buildMockPage(); const mockCdp = { send: vi.fn(async () => ({})) }; await humanType(page, raw, "Hello World!", cfg, mockCdp as any); expect(page.evaluate).not.toHaveBeenCalled(); }); it("password-like text 'SecurePass!123' uses CDP for '!'", async () => { const cfg = resolveConfig("default", { mistype_chance: 0 }); const { raw } = buildRawKeyboard(); const page = buildMockPage(); const cdpCalls: Array<[string, any]> = []; const mockCdp = { send: vi.fn(async (method: string, params: any) => { cdpCalls.push([method, params]); return {}; }), }; await humanType(page, raw, "SecurePass!123", cfg, mockCdp as any); const keyEvents = cdpCalls.filter( ([m]) => m === "Input.dispatchKeyEvent" ); expect(keyEvents.length).toBe(2); expect(keyEvents[0][1].key).toBe("!"); expect(page.evaluate).not.toHaveBeenCalled(); }); it("CDP modifier flag is always 8 (Shift)", async () => { const cfg = resolveConfig("default", { mistype_chance: 0, typing_delay: 0, shift_down_delay: [0, 0], shift_up_delay: [0, 0], key_hold: [0, 0], }); const allSymbols = '@#!$%^&*()_+{}|:"<>?~'; const { raw } = buildRawKeyboard(); const page = buildMockPage(); const cdpCalls: Array<[string, any]> = []; const mockCdp = { send: vi.fn(async (method: string, params: any) => { cdpCalls.push([method, params]); return {}; }), }; await humanType(page, raw, allSymbols, cfg, mockCdp as any); for (const [method, params] of cdpCalls) { if (method === "Input.dispatchKeyEvent") { expect(params.modifiers).toBe(8); } } }, 30000); }); // ========================================================================= // Non-ASCII text does NOT go through CDP shift path // ========================================================================= describe("Puppeteer: non-ASCII text avoids CDP shift path", () => { it("Cyrillic text uses insertText, not CDP", async () => { const cfg = resolveConfig("default", { mistype_chance: 0 }); const { raw, insertedChars } = buildRawKeyboard(); const page = buildMockPage(); const mockCdp = { send: vi.fn(async () => ({})) }; await humanType(page, raw, "Привет", cfg, mockCdp as any); expect(insertedChars.join("")).toBe("Привет"); expect(mockCdp.send).not.toHaveBeenCalled(); expect(page.evaluate).not.toHaveBeenCalled(); }); it("mixed text: ASCII + Cyrillic + shift symbol", async () => { const cfg = resolveConfig("default", { mistype_chance: 0 }); const { raw, downKeys, insertedChars } = buildRawKeyboard(); const page = buildMockPage(); const cdpCalls: Array<[string, any]> = []; const mockCdp = { send: vi.fn(async (method: string, params: any) => { cdpCalls.push([method, params]); return {}; }), }; await humanType(page, raw, "Hi! Мир", cfg, mockCdp as any); expect(downKeys).toContain("Shift"); expect(downKeys).toContain("H"); expect(downKeys).toContain("i"); const keyEvents = cdpCalls.filter(([m]) => m === "Input.dispatchKeyEvent"); expect(keyEvents.length).toBe(2); expect(downKeys).toContain(" "); expect(insertedChars).toContain("М"); expect(insertedChars).toContain("и"); expect(insertedChars).toContain("р"); expect(page.evaluate).not.toHaveBeenCalled(); }); }); // ========================================================================= // 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) // ========================================================================= describe("Puppeteer: patchPage stealth infrastructure", () => { it("page._stealth is a StealthEval instance after patching", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const page = buildMockPage(); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); expect((page as any)._stealth).toBeDefined(); expect(typeof (page as any)._stealth.evaluate).toBe("function"); expect(typeof (page as any)._stealth.invalidate).toBe("function"); expect(typeof (page as any)._stealth.getCdpSession).toBe("function"); }); it("page._original and page._humanCfg are set", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const page = buildMockPage(); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); expect((page as any)._original).toBeDefined(); expect((page as any)._humanCfg).toBe(cfg); }); it("goto invalidates stealth context", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const page = buildMockPage(); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); const stealth = (page as any)._stealth; const invalidateSpy = vi.spyOn(stealth, "invalidate"); await page.goto("https://example.com"); expect(invalidateSpy).toHaveBeenCalled(); }); it("internal helpers are stored on page for element/frame patching", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const page = buildMockPage(); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); // Only the helpers actually used by ElementHandle/Frame patching expect(typeof (page as any)._ensureCursorInit).toBe("function"); expect((page as any)._humanCursor).toBeDefined(); expect((page as any)._humanRaw).toBeDefined(); expect((page as any)._humanRawKb).toBeDefined(); }); it("page.createCDPSession is used (not context.newCDPSession)", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const page = buildMockPage(); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); const stealth = (page as any)._stealth; await stealth.getCdpSession(); expect(page.createCDPSession).toHaveBeenCalled(); }); }); // ========================================================================= // StealthEval lifecycle (Puppeteer — via page.createCDPSession) // ========================================================================= describe("Puppeteer: StealthEval lifecycle", () => { it("stealth.invalidate() is callable without error", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const page = buildMockPage(); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); const stealth = (page as any)._stealth; expect(() => stealth.invalidate()).not.toThrow(); }); it("stealth.getCdpSession() returns a CDP session", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const page = buildMockPage(); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); const stealth = (page as any)._stealth; const session = await stealth.getCdpSession(); expect(session).toBeDefined(); expect(typeof session.send).toBe("function"); }); it("stealth.evaluate() creates world and returns value", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const mockCdp = buildMockCDP({ send: vi.fn(async (method: string, params?: any) => { if (method === "Page.getFrameTree") { return { frameTree: { frame: { id: "F1" } } }; } if (method === "Page.createIsolatedWorld") { return { executionContextId: 42 }; } if (method === "Runtime.evaluate") { return { result: { value: true } }; } return {}; }), }); const page = buildMockPage(); page.createCDPSession = vi.fn(async () => mockCdp); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); const stealth = (page as any)._stealth; const result = await stealth.evaluate("1 + 1"); expect(result).toBe(true); }); it("stealth.evaluate() retries on exceptionDetails", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); let attempt = 0; const mockCdp = buildMockCDP({ send: vi.fn(async (method: string, params?: any) => { if (method === "Page.getFrameTree") { return { frameTree: { frame: { id: "F1" } } }; } if (method === "Page.createIsolatedWorld") { return { executionContextId: 50 + attempt }; } if (method === "Runtime.evaluate") { attempt++; if (attempt === 1) { return { exceptionDetails: { text: "stale" } }; } return { result: { value: "recovered" } }; } return {}; }), }); const page = buildMockPage(); page.createCDPSession = vi.fn(async () => mockCdp); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); const stealth = (page as any)._stealth; const result = await stealth.evaluate("test"); expect(result).toBe("recovered"); }); it("stealth.evaluate() returns undefined after double failure", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const mockCdp = buildMockCDP({ send: vi.fn(async (method: string) => { if (method === "Page.getFrameTree") { return { frameTree: { frame: { id: "F1" } } }; } if (method === "Page.createIsolatedWorld") { return { executionContextId: 70 }; } if (method === "Runtime.evaluate") { return { exceptionDetails: { text: "always broken" } }; } return {}; }), }); const page = buildMockPage(); page.createCDPSession = vi.fn(async () => mockCdp); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); const stealth = (page as any)._stealth; const result = await stealth.evaluate("broken"); expect(result).toBeUndefined(); }); }); // ========================================================================= // focus() — human-like click instead of programmatic focus // ========================================================================= describe("Puppeteer: focus() humanization", () => { it("page.focus is replaced with humanized version", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const page = buildMockPage(); const originalFocus = page.focus; const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); expect(page.focus).not.toBe(originalFocus); expect(typeof page.focus).toBe("function"); }); it("focus() calls click when element is not focused", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const mockCdp = buildMockCDP({ send: vi.fn(async (method: string, params?: any) => { if (method === "Page.getFrameTree") { return { frameTree: { frame: { id: "F1" } } }; } if (method === "Page.createIsolatedWorld") { return { executionContextId: 42 }; } if (method === "Runtime.evaluate") { // Return false = element is NOT focused → should click return { result: { value: false } }; } return {}; }), }); const page = buildMockPage(); page.createCDPSession = vi.fn(async () => mockCdp); const cfg = resolveConfig("default"); const cursor = { x: 100, y: 100, initialized: true }; patchPage(page as any, cfg, cursor as any); // page.focus is patched — it delegates to click internally expect(typeof page.focus).toBe("function"); // Verify it's not the original expect(page.focus).not.toBe((page as any)._original.focus); }); it("focus is patched on page for frame delegation", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const page = buildMockPage(); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; const originalFocus = page.focus; patchPage(page as any, cfg, cursor as any); // focus is patched directly on page — frames delegate to page.focus expect(page.focus).not.toBe(originalFocus); expect(typeof page.focus).toBe("function"); }); }); // ========================================================================= // uncheck() — fallback behavior (assume checked on error) // ========================================================================= describe("Puppeteer: select() humanization", () => { it("page.select is replaced with humanized version", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const page = buildMockPage(); const originalSelect = page.select; const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); expect(page.select).not.toBe(originalSelect); expect(typeof page.select).toBe("function"); }); it("select() hovers before delegating to original", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const page = buildMockPage(); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); // Verify original select is stored const originals = (page as any)._original; expect(typeof originals.select).toBe("function"); }); }); // ========================================================================= // mouse.wheel() — smooth scroll // ========================================================================= describe("Puppeteer: mouse.wheel() smooth scroll", () => { it("mouse.wheel is patched after patchPage", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const page = buildMockPage(); const originalWheel = page.mouse.wheel; const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); expect(page.mouse.wheel).not.toBe(originalWheel); }); it("mouse.wheel({deltaY: 300}) calls original wheel multiple times (smooth)", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const wheelCalls: any[] = []; const page = buildMockPage(); const origWheel = vi.fn(async (opts: any) => { wheelCalls.push(opts); }); page.mouse.wheel = origWheel; const cfg = resolveConfig("default"); const cursor = { x: 100, y: 100, initialized: true }; patchPage(page as any, cfg, cursor as any); await page.mouse.wheel({ deltaY: 300 }); // smoothWheel breaks 300px into multiple small chunks (20-40px each) // So original wheel should be called many times, not just once expect(wheelCalls.length).toBeGreaterThan(1); }); it("mouse.wheel({deltaX: 200}) smooths horizontal scroll too", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const wheelCalls: any[] = []; const page = buildMockPage(); const origWheel = vi.fn(async (opts: any) => { wheelCalls.push(opts); }); page.mouse.wheel = origWheel; const cfg = resolveConfig("default"); const cursor = { x: 100, y: 100, initialized: true }; patchPage(page as any, cfg, cursor as any); await page.mouse.wheel({ deltaX: 200 }); expect(wheelCalls.length).toBeGreaterThan(1); }); it("mouse.wheel with no args does nothing", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const wheelCalls: any[] = []; const page = buildMockPage(); const origWheel = vi.fn(async (opts: any) => { wheelCalls.push(opts); }); page.mouse.wheel = origWheel; const cfg = resolveConfig("default"); const cursor = { x: 100, y: 100, initialized: true }; patchPage(page as any, cfg, cursor as any); await page.mouse.wheel({}); expect(wheelCalls.length).toBe(0); }); }); // ========================================================================= // mouse.dragAndDrop() — Bézier drag between coordinates // ========================================================================= describe("Puppeteer: mouse.dragAndDrop() humanization", () => { it("mouse.dragAndDrop is patched after patchPage", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const page = buildMockPage(); const originalDnD = page.mouse.dragAndDrop; const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); expect(page.mouse.dragAndDrop).not.toBe(originalDnD); }); it("mouse.dragAndDrop calls mouseMove multiple times (Bézier), then mouseDown/mouseUp", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const moveCalls: Array<[number, number]> = []; const downCalls: any[] = []; const upCalls: any[] = []; const page = buildMockPage(); const origMove = vi.fn(async (x: number, y: number) => { moveCalls.push([x, y]); }); const origDown = vi.fn(async () => { downCalls.push(true); }); const origUp = vi.fn(async () => { upCalls.push(true); }); page.mouse.move = origMove; page.mouse.down = origDown; page.mouse.up = origUp; const cfg = resolveConfig("default"); const cursor = { x: 50, y: 50, initialized: true }; patchPage(page as any, cfg, cursor as any); await page.mouse.dragAndDrop( { x: 100, y: 100 }, { x: 400, y: 400 }, ); // Bézier movement generates many intermediate points expect(moveCalls.length).toBeGreaterThan(10); // mouseDown and mouseUp should each be called once expect(downCalls.length).toBe(1); expect(upCalls.length).toBe(1); }); }); // ========================================================================= // Keyboard patches — type, press, down, up // ========================================================================= describe("Puppeteer: keyboard patches", () => { it("keyboard.type is patched to use humanType", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const page = buildMockPage(); const originalType = page.keyboard.type; const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); expect(page.keyboard.type).not.toBe(originalType); }); it("keyboard.press is patched with delay", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const page = buildMockPage(); const originalPress = page.keyboard.press; const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); expect(page.keyboard.press).not.toBe(originalPress); }); it("keyboard.down is patched with small delay", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const downCalls: string[] = []; const page = buildMockPage(); const origDown = page.keyboard.down; page.keyboard.down = vi.fn(async (k: string) => { downCalls.push(k); }); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); expect(page.keyboard.down).not.toBe(origDown); await page.keyboard.down("a"); // Original should have been called via the patch // (the patched version calls originals.keyboardDown which is the stored original) }); it("keyboard.up is patched with small delay", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const page = buildMockPage(); const origUp = page.keyboard.up; const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); expect(page.keyboard.up).not.toBe(origUp); }); }); // ========================================================================= // Mouse patches — move, click with clickCount // ========================================================================= describe("Puppeteer: mouse patches", () => { it("mouse.move is patched with Bézier movement", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const moveCalls: Array<[number, number]> = []; const page = buildMockPage(); page.mouse.move = vi.fn(async (x: number, y: number) => { moveCalls.push([x, y]); }); const cfg = resolveConfig("default"); const cursor = { x: 50, y: 50, initialized: true }; patchPage(page as any, cfg, cursor as any); await page.mouse.move(500, 500); // Bézier generates many intermediate points expect(moveCalls.length).toBeGreaterThan(10); }); it("mouse.click is patched with Bézier + humanClick", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const moveCalls: Array<[number, number]> = []; const downCalls: any[] = []; const upCalls: any[] = []; const page = buildMockPage(); page.mouse.move = vi.fn(async (x: number, y: number) => { moveCalls.push([x, y]); }); page.mouse.down = vi.fn(async (opts?: any) => { downCalls.push(opts); }); page.mouse.up = vi.fn(async (opts?: any) => { upCalls.push(opts); }); const cfg = resolveConfig("default"); const cursor = { x: 50, y: 50, initialized: true }; patchPage(page as any, cfg, cursor as any); await page.mouse.click(300, 300); expect(moveCalls.length).toBeGreaterThan(5); expect(downCalls.length).toBe(1); expect(upCalls.length).toBe(1); }); it("mouse.click with clickCount:2 triggers double-click sequence", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const downCalls: any[] = []; const upCalls: any[] = []; const page = buildMockPage(); page.mouse.move = vi.fn(async () => {}); page.mouse.down = vi.fn(async (opts?: any) => { downCalls.push(opts ?? {}); }); page.mouse.up = vi.fn(async (opts?: any) => { upCalls.push(opts ?? {}); }); const cfg = resolveConfig("default"); const cursor = { x: 50, y: 50, initialized: true }; patchPage(page as any, cfg, cursor as any); await page.mouse.click(300, 300, { clickCount: 2 }); // First click: down() + up() (no clickCount), Second click: down({clickCount:2}) + up({clickCount:2}) expect(downCalls.length).toBe(2); expect(upCalls.length).toBe(2); // Second down/up should have clickCount:2 const secondDown = downCalls[1]; const secondUp = upCalls[1]; expect(secondDown.clickCount).toBe(2); expect(secondUp.clickCount).toBe(2); }); }); // ========================================================================= // select-all: Puppeteer uses down(modifier) → press('a') → up(modifier) // ========================================================================= describe("Puppeteer: select-all via modifier keys (not combo string)", () => { it("fill() calls keyboardDown(Control/Meta) → keyboardPress('a') → keyboardUp(Control/Meta)", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const keyDowns: string[] = []; const keyUps: string[] = []; const keyPresses: string[] = []; const page = buildMockPage(); page.keyboard.down = vi.fn(async (k: string) => { keyDowns.push(k); }); page.keyboard.up = vi.fn(async (k: string) => { keyUps.push(k); }); page.keyboard.press = vi.fn(async (k: string) => { keyPresses.push(k); }); const cfg = resolveConfig("default", { field_switch_delay: [0, 0], idle_between_actions: false, }); const cursor = { x: 100, y: 100, initialized: true }; patchPage(page as any, cfg, cursor as any); // fill → click → selectAll → Backspace → type // We can't easily call fill without scrollToElement working, // but we can test pressSelectAll indirectly by checking originals are stored const originals = (page as any)._original; expect(typeof originals.keyboardDown).toBe("function"); expect(typeof originals.keyboardPress).toBe("function"); expect(typeof originals.keyboardUp).toBe("function"); // Verify the modifier is correct for the platform const expectedModifier = process.platform === 'darwin' ? 'Meta' : 'Control'; // Test by importing and calling pressSelectAll-equivalent logic await originals.keyboardDown(expectedModifier); await originals.keyboardPress('a'); await originals.keyboardUp(expectedModifier); expect(keyDowns).toContain(expectedModifier); expect(keyPresses).toContain('a'); expect(keyUps).toContain(expectedModifier); }); }); // ========================================================================= // ElementHandle patching (Puppeteer-specific) // ========================================================================= describe("Puppeteer: ElementHandle patching", () => { it("page.$() returns patched ElementHandle", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const mockEl = buildMockElementHandle(); const page = buildMockPage({ $: vi.fn(async () => mockEl), }); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); const el = await page.$('#test'); expect(el).toBeDefined(); expect((el as any)._humanPatched).toBe(true); }); it("page.$$() returns patched ElementHandles", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const mockEl1 = buildMockElementHandle(); const mockEl2 = buildMockElementHandle(); const page = buildMockPage({ $$: vi.fn(async () => [mockEl1, mockEl2]), }); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); const els = await page.$$('.items'); expect(els.length).toBe(2); expect((els[0] as any)._humanPatched).toBe(true); expect((els[1] as any)._humanPatched).toBe(true); }); it("page.waitForSelector() returns patched ElementHandle", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const mockEl = buildMockElementHandle(); const page = buildMockPage({ waitForSelector: vi.fn(async () => mockEl), }); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); const el = await page.waitForSelector('#loading'); expect(el).toBeDefined(); expect((el as any)._humanPatched).toBe(true); }); it("patched el.click() uses humanMove + humanClick", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const moveCalls: Array<[number, number]> = []; const downCalls: any[] = []; const upCalls: any[] = []; const mockEl = buildMockElementHandle({ boundingBox: vi.fn(async () => ({ x: 200, y: 300, width: 100, height: 30 })), evaluate: vi.fn(async () => false), // not an input }); const page = buildMockPage({ $: vi.fn(async () => mockEl), }); page.mouse.move = vi.fn(async (x: number, y: number) => { moveCalls.push([x, y]); }); page.mouse.down = vi.fn(async (opts?: any) => { downCalls.push(opts); }); page.mouse.up = vi.fn(async (opts?: any) => { upCalls.push(opts); }); const cfg = resolveConfig("default", { idle_between_actions: false }); const cursor = { x: 50, y: 50, initialized: true }; patchPage(page as any, cfg, cursor as any); const el = await page.$('#btn'); await el.click(); // Bézier movement → multiple move calls expect(moveCalls.length).toBeGreaterThan(5); // Click → down + up expect(downCalls.length).toBe(1); expect(upCalls.length).toBe(1); }); it("patched el.click({clickCount:2}) triggers double-click", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const downCalls: any[] = []; const upCalls: any[] = []; const mockEl = buildMockElementHandle({ boundingBox: vi.fn(async () => ({ x: 200, y: 300, width: 100, height: 30 })), evaluate: vi.fn(async () => false), }); const page = buildMockPage({ $: vi.fn(async () => mockEl), }); page.mouse.move = vi.fn(async () => {}); page.mouse.down = vi.fn(async (opts?: any) => { downCalls.push(opts ?? {}); }); page.mouse.up = vi.fn(async (opts?: any) => { upCalls.push(opts ?? {}); }); const cfg = resolveConfig("default", { idle_between_actions: false }); const cursor = { x: 50, y: 50, initialized: true }; patchPage(page as any, cfg, cursor as any); const el = await page.$('#text'); await el.click({ clickCount: 2 }); // First click + second click with clickCount:2 expect(downCalls.length).toBe(2); expect(upCalls.length).toBe(2); expect(downCalls[1].clickCount).toBe(2); expect(upCalls[1].clickCount).toBe(2); }); it("patched el.hover() uses humanMove without clicking", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const moveCalls: Array<[number, number]> = []; const downCalls: any[] = []; const mockEl = buildMockElementHandle({ boundingBox: vi.fn(async () => ({ x: 150, y: 250, width: 80, height: 25 })), evaluate: vi.fn(async () => false), }); const page = buildMockPage({ $: vi.fn(async () => mockEl), }); page.mouse.move = vi.fn(async (x: number, y: number) => { moveCalls.push([x, y]); }); page.mouse.down = vi.fn(async (opts?: any) => { downCalls.push(opts); }); const cfg = resolveConfig("default", { idle_between_actions: false }); const cursor = { x: 50, y: 50, initialized: true }; patchPage(page as any, cfg, cursor as any); const el = await page.$('#link'); await el.hover(); expect(moveCalls.length).toBeGreaterThan(5); // Hover should NOT click expect(downCalls.length).toBe(0); }); it("patched el.type() moves, clicks, then types with humanType", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const downCalls: any[] = []; const charCalls: string[] = []; const mockEl = buildMockElementHandle({ boundingBox: vi.fn(async () => ({ x: 100, y: 200, width: 200, height: 30 })), evaluate: vi.fn(async () => true), // is an input }); const page = buildMockPage({ $: vi.fn(async () => mockEl), }); page.mouse.move = vi.fn(async () => {}); page.mouse.down = vi.fn(async (opts?: any) => { downCalls.push(opts); }); page.mouse.up = vi.fn(async () => {}); page.keyboard.down = vi.fn(async (k: string) => { charCalls.push(`down:${k}`); }); page.keyboard.up = vi.fn(async (k: string) => { charCalls.push(`up:${k}`); }); page.keyboard.sendCharacter = vi.fn(async () => {}); const cfg = resolveConfig("default", { mistype_chance: 0, idle_between_actions: false, typing_delay: 0, typing_delay_spread: 0, key_hold: [0, 0], }); const cursor = { x: 50, y: 50, initialized: true }; patchPage(page as any, cfg, cursor as any); const el = await page.$('#email'); await el.type('ab'); // Should have clicked first (mouseDown) expect(downCalls.length).toBe(1); // Should have typed 'a' and 'b' via keyboard.down/up expect(charCalls).toContain('down:a'); expect(charCalls).toContain('up:a'); expect(charCalls).toContain('down:b'); expect(charCalls).toContain('up:b'); }); it("patched el.focus() clicks to focus instead of programmatic focus", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const downCalls: any[] = []; const mockEl = buildMockElementHandle({ boundingBox: vi.fn(async () => ({ x: 100, y: 200, width: 200, height: 30 })), evaluate: vi.fn(async () => true), }); const page = buildMockPage({ $: vi.fn(async () => mockEl), }); page.mouse.move = vi.fn(async () => {}); page.mouse.down = vi.fn(async (opts?: any) => { downCalls.push(opts); }); page.mouse.up = vi.fn(async () => {}); const cfg = resolveConfig("default", { idle_between_actions: false }); const cursor = { x: 50, y: 50, initialized: true }; patchPage(page as any, cfg, cursor as any); const el = await page.$('#input'); await el.focus(); // focus should trigger a click (mouseDown + mouseUp) expect(downCalls.length).toBe(1); }); it("el with null boundingBox falls back to original", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const originalClickCalled = { value: false }; const mockEl = buildMockElementHandle({ boundingBox: vi.fn(async () => null), // not visible }); // Override the original click to track it mockEl.click = vi.fn(async () => { originalClickCalled.value = true; }); const page = buildMockPage({ $: vi.fn(async () => mockEl), }); const cfg = resolveConfig("default"); const cursor = { x: 50, y: 50, initialized: true }; patchPage(page as any, cfg, cursor as any); const el = await page.$('#hidden'); await el.click(); // Should have fallen back to original click expect(originalClickCalled.value).toBe(true); }); it("nested el.$() returns patched child", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const childEl = buildMockElementHandle(); const parentEl = buildMockElementHandle(); parentEl.$ = vi.fn(async () => childEl); const page = buildMockPage({ $: vi.fn(async () => parentEl), }); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); const parent = await page.$('#parent'); const child = await parent.$('.child'); expect(child).toBeDefined(); expect((child as any)._humanPatched).toBe(true); }); it("double-patching is prevented (_humanPatched guard)", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const mockEl = buildMockElementHandle(); const page = buildMockPage({ $: vi.fn(async () => mockEl), }); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); // First call const el1 = await page.$('#btn'); const clickFn1 = el1.click; // Second call — same element, should not re-patch const el2 = await page.$('#btn'); const clickFn2 = el2.click; // Functions should be identical (not re-wrapped) expect(clickFn1).toBe(clickFn2); }); }); // ========================================================================= // Frame-level patching // ========================================================================= describe("Puppeteer: frame-level patching", () => { it("child frames are marked _humanPatched", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const childFrame: any = { click: vi.fn(async () => {}), hover: vi.fn(async () => {}), type: vi.fn(async () => {}), fill: vi.fn(async () => {}), check: vi.fn(async () => {}), uncheck: vi.fn(async () => {}), select: vi.fn(async () => []), press: vi.fn(async () => {}), clear: vi.fn(async () => {}), focus: vi.fn(async () => {}), tap: vi.fn(async () => {}), pressSequentially: vi.fn(async () => {}), dragAndDrop: vi.fn(async () => {}), $: vi.fn(async () => null), $$: vi.fn(async () => []), waitForSelector: vi.fn(async () => null), childFrames: vi.fn(() => []), }; const mainFrame = { ...childFrame, childFrames: vi.fn(() => [childFrame]), }; const page = buildMockPage({ mainFrameReturn: mainFrame }); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); expect((childFrame as any)._humanPatched).toBe(true); }); it("frame.focus is patched to delegate to page.focus", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const childFrame: any = { click: vi.fn(async () => {}), hover: vi.fn(async () => {}), type: vi.fn(async () => {}), fill: vi.fn(async () => {}), check: vi.fn(async () => {}), uncheck: vi.fn(async () => {}), select: vi.fn(async () => []), press: vi.fn(async () => {}), clear: vi.fn(async () => {}), focus: vi.fn(async () => {}), tap: vi.fn(async () => {}), pressSequentially: vi.fn(async () => {}), dragAndDrop: vi.fn(async () => {}), $: vi.fn(async () => null), $$: vi.fn(async () => []), waitForSelector: vi.fn(async () => null), childFrames: vi.fn(() => []), }; const mainFrame = { ...childFrame, childFrames: vi.fn(() => [childFrame]), }; const page = buildMockPage({ mainFrameReturn: mainFrame }); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); // frame.focus should now be patched (not the original) const originalFrameFocus = vi.fn(async () => {}); expect(childFrame.focus).not.toBe(originalFrameFocus); expect(typeof childFrame.focus).toBe("function"); }); it("frame.$() returns patched ElementHandles", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const mockEl = buildMockElementHandle(); const childFrame: any = { click: vi.fn(async () => {}), hover: vi.fn(async () => {}), type: vi.fn(async () => {}), fill: vi.fn(async () => {}), check: vi.fn(async () => {}), uncheck: vi.fn(async () => {}), select: vi.fn(async () => []), press: vi.fn(async () => {}), clear: vi.fn(async () => {}), focus: vi.fn(async () => {}), tap: vi.fn(async () => {}), pressSequentially: vi.fn(async () => {}), dragAndDrop: vi.fn(async () => {}), $: vi.fn(async () => mockEl), $$: vi.fn(async () => []), waitForSelector: vi.fn(async () => null), childFrames: vi.fn(() => []), }; const mainFrame = { ...childFrame, childFrames: vi.fn(() => [childFrame]), }; const page = buildMockPage({ mainFrameReturn: mainFrame }); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); const el = await childFrame.$('#in-frame'); expect(el).toBeDefined(); expect((el as any)._humanPatched).toBe(true); }); }); // ========================================================================= // Browser-level patching // ========================================================================= describe("Puppeteer: browser-level patching", () => { it("patchBrowser patches newPage()", async () => { const { patchBrowser } = await import("../src/human-puppeteer/index.js"); const browser = buildMockBrowser(); const origNewPage = browser.newPage; const cfg = resolveConfig("default"); patchBrowser(browser as any, cfg); expect(browser.newPage).not.toBe(origNewPage); expect(typeof browser.newPage).toBe("function"); }); it("patchBrowser patches createIncognitoBrowserContext()", async () => { const { patchBrowser } = await import("../src/human-puppeteer/index.js"); const browser: any = { pages: vi.fn(async () => []), newPage: vi.fn(async () => buildMockPage()), createIncognitoBrowserContext: vi.fn(async () => ({ newPage: vi.fn(async () => buildMockPage()), })), on: vi.fn(), close: vi.fn(async () => {}), }; const origCreateCtx = browser.createIncognitoBrowserContext; const cfg = resolveConfig("default"); patchBrowser(browser as any, cfg); expect(browser.createIncognitoBrowserContext).not.toBe(origCreateCtx); }); it("patchBrowser listens for targetcreated event", async () => { const { patchBrowser } = await import("../src/human-puppeteer/index.js"); const browser = buildMockBrowser(); const cfg = resolveConfig("default"); patchBrowser(browser as any, cfg); expect(browser.on).toHaveBeenCalledWith("targetcreated", expect.any(Function)); }); it("newPage from patched browser returns page with _original", async () => { const { patchBrowser } = await import("../src/human-puppeteer/index.js"); const mockPage = buildMockPage(); const browser = buildMockBrowser(); const origNewPage = vi.fn(async () => mockPage); browser.newPage = origNewPage; const cfg = resolveConfig("default"); patchBrowser(browser as any, cfg); const page = await browser.newPage(); expect((page as any)._original).toBeDefined(); expect((page as any)._humanCfg).toBe(cfg); expect((page as any)._stealth).toBeDefined(); }); it("pages from patched createIncognitoBrowserContext also get humanized", async () => { const { patchBrowser } = await import("../src/human-puppeteer/index.js"); const mockPage = buildMockPage(); const mockCtx = { newPage: vi.fn(async () => mockPage), }; const browser: any = { pages: vi.fn(async () => []), newPage: vi.fn(async () => buildMockPage()), createIncognitoBrowserContext: vi.fn(async () => mockCtx), on: vi.fn(), close: vi.fn(async () => {}), }; const cfg = resolveConfig("default"); patchBrowser(browser as any, cfg); const ctx = await browser.createIncognitoBrowserContext(); const page = await ctx.newPage(); expect((page as any)._original).toBeDefined(); expect((page as any)._humanCfg).toBe(cfg); }); }); // ========================================================================= // isInputElement / isSelectorFocused — through patchPage click flow // ========================================================================= describe("Puppeteer: isInputElement stealth integration via patchPage", () => { it("click() uses stealth.evaluate for isInputElement (no page.evaluate)", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const evaluateCalls: any[] = []; const stealthEvaluateCalls: string[] = []; const mockCdp = buildMockCDP({ send: vi.fn(async (method: string, params?: any) => { if (method === "Page.getFrameTree") { return { frameTree: { frame: { id: "F1" } } }; } if (method === "Page.createIsolatedWorld") { return { executionContextId: 100 }; } if (method === "Runtime.evaluate") { stealthEvaluateCalls.push(params.expression); return { result: { value: false } }; } return {}; }), }); const mockEl = buildMockElementHandle(); const page = buildMockPage({ evaluate: vi.fn(async (...args: any[]) => { evaluateCalls.push(args); return false; }), $: vi.fn(async () => mockEl), }); page.createCDPSession = vi.fn(async () => mockCdp); const cfg = resolveConfig("default", { idle_between_actions: false }); const cursor = { x: 100, y: 100, initialized: true }; patchPage(page as any, cfg, cursor as any); try { await page.click("#btn"); } catch (e) { // scrollToElement might throw with mocks } const isInputCalls = stealthEvaluateCalls.filter( expr => expr.includes("tagName") || expr.includes("querySelector") ); const qsCalls = evaluateCalls.filter( args => typeof args[0] === "string" && args[0].includes("querySelector") ); if (isInputCalls.length > 0) { expect(qsCalls.length).toBe(0); } }); }); // ========================================================================= // isSelectorFocused stealth integration via patchPage press flow // ========================================================================= describe("Puppeteer: isSelectorFocused stealth integration via patchPage", () => { it("focus() uses stealth.evaluate for focus check", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const stealthEvaluateCalls: string[] = []; const mockCdp = buildMockCDP({ send: vi.fn(async (method: string, params?: any) => { if (method === "Page.getFrameTree") { return { frameTree: { frame: { id: "F1" } } }; } if (method === "Page.createIsolatedWorld") { return { executionContextId: 200 }; } if (method === "Runtime.evaluate") { stealthEvaluateCalls.push(params.expression); // Return true = element IS focused → focus() should skip clicking return { result: { value: true } }; } return {}; }), }); const page = buildMockPage({ evaluate: vi.fn(async () => true), }); page.createCDPSession = vi.fn(async () => mockCdp); const cfg = resolveConfig("default"); const cursor = { x: 50, y: 50, initialized: true }; patchPage(page as any, cfg, cursor as any); try { await page.focus("input#field"); } catch (e) { // May throw with mocks — that's fine, we just need the stealth call } const focusCalls = stealthEvaluateCalls.filter( expr => expr.includes("activeElement") ); expect(focusCalls.length).toBeGreaterThan(0); }); }); // ========================================================================= // Puppeteer-specific: sendCharacter mapped to insertText // ========================================================================= describe("Puppeteer: sendCharacter → insertText mapping", () => { it("rawKb.insertText is mapped from keyboard.sendCharacter", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const sentChars: string[] = []; const page = buildMockPage(); page.keyboard.sendCharacter = vi.fn(async (ch: string) => { sentChars.push(ch); }); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); const originals = (page as any)._original; expect(typeof originals.keyboardSendCharacter).toBe("function"); // rawKb.insertText should use the original sendCharacter await originals.keyboardSendCharacter("€"); expect(sentChars).toContain("€"); }); }); // ========================================================================= // Puppeteer-specific: viewport() vs viewportSize() // ========================================================================= describe("Puppeteer: viewport() API (not viewportSize)", () => { it("patchPage works with page.viewport() (Puppeteer API)", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const page = buildMockPage(); // Puppeteer uses viewport(), not viewportSize() page.viewport = vi.fn(() => ({ width: 1920, height: 1080 })); expect(page.viewportSize).toBeUndefined; // should NOT exist const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; // Should not throw expect(() => patchPage(page as any, cfg, cursor as any)).not.toThrow(); }); }); // ========================================================================= // Cursor initialization // ========================================================================= describe("Puppeteer: cursor initialization", () => { it("cursor is initialized on patchPage with random position", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const moveCalls: Array<[number, number]> = []; const page = buildMockPage(); page.mouse.move = vi.fn(async (x: number, y: number) => { moveCalls.push([x, y]); }); const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); // Wait for the async init await sleep(50); // Cursor should be initialized within the config range expect(cursor.x).toBeGreaterThanOrEqual(cfg.initial_cursor_x[0]); expect(cursor.x).toBeLessThanOrEqual(cfg.initial_cursor_x[1]); expect(cursor.y).toBeGreaterThanOrEqual(cfg.initial_cursor_y[0]); expect(cursor.y).toBeLessThanOrEqual(cfg.initial_cursor_y[1]); expect(cursor.initialized).toBe(true); }); }); // ========================================================================= // Page-level method replacement verification // ========================================================================= describe("Puppeteer: all page methods are replaced", () => { it("all interaction methods are replaced after patchPage", async () => { const { patchPage } = await import("../src/human-puppeteer/index.js"); const page = buildMockPage(); const originals = { goto: page.goto, click: page.click, hover: page.hover, type: page.type, select: page.select, focus: page.focus, tap: page.tap, mouseMove: page.mouse.move, mouseClick: page.mouse.click, mouseWheel: page.mouse.wheel, keyboardType: page.keyboard.type, keyboardPress: page.keyboard.press, keyboardDown: page.keyboard.down, keyboardUp: page.keyboard.up, }; const cfg = resolveConfig("default"); const cursor = { x: 0, y: 0, initialized: false }; patchPage(page as any, cfg, cursor as any); expect(page.goto).not.toBe(originals.goto); expect(page.click).not.toBe(originals.click); expect(page.hover).not.toBe(originals.hover); expect(page.type).not.toBe(originals.type); expect(page.select).not.toBe(originals.select); expect(page.focus).not.toBe(originals.focus); expect(page.mouse.move).not.toBe(originals.mouseMove); expect(page.mouse.click).not.toBe(originals.mouseClick); expect(page.mouse.wheel).not.toBe(originals.mouseWheel); expect(page.keyboard.type).not.toBe(originals.keyboardType); expect(page.keyboard.press).not.toBe(originals.keyboardPress); expect(page.keyboard.down).not.toBe(originals.keyboardDown); expect(page.keyboard.up).not.toBe(originals.keyboardUp); }); }); // ========================================================================= // SLOW TESTS — require real browser (run with: vitest run --testTimeout=60000) // Only run when SLOW=1 env var is set // ========================================================================= const SLOW = process.env.SLOW === '1'; const describeIfSlow = SLOW ? describe : describe.skip; describeIfSlow("Puppeteer stealth browser: no evaluate leak on click", () => { it("click() does not trigger querySelector from evaluate context", async () => { const { launch } = await import("../src/puppeteer.js"); const browser = await launch({ humanize: true, headless: true }); const page = await browser.newPage(); await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' }); await sleep(1000); // Inject detection script await page.evaluate(() => { (window as any).__evalLeaks = []; const origQS = document.querySelector.bind(document); document.querySelector = function (sel: string) { try { throw new Error(); } catch (e: any) { if (e.stack && e.stack.includes(':302:')) { (window as any).__evalLeaks.push(sel); } } return origQS(sel); } as any; }); await page.click('#searchInput'); await sleep(500); const leaks = await page.evaluate(() => (window as any).__evalLeaks || []); expect(leaks.length).toBe(0); await browser.close(); }, 30000); }); describeIfSlow("Puppeteer stealth browser: shift symbols isTrusted=true", () => { it("'!' produces isTrusted=true keydown, not isTrusted=false", async () => { const { launch } = await import("../src/puppeteer.js"); const browser = await launch({ humanize: true, headless: true }); const page = await browser.newPage(); await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' }); await sleep(1000); await page.evaluate(() => { (window as any).__untrustedKeys = []; (window as any).__trustedKeys = []; const input = document.querySelector('#searchInput'); if (input) { input.addEventListener('keydown', (e) => { if (!e.isTrusted) { (window as any).__untrustedKeys.push((e as KeyboardEvent).key); } else { (window as any).__trustedKeys.push((e as KeyboardEvent).key); } }, true); } }); await page.click('#searchInput'); await sleep(300); await page.keyboard.type('test!'); await sleep(500); const untrusted = await page.evaluate(() => (window as any).__untrustedKeys || []); const trusted = await page.evaluate(() => (window as any).__trustedKeys || []); expect(untrusted).not.toContain('!'); expect(trusted).toContain('!'); await browser.close(); }, 30000); }); describeIfSlow("Puppeteer stealth browser: navigation invalidation", () => { it("click works after navigation (isolated world re-created)", async () => { const { launch } = await import("../src/puppeteer.js"); const browser = await launch({ humanize: true, headless: true }); const page = await browser.newPage(); expect((page as any)._stealth).toBeDefined(); await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' }); await sleep(1000); await page.click('#searchInput'); await sleep(300); // Second navigation await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' }); await sleep(1000); // Should still work await page.click('#searchInput'); await sleep(300); await page.keyboard.type('after navigation'); await sleep(500); // Puppeteer doesn't have locator().inputValue(), use evaluate instead const val = await page.evaluate(() => { const el = document.querySelector('#searchInput') as HTMLInputElement; return el ? el.value : ''; }); expect(val).toContain('after navigation'); await browser.close(); }, 60000); }); describeIfSlow("Puppeteer stealth browser: ElementHandle humanization", () => { it("el.click() uses Bézier movement, not instant CDP click", async () => { const { launch } = await import("../src/puppeteer.js"); const browser = await launch({ humanize: true, headless: true }); const page = await browser.newPage(); await page.goto('https://example.com', { waitUntil: 'domcontentloaded' }); await sleep(1000); // Track mousemove events at document level await page.evaluate(() => { (window as any).__moveCounts = 0; document.addEventListener('mousemove', () => { (window as any).__moveCounts++; }, true); }); //

on example.com — always present, clickable, no navigation const el = await page.$('h1'); expect(el).not.toBeNull(); expect((el as any)._humanPatched).toBe(true); // Reset counter right before the click await page.evaluate(() => { (window as any).__moveCounts = 0; }); // Click via ElementHandle — should use Bézier curve await el!.click(); await sleep(500); const moveCount = await page.evaluate(() => (window as any).__moveCounts || 0); // Bézier movement generates many intermediate mousemove events (>10) // An instant CDP dispatchMouseEvent would generate 0 or 1 expect(moveCount).toBeGreaterThan(5); await browser.close(); }, 30000); }); describeIfSlow("Puppeteer stealth browser: focus() uses click", () => { it("page.focus() triggers mouse events (not programmatic focus)", async () => { const { launch } = await import("../src/puppeteer.js"); const browser = await launch({ humanize: true, headless: true }); const page = await browser.newPage(); await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' }); await sleep(2000); // Click somewhere else first to ensure #searchInput is NOT focused await page.mouse.click(50, 50); await sleep(500); // Inject event tracking on #searchInput AFTER ensuring it's not focused await page.evaluate(() => { (window as any).__mouseEvents = []; const input = document.querySelector('#searchInput'); if (input) { for (const evt of ['mousedown', 'mouseup', 'click', 'mousemove']) { input.addEventListener(evt, (e) => { (window as any).__mouseEvents.push(evt); }, true); } } }); // Now focus — should trigger humanized click with mouse events await page.focus('#searchInput'); await sleep(1000); const events = await page.evaluate(() => (window as any).__mouseEvents || []); expect(events.length).toBeGreaterThan(0); expect(events).toContain('mousedown'); await browser.close(); }, 30000); }); describeIfSlow("Puppeteer stealth browser: mouse.wheel smooth scroll", () => { it("mouse.wheel generates multiple small scroll events", async () => { const { launch } = await import("../src/puppeteer.js"); const browser = await launch({ humanize: true, headless: true }); const page = await browser.newPage(); await page.goto('https://en.wikipedia.org/wiki/Main_Page', { waitUntil: 'domcontentloaded' }); await sleep(1000); // Track scroll events await page.evaluate(() => { (window as any).__wheelEvents = 0; window.addEventListener('wheel', () => { (window as any).__wheelEvents++; }, { passive: true }); }); await page.mouse.wheel({ deltaY: 500 }); await sleep(1000); const wheelCount = await page.evaluate(() => (window as any).__wheelEvents || 0); // Smooth scroll should generate multiple wheel events, not just 1 expect(wheelCount).toBeGreaterThan(3); await browser.close(); }, 30000); });