Skip to content

feat: Add Puppeteer humanize support and fix Playwright humanize gaps#129

Merged
Cloak-HQ merged 1 commit intoCloakHQ:mainfrom
evelaa123:feature/humanize-puppeteer
Apr 9, 2026
Merged

feat: Add Puppeteer humanize support and fix Playwright humanize gaps#129
Cloak-HQ merged 1 commit intoCloakHQ:mainfrom
evelaa123:feature/humanize-puppeteer

Conversation

@evelaa123
Copy link
Copy Markdown
Contributor

@evelaa123 evelaa123 commented Apr 9, 2026

Fix critical Playwright humanize gaps + Add full Puppeteer support

Overview

Found and fixed 3 critical stealth bypasses in the existing Playwright humanize implementation, then added complete Puppeteer humanize support mirroring the same architecture.


Issues Found & Fixed

Issue #1 (CRITICAL): 3 page-level methods not patched in Playwright

File: js/src/human/index.ts

Problem: Functions humanPressSequentiallyFn, humanTapFn, humanClearFn were implemented but never assigned to page.pressSequentially, page.tap, page.clear. They only existed as internal properties for frame patches.

Impact:

  • page.pressSequentially('input', 'text') bypassed humanize completely, typing instantly with isTrusted=false for shift characters
  • page.tap('button') bypassed Bézier mouse movement, performing instant clicks
  • page.clear('input') bypassed CDP Isolated World focus checks

This directly compromised stealth. Anti-bot systems could detect superhuman typing speed and instant mouse teleportation.

Fix: Added 3 missing page-level patches


Issue #2 (CRITICAL): 2 frame-level methods not patched in Playwright

File: js/src/human/index.ts, function patchSingleFrame()

Problem: Frame-level patching covered 11 methods but was missing pressSequentially and tap.

Why this matters: Users commonly write code like this:

await page.locator('#input').pressSequentially('Hello!');
await page.locator('#button').tap();

Locator calls go through Frame methods. Without frame-level patches, the entire Locator chain bypasses humanize even if page-level patches exist.

Fix: Added 2 missing frame-level patches


Issue #3 (MODERATE): SLOW tests used incorrect API

File: js/tests/stealth.test.ts (SLOW section)

Problem: Browser-level SLOW tests (run with SLOW=1) used args: ['--humanize'] instead of humanize: true. This meant SLOW tests were not testing humanize at all. They ran with native Playwright behavior.

Fix: Changed to correct API: humanize: true


Puppeteer Implementation

Added complete Puppeteer humanize support with full API coverage:

Coverage:

  • Page-level: goto, click, hover, type, fill, check, uncheck, select, clear, focus, press, pressSequentially, tap (13 methods)
  • Frame-level: All core methods + pressSequentially, tap, focus, dragAndDrop
  • ElementHandle-level: click, hover, type, press, tap, focus, select, drop, dragAndDrop + nested $, $$, waitForSelector
  • Browser-level: Auto-patching via newPage, createIncognitoBrowserContext, targetcreated

Files added:

  • js/src/human-puppeteer/index.ts (1,247 lines)
  • js/src/human-puppeteer/keyboard.ts (89 lines)
  • js/src/human-puppeteer/scroll.ts (56 lines)
  • js/tests/stealth.puppeteer.test.ts (2,174 lines, 77 tests)

Note: I have limited experience with Puppeteer's internal architecture. Would really appreciate a detailed review. Happy to fix any issues or improve the implementation based on feedback.


Test Results

All tests passing. Added 81 new tests total:

  • 4 new Playwright tests validating the fixes
  • 77 new Puppeteer tests covering full implementation

Files Changed

  • js/src/human/index.ts — Added 11 lines (3 page-level, 8 frame-level patches)
  • js/tests/stealth.test.ts — Added 95 lines (4 new tests + SLOW test fixes)
  • js/src/human-puppeteer/index.ts — Added 1,247 lines (new file)
  • js/src/human-puppeteer/keyboard.ts — Added 89 lines (new file)
  • js/src/human-puppeteer/scroll.ts — Added 56 lines (new file)
  • js/src/puppeteer.ts — Added 2 lines (humanize integration)
  • js/tests/stealth.puppeteer.test.ts — Added 2,174 lines (new file)

Note on Puppeteer v21 Compatibility

This implementation uses createIncognitoBrowserContext() instead of createBrowserContext() because CloakBrowser depends on Puppeteer v21.

In Puppeteer v23+ (2024), createIncognitoBrowserContext was renamed to createBrowserContext as part of a breaking change, but v21 doesn't have that method yet.


Review Request

I have limited experience with Puppeteer internals, so detailed review is very welcome. Happy to fix any issues or improve the implementation based on feedback.

