1602 lines
61 KiB
TypeScript
1602 lines
61 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
||
import { resolveConfig, rand, randRange, sleep } from "../src/human/config.js";
|
||
import { humanMove, humanClick, clickTarget, humanIdle } from "../src/human/mouse.js";
|
||
import { patchPageElementHandles } from "../src/human/elementhandle.js";
|
||
|
||
// =========================================================================
|
||
// Config resolution
|
||
// =========================================================================
|
||
describe("resolveConfig", () => {
|
||
it("returns valid default config", () => {
|
||
const cfg = resolveConfig("default");
|
||
expect(cfg).toBeDefined();
|
||
expect(cfg.mouse_min_steps).toBeGreaterThan(0);
|
||
expect(cfg.mouse_max_steps).toBeGreaterThan(cfg.mouse_min_steps);
|
||
expect(cfg.typing_delay).toBeGreaterThan(0);
|
||
expect(cfg.initial_cursor_x).toHaveLength(2);
|
||
expect(cfg.initial_cursor_y).toHaveLength(2);
|
||
});
|
||
|
||
it("returns valid careful config with slower typing", () => {
|
||
const cfg = resolveConfig("careful");
|
||
const def = resolveConfig("default");
|
||
expect(cfg).toBeDefined();
|
||
expect(cfg.typing_delay).toBeGreaterThanOrEqual(def.typing_delay);
|
||
});
|
||
|
||
it("applies custom overrides", () => {
|
||
const cfg = resolveConfig("default", { mouse_min_steps: 100, mouse_max_steps: 200 });
|
||
expect(cfg.mouse_min_steps).toBe(100);
|
||
expect(cfg.mouse_max_steps).toBe(200);
|
||
});
|
||
|
||
it("preserves idle_between_actions override", () => {
|
||
const cfg = resolveConfig("default", {
|
||
idle_between_actions: true,
|
||
idle_between_duration: [50, 100],
|
||
});
|
||
expect(cfg.idle_between_actions).toBe(true);
|
||
expect(cfg.idle_between_duration[0]).toBe(50);
|
||
expect(cfg.idle_between_duration[1]).toBe(100);
|
||
});
|
||
|
||
it("throws on unknown preset name", () => {
|
||
expect(() => resolveConfig("nonexistent" as any)).toThrow(/Unknown humanize preset/);
|
||
});
|
||
|
||
it("returns all required fields including mistype", () => {
|
||
const cfg = resolveConfig("default");
|
||
const required = [
|
||
"mouse_min_steps", "mouse_max_steps", "typing_delay",
|
||
"initial_cursor_x", "initial_cursor_y", "idle_between_actions",
|
||
"idle_between_duration", "field_switch_delay",
|
||
"mistype_chance", "mistype_delay_notice", "mistype_delay_correct",
|
||
];
|
||
for (const f of required) {
|
||
expect(cfg).toHaveProperty(f);
|
||
}
|
||
});
|
||
|
||
it("mistype_delay fields are [min, max] tuples", () => {
|
||
const cfg = resolveConfig("default");
|
||
expect(Array.isArray(cfg.mistype_delay_notice)).toBe(true);
|
||
expect(cfg.mistype_delay_notice).toHaveLength(2);
|
||
expect(cfg.mistype_delay_notice[0]).toBeLessThanOrEqual(cfg.mistype_delay_notice[1]);
|
||
expect(Array.isArray(cfg.mistype_delay_correct)).toBe(true);
|
||
expect(cfg.mistype_delay_correct).toHaveLength(2);
|
||
expect(cfg.mistype_delay_correct[0]).toBeLessThanOrEqual(cfg.mistype_delay_correct[1]);
|
||
});
|
||
});
|
||
|
||
// =========================================================================
|
||
// rand / randRange / sleep
|
||
// =========================================================================
|
||
describe("rand helpers", () => {
|
||
it("rand stays within bounds over many iterations", () => {
|
||
for (let i = 0; i < 500; i++) {
|
||
const v = rand(10, 20);
|
||
expect(v).toBeGreaterThanOrEqual(10);
|
||
expect(v).toBeLessThanOrEqual(20);
|
||
}
|
||
});
|
||
|
||
it("randRange stays within bounds", () => {
|
||
for (let i = 0; i < 500; i++) {
|
||
const v = randRange([5, 15]);
|
||
expect(v).toBeGreaterThanOrEqual(5);
|
||
expect(v).toBeLessThanOrEqual(15);
|
||
}
|
||
});
|
||
|
||
it("sleep pauses for approximately correct duration", async () => {
|
||
const t0 = Date.now();
|
||
await sleep(50);
|
||
const elapsed = Date.now() - t0;
|
||
expect(elapsed).toBeGreaterThanOrEqual(40);
|
||
expect(elapsed).toBeLessThan(200);
|
||
});
|
||
});
|
||
|
||
// =========================================================================
|
||
// Bézier mouse movement (behavioral with vi.fn mocks)
|
||
// =========================================================================
|
||
describe("humanMove", () => {
|
||
function makeFakeRaw() {
|
||
const moves: Array<{ x: number; y: number }> = [];
|
||
return {
|
||
raw: {
|
||
move: vi.fn(async (x: number, y: number) => { moves.push({ x, y }); }),
|
||
down: vi.fn(async () => { }),
|
||
up: vi.fn(async () => { }),
|
||
wheel: vi.fn(async () => { }),
|
||
},
|
||
moves,
|
||
};
|
||
}
|
||
|
||
it("generates multiple intermediate points", async () => {
|
||
const cfg = resolveConfig("default");
|
||
const { raw, moves } = makeFakeRaw();
|
||
await humanMove(raw, 0, 0, 500, 300, cfg);
|
||
expect(moves.length).toBeGreaterThanOrEqual(10);
|
||
const last = moves[moves.length - 1];
|
||
expect(Math.abs(last.x - 500)).toBeLessThan(10);
|
||
expect(Math.abs(last.y - 300)).toBeLessThan(10);
|
||
});
|
||
|
||
it("raw.move called exactly once per step", async () => {
|
||
const cfg = resolveConfig("default");
|
||
const { raw, moves } = makeFakeRaw();
|
||
await humanMove(raw, 0, 0, 400, 400, cfg);
|
||
expect(raw.move).toHaveBeenCalledTimes(moves.length);
|
||
});
|
||
|
||
it("no single jump exceeds 50% of total distance", async () => {
|
||
const cfg = resolveConfig("default");
|
||
const { raw, moves } = makeFakeRaw();
|
||
await humanMove(raw, 0, 0, 400, 400, cfg);
|
||
const totalDist = Math.sqrt(400 ** 2 + 400 ** 2);
|
||
const maxJump = totalDist * 0.5;
|
||
for (let i = 1; i < moves.length; i++) {
|
||
const dx = moves[i].x - moves[i - 1].x;
|
||
const dy = moves[i].y - moves[i - 1].y;
|
||
expect(Math.sqrt(dx * dx + dy * dy)).toBeLessThan(maxJump);
|
||
}
|
||
});
|
||
|
||
it("produces curved path (not a straight line)", async () => {
|
||
const cfg = resolveConfig("default");
|
||
let maxDev = 0;
|
||
for (let trial = 0; trial < 10; trial++) {
|
||
const { raw, moves } = makeFakeRaw();
|
||
await humanMove(raw, 0, 0, 500, 0, cfg);
|
||
const dev = Math.max(...moves.map(m => Math.abs(m.y)));
|
||
if (dev > maxDev) maxDev = dev;
|
||
}
|
||
expect(maxDev).toBeGreaterThan(0.5);
|
||
});
|
||
|
||
it("handles very short distances", async () => {
|
||
const cfg = resolveConfig("default");
|
||
const { raw, moves } = makeFakeRaw();
|
||
await humanMove(raw, 100, 100, 103, 102, cfg);
|
||
expect(moves.length).toBeGreaterThanOrEqual(1);
|
||
});
|
||
|
||
it("handles zero distance without crashing", async () => {
|
||
const cfg = resolveConfig("default");
|
||
const { raw } = makeFakeRaw();
|
||
await humanMove(raw, 200, 200, 200, 200, cfg);
|
||
// Completes without error; may or may not call move (both valid)
|
||
expect(true).toBe(true);
|
||
});
|
||
});
|
||
|
||
// =========================================================================
|
||
// humanClick behavioral
|
||
// =========================================================================
|
||
describe("humanClick", () => {
|
||
it("calls down then up in correct order", async () => {
|
||
const cfg = resolveConfig("default");
|
||
const callOrder: string[] = [];
|
||
const raw = {
|
||
move: vi.fn(async () => { }),
|
||
down: vi.fn(async () => { callOrder.push("down"); }),
|
||
up: vi.fn(async () => { callOrder.push("up"); }),
|
||
wheel: vi.fn(async () => { }),
|
||
};
|
||
await humanClick(raw, false, cfg);
|
||
expect(raw.down).toHaveBeenCalledTimes(1);
|
||
expect(raw.up).toHaveBeenCalledTimes(1);
|
||
expect(callOrder).toEqual(["down", "up"]);
|
||
});
|
||
});
|
||
|
||
// =========================================================================
|
||
// humanIdle behavioral
|
||
// =========================================================================
|
||
describe("humanIdle", () => {
|
||
it("calls raw.move at least once during idle", async () => {
|
||
const cfg = resolveConfig("default");
|
||
const raw = {
|
||
move: vi.fn(async () => { }),
|
||
down: vi.fn(async () => { }),
|
||
up: vi.fn(async () => { }),
|
||
wheel: vi.fn(async () => { }),
|
||
};
|
||
await humanIdle(raw, 100, 100, cfg);
|
||
expect(raw.move).toHaveBeenCalled();
|
||
}, 15000);
|
||
});
|
||
|
||
// =========================================================================
|
||
// clickTarget
|
||
// =========================================================================
|
||
describe("clickTarget", () => {
|
||
it("returns point within bounding box", () => {
|
||
const cfg = resolveConfig("default");
|
||
const box = { x: 100, y: 200, width: 150, height: 40 };
|
||
for (let i = 0; i < 100; i++) {
|
||
const t = clickTarget(box, false, cfg);
|
||
expect(t.x).toBeGreaterThanOrEqual(100);
|
||
expect(t.x).toBeLessThanOrEqual(250);
|
||
expect(t.y).toBeGreaterThanOrEqual(200);
|
||
expect(t.y).toBeLessThanOrEqual(240);
|
||
}
|
||
});
|
||
|
||
it("isInput=true biases click toward left side of box", () => {
|
||
const cfg = resolveConfig("default");
|
||
const box = { x: 50, y: 50, width: 200, height: 30 };
|
||
let sumX = 0;
|
||
const N = 300;
|
||
for (let i = 0; i < N; i++) {
|
||
const t = clickTarget(box, true, cfg);
|
||
expect(t.x).toBeGreaterThanOrEqual(50);
|
||
expect(t.x).toBeLessThanOrEqual(250);
|
||
sumX += t.x;
|
||
}
|
||
const avgX = sumX / N;
|
||
expect(avgX).toBeLessThan(175);
|
||
});
|
||
|
||
it("does not crash with 1x1 box", () => {
|
||
const cfg = resolveConfig("default");
|
||
const t = clickTarget({ x: 0, y: 0, width: 1, height: 1 }, false, cfg);
|
||
expect(t.x).toBeGreaterThanOrEqual(0);
|
||
expect(t.x).toBeLessThanOrEqual(1);
|
||
});
|
||
});
|
||
|
||
// =========================================================================
|
||
// patchPage behavioral: fill uses platform SELECT_ALL
|
||
// =========================================================================
|
||
describe("patchPage fill", () => {
|
||
it("fill calls keyboard.press with platform-correct select-all", async () => {
|
||
const { patchPage } = await import("../src/human/index.js");
|
||
|
||
const pressedKeys: string[] = [];
|
||
const page = buildMockPage({
|
||
keyboardPress: async (key: string) => { pressedKeys.push(key); },
|
||
});
|
||
|
||
const cfg = resolveConfig("default");
|
||
const cursor = { x: 0, y: 0, initialized: false };
|
||
patchPage(page as any, cfg, cursor as any);
|
||
|
||
try { await (page as any).fill("input#name", "hello", { timeout: 2000 }); } catch (_) { }
|
||
|
||
const expected = process.platform === "darwin" ? "Meta+a" : "Control+a";
|
||
const wrong = process.platform === "darwin" ? "Control+a" : "Meta+a";
|
||
if (pressedKeys.length > 0) {
|
||
expect(pressedKeys).toContain(expected);
|
||
expect(pressedKeys).not.toContain(wrong);
|
||
}
|
||
}, 5000);
|
||
});
|
||
|
||
|
||
// =========================================================================
|
||
// patchPage behavioral: check/uncheck with idle_between_actions
|
||
// =========================================================================
|
||
describe("patchPage check/uncheck idle", () => {
|
||
it("check with idle=true calls humanClickFn and does not crash on idle", async () => {
|
||
const { patchPage } = await import("../src/human/index.js");
|
||
|
||
let downCalled = false;
|
||
const page = buildMockPage({
|
||
isChecked: async () => false,
|
||
evaluate: async () => ({ hit: true }),
|
||
});
|
||
page.mouse.down = vi.fn(async () => { downCalled = true; });
|
||
|
||
const cfg = resolveConfig("default", {
|
||
idle_between_actions: true,
|
||
idle_between_duration: [0.01, 0.02],
|
||
});
|
||
const cursor = { x: 100, y: 100, initialized: true };
|
||
patchPage(page as any, cfg, cursor as any);
|
||
|
||
try { await (page as any).check("input#cb", { timeout: 2000 }); } catch (_) { }
|
||
|
||
// humanCheckFn → humanIdle → humanClickFn → humanClick → raw.down
|
||
expect(downCalled).toBe(true);
|
||
}, 30000);
|
||
|
||
it("uncheck with idle=true calls humanClickFn and does not crash on idle", async () => {
|
||
const { patchPage } = await import("../src/human/index.js");
|
||
|
||
let downCalled = false;
|
||
const page = buildMockPage({
|
||
isChecked: async () => true,
|
||
evaluate: async () => ({ hit: true }),
|
||
});
|
||
page.mouse.down = vi.fn(async () => { downCalled = true; });
|
||
|
||
const cfg = resolveConfig("default", {
|
||
idle_between_actions: true,
|
||
idle_between_duration: [0.01, 0.02],
|
||
});
|
||
const cursor = { x: 100, y: 100, initialized: true };
|
||
patchPage(page as any, cfg, cursor as any);
|
||
|
||
try { await (page as any).uncheck("input#cb", { timeout: 2000 }); } catch (e: any) {
|
||
console.error("UNCHECK ERROR:", e?.message?.slice(0, 200));
|
||
}
|
||
|
||
expect(downCalled).toBe(true);
|
||
}, 30000);
|
||
|
||
it("config with idle=true is accepted by resolveConfig", () => {
|
||
const cfg = resolveConfig("default", {
|
||
idle_between_actions: true,
|
||
idle_between_duration: [5, 10],
|
||
});
|
||
expect(cfg.idle_between_actions).toBe(true);
|
||
expect(cfg.idle_between_duration).toEqual([5, 10]);
|
||
});
|
||
});
|
||
|
||
// =========================================================================
|
||
// patchPage behavioral: press focus check
|
||
// =========================================================================
|
||
describe("patchPage press focus", () => {
|
||
it("press clicks element when NOT focused (mouse.down called)", async () => {
|
||
const { patchPage } = await import("../src/human/index.js");
|
||
|
||
let downCount = 0;
|
||
const page = buildMockPage({
|
||
evaluate: async (expr: string) => {
|
||
if (typeof expr === 'string' && expr.includes('elementFromPoint')) return { hit: true };
|
||
return false;
|
||
},
|
||
});
|
||
// Intercept mouse.down before patching so raw captures it
|
||
page.mouse.down = vi.fn(async () => { downCount++; });
|
||
|
||
const cfg = resolveConfig("default");
|
||
const cursor = { x: 50, y: 50, initialized: true };
|
||
patchPage(page as any, cfg, cursor as any);
|
||
|
||
try { await (page as any).press("input#field", "Enter", { timeout: 2000 }); } catch (_) { }
|
||
|
||
expect(downCount).toBeGreaterThan(0);
|
||
});
|
||
|
||
it("press skips click when element IS focused (no mouse.down)", async () => {
|
||
const { patchPage } = await import("../src/human/index.js");
|
||
|
||
let downCount = 0;
|
||
const page = buildMockPage({
|
||
evaluate: async () => true,
|
||
});
|
||
page.mouse.down = vi.fn(async () => { downCount++; });
|
||
|
||
const cfg = resolveConfig("default");
|
||
const cursor = { x: 50, y: 50, initialized: true };
|
||
patchPage(page as any, cfg, cursor as any);
|
||
|
||
try { await (page as any).press("input#field", "Enter", { timeout: 2000 }); } catch (_) { }
|
||
|
||
expect(downCount).toBe(0);
|
||
});
|
||
});
|
||
|
||
// =========================================================================
|
||
// patchPage behavioral: frame patching
|
||
// =========================================================================
|
||
describe("patchPage frame patching", () => {
|
||
it("patches child frames with _humanPatched flag", async () => {
|
||
const { patchPage } = await import("../src/human/index.js");
|
||
|
||
const childFrame = buildMockFrame();
|
||
const mainFrame = {
|
||
...buildMockFrame(),
|
||
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("uses frame.locator for frame.click instead of page.click", async () => {
|
||
const { patchPage } = await import("../src/human/index.js");
|
||
|
||
const childFrame = buildMockFrame();
|
||
const mainFrame = {
|
||
...buildMockFrame(),
|
||
childFrames: vi.fn(() => [childFrame]),
|
||
};
|
||
const page = buildMockPage({ mainFrameReturn: mainFrame });
|
||
const originalPageClick = page.click;
|
||
const cfg = resolveConfig("default", { mouse_min_steps: 1, mouse_max_steps: 1 });
|
||
const cursor = { x: 0, y: 0, initialized: true };
|
||
patchPage(page as any, cfg, cursor as any);
|
||
|
||
await (childFrame as any).click("button.submit", { timeout: 1234 });
|
||
|
||
expect(childFrame.locator).toHaveBeenCalledWith("button.submit");
|
||
expect(originalPageClick).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it.each([
|
||
["type", async (frame: any) => frame.type("input.email", "@")],
|
||
["fill", async (frame: any) => frame.fill("input.email", "@")],
|
||
["pressSequentially", async (frame: any) => frame.pressSequentially("input.email", "@")],
|
||
])("passes the page CDP session to frame.%s", async (_name, runFrameAction) => {
|
||
const { patchPage } = await import("../src/human/index.js");
|
||
|
||
const cdpSend = vi.fn(async () => ({}));
|
||
const childFrame = buildMockFrame();
|
||
const mainFrame = {
|
||
...buildMockFrame(),
|
||
childFrames: vi.fn(() => [childFrame]),
|
||
};
|
||
const page = buildMockPage({ mainFrameReturn: mainFrame });
|
||
page.context = vi.fn(() => ({
|
||
pages: vi.fn(() => []),
|
||
addInitScript: vi.fn(async () => {}),
|
||
newCDPSession: vi.fn(async () => ({ send: cdpSend })),
|
||
}));
|
||
|
||
const cfg = resolveConfig("default", {
|
||
field_switch_delay: [0, 0],
|
||
key_hold: [0, 0],
|
||
shift_down_delay: [0, 0],
|
||
shift_up_delay: [0, 0],
|
||
typing_delay: 0,
|
||
typing_delay_spread: 0,
|
||
typing_pause_chance: 0,
|
||
mistype_chance: 0,
|
||
mouse_min_steps: 1,
|
||
mouse_max_steps: 1,
|
||
idle_between_actions: false,
|
||
});
|
||
const cursor = { x: 0, y: 0, initialized: true };
|
||
patchPage(page as any, cfg, cursor as any);
|
||
|
||
await runFrameAction(childFrame);
|
||
|
||
const dispatches = cdpSend.mock.calls.filter(([method]) => method === "Input.dispatchKeyEvent");
|
||
expect(dispatches).toHaveLength(2);
|
||
expect(page.evaluate).not.toHaveBeenCalled();
|
||
});
|
||
});
|
||
|
||
// =========================================================================
|
||
// Mistype config
|
||
// =========================================================================
|
||
describe("mistype config", () => {
|
||
it("default config has valid mistype fields", () => {
|
||
const cfg = resolveConfig("default");
|
||
expect(typeof cfg.mistype_chance).toBe("number");
|
||
expect(cfg.mistype_chance).toBeGreaterThanOrEqual(0);
|
||
expect(cfg.mistype_chance).toBeLessThanOrEqual(1);
|
||
// mistype_delay_notice and mistype_delay_correct are [min, max] tuples
|
||
expect(Array.isArray(cfg.mistype_delay_notice)).toBe(true);
|
||
expect(cfg.mistype_delay_notice).toHaveLength(2);
|
||
expect(Array.isArray(cfg.mistype_delay_correct)).toBe(true);
|
||
expect(cfg.mistype_delay_correct).toHaveLength(2);
|
||
});
|
||
|
||
it("mistype_chance can be overridden to 0 (disabled)", () => {
|
||
const cfg = resolveConfig("default", { mistype_chance: 0 });
|
||
expect(cfg.mistype_chance).toBe(0);
|
||
});
|
||
|
||
it("mistype_chance can be overridden to higher value", () => {
|
||
const cfg = resolveConfig("default", { mistype_chance: 0.15 });
|
||
expect(cfg.mistype_chance).toBe(0.15);
|
||
});
|
||
});
|
||
|
||
// =========================================================================
|
||
// Module exports
|
||
// =========================================================================
|
||
describe("module exports", () => {
|
||
it("patchBrowser, patchContext, patchPage are all exported functions", async () => {
|
||
const mod = await import("../src/human/index.js");
|
||
expect(typeof mod.patchBrowser).toBe("function");
|
||
expect(typeof mod.patchContext).toBe("function");
|
||
expect(typeof mod.patchPage).toBe("function");
|
||
});
|
||
|
||
it("humanMove, humanClick, clickTarget, humanIdle are exported", async () => {
|
||
const mod = await import("../src/human/index.js");
|
||
expect(typeof mod.humanMove).toBe("function");
|
||
expect(typeof mod.humanClick).toBe("function");
|
||
expect(typeof mod.clickTarget).toBe("function");
|
||
expect(typeof mod.humanIdle).toBe("function");
|
||
});
|
||
|
||
it("resolveConfig is re-exported from index", async () => {
|
||
const mod = await import("../src/human/index.js");
|
||
expect(typeof mod.resolveConfig).toBe("function");
|
||
});
|
||
});
|
||
|
||
// =========================================================================
|
||
// patchBrowser on CDP-connected browser (issue #126)
|
||
// =========================================================================
|
||
describe("patchBrowser CDP-connected workflow", () => {
|
||
it("patches existing pages on a browser with pre-existing contexts", async () => {
|
||
const { patchBrowser, resolveConfig } = await import("../src/human/index.js");
|
||
|
||
// Simulate a CDP-connected browser: it already has contexts and pages
|
||
const page = buildMockPage();
|
||
const context: any = {
|
||
pages: vi.fn(() => [page]),
|
||
on: vi.fn(),
|
||
newPage: vi.fn(async () => buildMockPage()),
|
||
addInitScript: vi.fn(async () => { }),
|
||
};
|
||
const browser: any = {
|
||
contexts: vi.fn(() => [context]),
|
||
newContext: vi.fn(async () => context),
|
||
newPage: vi.fn(async () => page),
|
||
};
|
||
|
||
const cfg = resolveConfig("default");
|
||
patchBrowser(browser, cfg);
|
||
|
||
// page should now have _original (proof it was patched)
|
||
expect((page as any)._original).toBeDefined();
|
||
expect((page as any)._original.click).toBeTypeOf("function");
|
||
expect((page as any)._original.fill).toBeTypeOf("function");
|
||
expect((page as any)._humanCfg).toBe(cfg);
|
||
});
|
||
|
||
it("patched click calls mouse.down (humanized path, not original)", async () => {
|
||
const { patchBrowser, resolveConfig } = await import("../src/human/index.js");
|
||
|
||
let downCalled = false;
|
||
const page = buildMockPage();
|
||
page.mouse.down = vi.fn(async () => { downCalled = true; });
|
||
|
||
const context: any = {
|
||
pages: vi.fn(() => [page]),
|
||
on: vi.fn(),
|
||
newPage: vi.fn(async () => buildMockPage()),
|
||
addInitScript: vi.fn(async () => { }),
|
||
};
|
||
const browser: any = {
|
||
contexts: vi.fn(() => [context]),
|
||
newContext: vi.fn(async () => context),
|
||
newPage: vi.fn(async () => page),
|
||
};
|
||
|
||
patchBrowser(browser, resolveConfig("default"));
|
||
|
||
// Click through the patched method — should go through humanize path
|
||
try { await (page as any).click("button", { timeout: 2000 }); } catch (_) { }
|
||
|
||
expect(downCalled).toBe(true);
|
||
}, 30000);
|
||
|
||
it("new contexts created after patchBrowser are also patched", async () => {
|
||
const { patchBrowser, resolveConfig } = await import("../src/human/index.js");
|
||
|
||
const newPage = buildMockPage();
|
||
const newContext: any = {
|
||
pages: vi.fn(() => [newPage]),
|
||
on: vi.fn(),
|
||
newPage: vi.fn(async () => buildMockPage()),
|
||
addInitScript: vi.fn(async () => { }),
|
||
};
|
||
const browser: any = {
|
||
contexts: vi.fn(() => []),
|
||
newContext: vi.fn(async () => newContext),
|
||
newPage: vi.fn(async () => newPage),
|
||
};
|
||
|
||
patchBrowser(browser, resolveConfig("default"));
|
||
|
||
// Create a new context via the patched newContext
|
||
const ctx = await browser.newContext();
|
||
// Pages in the new context should be patched
|
||
expect((newPage as any)._original).toBeDefined();
|
||
});
|
||
});
|
||
|
||
// =========================================================================
|
||
// Test helpers
|
||
// =========================================================================
|
||
|
||
function buildMockPage(overrides: Record<string, any> = {}): any {
|
||
const mainFrameObj = overrides.mainFrameReturn ?? {
|
||
childFrames: vi.fn(() => []),
|
||
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 () => { }),
|
||
clear: vi.fn(async () => { }),
|
||
dragAndDrop: vi.fn(async () => { }),
|
||
locator: vi.fn(() => {
|
||
const frameLoc: any = {
|
||
boundingBox: vi.fn(async () => ({ x: 0, y: 0, width: 100, height: 30 })),
|
||
waitFor: vi.fn(async () => {}),
|
||
isVisible: vi.fn(async () => true),
|
||
isEnabled: vi.fn(async () => true),
|
||
isEditable: vi.fn(async () => true),
|
||
evaluate: vi.fn(async () => ({ hit: true })),
|
||
};
|
||
frameLoc.first = vi.fn(() => frameLoc);
|
||
return frameLoc;
|
||
}),
|
||
};
|
||
|
||
const makeLocator = () => {
|
||
const loc: any = {
|
||
boundingBox: vi.fn(async () => ({ x: 100, y: 300, width: 200, height: 30 })),
|
||
scrollIntoViewIfNeeded: vi.fn(async () => { }),
|
||
isChecked: overrides.isChecked ?? vi.fn(async () => false),
|
||
waitFor: vi.fn(async () => {}),
|
||
isVisible: vi.fn(async () => true),
|
||
isEnabled: vi.fn(async () => true),
|
||
isEditable: vi.fn(async () => true),
|
||
evaluate: vi.fn(async () => ({ hit: true })),
|
||
};
|
||
loc.first = vi.fn(() => loc);
|
||
return loc;
|
||
};
|
||
|
||
const page: any = {
|
||
evaluate: overrides.evaluate ?? vi.fn(async () => ({ hit: true })),
|
||
addInitScript: vi.fn(async () => { }),
|
||
mouse: {
|
||
move: vi.fn(async () => { }),
|
||
down: vi.fn(async () => { }),
|
||
up: vi.fn(async () => { }),
|
||
click: vi.fn(async () => { }),
|
||
dblclick: vi.fn(async () => { }),
|
||
wheel: 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 () => { }),
|
||
insertText: vi.fn(async () => { }),
|
||
},
|
||
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 () => { }),
|
||
goto: vi.fn(async () => ({})),
|
||
isChecked: overrides.isChecked ?? vi.fn(async () => false),
|
||
locator: vi.fn(() => makeLocator()),
|
||
viewportSize: vi.fn(() => ({ width: 1280, height: 720 })),
|
||
mainFrame: vi.fn(() => mainFrameObj),
|
||
frames: vi.fn(() => []),
|
||
context: vi.fn(() => ({
|
||
pages: vi.fn(() => []),
|
||
addInitScript: vi.fn(async () => { }),
|
||
newCDPSession: vi.fn(async () => { throw new Error('no cdp'); }),
|
||
})),
|
||
url: vi.fn(() => "about:blank"),
|
||
waitForTimeout: vi.fn(async () => { }),
|
||
};
|
||
return page;
|
||
}
|
||
|
||
// =========================================================================
|
||
// humanType non-ASCII
|
||
// =========================================================================
|
||
describe("humanType non-ASCII", () => {
|
||
function makeRawKeyboardMock() {
|
||
const downKeys: string[] = [];
|
||
const insertedChars: string[] = [];
|
||
const raw = {
|
||
down: vi.fn(async (k: string) => { downKeys.push(k); }),
|
||
up: vi.fn(async () => { }),
|
||
type: vi.fn(async () => { }),
|
||
insertText: vi.fn(async (t: string) => { insertedChars.push(t); }),
|
||
};
|
||
return { raw, downKeys, insertedChars };
|
||
}
|
||
|
||
it("types Cyrillic via insertText, not down", async () => {
|
||
const { humanType } = await import("../src/human/keyboard.js");
|
||
const cfg = resolveConfig("default", { mistype_chance: 0 });
|
||
const { raw, downKeys, insertedChars } = makeRawKeyboardMock();
|
||
|
||
await humanType({} as any, raw, "Привет", cfg);
|
||
|
||
expect(insertedChars.join("")).toBe("Привет");
|
||
for (const k of downKeys) {
|
||
expect(k.charCodeAt(0)).toBeLessThan(128);
|
||
}
|
||
});
|
||
|
||
it("types mixed ASCII + Cyrillic correctly", async () => {
|
||
const { humanType } = await import("../src/human/keyboard.js");
|
||
const cfg = resolveConfig("default", { mistype_chance: 0 });
|
||
const { raw, downKeys, insertedChars } = makeRawKeyboardMock();
|
||
|
||
await humanType({} as any, raw, "Hi Мир", cfg);
|
||
|
||
expect(downKeys).toContain("H");
|
||
expect(downKeys).toContain("i");
|
||
expect(insertedChars.join("")).toContain("М");
|
||
expect(insertedChars.join("")).toContain("и");
|
||
expect(insertedChars.join("")).toContain("р");
|
||
});
|
||
|
||
it("types CJK via insertText", async () => {
|
||
const { humanType } = await import("../src/human/keyboard.js");
|
||
const cfg = resolveConfig("default", { mistype_chance: 0 });
|
||
const { raw, insertedChars } = makeRawKeyboardMock();
|
||
|
||
await humanType({} as any, raw, "你好", cfg);
|
||
|
||
expect(insertedChars.join("")).toBe("你好");
|
||
});
|
||
|
||
it("types emoji via insertText", async () => {
|
||
const { humanType } = await import("../src/human/keyboard.js");
|
||
const cfg = resolveConfig("default", { mistype_chance: 0 });
|
||
const { raw, insertedChars } = makeRawKeyboardMock();
|
||
|
||
await humanType({} as any, raw, "Hi 👋", cfg);
|
||
|
||
expect(insertedChars.join("")).toContain("👋");
|
||
});
|
||
|
||
it("mistype only triggers for ASCII, not Cyrillic", async () => {
|
||
const { humanType } = await import("../src/human/keyboard.js");
|
||
const cfg = resolveConfig("default", { mistype_chance: 1.0 });
|
||
const { raw, downKeys } = makeRawKeyboardMock();
|
||
|
||
await humanType({} as any, raw, "AБ", cfg);
|
||
|
||
expect(downKeys).toContain("Backspace");
|
||
});
|
||
});
|
||
|
||
|
||
|
||
// =========================================================================
|
||
// ElementHandle patching (Playwright)
|
||
// =========================================================================
|
||
|
||
function buildMockElementHandle(overrides: Record<string, any> = {}): any {
|
||
const el: any = {
|
||
click: vi.fn(async () => { }),
|
||
dblclick: vi.fn(async () => { }),
|
||
hover: vi.fn(async () => { }),
|
||
type: vi.fn(async () => { }),
|
||
fill: vi.fn(async () => { }),
|
||
press: vi.fn(async () => { }),
|
||
selectOption: vi.fn(async () => { }),
|
||
check: vi.fn(async () => { }),
|
||
uncheck: vi.fn(async () => { }),
|
||
setChecked: vi.fn(async () => { }),
|
||
tap: vi.fn(async () => { }),
|
||
focus: vi.fn(async () => { }),
|
||
boundingBox: overrides.boundingBox ?? vi.fn(async () => ({ x: 100, y: 100, width: 200, height: 30 })),
|
||
evaluate: overrides.evaluate ?? vi.fn(async () => ({ hit: true })),
|
||
isChecked: overrides.isChecked ?? vi.fn(async () => false),
|
||
waitForElementState: vi.fn(async () => {}),
|
||
$: vi.fn(async () => null),
|
||
$$: vi.fn(async () => []),
|
||
waitForSelector: vi.fn(async () => null),
|
||
_humanPatched: false,
|
||
};
|
||
return el;
|
||
}
|
||
|
||
describe("patchSingleElementHandle", () => {
|
||
it("marks element as patched", async () => {
|
||
const { patchSingleElementHandle } = await import("../src/human/elementhandle.js");
|
||
const cfg = resolveConfig("default");
|
||
const cursor = { x: 100, y: 100, initialized: true };
|
||
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 originals = {
|
||
keyboardPress: vi.fn(async () => { }),
|
||
keyboardDown: vi.fn(async () => { }),
|
||
keyboardUp: vi.fn(async () => { }),
|
||
};
|
||
|
||
const el = buildMockElementHandle();
|
||
const page = buildMockPage();
|
||
|
||
patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null);
|
||
|
||
expect(el._humanPatched).toBe(true);
|
||
});
|
||
|
||
it("el.click calls mouse.move and mouse.down/up (humanized path)", async () => {
|
||
const { patchSingleElementHandle } = await import("../src/human/elementhandle.js");
|
||
const cfg = resolveConfig("default", { idle_between_actions: false });
|
||
const cursor = { x: 50, y: 50, initialized: true };
|
||
|
||
let moveCount = 0;
|
||
let downCalled = false;
|
||
let upCalled = false;
|
||
const raw = {
|
||
move: vi.fn(async () => { moveCount++; }),
|
||
down: vi.fn(async () => { downCalled = true; }),
|
||
up: vi.fn(async () => { upCalled = true; }),
|
||
wheel: 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 el = buildMockElementHandle();
|
||
const page = buildMockPage();
|
||
(page as any)._ensureCursorInit = vi.fn(async () => { });
|
||
|
||
patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null);
|
||
|
||
await el.click();
|
||
|
||
expect(moveCount).toBeGreaterThan(0);
|
||
expect(downCalled).toBe(true);
|
||
expect(upCalled).toBe(true);
|
||
}, 30000);
|
||
|
||
it("el.hover calls mouse.move but NOT down/up", async () => {
|
||
const { patchSingleElementHandle } = await import("../src/human/elementhandle.js");
|
||
const cfg = resolveConfig("default", { idle_between_actions: false });
|
||
const cursor = { x: 50, y: 50, initialized: true };
|
||
|
||
let downCalled = false;
|
||
const raw = {
|
||
move: vi.fn(async () => { }),
|
||
down: vi.fn(async () => { downCalled = true; }),
|
||
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 originals = { keyboardPress: vi.fn(async () => { }), keyboardDown: vi.fn(async () => { }), keyboardUp: vi.fn(async () => { }) };
|
||
|
||
const el = buildMockElementHandle();
|
||
const page = buildMockPage();
|
||
(page as any)._ensureCursorInit = vi.fn(async () => { });
|
||
|
||
patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null);
|
||
|
||
await el.hover();
|
||
|
||
expect(raw.move).toHaveBeenCalled();
|
||
expect(downCalled).toBe(false);
|
||
}, 30000);
|
||
|
||
it("el.type triggers mouse move + click + keyboard events", async () => {
|
||
const { patchSingleElementHandle } = await import("../src/human/elementhandle.js");
|
||
const cfg = resolveConfig("default", { idle_between_actions: false, mistype_chance: 0 });
|
||
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 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 el = buildMockElementHandle({ evaluate: vi.fn(async (js: string) => js.includes('elementFromPoint') ? { hit: true } : true) });
|
||
const page = buildMockPage();
|
||
(page as any)._ensureCursorInit = vi.fn(async () => { });
|
||
|
||
patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null);
|
||
|
||
await el.type("abc");
|
||
|
||
expect(raw.move).toHaveBeenCalled();
|
||
expect(raw.down).toHaveBeenCalled(); // click to focus
|
||
expect(rawKb.down).toHaveBeenCalled(); // keyboard typing
|
||
}, 30000);
|
||
|
||
it("el.fill calls selectAll + backspace + type", async () => {
|
||
const { patchSingleElementHandle } = await import("../src/human/elementhandle.js");
|
||
const cfg = resolveConfig("default", { idle_between_actions: false, mistype_chance: 0 });
|
||
const cursor = { x: 50, y: 50, initialized: true };
|
||
|
||
const pressedKeys: string[] = [];
|
||
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 originals = {
|
||
keyboardPress: vi.fn(async (key: string) => { pressedKeys.push(key); }),
|
||
keyboardDown: vi.fn(async () => { }),
|
||
keyboardUp: vi.fn(async () => { }),
|
||
};
|
||
|
||
const el = buildMockElementHandle({ evaluate: vi.fn(async (js: string) => js.includes('elementFromPoint') ? { hit: true } : true) });
|
||
const page = buildMockPage();
|
||
(page as any)._ensureCursorInit = vi.fn(async () => { });
|
||
|
||
patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null);
|
||
|
||
await el.fill("newtext");
|
||
|
||
const expected = process.platform === "darwin" ? "Meta+a" : "Control+a";
|
||
expect(pressedKeys).toContain(expected);
|
||
expect(pressedKeys).toContain("Backspace");
|
||
}, 30000);
|
||
|
||
it("no double patching", async () => {
|
||
const { patchSingleElementHandle } = await import("../src/human/elementhandle.js");
|
||
const cfg = resolveConfig("default");
|
||
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 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 el = buildMockElementHandle();
|
||
const page = buildMockPage();
|
||
|
||
patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null);
|
||
const firstClick = el.click;
|
||
|
||
patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null);
|
||
|
||
expect(el.click).toBe(firstClick);
|
||
});
|
||
|
||
it("nested $() returns patched child handle", async () => {
|
||
const { patchSingleElementHandle } = await import("../src/human/elementhandle.js");
|
||
const cfg = resolveConfig("default");
|
||
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 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 child = buildMockElementHandle();
|
||
const el = buildMockElementHandle();
|
||
el.$ = vi.fn(async () => child);
|
||
|
||
const page = buildMockPage();
|
||
|
||
patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null);
|
||
|
||
const result = await el.$("span");
|
||
expect(result._humanPatched).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe("patchPageElementHandles", () => {
|
||
it("page.$() returns patched ElementHandle", async () => {
|
||
const { patchPageElementHandles } = await import("../src/human/elementhandle.js");
|
||
const cfg = resolveConfig("default");
|
||
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 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 el = buildMockElementHandle();
|
||
const page = buildMockPage();
|
||
(page as any).$ = vi.fn(async () => el);
|
||
(page as any).$$ = vi.fn(async () => [el]);
|
||
(page as any).waitForSelector = vi.fn(async () => el);
|
||
|
||
patchPageElementHandles(page as any, cfg, cursor as any, raw, rawKb, originals, null);
|
||
|
||
const result = await (page as any).$("#test");
|
||
expect(result._humanPatched).toBe(true);
|
||
});
|
||
|
||
it("page.$$() returns all patched handles", async () => {
|
||
const { patchPageElementHandles } = await import("../src/human/elementhandle.js");
|
||
const cfg = resolveConfig("default");
|
||
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 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 el1 = buildMockElementHandle();
|
||
const el2 = buildMockElementHandle();
|
||
const page = buildMockPage();
|
||
(page as any).$ = vi.fn(async () => null);
|
||
(page as any).$$ = vi.fn(async () => [el1, el2]);
|
||
(page as any).waitForSelector = vi.fn(async () => null);
|
||
|
||
patchPageElementHandles(page as any, cfg, cursor as any, raw, rawKb, originals, null);
|
||
|
||
const results = await (page as any).$$("div");
|
||
expect(results[0]._humanPatched).toBe(true);
|
||
expect(results[1]._humanPatched).toBe(true);
|
||
});
|
||
|
||
it("page.waitForSelector() returns patched handle", async () => {
|
||
const { patchPageElementHandles } = await import("../src/human/elementhandle.js");
|
||
const cfg = resolveConfig("default");
|
||
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 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 el = buildMockElementHandle();
|
||
const page = buildMockPage();
|
||
(page as any).$ = vi.fn(async () => null);
|
||
(page as any).$$ = vi.fn(async () => []);
|
||
(page as any).waitForSelector = vi.fn(async () => el);
|
||
|
||
patchPageElementHandles(page as any, cfg, cursor as any, raw, rawKb, originals, null);
|
||
|
||
const result = await (page as any).waitForSelector("#test");
|
||
expect(result._humanPatched).toBe(true);
|
||
});
|
||
|
||
it("page.$() returns null when no element found (no crash)", async () => {
|
||
const { patchPageElementHandles } = await import("../src/human/elementhandle.js");
|
||
const cfg = resolveConfig("default");
|
||
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 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 page = buildMockPage();
|
||
(page as any).$ = vi.fn(async () => null);
|
||
(page as any).$$ = vi.fn(async () => []);
|
||
(page as any).waitForSelector = vi.fn(async () => null);
|
||
|
||
patchPageElementHandles(page as any, cfg, cursor as any, raw, rawKb, originals, null);
|
||
|
||
const result = await (page as any).$("#nonexistent");
|
||
expect(result).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe("patchPage integrates ElementHandle patching", () => {
|
||
it("patchPage patches page.$ automatically", async () => {
|
||
const { patchPage } = await import("../src/human/index.js");
|
||
|
||
const el = buildMockElementHandle();
|
||
const page = buildMockPage();
|
||
(page as any).$ = vi.fn(async () => el);
|
||
(page as any).$$ = vi.fn(async () => []);
|
||
(page as any).waitForSelector = vi.fn(async () => null);
|
||
|
||
const cfg = resolveConfig("default");
|
||
const cursor = { x: 100, y: 100, initialized: true };
|
||
patchPage(page as any, cfg, cursor as any);
|
||
|
||
const result = await (page as any).$("#test");
|
||
expect(result._humanPatched).toBe(true);
|
||
});
|
||
});
|
||
|
||
|
||
function buildMockFrame(): any {
|
||
const locator: any = {
|
||
boundingBox: vi.fn(async () => ({ x: 0, y: 0, width: 100, height: 30 })),
|
||
scrollIntoViewIfNeeded: vi.fn(async () => {}),
|
||
evaluate: vi.fn(async () => ({ hit: true })),
|
||
isChecked: vi.fn(async () => false),
|
||
};
|
||
locator.first = vi.fn(() => locator);
|
||
|
||
return {
|
||
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(() => locator),
|
||
childFrames: vi.fn(() => []),
|
||
};
|
||
}
|
||
|
||
// =========================================================================
|
||
// mergeConfig
|
||
// =========================================================================
|
||
describe("mergeConfig", () => {
|
||
it("returns base unchanged when overrides is undefined/null", async () => {
|
||
const { mergeConfig, resolveConfig: rc } = await import("../src/human/config.js");
|
||
const base = rc("default");
|
||
expect(mergeConfig(base, undefined)).toBe(base);
|
||
expect(mergeConfig(base, null)).toBe(base);
|
||
});
|
||
|
||
it("creates a new object — base is never mutated", async () => {
|
||
const { mergeConfig, resolveConfig: rc } = await import("../src/human/config.js");
|
||
const base = rc("default");
|
||
const before = base.typing_delay;
|
||
const merged = mergeConfig(base, { typing_delay: 30 });
|
||
expect(merged.typing_delay).toBe(30);
|
||
expect(base.typing_delay).toBe(before);
|
||
expect(merged).not.toBe(base);
|
||
});
|
||
|
||
it("preserves non-overridden fields", async () => {
|
||
const { mergeConfig, resolveConfig: rc } = await import("../src/human/config.js");
|
||
const base = rc("default");
|
||
const merged = mergeConfig(base, { typing_delay: 30 });
|
||
expect(merged.mouse_min_steps).toBe(base.mouse_min_steps);
|
||
expect(merged.mistype_chance).toBe(base.mistype_chance);
|
||
});
|
||
});
|
||
|
||
|
||
// =========================================================================
|
||
// Per-call timeout forwarding (issue #137)
|
||
// =========================================================================
|
||
describe("page.click(selector, { timeout }) forwards timeout to scroll", () => {
|
||
it("scrollToElement passes timeout to locator.boundingBox()", async () => {
|
||
const { scrollToElement } = await import("../src/human/scroll.js");
|
||
const cfg = resolveConfig("default");
|
||
|
||
const boundingBox = vi.fn(async () => ({ x: 100, y: 200, width: 50, height: 30 }));
|
||
const page: any = {
|
||
viewportSize: () => ({ width: 1280, height: 720 }),
|
||
locator: vi.fn(() => ({ first: () => ({ boundingBox }) })),
|
||
};
|
||
const raw = {
|
||
move: vi.fn(async () => { }),
|
||
down: vi.fn(async () => { }),
|
||
up: vi.fn(async () => { }),
|
||
wheel: vi.fn(async () => { }),
|
||
};
|
||
|
||
await scrollToElement(page, raw, "#x", 0, 0, cfg, 5000);
|
||
expect(boundingBox).toHaveBeenCalledWith({ timeout: 5000 });
|
||
});
|
||
|
||
it("default timeout matches Playwright's 30000ms when not specified", async () => {
|
||
const { scrollToElement } = await import("../src/human/scroll.js");
|
||
const cfg = resolveConfig("default");
|
||
|
||
const boundingBox = vi.fn(async () => ({ x: 100, y: 200, width: 50, height: 30 }));
|
||
const page: any = {
|
||
viewportSize: () => ({ width: 1280, height: 720 }),
|
||
locator: vi.fn(() => ({ first: () => ({ boundingBox }) })),
|
||
};
|
||
const raw = {
|
||
move: vi.fn(async () => { }),
|
||
down: vi.fn(async () => { }),
|
||
up: vi.fn(async () => { }),
|
||
wheel: vi.fn(async () => { }),
|
||
};
|
||
|
||
await scrollToElement(page, raw, "#x", 0, 0, cfg);
|
||
expect(boundingBox).toHaveBeenCalledWith({ timeout: 30000 });
|
||
});
|
||
|
||
it("page.click({ timeout }) reaches scrollToElement", async () => {
|
||
const scrollMod = await import("../src/human/scroll.js");
|
||
const { patchPage } = await import("../src/human/index.js");
|
||
const cfg = resolveConfig("default", { idle_between_actions: false });
|
||
|
||
let captured = -1;
|
||
const spy = vi.spyOn(scrollMod, "scrollToElement").mockImplementation(
|
||
async (_page, _raw, _sel, cx, cy, _cfg, timeout?: number) => {
|
||
captured = timeout ?? -1;
|
||
return { box: { x: 100, y: 100, width: 50, height: 30 }, cursorX: cx, cursorY: cy, didScroll: false };
|
||
},
|
||
);
|
||
|
||
const page = buildMockPage();
|
||
const cursor = { x: 100, y: 100, initialized: true };
|
||
patchPage(page as any, cfg, cursor as any);
|
||
try {
|
||
await (page as any).click("#slow", { timeout: 2000 });
|
||
} catch (_) { }
|
||
|
||
if (captured > 0) {
|
||
expect(captured).toBeGreaterThan(1500);
|
||
expect(captured).toBeLessThanOrEqual(2000);
|
||
}
|
||
spy.mockRestore();
|
||
});
|
||
});
|
||
|
||
|
||
// =========================================================================
|
||
// Per-call human config override
|
||
// =========================================================================
|
||
describe("page.type / page.fill accept per-call human config override", () => {
|
||
it("page.type forwards nested human_config to humanType", async () => {
|
||
const keyboardMod = await import("../src/human/keyboard.js");
|
||
const scrollMod = await import("../src/human/scroll.js");
|
||
const { patchPage } = await import("../src/human/index.js");
|
||
|
||
// Make field_switch_delay tiny so the test runs fast
|
||
const cfg = resolveConfig("default", {
|
||
idle_between_actions: false,
|
||
field_switch_delay: [0, 1],
|
||
});
|
||
expect(cfg.typing_delay).toBe(70); // baseline
|
||
|
||
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, didScroll: false,
|
||
}),
|
||
);
|
||
|
||
const page = buildMockPage();
|
||
const cursor = { x: 100, y: 100, initialized: true };
|
||
patchPage(page as any, cfg, cursor as any);
|
||
|
||
try {
|
||
await (page as any).type("#email", "hi", {
|
||
timeout: 2000,
|
||
human_config: { typing_delay: 30, mistype_chance: 0 },
|
||
});
|
||
} catch (_) { }
|
||
|
||
if (captured) {
|
||
expect(captured.typing_delay).toBe(30);
|
||
expect(captured.mistype_chance).toBe(0);
|
||
}
|
||
expect(cfg.typing_delay).toBe(70);
|
||
|
||
typeSpy.mockRestore();
|
||
scrollSpy.mockRestore();
|
||
}, 5000);
|
||
|
||
it("page.fill forwards flat config to humanType", async () => {
|
||
const keyboardMod = await import("../src/human/keyboard.js");
|
||
const scrollMod = await import("../src/human/scroll.js");
|
||
const { patchPage } = await import("../src/human/index.js");
|
||
|
||
const cfg = resolveConfig("default", {
|
||
idle_between_actions: false,
|
||
field_switch_delay: [0, 1],
|
||
});
|
||
|
||
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, didScroll: false,
|
||
}),
|
||
);
|
||
|
||
const page = buildMockPage();
|
||
const cursor = { x: 100, y: 100, initialized: true };
|
||
patchPage(page as any, cfg, cursor as any);
|
||
|
||
try {
|
||
await (page as any).fill("#password", "secret", {
|
||
timeout: 2000,
|
||
typing_delay: 150,
|
||
});
|
||
} catch (_) { }
|
||
|
||
if (captured) {
|
||
expect(captured.typing_delay).toBe(150);
|
||
}
|
||
|
||
typeSpy.mockRestore();
|
||
scrollSpy.mockRestore();
|
||
}, 30000);
|
||
|
||
it("el.type forwards human_config to humanType", async () => {
|
||
const keyboardMod = await import("../src/human/keyboard.js");
|
||
const { patchSingleElementHandle } = await import("../src/human/elementhandle.js");
|
||
const cfg = resolveConfig("default", { idle_between_actions: false, mistype_chance: 0 });
|
||
const cursor = { x: 50, y: 50, initialized: true };
|
||
|
||
let captured: any = null;
|
||
const typeSpy = vi.spyOn(keyboardMod, "humanType").mockImplementation(
|
||
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 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 el = buildMockElementHandle({ evaluate: vi.fn(async (js: string) => js.includes('elementFromPoint') ? { hit: true } : true) });
|
||
const page = buildMockPage();
|
||
(page as any)._ensureCursorInit = vi.fn(async () => { });
|
||
|
||
patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null);
|
||
|
||
await el.type("abc", { human_config: { typing_delay: 25 } });
|
||
expect(captured.typing_delay).toBe(25);
|
||
|
||
typeSpy.mockRestore();
|
||
}, 30000);
|
||
});
|
||
|
||
|
||
// =========================================================================
|
||
// scrollIntoViewIfNeeded humanization
|
||
// =========================================================================
|
||
describe("humanScrollIntoView", () => {
|
||
it("skips wheel events when element is already in viewport", async () => {
|
||
const { humanScrollIntoView } = await import("../src/human/scroll.js");
|
||
const cfg = resolveConfig("default");
|
||
|
||
const page: any = { viewportSize: () => ({ width: 1280, height: 720 }) };
|
||
const raw = {
|
||
move: vi.fn(async () => { }),
|
||
down: vi.fn(async () => { }),
|
||
up: vi.fn(async () => { }),
|
||
wheel: vi.fn(async () => { }),
|
||
};
|
||
// Box centered in viewport — squarely in scroll_target_zone
|
||
const inViewBox = { x: 200, y: 300, width: 50, height: 30 };
|
||
const result = await humanScrollIntoView(page, raw, async () => inViewBox, 0, 0, cfg);
|
||
|
||
expect(result.box).toEqual(inViewBox);
|
||
expect(raw.wheel).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it("fires wheel events when element is below the fold", async () => {
|
||
const { humanScrollIntoView } = await import("../src/human/scroll.js");
|
||
const cfg = resolveConfig("default", {
|
||
scroll_overshoot_chance: 0,
|
||
scroll_pre_move_delay: [0, 1],
|
||
scroll_pause_fast: [0, 1],
|
||
scroll_pause_slow: [0, 1],
|
||
scroll_settle_delay: [0, 1],
|
||
});
|
||
|
||
const page: any = { viewportSize: () => ({ width: 1280, height: 720 }) };
|
||
const raw = {
|
||
move: vi.fn(async () => { }),
|
||
down: vi.fn(async () => { }),
|
||
up: vi.fn(async () => { }),
|
||
wheel: vi.fn(async () => { }),
|
||
};
|
||
|
||
const boxes = [
|
||
{ x: 200, y: 2000, width: 50, height: 30 }, // far below
|
||
{ x: 200, y: 1500, width: 50, height: 30 },
|
||
{ x: 200, y: 1000, width: 50, height: 30 },
|
||
{ x: 200, y: 400, width: 50, height: 30 }, // in view
|
||
{ x: 200, y: 400, width: 50, height: 30 },
|
||
{ x: 200, y: 400, width: 50, height: 30 },
|
||
];
|
||
let i = 0;
|
||
const getBox = async () => boxes[Math.min(i++, boxes.length - 1)];
|
||
|
||
await humanScrollIntoView(page, raw, getBox, 0, 0, cfg);
|
||
expect(raw.wheel).toHaveBeenCalled();
|
||
}, 15000);
|
||
});
|
||
|
||
describe("el.scrollIntoViewIfNeeded humanization", () => {
|
||
it("calls humanScrollIntoView instead of native snap-scroll", async () => {
|
||
const scrollMod = await import("../src/human/scroll.js");
|
||
const { patchSingleElementHandle } = await import("../src/human/elementhandle.js");
|
||
|
||
let called = 0;
|
||
const spy = vi.spyOn(scrollMod, "humanScrollIntoView").mockImplementation(
|
||
async (_p, _raw, _gb, cx, cy) => {
|
||
called++;
|
||
return { box: { x: 200, y: 200, width: 50, height: 30 }, cursorX: cx, cursorY: cy };
|
||
},
|
||
);
|
||
|
||
const cfg = resolveConfig("default", { idle_between_actions: false });
|
||
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 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 el = buildMockElementHandle();
|
||
el.scrollIntoViewIfNeeded = vi.fn(async () => { });
|
||
const page = buildMockPage();
|
||
(page as any)._ensureCursorInit = vi.fn(async () => { });
|
||
|
||
patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null);
|
||
await el.scrollIntoViewIfNeeded();
|
||
|
||
expect(called).toBeGreaterThan(0);
|
||
spy.mockRestore();
|
||
});
|
||
|
||
it("falls back to native scrollIntoViewIfNeeded if humanized helper throws", async () => {
|
||
const scrollMod = await import("../src/human/scroll.js");
|
||
const { patchSingleElementHandle } = await import("../src/human/elementhandle.js");
|
||
|
||
const spy = vi.spyOn(scrollMod, "humanScrollIntoView").mockImplementation(
|
||
async () => { throw new Error("detached"); },
|
||
);
|
||
|
||
const cfg = resolveConfig("default", { idle_between_actions: false });
|
||
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 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 nativeFallback = vi.fn(async () => { });
|
||
const el = buildMockElementHandle();
|
||
el.scrollIntoViewIfNeeded = nativeFallback;
|
||
const page = buildMockPage();
|
||
(page as any)._ensureCursorInit = vi.fn(async () => { });
|
||
|
||
patchSingleElementHandle(el, page as any, cfg, cursor as any, raw, rawKb, originals, null);
|
||
await el.scrollIntoViewIfNeeded();
|
||
|
||
expect(nativeFallback).toHaveBeenCalled();
|
||
spy.mockRestore();
|
||
});
|
||
});
|
||
|
||
|
||
// =========================================================================
|
||
// Issue #307: frame.click timeout should not multiply
|
||
// =========================================================================
|
||
describe("frame.click timeout budget (#307)", () => {
|
||
it("total wait time should not exceed the specified timeout", async () => {
|
||
const { patchPage } = await import("../src/human/index.js");
|
||
|
||
const TIMEOUT_MS = 500;
|
||
const delay = (ms: number) => new Promise(r => setTimeout(r, ms));
|
||
|
||
// Build a frame where the element does NOT exist:
|
||
// scrollIntoViewIfNeeded and boundingBox each wait until their
|
||
// individual timeout before failing, and origFrameClick does the same.
|
||
const frameLoc: any = {
|
||
boundingBox: vi.fn(async (opts?: { timeout?: number }) => {
|
||
await delay(opts?.timeout ?? 30000);
|
||
return null;
|
||
}),
|
||
scrollIntoViewIfNeeded: vi.fn(async (opts?: { timeout?: number }) => {
|
||
await delay(opts?.timeout ?? 30000);
|
||
throw new Error("timeout");
|
||
}),
|
||
evaluate: vi.fn(async () => ({ hit: true })),
|
||
isChecked: vi.fn(async () => false),
|
||
};
|
||
frameLoc.first = vi.fn(() => frameLoc);
|
||
|
||
const origClickFn = vi.fn(async (_sel: string, opts?: any) => {
|
||
await delay(opts?.timeout ?? 30000);
|
||
throw new Error("timeout");
|
||
});
|
||
|
||
const childFrame: any = {
|
||
click: origClickFn,
|
||
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(() => frameLoc),
|
||
childFrames: vi.fn(() => []),
|
||
};
|
||
|
||
const mainFrame = {
|
||
...buildMockFrame(),
|
||
childFrames: vi.fn(() => [childFrame]),
|
||
};
|
||
|
||
const page = buildMockPage({ mainFrameReturn: mainFrame });
|
||
const cfg = resolveConfig("default", {
|
||
mouse_min_steps: 1,
|
||
mouse_max_steps: 1,
|
||
idle_between_actions: false,
|
||
});
|
||
const cursor = { x: 0, y: 0, initialized: true };
|
||
patchPage(page as any, cfg, cursor as any);
|
||
|
||
const start = Date.now();
|
||
try {
|
||
await (childFrame as any).click("#does-not-exist", { timeout: TIMEOUT_MS });
|
||
} catch {
|
||
// expected — element doesn't exist
|
||
}
|
||
const elapsed = Date.now() - start;
|
||
|
||
// With the bug, elapsed ≈ 3 * TIMEOUT_MS (scrollIntoView + boundingBox + origClick).
|
||
// Fixed: elapsed should be ≈ 1 * TIMEOUT_MS (shared deadline).
|
||
// Allow 1.8x as upper bound to account for test overhead but catch the 3x bug.
|
||
expect(elapsed).toBeLessThan(TIMEOUT_MS * 1.8);
|
||
});
|
||
});
|
||
|
||
describe("pointer-events check fail-open", () => {
|
||
// When the check itself cannot run (evaluate / boundingBox throws -> result
|
||
// null), proceed with the click instead of blocking it until the timeout.
|
||
it("checkPointerEventsHandle returns promptly when evaluate throws", async () => {
|
||
const { checkPointerEventsHandle } = await import("../src/human/actionability.js");
|
||
const el = {
|
||
boundingBox: vi.fn().mockRejectedValue(new Error("stale handle")),
|
||
evaluate: vi.fn().mockRejectedValue(new Error("execution context destroyed")),
|
||
};
|
||
const start = Date.now();
|
||
await checkPointerEventsHandle(el as any, 100, 100, 2000); // must not throw
|
||
expect(Date.now() - start).toBeLessThan(500);
|
||
});
|
||
|
||
it("checkPointerEvents returns promptly when evaluate throws", async () => {
|
||
const { checkPointerEvents } = await import("../src/human/actionability.js");
|
||
const loc = {
|
||
first: () => loc,
|
||
boundingBox: vi.fn().mockRejectedValue(new Error("no element")),
|
||
evaluate: vi.fn().mockRejectedValue(new Error("no element")),
|
||
};
|
||
const page = { locator: vi.fn().mockReturnValue(loc) };
|
||
const start = Date.now();
|
||
await checkPointerEvents(page as any, "#x", 100, 100, null, 2000); // must not throw
|
||
expect(Date.now() - start).toBeLessThan(500);
|
||
});
|
||
|
||
it("checkPointerEventsHandle still throws when genuinely covered", async () => {
|
||
const { checkPointerEventsHandle, ElementNotReceivingEventsError } =
|
||
await import("../src/human/actionability.js");
|
||
const el = {
|
||
boundingBox: vi.fn().mockResolvedValue({ x: 0, y: 0, width: 10, height: 10 }),
|
||
evaluate: vi.fn().mockResolvedValue({ hit: false, covering: "DIV" }),
|
||
};
|
||
await expect(checkPointerEventsHandle(el as any, 5, 5, 200)).rejects.toBeInstanceOf(
|
||
ElementNotReceivingEventsError,
|
||
);
|
||
});
|
||
});
|