From 3e8ff142effbd8ab75d257ec892625345c4d6d77 Mon Sep 17 00:00:00 2001 From: Ramiro_quai Date: Sun, 30 Aug 2026 04:32:17 -0600 Subject: [PATCH] fix(actions): click dispatches full pointer/mouse sequence, not a bare click event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit actions.click() previously dispatched a single untrusted-shaped "click" MouseEvent directly on the target node, skipping pointerdown, mousedown, pointerup, and mouseup entirely. Many real-world widgets — custom autocomplete/combobox components in particular — open or otherwise react on mousedown, not click alone, so this made them unreachable through the click tool even though the element was correctly focused and targeted. WebDriver.zig's own click() already implements the correct sequence (pointerdown, mousedown, pointerup, mouseup, click) for testdriver's click, with a comment explicitly contrasting it against a lone untrusted click event. This change ports that same sequence into actions.click() so the MCP/CDP "click" action produces the same event sequence a real user click would. Adds a mousedown assertion to the existing MCP Actions test (mcp_actions.html + tools.zig) to catch a regression here; confirmed the new assertion fails against the old implementation and passes against this one. Full test suite (1339 tests) and zig fmt --check both pass. Reproduced against a real, previously-untested production site (a Wix-built autocomplete branch-selector widget) where click could focus the input but never open its option list; a minimal local reproduction (a mousedown-only widget) confirms the fix. --- src/browser/actions.zig | 48 +++++++++++++++++++++++++----- src/browser/tests/mcp_actions.html | 2 +- src/mcp/tools.zig | 2 +- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/browser/actions.zig b/src/browser/actions.zig index f4ef1b2fe..ab9572780 100644 --- a/src/browser/actions.zig +++ b/src/browser/actions.zig @@ -22,6 +22,7 @@ const DOMNode = @import("webapi/Node.zig"); const Element = @import("webapi/Element.zig"); const Event = @import("webapi/Event.zig"); const MouseEvent = @import("webapi/event/MouseEvent.zig"); +const PointerEvent = @import("webapi/event/PointerEvent.zig"); const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig"); const Frame = @import("Frame.zig"); const Session = @import("Session.zig"); @@ -38,23 +39,56 @@ fn dispatchInputAndChangeEvents(el: *Element, frame: *Frame) !void { }; } -pub fn click(node: *DOMNode, frame: *Frame) !void { - const el = node.is(Element) orelse return error.InvalidNodeType; - - const mouse_event: *MouseEvent = try .initTrusted(comptime .wrap("click"), .{ +fn dispatchPointer(el: *Element, comptime typ: []const u8, button: i32, buttons: u16, frame: *Frame) !void { + const event: *PointerEvent = try .initTrusted(typ, .{ .bubbles = true, .cancelable = true, .composed = true, + .button = button, + .buttons = buttons, + .pointerId = 1, + .pointerType = "mouse", + .isPrimary = true, + }, frame); + frame._event_manager.dispatch(el.asEventTarget(), event.asEvent()) catch |err| { + lp.log.err(.app, "click failed", .{ .err = err, .type = typ }); + return error.ActionFailed; + }; +} + +fn dispatchMouse(el: *Element, comptime typ: []const u8, button: i32, buttons: u16, frame: *Frame) !void { + const event: *MouseEvent = try .initTrusted(comptime .wrap(typ), .{ + .bubbles = true, + .cancelable = true, + .composed = true, + .button = button, + .buttons = buttons, + .detail = 1, .clientX = 0, .clientY = 0, }, frame); - - frame._event_manager.dispatch(el.asEventTarget(), mouse_event.asEvent()) catch |err| { - lp.log.err(.app, "click failed", .{ .err = err }); + frame._event_manager.dispatch(el.asEventTarget(), event.asEvent()) catch |err| { + lp.log.err(.app, "click failed", .{ .err = err, .type = typ }); return error.ActionFailed; }; } +// A full trusted primary-button click sequence on the element, as a real user +// click would produce: pointerdown, mousedown, pointerup, mouseup, click. +// Unlike a single bare "click" event, many widgets (autocomplete/combobox +// components in particular) key their open/interaction behavior off +// mousedown or pointerdown, not click alone -- see WebDriver.zig's `click`, +// which already implements this same sequence for testdriver's `click`. +pub fn click(node: *DOMNode, frame: *Frame) !void { + const el = node.is(Element) orelse return error.InvalidNodeType; + + try dispatchPointer(el, "pointerdown", 0, 1, frame); + try dispatchMouse(el, "mousedown", 0, 1, frame); + try dispatchPointer(el, "pointerup", 0, 0, frame); + try dispatchMouse(el, "mouseup", 0, 0, frame); + try dispatchMouse(el, "click", 0, 0, frame); +} + pub fn hover(node: *DOMNode, frame: *Frame) !void { const el = node.is(Element) orelse return error.InvalidNodeType; diff --git a/src/browser/tests/mcp_actions.html b/src/browser/tests/mcp_actions.html index f27c63ef4..9667f645a 100644 --- a/src/browser/tests/mcp_actions.html +++ b/src/browser/tests/mcp_actions.html @@ -1,7 +1,7 @@ - +