Files
servo/components/devtools/actors/tab.rs
Brent Schroeter 4c69d856f6 tools: Support tab navigation via navigateTo, goBack, and goForward (#43026)
Add support for navigation requests ("navigateTo", "goBack", and
"goForward") over the Remote Debugging Protocol. These may be sent by a
UI client in response to user input (for example the address bar in the
Firefox inspector), or they can be used to automate navigation during
unit tests.

This currently only supports navigation within the URL domain at which
servoshell is initially launched, due to a bug in servo's
`BrowsingContextActor` implementation. (Unit tests covering a fix for
that issue will depend on this change.)

Testing: The behavior of all 3 new message types is covered by a new
test case—`test_navigation`—in the devtools unit test suite.
Fixes: #38668

---------

Signed-off-by: Brent Schroeter <contact@brentsch.com>
Co-authored-by: eri <eri@igalia.com>
2026-03-06 09:13:39 +00:00

216 lines
7.0 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/. */
//! Descriptor actor that represents a web view. It can link a tab to the corresponding watcher
//! actor to enable inspection.
//!
//! Liberally derived from the [Firefox JS implementation].
//!
//! [Firefox JS implementation]: https://searchfox.org/mozilla-central/source/devtools/server/actors/descriptors/tab.js
use devtools_traits::DevtoolScriptControlMsg;
use malloc_size_of_derive::MallocSizeOf;
use serde::Serialize;
use serde_json::{Map, Value};
use servo_url::ServoUrl;
use crate::actor::{Actor, ActorEncode, ActorError, ActorRegistry};
use crate::actors::browsing_context::{BrowsingContextActor, BrowsingContextActorMsg};
use crate::actors::root::{DescriptorTraits, RootActor};
use crate::actors::watcher::{WatcherActor, WatcherActorMsg};
use crate::protocol::ClientRequest;
use crate::{EmptyReplyMsg, StreamId};
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct TabDescriptorActorMsg {
actor: String,
/// This correspond to webview_id
#[serde(rename = "browserId")]
browser_id: u32,
#[serde(rename = "browsingContextID")]
browsing_context_id: u32,
is_zombie_tab: bool,
#[serde(rename = "outerWindowID")]
outer_window_id: u32,
pub selected: bool,
title: String,
traits: DescriptorTraits,
url: String,
}
impl TabDescriptorActorMsg {
pub fn browser_id(&self) -> u32 {
self.browser_id
}
pub fn actor(&self) -> String {
self.actor.clone()
}
}
#[derive(Serialize)]
struct GetTargetReply {
from: String,
frame: BrowsingContextActorMsg,
}
#[derive(Serialize)]
struct GetFaviconReply {
from: String,
favicon: String,
}
#[derive(Serialize)]
struct GetWatcherReply {
from: String,
#[serde(flatten)]
watcher: WatcherActorMsg,
}
#[derive(MallocSizeOf)]
pub(crate) struct TabDescriptorActor {
name: String,
browsing_context_actor: String,
is_top_level_global: bool,
}
impl Actor for TabDescriptorActor {
fn name(&self) -> String {
self.name.clone()
}
/// The tab actor can handle the following messages:
///
/// - `getTarget`: Returns the surrounding `BrowsingContextActor`.
///
/// - `getFavicon`: Should return the tab favicon, but it is not yet supported.
///
/// - `getWatcher`: Returns a `WatcherActor` linked to the tab's `BrowsingContext`. It is used
/// to describe the debugging capabilities of this tab.
///
/// - `reloadDescriptor`: Causes the page to reload.
fn handle_message(
&self,
request: ClientRequest,
registry: &ActorRegistry,
msg_type: &str,
msg: &Map<String, Value>,
_id: StreamId,
) -> Result<(), ActorError> {
let ctx_actor = registry.find::<BrowsingContextActor>(&self.browsing_context_actor);
let pipeline = ctx_actor.pipeline_id();
match msg_type {
"getTarget" => request.reply_final(&GetTargetReply {
from: self.name(),
frame: registry.encode::<BrowsingContextActor, _>(&self.browsing_context_actor),
})?,
"getFavicon" => {
// TODO: Return a favicon when available
request.reply_final(&GetFaviconReply {
from: self.name(),
favicon: String::new(),
})?
},
"getWatcher" => request.reply_final(&GetWatcherReply {
from: self.name(),
watcher: registry.encode::<WatcherActor, _>(&ctx_actor.watcher),
})?,
"goBack" => {
ctx_actor
.script_chan
.send(DevtoolScriptControlMsg::GoBack(pipeline))
.map_err(|_| ActorError::Internal)?;
request.reply_final(&EmptyReplyMsg { from: self.name() })?
},
"goForward" => {
ctx_actor
.script_chan
.send(DevtoolScriptControlMsg::GoForward(pipeline))
.map_err(|_| ActorError::Internal)?;
request.reply_final(&EmptyReplyMsg { from: self.name() })?
},
"navigateTo" => {
if msg.get("waitForLoad").unwrap_or(&Value::Bool(false)) != &Value::Bool(false) {
log::warn!("waitForLoad option for devtools navigation is not supported.");
}
let url = msg
.get("url")
.and_then(|value| value.as_str())
.map(ServoUrl::parse)
.ok_or(ActorError::Internal)?
.map_err(|_| ActorError::Internal)?;
ctx_actor
.script_chan
.send(DevtoolScriptControlMsg::NavigateTo(pipeline, url))
.map_err(|_| ActorError::Internal)?;
request.reply_final(&EmptyReplyMsg { from: self.name() })?
},
"reloadDescriptor" => {
// There is an extra bypassCache parameter that we don't currently use.
ctx_actor
.script_chan
.send(DevtoolScriptControlMsg::Reload(pipeline))
.map_err(|_| ActorError::Internal)?;
request.reply_final(&EmptyReplyMsg { from: self.name() })?
},
_ => return Err(ActorError::UnrecognizedPacketType),
};
Ok(())
}
}
impl TabDescriptorActor {
pub(crate) fn new(
actors: &ActorRegistry,
browsing_context_actor: String,
is_top_level_global: bool,
) -> TabDescriptorActor {
let name = actors.new_name::<Self>();
let root = actors.find::<RootActor>("root");
root.tabs.borrow_mut().push(name.clone());
TabDescriptorActor {
name,
browsing_context_actor,
is_top_level_global,
}
}
pub(crate) fn is_top_level_global(&self) -> bool {
self.is_top_level_global
}
pub fn browsing_context(&self) -> String {
self.browsing_context_actor.clone()
}
}
impl ActorEncode<TabDescriptorActorMsg> for TabDescriptorActor {
fn encode(&self, registry: &ActorRegistry) -> TabDescriptorActorMsg {
let ctx_actor = registry.find::<BrowsingContextActor>(&self.browsing_context_actor);
let title = ctx_actor.title.borrow().clone();
let url = ctx_actor.url.borrow().clone();
TabDescriptorActorMsg {
actor: self.name(),
browser_id: ctx_actor.browser_id.value(),
browsing_context_id: ctx_actor.browsing_context_id.value(),
is_zombie_tab: false,
outer_window_id: ctx_actor.outer_window_id().value(),
selected: false,
title,
traits: DescriptorTraits {
watcher: true,
supports_navigation: true,
supports_reload_descriptor: true,
},
url,
}
}
}