fix(juggler): restore humanized mouse trajectory dropped in FF146 migration (#677)

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) <noreply@anthropic.com>
This commit is contained in:
Jake Writer
2026-07-17 13:43:23 -06:00
parent 73a04a6c5e
commit 15f2912d34
+39 -6
View File
@@ -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