Files
servo/components/devtools/actors/browsing_context.rs
Simon Wülker a60f9370c7 devtools: Apply attribute modifications in the inspector to the DOM tree (#42601)
The inspector view allows modifying the attributes of DOM elements.
However, we lie to the devtools client: While it looks like the
attributes change, the changes are never actually applied to the DOM.

This change fixes that, and also makes it so attribute modifications
from non-inspector sources are shown in the inspector.

Testing: This change adds two tests

---------

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
2026-02-17 13:05:41 +00:00

384 lines
13 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! Liberally derived from the [Firefox JS implementation](https://searchfox.org/mozilla-central/source/devtools/server/actors/webbrowser.js).
//! Connection point for remote devtools that wish to investigate a particular Browsing Context's contents.
//! Supports dynamic attaching and detaching which control notifications of navigation, etc.
use std::net::TcpStream;
use atomic_refcell::AtomicRefCell;
use base::generic_channel::{self, GenericSender, SendError};
use base::id::PipelineId;
use devtools_traits::DevtoolScriptControlMsg::{self, GetCssDatabase, SimulateColorScheme};
use devtools_traits::{DevtoolsPageInfo, NavigationState};
use embedder_traits::Theme;
use malloc_size_of_derive::MallocSizeOf;
use serde::Serialize;
use serde_json::{Map, Value};
use crate::actor::{Actor, ActorEncode, ActorError, ActorRegistry};
use crate::actors::inspector::InspectorActor;
use crate::actors::inspector::accessibility::AccessibilityActor;
use crate::actors::inspector::css_properties::CssPropertiesActor;
use crate::actors::reflow::ReflowActor;
use crate::actors::stylesheets::StyleSheetsActor;
use crate::actors::tab::TabDescriptorActor;
use crate::actors::thread::ThreadActor;
use crate::actors::watcher::{SessionContext, SessionContextType, WatcherActor};
use crate::id::{DevtoolsBrowserId, DevtoolsBrowsingContextId, DevtoolsOuterWindowId, IdMap};
use crate::protocol::{ClientRequest, JsonPacketStream};
use crate::resource::ResourceAvailable;
use crate::{EmptyReplyMsg, StreamId};
#[derive(Serialize)]
struct ListWorkersReply {
from: String,
workers: Vec<()>,
}
#[derive(Serialize)]
struct FrameUpdateReply {
from: String,
#[serde(rename = "type")]
type_: String,
frames: Vec<FrameUpdateMsg>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct FrameUpdateMsg {
id: u32,
is_top_level: bool,
url: String,
title: String,
}
#[derive(Serialize)]
struct TabNavigated {
from: String,
#[serde(rename = "type")]
type_: String,
url: String,
title: Option<String>,
#[serde(rename = "nativeConsoleAPI")]
native_console_api: bool,
state: String,
is_frame_switching: bool,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct BrowsingContextTraits {
frames: bool,
is_browsing_context: bool,
log_in_page: bool,
navigation: bool,
supports_top_level_target_flag: bool,
watchpoints: bool,
}
#[derive(Serialize)]
#[serde(rename_all = "lowercase")]
enum TargetType {
Frame,
// Other target types not implemented yet.
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct BrowsingContextActorMsg {
actor: String,
title: String,
url: String,
/// This correspond to webview_id
#[serde(rename = "browserId")]
browser_id: u32,
#[serde(rename = "outerWindowID")]
outer_window_id: u32,
#[serde(rename = "browsingContextID")]
browsing_context_id: u32,
is_top_level_target: bool,
traits: BrowsingContextTraits,
// Implemented actors
accessibility_actor: String,
console_actor: String,
css_properties_actor: String,
inspector_actor: String,
reflow_actor: String,
style_sheets_actor: String,
thread_actor: String,
target_type: TargetType,
// Part of the official protocol, but not yet implemented.
// animations_actor: String,
// changes_actor: String,
// framerate_actor: String,
// manifest_actor: String,
// memory_actor: String,
// network_content_actor: String,
// objects_manager: String,
// performance_actor: String,
// resonsive_actor: String,
// storage_actor: String,
// tracer_actor: String,
// web_extension_inspected_window_actor: String,
// web_socket_actor: String,
}
/// The browsing context actor encompasses all of the other supporting actors when debugging a web
/// view. To this extent, it contains a watcher actor that helps when communicating with the host,
/// as well as resource actors that each perform one debugging function.
#[derive(MallocSizeOf)]
pub(crate) struct BrowsingContextActor {
name: String,
pub title: AtomicRefCell<String>,
pub url: AtomicRefCell<String>,
/// This corresponds to webview_id
pub browser_id: DevtoolsBrowserId,
// TODO: Should these ids be atomic?
active_pipeline_id: AtomicRefCell<PipelineId>,
active_outer_window_id: AtomicRefCell<DevtoolsOuterWindowId>,
pub browsing_context_id: DevtoolsBrowsingContextId,
accessibility: String,
pub console: String,
css_properties: String,
pub(crate) inspector: String,
reflow: String,
style_sheets: String,
pub thread: String,
_tab: String,
pub script_chan: GenericSender<DevtoolScriptControlMsg>,
pub watcher: String,
}
impl ResourceAvailable for BrowsingContextActor {
fn actor_name(&self) -> String {
self.name.clone()
}
}
impl Actor for BrowsingContextActor {
fn name(&self) -> String {
self.name.clone()
}
fn handle_message(
&self,
request: ClientRequest,
_registry: &ActorRegistry,
msg_type: &str,
_msg: &Map<String, Value>,
_id: StreamId,
) -> Result<(), ActorError> {
match msg_type {
"listFrames" => {
// TODO: Find out what needs to be listed here
let msg = EmptyReplyMsg { from: self.name() };
request.reply_final(&msg)?
},
"listWorkers" => {
request.reply_final(&ListWorkersReply {
from: self.name(),
// TODO: Find out what needs to be listed here
workers: vec![],
})?
},
_ => return Err(ActorError::UnrecognizedPacketType),
};
Ok(())
}
}
impl BrowsingContextActor {
#[expect(clippy::too_many_arguments)]
pub(crate) fn new(
console: String,
browser_id: DevtoolsBrowserId,
browsing_context_id: DevtoolsBrowsingContextId,
page_info: DevtoolsPageInfo,
pipeline_id: PipelineId,
outer_window_id: DevtoolsOuterWindowId,
script_sender: GenericSender<DevtoolScriptControlMsg>,
actors: &ActorRegistry,
) -> BrowsingContextActor {
let name = actors.new_name::<BrowsingContextActor>();
let DevtoolsPageInfo {
title,
url,
is_top_level_global,
} = page_info;
let accessibility = AccessibilityActor::new(actors.new_name::<AccessibilityActor>());
let properties = (|| {
let (properties_sender, properties_receiver) = generic_channel::channel()?;
script_sender.send(GetCssDatabase(properties_sender)).ok()?;
properties_receiver.recv().ok()
})()
.unwrap_or_default();
let css_properties =
CssPropertiesActor::new(actors.new_name::<CssPropertiesActor>(), properties);
let inspector = InspectorActor::register(actors, pipeline_id, script_sender.clone());
let reflow = ReflowActor::new(actors.new_name::<ReflowActor>());
let style_sheets = StyleSheetsActor::new(actors.new_name::<StyleSheetsActor>());
let tabdesc = TabDescriptorActor::new(actors, name.clone(), is_top_level_global);
let thread = ThreadActor::new(actors.new_name::<ThreadActor>(), script_sender.clone());
let watcher = WatcherActor::new(
actors,
name.clone(),
SessionContext::new(SessionContextType::BrowserElement),
);
let target = BrowsingContextActor {
name,
script_chan: script_sender,
title: AtomicRefCell::new(title),
url: AtomicRefCell::new(url.into_string()),
active_pipeline_id: AtomicRefCell::new(pipeline_id),
active_outer_window_id: AtomicRefCell::new(outer_window_id),
browser_id,
browsing_context_id,
accessibility: accessibility.name(),
console,
css_properties: css_properties.name(),
inspector,
reflow: reflow.name(),
style_sheets: style_sheets.name(),
_tab: tabdesc.name(),
thread: thread.name(),
watcher: watcher.name(),
};
actors.register(accessibility);
actors.register(css_properties);
actors.register(reflow);
actors.register(style_sheets);
actors.register(tabdesc);
actors.register(thread);
actors.register(watcher);
target
}
pub(crate) fn navigate<'a>(
&self,
state: NavigationState,
id_map: &mut IdMap,
connections: impl Iterator<Item = &'a mut TcpStream>,
) {
let (pipeline_id, title, url, state) = match state {
NavigationState::Start(url) => (None, None, url, "start"),
NavigationState::Stop(pipeline, info) => {
(Some(pipeline), Some(info.title), info.url, "stop")
},
};
if let Some(pipeline_id) = pipeline_id {
let outer_window_id = id_map.outer_window_id(pipeline_id);
*self.active_outer_window_id.borrow_mut() = outer_window_id;
*self.active_pipeline_id.borrow_mut() = pipeline_id;
}
url.as_str().clone_into(&mut self.url.borrow_mut());
if let Some(ref t) = title {
self.title.borrow_mut().clone_from(t);
}
let msg = TabNavigated {
from: self.name(),
type_: "tabNavigated".to_owned(),
url: url.as_str().to_owned(),
title,
native_console_api: true,
state: state.to_owned(),
is_frame_switching: false,
};
for stream in connections {
let _ = stream.write_json_packet(&msg);
}
}
pub(crate) fn title_changed(&self, pipeline_id: PipelineId, title: String) {
if pipeline_id != self.pipeline_id() {
return;
}
*self.title.borrow_mut() = title;
}
pub(crate) fn frame_update(&self, request: &mut ClientRequest) {
let _ = request.write_json_packet(&FrameUpdateReply {
from: self.name(),
type_: "frameUpdate".into(),
frames: vec![FrameUpdateMsg {
id: self.browsing_context_id.value(),
is_top_level: true,
title: self.title.borrow().clone(),
url: self.url.borrow().clone(),
}],
});
}
pub fn simulate_color_scheme(&self, theme: Theme) -> Result<(), ()> {
self.script_chan
.send(SimulateColorScheme(self.pipeline_id(), theme))
.map_err(|_| ())
}
pub(crate) fn pipeline_id(&self) -> PipelineId {
*self.active_pipeline_id.borrow()
}
pub(crate) fn outer_window_id(&self) -> DevtoolsOuterWindowId {
*self.active_outer_window_id.borrow()
}
pub(crate) fn instruct_script_to_send_live_updates(&self, should_send_updates: bool) {
let result = self
.script_chan
.send(DevtoolScriptControlMsg::WantsLiveNotifications(
self.pipeline_id(),
should_send_updates,
));
// Notifying the script thread may fail with a "Disconnected" error if servo
// as a whole is being shut down.
debug_assert!(matches!(result, Ok(_) | Err(SendError::Disconnected)));
}
}
impl ActorEncode<BrowsingContextActorMsg> for BrowsingContextActor {
fn encode(&self, _: &ActorRegistry) -> BrowsingContextActorMsg {
BrowsingContextActorMsg {
actor: self.name(),
traits: BrowsingContextTraits {
is_browsing_context: true,
frames: true,
log_in_page: false,
navigation: true,
supports_top_level_target_flag: true,
watchpoints: true,
},
title: self.title.borrow().clone(),
url: self.url.borrow().clone(),
browser_id: self.browser_id.value(),
browsing_context_id: self.browsing_context_id.value(),
outer_window_id: self.outer_window_id().value(),
is_top_level_target: true,
accessibility_actor: self.accessibility.clone(),
console_actor: self.console.clone(),
css_properties_actor: self.css_properties.clone(),
inspector_actor: self.inspector.clone(),
reflow_actor: self.reflow.clone(),
style_sheets_actor: self.style_sheets.clone(),
thread_actor: self.thread.clone(),
target_type: TargetType::Frame,
}
}
}