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()); +}