Merge pull request #3254 from lightpanda-io/wpt-webdriver-click-semantics

webdriver: better mouse events
This commit is contained in:
Karl Seguin authored and GitHub committed 2026-08-25 07:09:55 +08:00
commit b72042365a
2 files changed
+66 -12

No files matched your search

+64 -9
View File
@@ -70,10 +70,10 @@ pub fn click(_: *const WebDriver, element: *Element, frame: *Frame) !void {
}
dispatchPointer(element, "pointerdown", 0, 1, frame);
dispatchMouse(element, "mousedown", 0, 1, frame);
dispatchMouse(element, "mousedown", 0, 1, 1, frame);
dispatchPointer(element, "pointerup", 0, 0, frame);
dispatchMouse(element, "mouseup", 0, 0, frame);
dispatchMouse(element, "click", 0, 0, frame);
dispatchMouse(element, "mouseup", 0, 0, 1, frame);
dispatchMouse(element, "click", 0, 0, 1, frame);
}
const WebDriverCookie = struct {
@@ -234,6 +234,12 @@ fn performPointerSource(source: js.Object, frame: *Frame) !void {
// whose origin resolved to an element.
var target: ?*Element = null;
var pressed = false;
// Where the last pointerDown landed: the click fires at the nearest common
// inclusive ancestor of the down and up targets when they differ.
var down_target: ?*Element = null;
var click_count: u32 = 0;
var last_click_button: i32 = 0;
var last_click_target: ?*Element = null;
for (0..actions.len()) |i| {
const action_val = try actions.get(@intCast(i));
@@ -255,17 +261,23 @@ fn performPointerSource(source: js.Object, frame: *Frame) !void {
dispatchTouch(el, "touchmove", frame);
}
} else {
dispatchMouse(el, "mousemove", 0, 0, frame);
dispatchMouse(el, "mousemove", 0, 0, 0, frame);
}
} else if (action_type.eql(comptime .wrap("pointerDown"))) {
const el = target orelse continue;
const button = readI32(action, "button", 0);
pressed = true;
dispatchPointer(el, "pointerdown", button, 1, frame);
down_target = el;
if (last_click_target == el and last_click_button == button) {
click_count += 1;
} else {
click_count = 1;
}
dispatchPointer(el, "pointerdown", button, buttonsMask(button), frame);
if (is_touch) {
dispatchTouch(el, "touchstart", frame);
} else {
dispatchMouse(el, "mousedown", button, 1, frame);
dispatchMouse(el, "mousedown", button, buttonsMask(button), click_count, frame);
Frame.user_input.focusEditingHostForMouseDown(frame, el) catch |err| {
log.warn(.app, "webdriver editable focus", .{ .err = err });
};
@@ -278,15 +290,57 @@ fn performPointerSource(source: js.Object, frame: *Frame) !void {
if (is_touch) {
dispatchTouch(el, "touchend", frame);
} else {
dispatchMouse(el, "mouseup", button, 0, frame);
dispatchMouse(el, "click", button, 0, frame);
dispatchMouse(el, "mouseup", button, 0, click_count, frame);
const click_target = commonClickTarget(down_target orelse el, el);
last_click_button = button;
last_click_target = click_target;
if (button == 0) {
dispatchMouse(click_target, "click", button, 0, click_count, frame);
if (click_count % 2 == 0) {
dispatchMouse(click_target, "dblclick", button, 0, click_count, frame);
}
} else {
if (button == 2) {
dispatchMouse(click_target, "contextmenu", button, 0, click_count, frame);
}
dispatchMouse(click_target, "auxclick", button, 0, click_count, frame);
}
}
down_target = null;
}
// "pause" carries timing only and is ignored. ("pointerCancel" is not
// emitted by the testdriver Actions builder.)
}
}
// The `buttons` bitmask bit for a WebDriver button number: the flag order does
// not follow the button numbering (left=1, right=2, middle=4).
fn buttonsMask(button: i32) u16 {
return switch (button) {
0 => 1,
1 => 4,
2 => 2,
3 => 8,
4 => 16,
else => 0,
};
}
// A click whose mousedown and mouseup landed on different elements fires at
// their nearest common inclusive ancestor element.
fn commonClickTarget(down: *Element, up: *Element) *Element {
if (down == up) {
return up;
}
var current: ?*@import("Node.zig") = down.asNode();
while (current) |node| : (current = node.parentNode()) {
if (node.contains(up.asNode())) {
return node.is(Element) orelse break;
}
}
return up;
}
fn performWheelSource(source: js.Object, frame: *Frame) !void {
const actions_val = try source.get("actions");
if (!actions_val.isArray()) {
@@ -392,13 +446,14 @@ fn dispatchPointer(el: *Element, comptime typ: []const u8, button: i32, buttons:
dispatch(el.asEventTarget(), event.asEvent(), frame, typ);
}
fn dispatchMouse(el: *Element, comptime typ: []const u8, button: i32, buttons: u16, frame: *Frame) void {
fn dispatchMouse(el: *Element, comptime typ: []const u8, button: i32, buttons: u16, detail: u32, frame: *Frame) void {
const event = MouseEvent.initTrusted(comptime .wrap(typ), .{
.bubbles = true,
.cancelable = true,
.composed = true,
.button = button,
.buttons = buttons,
.detail = detail,
}, frame) catch |err| {
log.warn(.app, "webdriver mouse event", .{ .err = err, .type = typ });
return;
+2 -3
View File
@@ -409,12 +409,11 @@ pub fn click(self: *HtmlElement, frame: *Frame) !void {
flags.click_in_progress = true;
defer flags.click_in_progress = false;
const event = (try @import("../event/MouseEvent.zig").init("click", .{
const event = (try @import("../event/PointerEvent.zig").init("click", .{
.bubbles = true,
.cancelable = true,
.composed = true,
.clientX = 0,
.clientY = 0,
.pointerId = -1,
}, frame)).asEvent();
// Keep the event alive past dispatch (which runs handlers/microtasks) so we