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,39 +653,25 @@ 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 // Test Click
const btn = page.document.getElementById("btn", page).?.asNode(); const btn = page.document.getElementById("btn", page).?.asNode();
const btn_id = (try server.node_registry.register(btn)).id; const btn_id = (try server.node_registry.register(btn)).id;
@@ -701,10 +679,12 @@ test "MCP - Actions: click, fill, scroll" {
const btn_id_str = std.fmt.bufPrint(&btn_id_buf, "{d}", .{btn_id}) catch unreachable; const btn_id_str = std.fmt.bufPrint(&btn_id_buf, "{d}", .{btn_id}) catch unreachable;
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 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 router.handleMessage(server, aa, click_msg); try router.handleMessage(server, aa, click_msg);
try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "Clicked element") != null); try testing.expect(std.mem.indexOf(u8, out.written(), "Clicked element") != null);
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(), "Page url: http://localhost:9582/src/browser/tests/mcp_actions.html") != null);
out_alloc.clearRetainingCapacity(); out.clearRetainingCapacity();
}
{
// Test Fill Input // Test Fill Input
const inp = page.document.getElementById("inp", page).?.asNode(); const inp = page.document.getElementById("inp", page).?.asNode();
const inp_id = (try server.node_registry.register(inp)).id; const inp_id = (try server.node_registry.register(inp)).id;
@@ -712,10 +692,12 @@ test "MCP - Actions: click, fill, scroll" {
const inp_id_str = std.fmt.bufPrint(&inp_id_buf, "{d}", .{inp_id}) catch unreachable; const inp_id_str = std.fmt.bufPrint(&inp_id_buf, "{d}", .{inp_id}) catch unreachable;
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 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 router.handleMessage(server, aa, fill_msg); try router.handleMessage(server, aa, fill_msg);
try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "Filled element") != null); try testing.expect(std.mem.indexOf(u8, out.written(), "Filled element") != null);
try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "with \\\"hello\\\"") != null); try testing.expect(std.mem.indexOf(u8, out.written(), "with \\\"hello\\\"") != null);
out_alloc.clearRetainingCapacity(); out.clearRetainingCapacity();
}
{
// Test Fill Select // Test Fill Select
const sel = page.document.getElementById("sel", page).?.asNode(); const sel = page.document.getElementById("sel", page).?.asNode();
const sel_id = (try server.node_registry.register(sel)).id; const sel_id = (try server.node_registry.register(sel)).id;
@@ -723,10 +705,12 @@ test "MCP - Actions: click, fill, scroll" {
const sel_id_str = std.fmt.bufPrint(&sel_id_buf, "{d}", .{sel_id}) catch unreachable; const sel_id_str = std.fmt.bufPrint(&sel_id_buf, "{d}", .{sel_id}) catch unreachable;
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 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 router.handleMessage(server, aa, fill_sel_msg); try router.handleMessage(server, aa, fill_sel_msg);
try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "Filled element") != null); try testing.expect(std.mem.indexOf(u8, out.written(), "Filled element") != null);
try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "with \\\"opt2\\\"") != null); try testing.expect(std.mem.indexOf(u8, out.written(), "with \\\"opt2\\\"") != null);
out_alloc.clearRetainingCapacity(); out.clearRetainingCapacity();
}
{
// Test Scroll // Test Scroll
const scrollbox = page.document.getElementById("scrollbox", page).?.asNode(); const scrollbox = page.document.getElementById("scrollbox", page).?.asNode();
const scrollbox_id = (try server.node_registry.register(scrollbox)).id; const scrollbox_id = (try server.node_registry.register(scrollbox)).id;
@@ -734,8 +718,9 @@ test "MCP - Actions: click, fill, scroll" {
const scroll_id_str = std.fmt.bufPrint(&scroll_id_buf, "{d}", .{scrollbox_id}) catch unreachable; const scroll_id_str = std.fmt.bufPrint(&scroll_id_buf, "{d}", .{scrollbox_id}) catch unreachable;
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_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 router.handleMessage(server, aa, scroll_msg); try router.handleMessage(server, aa, scroll_msg);
try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "Scrolled to x: 0, y: 50") != null); try testing.expect(std.mem.indexOf(u8, out.written(), "Scrolled to x: 0, y: 50") != null);
out_alloc.clearRetainingCapacity(); 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;
} }