fix(humanize): tolerate sub-pixel box edges in the scroll-into-view pinned check

Layout can report a top edge of -1/64 px on an unscrolled page, which skipped
the pinned-at-top check and sent a pointless upward scroll burst before the
click. Allow 1px slack in Python, JS and .NET, with regression tests.

Also credit @Shub3am in README contributors (#554).
This commit is contained in:
CloakHQ committed 2026-09-29 23:05:00 +02:00
1 parent 65b23cac51
commit 0e43a341ef
8 files changed
+64 -4

No files matched your search

+2 -1
View File
@@ -141,7 +141,8 @@ async function scrollYIntoView<T extends ElementBounds>(
return { box, cursorX, cursorY, didScroll: false };
}
const fullyVisible = box.y >= 0 && box.y + box.height <= viewport.height;
// 1px slack: layout can report a top edge of -1/64 px on an unscrolled page.
const fullyVisible = box.y >= -1 && box.y + box.height <= viewport.height + 1;
if (fullyVisible) {
const zoneMid = viewport.height * (cfg.scroll_target_zone[0] + cfg.scroll_target_zone[1]) / 2;
const needUp = box.y + box.height / 2 < zoneMid;
+21
View File
@@ -1634,6 +1634,27 @@ describe("humanScrollIntoView", () => {
expect(page.evaluate).not.toHaveBeenCalled();
});
it("bails when layout reports a sub-pixel negative top on an unscrolled page", async () => {
const { humanScrollIntoView } = await import("../src/human/scroll.js");
const cfg = resolveConfig("default");
const page: any = {
viewportSize: () => ({ width: 1000, height: 700 }),
evaluate: vi.fn(),
_stealth: { evaluate: vi.fn(async () => ({ y: 0, maxY: 0, x: 0, minX: 0, maxX: 0 })) },
};
const raw = {
move: vi.fn(async () => { }), down: vi.fn(async () => { }),
up: vi.fn(async () => { }), wheel: vi.fn(async () => { }),
};
const topBox = { x: 200, y: -0.015625, width: 200, height: 21.5 };
const result = await humanScrollIntoView(page, raw, async () => topBox, 0, 0, cfg);
expect(result.didScroll).toBe(false);
expect(raw.wheel).not.toHaveBeenCalled();
});
it("still scrolls a fully-visible above-zone element when the page CAN scroll up (no over-bail)", async () => {
const { humanScrollIntoView } = await import("../src/human/scroll.js");
const cfg = resolveConfig("default", {