feat(cdp): fire hover events on Input.dispatchMouseEvent mouseMoved

Wire the CDP Input.dispatchMouseEvent "mouseMoved" type to a new
Frame.triggerMouseMove, which hit-tests the point via elementFromPoint
and dispatches mousemove, mouseover and mouseenter on the target,
mirroring the existing triggerMouseClick and the actions.zig hover()
event semantics.

Previously mouseMoved was silently ignored, so element.hover() over
Playwright/Puppeteer (CDP) fired no events. Addresses the hover gap
reported in #2043.
This commit is contained in:
Rohit
2026-06-04 14:07:34 +05:30
parent 4426b91588
commit b347f8b2e2
2 changed files with 87 additions and 2 deletions

View File

@@ -3929,6 +3929,44 @@ pub fn triggerMouseClick(self: *Frame, x: f64, y: f64) !void {
try self._event_manager.dispatch(target.asEventTarget(), mouse_event.asEvent());
}
pub fn triggerMouseMove(self: *Frame, x: f64, y: f64) !void {
const target = (try self.window._document.elementFromPoint(x, y, self)) orelse return;
if (comptime IS_DEBUG) {
log.debug(.frame, "frame mouse move", .{
.url = self.url,
.node = target,
.x = x,
.y = y,
.type = self._type,
});
}
const move_event: *MouseEvent = try .initTrusted(comptime .wrap("mousemove"), .{
.bubbles = true,
.cancelable = true,
.composed = true,
.clientX = x,
.clientY = y,
}, self);
try self._event_manager.dispatch(target.asEventTarget(), move_event.asEvent());
const over_event: *MouseEvent = try .initTrusted(comptime .wrap("mouseover"), .{
.bubbles = true,
.cancelable = true,
.composed = true,
.clientX = x,
.clientY = y,
}, self);
try self._event_manager.dispatch(target.asEventTarget(), over_event.asEvent());
const enter_event: *MouseEvent = try .initTrusted(comptime .wrap("mouseenter"), .{
.composed = true,
.clientX = x,
.clientY = y,
}, self);
try self._event_manager.dispatch(target.asEventTarget(), enter_event.asEvent());
}
// 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

@@ -96,13 +96,17 @@ fn dispatchMouseEvent(cmd: *CDP.Command) !void {
// quickly ignore types we know we don't handle
switch (params.type) {
.mouseMoved, .mouseWheel, .mouseReleased => return,
.mouseWheel, .mouseReleased => return,
else => {},
}
const bc = cmd.browser_context orelse return;
const frame = bc.session.currentFrame() orelse return;
try frame.triggerMouseClick(params.x, params.y);
switch (params.type) {
.mousePressed => try frame.triggerMouseClick(params.x, params.y),
.mouseMoved => try frame.triggerMouseMove(params.x, params.y),
.mouseWheel, .mouseReleased => unreachable,
}
// result already sent
}
@@ -119,3 +123,46 @@ fn insertText(cmd: *CDP.Command) !void {
try cmd.sendResult(null, .{});
}
const lp = @import("lightpanda");
const testing = @import("../testing.zig");
test "cdp.input: dispatchMouseEvent mouseMoved fires hover events" {
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();
// Register listeners for the full enter sequence on #hoverTarget, then read
// its (faux-layout) position so we can target it precisely.
_ = try ls.local.compileAndRun(
\\const t = document.getElementById('hoverTarget');
\\t.addEventListener('mousemove', () => { window.moved = true; });
\\t.addEventListener('mouseenter', () => { window.entered = true; });
, null);
const rect_x = try (try ls.local.compileAndRun("document.getElementById('hoverTarget').getBoundingClientRect().x", null)).toF64();
const rect_y = try (try ls.local.compileAndRun("document.getElementById('hoverTarget').getBoundingClientRect().y", null)).toF64();
try ctx.processMessage(.{
.id = 1,
.method = "Input.dispatchMouseEvent",
.params = .{ .type = "mouseMoved", .x = rect_x, .y = rect_y },
});
const result = try ls.local.compileAndRun("window.hovered === true && window.entered === true && window.moved === true", null);
try testing.expect(result.isTrue());
}