mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-02-04 06:23:45 +00:00
V8's inspector world is made up of 4 components: Inspector, Client, Channel and Session. Currently, we treat all 4 components as a single unit which is tied to the lifetime of CDP BrowserContext - or, loosely speaking, 1 "Inspector Unit" per page / v8::Context. According to https://web.archive.org/web/20210622022956/https://hyperandroid.com/2020/02/12/v8-inspector-from-an-embedder-standpoint/ and conversation with Gemini, it's more typical to have 1 inspector per isolate. The general breakdown is the Inspector is the top-level manager, the Client is our implementation which control how the Inspector works (its function we expose that v8 calls into). These should be tied to the Isolate. Channels and Sessions are more closely tied to Context, where the Channel is v8->zig and the Session us zig->v8. This PR does a few things 1 - It creates 1 Inspector and Client per Isolate (Env.js) 2 - It creates 1 Session/Channel per BrowserContext 3 - It merges v8::Session and v8::Channel into Inspector.Session 4 - It moves the Inspector instance directly into the Env 5 - BrowserContext interacts with the Inspector.Session, not the Inspector 4 is arguably unnecessary with respect to the main goal of this commit, but the end-goal is to tighten the integration. Specifically, rather than CDP having to inform the inspector that a context was created/destroyed, the Env which manages Contexts directly (https://github.com/lightpanda-io/browser/pull/1432) and which now has direct access to the Inspector, is now equipped to keep this in sync.
722 lines
25 KiB
Zig
722 lines
25 KiB
Zig
// 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 log = @import("../../log.zig");
|
|
const Node = @import("../Node.zig");
|
|
const DOMNode = @import("../../browser/webapi/Node.zig");
|
|
const Selector = @import("../../browser/webapi/selector/Selector.zig");
|
|
|
|
const dump = @import("../../browser/dump.zig");
|
|
const js = @import("../../browser/js/js.zig");
|
|
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
pub fn processMessage(cmd: anytype) !void {
|
|
const action = std.meta.stringToEnum(enum {
|
|
enable,
|
|
getDocument,
|
|
performSearch,
|
|
getSearchResults,
|
|
discardSearchResults,
|
|
querySelector,
|
|
querySelectorAll,
|
|
resolveNode,
|
|
describeNode,
|
|
scrollIntoViewIfNeeded,
|
|
getContentQuads,
|
|
getBoxModel,
|
|
requestChildNodes,
|
|
getFrameOwner,
|
|
getOuterHTML,
|
|
requestNode,
|
|
}, 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),
|
|
.querySelector => return querySelector(cmd),
|
|
.querySelectorAll => return querySelectorAll(cmd),
|
|
.resolveNode => return resolveNode(cmd),
|
|
.describeNode => return describeNode(cmd),
|
|
.scrollIntoViewIfNeeded => return scrollIntoViewIfNeeded(cmd),
|
|
.getContentQuads => return getContentQuads(cmd),
|
|
.getBoxModel => return getBoxModel(cmd),
|
|
.requestChildNodes => return requestChildNodes(cmd),
|
|
.getFrameOwner => return getFrameOwner(cmd),
|
|
.getOuterHTML => return getOuterHTML(cmd),
|
|
.requestNode => return requestNode(cmd),
|
|
}
|
|
}
|
|
|
|
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getDocument
|
|
fn getDocument(cmd: anytype) !void {
|
|
const Params = struct {
|
|
// CDP documentation implies that 0 isn't valid, but it _does_ work in Chrome
|
|
depth: i32 = 3,
|
|
pierce: bool = false,
|
|
};
|
|
const params = try cmd.params(Params) orelse Params{};
|
|
|
|
if (params.pierce) {
|
|
log.warn(.not_implemented, "DOM.getDocument", .{ .param = "pierce" });
|
|
}
|
|
|
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
|
|
|
const node = try bc.node_registry.register(page.window._document.asNode());
|
|
return cmd.sendResult(.{ .root = bc.nodeWriter(node, .{ .depth = params.depth }) }, .{});
|
|
}
|
|
|
|
// 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 list = try Selector.querySelectorAll(page.window._document.asNode(), params.query, page);
|
|
const search = try bc.node_search_list.create(list._nodes);
|
|
|
|
// dispatch setChildNodesEvents to inform the client of the subpart of node
|
|
// tree covering the results.
|
|
try dispatchSetChildNodes(cmd, list._nodes);
|
|
|
|
return cmd.sendResult(.{
|
|
.searchId = search.name,
|
|
.resultCount = @as(u32, @intCast(search.node_ids.len)),
|
|
}, .{});
|
|
}
|
|
|
|
// dispatchSetChildNodes send the setChildNodes event for the whole DOM tree
|
|
// hierarchy of each nodes.
|
|
// We dispatch event in the reverse order: from the top level to the direct parents.
|
|
// We should dispatch a node only if it has never been sent.
|
|
fn dispatchSetChildNodes(cmd: anytype, dom_nodes: []const *DOMNode) !void {
|
|
const arena = cmd.arena;
|
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
|
const session_id = bc.session_id orelse return error.SessionIdNotLoaded;
|
|
|
|
var parents: std.ArrayList(*Node) = .empty;
|
|
for (dom_nodes) |dom_node| {
|
|
var current = dom_node;
|
|
while (true) {
|
|
const parent_node = current._parent orelse break;
|
|
|
|
const node = try bc.node_registry.register(parent_node);
|
|
if (node.set_child_nodes_event) {
|
|
break;
|
|
}
|
|
try parents.append(arena, node);
|
|
current = parent_node;
|
|
}
|
|
}
|
|
|
|
const plen = parents.items.len;
|
|
if (plen == 0) {
|
|
return;
|
|
}
|
|
|
|
var i: usize = plen;
|
|
// We're going to iterate in reverse order from how we added them.
|
|
// This ensures that we're emitting the tree of nodes top-down.
|
|
while (i > 0) {
|
|
i -= 1;
|
|
const node = parents.items[i];
|
|
// Although our above loop won't add an already-sent node to `parents`
|
|
// this can still be true because two nodes can share the same parent node
|
|
// so we might have just sent the node a previous iteration of this loop
|
|
if (node.set_child_nodes_event) continue;
|
|
|
|
node.set_child_nodes_event = true;
|
|
|
|
// If the node has no parent, it's the root node.
|
|
// We don't dispatch event for it because we assume the root node is
|
|
// dispatched via the DOM.getDocument command.
|
|
const dom_parent = node.dom._parent orelse continue;
|
|
|
|
// Retrieve the parent from the registry.
|
|
const parent_node = try bc.node_registry.register(dom_parent);
|
|
|
|
try cmd.sendEvent("DOM.setChildNodes", .{
|
|
.parentId = parent_node.id,
|
|
.nodes = .{bc.nodeWriter(node, .{})},
|
|
}, .{
|
|
.session_id = session_id,
|
|
});
|
|
}
|
|
}
|
|
|
|
// 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] }, .{});
|
|
}
|
|
|
|
fn querySelector(cmd: anytype) !void {
|
|
const params = (try cmd.params(struct {
|
|
nodeId: Node.Id,
|
|
selector: []const u8,
|
|
})) orelse return error.InvalidParams;
|
|
|
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
|
|
|
const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse {
|
|
return cmd.sendError(-32000, "Could not find node with given id", .{});
|
|
};
|
|
|
|
const element = try Selector.querySelector(node.dom, params.selector, page) orelse return error.NodeNotFoundForGivenId;
|
|
const dom_node = element.asNode();
|
|
const registered_node = try bc.node_registry.register(dom_node);
|
|
|
|
// Dispatch setChildNodesEvents to inform the client of the subpart of node tree covering the results.
|
|
var array = [1]*DOMNode{dom_node};
|
|
try dispatchSetChildNodes(cmd, array[0..]);
|
|
|
|
return cmd.sendResult(.{
|
|
.nodeId = registered_node.id,
|
|
}, .{});
|
|
}
|
|
|
|
fn querySelectorAll(cmd: anytype) !void {
|
|
const params = (try cmd.params(struct {
|
|
nodeId: Node.Id,
|
|
selector: []const u8,
|
|
})) orelse return error.InvalidParams;
|
|
|
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
|
|
|
const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse {
|
|
return cmd.sendError(-32000, "Could not find node with given id", .{});
|
|
};
|
|
|
|
const selected_nodes = try Selector.querySelectorAll(node.dom, params.selector, page);
|
|
const nodes = selected_nodes._nodes;
|
|
|
|
const node_ids = try cmd.arena.alloc(Node.Id, nodes.len);
|
|
for (nodes, node_ids) |selected_node, *node_id| {
|
|
node_id.* = (try bc.node_registry.register(selected_node)).id;
|
|
}
|
|
|
|
// Dispatch setChildNodesEvents to inform the client of the subpart of node tree covering the results.
|
|
try dispatchSetChildNodes(cmd, nodes);
|
|
|
|
return cmd.sendResult(.{
|
|
.nodeIds = node_ids,
|
|
}, .{});
|
|
}
|
|
|
|
fn resolveNode(cmd: anytype) !void {
|
|
const params = (try cmd.params(struct {
|
|
nodeId: ?Node.Id = null,
|
|
backendNodeId: ?u32 = null,
|
|
objectGroup: ?[]const u8 = null,
|
|
executionContextId: ?u32 = null,
|
|
})) orelse return error.InvalidParams;
|
|
|
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
|
|
|
var ls: ?js.Local.Scope = null;
|
|
defer if (ls) |*_ls| {
|
|
_ls.deinit();
|
|
};
|
|
|
|
if (params.executionContextId) |context_id| blk: {
|
|
ls = undefined;
|
|
page.js.localScope(&ls.?);
|
|
if (ls.?.local.debugContextId() == context_id) {
|
|
break :blk;
|
|
}
|
|
// not the default scope, check the other ones
|
|
for (bc.isolated_worlds.items) |*isolated_world| {
|
|
ls.?.deinit();
|
|
ls = null;
|
|
|
|
const ctx = (isolated_world.context orelse return error.ContextNotFound);
|
|
ls = undefined;
|
|
ctx.localScope(&ls.?);
|
|
if (ls.?.local.debugContextId() == context_id) {
|
|
break :blk;
|
|
}
|
|
} else return error.ContextNotFound;
|
|
} else {
|
|
ls = undefined;
|
|
page.js.localScope(&ls.?);
|
|
}
|
|
|
|
const input_node_id = params.nodeId orelse params.backendNodeId orelse return error.InvalidParam;
|
|
const node = bc.node_registry.lookup_by_id.get(input_node_id) orelse return error.UnknownNode;
|
|
|
|
// node._node is a *DOMNode we need this to be able to find its most derived type e.g. Node -> Element -> HTMLElement
|
|
// So we use the Node.Union when retrieve the value from the environment
|
|
const remote_object = try bc.inspector_session.getRemoteObject(
|
|
&ls.?.local,
|
|
params.objectGroup orelse "",
|
|
node.dom,
|
|
);
|
|
defer remote_object.deinit();
|
|
|
|
const arena = cmd.arena;
|
|
return cmd.sendResult(.{ .object = .{
|
|
.type = try remote_object.getType(arena),
|
|
.subtype = try remote_object.getSubtype(arena),
|
|
.className = try remote_object.getClassName(arena),
|
|
.description = try remote_object.getDescription(arena),
|
|
.objectId = try remote_object.getObjectId(arena),
|
|
} }, .{});
|
|
}
|
|
|
|
fn describeNode(cmd: anytype) !void {
|
|
const params = (try cmd.params(struct {
|
|
nodeId: ?Node.Id = null,
|
|
backendNodeId: ?Node.Id = null,
|
|
objectId: ?[]const u8 = null,
|
|
depth: i32 = 1,
|
|
pierce: bool = false,
|
|
})) orelse return error.InvalidParams;
|
|
|
|
if (params.pierce) {
|
|
log.warn(.not_implemented, "DOM.describeNode", .{ .param = "pierce" });
|
|
}
|
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
|
|
|
const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);
|
|
|
|
return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{ .depth = params.depth }) }, .{});
|
|
}
|
|
|
|
// An array of quad vertices, x immediately followed by y for each point, points clock-wise.
|
|
// Note Y points downward
|
|
// We are assuming the start/endpoint is not repeated.
|
|
const Quad = [8]f64;
|
|
|
|
const BoxModel = struct {
|
|
content: Quad,
|
|
padding: Quad,
|
|
border: Quad,
|
|
margin: Quad,
|
|
width: i32,
|
|
height: i32,
|
|
// shapeOutside: ?ShapeOutsideInfo,
|
|
};
|
|
|
|
fn rectToQuad(rect: *const DOMNode.Element.DOMRect) Quad {
|
|
return Quad{
|
|
rect._x,
|
|
rect._y,
|
|
rect._x + rect._width,
|
|
rect._y,
|
|
rect._x + rect._width,
|
|
rect._y + rect._height,
|
|
rect._x,
|
|
rect._y + rect._height,
|
|
};
|
|
}
|
|
|
|
fn scrollIntoViewIfNeeded(cmd: anytype) !void {
|
|
const params = (try cmd.params(struct {
|
|
nodeId: ?Node.Id = null,
|
|
backendNodeId: ?u32 = null,
|
|
objectId: ?[]const u8 = null,
|
|
rect: ?DOMNode.Element.DOMRect = null,
|
|
})) orelse return error.InvalidParams;
|
|
// Only 1 of nodeId, backendNodeId, objectId may be set, but chrome just takes the first non-null
|
|
|
|
// We retrieve the node to at least check if it exists and is valid.
|
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
|
const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);
|
|
|
|
switch (node.dom._type) {
|
|
.element => {},
|
|
.document => {},
|
|
.cdata => {},
|
|
else => return error.NodeDoesNotHaveGeometry,
|
|
}
|
|
|
|
return cmd.sendResult(null, .{});
|
|
}
|
|
|
|
fn getNode(arena: Allocator, bc: anytype, node_id: ?Node.Id, backend_node_id: ?Node.Id, object_id: ?[]const u8) !*Node {
|
|
const input_node_id = node_id orelse backend_node_id;
|
|
if (input_node_id) |input_node_id_| {
|
|
return bc.node_registry.lookup_by_id.get(input_node_id_) orelse return error.NodeNotFound;
|
|
}
|
|
if (object_id) |object_id_| {
|
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
|
var ls: js.Local.Scope = undefined;
|
|
page.js.localScope(&ls);
|
|
defer ls.deinit();
|
|
|
|
// Retrieve the object from which ever context it is in.
|
|
const parser_node = try bc.inspector_session.getNodePtr(arena, object_id_, &ls.local);
|
|
return try bc.node_registry.register(@ptrCast(@alignCast(parser_node)));
|
|
}
|
|
return error.MissingParams;
|
|
}
|
|
|
|
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getContentQuads
|
|
// Related to: https://drafts.csswg.org/cssom-view/#the-geometryutils-interface
|
|
fn getContentQuads(cmd: anytype) !void {
|
|
const params = (try cmd.params(struct {
|
|
nodeId: ?Node.Id = null,
|
|
backendNodeId: ?Node.Id = null,
|
|
objectId: ?[]const u8 = null,
|
|
})) orelse return error.InvalidParams;
|
|
|
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
|
|
|
const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);
|
|
|
|
// TODO likely if the following CSS properties are set the quads should be empty
|
|
// visibility: hidden
|
|
// display: none
|
|
|
|
const element = node.dom.is(DOMNode.Element) orelse return error.NodeIsNotAnElement;
|
|
// TODO implement for document or text
|
|
// Most likely document would require some hierachgy in the renderer. It is left unimplemented till we have a good example.
|
|
// Text may be tricky, multiple quads in case of multiple lines? empty quads of text = ""?
|
|
// Elements like SVGElement may have multiple quads.
|
|
|
|
const rect = try element.getBoundingClientRect(page);
|
|
const quad = rectToQuad(rect);
|
|
|
|
return cmd.sendResult(.{ .quads = &.{quad} }, .{});
|
|
}
|
|
|
|
fn getBoxModel(cmd: anytype) !void {
|
|
const params = (try cmd.params(struct {
|
|
nodeId: ?Node.Id = null,
|
|
backendNodeId: ?u32 = null,
|
|
objectId: ?[]const u8 = null,
|
|
})) orelse return error.InvalidParams;
|
|
|
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
|
|
|
const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);
|
|
|
|
// TODO implement for document or text
|
|
const element = node.dom.is(DOMNode.Element) orelse return error.NodeIsNotAnElement;
|
|
|
|
const rect = try element.getBoundingClientRect(page);
|
|
const quad = rectToQuad(rect);
|
|
const zero = [_]f64{0.0} ** 8;
|
|
|
|
return cmd.sendResult(.{ .model = BoxModel{
|
|
.content = quad,
|
|
.padding = zero,
|
|
.border = zero,
|
|
.margin = zero,
|
|
.width = @intFromFloat(rect._width),
|
|
.height = @intFromFloat(rect._height),
|
|
} }, .{});
|
|
}
|
|
|
|
fn requestChildNodes(cmd: anytype) !void {
|
|
const params = (try cmd.params(struct {
|
|
nodeId: Node.Id,
|
|
depth: i32 = 1,
|
|
pierce: bool = false,
|
|
})) orelse return error.InvalidParams;
|
|
|
|
if (params.depth == 0) return error.InvalidParams;
|
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
|
const session_id = bc.session_id orelse return error.SessionIdNotLoaded;
|
|
const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse {
|
|
return error.InvalidNode;
|
|
};
|
|
|
|
try cmd.sendEvent("DOM.setChildNodes", .{
|
|
.parentId = node.id,
|
|
.nodes = bc.nodeWriter(node, .{ .depth = params.depth, .exclude_root = true }),
|
|
}, .{
|
|
.session_id = session_id,
|
|
});
|
|
|
|
return cmd.sendResult(null, .{});
|
|
}
|
|
|
|
fn getFrameOwner(cmd: anytype) !void {
|
|
const params = (try cmd.params(struct {
|
|
frameId: []const u8,
|
|
})) orelse return error.InvalidParams;
|
|
|
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
|
const target_id = bc.target_id orelse return error.TargetNotLoaded;
|
|
if (std.mem.eql(u8, target_id, params.frameId) == false) {
|
|
return cmd.sendError(-32000, "Frame with the given id does not belong to the target.", .{});
|
|
}
|
|
|
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
|
|
|
const node = try bc.node_registry.register(page.window._document.asNode());
|
|
return cmd.sendResult(.{ .nodeId = node.id, .backendNodeId = node.id }, .{});
|
|
}
|
|
|
|
fn getOuterHTML(cmd: anytype) !void {
|
|
const params = (try cmd.params(struct {
|
|
nodeId: ?Node.Id = null,
|
|
backendNodeId: ?Node.Id = null,
|
|
objectId: ?[]const u8 = null,
|
|
includeShadowDOM: bool = false,
|
|
})) orelse return error.InvalidParams;
|
|
|
|
if (params.includeShadowDOM) {
|
|
log.warn(.not_implemented, "DOM.getOuterHTML", .{ .param = "includeShadowDOM" });
|
|
}
|
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
|
|
|
const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);
|
|
|
|
var aw = std.Io.Writer.Allocating.init(cmd.arena);
|
|
try dump.deep(node.dom, .{}, &aw.writer, page);
|
|
|
|
return cmd.sendResult(.{ .outerHTML = aw.written() }, .{});
|
|
}
|
|
|
|
fn requestNode(cmd: anytype) !void {
|
|
const params = (try cmd.params(struct {
|
|
objectId: []const u8,
|
|
})) orelse return error.InvalidParams;
|
|
|
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
|
const node = try getNode(cmd.arena, bc, null, null, params.objectId);
|
|
|
|
return cmd.sendResult(.{ .nodeId = node.id }, .{});
|
|
}
|
|
|
|
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", .url = "cdp/dom1.html" });
|
|
|
|
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 = &.{ 1, 2 } }, .{ .id = 13 });
|
|
|
|
// different fromIndex
|
|
try ctx.processMessage(.{
|
|
.id = 14,
|
|
.method = "DOM.getSearchResults",
|
|
.params = .{ .searchId = "0", .fromIndex = 1, .toIndex = 2 },
|
|
});
|
|
try ctx.expectSentResult(.{ .nodeIds = &.{2} }, .{ .id = 14 });
|
|
|
|
// different toIndex
|
|
try ctx.processMessage(.{
|
|
.id = 15,
|
|
.method = "DOM.getSearchResults",
|
|
.params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 1 },
|
|
});
|
|
try ctx.expectSentResult(.{ .nodeIds = &.{1} }, .{ .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 },
|
|
}));
|
|
}
|
|
|
|
test "cdp.dom: querySelector unknown search id" {
|
|
var ctx = testing.context();
|
|
defer ctx.deinit();
|
|
|
|
_ = try ctx.loadBrowserContext(.{ .id = "BID-A", .url = "cdp/dom1.html" });
|
|
|
|
try ctx.processMessage(.{
|
|
.id = 9,
|
|
.method = "DOM.querySelector",
|
|
.params = .{ .nodeId = 99, .selector = "" },
|
|
});
|
|
try ctx.expectSentError(-32000, "Could not find node with given id", .{});
|
|
|
|
try ctx.processMessage(.{
|
|
.id = 9,
|
|
.method = "DOM.querySelectorAll",
|
|
.params = .{ .nodeId = 99, .selector = "" },
|
|
});
|
|
try ctx.expectSentError(-32000, "Could not find node with given id", .{});
|
|
}
|
|
|
|
test "cdp.dom: querySelector Node not found" {
|
|
var ctx = testing.context();
|
|
defer ctx.deinit();
|
|
|
|
_ = try ctx.loadBrowserContext(.{ .id = "BID-A", .url = "cdp/dom1.html" });
|
|
|
|
try ctx.processMessage(.{ // Hacky way to make sure nodeId 1 exists in the registry
|
|
.id = 3,
|
|
.method = "DOM.performSearch",
|
|
.params = .{ .query = "p" },
|
|
});
|
|
try ctx.expectSentResult(.{ .searchId = "0", .resultCount = 2 }, .{ .id = 3 });
|
|
|
|
try testing.expectError(error.NodeNotFoundForGivenId, ctx.processMessage(.{
|
|
.id = 4,
|
|
.method = "DOM.querySelector",
|
|
.params = .{ .nodeId = 1, .selector = "a" },
|
|
}));
|
|
|
|
try ctx.processMessage(.{
|
|
.id = 5,
|
|
.method = "DOM.querySelectorAll",
|
|
.params = .{ .nodeId = 1, .selector = "a" },
|
|
});
|
|
try ctx.expectSentResult(.{ .nodeIds = &[_]u32{} }, .{ .id = 5 });
|
|
}
|
|
|
|
test "cdp.dom: querySelector Nodes found" {
|
|
var ctx = testing.context();
|
|
defer ctx.deinit();
|
|
|
|
_ = try ctx.loadBrowserContext(.{ .id = "BID-A", .url = "cdp/dom2.html" });
|
|
|
|
try ctx.processMessage(.{ // Hacky way to make sure nodeId 1 exists in the registry
|
|
.id = 3,
|
|
.method = "DOM.performSearch",
|
|
.params = .{ .query = "div" },
|
|
});
|
|
try ctx.expectSentResult(.{ .searchId = "0", .resultCount = 1 }, .{ .id = 3 });
|
|
|
|
try ctx.processMessage(.{
|
|
.id = 4,
|
|
.method = "DOM.querySelector",
|
|
.params = .{ .nodeId = 1, .selector = "p" },
|
|
});
|
|
try ctx.expectSentEvent("DOM.setChildNodes", null, .{});
|
|
try ctx.expectSentResult(.{ .nodeId = 7 }, .{ .id = 4 });
|
|
|
|
try ctx.processMessage(.{
|
|
.id = 5,
|
|
.method = "DOM.querySelectorAll",
|
|
.params = .{ .nodeId = 1, .selector = "p" },
|
|
});
|
|
try ctx.expectSentEvent("DOM.setChildNodes", null, .{});
|
|
try ctx.expectSentResult(.{ .nodeIds = &.{7} }, .{ .id = 5 });
|
|
}
|
|
|
|
test "cdp.dom: getBoxModel" {
|
|
var ctx = testing.context();
|
|
defer ctx.deinit();
|
|
|
|
_ = try ctx.loadBrowserContext(.{ .id = "BID-A", .url = "cdp/dom2.html" });
|
|
|
|
try ctx.processMessage(.{ // Hacky way to make sure nodeId 1 exists in the registry
|
|
.id = 3,
|
|
.method = "DOM.getDocument",
|
|
});
|
|
|
|
try ctx.processMessage(.{
|
|
.id = 4,
|
|
.method = "DOM.querySelector",
|
|
.params = .{ .nodeId = 1, .selector = "p" },
|
|
});
|
|
try ctx.expectSentResult(.{ .nodeId = 3 }, .{ .id = 4 });
|
|
|
|
try ctx.processMessage(.{
|
|
.id = 5,
|
|
.method = "DOM.getBoxModel",
|
|
.params = .{ .nodeId = 6 },
|
|
});
|
|
try ctx.expectSentResult(.{ .model = BoxModel{
|
|
.content = Quad{ 10.0, 10.0, 15.0, 10.0, 15.0, 15.0, 10.0, 15.0 },
|
|
.padding = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
|
|
.border = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
|
|
.margin = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
|
|
.width = 5,
|
|
.height = 5,
|
|
} }, .{ .id = 5 });
|
|
}
|