mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-09-23 04:55:23 -04:00
fix(actions): click dispatches full pointer/mouse sequence, not a bare click event
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.
This commit is contained in:
1 parent
5d5459fc8b
commit
3e8ff142ef
3 files changed
+43
-9
No files matched your search
+41
-7
@@ -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;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<button id="btn" onclick="window.clicked = true;">Click Me</button>
|
||||
<button id="btn" onmousedown="window.mousedowned = true;" onclick="window.clicked = true;">Click Me</button>
|
||||
<input id="inp" oninput="window.inputVal = this.value" onchange="window.changed = true;">
|
||||
<select id="sel" onchange="window.selChanged = this.value">
|
||||
<option value="opt1">Option 1</option>
|
||||
|
||||
+1
-1
@@ -1161,7 +1161,7 @@ test "MCP - Actions: click, fill, scroll, hover, press, selectOption, setChecked
|
||||
defer try_catch.deinit();
|
||||
|
||||
const result = try ls.local.exec(
|
||||
\\ window.clicked === true && window.inputVal === 'hello' &&
|
||||
\\ window.mousedowned === true && window.clicked === true && window.inputVal === 'hello' &&
|
||||
\\ window.changed === true && window.selChanged === 'opt2' &&
|
||||
\\ window.scrolled === true &&
|
||||
\\ window.hovered === true &&
|
||||
|
||||
Reference in new issue
Block a user