Firefox keeps using the same walker after navigation and expects it to publish the new document root. Normal tab debugging also relies on target switching. Re-inspect the tab when navigation finishes, replace the walker DOM data, and notify the watcher that the previous top-level frame target was destroyed before a fresh target becomes available. Send target availability as a watcher event, not as the watchTargets response, and give each navigation a new inner window id so Firefox can associate the new target with the new document.
1112 lines
38 KiB
C++
1112 lines
38 KiB
C++
/*
|
|
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/JsonArray.h>
|
|
#include <AK/StringUtils.h>
|
|
#include <LibDevTools/Actors/AccessibilityNodeActor.h>
|
|
#include <LibDevTools/Actors/LayoutInspectorActor.h>
|
|
#include <LibDevTools/Actors/TabActor.h>
|
|
#include <LibDevTools/Actors/WalkerActor.h>
|
|
#include <LibDevTools/DevToolsDelegate.h>
|
|
#include <LibDevTools/DevToolsServer.h>
|
|
#include <LibWeb/DOM/NodeType.h>
|
|
#include <LibWebView/Mutation.h>
|
|
|
|
namespace DevTools {
|
|
|
|
NonnullRefPtr<WalkerActor> WalkerActor::create(DevToolsServer& devtools, String name, WeakPtr<TabActor> tab, JsonObject dom_tree)
|
|
{
|
|
return adopt_ref(*new WalkerActor(devtools, move(name), move(tab), move(dom_tree)));
|
|
}
|
|
|
|
WalkerActor::WalkerActor(DevToolsServer& devtools, String name, WeakPtr<TabActor> tab, JsonObject dom_tree)
|
|
: Actor(devtools, move(name))
|
|
, m_tab(move(tab))
|
|
, m_dom_tree(move(dom_tree))
|
|
{
|
|
populate_dom_tree_cache();
|
|
|
|
if (auto tab = m_tab.strong_ref()) {
|
|
devtools.delegate().listen_for_dom_mutations(tab->description(),
|
|
weak_callback(*this, [](auto& self, WebView::Mutation mutation) {
|
|
self.new_dom_node_mutation(move(mutation));
|
|
}));
|
|
}
|
|
}
|
|
|
|
WalkerActor::~WalkerActor()
|
|
{
|
|
stop_node_picker();
|
|
|
|
if (auto tab = m_tab.strong_ref())
|
|
devtools().delegate().stop_listening_for_dom_mutations(tab->description());
|
|
}
|
|
|
|
void WalkerActor::handle_message(Message const& message)
|
|
{
|
|
JsonObject response;
|
|
|
|
if (message.type == "children"sv) {
|
|
auto node = get_required_parameter<String>(message, "node"sv);
|
|
if (!node.has_value())
|
|
return;
|
|
|
|
auto ancestor_node = WalkerActor::dom_node_for(*this, *node);
|
|
if (!ancestor_node.has_value()) {
|
|
send_unknown_actor_error(message, *node);
|
|
return;
|
|
}
|
|
|
|
JsonArray nodes;
|
|
|
|
if (auto children = ancestor_node->node.get_array("children"sv); children.has_value()) {
|
|
children->for_each([&](JsonValue const& child) {
|
|
nodes.must_append(serialize_node(child.as_object()));
|
|
});
|
|
}
|
|
|
|
response.set("hasFirst"sv, !nodes.is_empty());
|
|
response.set("hasLast"sv, !nodes.is_empty());
|
|
response.set("nodes"sv, move(nodes));
|
|
send_response(message, move(response));
|
|
return;
|
|
}
|
|
|
|
if (message.type == "duplicateNode"sv) {
|
|
auto node = get_required_parameter<String>(message, "node"sv);
|
|
if (!node.has_value())
|
|
return;
|
|
|
|
auto dom_node = WalkerActor::dom_node_for(*this, *node);
|
|
if (!dom_node.has_value()) {
|
|
send_unknown_actor_error(message, *node);
|
|
return;
|
|
}
|
|
|
|
devtools().delegate().clone_dom_node(dom_node->tab->description(), dom_node->identifier.id, default_async_handler(message));
|
|
return;
|
|
}
|
|
|
|
if (message.type == "editTagName"sv) {
|
|
auto node = get_required_parameter<String>(message, "node"sv);
|
|
if (!node.has_value())
|
|
return;
|
|
|
|
auto tag_name = get_required_parameter<String>(message, "tagName"sv);
|
|
if (!tag_name.has_value())
|
|
return;
|
|
|
|
auto dom_node = WalkerActor::dom_node_for(*this, *node);
|
|
if (!dom_node.has_value()) {
|
|
send_unknown_actor_error(message, *node);
|
|
return;
|
|
}
|
|
|
|
devtools().delegate().set_dom_node_tag(dom_node->tab->description(), dom_node->identifier.id, *tag_name, default_async_handler(message));
|
|
return;
|
|
}
|
|
|
|
if (message.type == "getLayoutInspector"sv) {
|
|
if (!m_layout_inspector)
|
|
m_layout_inspector = devtools().register_actor<LayoutInspectorActor>(m_tab, make_weak_ptr<WalkerActor>());
|
|
|
|
JsonObject actor;
|
|
actor.set("actor"sv, m_layout_inspector->name());
|
|
|
|
response.set("actor"sv, move(actor));
|
|
send_response(message, move(response));
|
|
return;
|
|
}
|
|
|
|
if (message.type == "getMutations"sv) {
|
|
response.set("mutations"sv, serialize_mutations());
|
|
send_response(message, move(response));
|
|
|
|
m_has_new_mutations_since_last_mutations_request = false;
|
|
return;
|
|
}
|
|
|
|
if (message.type == "getNodeFromActor"sv) {
|
|
auto path = get_required_parameter<JsonArray>(message, "path"sv);
|
|
if (!path.has_value())
|
|
return;
|
|
|
|
auto actor_id = get_required_parameter<String>(message, "actorID"sv);
|
|
if (!actor_id.has_value())
|
|
return;
|
|
|
|
// The ["rawAccessible","DOMNode"] path retrieves the DOM node corresponding to an AccessibilityNodeActor.
|
|
if (path->size() == 2) {
|
|
auto const& first = path->at(0);
|
|
auto const& second = path->at(1);
|
|
if (first.is_string() && first.as_string() == "rawAccessible"sv
|
|
&& second.is_string() && second.as_string() == "DOMNode"sv) {
|
|
|
|
auto maybe_actor = devtools().actor_registry().find(actor_id.value());
|
|
if (maybe_actor == devtools().actor_registry().end()) {
|
|
send_unknown_actor_error(message, actor_id.value());
|
|
return;
|
|
}
|
|
|
|
auto accessibility_actor = as<AccessibilityNodeActor>(maybe_actor->value.ptr());
|
|
if (auto node_actor_name = m_dom_node_id_to_actor_map.get(accessibility_actor->node_identifier().id); node_actor_name.has_value()) {
|
|
auto dom_node = dom_node_for(this, node_actor_name.value());
|
|
if (!dom_node.has_value()) {
|
|
send_unknown_actor_error(message, node_actor_name.value());
|
|
return;
|
|
}
|
|
|
|
JsonObject node;
|
|
node.set("node"sv, serialize_node(dom_node->node));
|
|
node.set("newParents"sv, JsonArray {});
|
|
|
|
response.set("node"sv, move(node));
|
|
send_response(message, move(response));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Firefox asks for ["containerEl"] on layout actors when the form did not include a containerNodeActorID.
|
|
if (path->size() == 1 && path->at(0).is_string() && path->at(0).as_string() == "containerEl"sv) {
|
|
auto maybe_actor = devtools().actor_registry().find(actor_id.value());
|
|
if (maybe_actor == devtools().actor_registry().end()) {
|
|
send_unknown_actor_error(message, actor_id.value());
|
|
return;
|
|
}
|
|
|
|
Optional<Web::UniqueNodeID> container_node_id;
|
|
if (auto* grid_actor = as_if<GridActor>(maybe_actor->value.ptr()))
|
|
container_node_id = grid_actor->container_node_id();
|
|
else if (auto* flexbox_actor = as_if<FlexboxActor>(maybe_actor->value.ptr()))
|
|
container_node_id = flexbox_actor->container_node_id();
|
|
|
|
if (container_node_id.has_value()) {
|
|
if (auto node_actor_name = m_dom_node_id_to_actor_map.get(*container_node_id); node_actor_name.has_value()) {
|
|
auto dom_node = dom_node_for(this, node_actor_name.value());
|
|
if (!dom_node.has_value()) {
|
|
send_unknown_actor_error(message, node_actor_name.value());
|
|
return;
|
|
}
|
|
|
|
JsonObject node;
|
|
node.set("node"sv, serialize_node(dom_node->node));
|
|
node.set("newParents"sv, JsonArray {});
|
|
|
|
response.set("node"sv, move(node));
|
|
send_response(message, move(response));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Firefox asks for ["element"] on a FlexItemActor when the form did not include a nodeActorID.
|
|
if (path->size() == 1 && path->at(0).is_string() && path->at(0).as_string() == "element"sv) {
|
|
auto maybe_actor = devtools().actor_registry().find(actor_id.value());
|
|
if (maybe_actor == devtools().actor_registry().end()) {
|
|
send_unknown_actor_error(message, actor_id.value());
|
|
return;
|
|
}
|
|
|
|
if (auto* flex_item_actor = as_if<FlexItemActor>(maybe_actor->value.ptr())) {
|
|
if (auto node_actor_name = m_dom_node_id_to_actor_map.get(flex_item_actor->node_id()); node_actor_name.has_value()) {
|
|
auto dom_node = dom_node_for(this, node_actor_name.value());
|
|
if (!dom_node.has_value()) {
|
|
send_unknown_actor_error(message, node_actor_name.value());
|
|
return;
|
|
}
|
|
|
|
JsonObject node;
|
|
node.set("node"sv, serialize_node(dom_node->node));
|
|
node.set("newParents"sv, JsonArray {});
|
|
|
|
response.set("node"sv, move(node));
|
|
send_response(message, move(response));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
JsonObject error;
|
|
error.set("error"sv, "unrecognizedNodePath"sv);
|
|
error.set("message"sv, MUST(String::formatted("Unrecognized or missing path for getNodeFromActor: '{}'", message.data.serialized())));
|
|
send_response(message, move(error));
|
|
return;
|
|
}
|
|
|
|
if (message.type == "getOffsetParent"sv) {
|
|
response.set("node"sv, JsonValue {});
|
|
send_response(message, move(response));
|
|
return;
|
|
}
|
|
|
|
if (message.type == "getParentGridNode"sv) {
|
|
auto node = get_required_parameter<String>(message, "node"sv);
|
|
if (!node.has_value())
|
|
return;
|
|
|
|
auto dom_node = WalkerActor::dom_node_for(*this, *node);
|
|
if (!dom_node.has_value()) {
|
|
send_unknown_actor_error(message, *node);
|
|
return;
|
|
}
|
|
|
|
JsonValue parent_grid_node;
|
|
if (auto parent_grid = parent_grid_node_for_node(dom_node->node); parent_grid.has_value())
|
|
parent_grid_node = serialize_node(parent_grid.value());
|
|
|
|
response.set("node"sv, move(parent_grid_node));
|
|
send_response(message, move(response));
|
|
return;
|
|
}
|
|
|
|
if (message.type == "innerHTML"sv) {
|
|
auto node = get_required_parameter<String>(message, "node"sv);
|
|
if (!node.has_value())
|
|
return;
|
|
|
|
auto dom_node = WalkerActor::dom_node_for(*this, *node);
|
|
if (!dom_node.has_value()) {
|
|
send_unknown_actor_error(message, *node);
|
|
return;
|
|
}
|
|
|
|
devtools().delegate().get_dom_node_inner_html(dom_node->tab->description(), dom_node->identifier.id,
|
|
async_handler(message, [](auto&, auto html, auto& response) {
|
|
response.set("value"sv, move(html));
|
|
}));
|
|
|
|
return;
|
|
}
|
|
|
|
if (message.type == "insertAdjacentHTML") {
|
|
// FIXME: This message also contains `value` and `position` parameters, containing the HTML to insert and the
|
|
// location to insert it. For the "Create New Node" action, this is always "<div></div>" and "beforeEnd",
|
|
// which is exactly what our WebView implementation currently supports.
|
|
auto node = get_required_parameter<String>(message, "node"sv);
|
|
if (!node.has_value())
|
|
return;
|
|
|
|
auto dom_node = WalkerActor::dom_node_for(*this, *node);
|
|
if (!dom_node.has_value()) {
|
|
send_unknown_actor_error(message, *node);
|
|
return;
|
|
}
|
|
|
|
devtools().delegate().create_child_element(dom_node->tab->description(), dom_node->identifier.id,
|
|
async_handler<WalkerActor>(message, [](auto& self, auto node_id, auto& response) {
|
|
JsonArray nodes;
|
|
|
|
if (auto actor = self.m_dom_node_id_to_actor_map.get(node_id); actor.has_value()) {
|
|
if (auto dom_node = WalkerActor::dom_node_for(self, *actor); dom_node.has_value())
|
|
nodes.must_append(self.serialize_node(dom_node->node));
|
|
}
|
|
|
|
response.set("newParents"sv, JsonArray {});
|
|
response.set("nodes"sv, move(nodes));
|
|
}));
|
|
|
|
return;
|
|
}
|
|
|
|
if (message.type == "insertBefore"sv) {
|
|
auto node = get_required_parameter<String>(message, "node"sv);
|
|
if (!node.has_value())
|
|
return;
|
|
|
|
auto parent = get_required_parameter<String>(message, "parent"sv);
|
|
if (!parent.has_value())
|
|
return;
|
|
|
|
auto dom_node = WalkerActor::dom_node_for(*this, *node);
|
|
if (!dom_node.has_value()) {
|
|
send_unknown_actor_error(message, *node);
|
|
return;
|
|
}
|
|
|
|
auto parent_dom_node = WalkerActor::dom_node_for(*this, *parent);
|
|
if (!parent_dom_node.has_value()) {
|
|
send_unknown_actor_error(message, *parent);
|
|
return;
|
|
}
|
|
|
|
Optional<Web::UniqueNodeID> sibling_node_id;
|
|
if (auto sibling = message.data.get_string("sibling"sv); sibling.has_value()) {
|
|
auto sibling_dom_node = WalkerActor::dom_node_for(*this, *sibling);
|
|
if (!sibling_dom_node.has_value()) {
|
|
send_unknown_actor_error(message, *sibling);
|
|
return;
|
|
}
|
|
|
|
sibling_node_id = sibling_dom_node->identifier.id;
|
|
}
|
|
|
|
devtools().delegate().insert_dom_node_before(dom_node->tab->description(), dom_node->identifier.id, parent_dom_node->identifier.id, sibling_node_id, default_async_handler(message));
|
|
return;
|
|
}
|
|
|
|
if (message.type == "isInDOMTree"sv) {
|
|
auto node = get_required_parameter<String>(message, "node"sv);
|
|
if (!node.has_value())
|
|
return;
|
|
|
|
response.set("attached"sv, m_actor_to_dom_node_map.contains(*node));
|
|
send_response(message, move(response));
|
|
return;
|
|
}
|
|
|
|
if (message.type == "outerHTML"sv) {
|
|
auto node = get_required_parameter<String>(message, "node"sv);
|
|
if (!node.has_value())
|
|
return;
|
|
|
|
auto dom_node = WalkerActor::dom_node_for(*this, *node);
|
|
if (!dom_node.has_value()) {
|
|
send_unknown_actor_error(message, *node);
|
|
return;
|
|
}
|
|
|
|
devtools().delegate().get_dom_node_outer_html(dom_node->tab->description(), dom_node->identifier.id,
|
|
async_handler(message, [](auto&, auto html, auto& response) {
|
|
response.set("value"sv, move(html));
|
|
}));
|
|
|
|
return;
|
|
}
|
|
|
|
if (message.type == "previousSibling"sv) {
|
|
auto node = get_required_parameter<String>(message, "node"sv);
|
|
if (!node.has_value())
|
|
return;
|
|
|
|
auto dom_node = WalkerActor::dom_node_for(*this, *node);
|
|
if (!dom_node.has_value()) {
|
|
send_unknown_actor_error(message, *node);
|
|
return;
|
|
}
|
|
|
|
JsonValue previous_sibling;
|
|
if (auto previous_sibling_node = previous_sibling_for_node(dom_node->node); previous_sibling_node.has_value())
|
|
previous_sibling = serialize_node(*previous_sibling_node);
|
|
|
|
response.set("node"sv, move(previous_sibling));
|
|
send_response(message, move(response));
|
|
return;
|
|
}
|
|
|
|
if (message.type == "pick"sv) {
|
|
if (auto tab = m_tab.strong_ref()) {
|
|
m_is_picking_node = true;
|
|
m_picker_hovered_node_id.clear();
|
|
devtools().delegate().start_node_picker(tab->description(), weak_callback(*this, [](auto& self, DevToolsDelegate::NodePickerEvent event) {
|
|
self.handle_node_picker_event(event);
|
|
}));
|
|
}
|
|
|
|
send_response(message, move(response));
|
|
return;
|
|
}
|
|
|
|
if (message.type == "cancelPick"sv) {
|
|
stop_node_picker();
|
|
send_response(message, move(response));
|
|
return;
|
|
}
|
|
|
|
if (message.type == "clearPicker"sv) {
|
|
if (auto tab = m_tab.strong_ref())
|
|
devtools().delegate().clear_node_picker(tab->description());
|
|
m_picker_hovered_node_id.clear();
|
|
send_response(message, move(response));
|
|
return;
|
|
}
|
|
|
|
if (message.type == "querySelector"sv) {
|
|
auto node = get_required_parameter<String>(message, "node"sv);
|
|
if (!node.has_value())
|
|
return;
|
|
|
|
auto selector = get_required_parameter<String>(message, "selector"sv);
|
|
if (!selector.has_value())
|
|
return;
|
|
|
|
auto ancestor_node = WalkerActor::dom_node_for(*this, *node);
|
|
if (!ancestor_node.has_value()) {
|
|
send_unknown_actor_error(message, *node);
|
|
return;
|
|
}
|
|
|
|
if (auto selected_node = find_node_by_selector(ancestor_node->node, *selector); selected_node.has_value()) {
|
|
response.set("node"sv, serialize_node(*selected_node));
|
|
|
|
if (auto parent = m_dom_node_to_parent_map.get(&selected_node.value()); parent.value() && parent.value() != &ancestor_node->node) {
|
|
// FIXME: Should this be a stack of nodes leading to `ancestor_node`?
|
|
JsonArray new_parents;
|
|
new_parents.must_append(serialize_node(*parent.value()));
|
|
|
|
response.set("newParents"sv, move(new_parents));
|
|
}
|
|
}
|
|
|
|
send_response(message, move(response));
|
|
return;
|
|
}
|
|
|
|
if (message.type == "removeNode"sv) {
|
|
auto node = get_required_parameter<String>(message, "node"sv);
|
|
if (!node.has_value())
|
|
return;
|
|
|
|
auto dom_node = WalkerActor::dom_node_for(*this, *node);
|
|
if (!dom_node.has_value()) {
|
|
send_unknown_actor_error(message, *node);
|
|
return;
|
|
}
|
|
|
|
JsonValue next_sibling;
|
|
if (auto next_sibling_node = next_sibling_for_node(dom_node->node); next_sibling_node.has_value())
|
|
next_sibling = serialize_node(*next_sibling_node);
|
|
|
|
auto parent_node = remove_node(dom_node->node);
|
|
if (!parent_node.has_value())
|
|
return;
|
|
|
|
devtools().delegate().remove_dom_node(dom_node->tab->description(), dom_node->identifier.id,
|
|
async_handler(message, [next_sibling = move(next_sibling)](auto&, auto, auto& response) mutable {
|
|
response.set("nextSibling"sv, move(next_sibling));
|
|
}));
|
|
|
|
return;
|
|
}
|
|
|
|
if (message.type == "retainNode"sv) {
|
|
send_response(message, move(response));
|
|
return;
|
|
}
|
|
|
|
if (message.type == "setOuterHTML"sv) {
|
|
auto node = get_required_parameter<String>(message, "node"sv);
|
|
if (!node.has_value())
|
|
return;
|
|
|
|
auto value = get_required_parameter<String>(message, "value"sv);
|
|
if (!value.has_value())
|
|
return;
|
|
|
|
auto dom_node = WalkerActor::dom_node_for(*this, *node);
|
|
if (!dom_node.has_value()) {
|
|
send_unknown_actor_error(message, *node);
|
|
return;
|
|
}
|
|
|
|
devtools().delegate().set_dom_node_outer_html(dom_node->tab->description(), dom_node->identifier.id, value.release_value(), default_async_handler(message));
|
|
return;
|
|
}
|
|
|
|
if (message.type == "watchRootNode"sv) {
|
|
response.set("type"sv, "root-available"sv);
|
|
response.set("node"sv, serialize_root());
|
|
send_response(message, move(response));
|
|
|
|
send_message({});
|
|
return;
|
|
}
|
|
|
|
send_unrecognized_packet_type_error(message);
|
|
}
|
|
|
|
bool WalkerActor::is_suitable_for_dom_inspection(JsonValue const& node)
|
|
{
|
|
if (!node.is_object())
|
|
return true;
|
|
|
|
auto const& object = node.as_object();
|
|
|
|
if (!object.has_string("name"sv) || !object.has_string("type"sv))
|
|
return false;
|
|
|
|
if (auto text = object.get_string("text"sv); text.has_value()) {
|
|
if (AK::StringUtils::is_whitespace(*text))
|
|
return false;
|
|
}
|
|
if (auto data = object.get_string("data"sv); data.has_value()) {
|
|
if (AK::StringUtils::is_whitespace(*data))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
JsonValue WalkerActor::serialize_root() const
|
|
{
|
|
if (!m_has_live_dom_tree)
|
|
return {};
|
|
|
|
return serialize_node(m_dom_tree);
|
|
}
|
|
|
|
JsonValue WalkerActor::serialize_node(JsonObject const& node) const
|
|
{
|
|
auto tab = m_tab.strong_ref();
|
|
if (!tab)
|
|
return {};
|
|
|
|
auto actor = node.get_string("actor"sv);
|
|
if (!actor.has_value())
|
|
return {};
|
|
|
|
auto name = node.get_string("name"sv).release_value();
|
|
auto type = node.get_string("type"sv).release_value();
|
|
|
|
auto dom_type = parse_dom_node_type(type);
|
|
JsonValue node_value;
|
|
|
|
auto is_top_level_document = &node == &m_dom_tree;
|
|
auto is_displayed = !is_top_level_document && node.get_bool("visible"sv).value_or(false);
|
|
auto is_scrollable = node.get_bool("scrollable"sv).value_or(false);
|
|
|
|
auto is_shadow_root = false;
|
|
auto is_after_pseudo_element = false;
|
|
auto is_before_pseudo_element = false;
|
|
auto is_marker_pseudo_element = false;
|
|
|
|
if (dom_type == Web::DOM::NodeType::TEXT_NODE) {
|
|
if (auto text = node.get_string("text"sv); text.has_value())
|
|
node_value = text.release_value();
|
|
} else if (dom_type == Web::DOM::NodeType::COMMENT_NODE) {
|
|
if (auto data = node.get_string("data"sv); data.has_value())
|
|
node_value = data.release_value();
|
|
}
|
|
|
|
if (type == "shadow-root"sv) {
|
|
is_shadow_root = true;
|
|
} else if (type == "pseudo-element"sv) {
|
|
auto pseudo_element = node.get_integer<UnderlyingType<Web::CSS::PseudoElement>>("pseudo-element"sv).map([](auto value) {
|
|
VERIFY(value < to_underlying(Web::CSS::PseudoElement::KnownPseudoElementCount));
|
|
return static_cast<Web::CSS::PseudoElement>(value);
|
|
});
|
|
|
|
is_after_pseudo_element = pseudo_element == Web::CSS::PseudoElement::After;
|
|
is_before_pseudo_element = pseudo_element == Web::CSS::PseudoElement::Before;
|
|
is_marker_pseudo_element = pseudo_element == Web::CSS::PseudoElement::Marker;
|
|
|
|
auto parent_id = node.get_integer<Web::UniqueNodeID::Type>("parent-id"sv).value();
|
|
|
|
if (auto parent_actor = m_dom_node_id_to_actor_map.get(parent_id); parent_actor.has_value()) {
|
|
if (auto parent_node = WalkerActor::dom_node_for(this, *parent_actor); parent_node.has_value()) {
|
|
dom_type = parse_dom_node_type(parent_node->node.get_string("type"sv).value());
|
|
is_displayed = !is_top_level_document && parent_node->node.get_bool("visible"sv).value_or(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
size_t child_count = 0;
|
|
if (auto children = node.get_array("children"sv); children.has_value())
|
|
child_count = children->size();
|
|
|
|
JsonArray attrs;
|
|
|
|
if (auto attributes = node.get_object("attributes"sv); attributes.has_value()) {
|
|
attributes->for_each_member([&](String const& name, JsonValue const& value) {
|
|
if (!value.is_string())
|
|
return;
|
|
|
|
JsonObject attr;
|
|
attr.set("name"sv, name);
|
|
attr.set("value"sv, value.as_string());
|
|
attrs.must_append(move(attr));
|
|
});
|
|
}
|
|
|
|
JsonObject serialized;
|
|
serialized.set("actor"sv, actor.release_value());
|
|
serialized.set("attrs"sv, move(attrs));
|
|
serialized.set("baseURI"sv, tab->description().url);
|
|
serialized.set("causesOverflow"sv, false);
|
|
serialized.set("containerType"sv, JsonValue {});
|
|
serialized.set("displayName"sv, name.to_ascii_lowercase());
|
|
serialized.set("displayType"sv, node.get_string("display"sv).value_or("block"_string));
|
|
serialized.set("hasEventListeners"sv, false);
|
|
serialized.set("isAfterPseudoElement"sv, is_after_pseudo_element);
|
|
serialized.set("isAnonymous"sv, false);
|
|
serialized.set("isBeforePseudoElement"sv, is_before_pseudo_element);
|
|
serialized.set("isDirectShadowHostChild"sv, JsonValue {});
|
|
serialized.set("isDisplayed"sv, is_displayed);
|
|
serialized.set("isInHTMLDocument"sv, true);
|
|
serialized.set("isMarkerPseudoElement"sv, is_marker_pseudo_element);
|
|
serialized.set("isNativeAnonymous"sv, false);
|
|
serialized.set("isScrollable"sv, is_scrollable);
|
|
serialized.set("isShadowHost"sv, false);
|
|
serialized.set("isShadowRoot"sv, is_shadow_root);
|
|
serialized.set("isTopLevelDocument"sv, is_top_level_document);
|
|
serialized.set("nodeName"sv, name);
|
|
serialized.set("nodeType"sv, to_underlying(dom_type));
|
|
serialized.set("nodeValue"sv, move(node_value));
|
|
serialized.set("numChildren"sv, child_count);
|
|
serialized.set("shadowRootMode"sv, JsonValue {});
|
|
serialized.set("traits"sv, JsonObject {});
|
|
|
|
// FIXME: De-duplicate this string. LibDevTools currently cannot depend on LibWeb.
|
|
serialized.set("namespaceURI"sv, "http://www.w3.org/1999/xhtml"sv);
|
|
|
|
if (!is_top_level_document) {
|
|
if (auto parent = m_dom_node_to_parent_map.get(&node); parent.has_value() && parent.value()) {
|
|
actor = parent.value()->get_string("actor"sv);
|
|
if (!actor.has_value())
|
|
return {};
|
|
|
|
serialized.set("parent"sv, actor.release_value());
|
|
}
|
|
}
|
|
|
|
return serialized;
|
|
}
|
|
|
|
Optional<Node> WalkerActor::dom_node_for(WeakPtr<WalkerActor> const& weak_walker, StringView actor)
|
|
{
|
|
if (auto walker = weak_walker.strong_ref())
|
|
return walker->dom_node(actor);
|
|
return {};
|
|
}
|
|
|
|
Optional<Node> WalkerActor::dom_node(StringView actor)
|
|
{
|
|
if (!m_has_live_dom_tree)
|
|
return {};
|
|
|
|
auto tab = m_tab.strong_ref();
|
|
if (!tab)
|
|
return {};
|
|
|
|
auto maybe_dom_node = m_actor_to_dom_node_map.get(actor);
|
|
if (!maybe_dom_node.has_value() || !maybe_dom_node.value())
|
|
return {};
|
|
|
|
auto const& dom_node = *maybe_dom_node.value();
|
|
auto identifier = NodeIdentifier::for_node(dom_node);
|
|
|
|
return Node { .node = dom_node, .identifier = move(identifier), .tab = tab.release_nonnull() };
|
|
}
|
|
|
|
Optional<String> WalkerActor::node_actor_name_for(Web::UniqueNodeID node_id) const
|
|
{
|
|
if (!m_has_live_dom_tree)
|
|
return {};
|
|
|
|
return m_dom_node_id_to_actor_map.get(node_id).copy();
|
|
}
|
|
|
|
Optional<JsonObject const&> WalkerActor::element_node_for_picker_node(JsonObject const& node) const
|
|
{
|
|
// To match Firefox, don't make text nodes pickable. Instead, find their nearest ancestor element node.
|
|
auto const* candidate = &node;
|
|
while (candidate) {
|
|
if (candidate->get_string("type"sv).value_or({}) == "element"sv)
|
|
return *candidate;
|
|
|
|
auto parent = m_dom_node_to_parent_map.get(candidate);
|
|
if (!parent.has_value())
|
|
break;
|
|
candidate = parent.value();
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
JsonObject WalkerActor::serialize_disconnected_node(JsonObject const& node) const
|
|
{
|
|
JsonArray new_parents;
|
|
for (auto parent = m_dom_node_to_parent_map.get(&node); parent.has_value() && parent.value() && parent.value() != &m_dom_tree; parent = m_dom_node_to_parent_map.get(parent.value()))
|
|
new_parents.must_append(serialize_node(*parent.value()));
|
|
|
|
JsonObject disconnected_node;
|
|
disconnected_node.set("node"sv, serialize_node(node));
|
|
disconnected_node.set("newParents"sv, move(new_parents));
|
|
return disconnected_node;
|
|
}
|
|
|
|
void WalkerActor::handle_node_picker_event(DevToolsDelegate::NodePickerEvent event)
|
|
{
|
|
if (!m_is_picking_node && event.type != DevToolsDelegate::NodePickerEvent::Type::Canceled)
|
|
return;
|
|
|
|
auto tab = m_tab.strong_ref();
|
|
if (!tab)
|
|
return;
|
|
|
|
if (event.type == DevToolsDelegate::NodePickerEvent::Type::Canceled) {
|
|
stop_node_picker();
|
|
|
|
JsonObject packet;
|
|
packet.set("type"sv, "pickerNodeCanceled"sv);
|
|
send_message(move(packet));
|
|
return;
|
|
}
|
|
|
|
if (!event.node_id.has_value() || *event.node_id == 0) {
|
|
if (event.type == DevToolsDelegate::NodePickerEvent::Type::Hovered)
|
|
devtools().delegate().clear_node_picker(tab->description());
|
|
return;
|
|
}
|
|
|
|
auto actor_name = node_actor_name_for(*event.node_id);
|
|
if (!actor_name.has_value())
|
|
return;
|
|
|
|
auto dom_node = this->dom_node(actor_name.value());
|
|
if (!dom_node.has_value())
|
|
return;
|
|
|
|
auto picker_node = element_node_for_picker_node(dom_node->node);
|
|
if (!picker_node.has_value())
|
|
return;
|
|
|
|
auto picker_node_id = picker_node->get_integer<Web::UniqueNodeID::Type>("id"sv);
|
|
if (!picker_node_id.has_value())
|
|
return;
|
|
Web::UniqueNodeID picked_node_id { *picker_node_id };
|
|
|
|
if (event.type == DevToolsDelegate::NodePickerEvent::Type::Hovered) {
|
|
if (m_picker_hovered_node_id == picked_node_id)
|
|
return;
|
|
|
|
m_picker_hovered_node_id = picked_node_id;
|
|
devtools().delegate().highlight_dom_node(tab->description(), picked_node_id, {});
|
|
}
|
|
|
|
JsonObject packet;
|
|
switch (event.type) {
|
|
case DevToolsDelegate::NodePickerEvent::Type::Hovered:
|
|
packet.set("type"sv, "pickerNodeHovered"sv);
|
|
break;
|
|
case DevToolsDelegate::NodePickerEvent::Type::Picked:
|
|
packet.set("type"sv, "pickerNodePicked"sv);
|
|
break;
|
|
case DevToolsDelegate::NodePickerEvent::Type::Previewed:
|
|
packet.set("type"sv, "pickerNodePreviewed"sv);
|
|
break;
|
|
case DevToolsDelegate::NodePickerEvent::Type::Canceled:
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
|
|
packet.set("node"sv, serialize_disconnected_node(*picker_node));
|
|
send_message(move(packet));
|
|
|
|
if (event.type == DevToolsDelegate::NodePickerEvent::Type::Picked)
|
|
stop_node_picker();
|
|
}
|
|
|
|
void WalkerActor::stop_node_picker()
|
|
{
|
|
if (!m_is_picking_node)
|
|
return;
|
|
|
|
if (auto tab = m_tab.strong_ref()) {
|
|
devtools().delegate().stop_node_picker(tab->description());
|
|
devtools().delegate().clear_node_picker(tab->description());
|
|
}
|
|
|
|
m_is_picking_node = false;
|
|
m_picker_hovered_node_id.clear();
|
|
}
|
|
|
|
Optional<JsonObject const&> WalkerActor::find_node_by_selector(JsonObject const& node, StringView selector)
|
|
{
|
|
auto matches = [&](auto const& candidate) {
|
|
return candidate.get_string("name"sv)->equals_ignoring_ascii_case(selector);
|
|
};
|
|
|
|
if (matches(node))
|
|
return node;
|
|
|
|
if (auto children = node.get_array("children"sv); children.has_value()) {
|
|
for (size_t i = 0; i < children->size(); ++i) {
|
|
auto const& child = children->at(i);
|
|
|
|
if (matches(child.as_object()))
|
|
return child.as_object();
|
|
|
|
if (auto result = find_node_by_selector(child.as_object(), selector); result.has_value())
|
|
return result;
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
enum class Direction {
|
|
Previous,
|
|
Next,
|
|
};
|
|
static Optional<JsonObject const&> sibling_for_node(JsonObject const& parent, JsonObject const& node, Direction direction)
|
|
{
|
|
auto children = parent.get_array("children"sv);
|
|
VERIFY(children.has_value());
|
|
|
|
auto index = children->values().find_first_index_if([&](auto const& child) {
|
|
return &child.as_object() == &node;
|
|
});
|
|
VERIFY(index.has_value());
|
|
|
|
switch (direction) {
|
|
case Direction::Previous:
|
|
if (*index == 0)
|
|
return {};
|
|
index = *index - 1;
|
|
break;
|
|
|
|
case Direction::Next:
|
|
if (*index == children->size() - 1)
|
|
return {};
|
|
index = *index + 1;
|
|
break;
|
|
}
|
|
|
|
return children->at(*index).as_object();
|
|
}
|
|
|
|
Optional<JsonObject const&> WalkerActor::previous_sibling_for_node(JsonObject const& node)
|
|
{
|
|
auto parent = m_dom_node_to_parent_map.get(&node);
|
|
if (!parent.has_value() || !parent.value())
|
|
return {};
|
|
return sibling_for_node(*parent.value(), node, Direction::Previous);
|
|
}
|
|
|
|
Optional<JsonObject const&> WalkerActor::next_sibling_for_node(JsonObject const& node)
|
|
{
|
|
auto parent = m_dom_node_to_parent_map.get(&node);
|
|
if (!parent.has_value() || !parent.value())
|
|
return {};
|
|
return sibling_for_node(*parent.value(), node, Direction::Next);
|
|
}
|
|
|
|
Optional<JsonObject const&> WalkerActor::parent_grid_node_for_node(JsonObject const& node) const
|
|
{
|
|
auto parent = m_dom_node_to_parent_map.get(&node);
|
|
while (parent.has_value() && parent.value()) {
|
|
auto display = parent.value()->get_string("display"sv);
|
|
if (!display.has_value())
|
|
return {};
|
|
if (display->contains("grid"sv))
|
|
return *parent.value();
|
|
if (*display != "contents"sv)
|
|
return {};
|
|
|
|
parent = m_dom_node_to_parent_map.get(parent.value());
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
Optional<JsonObject const&> WalkerActor::remove_node(JsonObject const& node)
|
|
{
|
|
auto maybe_parent = m_dom_node_to_parent_map.get(&node);
|
|
if (!maybe_parent.has_value() || !maybe_parent.value())
|
|
return {};
|
|
auto const& parent = *maybe_parent.value();
|
|
|
|
auto children = parent.get_array("children"sv);
|
|
VERIFY(children.has_value());
|
|
|
|
const_cast<JsonArray&>(*children).values().remove_first_matching([&](auto const& child) {
|
|
return &child.as_object() == &node;
|
|
});
|
|
|
|
populate_dom_tree_cache();
|
|
return parent;
|
|
}
|
|
|
|
void WalkerActor::new_dom_node_mutation(WebView::Mutation mutation)
|
|
{
|
|
if (!m_has_live_dom_tree)
|
|
return;
|
|
|
|
auto serialized_target = JsonValue::from_string(mutation.serialized_target);
|
|
if (serialized_target.is_error() || !serialized_target.value().is_object()) {
|
|
dbgln_if(DEVTOOLS_DEBUG, "Unable to parse serialized target as JSON object: {}", serialized_target.error());
|
|
return;
|
|
}
|
|
|
|
if (!replace_node_in_tree(move(serialized_target.release_value().as_object()))) {
|
|
dbgln_if(DEVTOOLS_DEBUG, "Unable to apply mutation to DOM tree");
|
|
return;
|
|
}
|
|
|
|
m_dom_node_mutations.append(move(mutation));
|
|
|
|
if (m_has_new_mutations_since_last_mutations_request)
|
|
return;
|
|
|
|
JsonObject message;
|
|
message.set("type"sv, "newMutations"sv);
|
|
send_message(move(message));
|
|
|
|
m_has_new_mutations_since_last_mutations_request = true;
|
|
}
|
|
|
|
JsonValue WalkerActor::serialize_mutations()
|
|
{
|
|
JsonArray mutations;
|
|
mutations.ensure_capacity(m_dom_node_mutations.size());
|
|
|
|
for (auto& mutation : m_dom_node_mutations) {
|
|
auto target = m_dom_node_id_to_actor_map.get(mutation.target);
|
|
if (!target.has_value())
|
|
continue;
|
|
|
|
JsonObject serialized;
|
|
serialized.set("target"sv, target.release_value());
|
|
serialized.set("type"sv, move(mutation.type));
|
|
|
|
mutation.mutation.visit(
|
|
[&](WebView::AttributeMutation& mutation) {
|
|
serialized.set("attributeName"sv, move(mutation.attribute_name));
|
|
|
|
if (mutation.new_value.has_value())
|
|
serialized.set("newValue"sv, mutation.new_value.release_value());
|
|
else
|
|
serialized.set("newValue"sv, JsonValue {});
|
|
},
|
|
[&](WebView::CharacterDataMutation& mutation) {
|
|
serialized.set("newValue"sv, move(mutation.new_value));
|
|
},
|
|
[&](WebView::ChildListMutation const& mutation) {
|
|
JsonArray added;
|
|
JsonArray removed;
|
|
|
|
for (auto id : mutation.added) {
|
|
if (auto node = m_dom_node_id_to_actor_map.get(id); node.has_value())
|
|
added.must_append(node.release_value());
|
|
}
|
|
for (auto id : mutation.removed) {
|
|
if (auto node = m_dom_node_id_to_actor_map.get(id); node.has_value())
|
|
removed.must_append(node.release_value());
|
|
}
|
|
|
|
serialized.set("added"sv, move(added));
|
|
serialized.set("removed"sv, move(removed));
|
|
serialized.set("numChildren"sv, mutation.target_child_count);
|
|
});
|
|
|
|
mutations.must_append(move(serialized));
|
|
}
|
|
|
|
m_dom_node_mutations.clear();
|
|
return mutations;
|
|
}
|
|
|
|
bool WalkerActor::replace_node_in_tree(JsonObject replacement)
|
|
{
|
|
auto const& actor = actor_for_node(replacement);
|
|
|
|
auto node = m_actor_to_dom_node_map.get(actor.name());
|
|
if (!node.has_value() || !node.value())
|
|
return false;
|
|
|
|
const_cast<JsonObject&>(*node.value()) = move(replacement);
|
|
populate_dom_tree_cache();
|
|
|
|
return true;
|
|
}
|
|
|
|
void WalkerActor::populate_dom_tree_cache()
|
|
{
|
|
clear_dom_tree_cache();
|
|
|
|
if (!m_has_live_dom_tree)
|
|
return;
|
|
|
|
populate_dom_tree_cache(m_dom_tree, nullptr);
|
|
}
|
|
|
|
void WalkerActor::document_unloaded()
|
|
{
|
|
if (!m_has_live_dom_tree)
|
|
return;
|
|
|
|
auto root = serialize_root();
|
|
|
|
stop_node_picker();
|
|
clear_dom_tree_state();
|
|
m_has_live_dom_tree = false;
|
|
|
|
JsonObject message;
|
|
message.set("type"sv, "root-destroyed"sv);
|
|
message.set("node"sv, move(root));
|
|
send_message(move(message));
|
|
}
|
|
|
|
void WalkerActor::replace_dom_tree(JsonObject dom_tree)
|
|
{
|
|
stop_node_picker();
|
|
clear_dom_tree_state();
|
|
|
|
m_dom_tree = move(dom_tree);
|
|
m_has_live_dom_tree = true;
|
|
populate_dom_tree_cache();
|
|
|
|
JsonObject message;
|
|
message.set("type"sv, "root-available"sv);
|
|
message.set("node"sv, serialize_root());
|
|
send_message(move(message));
|
|
}
|
|
|
|
void WalkerActor::clear_dom_tree_state()
|
|
{
|
|
m_dom_node_mutations.clear();
|
|
m_has_new_mutations_since_last_mutations_request = false;
|
|
clear_dom_tree_cache();
|
|
m_node_actors.clear();
|
|
}
|
|
|
|
void WalkerActor::clear_dom_tree_cache()
|
|
{
|
|
m_dom_node_to_parent_map.clear();
|
|
m_actor_to_dom_node_map.clear();
|
|
m_dom_node_id_to_actor_map.clear();
|
|
}
|
|
|
|
void WalkerActor::populate_dom_tree_cache(JsonObject& node, JsonObject const* parent)
|
|
{
|
|
auto const& node_actor = actor_for_node(node);
|
|
node.set("actor"sv, node_actor.name());
|
|
|
|
m_dom_node_to_parent_map.set(&node, parent);
|
|
m_actor_to_dom_node_map.set(node_actor.name(), &node);
|
|
|
|
if (!node_actor.node_identifier().pseudo_element.has_value())
|
|
m_dom_node_id_to_actor_map.set(node_actor.node_identifier().id, node_actor.name());
|
|
|
|
auto children = node.get_array("children"sv);
|
|
if (!children.has_value())
|
|
return;
|
|
|
|
children->values().remove_all_matching([&](JsonValue const& child) {
|
|
return !is_suitable_for_dom_inspection(child);
|
|
});
|
|
|
|
children->for_each([&](JsonValue& child) {
|
|
populate_dom_tree_cache(child.as_object(), &node);
|
|
});
|
|
}
|
|
|
|
NodeActor const& WalkerActor::actor_for_node(JsonObject const& node)
|
|
{
|
|
auto identifier = NodeIdentifier::for_node(node);
|
|
|
|
if (auto it = m_node_actors.find(identifier); it != m_node_actors.end()) {
|
|
if (auto node_actor = it->value.strong_ref())
|
|
return *node_actor;
|
|
}
|
|
|
|
auto& node_actor = devtools().register_actor<NodeActor>(identifier, *this);
|
|
m_node_actors.set(identifier, node_actor);
|
|
|
|
return node_actor;
|
|
}
|
|
|
|
}
|