feat(cdp): support mouse button and clickCount in Input.dispatchMouseEvent

Input.dispatchMouseEvent ignored the button and clickCount params, and
mousePressed only fired a click event (never mousedown). Add them:

- mousePressed now fires mousedown carrying the pressed button.
- mouseReleased fires mouseup, then the button-appropriate activation
  event: click for the main button, auxclick for the auxiliary button,
  and contextmenu for the secondary (right) button.
- a clickCount of 2 additionally fires dblclick.

This unblocks right-click, middle-click and double-click interactions
for Playwright/Puppeteer scripts. Follows the mouse event work in
#2636, #2640 and #2641.
This commit is contained in:
Rohit
2026-06-05 20:08:46 +05:30
parent 37f5a8c819
commit 1776d0ea71
2 changed files with 123 additions and 24 deletions

View File

@@ -3902,25 +3902,35 @@ fn findFrameByName(frame: *Frame, name: []const u8) ?*Frame {
return null;
}
pub fn triggerMouseClick(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 click", .{
.url = self.url,
.node = target,
.x = x,
.y = y,
.type = self._type,
});
}
const mouse_event: *MouseEvent = try .initTrusted(comptime .wrap("click"), .{
// Dispatch a single trusted mouse event of the given type on `target`, carrying
// the pressed button and pointer position. `detail` is the click count (used for
// click/dblclick); 0 for events where it does not apply.
fn dispatchMouseEventOn(self: *Frame, target: *Element, comptime typ: []const u8, x: f64, y: f64, button: i32, detail: u32) !void {
const event: *MouseEvent = try .initTrusted(comptime .wrap(typ), .{
.bubbles = true,
.cancelable = true,
.composed = true,
.clientX = x,
.clientY = y,
.button = button,
.detail = detail,
}, self);
try self._event_manager.dispatch(target.asEventTarget(), mouse_event.asEvent());
try self._event_manager.dispatch(target.asEventTarget(), event.asEvent());
}
pub fn triggerMousePress(self: *Frame, x: f64, y: f64, button: i32) !void {
const target = (try self.window._document.elementFromPoint(x, y, self)) orelse return;
if (comptime IS_DEBUG) {
log.debug(.frame, "frame mouse press", .{
.url = self.url,
.node = target,
.x = x,
.y = y,
.button = button,
.type = self._type,
});
}
try self.dispatchMouseEventOn(target, "mousedown", x, y, button, 0);
}
pub fn triggerMouseMove(self: *Frame, x: f64, y: f64) !void {
@@ -3961,7 +3971,7 @@ pub fn triggerMouseMove(self: *Frame, x: f64, y: f64) !void {
try self._event_manager.dispatch(target.asEventTarget(), enter_event.asEvent());
}
pub fn triggerMouseRelease(self: *Frame, x: f64, y: f64) !void {
pub fn triggerMouseRelease(self: *Frame, x: f64, y: f64, button: i32, click_count: i32) !void {
const target = (try self.window._document.elementFromPoint(x, y, self)) orelse return;
if (comptime IS_DEBUG) {
log.debug(.frame, "frame mouse release", .{
@@ -3969,17 +3979,31 @@ pub fn triggerMouseRelease(self: *Frame, x: f64, y: f64) !void {
.node = target,
.x = x,
.y = y,
.button = button,
.type = self._type,
});
}
const up_event: *MouseEvent = try .initTrusted(comptime .wrap("mouseup"), .{
.bubbles = true,
.cancelable = true,
.composed = true,
.clientX = x,
.clientY = y,
}, self);
try self._event_manager.dispatch(target.asEventTarget(), up_event.asEvent());
const detail: u32 = if (click_count > 0) @intCast(click_count) else 1;
try self.dispatchMouseEventOn(target, "mouseup", x, y, button, detail);
// After mouseup, the activation event depends on the button:
// main (0) -> click
// auxiliary (1) -> auxclick
// secondary (2) -> contextmenu
switch (button) {
0 => {
try self.dispatchMouseEventOn(target, "click", x, y, button, detail);
// A second click in quick succession also fires dblclick.
if (click_count == 2) {
try self.dispatchMouseEventOn(target, "dblclick", x, y, button, detail);
}
},
1 => try self.dispatchMouseEventOn(target, "auxclick", x, y, button, detail),
2 => try self.dispatchMouseEventOn(target, "contextmenu", x, y, button, detail),
else => {},
}
}
pub fn triggerMouseWheel(self: *Frame, x: f64, y: f64, delta_x: f64, delta_y: f64) !void {

View File

@@ -82,6 +82,8 @@ fn dispatchMouseEvent(cmd: *CDP.Command) !void {
x: f64,
y: f64,
type: Type,
button: Button = .none,
clickCount: i32 = 0,
deltaX: f64 = 0,
deltaY: f64 = 0,
// Many optional parameters are not implemented yet, see documentation url.
@@ -92,15 +94,36 @@ fn dispatchMouseEvent(cmd: *CDP.Command) !void {
mouseMoved,
mouseWheel,
};
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#type-MouseButton
const Button = enum {
none,
left,
middle,
right,
back,
forward,
};
})) orelse return error.InvalidParams;
try cmd.sendResult(null, .{});
const bc = cmd.browser_context orelse return;
const frame = bc.session.currentFrame() orelse return;
// Map the CDP button name to the DOM MouseEvent.button numbering
// (left/main = 0, middle/auxiliary = 1, right/secondary = 2, ...).
const button: i32 = switch (params.button) {
.none, .left => 0,
.middle => 1,
.right => 2,
.back => 3,
.forward => 4,
};
switch (params.type) {
.mousePressed => try frame.triggerMouseClick(params.x, params.y),
.mouseReleased => try frame.triggerMouseRelease(params.x, params.y),
.mousePressed => try frame.triggerMousePress(params.x, params.y, button),
.mouseReleased => try frame.triggerMouseRelease(params.x, params.y, button, params.clickCount),
.mouseMoved => try frame.triggerMouseMove(params.x, params.y),
.mouseWheel => try frame.triggerMouseWheel(params.x, params.y, params.deltaX, params.deltaY),
}
@@ -237,3 +260,55 @@ test "cdp.input: dispatchMouseEvent mouseWheel fires wheel event" {
const result = try ls.local.compileAndRun("window.wheelDeltaY === 40", null);
try testing.expect(result.isTrue());
}
test "cdp.input: dispatchMouseEvent right button fires contextmenu, double-click fires dblclick" {
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(
\\const t = document.getElementById('hoverTarget');
\\t.addEventListener('mousedown', (e) => { window.downButton = e.button; });
\\t.addEventListener('contextmenu', (e) => { window.ctxButton = e.button; });
\\t.addEventListener('dblclick', () => { window.dbl = 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();
// Right button: press carries button=2, release fires contextmenu (not click).
try ctx.processMessage(.{
.id = 1,
.method = "Input.dispatchMouseEvent",
.params = .{ .type = "mousePressed", .x = rect_x, .y = rect_y, .button = "right" },
});
try ctx.processMessage(.{
.id = 2,
.method = "Input.dispatchMouseEvent",
.params = .{ .type = "mouseReleased", .x = rect_x, .y = rect_y, .button = "right" },
});
// Left button with clickCount 2 fires dblclick.
try ctx.processMessage(.{
.id = 3,
.method = "Input.dispatchMouseEvent",
.params = .{ .type = "mouseReleased", .x = rect_x, .y = rect_y, .button = "left", .clickCount = 2 },
});
const result = try ls.local.compileAndRun("window.downButton === 2 && window.ctxButton === 2 && window.dbl === true", null);
try testing.expect(result.isTrue());
}