feat(cdp): fire wheel events on Input.dispatchMouseEvent mouseWheel

Wire the CDP Input.dispatchMouseEvent "mouseWheel" type to a new
Frame.triggerMouseWheel, which hit-tests the point via elementFromPoint
and dispatches a wheel event (with deltaX/deltaY) on the target. When
the wheel event is not cancelled, it applies the scroll offset and
fires a scroll event, mirroring the wheel handling in WebDriver.zig.

Previously mouseWheel was silently ignored. Follow-up to #2636.
This commit is contained in:
Rohit
2026-06-04 17:14:54 +05:30
parent 426c167470
commit e05074dbf5
2 changed files with 90 additions and 7 deletions

View File

@@ -61,6 +61,7 @@ const popover = @import("webapi/element/popover.zig");
const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind;
const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig");
const MouseEvent = @import("webapi/event/MouseEvent.zig");
const WheelEvent = @import("webapi/event/WheelEvent.zig");
const HttpClient = @import("HttpClient.zig");
@@ -3962,6 +3963,55 @@ pub fn triggerMouseRelease(self: *Frame, x: f64, y: f64) !void {
try self._event_manager.dispatch(target.asEventTarget(), up_event.asEvent());
}
pub fn triggerMouseWheel(self: *Frame, x: f64, y: f64, delta_x: f64, delta_y: f64) !void {
const target = (try self.window._document.elementFromPoint(x, y, self)) orelse return;
if (comptime IS_DEBUG) {
log.debug(.frame, "frame mouse wheel", .{
.url = self.url,
.node = target,
.x = x,
.y = y,
.delta_x = delta_x,
.delta_y = delta_y,
.type = self._type,
});
}
const wheel_event: *WheelEvent = try .initTrusted("wheel", .{
.bubbles = true,
.cancelable = true,
.composed = true,
.clientX = x,
.clientY = y,
.deltaX = delta_x,
.deltaY = delta_y,
}, self);
// Keep the event alive past dispatch so we can read _prevent_default.
wheel_event.asEvent().acquireRef();
defer _ = wheel_event.asEvent().releaseRef(self._page);
try self._event_manager.dispatch(target.asEventTarget(), wheel_event.asEvent());
if (wheel_event.asEvent()._prevent_default) {
return;
}
// Apply the scroll and fire a trusted scroll event, mirroring WebDriver wheel.
// CDP deltas are untrusted, so guard NaN and saturate the addition.
const new_left: i32 = @as(i32, @intCast(target.getScrollLeft(self))) +| deltaToScroll(delta_x);
const new_top: i32 = @as(i32, @intCast(target.getScrollTop(self))) +| deltaToScroll(delta_y);
try target.setScrollLeft(new_left, self);
try target.setScrollTop(new_top, self);
const scroll_event = try Event.initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, self._page);
try self._event_manager.dispatch(target.asEventTarget(), scroll_event);
}
fn deltaToScroll(d: f64) i32 {
if (std.math.isNan(d)) return 0;
return @intFromFloat(std.math.clamp(d, std.math.minInt(i32), std.math.maxInt(i32)));
}
// callback when the "click" event reaches the frame.
pub fn handleClick(self: *Frame, target: *Node) !void {
// TODO: Also support <area> elements when implement

View File

@@ -82,6 +82,8 @@ fn dispatchMouseEvent(cmd: *CDP.Command) !void {
x: f64,
y: f64,
type: Type,
deltaX: f64 = 0,
deltaY: f64 = 0,
// Many optional parameters are not implemented yet, see documentation url.
const Type = enum {
@@ -94,19 +96,13 @@ fn dispatchMouseEvent(cmd: *CDP.Command) !void {
try cmd.sendResult(null, .{});
// quickly ignore types we know we don't handle
switch (params.type) {
.mouseWheel => return,
else => {},
}
const bc = cmd.browser_context orelse return;
const frame = bc.session.currentFrame() orelse return;
switch (params.type) {
.mousePressed => try frame.triggerMouseClick(params.x, params.y),
.mouseReleased => try frame.triggerMouseRelease(params.x, params.y),
.mouseMoved => try frame.triggerMouseMove(params.x, params.y),
.mouseWheel => unreachable,
.mouseWheel => try frame.triggerMouseWheel(params.x, params.y, params.deltaX, params.deltaY),
}
// result already sent
}
@@ -204,3 +200,40 @@ test "cdp.input: dispatchMouseEvent mouseReleased fires mouseup" {
const result = try ls.local.compileAndRun("window.released === true", null);
try testing.expect(result.isTrue());
}
test "cdp.input: dispatchMouseEvent mouseWheel fires wheel event" {
var ctx = try testing.context();
defer ctx.deinit();
const bc = try ctx.loadBrowserContext(.{});
const frame = try bc.session.createPage();
const url = "http://localhost:9582/src/browser/tests/mcp_actions.html";
try frame.navigate(url, .{ .reason = .address_bar, .kind = .{ .push = null } });
var runner = try bc.session.runner(.{});
try runner.wait(.{ .ms = 2000 });
var ls: lp.js.Local.Scope = undefined;
frame.js.localScope(&ls);
defer ls.deinit();
var try_catch: lp.js.TryCatch = undefined;
try_catch.init(&ls.local);
defer try_catch.deinit();
_ = try ls.local.compileAndRun(
\\document.getElementById('scrollbox')
\\ .addEventListener('wheel', (e) => { window.wheelDeltaY = e.deltaY; });
, null);
const rect_x = try (try ls.local.compileAndRun("document.getElementById('scrollbox').getBoundingClientRect().x", null)).toF64();
const rect_y = try (try ls.local.compileAndRun("document.getElementById('scrollbox').getBoundingClientRect().y", null)).toF64();
try ctx.processMessage(.{
.id = 1,
.method = "Input.dispatchMouseEvent",
.params = .{ .type = "mouseWheel", .x = rect_x, .y = rect_y, .deltaY = 40 },
});
const result = try ls.local.compileAndRun("window.wheelDeltaY === 40", null);
try testing.expect(result.isTrue());
}