From 1776d0ea71e8dcb6d566250632e668f0f9b7635f Mon Sep 17 00:00:00 2001 From: Rohit <71192000+rohitsux@users.noreply.github.com> Date: Fri, 5 Jun 2026 20:08:46 +0530 Subject: [PATCH 1/3] 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. --- src/browser/Frame.zig | 68 ++++++++++++++++++++++----------- src/cdp/domains/input.zig | 79 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 123 insertions(+), 24 deletions(-) diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index eea0b8aa..42d127de 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -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 { diff --git a/src/cdp/domains/input.zig b/src/cdp/domains/input.zig index 17a91dc9..64a52b83 100644 --- a/src/cdp/domains/input.zig +++ b/src/cdp/domains/input.zig @@ -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()); +} From e68def97b3337442b45f2db4bce908f4a5a90868 Mon Sep 17 00:00:00 2001 From: Rohit <71192000+rohitsux@users.noreply.github.com> Date: Sat, 6 Jun 2026 19:03:45 +0530 Subject: [PATCH 2/3] refactor(cdp): use named constants for mouse button values Address review: replace the bare 0/1/2 button values in the mousedown/release switch (Frame.zig) and the CDP button mapping (input.zig) with named constants so the code self-documents. --- src/browser/Frame.zig | 19 ++++++++++++------- src/cdp/domains/input.zig | 21 ++++++++++++++------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index 42d127de..2258fad3 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -3902,6 +3902,14 @@ fn findFrameByName(frame: *Frame, name: []const u8) ?*Frame { return null; } +// DOM MouseEvent.button values. +// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button +const mouse_button = struct { + const main: i32 = 0; // left + const auxiliary: i32 = 1; // middle + const secondary: i32 = 2; // right +}; + // 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. @@ -3988,20 +3996,17 @@ pub fn triggerMouseRelease(self: *Frame, x: f64, y: f64, button: i32, click_coun 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 + // After mouseup, the activation event depends on the button. switch (button) { - 0 => { + mouse_button.main => { 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), + mouse_button.auxiliary => try self.dispatchMouseEventOn(target, "auxclick", x, y, button, detail), + mouse_button.secondary => try self.dispatchMouseEventOn(target, "contextmenu", x, y, button, detail), else => {}, } } diff --git a/src/cdp/domains/input.zig b/src/cdp/domains/input.zig index 64a52b83..4d0afa01 100644 --- a/src/cdp/domains/input.zig +++ b/src/cdp/domains/input.zig @@ -111,14 +111,21 @@ fn dispatchMouseEvent(cmd: *CDP.Command) !void { 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, ...). + // Map the CDP button name to the DOM MouseEvent.button value. + // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button + const dom_button = struct { + const main: i32 = 0; + const auxiliary: i32 = 1; + const secondary: i32 = 2; + const fourth: i32 = 3; + const fifth: i32 = 4; + }; const button: i32 = switch (params.button) { - .none, .left => 0, - .middle => 1, - .right => 2, - .back => 3, - .forward => 4, + .none, .left => dom_button.main, + .middle => dom_button.auxiliary, + .right => dom_button.secondary, + .back => dom_button.fourth, + .forward => dom_button.fifth, }; switch (params.type) { From fdf6276f39bc4097abf53bed19ab7f4fc578445f Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 8 Jun 2026 08:42:54 +0200 Subject: [PATCH 3/3] cdp: use Frame.mouse_button consts from cdp.input.zig --- src/browser/Frame.zig | 10 ++++++---- src/cdp/domains/input.zig | 9 ++------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index 2258fad3..612f4bb0 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -3904,10 +3904,12 @@ fn findFrameByName(frame: *Frame, name: []const u8) ?*Frame { // DOM MouseEvent.button values. // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button -const mouse_button = struct { - const main: i32 = 0; // left - const auxiliary: i32 = 1; // middle - const secondary: i32 = 2; // right +pub const mouse_button = struct { + pub const main: i32 = 0; // left + pub const auxiliary: i32 = 1; // middle + pub const secondary: i32 = 2; // right + pub const fourth: i32 = 3; // back + pub const fifth: i32 = 4; // forward }; // Dispatch a single trusted mouse event of the given type on `target`, carrying diff --git a/src/cdp/domains/input.zig b/src/cdp/domains/input.zig index 4d0afa01..a6772abd 100644 --- a/src/cdp/domains/input.zig +++ b/src/cdp/domains/input.zig @@ -19,6 +19,8 @@ const std = @import("std"); const CDP = @import("../CDP.zig"); +const dom_button = @import("../../browser/Frame.zig").mouse_button; + pub fn processMessage(cmd: *CDP.Command) !void { const action = std.meta.stringToEnum(enum { dispatchKeyEvent, @@ -113,13 +115,6 @@ fn dispatchMouseEvent(cmd: *CDP.Command) !void { // Map the CDP button name to the DOM MouseEvent.button value. // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button - const dom_button = struct { - const main: i32 = 0; - const auxiliary: i32 = 1; - const secondary: i32 = 2; - const fourth: i32 = 3; - const fifth: i32 = 4; - }; const button: i32 = switch (params.button) { .none, .left => dom_button.main, .middle => dom_button.auxiliary,