From f63b8ae0044923ab22b007b77361e2e87f2fd0e7 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Fri, 24 Apr 2026 11:24:16 +0200 Subject: [PATCH 1/2] cdp: AXNodeId is a string per spec AXNodeId is defined as string in CDP spec. This this a difference with DOM.NodeId which is an int. https://chromedevtools.github.io/devtools-protocol/tot/Accessibility/#type-AXNodeId https://chromedevtools.github.io/devtools-protocol/tot/DOM/#type-NodeId --- src/cdp/AXNode.zig | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/cdp/AXNode.zig b/src/cdp/AXNode.zig index 8f145e5a..f389aea9 100644 --- a/src/cdp/AXNode.zig +++ b/src/cdp/AXNode.zig @@ -67,6 +67,14 @@ pub const Writer = struct { return w.endArray(); } + // CDP spec defines AXNodeId as a string, so nodeId/parentId/childIds must + // be serialized as JSON strings even though we track them internally as u32. + fn writeIdString(id: u32, w: anytype) !void { + var buf: [10]u8 = undefined; + const s = try std.fmt.bufPrint(&buf, "{d}", .{id}); + try w.write(s); + } + fn writeNodeChildren(self: *const Writer, parent: AXNode, in_aria_hidden: bool, w: anytype) !void { // Add ListMarker for listitem elements if (parent.dom.is(DOMNode.Element)) |parent_el| { @@ -131,7 +139,7 @@ pub const Writer = struct { try w.objectField("nodeId"); const marker_id = self.registry.node_id; self.registry.node_id += 1; - try w.write(marker_id); + try writeIdString(marker_id, w); try w.objectField("backendDOMNodeId"); try w.write(marker_id); @@ -189,7 +197,7 @@ pub const Writer = struct { // Get the parent node ID for the parentId field const li_registered = try self.registry.register(li_node); try w.objectField("parentId"); - try w.write(li_registered.id); + try writeIdString(li_registered.id, w); try w.objectField("childIds"); try w.beginArray(); @@ -472,7 +480,7 @@ pub const Writer = struct { try w.beginObject(); try w.objectField("nodeId"); - try w.write(id); + try writeIdString(id, w); try w.objectField("backendDOMNodeId"); try w.write(id); @@ -546,7 +554,7 @@ pub const Writer = struct { if (n._parent) |p| { const parent_node = try self.registry.register(p); try w.objectField("parentId"); - try w.write(parent_node.id); + try writeIdString(parent_node.id, w); } // Children @@ -578,7 +586,7 @@ pub const Writer = struct { } const child_node = try registry.register(child); - try w.write(child_node.id); + try writeIdString(child_node.id, w); } } try w.endArray(); @@ -1393,7 +1401,8 @@ test "AXNode: writer" { // First node should be the document const doc_node = nodes[0].object; - try testing.expectEqual(1, doc_node.get("nodeId").?.integer); + // CDP spec: AXNodeId is a string; backendDOMNodeId (DOM.BackendNodeId) is an integer. + try testing.expectEqual("1", doc_node.get("nodeId").?.string); try testing.expectEqual(1, doc_node.get("backendDOMNodeId").?.integer); try testing.expectEqual(false, doc_node.get("ignored").?.bool); @@ -1412,6 +1421,21 @@ test "AXNode: writer" { // Check childIds array exists const child_ids = doc_node.get("childIds").?.array.items; try testing.expect(child_ids.len > 0); + // CDP spec: childIds entries are AXNodeId (strings). + for (child_ids) |cid| { + try testing.expect(cid == .string); + } + + // A non-root node must have parentId serialized as a string. + var saw_parent_id = false; + for (nodes[1..]) |node_val| { + if (node_val.object.get("parentId")) |pid| { + try testing.expect(pid == .string); + saw_parent_id = true; + break; + } + } + try testing.expect(saw_parent_id); // Find the h1 node and verify its level property is serialized as a string for (nodes) |node_val| { From 6a65801527fff8b583561b6a64abfc166146d11a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Fri, 24 Apr 2026 13:08:40 +0200 Subject: [PATCH 2/2] cli: allow optional positional arguments in command builder --- src/cli.zig | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/cli.zig b/src/cli.zig index ed6a1f0b..061fa784 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -580,10 +580,12 @@ pub fn Builder(comptime commands: anytype) type { } } - // Parsing is complete and positional is null. - const positional_is_null = @hasField(@TypeOf(command), "positional") and @field(c, command.positional.name) == null; - if (positional_is_null) { - return error.MissingArgument; + // A non-optional positional that is still null after parsing is missing. + if (comptime @hasField(@TypeOf(command), "positional")) { + const is_optional = @typeInfo(command.positional.type) == .optional; + if (!is_optional and @field(c, command.positional.name) == null) { + return error.MissingArgument; + } } return @unionInit(Union, command.name, c);