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

- Add full Puppeteer humanize implementation (page, frame, element handle patching)
- Fix critical Playwright gaps: page.pressSequentially, page.tap, page.clear
- Fix frame-level patching: frame.pressSequentially, frame.tap
- Add comprehensive stealth tests for Puppeteer
- Update SLOW test suite to use correct humanize: true API
- Add 4 new tests validating fixed Playwright methods
This commit is contained in:
lilos
2026-04-09 20:49:20 +02:00
committed by GitHub
parent 1cef71133d
commit 7afe59435e
7 changed files with 3502 additions and 16 deletions
+103 -4
View File
@@ -831,6 +831,101 @@ describe("frame patching with stealth", () => {
});
// =========================================================================
// Page-level: pressSequentially, tap, clear are patched
// =========================================================================
describe("page-level pressSequentially, tap, clear patches", () => {
it("page.pressSequentially is replaced after patchPage", async () => {
const { patchPage } = await import("../src/human/index.js");
const page = buildMockPage();
const originalPressSeq = page.pressSequentially ?? (() => {});
const cfg = resolveConfig("default");
const cursor = { x: 0, y: 0, initialized: false };
patchPage(page as any, cfg, cursor as any);
expect(typeof (page as any).pressSequentially).toBe("function");
expect((page as any).pressSequentially).not.toBe(originalPressSeq);
});
it("page.tap is replaced after patchPage", async () => {
const { patchPage } = await import("../src/human/index.js");
const page = buildMockPage();
const originalTap = page.tap ?? (() => {});
const cfg = resolveConfig("default");
const cursor = { x: 0, y: 0, initialized: false };
patchPage(page as any, cfg, cursor as any);
expect(typeof (page as any).tap).toBe("function");
expect((page as any).tap).not.toBe(originalTap);
});
it("page.clear is replaced after patchPage", async () => {
const { patchPage } = await import("../src/human/index.js");
const page = buildMockPage();
const originalClear = page.clear ?? (() => {});
const cfg = resolveConfig("default");
const cursor = { x: 0, y: 0, initialized: false };
patchPage(page as any, cfg, cursor as any);
expect(typeof (page as any).clear).toBe("function");
expect((page as any).clear).not.toBe(originalClear);
});
});
// =========================================================================
// Frame-level: pressSequentially, tap are patched
// =========================================================================
describe("frame-level pressSequentially, tap patches", () => {
it("child frame has pressSequentially patched", async () => {
const { patchPage } = await import("../src/human/index.js");
const childFrame: any = {
click: vi.fn(async () => {}),
dblclick: vi.fn(async () => {}),
hover: vi.fn(async () => {}),
type: vi.fn(async () => {}),
fill: vi.fn(async () => {}),
check: vi.fn(async () => {}),
uncheck: vi.fn(async () => {}),
selectOption: vi.fn(async () => {}),
press: vi.fn(async () => {}),
pressSequentially: vi.fn(async () => {}),
tap: vi.fn(async () => {}),
clear: vi.fn(async () => {}),
dragAndDrop: vi.fn(async () => {}),
locator: vi.fn(() => ({
boundingBox: vi.fn(async () => ({ x: 0, y: 0, width: 100, height: 30 })),
})),
childFrames: vi.fn(() => []),
};
const origPressSeq = childFrame.pressSequentially;
const origTap = childFrame.tap;
const mainFrame = {
...childFrame,
childFrames: vi.fn(() => [childFrame]),
};
const page = buildMockPage({ mainFrameReturn: mainFrame });
const cfg = resolveConfig("default");
const cursor = { x: 0, y: 0, initialized: false };
patchPage(page as any, cfg, cursor as any);
expect((childFrame as any)._humanPatched).toBe(true);
// pressSequentially and tap should be replaced with humanized versions
expect(childFrame.pressSequentially).not.toBe(origPressSeq);
expect(childFrame.tap).not.toBe(origTap);
expect(typeof childFrame.pressSequentially).toBe("function");
expect(typeof childFrame.tap).toBe("function");
});
});
// =========================================================================
// Non-ASCII text does NOT go through CDP shift symbol path
// =========================================================================
@@ -898,7 +993,8 @@ describeIfSlow("stealth browser: no evaluate leak on click", () => {
it("click() does not trigger querySelector from evaluate context", async () => {
const { launch } = await import("../src/index.js");
const browser = await launch({ headless: true, args: ['--humanize'] });
const browser = await launch({ headless: true, humanize: true });
const page = await browser.newPage();
await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' });
@@ -932,7 +1028,8 @@ describeIfSlow("stealth browser: shift symbols isTrusted=true", () => {
it("'!' produces isTrusted=true keydown, not isTrusted=false", async () => {
const { launch } = await import("../src/index.js");
const browser = await launch({ headless: true, args: ['--humanize'] });
const browser = await launch({ headless: true, humanize: true });
const page = await browser.newPage();
await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' });
@@ -972,7 +1069,8 @@ describeIfSlow("stealth browser: navigation invalidation", () => {
it("click works after navigation (isolated world re-created)", async () => {
const { launch } = await import("../src/index.js");
const browser = await launch({ headless: true, args: ['--humanize'] });
const browser = await launch({ headless: true, humanize: true });
const page = await browser.newPage();
expect((page as any)._stealth).toBeDefined();
@@ -1003,7 +1101,8 @@ describeIfSlow("stealth browser: full form no evaluate leak", () => {
it("form with shift symbols has zero evaluate leaks and zero untrusted events", async () => {
const { launch } = await import("../src/index.js");
const browser = await launch({ headless: true, args: ['--humanize'] });
const browser = await launch({ headless: true, humanize: true });
const page = await browser.newPage();
await page.goto(