From 15f2912d34eaf91c1c43bed0a6d78fd1fe7396ee Mon Sep 17 00:00:00 2001 From: Jake Writer Date: Fri, 17 Jul 2026 13:43:23 -0600 Subject: [PATCH] fix(juggler): restore humanized mouse trajectory dropped in FF146 migration (#677) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The FF146 Juggler migration (03c1230) rewrote the sendEvents() helper in Page.dispatchMouseEvent to batch events via Promise.all and, in doing so, dropped the Camoufox humanize branch. Since then, with humanize=True a long page.mouse.move() emitted only the single endpoint mousemove — the native C++ cursor trajectory (MouseTrajectories.hpp / ChromeUtils.camouGetMouseTrajectory) was never invoked, even though the generator and config plumbing remained. Reintroduce the trajectory expansion inside the new sendEvents(): - factor the single-event dispatch into a sendOne() helper - when humanize is enabled, expand a mousemove into the C++-generated trajectory, dispatching intermediate points sequentially with a 10ms delay, then always finishing exactly on the requested destination so clicks/hover still land on target - restore _lastTrackedPos so each move's trajectory starts from the real previous cursor position Verified against the built v152.0.4-beta.25 binary: humanize=True now emits ~180 intermediate mousemoves ending exactly at the destination (was 2); normal click/fill/drag and humanized click-to-target are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- additions/juggler/protocol/PageHandler.js | 45 ++++++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/additions/juggler/protocol/PageHandler.js b/additions/juggler/protocol/PageHandler.js index d94a148..d40ab65 100644 --- a/additions/juggler/protocol/PageHandler.js +++ b/additions/juggler/protocol/PageHandler.js @@ -81,6 +81,9 @@ export class PageHandler { this._isDragging = false; this._lastMousePosition = { x: 0, y: 0 }; + // Camoufox: last position an actual mousemove landed on. Used as the + // start point for the humanized cursor trajectory (humanize=True). + this._lastTrackedPos = { x: 0, y: 0 }; this._reportedFrameIds = new Set(); this._networkEventsForUnreportedFrameIds = new Map(); @@ -523,13 +526,14 @@ export class PageHandler { await helper.awaitTopic('apz-repaints-flushed'); const watcher = new EventWatcher(this._pageEventSink, types, this._pendingEventWatchers); - const promises = []; - for (const type of types) { + // Dispatch a single synthesized mouse event to the renderer and return a + // promise that resolves once the renderer acks it. + const sendOne = (eventType, eventX, eventY) => { // This dispatches to the renderer synchronously. const jugglerEventId = win.windowUtils.jugglerSendMouseEvent( - type, - x + boundingBox.left, - y + boundingBox.top, + eventType, + eventX + boundingBox.left, + eventY + boundingBox.top, button, clickCount, modifiers, @@ -542,7 +546,33 @@ export class PageHandler { win.windowUtils.DEFAULT_MOUSE_POINTER_ID /* pointerIdentifier */, false /* disablePointerEvent */ ); - promises.push(watcher.ensureEvent(type, eventObject => eventObject.jugglerEventId === jugglerEventId)); + return watcher.ensureEvent(eventType, eventObject => eventObject.jugglerEventId === jugglerEventId); + }; + + const promises = []; + for (const type of types) { + // Camoufox: when humanize is enabled, expand a direct mousemove into a + // human-like trajectory of intermediate mousemoves generated in C++ + // (ChromeUtils.camouGetMouseTrajectory / MouseTrajectories.hpp). + if (type === 'mousemove' && ChromeUtils.camouGetBool('humanize', false)) { + const trajectory = ChromeUtils.camouGetMouseTrajectory(this._lastTrackedPos.x, this._lastTrackedPos.y, x, y); + // Dispatch intermediate points sequentially with a short delay. The + // first/last pairs are skipped: the last pair is the exact + // destination, which is dispatched explicitly below. + for (let i = 2; i < trajectory.length - 2; i += 2) { + const currentX = trajectory[i]; + const currentY = trajectory[i + 1]; + // Skip movement that is out of bounds + if (currentX < 0 || currentY < 0 || currentX > boundingBox.width || currentY > boundingBox.height) + continue; + await sendOne('mousemove', currentX, currentY); + await new Promise(resolve => setTimeout(resolve, 10)); + } + // Always finish exactly on the requested destination. + promises.push(sendOne('mousemove', x, y)); + } else { + promises.push(sendOne(type, x, y)); + } } await Promise.all(promises); await watcher.dispose(); @@ -603,6 +633,9 @@ export class PageHandler { const watcher = new EventWatcher(this._pageEventSink, ['dragstart', 'juggler-drag-finalized'], this._pendingEventWatchers); await sendEvents(['mousemove']); + // Camoufox: remember where the cursor landed so the next humanized + // move starts its trajectory from the real previous position. + this._lastTrackedPos = { x, y }; // The order of events after 'mousemove' is sent: // 1. [dragstart] - might or might NOT be emitted