mirror of
				https://github.com/lightpanda-io/browser.git
				synced 2025-10-30 15:41:48 +00:00 
			
		
		
		
	Merge pull request #502 from lightpanda-io/cdp_node_children
Cdp node children
This commit is contained in:
		
							
								
								
									
										328
									
								
								src/cdp/Node.zig
									
									
									
									
									
								
							
							
						
						
									
										328
									
								
								src/cdp/Node.zig
									
									
									
									
									
								
							| @@ -22,79 +22,19 @@ const Allocator = std.mem.Allocator; | |||||||
|  |  | ||||||
| pub const Id = u32; | pub const Id = u32; | ||||||
|  |  | ||||||
|  | const log = std.log.scoped(.cdp_node); | ||||||
|  |  | ||||||
| const Node = @This(); | const Node = @This(); | ||||||
|  |  | ||||||
| id: Id, | id: Id, | ||||||
| parent_id: ?Id = null, |  | ||||||
| node_type: u32, |  | ||||||
| backend_node_id: Id, |  | ||||||
| node_name: []const u8, |  | ||||||
| local_name: []const u8, |  | ||||||
| node_value: []const u8, |  | ||||||
| child_node_count: u32, |  | ||||||
| children: []const *Node, |  | ||||||
| document_url: ?[]const u8, |  | ||||||
| base_url: ?[]const u8, |  | ||||||
| xml_version: []const u8, |  | ||||||
| compatibility_mode: CompatibilityMode, |  | ||||||
| is_scrollable: bool, |  | ||||||
| _node: *parser.Node, | _node: *parser.Node, | ||||||
|  |  | ||||||
| const CompatibilityMode = enum { |  | ||||||
|     NoQuirksMode, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| pub fn jsonStringify(self: *const Node, writer: anytype) !void { |  | ||||||
|     try writer.beginObject(); |  | ||||||
|     try writer.objectField("nodeId"); |  | ||||||
|     try writer.write(self.id); |  | ||||||
|  |  | ||||||
|     try writer.objectField("parentId"); |  | ||||||
|     try writer.write(self.parent_id); |  | ||||||
|  |  | ||||||
|     try writer.objectField("backendNodeId"); |  | ||||||
|     try writer.write(self.backend_node_id); |  | ||||||
|  |  | ||||||
|     try writer.objectField("nodeType"); |  | ||||||
|     try writer.write(self.node_type); |  | ||||||
|  |  | ||||||
|     try writer.objectField("nodeName"); |  | ||||||
|     try writer.write(self.node_name); |  | ||||||
|  |  | ||||||
|     try writer.objectField("localName"); |  | ||||||
|     try writer.write(self.local_name); |  | ||||||
|  |  | ||||||
|     try writer.objectField("nodeValue"); |  | ||||||
|     try writer.write(self.node_value); |  | ||||||
|  |  | ||||||
|     try writer.objectField("childNodeCount"); |  | ||||||
|     try writer.write(self.child_node_count); |  | ||||||
|  |  | ||||||
|     try writer.objectField("children"); |  | ||||||
|     try writer.write(self.children); |  | ||||||
|  |  | ||||||
|     try writer.objectField("documentURL"); |  | ||||||
|     try writer.write(self.document_url); |  | ||||||
|  |  | ||||||
|     try writer.objectField("baseURL"); |  | ||||||
|     try writer.write(self.base_url); |  | ||||||
|  |  | ||||||
|     try writer.objectField("xmlVersion"); |  | ||||||
|     try writer.write(self.xml_version); |  | ||||||
|  |  | ||||||
|     try writer.objectField("compatibilityMode"); |  | ||||||
|     try writer.write(self.compatibility_mode); |  | ||||||
|  |  | ||||||
|     try writer.objectField("isScrollable"); |  | ||||||
|     try writer.write(self.is_scrollable); |  | ||||||
|     try writer.endObject(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Whenever we send a node to the client, we register it here for future lookup. | // Whenever we send a node to the client, we register it here for future lookup. | ||||||
| // We maintain a node -> id and id -> node lookup. | // We maintain a node -> id and id -> node lookup. | ||||||
| pub const Registry = struct { | pub const Registry = struct { | ||||||
|     node_id: u32, |     node_id: u32, | ||||||
|     allocator: Allocator, |     allocator: Allocator, | ||||||
|  |     arena: std.heap.ArenaAllocator, | ||||||
|     node_pool: std.heap.MemoryPool(Node), |     node_pool: std.heap.MemoryPool(Node), | ||||||
|     lookup_by_id: std.AutoHashMapUnmanaged(Id, *Node), |     lookup_by_id: std.AutoHashMapUnmanaged(Id, *Node), | ||||||
|     lookup_by_node: std.HashMapUnmanaged(*parser.Node, *Node, NodeContext, std.hash_map.default_max_load_percentage), |     lookup_by_node: std.HashMapUnmanaged(*parser.Node, *Node, NodeContext, std.hash_map.default_max_load_percentage), | ||||||
| @@ -102,9 +42,10 @@ pub const Registry = struct { | |||||||
|     pub fn init(allocator: Allocator) Registry { |     pub fn init(allocator: Allocator) Registry { | ||||||
|         return .{ |         return .{ | ||||||
|             .node_id = 0, |             .node_id = 0, | ||||||
|             .allocator = allocator, |  | ||||||
|             .lookup_by_id = .{}, |             .lookup_by_id = .{}, | ||||||
|             .lookup_by_node = .{}, |             .lookup_by_node = .{}, | ||||||
|  |             .allocator = allocator, | ||||||
|  |             .arena = std.heap.ArenaAllocator.init(allocator), | ||||||
|             .node_pool = std.heap.MemoryPool(Node).init(allocator), |             .node_pool = std.heap.MemoryPool(Node).init(allocator), | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
| @@ -114,12 +55,14 @@ pub const Registry = struct { | |||||||
|         self.lookup_by_id.deinit(allocator); |         self.lookup_by_id.deinit(allocator); | ||||||
|         self.lookup_by_node.deinit(allocator); |         self.lookup_by_node.deinit(allocator); | ||||||
|         self.node_pool.deinit(); |         self.node_pool.deinit(); | ||||||
|  |         self.arena.deinit(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn reset(self: *Registry) void { |     pub fn reset(self: *Registry) void { | ||||||
|         self.lookup_by_id.clearRetainingCapacity(); |         self.lookup_by_id.clearRetainingCapacity(); | ||||||
|         self.lookup_by_node.clearRetainingCapacity(); |         self.lookup_by_node.clearRetainingCapacity(); | ||||||
|         _ = self.node_pool.reset(.{ .retain_capacity = {} }); |         _ = self.arena.reset(.{ .retain_with_limit = 1024 }); | ||||||
|  |         _ = self.node_pool.reset(.{ .retain_with_limit = 1024 }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn register(self: *Registry, n: *parser.Node) !*Node { |     pub fn register(self: *Registry, n: *parser.Node) !*Node { | ||||||
| @@ -132,38 +75,17 @@ pub const Registry = struct { | |||||||
|         // but, just in case, let's try to keep things tidy. |         // but, just in case, let's try to keep things tidy. | ||||||
|         errdefer _ = self.lookup_by_node.remove(n); |         errdefer _ = self.lookup_by_node.remove(n); | ||||||
|  |  | ||||||
|         const children = try parser.nodeGetChildNodes(n); |  | ||||||
|         const children_count = try parser.nodeListLength(children); |  | ||||||
|  |  | ||||||
|         const id = self.node_id; |  | ||||||
|         defer self.node_id = id + 1; |  | ||||||
|  |  | ||||||
|         const node = try self.node_pool.create(); |         const node = try self.node_pool.create(); | ||||||
|         errdefer self.node_pool.destroy(node); |         errdefer self.node_pool.destroy(node); | ||||||
|  |  | ||||||
|  |         const id = self.node_id; | ||||||
|  |         self.node_id = id + 1; | ||||||
|  |  | ||||||
|         node.* = .{ |         node.* = .{ | ||||||
|             ._node = n, |             ._node = n, | ||||||
|             .id = id, |             .id = id, | ||||||
|             .parent_id = null, // TODO |  | ||||||
|             .backend_node_id = id, // ?? |  | ||||||
|             .node_name = try parser.nodeName(n), |  | ||||||
|             .local_name = try parser.nodeLocalName(n), |  | ||||||
|             .node_value = try parser.nodeValue(n) orelse "", |  | ||||||
|             .node_type = @intFromEnum(try parser.nodeType(n)), |  | ||||||
|             .child_node_count = children_count, |  | ||||||
|             .children = &.{}, // TODO |  | ||||||
|             .document_url = null, |  | ||||||
|             .base_url = null, |  | ||||||
|             .xml_version = "", |  | ||||||
|             .compatibility_mode = .NoQuirksMode, |  | ||||||
|             .is_scrollable = false, |  | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         // if (try parser.nodeParentNode(n)) |pn| { |  | ||||||
|         //     _  = pn; |  | ||||||
|         //     // TODO |  | ||||||
|         // } |  | ||||||
|  |  | ||||||
|         node_lookup_gop.value_ptr.* = node; |         node_lookup_gop.value_ptr.* = node; | ||||||
|         try self.lookup_by_id.putNoClobber(self.allocator, id, node); |         try self.lookup_by_id.putNoClobber(self.allocator, id, node); | ||||||
|         return node; |         return node; | ||||||
| @@ -271,8 +193,107 @@ pub const Search = struct { | |||||||
|     }; |     }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | // Need a custom writer, because we can't just serialize the node as-is. | ||||||
|  | // Sometimes we want to serializ the node without chidren, sometimes with just | ||||||
|  | // its direct children, and sometimes the entire tree. | ||||||
|  | // (For now, we only support direct children) | ||||||
|  |  | ||||||
|  | pub const Writer = struct { | ||||||
|  |     opts: Opts, | ||||||
|  |     node: *const Node, | ||||||
|  |     registry: *Registry, | ||||||
|  |  | ||||||
|  |     pub const Opts = struct {}; | ||||||
|  |  | ||||||
|  |     pub fn jsonStringify(self: *const Writer, w: anytype) !void { | ||||||
|  |         self.toJSON(w) catch |err| { | ||||||
|  |             // The only error our jsonStringify method can return is | ||||||
|  |             // @TypeOf(w).Error. In other words, our code can't return its own | ||||||
|  |             // error, we can only return a writer error. Kinda sucks. | ||||||
|  |             log.err("json stringify: {}", .{err}); | ||||||
|  |             return error.OutOfMemory; | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn toJSON(self: *const Writer, w: anytype) !void { | ||||||
|  |         try w.beginObject(); | ||||||
|  |         try writeCommon(self.node, false, w); | ||||||
|  |  | ||||||
|  |         { | ||||||
|  |             var registry = self.registry; | ||||||
|  |             const child_nodes = try parser.nodeGetChildNodes(self.node._node); | ||||||
|  |             const child_count = try parser.nodeListLength(child_nodes); | ||||||
|  |  | ||||||
|  |             var i: usize = 0; | ||||||
|  |             try w.objectField("children"); | ||||||
|  |             try w.beginArray(); | ||||||
|  |             for (0..child_count) |_| { | ||||||
|  |                 const child = (try parser.nodeListItem(child_nodes, @intCast(i))) orelse break; | ||||||
|  |                 const child_node = try registry.register(child); | ||||||
|  |                 try w.beginObject(); | ||||||
|  |                 try writeCommon(child_node, true, w); | ||||||
|  |                 try w.endObject(); | ||||||
|  |                 i += 1; | ||||||
|  |             } | ||||||
|  |             try w.endArray(); | ||||||
|  |  | ||||||
|  |             try w.objectField("childNodeCount"); | ||||||
|  |             try w.write(i); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         try w.endObject(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn writeCommon(node: *const Node, include_child_count: bool, w: anytype) !void { | ||||||
|  |         try w.objectField("nodeId"); | ||||||
|  |         try w.write(node.id); | ||||||
|  |  | ||||||
|  |         try w.objectField("backendNodeId"); | ||||||
|  |         try w.write(node.id); | ||||||
|  |  | ||||||
|  |         const n = node._node; | ||||||
|  |  | ||||||
|  |         // TODO: | ||||||
|  |         // try w.objectField("parentId"); | ||||||
|  |         // try w.write(pid); | ||||||
|  |  | ||||||
|  |         try w.objectField("nodeType"); | ||||||
|  |         try w.write(@intFromEnum(try parser.nodeType(n))); | ||||||
|  |  | ||||||
|  |         try w.objectField("nodeName"); | ||||||
|  |         try w.write(try parser.nodeName(n)); | ||||||
|  |  | ||||||
|  |         try w.objectField("localName"); | ||||||
|  |         try w.write(try parser.nodeLocalName(n)); | ||||||
|  |  | ||||||
|  |         try w.objectField("nodeValue"); | ||||||
|  |         try w.write((try parser.nodeValue(n)) orelse ""); | ||||||
|  |  | ||||||
|  |         if (include_child_count) { | ||||||
|  |             try w.objectField("childNodeCount"); | ||||||
|  |             const child_nodes = try parser.nodeGetChildNodes(n); | ||||||
|  |             try w.write(try parser.nodeListLength(child_nodes)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         try w.objectField("documentURL"); | ||||||
|  |         try w.write(null); | ||||||
|  |  | ||||||
|  |         try w.objectField("baseURL"); | ||||||
|  |         try w.write(null); | ||||||
|  |  | ||||||
|  |         try w.objectField("xmlVersion"); | ||||||
|  |         try w.write(""); | ||||||
|  |  | ||||||
|  |         try w.objectField("compatibilityMode"); | ||||||
|  |         try w.write("NoQuirksMode"); | ||||||
|  |  | ||||||
|  |         try w.objectField("isScrollable"); | ||||||
|  |         try w.write(false); | ||||||
|  |     } | ||||||
|  | }; | ||||||
|  |  | ||||||
| const testing = @import("testing.zig"); | const testing = @import("testing.zig"); | ||||||
| test "CDP Node: Registry register" { | test "cdp Node: Registry register" { | ||||||
|     var registry = Registry.init(testing.allocator); |     var registry = Registry.init(testing.allocator); | ||||||
|     defer registry.deinit(); |     defer registry.deinit(); | ||||||
|  |  | ||||||
| @@ -291,19 +312,6 @@ test "CDP Node: Registry register" { | |||||||
|         try testing.expectEqual(node, n1c); |         try testing.expectEqual(node, n1c); | ||||||
|  |  | ||||||
|         try testing.expectEqual(0, node.id); |         try testing.expectEqual(0, node.id); | ||||||
|         try testing.expectEqual(null, node.parent_id); |  | ||||||
|         try testing.expectEqual(1, node.node_type); |  | ||||||
|         try testing.expectEqual(0, node.backend_node_id); |  | ||||||
|         try testing.expectEqual("A", node.node_name); |  | ||||||
|         try testing.expectEqual("a", node.local_name); |  | ||||||
|         try testing.expectEqual("", node.node_value); |  | ||||||
|         try testing.expectEqual(1, node.child_node_count); |  | ||||||
|         try testing.expectEqual(0, node.children.len); |  | ||||||
|         try testing.expectEqual(null, node.document_url); |  | ||||||
|         try testing.expectEqual(null, node.base_url); |  | ||||||
|         try testing.expectEqual("", node.xml_version); |  | ||||||
|         try testing.expectEqual(.NoQuirksMode, node.compatibility_mode); |  | ||||||
|         try testing.expectEqual(false, node.is_scrollable); |  | ||||||
|         try testing.expectEqual(n, node._node); |         try testing.expectEqual(n, node._node); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -316,24 +324,11 @@ test "CDP Node: Registry register" { | |||||||
|         try testing.expectEqual(node, n1c); |         try testing.expectEqual(node, n1c); | ||||||
|  |  | ||||||
|         try testing.expectEqual(1, node.id); |         try testing.expectEqual(1, node.id); | ||||||
|         try testing.expectEqual(null, node.parent_id); |  | ||||||
|         try testing.expectEqual(1, node.node_type); |  | ||||||
|         try testing.expectEqual(1, node.backend_node_id); |  | ||||||
|         try testing.expectEqual("P", node.node_name); |  | ||||||
|         try testing.expectEqual("p", node.local_name); |  | ||||||
|         try testing.expectEqual("", node.node_value); |  | ||||||
|         try testing.expectEqual(1, node.child_node_count); |  | ||||||
|         try testing.expectEqual(0, node.children.len); |  | ||||||
|         try testing.expectEqual(null, node.document_url); |  | ||||||
|         try testing.expectEqual(null, node.base_url); |  | ||||||
|         try testing.expectEqual("", node.xml_version); |  | ||||||
|         try testing.expectEqual(.NoQuirksMode, node.compatibility_mode); |  | ||||||
|         try testing.expectEqual(false, node.is_scrollable); |  | ||||||
|         try testing.expectEqual(n, node._node); |         try testing.expectEqual(n, node._node); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| test "CDP Node: search list" { | test "cdp Node: search list" { | ||||||
|     var registry = Registry.init(testing.allocator); |     var registry = Registry.init(testing.allocator); | ||||||
|     defer registry.deinit(); |     defer registry.deinit(); | ||||||
|  |  | ||||||
| @@ -383,3 +378,102 @@ test "CDP Node: search list" { | |||||||
|         try testing.expectEqual(2, registry.lookup_by_node.count()); |         try testing.expectEqual(2, registry.lookup_by_node.count()); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | test "cdp Node: Writer" { | ||||||
|  |     var registry = Registry.init(testing.allocator); | ||||||
|  |     defer registry.deinit(); | ||||||
|  |  | ||||||
|  |     var doc = try testing.Document.init("<a id=a1></a><a id=a2></a>"); | ||||||
|  |     defer doc.deinit(); | ||||||
|  |  | ||||||
|  |     { | ||||||
|  |         const node = try registry.register(doc.asNode()); | ||||||
|  |         const json = try std.json.stringifyAlloc(testing.allocator, Writer{ | ||||||
|  |             .node = node, | ||||||
|  |             .opts = .{}, | ||||||
|  |             .registry = ®istry, | ||||||
|  |         }, .{}); | ||||||
|  |         defer testing.allocator.free(json); | ||||||
|  |  | ||||||
|  |         try testing.expectJson(.{ | ||||||
|  |             .nodeId = 0, | ||||||
|  |             .backendNodeId = 0, | ||||||
|  |             .nodeType = 9, | ||||||
|  |             .nodeName = "#document", | ||||||
|  |             .localName = "", | ||||||
|  |             .nodeValue = "", | ||||||
|  |             .documentURL = null, | ||||||
|  |             .baseURL = null, | ||||||
|  |             .xmlVersion = "", | ||||||
|  |             .isScrollable = false, | ||||||
|  |             .compatibilityMode = "NoQuirksMode", | ||||||
|  |             .childNodeCount = 1, | ||||||
|  |             .children = &.{.{ | ||||||
|  |                 .nodeId = 1, | ||||||
|  |                 .backendNodeId = 1, | ||||||
|  |                 .nodeType = 1, | ||||||
|  |                 .nodeName = "HTML", | ||||||
|  |                 .localName = "html", | ||||||
|  |                 .nodeValue = "", | ||||||
|  |                 .childNodeCount = 2, | ||||||
|  |                 .documentURL = null, | ||||||
|  |                 .baseURL = null, | ||||||
|  |                 .xmlVersion = "", | ||||||
|  |                 .compatibilityMode = "NoQuirksMode", | ||||||
|  |                 .isScrollable = false, | ||||||
|  |             }}, | ||||||
|  |         }, json); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     { | ||||||
|  |         const node = registry.lookup_by_id.get(1).?; | ||||||
|  |         const json = try std.json.stringifyAlloc(testing.allocator, Writer{ | ||||||
|  |             .node = node, | ||||||
|  |             .opts = .{}, | ||||||
|  |             .registry = ®istry, | ||||||
|  |         }, .{}); | ||||||
|  |         defer testing.allocator.free(json); | ||||||
|  |  | ||||||
|  |         try testing.expectJson(.{ | ||||||
|  |             .nodeId = 1, | ||||||
|  |             .backendNodeId = 1, | ||||||
|  |             .nodeType = 1, | ||||||
|  |             .nodeName = "HTML", | ||||||
|  |             .localName = "html", | ||||||
|  |             .nodeValue = "", | ||||||
|  |             .childNodeCount = 2, | ||||||
|  |             .documentURL = null, | ||||||
|  |             .baseURL = null, | ||||||
|  |             .xmlVersion = "", | ||||||
|  |             .compatibilityMode = "NoQuirksMode", | ||||||
|  |             .isScrollable = false, | ||||||
|  |             .children = &.{ .{ | ||||||
|  |                 .nodeId = 2, | ||||||
|  |                 .backendNodeId = 2, | ||||||
|  |                 .nodeType = 1, | ||||||
|  |                 .nodeName = "HEAD", | ||||||
|  |                 .localName = "head", | ||||||
|  |                 .nodeValue = "", | ||||||
|  |                 .childNodeCount = 0, | ||||||
|  |                 .documentURL = null, | ||||||
|  |                 .baseURL = null, | ||||||
|  |                 .xmlVersion = "", | ||||||
|  |                 .compatibilityMode = "NoQuirksMode", | ||||||
|  |                 .isScrollable = false, | ||||||
|  |             }, .{ | ||||||
|  |                 .nodeId = 3, | ||||||
|  |                 .backendNodeId = 3, | ||||||
|  |                 .nodeType = 1, | ||||||
|  |                 .nodeName = "BODY", | ||||||
|  |                 .localName = "body", | ||||||
|  |                 .nodeValue = "", | ||||||
|  |                 .childNodeCount = 2, | ||||||
|  |                 .documentURL = null, | ||||||
|  |                 .baseURL = null, | ||||||
|  |                 .xmlVersion = "", | ||||||
|  |                 .compatibilityMode = "NoQuirksMode", | ||||||
|  |                 .isScrollable = false, | ||||||
|  |             } }, | ||||||
|  |         }, json); | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -325,6 +325,14 @@ pub fn BrowserContext(comptime CDP_T: type) type { | |||||||
|             self.node_search_list.reset(); |             self.node_search_list.reset(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         pub fn nodeWriter(self: *Self, node: *const Node, opts: Node.Writer.Opts) Node.Writer { | ||||||
|  |             return .{ | ||||||
|  |                 .node = node, | ||||||
|  |                 .opts = opts, | ||||||
|  |                 .registry = &self.node_registry, | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void { |         pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void { | ||||||
|             if (std.log.defaultLogEnabled(.debug)) { |             if (std.log.defaultLogEnabled(.debug)) { | ||||||
|                 // msg should be {"id":<id>,... |                 // msg should be {"id":<id>,... | ||||||
|   | |||||||
| @@ -51,9 +51,7 @@ fn getDocument(cmd: anytype) !void { | |||||||
|     const doc = page.doc orelse return error.DocumentNotLoaded; |     const doc = page.doc orelse return error.DocumentNotLoaded; | ||||||
|  |  | ||||||
|     const node = try bc.node_registry.register(parser.documentToNode(doc)); |     const node = try bc.node_registry.register(parser.documentToNode(doc)); | ||||||
|     return cmd.sendResult(.{ |     return cmd.sendResult(.{ .root = bc.nodeWriter(node, .{}) }, .{}); | ||||||
|         .root = node, |  | ||||||
|     }, .{}); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch | // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch | ||||||
| @@ -118,6 +116,7 @@ fn getSearchResults(cmd: anytype) !void { | |||||||
| } | } | ||||||
|  |  | ||||||
| const testing = @import("../testing.zig"); | const testing = @import("../testing.zig"); | ||||||
|  |  | ||||||
| test "cdp.dom: getSearchResults unknown search id" { | test "cdp.dom: getSearchResults unknown search id" { | ||||||
|     var ctx = testing.context(); |     var ctx = testing.context(); | ||||||
|     defer ctx.deinit(); |     defer ctx.deinit(); | ||||||
|   | |||||||
| @@ -26,11 +26,12 @@ const main = @import("cdp.zig"); | |||||||
| const parser = @import("netsurf"); | const parser = @import("netsurf"); | ||||||
| const App = @import("../app.zig").App; | const App = @import("../app.zig").App; | ||||||
|  |  | ||||||
| pub const allocator = @import("../testing.zig").allocator; | const base = @import("../testing.zig"); | ||||||
|  | pub const allocator = base.allocator; | ||||||
| pub const expectEqual = @import("../testing.zig").expectEqual; | pub const expectJson = base.expectJson; | ||||||
| pub const expectError = @import("../testing.zig").expectError; | pub const expectEqual = base.expectEqual; | ||||||
| pub const expectEqualSlices = @import("../testing.zig").expectEqualSlices; | pub const expectError = base.expectError; | ||||||
|  | pub const expectEqualSlices = base.expectEqualSlices; | ||||||
|  |  | ||||||
| pub const Document = @import("../testing.zig").Document; | pub const Document = @import("../testing.zig").Document; | ||||||
|  |  | ||||||
| @@ -284,6 +285,7 @@ const TestContext = struct { | |||||||
|             _ = self.client.?.serialized.orderedRemove(i); |             _ = self.client.?.serialized.orderedRemove(i); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         std.debug.print("Error not found. Expecting:\n{s}\n\nGot:\n", .{serialized}); |         std.debug.print("Error not found. Expecting:\n{s}\n\nGot:\n", .{serialized}); | ||||||
|         for (self.client.?.serialized.items, 0..) |sent, i| { |         for (self.client.?.serialized.items, 0..) |sent, i| { | ||||||
|             std.debug.print("#{d}\n{s}\n\n", .{ i, sent }); |             std.debug.print("#{d}\n{s}\n\n", .{ i, sent }); | ||||||
| @@ -310,47 +312,5 @@ pub fn context() TestContext { | |||||||
| fn compareExpectedToSent(expected: []const u8, actual: json.Value) !bool { | fn compareExpectedToSent(expected: []const u8, actual: json.Value) !bool { | ||||||
|     const expected_value = try std.json.parseFromSlice(json.Value, std.testing.allocator, expected, .{}); |     const expected_value = try std.json.parseFromSlice(json.Value, std.testing.allocator, expected, .{}); | ||||||
|     defer expected_value.deinit(); |     defer expected_value.deinit(); | ||||||
|     return compareJsonValues(expected_value.value, actual); |     return base.isEqualJson(expected_value.value, actual); | ||||||
| } |  | ||||||
|  |  | ||||||
| fn compareJsonValues(a: std.json.Value, b: std.json.Value) bool { |  | ||||||
|     if (!std.mem.eql(u8, @tagName(a), @tagName(b))) { |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     switch (a) { |  | ||||||
|         .null => return true, |  | ||||||
|         .bool => return a.bool == b.bool, |  | ||||||
|         .integer => return a.integer == b.integer, |  | ||||||
|         .float => return a.float == b.float, |  | ||||||
|         .number_string => return std.mem.eql(u8, a.number_string, b.number_string), |  | ||||||
|         .string => return std.mem.eql(u8, a.string, b.string), |  | ||||||
|         .array => { |  | ||||||
|             const a_len = a.array.items.len; |  | ||||||
|             const b_len = b.array.items.len; |  | ||||||
|             if (a_len != b_len) { |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
|             for (a.array.items, b.array.items) |a_item, b_item| { |  | ||||||
|                 if (compareJsonValues(a_item, b_item) == false) { |  | ||||||
|                     return false; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             return true; |  | ||||||
|         }, |  | ||||||
|         .object => { |  | ||||||
|             var it = a.object.iterator(); |  | ||||||
|             while (it.next()) |entry| { |  | ||||||
|                 const key = entry.key_ptr.*; |  | ||||||
|                 if (b.object.get(key)) |b_item| { |  | ||||||
|                     if (compareJsonValues(entry.value_ptr.*, b_item) == false) { |  | ||||||
|                         return false; |  | ||||||
|                     } |  | ||||||
|                 } else { |  | ||||||
|                     return false; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             return true; |  | ||||||
|         }, |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										130
									
								
								src/testing.zig
									
									
									
									
									
								
							
							
						
						
									
										130
									
								
								src/testing.zig
									
									
									
									
									
								
							| @@ -17,14 +17,15 @@ | |||||||
| // along with this program.  If not, see <https://www.gnu.org/licenses/>. | // along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
| const std = @import("std"); | const std = @import("std"); | ||||||
| const parser = @import("netsurf"); |  | ||||||
|  |  | ||||||
|  | const parser = @import("netsurf"); | ||||||
| pub const allocator = std.testing.allocator; | pub const allocator = std.testing.allocator; | ||||||
| pub const expectError = std.testing.expectError; | pub const expectError = std.testing.expectError; | ||||||
| pub const expectString = std.testing.expectEqualStrings; | pub const expectString = std.testing.expectEqualStrings; | ||||||
| pub const expectEqualSlices = std.testing.expectEqualSlices; | pub const expectEqualSlices = std.testing.expectEqualSlices; | ||||||
|  |  | ||||||
| const App = @import("app.zig").App; | const App = @import("app.zig").App; | ||||||
|  | const Allocator = std.mem.Allocator; | ||||||
|  |  | ||||||
| // Merged std.testing.expectEqual and std.testing.expectString | // Merged std.testing.expectEqual and std.testing.expectString | ||||||
| // can be useful when testing fields of an anytype an you don't know | // can be useful when testing fields of an anytype an you don't know | ||||||
| @@ -217,12 +218,135 @@ pub const Document = struct { | |||||||
|  |  | ||||||
|     pub fn querySelectorAll(self: *Document, selector: []const u8) ![]const *parser.Node { |     pub fn querySelectorAll(self: *Document, selector: []const u8) ![]const *parser.Node { | ||||||
|         const css = @import("dom/css.zig"); |         const css = @import("dom/css.zig"); | ||||||
|         const node_list = try css.querySelectorAll(self.arena.allocator(), parser.documentToNode(self.doc), selector); |         const node_list = try css.querySelectorAll(self.arena.allocator(), self.asNode(), selector); | ||||||
|         return node_list.nodes.items; |         return node_list.nodes.items; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn querySelector(self: *Document, selector: []const u8) !?*parser.Node { |     pub fn querySelector(self: *Document, selector: []const u8) !?*parser.Node { | ||||||
|         const css = @import("dom/css.zig"); |         const css = @import("dom/css.zig"); | ||||||
|         return css.querySelector(self.arena.allocator(), parser.documentToNode(self.doc), selector); |         return css.querySelector(self.arena.allocator(), self.asNode(), selector); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub fn asNode(self: *const Document) *parser.Node { | ||||||
|  |         return parser.documentToNode(self.doc); | ||||||
|     } |     } | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | pub fn expectJson(a: anytype, b: anytype) !void { | ||||||
|  |     var arena = std.heap.ArenaAllocator.init(allocator); | ||||||
|  |     defer arena.deinit(); | ||||||
|  |  | ||||||
|  |     const aa = arena.allocator(); | ||||||
|  |  | ||||||
|  |     const a_value = try convertToJson(aa, a); | ||||||
|  |     const b_value = try convertToJson(aa, b); | ||||||
|  |  | ||||||
|  |     errdefer { | ||||||
|  |         const a_json = std.json.stringifyAlloc(aa, a_value, .{ .whitespace = .indent_2 }) catch unreachable; | ||||||
|  |         const b_json = std.json.stringifyAlloc(aa, b_value, .{ .whitespace = .indent_2 }) catch unreachable; | ||||||
|  |         std.debug.print("== Expected ==\n{s}\n\n== Actual ==\n{s}", .{ a_json, b_json }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     try expectJsonValue(a_value, b_value); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn isEqualJson(a: anytype, b: anytype) !bool { | ||||||
|  |     var arena = std.heap.ArenaAllocator.init(allocator); | ||||||
|  |     defer arena.deinit(); | ||||||
|  |  | ||||||
|  |     const aa = arena.allocator(); | ||||||
|  |     const a_value = try convertToJson(aa, a); | ||||||
|  |     const b_value = try convertToJson(aa, b); | ||||||
|  |     return isJsonValue(a_value, b_value); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn convertToJson(arena: Allocator, value: anytype) !std.json.Value { | ||||||
|  |     const T = @TypeOf(value); | ||||||
|  |     if (T == std.json.Value) { | ||||||
|  |         return value; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     var str: []const u8 = undefined; | ||||||
|  |     if (T == []u8 or T == []const u8 or comptime isStringArray(T)) { | ||||||
|  |         str = value; | ||||||
|  |     } else { | ||||||
|  |         str = try std.json.stringifyAlloc(arena, value, .{}); | ||||||
|  |     } | ||||||
|  |     return std.json.parseFromSliceLeaky(std.json.Value, arena, str, .{}); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn expectJsonValue(a: std.json.Value, b: std.json.Value) !void { | ||||||
|  |     try expectEqual(@tagName(a), @tagName(b)); | ||||||
|  |  | ||||||
|  |     // at this point, we know that if a is an int, b must also be an int | ||||||
|  |     switch (a) { | ||||||
|  |         .null => return, | ||||||
|  |         .bool => try expectEqual(a.bool, b.bool), | ||||||
|  |         .integer => try expectEqual(a.integer, b.integer), | ||||||
|  |         .float => try expectEqual(a.float, b.float), | ||||||
|  |         .number_string => try expectEqual(a.number_string, b.number_string), | ||||||
|  |         .string => try expectEqual(a.string, b.string), | ||||||
|  |         .array => { | ||||||
|  |             const a_len = a.array.items.len; | ||||||
|  |             const b_len = b.array.items.len; | ||||||
|  |             try expectEqual(a_len, b_len); | ||||||
|  |             for (a.array.items, b.array.items) |a_item, b_item| { | ||||||
|  |                 try expectJsonValue(a_item, b_item); | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         .object => { | ||||||
|  |             var it = a.object.iterator(); | ||||||
|  |             while (it.next()) |entry| { | ||||||
|  |                 const key = entry.key_ptr.*; | ||||||
|  |                 if (b.object.get(key)) |b_item| { | ||||||
|  |                     try expectJsonValue(entry.value_ptr.*, b_item); | ||||||
|  |                 } else { | ||||||
|  |                     return error.MissingKey; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn isJsonValue(a: std.json.Value, b: std.json.Value) bool { | ||||||
|  |     if (std.mem.eql(u8, @tagName(a), @tagName(b)) == false) { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // at this point, we know that if a is an int, b must also be an int | ||||||
|  |     switch (a) { | ||||||
|  |         .null => return true, | ||||||
|  |         .bool => return a.bool == b.bool, | ||||||
|  |         .integer => return a.integer == b.integer, | ||||||
|  |         .float => return a.float == b.float, | ||||||
|  |         .number_string => return std.mem.eql(u8, a.number_string, b.number_string), | ||||||
|  |         .string => return std.mem.eql(u8, a.string, b.string), | ||||||
|  |         .array => { | ||||||
|  |             const a_len = a.array.items.len; | ||||||
|  |             const b_len = b.array.items.len; | ||||||
|  |             if (a_len != b_len) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |             for (a.array.items, b.array.items) |a_item, b_item| { | ||||||
|  |                 if (isJsonValue(a_item, b_item) == false) { | ||||||
|  |                     return false; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             return true; | ||||||
|  |         }, | ||||||
|  |         .object => { | ||||||
|  |             var it = a.object.iterator(); | ||||||
|  |             while (it.next()) |entry| { | ||||||
|  |                 const key = entry.key_ptr.*; | ||||||
|  |                 if (b.object.get(key)) |b_item| { | ||||||
|  |                     if (isJsonValue(entry.value_ptr.*, b_item) == false) { | ||||||
|  |                         return false; | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     return false; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             return true; | ||||||
|  |         }, | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Pierre Tachoire
					Pierre Tachoire