@evelaa123 evelaa123 force-pushed the feature/humanize-puppeteer branch from 1703d45 to 1b63aeb Compare April 9, 2026 01:47
Copy link
Copy Markdown
Contributor

@Cloak-HQ Cloak-HQ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! Found a few things to address:

1. Horizontal scroll bug in smoothWheel

smoothWheel() hardcodes the Y axis — raw.wheel(0, chunk * sign). When mouse.wheel({deltaX: 200}) hits the patch, it passes dx to smoothWheel, which scrolls vertically instead of horizontally.

2. createIncognitoBrowserContext — forward compat

Works on puppeteer-core 21.x (our current dev dep), but current Puppeteer docs show createBrowserContext() as the API. Since our peer dep is >=21.0.0, worth checking for both names so it doesn't silently skip context patching on newer versions.

3. el.evaluate() in moveToElement bypasses stealth

Page-level methods correctly use StealthEval (CDP isolated world) for isInputElement. But the ElementHandle path in moveToElement() calls el.evaluate() directly — main world execution on every el.click() / el.hover() / el.type(). Should route through the existing StealthEval instance instead.

4. Dead patches (non-blocking)

Puppeteer Page doesn't have fill, check, uncheck, clear, press, or pressSequentially — those are Playwright-only. Same for several Frame methods. The (page as any) casts create new properties no standard Puppeteer caller would use. Not harmful, just dead code. Up to you whether to keep or trim.

5. Missing newline at EOF in puppeteer.ts


Playwright fixes (pressSequentially, tap, clear on page + frame) look good. Let us know if you have questions on any of these.

- Add full Puppeteer humanize implementation (page, frame, element handle patching)
- Fix critical Playwright gaps: page.pressSequentially, page.tap, page.clear
- Fix frame-level patching: frame.pressSequentially, frame.tap
- Add comprehensive stealth tests for Puppeteer
- Update SLOW test suite to use correct humanize: true API
- Add 4 new tests validating fixed Playwright methods
@evelaa123 evelaa123 force-pushed the feature/humanize-puppeteer branch from 1b63aeb to 1f5ed8a Compare April 9, 2026 17:57
@evelaa123
Copy link
Copy Markdown
Contributor Author

Addressed all review feedback

All 5 points from the review have been fixed:

1. Horizontal scroll bug in smoothWheel ✅

smoothWheel() now accepts an axis: 'x' | 'y' parameter (default 'y'). The mouse.wheel patch passes 'y' for deltaY and 'x' for deltaX.

  • scroll.tssmoothWheel routes to raw.wheel(d, 0) or raw.wheel(0, d) based on axis.
  • index.tsmouse.wheel patch passes correct axis to each smoothWheel call.

2. createIncognitoBrowserContext — forward compat ✅

patchBrowser now iterates over both API names:

for (const methodName of ['createBrowserContext', 'createIncognitoBrowserContext'] as const) {
  if (typeof (browser as any)[methodName] === 'function') { ... }
}

Works on v21 (createIncognitoBrowserContext) and v22+ (createBrowserContext).

3. el.evaluate() in moveToElement bypasses stealth ✅

New isInputElementHandle() function uses CDP DOM.describeNode to read nodeName and attributes directly — zero JS execution in any world.

Falls back to el.evaluate() only if CDP session is unavailable.

4. Dead patches removed ✅

Removed all Playwright-only methods that don't exist on Puppeteer's API:

  • Page: removed fill, check, uncheck, clear, press, pressSequentially
  • Frame: removed fill, check, uncheck, clear, press, pressSequentially, dragAndDrop
  • Helpers: removed isChecked, pressSelectAll, SELECT_ALL_MODIFIER
  • Stored refs: removed _humanClickFn, _humanHoverFn, _humanTypeFn, _humanFillFn, _humanClearFn, _humanFocusFn, _humanPressFn, _humanPressSequentiallyFn, _humanTapFn, _humanCheckFn, _humanUncheckFn, _humanSelectFn

Only _humanCursor, _humanRaw, _humanRawKb, _ensureCursorInit remain — these are actively used by patchSingleElementHandle.

Tests updated to match — removed tests for dead APIs, replaced with tests for actual Puppeteer APIs.

5. Missing newline at EOF ✅

Added.

Test results

77/77 passing (including 6 SLOW tests with real browser):

✓ tests/stealth.puppeteer.test.ts (77) 59503ms
Test Files  1 passed (1)
     Tests  77 passed (77)

@Cloak-HQ Cloak-HQ merged commit 7afe594 into CloakHQ:main Apr 9, 2026
2 checks passed
@Cloak-HQ
Copy link
Copy Markdown
Contributor

Cloak-HQ commented Apr 9, 2026

Merged, thanks! All tests pass including integration with a real browser on our test infra. Nice work on the CDP DOM.describeNode approach for ElementHandle — cleaner than routing through StealthEval.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants