Improve MCP tools test

Add helper to navigate to page, to reduce the boilerplate in each test.

Reduce waitForSelector time from 200ms to 20ms to speed up tests.
This commit is contained in:
Karl Seguin
2026-03-23 19:15:50 +08:00
parent 23334edc05
commit 0fcdc1d194
2 changed files with 101 additions and 148 deletions

View File

@@ -8,7 +8,7 @@
el.id = "delayed"; el.id = "delayed";
el.textContent = "Appeared after delay"; el.textContent = "Appeared after delay";
document.body.appendChild(el); document.body.appendChild(el);
}, 200); }, 20);
</script> </script>
</body> </body>
</html> </html>

View File

@@ -629,22 +629,14 @@ fn performGoto(server: *Server, url: [:0]const u8, id: std.json.Value) !void {
try runner.wait(.{ .ms = 2000 }); try runner.wait(.{ .ms = 2000 });
} }
const testing = @import("../testing.zig");
const router = @import("router.zig"); const router = @import("router.zig");
const testing = @import("../testing.zig");
test "MCP - evaluate error reporting" { test "MCP - evaluate error reporting" {
defer testing.reset(); defer testing.reset();
const allocator = testing.allocator; var out: std.io.Writer.Allocating = .init(testing.arena_allocator);
const app = testing.test_app; const server = try testLoadPage("about:blank", &out.writer);
var out_alloc: std.io.Writer.Allocating = .init(testing.arena_allocator);
defer out_alloc.deinit();
var server = try Server.init(allocator, app, &out_alloc.writer);
defer server.deinit(); defer server.deinit();
_ = try server.session.createPage();
const aa = testing.arena_allocator;
// Call evaluate with a script that throws an error // Call evaluate with a script that throws an error
const msg = const msg =
@@ -661,81 +653,74 @@ test "MCP - evaluate error reporting" {
\\} \\}
; ;
try router.handleMessage(server, aa, msg); try router.handleMessage(server, testing.arena_allocator, msg);
try testing.expectJson( try testing.expectJson(.{ .id = 1, .result = .{
\\{ .isError = true,
\\ "id": 1, .content = &.{.{ .type = "text" }},
\\ "result": { } }, out.written());
\\ "isError": true,
\\ "content": [
\\ { "type": "text" }
\\ ]
\\ }
\\}
, out_alloc.writer.buffered());
} }
test "MCP - Actions: click, fill, scroll" { test "MCP - Actions: click, fill, scroll" {
defer testing.reset(); defer testing.reset();
const allocator = testing.allocator; const aa = testing.arena_allocator;
const app = testing.test_app;
var out_alloc: std.io.Writer.Allocating = .init(testing.arena_allocator); var out: std.io.Writer.Allocating = .init(aa);
defer out_alloc.deinit(); const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_actions.html", &out.writer);
var server = try Server.init(allocator, app, &out_alloc.writer);
defer server.deinit(); defer server.deinit();
const aa = testing.arena_allocator; const page = &server.session.page.?;
const page = try server.session.createPage();
const url = "http://localhost:9582/src/browser/tests/mcp_actions.html";
try page.navigate(url, .{ .reason = .address_bar, .kind = .{ .push = null } });
var runner = try server.session.runner(.{});
try runner.wait(.{ .ms = 2000 });
// Test Click {
const btn = page.document.getElementById("btn", page).?.asNode(); // Test Click
const btn_id = (try server.node_registry.register(btn)).id; const btn = page.document.getElementById("btn", page).?.asNode();
var btn_id_buf: [12]u8 = undefined; const btn_id = (try server.node_registry.register(btn)).id;
const btn_id_str = std.fmt.bufPrint(&btn_id_buf, "{d}", .{btn_id}) catch unreachable; var btn_id_buf: [12]u8 = undefined;
const click_msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"click\",\"arguments\":{\"backendNodeId\":", btn_id_str, "}}}" }); const btn_id_str = std.fmt.bufPrint(&btn_id_buf, "{d}", .{btn_id}) catch unreachable;
try router.handleMessage(server, aa, click_msg); const click_msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"click\",\"arguments\":{\"backendNodeId\":", btn_id_str, "}}}" });
try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "Clicked element") != null); try router.handleMessage(server, aa, click_msg);
try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "Page url: http://localhost:9582/src/browser/tests/mcp_actions.html") != null); try testing.expect(std.mem.indexOf(u8, out.written(), "Clicked element") != null);
out_alloc.clearRetainingCapacity(); try testing.expect(std.mem.indexOf(u8, out.written(), "Page url: http://localhost:9582/src/browser/tests/mcp_actions.html") != null);
out.clearRetainingCapacity();
}
// Test Fill Input {
const inp = page.document.getElementById("inp", page).?.asNode(); // Test Fill Input
const inp_id = (try server.node_registry.register(inp)).id; const inp = page.document.getElementById("inp", page).?.asNode();
var inp_id_buf: [12]u8 = undefined; const inp_id = (try server.node_registry.register(inp)).id;
const inp_id_str = std.fmt.bufPrint(&inp_id_buf, "{d}", .{inp_id}) catch unreachable; var inp_id_buf: [12]u8 = undefined;
const fill_msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"fill\",\"arguments\":{\"backendNodeId\":", inp_id_str, ",\"text\":\"hello\"}}}" }); const inp_id_str = std.fmt.bufPrint(&inp_id_buf, "{d}", .{inp_id}) catch unreachable;
try router.handleMessage(server, aa, fill_msg); const fill_msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"fill\",\"arguments\":{\"backendNodeId\":", inp_id_str, ",\"text\":\"hello\"}}}" });
try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "Filled element") != null); try router.handleMessage(server, aa, fill_msg);
try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "with \\\"hello\\\"") != null); try testing.expect(std.mem.indexOf(u8, out.written(), "Filled element") != null);
out_alloc.clearRetainingCapacity(); try testing.expect(std.mem.indexOf(u8, out.written(), "with \\\"hello\\\"") != null);
out.clearRetainingCapacity();
}
// Test Fill Select {
const sel = page.document.getElementById("sel", page).?.asNode(); // Test Fill Select
const sel_id = (try server.node_registry.register(sel)).id; const sel = page.document.getElementById("sel", page).?.asNode();
var sel_id_buf: [12]u8 = undefined; const sel_id = (try server.node_registry.register(sel)).id;
const sel_id_str = std.fmt.bufPrint(&sel_id_buf, "{d}", .{sel_id}) catch unreachable; var sel_id_buf: [12]u8 = undefined;
const fill_sel_msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"fill\",\"arguments\":{\"backendNodeId\":", sel_id_str, ",\"text\":\"opt2\"}}}" }); const sel_id_str = std.fmt.bufPrint(&sel_id_buf, "{d}", .{sel_id}) catch unreachable;
try router.handleMessage(server, aa, fill_sel_msg); const fill_sel_msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"fill\",\"arguments\":{\"backendNodeId\":", sel_id_str, ",\"text\":\"opt2\"}}}" });
try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "Filled element") != null); try router.handleMessage(server, aa, fill_sel_msg);
try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "with \\\"opt2\\\"") != null); try testing.expect(std.mem.indexOf(u8, out.written(), "Filled element") != null);
out_alloc.clearRetainingCapacity(); try testing.expect(std.mem.indexOf(u8, out.written(), "with \\\"opt2\\\"") != null);
out.clearRetainingCapacity();
}
// Test Scroll {
const scrollbox = page.document.getElementById("scrollbox", page).?.asNode(); // Test Scroll
const scrollbox_id = (try server.node_registry.register(scrollbox)).id; const scrollbox = page.document.getElementById("scrollbox", page).?.asNode();
var scroll_id_buf: [12]u8 = undefined; const scrollbox_id = (try server.node_registry.register(scrollbox)).id;
const scroll_id_str = std.fmt.bufPrint(&scroll_id_buf, "{d}", .{scrollbox_id}) catch unreachable; var scroll_id_buf: [12]u8 = undefined;
const scroll_msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"tools/call\",\"params\":{\"name\":\"scroll\",\"arguments\":{\"backendNodeId\":", scroll_id_str, ",\"y\":50}}}" }); const scroll_id_str = std.fmt.bufPrint(&scroll_id_buf, "{d}", .{scrollbox_id}) catch unreachable;
try router.handleMessage(server, aa, scroll_msg); const scroll_msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"tools/call\",\"params\":{\"name\":\"scroll\",\"arguments\":{\"backendNodeId\":", scroll_id_str, ",\"y\":50}}}" });
try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "Scrolled to x: 0, y: 50") != null); try router.handleMessage(server, aa, scroll_msg);
out_alloc.clearRetainingCapacity(); try testing.expect(std.mem.indexOf(u8, out.written(), "Scrolled to x: 0, y: 50") != null);
out.clearRetainingCapacity();
}
// Evaluate assertions // Evaluate assertions
var ls: js.Local.Scope = undefined; var ls: js.Local.Scope = undefined;
@@ -746,111 +731,79 @@ test "MCP - Actions: click, fill, scroll" {
try_catch.init(&ls.local); try_catch.init(&ls.local);
defer try_catch.deinit(); defer try_catch.deinit();
const result = try ls.local.compileAndRun("window.clicked === true && window.inputVal === 'hello' && window.changed === true && window.selChanged === 'opt2' && window.scrolled === true", null); const result = try ls.local.exec(
\\ window.clicked === true && window.inputVal === 'hello' &&
\\ window.changed === true && window.selChanged === 'opt2' &&
\\ window.scrolled === true
, null);
try testing.expect(result.isTrue()); try testing.expect(result.isTrue());
} }
test "MCP - waitForSelector: existing element" { test "MCP - waitForSelector: existing element" {
defer testing.reset(); defer testing.reset();
const allocator = testing.allocator; var out: std.io.Writer.Allocating = .init(testing.arena_allocator);
const app = testing.test_app; const server = try testLoadPage(
"http://localhost:9582/src/browser/tests/mcp_wait_for_selector.html",
var out_alloc: std.io.Writer.Allocating = .init(testing.arena_allocator); &out.writer,
defer out_alloc.deinit(); );
var server = try Server.init(allocator, app, &out_alloc.writer);
defer server.deinit(); defer server.deinit();
const aa = testing.arena_allocator;
const page = try server.session.createPage();
const url = "http://localhost:9582/src/browser/tests/mcp_wait_for_selector.html";
try page.navigate(url, .{ .reason = .address_bar, .kind = .{ .push = null } });
var runner = try server.session.runner(.{});
try runner.wait(.{ .ms = 2000 });
// waitForSelector on an element that already exists returns immediately // waitForSelector on an element that already exists returns immediately
const msg = const msg =
\\{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"waitForSelector","arguments":{"selector":"#existing","timeout":2000}}} \\{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"waitForSelector","arguments":{"selector":"#existing","timeout":2000}}}
; ;
try router.handleMessage(server, aa, msg); try router.handleMessage(server, testing.arena_allocator, msg);
try testing.expectJson( try testing.expectJson(.{ .id = 1, .result = .{ .content = &.{.{ .type = "text" }} } }, out.written());
\\{
\\ "id": 1,
\\ "result": {
\\ "content": [
\\ { "type": "text" }
\\ ]
\\ }
\\}
, out_alloc.writer.buffered());
} }
test "MCP - waitForSelector: delayed element" { test "MCP - waitForSelector: delayed element" {
defer testing.reset(); defer testing.reset();
const allocator = testing.allocator; var out: std.io.Writer.Allocating = .init(testing.arena_allocator);
const app = testing.test_app; const server = try testLoadPage(
"http://localhost:9582/src/browser/tests/mcp_wait_for_selector.html",
var out_alloc: std.io.Writer.Allocating = .init(testing.arena_allocator); &out.writer,
defer out_alloc.deinit(); );
var server = try Server.init(allocator, app, &out_alloc.writer);
defer server.deinit(); defer server.deinit();
const aa = testing.arena_allocator;
const page = try server.session.createPage();
const url = "http://localhost:9582/src/browser/tests/mcp_wait_for_selector.html";
try page.navigate(url, .{ .reason = .address_bar, .kind = .{ .push = null } });
var runner = try server.session.runner(.{});
try runner.wait(.{ .ms = 2000 });
// waitForSelector on an element added after 200ms via setTimeout // waitForSelector on an element added after 200ms via setTimeout
const msg = const msg =
\\{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"waitForSelector","arguments":{"selector":"#delayed","timeout":5000}}} \\{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"waitForSelector","arguments":{"selector":"#delayed","timeout":5000}}}
; ;
try router.handleMessage(server, aa, msg); try router.handleMessage(server, testing.arena_allocator, msg);
try testing.expectJson( try testing.expectJson(.{ .id = 1, .result = .{ .content = &.{.{ .type = "text" }} } }, out.written());
\\{
\\ "id": 1,
\\ "result": {
\\ "content": [
\\ { "type": "text" }
\\ ]
\\ }
\\}
, out_alloc.writer.buffered());
} }
test "MCP - waitForSelector: timeout" { test "MCP - waitForSelector: timeout" {
defer testing.reset(); defer testing.reset();
const allocator = testing.allocator; var out: std.io.Writer.Allocating = .init(testing.arena_allocator);
const app = testing.test_app; const server = try testLoadPage(
"http://localhost:9582/src/browser/tests/mcp_wait_for_selector.html",
var out_alloc: std.io.Writer.Allocating = .init(testing.arena_allocator); &out.writer,
defer out_alloc.deinit(); );
var server = try Server.init(allocator, app, &out_alloc.writer);
defer server.deinit(); defer server.deinit();
const aa = testing.arena_allocator;
const page = try server.session.createPage();
const url = "http://localhost:9582/src/browser/tests/mcp_wait_for_selector.html";
try page.navigate(url, .{ .reason = .address_bar, .kind = .{ .push = null } });
var runner = try server.session.runner(.{});
try runner.wait(.{ .ms = 2000 });
// waitForSelector with a short timeout on a non-existent element should error // waitForSelector with a short timeout on a non-existent element should error
const msg = const msg =
\\{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"waitForSelector","arguments":{"selector":"#nonexistent","timeout":100}}} \\{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"waitForSelector","arguments":{"selector":"#nonexistent","timeout":100}}}
; ;
try router.handleMessage(server, aa, msg); try router.handleMessage(server, testing.arena_allocator, msg);
try testing.expectJson(.{
try testing.expectJson( .id = 1,
\\{ .@"error" = struct {}{},
\\ "id": 1, }, out.written());
\\ "error": {} }
\\}
, out_alloc.writer.buffered()); fn testLoadPage(url: [:0]const u8, writer: *std.Io.Writer) !*Server {
var server = try Server.init(testing.allocator, testing.test_app, writer);
errdefer server.deinit();
const page = try server.session.createPage();
try page.navigate(url, .{});
var runner = try server.session.runner(.{});
try runner.wait(.{ .ms = 2000 });
return server;
} }