mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
Merge branch 'main' into css-improvements
This commit is contained in:
@@ -295,7 +295,7 @@ pub const Client = struct {
|
||||
}
|
||||
|
||||
var cdp = &self.mode.cdp;
|
||||
var last_message = timestamp(.monotonic);
|
||||
var last_message = milliTimestamp(.monotonic);
|
||||
var ms_remaining = self.ws.timeout_ms;
|
||||
|
||||
while (true) {
|
||||
@@ -304,7 +304,7 @@ pub const Client = struct {
|
||||
if (self.readSocket() == false) {
|
||||
return;
|
||||
}
|
||||
last_message = timestamp(.monotonic);
|
||||
last_message = milliTimestamp(.monotonic);
|
||||
ms_remaining = self.ws.timeout_ms;
|
||||
},
|
||||
.no_page => {
|
||||
@@ -319,16 +319,18 @@ pub const Client = struct {
|
||||
if (self.readSocket() == false) {
|
||||
return;
|
||||
}
|
||||
last_message = timestamp(.monotonic);
|
||||
last_message = milliTimestamp(.monotonic);
|
||||
ms_remaining = self.ws.timeout_ms;
|
||||
},
|
||||
.done => {
|
||||
const elapsed = timestamp(.monotonic) - last_message;
|
||||
if (elapsed > ms_remaining) {
|
||||
const now = milliTimestamp(.monotonic);
|
||||
const elapsed = now - last_message;
|
||||
if (elapsed >= ms_remaining) {
|
||||
log.info(.app, "CDP timeout", .{});
|
||||
return;
|
||||
}
|
||||
ms_remaining -= @intCast(elapsed);
|
||||
last_message = now;
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -501,6 +503,7 @@ fn buildJSONVersionResponse(
|
||||
}
|
||||
|
||||
pub const timestamp = @import("datetime.zig").timestamp;
|
||||
pub const milliTimestamp = @import("datetime.zig").milliTimestamp;
|
||||
|
||||
const testing = std.testing;
|
||||
test "server: buildJSONVersionResponse" {
|
||||
|
||||
@@ -1091,7 +1091,6 @@ pub fn iframeAddedCallback(self: *Page, iframe: *IFrame) !void {
|
||||
log.warn(.page, "iframe navigate failure", .{ .url = url, .err = err });
|
||||
self._pending_loads -= 1;
|
||||
iframe._window = null;
|
||||
page_frame.deinit(true);
|
||||
return error.IFrameLoadError;
|
||||
};
|
||||
|
||||
|
||||
@@ -548,7 +548,9 @@ fn processQueuedNavigation(self: *Session) !void {
|
||||
continue;
|
||||
}
|
||||
|
||||
try self.processFrameNavigation(page, qn);
|
||||
self.processFrameNavigation(page, qn) catch |err| {
|
||||
log.warn(.page, "frame navigation", .{ .url = qn.url, .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
// Clear the queue after first pass
|
||||
@@ -588,7 +590,8 @@ fn processFrameNavigation(self: *Session, page: *Page, qn: *QueuedNavigation) !v
|
||||
|
||||
errdefer iframe._window = null;
|
||||
|
||||
if (page._parent_notified) {
|
||||
const parent_notified = page._parent_notified;
|
||||
if (parent_notified) {
|
||||
// we already notified the parent that we had loaded
|
||||
parent._pending_loads += 1;
|
||||
}
|
||||
@@ -598,7 +601,19 @@ fn processFrameNavigation(self: *Session, page: *Page, qn: *QueuedNavigation) !v
|
||||
page.* = undefined;
|
||||
|
||||
try Page.init(page, frame_id, self, parent);
|
||||
errdefer page.deinit(true);
|
||||
errdefer {
|
||||
for (parent.frames.items, 0..) |frame, i| {
|
||||
if (frame == page) {
|
||||
parent.frames_sorted = false;
|
||||
_ = parent.frames.swapRemove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (parent_notified) {
|
||||
parent._pending_loads -= 1;
|
||||
}
|
||||
page.deinit(true);
|
||||
}
|
||||
|
||||
page.iframe = iframe;
|
||||
iframe._window = page.window;
|
||||
|
||||
@@ -197,18 +197,20 @@ pub fn trackTemp(self: *Context, global: v8.Global) !void {
|
||||
}
|
||||
|
||||
pub fn weakRef(self: *Context, obj: anytype) void {
|
||||
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(obj)) orelse {
|
||||
const resolved = js.Local.resolveValue(obj);
|
||||
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
|
||||
if (comptime IS_DEBUG) {
|
||||
// should not be possible
|
||||
std.debug.assert(false);
|
||||
}
|
||||
return;
|
||||
};
|
||||
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, bridge.Struct(@TypeOf(obj)).JsApi.Meta.finalizer.from_v8, v8.kParameter);
|
||||
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, resolved.finalizer_from_v8, v8.kParameter);
|
||||
}
|
||||
|
||||
pub fn safeWeakRef(self: *Context, obj: anytype) void {
|
||||
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(obj)) orelse {
|
||||
const resolved = js.Local.resolveValue(obj);
|
||||
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
|
||||
if (comptime IS_DEBUG) {
|
||||
// should not be possible
|
||||
std.debug.assert(false);
|
||||
@@ -216,11 +218,12 @@ pub fn safeWeakRef(self: *Context, obj: anytype) void {
|
||||
return;
|
||||
};
|
||||
v8.v8__Global__ClearWeak(&fc.global);
|
||||
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, bridge.Struct(@TypeOf(obj)).JsApi.Meta.finalizer.from_v8, v8.kParameter);
|
||||
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, resolved.finalizer_from_v8, v8.kParameter);
|
||||
}
|
||||
|
||||
pub fn strongRef(self: *Context, obj: anytype) void {
|
||||
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(obj)) orelse {
|
||||
const resolved = js.Local.resolveValue(obj);
|
||||
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
|
||||
if (comptime IS_DEBUG) {
|
||||
// should not be possible
|
||||
std.debug.assert(false);
|
||||
|
||||
@@ -63,6 +63,20 @@ pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype
|
||||
};
|
||||
}
|
||||
|
||||
const RejectError = union(enum) {
|
||||
generic: []const u8,
|
||||
type_error: []const u8,
|
||||
};
|
||||
pub fn rejectError(self: PromiseResolver, comptime source: []const u8, err: RejectError) void {
|
||||
const handle = switch (err) {
|
||||
.type_error => |str| self.local.isolate.createTypeError(str),
|
||||
.generic => |str| self.local.isolate.createError(str),
|
||||
};
|
||||
self._reject(js.Value{ .handle = handle, .local = self.local }) catch |reject_err| {
|
||||
log.err(.bug, "rejectError", .{ .source = source, .err = reject_err, .persistent = false });
|
||||
};
|
||||
}
|
||||
|
||||
fn _reject(self: PromiseResolver, value: anytype) !void {
|
||||
const local = self.local;
|
||||
const js_val = try local.zigValueToJs(value, .{});
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
}
|
||||
|
||||
{
|
||||
// Empty XML is a parse error (no root element)
|
||||
const parser = new DOMParser();
|
||||
testing.expectError('Error', () => parser.parseFromString('', 'text/xml'));
|
||||
let d = parser.parseFromString('', 'text/xml');
|
||||
testing.expectEqual('<parsererror>error</parsererror>', new XMLSerializer().serializeToString(d));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<body onload="loaded()"></body>
|
||||
<body onload="loadEvent = event"></body>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id=bodyOnLoad2>
|
||||
let called = 0;
|
||||
function loaded(e) {
|
||||
called += 1;
|
||||
}
|
||||
// Per spec, the handler is compiled as: function(event) { loadEvent = event }
|
||||
// Verify: handler fires, "event" parameter is a proper Event, and handler is a function.
|
||||
let loadEvent = null;
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(1, called);
|
||||
testing.expectEqual("function", typeof document.body.onload);
|
||||
testing.expectTrue(loadEvent instanceof Event);
|
||||
testing.expectEqual("load", loadEvent.type);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
28
src/browser/tests/window/body_onload3.html
Normal file
28
src/browser/tests/window/body_onload3.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<body onload="called++"></body>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id=bodyOnLoad3>
|
||||
// Per spec, the handler is compiled as: function(event) { called++ }
|
||||
// Verify: handler fires exactly once, and body.onload reflects to window.onload.
|
||||
let called = 0;
|
||||
|
||||
testing.eventually(() => {
|
||||
// The attribute handler should have fired exactly once.
|
||||
testing.expectEqual(1, called);
|
||||
|
||||
// body.onload is a Window-reflecting handler per spec.
|
||||
testing.expectEqual("function", typeof document.body.onload);
|
||||
testing.expectEqual(document.body.onload, window.onload);
|
||||
|
||||
// Setting body.onload via property replaces the attribute handler.
|
||||
let propertyCalled = false;
|
||||
document.body.onload = function() { propertyCalled = true; };
|
||||
testing.expectEqual(document.body.onload, window.onload);
|
||||
|
||||
// Setting onload to null removes the handler.
|
||||
document.body.onload = null;
|
||||
testing.expectEqual(null, document.body.onload);
|
||||
testing.expectEqual(null, window.onload);
|
||||
});
|
||||
</script>
|
||||
@@ -86,15 +86,15 @@ pub fn parseFromString(
|
||||
var parser = Parser.init(arena, doc_node, page);
|
||||
parser.parseXML(html);
|
||||
|
||||
if (parser.err) |pe| {
|
||||
return pe.err;
|
||||
if (parser.err != null or doc_node.firstChild() == null) {
|
||||
// Return a document with a <parsererror> element per spec.
|
||||
const err_doc = try page._factory.document(XMLDocument{ ._proto = undefined });
|
||||
var err_parser = Parser.init(arena, err_doc.asNode(), page);
|
||||
err_parser.parseXML("<parsererror xmlns=\"http://www.mozilla.org/newlayout/xml/parsererror.xml\">error</parsererror>");
|
||||
return err_doc.asDocument();
|
||||
}
|
||||
|
||||
const first_child = doc_node.firstChild() orelse {
|
||||
// Empty XML or no root element - this is a parse error.
|
||||
// TODO: Return a document with a <parsererror> element per spec.
|
||||
return error.JsException;
|
||||
};
|
||||
const first_child = doc_node.firstChild().?;
|
||||
|
||||
// If first node is a `ProcessingInstruction`, skip it.
|
||||
if (first_child.getNodeType() == 7) {
|
||||
|
||||
@@ -192,7 +192,8 @@ fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||
self._page.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
ls.toLocal(self._resolver).reject("fetch error", @errorName(err));
|
||||
// fetch() must reject with a TypeError on network errors per spec
|
||||
ls.toLocal(self._resolver).rejectError("fetch error", .{ .type_error = @errorName(err) });
|
||||
}
|
||||
|
||||
fn httpShutdownCallback(ctx: *anyopaque) void {
|
||||
|
||||
@@ -228,6 +228,13 @@ pub const Writer = struct {
|
||||
|
||||
try w.objectField("value");
|
||||
switch (value) {
|
||||
.integer => |v| {
|
||||
// CDP spec requires integer values to be serialized as strings.
|
||||
// 20 bytes is enough for the decimal representation of a 64-bit integer.
|
||||
var buf: [20]u8 = undefined;
|
||||
const s = try std.fmt.bufPrint(&buf, "{d}", .{v});
|
||||
try w.write(s);
|
||||
},
|
||||
inline else => |v| try w.write(v),
|
||||
}
|
||||
|
||||
@@ -1212,4 +1219,25 @@ test "AXNode: writer" {
|
||||
// Check childIds array exists
|
||||
const child_ids = doc_node.get("childIds").?.array.items;
|
||||
try testing.expect(child_ids.len > 0);
|
||||
|
||||
// Find the h1 node and verify its level property is serialized as a string
|
||||
for (nodes) |node_val| {
|
||||
const obj = node_val.object;
|
||||
const role_obj = obj.get("role") orelse continue;
|
||||
const role_val = role_obj.object.get("value") orelse continue;
|
||||
if (!std.mem.eql(u8, role_val.string, "heading")) continue;
|
||||
|
||||
const props = obj.get("properties").?.array.items;
|
||||
for (props) |prop| {
|
||||
const prop_obj = prop.object;
|
||||
const name_str = prop_obj.get("name").?.string;
|
||||
if (!std.mem.eql(u8, name_str, "level")) continue;
|
||||
const level_value = prop_obj.get("value").?.object;
|
||||
try testing.expectEqual("integer", level_value.get("type").?.string);
|
||||
// CDP spec: integer values must be serialized as strings
|
||||
try testing.expectEqual("1", level_value.get("value").?.string);
|
||||
return;
|
||||
}
|
||||
}
|
||||
return error.HeadingNodeNotFound;
|
||||
}
|
||||
|
||||
@@ -168,13 +168,11 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
||||
|
||||
if (is_startup) {
|
||||
dispatchStartupCommand(&command, input.method) catch |err| {
|
||||
command.sendError(-31999, @errorName(err), .{}) catch {};
|
||||
return err;
|
||||
command.sendError(-31999, @errorName(err), .{}) catch return err;
|
||||
};
|
||||
} else {
|
||||
dispatchCommand(&command, input.method) catch |err| {
|
||||
command.sendError(-31998, @errorName(err), .{}) catch {};
|
||||
return err;
|
||||
command.sendError(-31998, @errorName(err), .{}) catch return err;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -924,18 +922,20 @@ test "cdp: invalid json" {
|
||||
// method is required
|
||||
try testing.expectError(error.InvalidJSON, ctx.processMessage(.{}));
|
||||
|
||||
try testing.expectError(error.InvalidMethod, ctx.processMessage(.{
|
||||
try ctx.processMessage(.{
|
||||
.method = "Target",
|
||||
}));
|
||||
});
|
||||
try ctx.expectSentError(-31998, "InvalidMethod", .{});
|
||||
|
||||
try testing.expectError(error.UnknownDomain, ctx.processMessage(.{
|
||||
try ctx.processMessage(.{
|
||||
.method = "Unknown.domain",
|
||||
}));
|
||||
});
|
||||
try ctx.expectSentError(-31998, "UnknownDomain", .{});
|
||||
|
||||
try testing.expectError(error.UnknownMethod, ctx.processMessage(.{
|
||||
try ctx.processMessage(.{
|
||||
.method = "Target.over9000",
|
||||
}));
|
||||
});
|
||||
try ctx.expectSentError(-31998, "UnknownMethod", .{});
|
||||
}
|
||||
|
||||
test "cdp: invalid sessionId" {
|
||||
|
||||
@@ -550,11 +550,12 @@ test "cdp.dom: getSearchResults unknown search id" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{
|
||||
try ctx.processMessage(.{
|
||||
.id = 8,
|
||||
.method = "DOM.getSearchResults",
|
||||
.params = .{ .searchId = "Nope", .fromIndex = 0, .toIndex = 10 },
|
||||
}));
|
||||
});
|
||||
try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 8 });
|
||||
}
|
||||
|
||||
test "cdp.dom: search flow" {
|
||||
@@ -604,11 +605,12 @@ test "cdp.dom: search flow" {
|
||||
try ctx.expectSentResult(null, .{ .id = 16 });
|
||||
|
||||
// make sure the delete actually did something
|
||||
try testing.expectError(error.SearchResultNotFound, ctx.processMessage(.{
|
||||
try ctx.processMessage(.{
|
||||
.id = 17,
|
||||
.method = "DOM.getSearchResults",
|
||||
.params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 1 },
|
||||
}));
|
||||
});
|
||||
try ctx.expectSentError(-31998, "SearchResultNotFound", .{ .id = 17 });
|
||||
}
|
||||
|
||||
test "cdp.dom: querySelector unknown search id" {
|
||||
@@ -645,11 +647,12 @@ test "cdp.dom: querySelector Node not found" {
|
||||
});
|
||||
try ctx.expectSentResult(.{ .searchId = "0", .resultCount = 2 }, .{ .id = 3 });
|
||||
|
||||
try testing.expectError(error.NodeNotFoundForGivenId, ctx.processMessage(.{
|
||||
try ctx.processMessage(.{
|
||||
.id = 4,
|
||||
.method = "DOM.querySelector",
|
||||
.params = .{ .nodeId = 1, .selector = "a" },
|
||||
}));
|
||||
});
|
||||
try ctx.expectSentError(-31998, "NodeNotFoundForGivenId", .{ .id = 4 });
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 5,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const CdpStorage = @import("storage.zig");
|
||||
|
||||
@@ -117,7 +118,12 @@ fn deleteCookies(cmd: anytype) !void {
|
||||
path: ?[]const u8 = null,
|
||||
partitionKey: ?CdpStorage.CookiePartitionKey = null,
|
||||
})) orelse return error.InvalidParams;
|
||||
if (params.partitionKey != null) return error.NotImplemented;
|
||||
// Silently ignore partitionKey since we don't support partitioned cookies (CHIPS).
|
||||
// This allows Puppeteer's page.setCookie() to work, which sends deleteCookies
|
||||
// with partitionKey as part of its cookie-setting workflow.
|
||||
if (params.partitionKey != null) {
|
||||
log.warn(.not_implemented, "partition key", .{ .src = "deleteCookies" });
|
||||
}
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const cookies = &bc.session.cookie_jar.cookies;
|
||||
|
||||
@@ -633,7 +633,7 @@ test "cdp.page: getFrameTree" {
|
||||
defer ctx.deinit();
|
||||
|
||||
{
|
||||
try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Page.getFrameTree", .params = .{ .targetId = "X" } }));
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Page.getFrameTree", .params = .{ .targetId = "X" } });
|
||||
try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 });
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +128,14 @@ pub const CdpCookie = struct {
|
||||
};
|
||||
|
||||
pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
|
||||
if (param.priority != .Medium or param.sameParty != null or param.sourceScheme != null or param.partitionKey != null) {
|
||||
// Silently ignore partitionKey since we don't support partitioned cookies (CHIPS).
|
||||
// This allows Puppeteer's page.setCookie() to work, which may send cookies with
|
||||
// partitionKey as part of its cookie-setting workflow.
|
||||
if (param.partitionKey != null) {
|
||||
log.warn(.not_implemented, "partition key", .{ .src = "setCdpCookie" });
|
||||
}
|
||||
// Still reject unsupported features
|
||||
if (param.priority != .Medium or param.sameParty != null or param.sourceScheme != null) {
|
||||
return error.NotImplemented;
|
||||
}
|
||||
|
||||
|
||||
@@ -556,7 +556,7 @@ test "cdp.target: disposeBrowserContext" {
|
||||
defer ctx.deinit();
|
||||
|
||||
{
|
||||
try testing.expectError(error.InvalidParams, ctx.processMessage(.{ .id = 7, .method = "Target.disposeBrowserContext" }));
|
||||
try ctx.processMessage(.{ .id = 7, .method = "Target.disposeBrowserContext" });
|
||||
try ctx.expectSentError(-31998, "InvalidParams", .{ .id = 7 });
|
||||
}
|
||||
|
||||
@@ -609,7 +609,7 @@ test "cdp.target: createTarget" {
|
||||
defer ctx.deinit();
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
|
||||
{
|
||||
try testing.expectError(error.UnknownBrowserContextId, ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-8" } }));
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-8" } });
|
||||
try ctx.expectSentError(-31998, "UnknownBrowserContextId", .{ .id = 10 });
|
||||
}
|
||||
|
||||
@@ -626,13 +626,13 @@ test "cdp.target: closeTarget" {
|
||||
defer ctx.deinit();
|
||||
|
||||
{
|
||||
try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "X" } }));
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "X" } });
|
||||
try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 });
|
||||
}
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
|
||||
{
|
||||
try testing.expectError(error.TargetNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } }));
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } });
|
||||
try ctx.expectSentError(-31998, "TargetNotLoaded", .{ .id = 10 });
|
||||
}
|
||||
|
||||
@@ -640,7 +640,7 @@ test "cdp.target: closeTarget" {
|
||||
_ = try bc.session.createPage();
|
||||
bc.target_id = "TID-000000000A".*;
|
||||
{
|
||||
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } }));
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } });
|
||||
try ctx.expectSentError(-31998, "UnknownTargetId", .{ .id = 10 });
|
||||
}
|
||||
|
||||
@@ -657,13 +657,13 @@ test "cdp.target: attachToTarget" {
|
||||
defer ctx.deinit();
|
||||
|
||||
{
|
||||
try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "X" } }));
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "X" } });
|
||||
try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 });
|
||||
}
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
|
||||
{
|
||||
try testing.expectError(error.TargetNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } }));
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } });
|
||||
try ctx.expectSentError(-31998, "TargetNotLoaded", .{ .id = 10 });
|
||||
}
|
||||
|
||||
@@ -671,7 +671,7 @@ test "cdp.target: attachToTarget" {
|
||||
_ = try bc.session.createPage();
|
||||
bc.target_id = "TID-000000000B".*;
|
||||
{
|
||||
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } }));
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } });
|
||||
try ctx.expectSentError(-31998, "UnknownTargetId", .{ .id = 10 });
|
||||
}
|
||||
|
||||
@@ -701,13 +701,13 @@ test "cdp.target: getTargetInfo" {
|
||||
}
|
||||
|
||||
{
|
||||
try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "X" } }));
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "X" } });
|
||||
try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 });
|
||||
}
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
|
||||
{
|
||||
try testing.expectError(error.TargetNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } }));
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } });
|
||||
try ctx.expectSentError(-31998, "TargetNotLoaded", .{ .id = 10 });
|
||||
}
|
||||
|
||||
@@ -715,7 +715,7 @@ test "cdp.target: getTargetInfo" {
|
||||
_ = try bc.session.createPage();
|
||||
bc.target_id = "TID-000000000C".*;
|
||||
{
|
||||
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } }));
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } });
|
||||
try ctx.expectSentError(-31998, "UnknownTargetId", .{ .id = 10 });
|
||||
}
|
||||
|
||||
|
||||
@@ -116,3 +116,24 @@ test "MCP.Server - Integration: synchronous smoke test" {
|
||||
|
||||
try testing.expectJson(.{ .id = 1 }, out_alloc.writer.buffered());
|
||||
}
|
||||
|
||||
test "MCP.Server - Integration: ping request returns an empty result" {
|
||||
defer testing.reset();
|
||||
const allocator = testing.allocator;
|
||||
const app = testing.test_app;
|
||||
|
||||
const input =
|
||||
\\{"jsonrpc":"2.0","id":"ping-1","method":"ping"}
|
||||
;
|
||||
|
||||
var in_reader: std.io.Reader = .fixed(input);
|
||||
var out_alloc: std.io.Writer.Allocating = .init(testing.arena_allocator);
|
||||
defer out_alloc.deinit();
|
||||
|
||||
var server = try Self.init(allocator, app, &out_alloc.writer);
|
||||
defer server.deinit();
|
||||
|
||||
try router.processRequests(server, &in_reader);
|
||||
|
||||
try testing.expectJson(.{ .id = "ping-1", .result = .{} }, out_alloc.writer.buffered());
|
||||
}
|
||||
|
||||
@@ -238,6 +238,27 @@ test "MCP.protocol - request parsing" {
|
||||
try testing.expectString("1.0.0", init_params.value.clientInfo.version);
|
||||
}
|
||||
|
||||
test "MCP.protocol - ping request parsing" {
|
||||
defer testing.reset();
|
||||
const raw_json =
|
||||
\\{
|
||||
\\ "jsonrpc": "2.0",
|
||||
\\ "id": "123",
|
||||
\\ "method": "ping"
|
||||
\\}
|
||||
;
|
||||
|
||||
const parsed = try std.json.parseFromSlice(Request, testing.arena_allocator, raw_json, .{ .ignore_unknown_fields = true });
|
||||
defer parsed.deinit();
|
||||
|
||||
const req = parsed.value;
|
||||
try testing.expectString("2.0", req.jsonrpc);
|
||||
try testing.expectString("ping", req.method);
|
||||
try testing.expect(req.id.? == .string);
|
||||
try testing.expectString("123", req.id.?.string);
|
||||
try testing.expectEqual(null, req.params);
|
||||
}
|
||||
|
||||
test "MCP.protocol - response formatting" {
|
||||
defer testing.reset();
|
||||
const response = Response{
|
||||
|
||||
@@ -34,6 +34,7 @@ const log = @import("../log.zig");
|
||||
|
||||
const Method = enum {
|
||||
initialize,
|
||||
ping,
|
||||
@"notifications/initialized",
|
||||
@"tools/list",
|
||||
@"tools/call",
|
||||
@@ -43,6 +44,7 @@ const Method = enum {
|
||||
|
||||
const method_map = std.StaticStringMap(Method).initComptime(.{
|
||||
.{ "initialize", .initialize },
|
||||
.{ "ping", .ping },
|
||||
.{ "notifications/initialized", .@"notifications/initialized" },
|
||||
.{ "tools/list", .@"tools/list" },
|
||||
.{ "tools/call", .@"tools/call" },
|
||||
@@ -68,6 +70,7 @@ pub fn handleMessage(server: *Server, arena: std.mem.Allocator, msg: []const u8)
|
||||
|
||||
switch (method) {
|
||||
.initialize => try handleInitialize(server, req),
|
||||
.ping => try handlePing(server, req),
|
||||
.@"notifications/initialized" => {},
|
||||
.@"tools/list" => try tools.handleList(server, arena, req),
|
||||
.@"tools/call" => try tools.handleCall(server, arena, req),
|
||||
@@ -92,6 +95,11 @@ fn handleInitialize(server: *Server, req: protocol.Request) !void {
|
||||
try server.sendResult(req.id.?, result);
|
||||
}
|
||||
|
||||
fn handlePing(server: *Server, req: protocol.Request) !void {
|
||||
const id = req.id orelse return;
|
||||
try server.sendResult(id, .{});
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
|
||||
test "MCP.router - handleMessage - synchronous unit tests" {
|
||||
@@ -116,22 +124,29 @@ test "MCP.router - handleMessage - synchronous unit tests" {
|
||||
, out_alloc.writer.buffered());
|
||||
out_alloc.writer.end = 0;
|
||||
|
||||
// 2. Tools list
|
||||
// 2. Ping
|
||||
try handleMessage(server, aa,
|
||||
\\{"jsonrpc":"2.0","id":2,"method":"tools/list"}
|
||||
\\{"jsonrpc":"2.0","id":2,"method":"ping"}
|
||||
);
|
||||
try testing.expectJson(.{ .id = 2 }, out_alloc.writer.buffered());
|
||||
try testing.expectJson(.{ .id = 2, .result = .{} }, out_alloc.writer.buffered());
|
||||
out_alloc.writer.end = 0;
|
||||
|
||||
// 3. Tools list
|
||||
try handleMessage(server, aa,
|
||||
\\{"jsonrpc":"2.0","id":3,"method":"tools/list"}
|
||||
);
|
||||
try testing.expectJson(.{ .id = 3 }, out_alloc.writer.buffered());
|
||||
try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "\"name\":\"goto\"") != null);
|
||||
out_alloc.writer.end = 0;
|
||||
|
||||
// 3. Method not found
|
||||
// 4. Method not found
|
||||
try handleMessage(server, aa,
|
||||
\\{"jsonrpc":"2.0","id":3,"method":"unknown_method"}
|
||||
\\{"jsonrpc":"2.0","id":4,"method":"unknown_method"}
|
||||
);
|
||||
try testing.expectJson(.{ .id = 3, .@"error" = .{ .code = -32601 } }, out_alloc.writer.buffered());
|
||||
try testing.expectJson(.{ .id = 4, .@"error" = .{ .code = -32601 } }, out_alloc.writer.buffered());
|
||||
out_alloc.writer.end = 0;
|
||||
|
||||
// 4. Parse error
|
||||
// 5. Parse error
|
||||
{
|
||||
const filter: testing.LogFilter = .init(.mcp);
|
||||
defer filter.deinit();
|
||||
|
||||
Reference in New Issue
Block a user