mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 15:13:28 +00:00
Merge pull request #495 from lightpanda-io/cdp_node
Some checks failed
e2e-test / zig build release (push) Has been cancelled
wpt / web platform tests (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
Some checks failed
e2e-test / zig build release (push) Has been cancelled
wpt / web platform tests (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
Add CDP Node Registry
This commit is contained in:
385
src/cdp/Node.zig
Normal file
385
src/cdp/Node.zig
Normal file
@@ -0,0 +1,385 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const parser = @import("netsurf");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const Id = u32;
|
||||
|
||||
const Node = @This();
|
||||
|
||||
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,
|
||||
|
||||
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.
|
||||
// We maintain a node -> id and id -> node lookup.
|
||||
pub const Registry = struct {
|
||||
node_id: u32,
|
||||
allocator: Allocator,
|
||||
node_pool: std.heap.MemoryPool(Node),
|
||||
lookup_by_id: std.AutoHashMapUnmanaged(Id, *Node),
|
||||
lookup_by_node: std.HashMapUnmanaged(*parser.Node, *Node, NodeContext, std.hash_map.default_max_load_percentage),
|
||||
|
||||
pub fn init(allocator: Allocator) Registry {
|
||||
return .{
|
||||
.node_id = 0,
|
||||
.allocator = allocator,
|
||||
.lookup_by_id = .{},
|
||||
.lookup_by_node = .{},
|
||||
.node_pool = std.heap.MemoryPool(Node).init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Registry) void {
|
||||
const allocator = self.allocator;
|
||||
self.lookup_by_id.deinit(allocator);
|
||||
self.lookup_by_node.deinit(allocator);
|
||||
self.node_pool.deinit();
|
||||
}
|
||||
|
||||
pub fn reset(self: *Registry) void {
|
||||
self.lookup_by_id.clearRetainingCapacity();
|
||||
self.lookup_by_node.clearRetainingCapacity();
|
||||
_ = self.node_pool.reset(.{ .retain_capacity = {} });
|
||||
}
|
||||
|
||||
pub fn register(self: *Registry, n: *parser.Node) !*Node {
|
||||
const node_lookup_gop = try self.lookup_by_node.getOrPut(self.allocator, n);
|
||||
if (node_lookup_gop.found_existing) {
|
||||
return node_lookup_gop.value_ptr.*;
|
||||
}
|
||||
|
||||
// on error, we're probably going to abort the entire browser context
|
||||
// but, just in case, let's try to keep things tidy.
|
||||
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();
|
||||
errdefer self.node_pool.destroy(node);
|
||||
|
||||
node.* = .{
|
||||
._node = n,
|
||||
.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;
|
||||
try self.lookup_by_id.putNoClobber(self.allocator, id, node);
|
||||
return node;
|
||||
}
|
||||
};
|
||||
|
||||
const NodeContext = struct {
|
||||
pub fn hash(_: NodeContext, n: *parser.Node) u64 {
|
||||
return std.hash.Wyhash.hash(0, std.mem.asBytes(&@intFromPtr(n)));
|
||||
}
|
||||
|
||||
pub fn eql(_: NodeContext, a: *parser.Node, b: *parser.Node) bool {
|
||||
return @intFromPtr(a) == @intFromPtr(b);
|
||||
}
|
||||
};
|
||||
|
||||
// Searches are a 3 step process:
|
||||
// 1 - Dom.performSearch
|
||||
// 2 - Dom.getSearchResults
|
||||
// 3 - Dom.discardSearchResults
|
||||
//
|
||||
// For a given browser context, we can have multiple active searches. I.e.
|
||||
// performSearch could be called multiple times without getSearchResults or
|
||||
// discardSearchResults being called. We keep these active searches in the
|
||||
// browser context's node_search_list, which is a SearchList. Since we don't
|
||||
// expect many active searches (mostly just 1), a list is fine to scan through.
|
||||
pub const Search = struct {
|
||||
name: []const u8,
|
||||
node_ids: []const Id,
|
||||
|
||||
pub const List = struct {
|
||||
registry: *Registry,
|
||||
search_id: u16 = 0,
|
||||
arena: std.heap.ArenaAllocator,
|
||||
searches: std.ArrayListUnmanaged(Search) = .{},
|
||||
|
||||
pub fn init(allocator: Allocator, registry: *Registry) List {
|
||||
return .{
|
||||
.registry = registry,
|
||||
.arena = std.heap.ArenaAllocator.init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *List) void {
|
||||
self.arena.deinit();
|
||||
}
|
||||
|
||||
pub fn reset(self: *List) void {
|
||||
self.search_id = 0;
|
||||
self.searches = .{};
|
||||
_ = self.arena.reset(.{ .retain_with_limit = 4096 });
|
||||
}
|
||||
|
||||
pub fn create(self: *List, nodes: []const *parser.Node) !Search {
|
||||
const id = self.search_id;
|
||||
defer self.search_id = id +% 1;
|
||||
|
||||
const arena = self.arena.allocator();
|
||||
|
||||
const name = switch (id) {
|
||||
0 => "0",
|
||||
1 => "1",
|
||||
2 => "2",
|
||||
3 => "3",
|
||||
4 => "4",
|
||||
5 => "5",
|
||||
6 => "6",
|
||||
7 => "7",
|
||||
8 => "8",
|
||||
9 => "9",
|
||||
else => try std.fmt.allocPrint(arena, "{d}", .{id}),
|
||||
};
|
||||
|
||||
var registry = self.registry;
|
||||
const node_ids = try arena.alloc(Id, nodes.len);
|
||||
for (nodes, node_ids) |node, *node_id| {
|
||||
node_id.* = (try registry.register(node)).id;
|
||||
}
|
||||
|
||||
const search = Search{
|
||||
.name = name,
|
||||
.node_ids = node_ids,
|
||||
};
|
||||
try self.searches.append(arena, search);
|
||||
return search;
|
||||
}
|
||||
|
||||
pub fn remove(self: *List, name: []const u8) void {
|
||||
for (self.searches.items, 0..) |search, i| {
|
||||
if (std.mem.eql(u8, name, search.name)) {
|
||||
_ = self.searches.swapRemove(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(self: *const List, name: []const u8) ?Search {
|
||||
for (self.searches.items) |search| {
|
||||
if (std.mem.eql(u8, name, search.name)) {
|
||||
return search;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const testing = @import("testing.zig");
|
||||
test "CDP Node: Registry register" {
|
||||
var registry = Registry.init(testing.allocator);
|
||||
defer registry.deinit();
|
||||
|
||||
try testing.expectEqual(0, registry.lookup_by_id.count());
|
||||
try testing.expectEqual(0, registry.lookup_by_node.count());
|
||||
|
||||
var doc = try testing.Document.init("<a id=a1>link1</a><div id=d2><p>other</p></div>");
|
||||
defer doc.deinit();
|
||||
|
||||
{
|
||||
const n = (try doc.querySelector("#a1")).?;
|
||||
const node = try registry.register(n);
|
||||
const n1b = registry.lookup_by_id.get(0).?;
|
||||
const n1c = registry.lookup_by_node.get(node._node).?;
|
||||
try testing.expectEqual(node, n1b);
|
||||
try testing.expectEqual(node, n1c);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
{
|
||||
const n = (try doc.querySelector("p")).?;
|
||||
const node = try registry.register(n);
|
||||
const n1b = registry.lookup_by_id.get(1).?;
|
||||
const n1c = registry.lookup_by_node.get(node._node).?;
|
||||
try testing.expectEqual(node, n1b);
|
||||
try testing.expectEqual(node, n1c);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
test "CDP Node: search list" {
|
||||
var registry = Registry.init(testing.allocator);
|
||||
defer registry.deinit();
|
||||
|
||||
var search_list = Search.List.init(testing.allocator, ®istry);
|
||||
defer search_list.deinit();
|
||||
|
||||
{
|
||||
// empty search list, noops
|
||||
search_list.remove("0");
|
||||
try testing.expectEqual(null, search_list.get("0"));
|
||||
}
|
||||
|
||||
{
|
||||
// empty nodes
|
||||
const s1 = try search_list.create(&.{});
|
||||
try testing.expectEqual("0", s1.name);
|
||||
try testing.expectEqual(0, s1.node_ids.len);
|
||||
|
||||
const s2 = search_list.get("0").?;
|
||||
try testing.expectEqual("0", s2.name);
|
||||
try testing.expectEqual(0, s2.node_ids.len);
|
||||
|
||||
search_list.remove("0");
|
||||
try testing.expectEqual(null, search_list.get("0"));
|
||||
}
|
||||
|
||||
{
|
||||
var doc = try testing.Document.init("<a id=a1></a><a id=a2></a>");
|
||||
defer doc.deinit();
|
||||
|
||||
const s1 = try search_list.create(try doc.querySelectorAll("a"));
|
||||
try testing.expectEqual("1", s1.name);
|
||||
try testing.expectEqualSlices(u32, &.{ 0, 1 }, s1.node_ids);
|
||||
|
||||
try testing.expectEqual(2, registry.lookup_by_id.count());
|
||||
try testing.expectEqual(2, registry.lookup_by_node.count());
|
||||
|
||||
const s2 = try search_list.create(try doc.querySelectorAll("#a1"));
|
||||
try testing.expectEqual("2", s2.name);
|
||||
try testing.expectEqualSlices(u32, &.{0}, s2.node_ids);
|
||||
|
||||
const s3 = try search_list.create(try doc.querySelectorAll("#a2"));
|
||||
try testing.expectEqual("3", s3.name);
|
||||
try testing.expectEqualSlices(u32, &.{1}, s3.node_ids);
|
||||
|
||||
try testing.expectEqual(2, registry.lookup_by_id.count());
|
||||
try testing.expectEqual(2, registry.lookup_by_node.count());
|
||||
}
|
||||
}
|
||||
@@ -29,10 +29,6 @@ const log = std.log.scoped(.cdp);
|
||||
pub const URL_BASE = "chrome://newtab/";
|
||||
pub const LOADER_ID = "LOADERID24DD2FD56CF1EF33C965C79C";
|
||||
|
||||
pub const TimestampEvent = struct {
|
||||
timestamp: f64,
|
||||
};
|
||||
|
||||
pub const CDP = CDPT(struct {
|
||||
const Client = *@import("../server.zig").Client;
|
||||
const Browser = @import("../browser/browser.zig").Browser;
|
||||
@@ -176,40 +172,40 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
||||
|
||||
switch (domain.len) {
|
||||
3 => switch (@as(u24, @bitCast(domain[0..3].*))) {
|
||||
asUint("DOM") => return @import("dom.zig").processMessage(command),
|
||||
asUint("Log") => return @import("log.zig").processMessage(command),
|
||||
asUint("CSS") => return @import("css.zig").processMessage(command),
|
||||
asUint("DOM") => return @import("domains/dom.zig").processMessage(command),
|
||||
asUint("Log") => return @import("domains/log.zig").processMessage(command),
|
||||
asUint("CSS") => return @import("domains/css.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
4 => switch (@as(u32, @bitCast(domain[0..4].*))) {
|
||||
asUint("Page") => return @import("page.zig").processMessage(command),
|
||||
asUint("Page") => return @import("domains/page.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
5 => switch (@as(u40, @bitCast(domain[0..5].*))) {
|
||||
asUint("Fetch") => return @import("fetch.zig").processMessage(command),
|
||||
asUint("Fetch") => return @import("domains/fetch.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
6 => switch (@as(u48, @bitCast(domain[0..6].*))) {
|
||||
asUint("Target") => return @import("target.zig").processMessage(command),
|
||||
asUint("Target") => return @import("domains/target.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
7 => switch (@as(u56, @bitCast(domain[0..7].*))) {
|
||||
asUint("Browser") => return @import("browser.zig").processMessage(command),
|
||||
asUint("Runtime") => return @import("runtime.zig").processMessage(command),
|
||||
asUint("Network") => return @import("network.zig").processMessage(command),
|
||||
asUint("Browser") => return @import("domains/browser.zig").processMessage(command),
|
||||
asUint("Runtime") => return @import("domains/runtime.zig").processMessage(command),
|
||||
asUint("Network") => return @import("domains/network.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
8 => switch (@as(u64, @bitCast(domain[0..8].*))) {
|
||||
asUint("Security") => return @import("security.zig").processMessage(command),
|
||||
asUint("Security") => return @import("domains/security.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
9 => switch (@as(u72, @bitCast(domain[0..9].*))) {
|
||||
asUint("Emulation") => return @import("emulation.zig").processMessage(command),
|
||||
asUint("Inspector") => return @import("inspector.zig").processMessage(command),
|
||||
asUint("Emulation") => return @import("domains/emulation.zig").processMessage(command),
|
||||
asUint("Inspector") => return @import("domains/inspector.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
11 => switch (@as(u88, @bitCast(domain[0..11].*))) {
|
||||
asUint("Performance") => return @import("performance.zig").processMessage(command),
|
||||
asUint("Performance") => return @import("domains/performance.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
else => {},
|
||||
@@ -258,7 +254,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
||||
}
|
||||
|
||||
pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
const dom = @import("dom.zig");
|
||||
const Node = @import("Node.zig");
|
||||
|
||||
return struct {
|
||||
id: []const u8,
|
||||
@@ -291,12 +287,17 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
security_origin: []const u8,
|
||||
page_life_cycle_events: bool,
|
||||
secure_context_type: []const u8,
|
||||
node_list: dom.NodeList,
|
||||
node_search_list: dom.NodeSearchList,
|
||||
node_registry: Node.Registry,
|
||||
node_search_list: Node.Search.List,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
fn init(self: *Self, id: []const u8, cdp: *CDP_T) !void {
|
||||
const allocator = cdp.allocator;
|
||||
|
||||
var registry = Node.Registry.init(allocator);
|
||||
errdefer registry.deinit();
|
||||
|
||||
self.* = .{
|
||||
.id = id,
|
||||
.cdp = cdp,
|
||||
@@ -308,27 +309,20 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
.loader_id = LOADER_ID,
|
||||
.session = try cdp.browser.newSession(self),
|
||||
.page_life_cycle_events = false, // TODO; Target based value
|
||||
.node_list = dom.NodeList.init(cdp.allocator),
|
||||
.node_search_list = dom.NodeSearchList.init(cdp.allocator),
|
||||
.node_registry = registry,
|
||||
.node_search_list = undefined,
|
||||
};
|
||||
self.node_search_list = Node.Search.List.init(allocator, &self.node_registry);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.node_list.deinit();
|
||||
for (self.node_search_list.items) |*s| {
|
||||
s.deinit();
|
||||
}
|
||||
self.node_registry.deinit();
|
||||
self.node_search_list.deinit();
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
self.node_list.reset();
|
||||
|
||||
// deinit all node searches.
|
||||
for (self.node_search_list.items) |*s| {
|
||||
s.deinit();
|
||||
}
|
||||
self.node_search_list.clearAndFree();
|
||||
self.node_registry.reset();
|
||||
self.node_search_list.reset();
|
||||
}
|
||||
|
||||
pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void {
|
||||
|
||||
259
src/cdp/dom.zig
259
src/cdp/dom.zig
@@ -1,259 +0,0 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const cdp = @import("cdp.zig");
|
||||
const css = @import("../dom/css.zig");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
enable,
|
||||
getDocument,
|
||||
performSearch,
|
||||
getSearchResults,
|
||||
discardSearchResults,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.enable => return cmd.sendResult(null, .{}),
|
||||
.getDocument => return getDocument(cmd),
|
||||
.performSearch => return performSearch(cmd),
|
||||
.getSearchResults => return getSearchResults(cmd),
|
||||
.discardSearchResults => return discardSearchResults(cmd),
|
||||
}
|
||||
}
|
||||
|
||||
// NodeList references tree nodes with an array id.
|
||||
pub const NodeList = struct {
|
||||
coll: List,
|
||||
|
||||
const List = std.ArrayList(*parser.Node);
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) NodeList {
|
||||
return .{
|
||||
.coll = List.init(alloc),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *NodeList) void {
|
||||
self.coll.deinit();
|
||||
}
|
||||
|
||||
pub fn reset(self: *NodeList) void {
|
||||
self.coll.clearAndFree();
|
||||
}
|
||||
|
||||
pub fn set(self: *NodeList, node: *parser.Node) !NodeId {
|
||||
const coll = &self.coll;
|
||||
for (coll.items, 0..) |n, i| {
|
||||
if (n == node) {
|
||||
return @intCast(i);
|
||||
}
|
||||
}
|
||||
|
||||
try coll.append(node);
|
||||
return @intCast(coll.items.len);
|
||||
}
|
||||
};
|
||||
|
||||
const NodeId = u32;
|
||||
|
||||
const Node = struct {
|
||||
nodeId: NodeId,
|
||||
parentId: ?NodeId = null,
|
||||
backendNodeId: NodeId,
|
||||
nodeType: u32,
|
||||
nodeName: []const u8 = "",
|
||||
localName: []const u8 = "",
|
||||
nodeValue: []const u8 = "",
|
||||
childNodeCount: ?u32 = null,
|
||||
children: ?[]const Node = null,
|
||||
documentURL: ?[]const u8 = null,
|
||||
baseURL: ?[]const u8 = null,
|
||||
xmlVersion: []const u8 = "",
|
||||
compatibilityMode: []const u8 = "NoQuirksMode",
|
||||
isScrollable: bool = false,
|
||||
|
||||
fn init(n: *parser.Node, nlist: *NodeList) !Node {
|
||||
const id = try nlist.set(n);
|
||||
return .{
|
||||
.nodeId = id,
|
||||
.backendNodeId = id,
|
||||
.nodeType = @intFromEnum(try parser.nodeType(n)),
|
||||
.nodeName = try parser.nodeName(n),
|
||||
.localName = try parser.nodeLocalName(n),
|
||||
.nodeValue = try parser.nodeValue(n) orelse "",
|
||||
};
|
||||
}
|
||||
|
||||
fn initChildren(
|
||||
self: *Node,
|
||||
alloc: std.mem.Allocator,
|
||||
n: *parser.Node,
|
||||
nlist: *NodeList,
|
||||
) !std.ArrayList(Node) {
|
||||
const children = try parser.nodeGetChildNodes(n);
|
||||
const ln = try parser.nodeListLength(children);
|
||||
self.childNodeCount = ln;
|
||||
|
||||
var list = try std.ArrayList(Node).initCapacity(alloc, ln);
|
||||
|
||||
for (0..ln) |i| {
|
||||
const child = try parser.nodeListItem(children, @intCast(i)) orelse continue;
|
||||
list.appendAssumeCapacity(try Node.init(child, nlist));
|
||||
}
|
||||
|
||||
self.children = list.items;
|
||||
|
||||
return list;
|
||||
}
|
||||
};
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getDocument
|
||||
fn getDocument(cmd: anytype) !void {
|
||||
// const params = (try cmd.params(struct {
|
||||
// depth: ?u32 = null,
|
||||
// pierce: ?bool = null,
|
||||
// })) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
const doc = page.doc orelse return error.DocumentNotLoaded;
|
||||
|
||||
const node = parser.documentToNode(doc);
|
||||
var n = try Node.init(node, &bc.node_list);
|
||||
_ = try n.initChildren(cmd.arena, node, &bc.node_list);
|
||||
|
||||
return cmd.sendResult(.{
|
||||
.root = n,
|
||||
}, .{});
|
||||
}
|
||||
|
||||
pub const NodeSearch = struct {
|
||||
coll: List,
|
||||
name: []u8,
|
||||
alloc: std.mem.Allocator,
|
||||
|
||||
var count: u8 = 0;
|
||||
|
||||
const List = std.ArrayListUnmanaged(NodeId);
|
||||
|
||||
pub fn initCapacity(alloc: std.mem.Allocator, ln: usize) !NodeSearch {
|
||||
count += 1;
|
||||
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
.coll = try List.initCapacity(alloc, ln),
|
||||
.name = try std.fmt.allocPrint(alloc, "{d}", .{count}),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *NodeSearch) void {
|
||||
self.coll.deinit(self.alloc);
|
||||
self.alloc.free(self.name);
|
||||
}
|
||||
|
||||
pub fn append(self: *NodeSearch, id: NodeId) !void {
|
||||
try self.coll.append(self.alloc, id);
|
||||
}
|
||||
};
|
||||
pub const NodeSearchList = std.ArrayList(NodeSearch);
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch
|
||||
fn performSearch(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
query: []const u8,
|
||||
includeUserAgentShadowDOM: ?bool = null,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
const doc = page.doc orelse return error.DocumentNotLoaded;
|
||||
|
||||
const list = try css.querySelectorAll(cmd.cdp.allocator, parser.documentToNode(doc), params.query);
|
||||
const ln = list.nodes.items.len;
|
||||
var ns = try NodeSearch.initCapacity(cmd.cdp.allocator, ln);
|
||||
|
||||
for (list.nodes.items) |n| {
|
||||
const id = try bc.node_list.set(n);
|
||||
try ns.append(id);
|
||||
}
|
||||
|
||||
try bc.node_search_list.append(ns);
|
||||
|
||||
return cmd.sendResult(.{
|
||||
.searchId = ns.name,
|
||||
.resultCount = @as(u32, @intCast(ln)),
|
||||
}, .{});
|
||||
}
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-discardSearchResults
|
||||
fn discardSearchResults(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
searchId: []const u8,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
|
||||
// retrieve the search from context
|
||||
for (bc.node_search_list.items, 0..) |*s, i| {
|
||||
if (!std.mem.eql(u8, s.name, params.searchId)) continue;
|
||||
|
||||
s.deinit();
|
||||
_ = bc.node_search_list.swapRemove(i);
|
||||
break;
|
||||
}
|
||||
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getSearchResults
|
||||
fn getSearchResults(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
searchId: []const u8,
|
||||
fromIndex: u32,
|
||||
toIndex: u32,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
if (params.fromIndex >= params.toIndex) {
|
||||
return error.BadIndices;
|
||||
}
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
|
||||
// retrieve the search from context
|
||||
var ns: ?*const NodeSearch = undefined;
|
||||
for (bc.node_search_list.items) |s| {
|
||||
if (!std.mem.eql(u8, s.name, params.searchId)) continue;
|
||||
ns = &s;
|
||||
break;
|
||||
}
|
||||
|
||||
if (ns == null) {
|
||||
return error.searchResultNotFound;
|
||||
}
|
||||
|
||||
const items = ns.?.coll.items;
|
||||
|
||||
if (params.fromIndex >= items.len) return error.BadFromIndex;
|
||||
if (params.toIndex > items.len) return error.BadToIndex;
|
||||
|
||||
return cmd.sendResult(.{ .nodeIds = ns.?.coll.items[params.fromIndex..params.toIndex] }, .{});
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const cdp = @import("cdp.zig");
|
||||
|
||||
// TODO: hard coded data
|
||||
const PROTOCOL_VERSION = "1.3";
|
||||
@@ -81,7 +80,7 @@ fn setWindowBounds(cmd: anytype) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
const testing = @import("testing.zig");
|
||||
const testing = @import("../testing.zig");
|
||||
test "cdp.browser: getVersion" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
@@ -17,7 +17,6 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const cdp = @import("cdp.zig");
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
184
src/cdp/domains/dom.zig
Normal file
184
src/cdp/domains/dom.zig
Normal file
@@ -0,0 +1,184 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const parser = @import("netsurf");
|
||||
const Node = @import("../Node.zig");
|
||||
const css = @import("../../dom/css.zig");
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
enable,
|
||||
getDocument,
|
||||
performSearch,
|
||||
getSearchResults,
|
||||
discardSearchResults,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.enable => return cmd.sendResult(null, .{}),
|
||||
.getDocument => return getDocument(cmd),
|
||||
.performSearch => return performSearch(cmd),
|
||||
.getSearchResults => return getSearchResults(cmd),
|
||||
.discardSearchResults => return discardSearchResults(cmd),
|
||||
}
|
||||
}
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getDocument
|
||||
fn getDocument(cmd: anytype) !void {
|
||||
// const params = (try cmd.params(struct {
|
||||
// depth: ?u32 = null,
|
||||
// pierce: ?bool = null,
|
||||
// })) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
const doc = page.doc orelse return error.DocumentNotLoaded;
|
||||
|
||||
const node = try bc.node_registry.register(parser.documentToNode(doc));
|
||||
return cmd.sendResult(.{
|
||||
.root = node,
|
||||
}, .{});
|
||||
}
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch
|
||||
fn performSearch(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
query: []const u8,
|
||||
includeUserAgentShadowDOM: ?bool = null,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
const doc = page.doc orelse return error.DocumentNotLoaded;
|
||||
|
||||
const allocator = cmd.cdp.allocator;
|
||||
var list = try css.querySelectorAll(allocator, parser.documentToNode(doc), params.query);
|
||||
defer list.deinit(allocator);
|
||||
|
||||
const search = try bc.node_search_list.create(list.nodes.items);
|
||||
|
||||
return cmd.sendResult(.{
|
||||
.searchId = search.name,
|
||||
.resultCount = @as(u32, @intCast(search.node_ids.len)),
|
||||
}, .{});
|
||||
}
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-discardSearchResults
|
||||
fn discardSearchResults(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
searchId: []const u8,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
|
||||
bc.node_search_list.remove(params.searchId);
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getSearchResults
|
||||
fn getSearchResults(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
searchId: []const u8,
|
||||
fromIndex: u32,
|
||||
toIndex: u32,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
if (params.fromIndex >= params.toIndex) {
|
||||
return error.BadIndices;
|
||||
}
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
|
||||
const search = bc.node_search_list.get(params.searchId) orelse {
|
||||
return error.SearchResultNotFound;
|
||||
};
|
||||
|
||||
const node_ids = search.node_ids;
|
||||
|
||||
if (params.fromIndex >= node_ids.len) return error.BadFromIndex;
|
||||
if (params.toIndex > node_ids.len) return error.BadToIndex;
|
||||
|
||||
return cmd.sendResult(.{ .nodeIds = node_ids[params.fromIndex..params.toIndex] }, .{});
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "cdp.dom: getSearchResults unknown search id" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{
|
||||
.id = 8,
|
||||
.method = "DOM.getSearchResults",
|
||||
.params = .{ .searchId = "Nope", .fromIndex = 0, .toIndex = 10 },
|
||||
}));
|
||||
}
|
||||
|
||||
test "cdp.dom: search flow" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
_ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "<p>1</p> <p>2</p>" });
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 12,
|
||||
.method = "DOM.performSearch",
|
||||
.params = .{ .query = "p" },
|
||||
});
|
||||
try ctx.expectSentResult(.{ .searchId = "0", .resultCount = 2 }, .{ .id = 12 });
|
||||
|
||||
{
|
||||
// getSearchResults
|
||||
try ctx.processMessage(.{
|
||||
.id = 13,
|
||||
.method = "DOM.getSearchResults",
|
||||
.params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 2 },
|
||||
});
|
||||
try ctx.expectSentResult(.{ .nodeIds = &.{ 0, 1 } }, .{ .id = 13 });
|
||||
|
||||
// different fromIndex
|
||||
try ctx.processMessage(.{
|
||||
.id = 14,
|
||||
.method = "DOM.getSearchResults",
|
||||
.params = .{ .searchId = "0", .fromIndex = 1, .toIndex = 2 },
|
||||
});
|
||||
try ctx.expectSentResult(.{ .nodeIds = &.{1} }, .{ .id = 14 });
|
||||
|
||||
// different toIndex
|
||||
try ctx.processMessage(.{
|
||||
.id = 15,
|
||||
.method = "DOM.getSearchResults",
|
||||
.params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 1 },
|
||||
});
|
||||
try ctx.expectSentResult(.{ .nodeIds = &.{0} }, .{ .id = 15 });
|
||||
}
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 16,
|
||||
.method = "DOM.discardSearchResults",
|
||||
.params = .{ .searchId = "0" },
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 16 });
|
||||
|
||||
// make sure the delete actually did something
|
||||
try testing.expectError(error.SearchResultNotFound, ctx.processMessage(.{
|
||||
.id = 17,
|
||||
.method = "DOM.getSearchResults",
|
||||
.params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 1 },
|
||||
}));
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const cdp = @import("cdp.zig");
|
||||
const Runtime = @import("runtime.zig");
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
@@ -17,7 +17,6 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const cdp = @import("cdp.zig");
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
@@ -17,7 +17,6 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const cdp = @import("cdp.zig");
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
@@ -17,7 +17,6 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const cdp = @import("cdp.zig");
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
@@ -17,7 +17,6 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const cdp = @import("cdp.zig");
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
@@ -17,7 +17,6 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const cdp = @import("cdp.zig");
|
||||
const runtime = @import("runtime.zig");
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
@@ -230,7 +229,7 @@ fn navigate(cmd: anytype) !void {
|
||||
// TODO: partially hard coded
|
||||
try cmd.sendEvent(
|
||||
"Page.domContentEventFired",
|
||||
cdp.TimestampEvent{ .timestamp = 343721.803338 },
|
||||
.{ .timestamp = 343721.803338 },
|
||||
.{ .session_id = session_id },
|
||||
);
|
||||
|
||||
@@ -246,7 +245,7 @@ fn navigate(cmd: anytype) !void {
|
||||
// TODO: partially hard coded
|
||||
try cmd.sendEvent(
|
||||
"Page.loadEventFired",
|
||||
cdp.TimestampEvent{ .timestamp = 343721.824655 },
|
||||
.{ .timestamp = 343721.824655 },
|
||||
.{ .session_id = session_id },
|
||||
);
|
||||
|
||||
@@ -264,7 +263,7 @@ fn navigate(cmd: anytype) !void {
|
||||
}, .{ .session_id = session_id });
|
||||
}
|
||||
|
||||
const testing = @import("testing.zig");
|
||||
const testing = @import("../testing.zig");
|
||||
test "cdp.page: getFrameTree" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
@@ -17,8 +17,6 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const cdp = @import("cdp.zig");
|
||||
const asUint = @import("../str/parser.zig").asUint;
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
@@ -16,9 +16,8 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const builtin = @import("builtin");
|
||||
const std = @import("std");
|
||||
const cdp = @import("cdp.zig");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
@@ -17,7 +17,6 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const cdp = @import("cdp.zig");
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
@@ -413,7 +413,7 @@ const TargetInfo = struct {
|
||||
browserContextId: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
const testing = @import("testing.zig");
|
||||
const testing = @import("../testing.zig");
|
||||
test "cdp.target: getBrowserContexts" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
@@ -521,7 +521,7 @@ test "cdp.target: createTarget" {
|
||||
{
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
|
||||
try testing.expectEqual(true, bc.target_id != null);
|
||||
try testing.expectString(
|
||||
try testing.expectEqual(
|
||||
\\{"isDefault":true,"type":"default","frameId":"TID-1"}
|
||||
, bc.session.page.?.aux_data);
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const json = std.json;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
@@ -27,9 +26,13 @@ const main = @import("cdp.zig");
|
||||
const parser = @import("netsurf");
|
||||
const App = @import("../app.zig").App;
|
||||
|
||||
pub const expectEqual = std.testing.expectEqual;
|
||||
pub const expectError = std.testing.expectError;
|
||||
pub const expectString = std.testing.expectEqualStrings;
|
||||
pub const allocator = @import("../testing.zig").allocator;
|
||||
|
||||
pub const expectEqual = @import("../testing.zig").expectEqual;
|
||||
pub const expectError = @import("../testing.zig").expectError;
|
||||
pub const expectEqualSlices = @import("../testing.zig").expectEqualSlices;
|
||||
|
||||
pub const Document = @import("../testing.zig").Document;
|
||||
|
||||
const Browser = struct {
|
||||
session: ?*Session = null,
|
||||
@@ -51,11 +54,11 @@ const Browser = struct {
|
||||
return error.MockBrowserSessionAlreadyExists;
|
||||
}
|
||||
|
||||
const allocator = self.arena.allocator();
|
||||
self.session = try allocator.create(Session);
|
||||
const arena = self.arena.allocator();
|
||||
self.session = try arena.create(Session);
|
||||
self.session.?.* = .{
|
||||
.page = null,
|
||||
.allocator = allocator,
|
||||
.arena = arena,
|
||||
};
|
||||
return self.session.?;
|
||||
}
|
||||
@@ -70,7 +73,7 @@ const Browser = struct {
|
||||
|
||||
const Session = struct {
|
||||
page: ?Page = null,
|
||||
allocator: Allocator,
|
||||
arena: Allocator,
|
||||
|
||||
pub fn currentPage(self: *Session) ?*Page {
|
||||
return &(self.page orelse return null);
|
||||
@@ -82,7 +85,7 @@ const Session = struct {
|
||||
}
|
||||
self.page = .{
|
||||
.session = self,
|
||||
.aux_data = try self.allocator.dupe(u8, aux_data orelse ""),
|
||||
.aux_data = try self.arena.dupe(u8, aux_data orelse ""),
|
||||
};
|
||||
return &self.page.?;
|
||||
}
|
||||
@@ -114,9 +117,9 @@ const Client = struct {
|
||||
sent: std.ArrayListUnmanaged(json.Value) = .{},
|
||||
serialized: std.ArrayListUnmanaged([]const u8) = .{},
|
||||
|
||||
fn init(allocator: Allocator) Client {
|
||||
fn init(alloc: Allocator) Client {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.allocator = alloc,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -165,6 +168,7 @@ const TestContext = struct {
|
||||
id: ?[]const u8 = null,
|
||||
target_id: ?[]const u8 = null,
|
||||
session_id: ?[]const u8 = null,
|
||||
html: ?[]const u8 = null,
|
||||
};
|
||||
pub fn loadBrowserContext(self: *TestContext, opts: BrowserContextOpts) !*main.BrowserContext(TestCDP) {
|
||||
var c = self.cdp();
|
||||
@@ -189,6 +193,13 @@ const TestContext = struct {
|
||||
if (opts.session_id) |sid| {
|
||||
bc.session_id = sid;
|
||||
}
|
||||
|
||||
if (opts.html) |html| {
|
||||
parser.deinit();
|
||||
try parser.init();
|
||||
const page = try bc.session.createPage(null);
|
||||
page.doc = (try Document.init(html)).doc;
|
||||
}
|
||||
return bc;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const parser = @import("netsurf");
|
||||
const tls = @import("tls");
|
||||
const parser = @import("netsurf");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
@@ -32,7 +32,6 @@ test {
|
||||
std.testing.refAllDecls(@import("css/match_test.zig"));
|
||||
std.testing.refAllDecls(@import("css/parser.zig"));
|
||||
std.testing.refAllDecls(@import("generate.zig"));
|
||||
std.testing.refAllDecls(@import("http/client.zig"));
|
||||
std.testing.refAllDecls(@import("storage/storage.zig"));
|
||||
std.testing.refAllDecls(@import("storage/cookie.zig"));
|
||||
std.testing.refAllDecls(@import("iterator/iterator.zig"));
|
||||
|
||||
@@ -17,10 +17,12 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
pub const allocator = std.testing.allocator;
|
||||
pub const expectError = std.testing.expectError;
|
||||
pub const expectString = std.testing.expectEqualStrings;
|
||||
pub const expectEqualSlices = std.testing.expectEqualSlices;
|
||||
|
||||
const App = @import("app.zig").App;
|
||||
|
||||
@@ -190,3 +192,37 @@ pub const Random = struct {
|
||||
return instance.?.random();
|
||||
}
|
||||
};
|
||||
|
||||
pub const Document = struct {
|
||||
doc: *parser.Document,
|
||||
arena: std.heap.ArenaAllocator,
|
||||
|
||||
pub fn init(html: []const u8) !Document {
|
||||
parser.deinit();
|
||||
try parser.init();
|
||||
|
||||
var fbs = std.io.fixedBufferStream(html);
|
||||
const html_doc = try parser.documentHTMLParse(fbs.reader(), "utf-8");
|
||||
|
||||
return .{
|
||||
.arena = std.heap.ArenaAllocator.init(allocator),
|
||||
.doc = parser.documentHTMLToDocument(html_doc),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Document) void {
|
||||
parser.deinit();
|
||||
self.arena.deinit();
|
||||
}
|
||||
|
||||
pub fn querySelectorAll(self: *Document, selector: []const u8) ![]const *parser.Node {
|
||||
const css = @import("dom/css.zig");
|
||||
const node_list = try css.querySelectorAll(self.arena.allocator(), parser.documentToNode(self.doc), selector);
|
||||
return node_list.nodes.items;
|
||||
}
|
||||
|
||||
pub fn querySelector(self: *Document, selector: []const u8) !?*parser.Node {
|
||||
const css = @import("dom/css.zig");
|
||||
return css.querySelector(self.arena.allocator(), parser.documentToNode(self.doc), selector);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user