Fix event order and add tests

- Fix setChecked event order: click fires before input/change to match
  browser behavior
- Add tests for hover, press, selectOption, setChecked MCP tools
- Merge all action tests into a single test case sharing one page load
- Add test elements to mcp_actions.html (hover target, key input,
  second select, checkbox, radio)
This commit is contained in:
Adrià Arrufat
2026-04-02 10:46:24 +02:00
parent 5e79af42f4
commit 58143ee3d1
3 changed files with 94 additions and 13 deletions

View File

@@ -132,16 +132,7 @@ pub fn setChecked(node: *DOMNode, checked: bool, page: *Page) !void {
return error.ActionFailed; return error.ActionFailed;
}; };
const input_evt: *Event = try .initTrusted(comptime .wrap("input"), .{ .bubbles = true }, page); // Match browser event order: click fires first, then input and change.
page._event_manager.dispatch(el.asEventTarget(), input_evt) catch |err| {
lp.log.err(.app, "dispatch input event failed", .{ .err = err });
};
const change_evt: *Event = try .initTrusted(comptime .wrap("change"), .{ .bubbles = true }, page);
page._event_manager.dispatch(el.asEventTarget(), change_evt) catch |err| {
lp.log.err(.app, "dispatch change event failed", .{ .err = err });
};
const click_event: *MouseEvent = try .initTrusted(comptime .wrap("click"), .{ const click_event: *MouseEvent = try .initTrusted(comptime .wrap("click"), .{
.bubbles = true, .bubbles = true,
.cancelable = true, .cancelable = true,
@@ -151,6 +142,16 @@ pub fn setChecked(node: *DOMNode, checked: bool, page: *Page) !void {
page._event_manager.dispatch(el.asEventTarget(), click_event.asEvent()) catch |err| { page._event_manager.dispatch(el.asEventTarget(), click_event.asEvent()) catch |err| {
lp.log.err(.app, "dispatch click event failed", .{ .err = err }); lp.log.err(.app, "dispatch click event failed", .{ .err = err });
}; };
const input_evt: *Event = try .initTrusted(comptime .wrap("input"), .{ .bubbles = true }, page);
page._event_manager.dispatch(el.asEventTarget(), input_evt) catch |err| {
lp.log.err(.app, "dispatch input event failed", .{ .err = err });
};
const change_evt: *Event = try .initTrusted(comptime .wrap("change"), .{ .bubbles = true }, page);
page._event_manager.dispatch(el.asEventTarget(), change_evt) catch |err| {
lp.log.err(.app, "dispatch change event failed", .{ .err = err });
};
} }
pub fn fill(node: *DOMNode, text: []const u8, page: *Page) !void { pub fn fill(node: *DOMNode, text: []const u8, page: *Page) !void {

View File

@@ -10,5 +10,20 @@
<div id="scrollbox" style="width: 100px; height: 100px; overflow: scroll;" onscroll="window.scrolled = true;"> <div id="scrollbox" style="width: 100px; height: 100px; overflow: scroll;" onscroll="window.scrolled = true;">
<div style="height: 500px;">Long content</div> <div style="height: 500px;">Long content</div>
</div> </div>
<div id="hoverTarget" onmouseover="window.hovered = true;">Hover Me</div>
<input id="keyTarget" onkeydown="window.keyPressed = event.key;" onkeyup="window.keyReleased = event.key;">
<select id="sel2" onchange="window.sel2Changed = this.value">
<option value="a">Alpha</option>
<option value="b">Beta</option>
<option value="c">Gamma</option>
</select>
<input id="chk" type="checkbox">
<input id="rad" type="radio" name="group1">
<script>
document.getElementById('chk').addEventListener('click', function() { window.chkClicked = true; });
document.getElementById('chk').addEventListener('change', function() { window.chkChanged = true; });
document.getElementById('rad').addEventListener('click', function() { window.radClicked = true; });
document.getElementById('rad').addEventListener('change', function() { window.radChanged = true; });
</script>
</body> </body>
</html> </html>

View File

@@ -928,7 +928,7 @@ test "MCP - evaluate error reporting" {
} }, out.written()); } }, out.written());
} }
test "MCP - Actions: click, fill, scroll" { test "MCP - Actions: click, fill, scroll, hover, press, selectOption, setChecked" {
defer testing.reset(); defer testing.reset();
const aa = testing.arena_allocator; const aa = testing.arena_allocator;
@@ -989,7 +989,67 @@ test "MCP - Actions: click, fill, scroll" {
out.clearRetainingCapacity(); out.clearRetainingCapacity();
} }
// Evaluate assertions {
// Test Hover
const el = page.document.getElementById("hoverTarget", page).?.asNode();
const el_id = (try server.node_registry.register(el)).id;
var id_buf: [12]u8 = undefined;
const id_str = std.fmt.bufPrint(&id_buf, "{d}", .{el_id}) catch unreachable;
const msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":5,\"method\":\"tools/call\",\"params\":{\"name\":\"hover\",\"arguments\":{\"backendNodeId\":", id_str, "}}}" });
try router.handleMessage(server, aa, msg);
try testing.expect(std.mem.indexOf(u8, out.written(), "Hovered element") != null);
out.clearRetainingCapacity();
}
{
// Test Press
const el = page.document.getElementById("keyTarget", page).?.asNode();
const el_id = (try server.node_registry.register(el)).id;
var id_buf: [12]u8 = undefined;
const id_str = std.fmt.bufPrint(&id_buf, "{d}", .{el_id}) catch unreachable;
const msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":6,\"method\":\"tools/call\",\"params\":{\"name\":\"press\",\"arguments\":{\"key\":\"Enter\",\"backendNodeId\":", id_str, "}}}" });
try router.handleMessage(server, aa, msg);
try testing.expect(std.mem.indexOf(u8, out.written(), "Pressed key") != null);
out.clearRetainingCapacity();
}
{
// Test SelectOption
const el = page.document.getElementById("sel2", page).?.asNode();
const el_id = (try server.node_registry.register(el)).id;
var id_buf: [12]u8 = undefined;
const id_str = std.fmt.bufPrint(&id_buf, "{d}", .{el_id}) catch unreachable;
const msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":7,\"method\":\"tools/call\",\"params\":{\"name\":\"selectOption\",\"arguments\":{\"backendNodeId\":", id_str, ",\"value\":\"b\"}}}" });
try router.handleMessage(server, aa, msg);
try testing.expect(std.mem.indexOf(u8, out.written(), "Selected option") != null);
out.clearRetainingCapacity();
}
{
// Test SetChecked (checkbox)
const el = page.document.getElementById("chk", page).?.asNode();
const el_id = (try server.node_registry.register(el)).id;
var id_buf: [12]u8 = undefined;
const id_str = std.fmt.bufPrint(&id_buf, "{d}", .{el_id}) catch unreachable;
const msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":8,\"method\":\"tools/call\",\"params\":{\"name\":\"setChecked\",\"arguments\":{\"backendNodeId\":", id_str, ",\"checked\":true}}}" });
try router.handleMessage(server, aa, msg);
try testing.expect(std.mem.indexOf(u8, out.written(), "checked") != null);
out.clearRetainingCapacity();
}
{
// Test SetChecked (radio)
const el = page.document.getElementById("rad", page).?.asNode();
const el_id = (try server.node_registry.register(el)).id;
var id_buf: [12]u8 = undefined;
const id_str = std.fmt.bufPrint(&id_buf, "{d}", .{el_id}) catch unreachable;
const msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":9,\"method\":\"tools/call\",\"params\":{\"name\":\"setChecked\",\"arguments\":{\"backendNodeId\":", id_str, ",\"checked\":true}}}" });
try router.handleMessage(server, aa, msg);
try testing.expect(std.mem.indexOf(u8, out.written(), "checked") != null);
out.clearRetainingCapacity();
}
// Evaluate JS assertions for all actions
var ls: js.Local.Scope = undefined; var ls: js.Local.Scope = undefined;
page.js.localScope(&ls); page.js.localScope(&ls);
defer ls.deinit(); defer ls.deinit();
@@ -1001,7 +1061,12 @@ test "MCP - Actions: click, fill, scroll" {
const result = try ls.local.exec( const result = try ls.local.exec(
\\ window.clicked === true && window.inputVal === 'hello' && \\ window.clicked === true && window.inputVal === 'hello' &&
\\ window.changed === true && window.selChanged === 'opt2' && \\ window.changed === true && window.selChanged === 'opt2' &&
\\ window.scrolled === true \\ window.scrolled === true &&
\\ window.hovered === true &&
\\ window.keyPressed === 'Enter' && window.keyReleased === 'Enter' &&
\\ window.sel2Changed === 'b' &&
\\ window.chkClicked === true && window.chkChanged === true &&
\\ window.radClicked === true && window.radChanged === true
, null); , null);
try testing.expect(result.isTrue()); try testing.expect(result.isTrue());