use std::sync::Arc;
use std::sync::Mutex;
use serde_json::Value;
use super::error::BridgeError;
#[derive(Debug, Clone, Default)]
pub struct NodeDescriptor {
pub node_id: i64,
pub node_name: String,
pub node_value: String,
pub backend_node_id: i64,
pub children: Vec<NodeDescriptor>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BridgeScreenshotFormat {
Jpeg,
Png,
Webp,
}
impl BridgeScreenshotFormat {
pub fn from_cdp(s: Option<&str>) -> Self {
match s {
Some("jpeg") => Self::Jpeg,
Some("webp") => Self::Webp,
_ => Self::Png,
}
}
}
pub trait ServoBackend: Send + Sync {
fn page_navigate(&self, target_id: &str, url: &str) -> Result<NavigateResult, BridgeError>;
fn page_reload(&self, target_id: &str) -> Result<(), BridgeError>;
fn page_screenshot(
&self,
target_id: &str,
format: BridgeScreenshotFormat,
) -> Result<Vec<u8>, BridgeError>;
fn page_frame_tree(&self, target_id: &str) -> Result<FrameTree, BridgeError>;
fn page_navigation_history(&self, target_id: &str) -> Result<NavigationHistory, BridgeError>;
fn page_navigate_to_history_entry(
&self,
target_id: &str,
entry_id: i64,
) -> Result<(), BridgeError>;
fn page_set_content(&self, target_id: &str, html: &str) -> Result<(), BridgeError>;
fn page_close(&self, target_id: &str) -> Result<(), BridgeError>;
fn page_bring_to_front(&self, target_id: &str) -> Result<(), BridgeError>;
fn page_layout_metrics(&self, target_id: &str) -> Result<LayoutMetrics, BridgeError>;
fn page_print_to_pdf(&self, target_id: &str) -> Result<Vec<u8>, BridgeError>;
fn runtime_evaluate(
&self,
target_id: &str,
expression: &str,
) -> Result<EvaluateResult, BridgeError>;
fn runtime_call_function_on(
&self,
target_id: &str,
object_id: &str,
function_declaration: &str,
args: &[Value],
) -> Result<EvaluateResult, BridgeError>;
fn runtime_get_properties(
&self,
target_id: &str,
object_id: &str,
own_properties: bool,
) -> Result<Vec<PropertyDescriptor>, BridgeError>;
fn runtime_release_object(&self, target_id: &str, object_id: &str) -> Result<(), BridgeError>;
fn runtime_enable(&self, target_id: &str) -> Result<(), BridgeError>;
fn runtime_disable(&self, target_id: &str) -> Result<(), BridgeError>;
fn dom_get_document(&self, target_id: &str, depth: i64) -> Result<NodeDescriptor, BridgeError>;
fn dom_query_selector(
&self,
target_id: &str,
node_id: i64,
selector: &str,
) -> Result<Option<i64>, BridgeError>;
fn dom_query_selector_all(
&self,
target_id: &str,
node_id: i64,
selector: &str,
) -> Result<Vec<i64>, BridgeError>;
fn dom_get_box_model(&self, target_id: &str, node_id: i64) -> Result<BoxModel, BridgeError>;
fn dom_resolve_node(
&self,
target_id: &str,
backend_node_id: i64,
) -> Result<RemoteObject, BridgeError>;
fn dom_describe_node(
&self,
target_id: &str,
node_id: i64,
depth: i64,
) -> Result<NodeDescriptor, BridgeError>;
fn dom_set_attribute(
&self,
target_id: &str,
node_id: i64,
name: &str,
value: &str,
) -> Result<(), BridgeError>;
fn dom_remove_attribute(
&self,
target_id: &str,
node_id: i64,
name: &str,
) -> Result<(), BridgeError>;
fn dom_get_outer_html(&self, target_id: &str, node_id: i64) -> Result<String, BridgeError>;
fn dom_set_outer_html(
&self,
target_id: &str,
node_id: i64,
html: &str,
) -> Result<(), BridgeError>;
fn dom_request_node(&self, target_id: &str, object_id: &str) -> Result<i64, BridgeError>;
fn network_enable(&self, target_id: &str) -> Result<(), BridgeError>;
fn network_disable(&self, target_id: &str) -> Result<(), BridgeError>;
fn network_get_response_body(
&self,
target_id: &str,
request_id: &str,
) -> Result<ResponseBody, BridgeError>;
fn network_set_cache_disabled(
&self,
target_id: &str,
disabled: bool,
) -> Result<(), BridgeError>;
fn input_dispatch_mouse_event(
&self,
target_id: &str,
event: MouseEvent,
) -> Result<(), BridgeError>;
fn input_dispatch_key_event(&self, target_id: &str, event: KeyEvent)
-> Result<(), BridgeError>;
fn input_dispatch_touch_event(
&self,
target_id: &str,
event_type: &str,
touch_points: &[TouchPoint],
) -> Result<(), BridgeError>;
fn input_set_ignore_input_events(
&self,
target_id: &str,
ignore: bool,
) -> Result<(), BridgeError>;
fn emulation_set_device_metrics(
&self,
target_id: &str,
metrics: DeviceMetrics,
) -> Result<(), BridgeError>;
fn emulation_clear_device_metrics(&self, target_id: &str) -> Result<(), BridgeError>;
fn emulation_set_user_agent_override(
&self,
target_id: &str,
user_agent: &str,
) -> Result<(), BridgeError>;
fn emulation_set_geolocation_override(
&self,
target_id: &str,
latitude: f64,
longitude: f64,
accuracy: f64,
) -> Result<(), BridgeError>;
fn target_get_targets(&self) -> Result<Vec<TargetInfo>, BridgeError>;
fn target_create_target(&self, url: &str) -> Result<String, BridgeError>;
fn target_close_target(&self, target_id: &str) -> Result<(), BridgeError>;
fn target_attach_to_target(&self, target_id: &str) -> Result<String, BridgeError>;
fn target_detach_from_target(&self, session_id: &str) -> Result<(), BridgeError>;
fn target_set_auto_attach(
&self,
target_id: &str,
auto_attach: bool,
wait_for_debugger_on_start: bool,
) -> Result<(), BridgeError>;
fn css_get_computed_style_for_node(
&self,
target_id: &str,
node_id: i64,
) -> Result<Vec<CSSComputedStyleProperty>, BridgeError>;
fn css_get_matched_styles_for_node(
&self,
target_id: &str,
node_id: i64,
) -> Result<MatchedStyles, BridgeError>;
fn debugger_enable(&self, target_id: &str) -> Result<(), BridgeError>;
fn debugger_disable(&self, target_id: &str) -> Result<(), BridgeError>;
fn debugger_set_breakpoint_by_url(
&self,
target_id: &str,
url: &str,
line: u32,
column: u32,
) -> Result<BreakpointResult, BridgeError>;
fn debugger_remove_breakpoint(
&self,
target_id: &str,
script_id: u32,
line: u32,
column: u32,
) -> Result<(), BridgeError>;
fn debugger_pause(&self, target_id: &str) -> Result<(), BridgeError>;
fn debugger_resume(
&self,
target_id: &str,
step_action: Option<DebugStepAction>,
) -> Result<(), BridgeError>;
fn debugger_evaluate_on_call_frame(
&self,
target_id: &str,
call_frame_id: &str,
expression: &str,
) -> Result<DebuggerEvalResult, BridgeError>;
fn debugger_get_possible_breakpoints(
&self,
target_id: &str,
script_id: u32,
) -> Result<Vec<PossibleBreakpoint>, BridgeError>;
fn debugger_get_script_source(
&self,
target_id: &str,
script_id: u32,
) -> Result<String, BridgeError>;
}
#[derive(Debug, Clone, Default)]
pub struct NavigateResult {
pub frame_id: String,
pub loader_id: String,
pub error_text: Option<String>,
}
#[derive(Debug, Clone)]
pub struct FrameTree {
pub frame: Frame,
pub child_frames: Vec<FrameTree>,
}
#[derive(Debug, Clone, Default)]
pub struct Frame {
pub id: String,
pub parent_id: Option<String>,
pub loader_id: String,
pub name: Option<String>,
pub url: String,
pub security_origin: String,
pub mime_type: String,
}
#[derive(Debug, Clone, Default)]
pub struct NavigationHistory {
pub current_index: i64,
pub entries: Vec<NavigationEntry>,
}
#[derive(Debug, Clone, Default)]
pub struct NavigationEntry {
pub id: i64,
pub url: String,
pub title: String,
}
#[derive(Debug, Clone, Default)]
pub struct LayoutMetrics {
pub layout_width: f64,
pub layout_height: f64,
pub content_width: f64,
pub content_height: f64,
}
#[derive(Debug, Clone, Default)]
pub struct EvaluateResult {
pub result: RemoteObject,
pub exception_details: Option<ExceptionDetails>,
}
#[derive(Debug, Clone, Default)]
pub struct RemoteObject {
pub object_id: Option<String>,
pub type_: String,
pub subtype: Option<String>,
pub value: Option<Value>,
pub unserializable_value: Option<String>,
pub class_name: Option<String>,
pub description: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ExceptionDetails {
pub exception_id: i64,
pub text: String,
pub line_number: i64,
pub column_number: i64,
pub exception: Option<RemoteObject>,
}
#[derive(Debug, Clone)]
pub struct PropertyDescriptor {
pub name: String,
pub value: Option<RemoteObject>,
pub writable: Option<bool>,
pub get: Option<RemoteObject>,
pub set: Option<RemoteObject>,
pub configurable: Option<bool>,
pub enumerable: Option<bool>,
pub is_own: bool,
pub symbol: Option<RemoteObject>,
}
#[derive(Debug, Clone, Default)]
pub struct BoxModel {
pub content: Vec<f64>, pub padding: Vec<f64>,
pub border: Vec<f64>,
pub margin: Vec<f64>,
pub width: i64,
pub height: i64,
}
#[derive(Debug, Clone, Default)]
pub struct ResponseBody {
pub body: String,
pub base64_encoded: bool,
}
#[derive(Debug, Clone, Default)]
pub struct MouseEvent {
pub event_type: String, pub x: f64,
pub y: f64,
pub button: String, pub click_count: i64,
pub modifiers: i64,
}
#[derive(Debug, Clone, Default)]
pub struct KeyEvent {
pub event_type: String, pub key: String,
pub code: String,
pub modifiers: i64,
pub text: String,
pub windows_virtual_key_code: i64,
}
#[derive(Debug, Clone, Default)]
pub struct TouchPoint {
pub state: String,
pub x: f64,
pub y: f64,
pub radius_x: f64,
pub radius_y: f64,
pub force: f64,
}
#[derive(Debug, Clone, Default)]
pub struct DeviceMetrics {
pub width: i64,
pub height: i64,
pub device_scale_factor: f64,
pub mobile: bool,
}
#[derive(Debug, Clone, Default)]
pub struct TargetInfo {
pub target_id: String,
pub type_: String, pub title: String,
pub url: String,
pub attached: bool,
pub browser_context_id: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct CSSComputedStyleProperty {
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, Default)]
pub struct MatchedStyles {
pub inline_style: Option<CSSStyle>,
pub attributes_style: Option<CSSStyle>,
pub matched_rules: Vec<MatchedRule>,
}
#[derive(Debug, Clone, Default)]
pub struct CSSStyle {
pub style_sheet_id: String,
pub css_properties: Vec<CSSProperty>,
}
#[derive(Debug, Clone, Default)]
pub struct CSSProperty {
pub name: String,
pub value: String,
pub important: bool,
}
#[derive(Debug, Clone, Default)]
pub struct MatchedRule {
pub selector: String,
pub style: CSSStyle,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DebugStepAction {
Next,
Into,
Out,
}
impl DebugStepAction {
pub fn from_cdp(s: &str) -> Option<Self> {
match s {
"over" | "next" => Some(Self::Next),
"into" | "step" => Some(Self::Into),
"out" | "finish" => Some(Self::Out),
_ => None,
}
}
pub fn servo_resume_limit(self) -> &'static str {
match self {
Self::Next => "next",
Self::Into => "step",
Self::Out => "finish",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct BreakpointResult {
pub breakpoint_id: String,
pub script_id: u32,
pub actual_line: u32,
pub actual_column: u32,
}
#[derive(Debug, Clone, Default)]
pub struct PossibleBreakpoint {
pub script_id: u32,
pub line_number: u32,
pub column_number: u32,
}
#[derive(Debug, Clone, Default)]
pub struct DebuggerEvalResult {
pub result: DebuggerRemoteObject,
pub has_exception: bool,
}
#[derive(Debug, Clone, Default)]
pub struct DebuggerRemoteObject {
pub type_: String,
pub object_id: Option<String>,
pub class_name: Option<String>,
pub description: Option<String>,
pub value: Option<Value>,
}
pub struct MockServoBackend {
pub call_log: Mutex<Vec<(String, String, String)>>,
pub default_frame_id: String,
pub known_targets: Mutex<Vec<String>>,
}
impl Default for MockServoBackend {
fn default() -> Self {
Self {
call_log: Mutex::new(Vec::new()),
default_frame_id: "FRAME_0".to_string(),
known_targets: Mutex::new(vec!["1".to_string(), "default".to_string()]),
}
}
}
impl MockServoBackend {
pub fn new() -> Self {
Self::default()
}
pub fn into_backend(self) -> Arc<dyn ServoBackend> {
Arc::new(self)
}
fn log(&self, target_id: &str, method: &str, payload: &str) {
self.call_log.lock().unwrap().push((
target_id.to_string(),
method.to_string(),
payload.to_string(),
));
}
fn ensure_target(&self, target_id: &str) -> Result<(), BridgeError> {
let known = self.known_targets.lock().unwrap();
if !known.iter().any(|t| t == target_id) {
return Err(BridgeError::PageNotFound(target_id.to_string()));
}
Ok(())
}
pub fn add_target(&self, target_id: impl Into<String>) {
self.known_targets.lock().unwrap().push(target_id.into());
}
}
impl ServoBackend for MockServoBackend {
fn page_navigate(&self, target_id: &str, url: &str) -> Result<NavigateResult, BridgeError> {
self.log(target_id, "page_navigate", url);
self.ensure_target(target_id)?;
Ok(NavigateResult {
frame_id: self.default_frame_id.clone(),
loader_id: format!("LOADER_{url:?}"),
error_text: None,
})
}
fn page_reload(&self, target_id: &str) -> Result<(), BridgeError> {
self.log(target_id, "page_reload", "");
self.ensure_target(target_id)?;
Ok(())
}
fn page_screenshot(
&self,
target_id: &str,
format: BridgeScreenshotFormat,
) -> Result<Vec<u8>, BridgeError> {
self.log(target_id, "page_screenshot", &format!("{format:?}"));
self.ensure_target(target_id)?;
Ok(vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
}
fn page_frame_tree(&self, target_id: &str) -> Result<FrameTree, BridgeError> {
self.log(target_id, "page_frame_tree", "");
self.ensure_target(target_id)?;
Ok(FrameTree {
frame: Frame {
id: self.default_frame_id.clone(),
url: "about:blank".to_string(),
mime_type: "text/html".to_string(),
security_origin: "://".to_string(),
..Default::default()
},
child_frames: vec![],
})
}
fn page_navigation_history(&self, target_id: &str) -> Result<NavigationHistory, BridgeError> {
self.log(target_id, "page_navigation_history", "");
self.ensure_target(target_id)?;
Ok(NavigationHistory {
current_index: 0,
entries: vec![NavigationEntry {
id: 0,
url: "about:blank".to_string(),
title: String::new(),
}],
})
}
fn page_navigate_to_history_entry(
&self,
target_id: &str,
entry_id: i64,
) -> Result<(), BridgeError> {
self.log(
target_id,
"page_navigate_to_history_entry",
&entry_id.to_string(),
);
self.ensure_target(target_id)?;
Ok(())
}
fn page_set_content(&self, target_id: &str, html: &str) -> Result<(), BridgeError> {
self.log(target_id, "page_set_content", html);
self.ensure_target(target_id)?;
Ok(())
}
fn page_close(&self, target_id: &str) -> Result<(), BridgeError> {
self.log(target_id, "page_close", "");
self.ensure_target(target_id)?;
let mut known = self.known_targets.lock().unwrap();
known.retain(|t| t != target_id);
Ok(())
}
fn page_bring_to_front(&self, target_id: &str) -> Result<(), BridgeError> {
self.log(target_id, "page_bring_to_front", "");
self.ensure_target(target_id)?;
Ok(())
}
fn page_layout_metrics(&self, target_id: &str) -> Result<LayoutMetrics, BridgeError> {
self.log(target_id, "page_layout_metrics", "");
self.ensure_target(target_id)?;
Ok(LayoutMetrics {
layout_width: 1280.0,
layout_height: 720.0,
content_width: 1280.0,
content_height: 720.0,
})
}
fn page_print_to_pdf(&self, target_id: &str) -> Result<Vec<u8>, BridgeError> {
self.log(target_id, "page_print_to_pdf", "");
self.ensure_target(target_id)?;
Ok(b"%PDF-1.4 mock".to_vec())
}
fn runtime_evaluate(
&self,
target_id: &str,
expression: &str,
) -> Result<EvaluateResult, BridgeError> {
self.log(target_id, "runtime_evaluate", expression);
self.ensure_target(target_id)?;
Ok(EvaluateResult {
result: RemoteObject {
type_: "string".to_string(),
value: Some(Value::String(expression.to_string())),
..Default::default()
},
exception_details: None,
})
}
fn runtime_call_function_on(
&self,
target_id: &str,
object_id: &str,
function_declaration: &str,
args: &[Value],
) -> Result<EvaluateResult, BridgeError> {
self.log(
target_id,
"runtime_call_function_on",
&format!("{object_id}|{function_declaration}|{}args", args.len()),
);
self.ensure_target(target_id)?;
Ok(EvaluateResult {
result: RemoteObject {
type_: "undefined".to_string(),
..Default::default()
},
exception_details: None,
})
}
fn runtime_get_properties(
&self,
target_id: &str,
object_id: &str,
own_properties: bool,
) -> Result<Vec<PropertyDescriptor>, BridgeError> {
self.log(
target_id,
"runtime_get_properties",
&format!("{object_id}|own={own_properties}"),
);
self.ensure_target(target_id)?;
Ok(vec![])
}
fn runtime_release_object(&self, target_id: &str, object_id: &str) -> Result<(), BridgeError> {
self.log(target_id, "runtime_release_object", object_id);
self.ensure_target(target_id)?;
Ok(())
}
fn runtime_enable(&self, target_id: &str) -> Result<(), BridgeError> {
self.log(target_id, "runtime_enable", "");
Ok(())
}
fn runtime_disable(&self, target_id: &str) -> Result<(), BridgeError> {
self.log(target_id, "runtime_disable", "");
Ok(())
}
fn dom_get_document(&self, target_id: &str, depth: i64) -> Result<NodeDescriptor, BridgeError> {
self.log(target_id, "dom_get_document", &depth.to_string());
self.ensure_target(target_id)?;
Ok(NodeDescriptor {
node_id: 1,
node_name: "#document".to_string(),
backend_node_id: 1,
children: vec![],
..Default::default()
})
}
fn dom_query_selector(
&self,
target_id: &str,
node_id: i64,
selector: &str,
) -> Result<Option<i64>, BridgeError> {
self.log(
target_id,
"dom_query_selector",
&format!("{node_id}|{selector}"),
);
self.ensure_target(target_id)?;
Ok(Some(2))
}
fn dom_query_selector_all(
&self,
target_id: &str,
node_id: i64,
selector: &str,
) -> Result<Vec<i64>, BridgeError> {
self.log(
target_id,
"dom_query_selector_all",
&format!("{node_id}|{selector}"),
);
self.ensure_target(target_id)?;
Ok(vec![2, 3])
}
fn dom_get_box_model(&self, target_id: &str, node_id: i64) -> Result<BoxModel, BridgeError> {
self.log(target_id, "dom_get_box_model", &node_id.to_string());
self.ensure_target(target_id)?;
Ok(BoxModel {
content: vec![0.0; 8],
padding: vec![0.0; 8],
border: vec![0.0; 8],
margin: vec![0.0; 8],
width: 100,
height: 100,
})
}
fn dom_resolve_node(
&self,
target_id: &str,
backend_node_id: i64,
) -> Result<RemoteObject, BridgeError> {
self.log(target_id, "dom_resolve_node", &backend_node_id.to_string());
self.ensure_target(target_id)?;
Ok(RemoteObject {
object_id: Some(format!("{{node-{backend_node_id}}}")),
type_: "node".to_string(),
..Default::default()
})
}
fn dom_describe_node(
&self,
target_id: &str,
node_id: i64,
depth: i64,
) -> Result<NodeDescriptor, BridgeError> {
self.log(
target_id,
"dom_describe_node",
&format!("{node_id}|{depth}"),
);
self.ensure_target(target_id)?;
Ok(NodeDescriptor {
node_id,
node_name: "DIV".to_string(),
backend_node_id: node_id,
..Default::default()
})
}
fn dom_set_attribute(
&self,
target_id: &str,
node_id: i64,
name: &str,
value: &str,
) -> Result<(), BridgeError> {
self.log(
target_id,
"dom_set_attribute",
&format!("{node_id}|{name}={value}"),
);
self.ensure_target(target_id)?;
Ok(())
}
fn dom_remove_attribute(
&self,
target_id: &str,
node_id: i64,
name: &str,
) -> Result<(), BridgeError> {
self.log(
target_id,
"dom_remove_attribute",
&format!("{node_id}|{name}"),
);
self.ensure_target(target_id)?;
Ok(())
}
fn dom_get_outer_html(&self, target_id: &str, node_id: i64) -> Result<String, BridgeError> {
self.log(target_id, "dom_get_outer_html", &node_id.to_string());
self.ensure_target(target_id)?;
Ok(format!("<div id=\"node-{node_id}\"></div>"))
}
fn dom_set_outer_html(
&self,
target_id: &str,
node_id: i64,
html: &str,
) -> Result<(), BridgeError> {
self.log(
target_id,
"dom_set_outer_html",
&format!("{node_id}|{html}"),
);
self.ensure_target(target_id)?;
Ok(())
}
fn dom_request_node(&self, target_id: &str, object_id: &str) -> Result<i64, BridgeError> {
self.log(target_id, "dom_request_node", object_id);
self.ensure_target(target_id)?;
Ok(3)
}
fn network_enable(&self, target_id: &str) -> Result<(), BridgeError> {
self.log(target_id, "network_enable", "");
Ok(())
}
fn network_disable(&self, target_id: &str) -> Result<(), BridgeError> {
self.log(target_id, "network_disable", "");
Ok(())
}
fn network_get_response_body(
&self,
target_id: &str,
request_id: &str,
) -> Result<ResponseBody, BridgeError> {
self.log(target_id, "network_get_response_body", request_id);
self.ensure_target(target_id)?;
Ok(ResponseBody {
body: String::new(),
base64_encoded: false,
})
}
fn network_set_cache_disabled(
&self,
target_id: &str,
disabled: bool,
) -> Result<(), BridgeError> {
self.log(
target_id,
"network_set_cache_disabled",
&disabled.to_string(),
);
Ok(())
}
fn input_dispatch_mouse_event(
&self,
target_id: &str,
event: MouseEvent,
) -> Result<(), BridgeError> {
self.log(
target_id,
"input_dispatch_mouse_event",
&format!("{:?}", event),
);
self.ensure_target(target_id)?;
Ok(())
}
fn input_dispatch_key_event(
&self,
target_id: &str,
event: KeyEvent,
) -> Result<(), BridgeError> {
self.log(
target_id,
"input_dispatch_key_event",
&format!("{:?}", event),
);
self.ensure_target(target_id)?;
Ok(())
}
fn input_dispatch_touch_event(
&self,
target_id: &str,
event_type: &str,
touch_points: &[TouchPoint],
) -> Result<(), BridgeError> {
self.log(
target_id,
"input_dispatch_touch_event",
&format!("{event_type}|{}points", touch_points.len()),
);
self.ensure_target(target_id)?;
Ok(())
}
fn input_set_ignore_input_events(
&self,
target_id: &str,
ignore: bool,
) -> Result<(), BridgeError> {
self.log(
target_id,
"input_set_ignore_input_events",
&ignore.to_string(),
);
Ok(())
}
fn emulation_set_device_metrics(
&self,
target_id: &str,
metrics: DeviceMetrics,
) -> Result<(), BridgeError> {
self.log(
target_id,
"emulation_set_device_metrics",
&format!("{:?}", metrics),
);
self.ensure_target(target_id)?;
Ok(())
}
fn emulation_clear_device_metrics(&self, target_id: &str) -> Result<(), BridgeError> {
self.log(target_id, "emulation_clear_device_metrics", "");
self.ensure_target(target_id)?;
Ok(())
}
fn emulation_set_user_agent_override(
&self,
target_id: &str,
user_agent: &str,
) -> Result<(), BridgeError> {
self.log(target_id, "emulation_set_user_agent_override", user_agent);
self.ensure_target(target_id)?;
Ok(())
}
fn emulation_set_geolocation_override(
&self,
target_id: &str,
latitude: f64,
longitude: f64,
accuracy: f64,
) -> Result<(), BridgeError> {
self.log(
target_id,
"emulation_set_geolocation_override",
&format!("{latitude},{longitude},{accuracy}"),
);
self.ensure_target(target_id)?;
Ok(())
}
fn target_get_targets(&self) -> Result<Vec<TargetInfo>, BridgeError> {
let known = self.known_targets.lock().unwrap();
let targets: Vec<TargetInfo> = known
.iter()
.map(|t| TargetInfo {
target_id: t.clone(),
type_: "page".to_string(),
title: format!("Mock page {t}"),
url: "about:blank".to_string(),
attached: true,
browser_context_id: None,
})
.collect();
Ok(targets)
}
fn target_create_target(&self, url: &str) -> Result<String, BridgeError> {
let known = self.known_targets.lock().unwrap();
let max_id = known
.iter()
.filter_map(|t| t.parse::<usize>().ok())
.max()
.unwrap_or(1);
drop(known);
let new_id = (max_id + 1).to_string();
self.add_target(&new_id);
self.log(&new_id, "target_create_target", url);
Ok(new_id)
}
fn target_close_target(&self, target_id: &str) -> Result<(), BridgeError> {
self.log(target_id, "target_close_target", "");
let mut known = self.known_targets.lock().unwrap();
if !known.iter().any(|t| t == target_id) {
return Err(BridgeError::PageNotFound(target_id.to_string()));
}
known.retain(|t| t != target_id);
Ok(())
}
fn target_attach_to_target(&self, target_id: &str) -> Result<String, BridgeError> {
self.log(target_id, "target_attach_to_target", "");
self.ensure_target(target_id)?;
Ok(format!("{target_id}-session"))
}
fn target_detach_from_target(&self, session_id: &str) -> Result<(), BridgeError> {
self.log(session_id, "target_detach_from_target", "");
Ok(())
}
fn target_set_auto_attach(
&self,
target_id: &str,
auto_attach: bool,
wait_for_debugger_on_start: bool,
) -> Result<(), BridgeError> {
self.log(
target_id,
"target_set_auto_attach",
&format!("{auto_attach}|{wait_for_debugger_on_start}"),
);
Ok(())
}
fn css_get_computed_style_for_node(
&self,
target_id: &str,
node_id: i64,
) -> Result<Vec<CSSComputedStyleProperty>, BridgeError> {
self.log(
target_id,
"css_get_computed_style_for_node",
&node_id.to_string(),
);
self.ensure_target(target_id)?;
Ok(vec![CSSComputedStyleProperty {
name: "display".to_string(),
value: "block".to_string(),
}])
}
fn css_get_matched_styles_for_node(
&self,
target_id: &str,
node_id: i64,
) -> Result<MatchedStyles, BridgeError> {
self.log(
target_id,
"css_get_matched_styles_for_node",
&node_id.to_string(),
);
self.ensure_target(target_id)?;
Ok(MatchedStyles {
inline_style: None,
attributes_style: None,
matched_rules: vec![],
})
}
fn debugger_enable(&self, target_id: &str) -> Result<(), BridgeError> {
self.ensure_target(target_id)?;
self.log(target_id, "debugger_enable", "");
Ok(())
}
fn debugger_disable(&self, target_id: &str) -> Result<(), BridgeError> {
self.ensure_target(target_id)?;
self.log(target_id, "debugger_disable", "");
Ok(())
}
fn debugger_set_breakpoint_by_url(
&self,
target_id: &str,
url: &str,
line: u32,
column: u32,
) -> Result<BreakpointResult, BridgeError> {
self.ensure_target(target_id)?;
self.log(
target_id,
"debugger_set_breakpoint_by_url",
&format!("{url}|{line}|{column}"),
);
Ok(BreakpointResult {
breakpoint_id: format!("bp:1:{line}:{column}"),
script_id: 1,
actual_line: line,
actual_column: column,
})
}
fn debugger_remove_breakpoint(
&self,
target_id: &str,
script_id: u32,
line: u32,
column: u32,
) -> Result<(), BridgeError> {
self.ensure_target(target_id)?;
self.log(
target_id,
"debugger_remove_breakpoint",
&format!("{script_id}:{line}:{column}"),
);
Ok(())
}
fn debugger_pause(&self, target_id: &str) -> Result<(), BridgeError> {
self.ensure_target(target_id)?;
self.log(target_id, "debugger_pause", "Interrupt");
Ok(())
}
fn debugger_resume(
&self,
target_id: &str,
step_action: Option<DebugStepAction>,
) -> Result<(), BridgeError> {
self.ensure_target(target_id)?;
let payload = match step_action {
None => "Resume()".to_string(),
Some(a) => format!("Resume({})", a.servo_resume_limit()),
};
self.log(target_id, "debugger_resume", &payload);
Ok(())
}
fn debugger_evaluate_on_call_frame(
&self,
target_id: &str,
call_frame_id: &str,
expression: &str,
) -> Result<DebuggerEvalResult, BridgeError> {
self.ensure_target(target_id)?;
self.log(
target_id,
"debugger_evaluate_on_call_frame",
&format!("{call_frame_id}|{expression}"),
);
Ok(DebuggerEvalResult {
result: DebuggerRemoteObject {
type_: "string".to_string(),
value: Some(Value::String(expression.to_string())),
..Default::default()
},
has_exception: false,
})
}
fn debugger_get_possible_breakpoints(
&self,
target_id: &str,
script_id: u32,
) -> Result<Vec<PossibleBreakpoint>, BridgeError> {
self.ensure_target(target_id)?;
self.log(
target_id,
"debugger_get_possible_breakpoints",
&script_id.to_string(),
);
Ok(vec![
PossibleBreakpoint {
script_id,
line_number: 0,
column_number: 0,
},
PossibleBreakpoint {
script_id,
line_number: 1,
column_number: 0,
},
])
}
fn debugger_get_script_source(
&self,
target_id: &str,
script_id: u32,
) -> Result<String, BridgeError> {
self.ensure_target(target_id)?;
self.log(
target_id,
"debugger_get_script_source",
&script_id.to_string(),
);
Ok(format!("// mock source for script {script_id}\n"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mock_navigate_records_call() {
let b = MockServoBackend::new();
let r = b.page_navigate("1", "https://example.com").unwrap();
assert_eq!(r.frame_id, "FRAME_0");
let log = b.call_log.lock().unwrap();
assert_eq!(log.len(), 1);
assert_eq!(log[0].0, "1");
assert_eq!(log[0].1, "page_navigate");
assert_eq!(log[0].2, "https://example.com");
}
#[test]
fn mock_navigate_unknown_target_returns_page_not_found() {
let b = MockServoBackend::new();
let err = b.page_navigate("999", "x").unwrap_err();
assert!(matches!(err, BridgeError::PageNotFound(_)));
}
#[test]
fn mock_screenshot_returns_png_header() {
let b = MockServoBackend::new();
let bytes = b.page_screenshot("1", BridgeScreenshotFormat::Png).unwrap();
assert_eq!(&bytes[..4], &[0x89, 0x50, 0x4E, 0x47]);
}
#[test]
fn mock_target_create_then_close_roundtrip() {
let b = MockServoBackend::new();
let new_id = b.target_create_target("about:blank").unwrap();
b.page_navigate(&new_id, "https://x").unwrap();
b.target_close_target(&new_id).unwrap();
let err = b.page_navigate(&new_id, "x").unwrap_err();
assert!(matches!(err, BridgeError::PageNotFound(_)));
}
#[test]
fn screenshot_format_parsing() {
assert_eq!(
BridgeScreenshotFormat::from_cdp(Some("jpeg")),
BridgeScreenshotFormat::Jpeg
);
assert_eq!(
BridgeScreenshotFormat::from_cdp(Some("png")),
BridgeScreenshotFormat::Png
);
assert_eq!(
BridgeScreenshotFormat::from_cdp(None),
BridgeScreenshotFormat::Png
);
assert_eq!(
BridgeScreenshotFormat::from_cdp(Some("unknown")),
BridgeScreenshotFormat::Png
);
}
#[test]
fn target_get_targets_lists_known() {
let b = MockServoBackend::new();
let ts = b.target_get_targets().unwrap();
assert!(!ts.is_empty());
assert!(ts.iter().any(|t| t.target_id == "1"));
}
#[test]
fn target_attach_returns_session_id() {
let b = MockServoBackend::new();
let s = b.target_attach_to_target("1").unwrap();
assert!(s.contains("1"));
assert!(s.contains("session"));
}
#[test]
fn mock_runtime_evaluate_returns_expression_echo() {
let b = MockServoBackend::new();
let r = b.runtime_evaluate("1", "1+1").unwrap();
assert_eq!(r.result.type_, "string");
assert_eq!(
r.result.value.as_ref().unwrap(),
&Value::String("1+1".into())
);
}
}
impl ServoBackend for Arc<dyn ServoBackend> {
fn page_navigate(&self, target_id: &str, url: &str) -> Result<NavigateResult, BridgeError> {
(**self).page_navigate(target_id, url)
}
fn page_reload(&self, target_id: &str) -> Result<(), BridgeError> {
(**self).page_reload(target_id)
}
fn page_screenshot(
&self,
target_id: &str,
format: BridgeScreenshotFormat,
) -> Result<Vec<u8>, BridgeError> {
(**self).page_screenshot(target_id, format)
}
fn page_frame_tree(&self, target_id: &str) -> Result<FrameTree, BridgeError> {
(**self).page_frame_tree(target_id)
}
fn page_navigation_history(&self, target_id: &str) -> Result<NavigationHistory, BridgeError> {
(**self).page_navigation_history(target_id)
}
fn page_navigate_to_history_entry(
&self,
target_id: &str,
entry_id: i64,
) -> Result<(), BridgeError> {
(**self).page_navigate_to_history_entry(target_id, entry_id)
}
fn page_set_content(&self, target_id: &str, html: &str) -> Result<(), BridgeError> {
(**self).page_set_content(target_id, html)
}
fn page_close(&self, target_id: &str) -> Result<(), BridgeError> {
(**self).page_close(target_id)
}
fn page_bring_to_front(&self, target_id: &str) -> Result<(), BridgeError> {
(**self).page_bring_to_front(target_id)
}
fn page_layout_metrics(&self, target_id: &str) -> Result<LayoutMetrics, BridgeError> {
(**self).page_layout_metrics(target_id)
}
fn page_print_to_pdf(&self, target_id: &str) -> Result<Vec<u8>, BridgeError> {
(**self).page_print_to_pdf(target_id)
}
fn runtime_evaluate(
&self,
target_id: &str,
expression: &str,
) -> Result<EvaluateResult, BridgeError> {
(**self).runtime_evaluate(target_id, expression)
}
fn runtime_call_function_on(
&self,
target_id: &str,
object_id: &str,
function_declaration: &str,
args: &[Value],
) -> Result<EvaluateResult, BridgeError> {
(**self).runtime_call_function_on(target_id, object_id, function_declaration, args)
}
fn runtime_get_properties(
&self,
target_id: &str,
object_id: &str,
own_properties: bool,
) -> Result<Vec<PropertyDescriptor>, BridgeError> {
(**self).runtime_get_properties(target_id, object_id, own_properties)
}
fn runtime_release_object(&self, target_id: &str, object_id: &str) -> Result<(), BridgeError> {
(**self).runtime_release_object(target_id, object_id)
}
fn runtime_enable(&self, target_id: &str) -> Result<(), BridgeError> {
(**self).runtime_enable(target_id)
}
fn runtime_disable(&self, target_id: &str) -> Result<(), BridgeError> {
(**self).runtime_disable(target_id)
}
fn dom_get_document(&self, target_id: &str, depth: i64) -> Result<NodeDescriptor, BridgeError> {
(**self).dom_get_document(target_id, depth)
}
fn dom_query_selector(
&self,
target_id: &str,
node_id: i64,
selector: &str,
) -> Result<Option<i64>, BridgeError> {
(**self).dom_query_selector(target_id, node_id, selector)
}
fn dom_query_selector_all(
&self,
target_id: &str,
node_id: i64,
selector: &str,
) -> Result<Vec<i64>, BridgeError> {
(**self).dom_query_selector_all(target_id, node_id, selector)
}
fn dom_get_box_model(&self, target_id: &str, node_id: i64) -> Result<BoxModel, BridgeError> {
(**self).dom_get_box_model(target_id, node_id)
}
fn dom_resolve_node(
&self,
target_id: &str,
backend_node_id: i64,
) -> Result<RemoteObject, BridgeError> {
(**self).dom_resolve_node(target_id, backend_node_id)
}
fn dom_describe_node(
&self,
target_id: &str,
node_id: i64,
depth: i64,
) -> Result<NodeDescriptor, BridgeError> {
(**self).dom_describe_node(target_id, node_id, depth)
}
fn dom_set_attribute(
&self,
target_id: &str,
node_id: i64,
name: &str,
value: &str,
) -> Result<(), BridgeError> {
(**self).dom_set_attribute(target_id, node_id, name, value)
}
fn dom_remove_attribute(
&self,
target_id: &str,
node_id: i64,
name: &str,
) -> Result<(), BridgeError> {
(**self).dom_remove_attribute(target_id, node_id, name)
}
fn dom_get_outer_html(&self, target_id: &str, node_id: i64) -> Result<String, BridgeError> {
(**self).dom_get_outer_html(target_id, node_id)
}
fn dom_set_outer_html(
&self,
target_id: &str,
node_id: i64,
html: &str,
) -> Result<(), BridgeError> {
(**self).dom_set_outer_html(target_id, node_id, html)
}
fn dom_request_node(&self, target_id: &str, object_id: &str) -> Result<i64, BridgeError> {
(**self).dom_request_node(target_id, object_id)
}
fn network_enable(&self, target_id: &str) -> Result<(), BridgeError> {
(**self).network_enable(target_id)
}
fn network_disable(&self, target_id: &str) -> Result<(), BridgeError> {
(**self).network_disable(target_id)
}
fn network_get_response_body(
&self,
target_id: &str,
request_id: &str,
) -> Result<ResponseBody, BridgeError> {
(**self).network_get_response_body(target_id, request_id)
}
fn network_set_cache_disabled(
&self,
target_id: &str,
disabled: bool,
) -> Result<(), BridgeError> {
(**self).network_set_cache_disabled(target_id, disabled)
}
fn input_dispatch_mouse_event(
&self,
target_id: &str,
event: MouseEvent,
) -> Result<(), BridgeError> {
(**self).input_dispatch_mouse_event(target_id, event)
}
fn input_dispatch_key_event(
&self,
target_id: &str,
event: KeyEvent,
) -> Result<(), BridgeError> {
(**self).input_dispatch_key_event(target_id, event)
}
fn input_dispatch_touch_event(
&self,
target_id: &str,
event_type: &str,
touch_points: &[TouchPoint],
) -> Result<(), BridgeError> {
(**self).input_dispatch_touch_event(target_id, event_type, touch_points)
}
fn input_set_ignore_input_events(
&self,
target_id: &str,
ignore: bool,
) -> Result<(), BridgeError> {
(**self).input_set_ignore_input_events(target_id, ignore)
}
fn emulation_set_device_metrics(
&self,
target_id: &str,
metrics: DeviceMetrics,
) -> Result<(), BridgeError> {
(**self).emulation_set_device_metrics(target_id, metrics)
}
fn emulation_clear_device_metrics(&self, target_id: &str) -> Result<(), BridgeError> {
(**self).emulation_clear_device_metrics(target_id)
}
fn emulation_set_user_agent_override(
&self,
target_id: &str,
user_agent: &str,
) -> Result<(), BridgeError> {
(**self).emulation_set_user_agent_override(target_id, user_agent)
}
fn emulation_set_geolocation_override(
&self,
target_id: &str,
latitude: f64,
longitude: f64,
accuracy: f64,
) -> Result<(), BridgeError> {
(**self).emulation_set_geolocation_override(target_id, latitude, longitude, accuracy)
}
fn target_get_targets(&self) -> Result<Vec<TargetInfo>, BridgeError> {
(**self).target_get_targets()
}
fn target_create_target(&self, url: &str) -> Result<String, BridgeError> {
(**self).target_create_target(url)
}
fn target_close_target(&self, target_id: &str) -> Result<(), BridgeError> {
(**self).target_close_target(target_id)
}
fn target_attach_to_target(&self, target_id: &str) -> Result<String, BridgeError> {
(**self).target_attach_to_target(target_id)
}
fn target_detach_from_target(&self, session_id: &str) -> Result<(), BridgeError> {
(**self).target_detach_from_target(session_id)
}
fn target_set_auto_attach(
&self,
target_id: &str,
auto_attach: bool,
wait_for_debugger_on_start: bool,
) -> Result<(), BridgeError> {
(**self).target_set_auto_attach(target_id, auto_attach, wait_for_debugger_on_start)
}
fn css_get_computed_style_for_node(
&self,
target_id: &str,
node_id: i64,
) -> Result<Vec<CSSComputedStyleProperty>, BridgeError> {
(**self).css_get_computed_style_for_node(target_id, node_id)
}
fn css_get_matched_styles_for_node(
&self,
target_id: &str,
node_id: i64,
) -> Result<MatchedStyles, BridgeError> {
(**self).css_get_matched_styles_for_node(target_id, node_id)
}
fn debugger_enable(&self, target_id: &str) -> Result<(), BridgeError> {
(**self).debugger_enable(target_id)
}
fn debugger_disable(&self, target_id: &str) -> Result<(), BridgeError> {
(**self).debugger_disable(target_id)
}
fn debugger_set_breakpoint_by_url(
&self,
target_id: &str,
url: &str,
line: u32,
column: u32,
) -> Result<BreakpointResult, BridgeError> {
(**self).debugger_set_breakpoint_by_url(target_id, url, line, column)
}
fn debugger_remove_breakpoint(
&self,
target_id: &str,
script_id: u32,
line: u32,
column: u32,
) -> Result<(), BridgeError> {
(**self).debugger_remove_breakpoint(target_id, script_id, line, column)
}
fn debugger_pause(&self, target_id: &str) -> Result<(), BridgeError> {
(**self).debugger_pause(target_id)
}
fn debugger_resume(
&self,
target_id: &str,
step_action: Option<DebugStepAction>,
) -> Result<(), BridgeError> {
(**self).debugger_resume(target_id, step_action)
}
fn debugger_evaluate_on_call_frame(
&self,
target_id: &str,
call_frame_id: &str,
expression: &str,
) -> Result<DebuggerEvalResult, BridgeError> {
(**self).debugger_evaluate_on_call_frame(target_id, call_frame_id, expression)
}
fn debugger_get_possible_breakpoints(
&self,
target_id: &str,
script_id: u32,
) -> Result<Vec<PossibleBreakpoint>, BridgeError> {
(**self).debugger_get_possible_breakpoints(target_id, script_id)
}
fn debugger_get_script_source(
&self,
target_id: &str,
script_id: u32,
) -> Result<String, BridgeError> {
(**self).debugger_get_script_source(target_id, script_id)
}
}