diff --git a/src/browser/actions.zig b/src/browser/actions.zig index d55721b9..9341572e 100644 --- a/src/browser/actions.zig +++ b/src/browser/actions.zig @@ -132,16 +132,7 @@ pub fn setChecked(node: *DOMNode, checked: bool, page: *Page) !void { return error.ActionFailed; }; - 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 }); - }; - + // Match browser event order: click fires first, then input and change. const click_event: *MouseEvent = try .initTrusted(comptime .wrap("click"), .{ .bubbles = 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| { 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 { diff --git a/src/browser/tests/mcp_actions.html b/src/browser/tests/mcp_actions.html index 88cb70b1..f27c63ef 100644 --- a/src/browser/tests/mcp_actions.html +++ b/src/browser/tests/mcp_actions.html @@ -10,5 +10,20 @@
Long content
+
Hover Me
+ + + + + diff --git a/src/mcp/tools.zig b/src/mcp/tools.zig index ec8ed889..ebb7baf5 100644 --- a/src/mcp/tools.zig +++ b/src/mcp/tools.zig @@ -928,7 +928,7 @@ test "MCP - evaluate error reporting" { } }, out.written()); } -test "MCP - Actions: click, fill, scroll" { +test "MCP - Actions: click, fill, scroll, hover, press, selectOption, setChecked" { defer testing.reset(); const aa = testing.arena_allocator; @@ -989,7 +989,67 @@ test "MCP - Actions: click, fill, scroll" { 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; page.js.localScope(&ls); defer ls.deinit(); @@ -1001,7 +1061,12 @@ test "MCP - Actions: click, fill, scroll" { const result = try ls.local.exec( \\ window.clicked === true && window.inputVal === 'hello' && \\ 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); try testing.expect(result.isTrue());