Skip to main content

ferroui_devtools/
protocol.rs

1use crate::input::Input;
2use base64::Engine;
3use ferroui_base::input::InputElement;
4use ferroui_base::interactivity::RoutingStrategies;
5use ferroui_base::reactive::{Disposable, IDisposable};
6use ferroui_base::{Point, Rect, Ref, StyledElement};
7use ferroui_controls::automation::peers::{AutomationPeer, ControlAutomationPeer};
8use ferroui_controls::{Control, TopLevel};
9use ferroui_inspect::target::{Node, Target};
10use ferroui_inspect::{NodeId, TreeKind};
11use serde_json::{json, Value};
12use std::cell::RefCell;
13use std::collections::{HashMap, HashSet};
14use std::rc::Rc;
15
16pub(crate) type Result<T> = std::result::Result<T, Error>;
17#[derive(Debug)]
18pub(crate) struct Error {
19    pub code: i32,
20    pub message: String,
21}
22impl Error {
23    pub fn params(message: impl Into<String>) -> Self {
24        Self {
25            code: -32602,
26            message: message.into(),
27        }
28    }
29    pub fn server(message: impl Into<String>) -> Self {
30        Self {
31            code: -32000,
32            message: message.into(),
33        }
34    }
35    pub fn method(method: &str) -> Self {
36        Self {
37            code: -32601,
38            message: format!("Unsupported method: {method}"),
39        }
40    }
41}
42impl From<String> for Error {
43    fn from(value: String) -> Self {
44        Self::server(value)
45    }
46}
47pub(crate) fn string<'a>(p: &'a Value, key: &str) -> Result<&'a str> {
48    p[key]
49        .as_str()
50        .ok_or_else(|| Error::params(format!("{key} must be a string")))
51}
52pub(crate) fn number(p: &Value, key: &str) -> Result<f64> {
53    p[key]
54        .as_f64()
55        .filter(|n| n.is_finite())
56        .ok_or_else(|| Error::params(format!("{key} must be a finite number")))
57}
58fn node_id(p: &Value) -> Result<NodeId> {
59    p["nodeId"]
60        .as_u64()
61        .or_else(|| p["backendNodeId"].as_u64())
62        .filter(|n| *n > 0)
63        .ok_or_else(|| Error::params("nodeId is required"))
64}
65fn depth(p: &Value, default: i64) -> Result<i64> {
66    let d = p
67        .get("depth")
68        .map(|d| {
69            d.as_i64()
70                .ok_or_else(|| Error::params("depth must be an integer"))
71        })
72        .transpose()?
73        .unwrap_or(default);
74    if d < -1 || d == 0 || d > 256 {
75        Err(Error::params("depth must be -1 or 1..256"))
76    } else {
77        Ok(if d == -1 { 256 } else { d })
78    }
79}
80fn quad(r: Rect) -> Value {
81    json!([
82        r.x,
83        r.y,
84        r.x + r.width,
85        r.y,
86        r.x + r.width,
87        r.y + r.height,
88        r.x,
89        r.y + r.height
90    ])
91}
92fn rect(r: Rect) -> Value {
93    json!({"x":r.x,"y":r.y,"width":r.width,"height":r.height})
94}
95fn style(properties: Vec<Value>) -> Value {
96    json!({"cssProperties":properties,"shorthandEntries":[]})
97}
98fn dom_node(id: NodeId, nodes: &HashMap<NodeId, &Node>, depth: i64) -> Value {
99    let n = nodes[&id];
100    let mut value = json!({"nodeId":id,"backendNodeId":id,"nodeType":1,"nodeName":n.name,"localName":n.name,"nodeValue":"","childNodeCount":n.children.len(),"attributes":n.attributes});
101    if let Some(parent) = n.parent {
102        value["parentId"] = json!(parent);
103    }
104    if depth > 0 {
105        value["children"] = n
106            .children
107            .iter()
108            .filter(|id| nodes.contains_key(id))
109            .map(|id| dom_node(*id, nodes, depth - 1))
110            .collect();
111    }
112    value
113}
114
115/// A transport-independent CDP connection to a single top-level. All calls run on its UI thread.
116/// `handle` returns the response followed by any events; call `poll` for asynchronous events.
117pub struct CdpSession {
118    pub(crate) inspector: Target,
119    input: Input,
120    pub(crate) enabled: HashSet<String>,
121    target_id: String,
122    inspect_subscriptions: Vec<Rc<dyn IDisposable>>,
123    picked: Rc<RefCell<Vec<(Point, bool)>>>,
124    events: Vec<Value>,
125    last_tree: Option<Vec<Node>>,
126    pub(crate) log_cursor: u64,
127}
128impl CdpSession {
129    pub fn new(root: &Ref<TopLevel>, target_id: impl Into<String>) -> Self {
130        Self {
131            inspector: Target::new(root),
132            input: Input::default(),
133            enabled: HashSet::new(),
134            target_id: target_id.into(),
135            inspect_subscriptions: Vec::new(),
136            picked: Rc::new(RefCell::new(Vec::new())),
137            events: Vec::new(),
138            last_tree: None,
139            log_cursor: 0,
140        }
141    }
142    pub fn handle(&mut self, message: &str) -> Vec<Value> {
143        let request: Value = match serde_json::from_str(message) {
144            Ok(v) => v,
145            Err(e) => {
146                return vec![json!({"id":null,"error":{"code":-32700,"message":e.to_string()}})]
147            }
148        };
149        let id = request["id"].clone();
150        let result = if !request["id"].is_u64() {
151            Err(Error {
152                code: -32600,
153                message: "Request requires a nonnegative integer id".into(),
154            })
155        } else {
156            string(&request, "method").and_then(|method| {
157                self.command(method, request.get("params").unwrap_or(&json!({})))
158            })
159        };
160        let response = response(id, result);
161        if request["method"] == "DOM.requestChildNodes"
162            || request["method"] == "DOM.pushNodesByBackendIdsToFrontend"
163            || request["method"] == "DOM.pushNodeByPathToFrontend"
164        {
165            let mut messages = std::mem::take(&mut self.events);
166            messages.push(response);
167            messages
168        } else {
169            let mut messages = vec![response];
170            messages.append(&mut self.events);
171            messages
172        }
173    }
174    pub(crate) fn command(&mut self, method: &str, p: &Value) -> Result<Value> {
175        if !p.is_object() {
176            return Err(Error::params("params must be an object"));
177        }
178        self.inspector.root()?;
179        if let Some(domain) = method.strip_suffix(".enable").filter(|d| {
180            matches!(
181                *d,
182                "DOM" | "CSS" | "Page" | "Runtime" | "Log" | "Accessibility" | "Overlay"
183            )
184        }) {
185            let newly_enabled = self.enabled.insert(domain.into());
186            if domain == "Runtime" && newly_enabled {
187                self.events.push(json!({"method":"Runtime.executionContextCreated","params":{"context":{"id":1,"origin":"ferroui://","name":"FerroUI (no JavaScript engine)","uniqueId":format!("ferroui-{}",self.target_id),"auxData":{"isDefault":true,"frameId":self.target_id}}}}));
188            }
189            return Ok(json!({}));
190        }
191        if let Some(domain) = method.strip_suffix(".disable").filter(|d| {
192            matches!(
193                *d,
194                "DOM" | "CSS" | "Page" | "Runtime" | "Log" | "Accessibility" | "Overlay"
195            )
196        }) {
197            self.enabled.remove(domain);
198            if domain == "Overlay" {
199                self.stop_inspecting();
200                self.inspector.hide_highlight();
201            }
202            return Ok(json!({}));
203        }
204        match method {
205            "DOM.getDocument" => {
206                self.enabled.insert("DOM".into());
207                let depth = depth(p,1)?;
208                let tree = match p["tree"].as_str() { None => self.inspector.tree_kind, Some("visual") => TreeKind::Visual, Some("logical") => TreeKind::Logical, _ => return Err(Error::params("tree must be visual or logical")) };
209                self.inspector.tree_kind = tree;
210                let nodes = self.inspector.snapshot()?;
211                self.last_tree = Some(nodes.clone());
212                let map = nodes.iter().map(|n| (n.id,n)).collect();
213                let children: Vec<_> = nodes.first().map(|n| dom_node(n.id,&map,depth-1)).into_iter().collect();
214                Ok(json!({"root":{"nodeId":1,"backendNodeId":1,"nodeType":9,"nodeName":"#document","localName":"","nodeValue":"","documentURL":self.url(),"baseURL":self.url(),"xmlVersion":"1.0","childNodeCount":1,"children":children}}))
215            }
216            "DOM.describeNode" | "DOM.requestChildNodes" => {
217                let id = node_id(p)?; let depth = depth(p,1)?;
218                let nodes = self.inspector.snapshot()?;
219                let map: HashMap<_,_> = nodes.iter().map(|n| (n.id,n)).collect();
220                if id == 1 {
221                    if method == "DOM.describeNode" { return self.command("DOM.getDocument",p).map(|v| json!({"node":v["root"]})); }
222                    let children: Vec<_> = nodes.first().map(|n| dom_node(n.id,&map,depth-1)).into_iter().collect();
223                    self.events.push(json!({"method":"DOM.setChildNodes","params":{"parentId":1,"nodes":children}}));
224                } else {
225                    self.inspector.element(id)?;
226                    if method == "DOM.describeNode" { return Ok(json!({"node":dom_node(id,&map,depth)})); }
227                    let children: Vec<_> = map[&id].children.iter().map(|id| dom_node(*id,&map,depth-1)).collect();
228                    self.events.push(json!({"method":"DOM.setChildNodes","params":{"parentId":id,"nodes":children}}));
229                }
230                Ok(json!({}))
231            }
232            "DOM.pushNodeByPathToFrontend" => {
233                let path = string(p, "path")?;
234                let parts: Vec<_> = path.split(',').collect();
235                if parts.is_empty() || parts.len() % 2 != 0 || parts.len() > 512 { return Err(Error::params("Invalid node path")); }
236                let nodes = self.inspector.snapshot()?;
237                let map: HashMap<_,_> = nodes.iter().map(|n|(n.id,n)).collect();
238                let mut children: Vec<_> = nodes.first().map(|n|n.id).into_iter().collect();
239                let mut id = 0;
240                for pair in parts.as_chunks::<2>().0.iter() {
241                    let index: usize = pair[0].parse().map_err(|_|Error::params("Invalid node path index"))?;
242                    let Some(node) = children.get(index).and_then(|id|map.get(id)) else { return Ok(json!({"nodeId":0})); };
243                    if node.name != pair[1] { return Ok(json!({"nodeId":0})); }
244                    id = node.id; children = node.children.clone();
245                }
246                self.command("DOM.pushNodesByBackendIdsToFrontend", &json!({"backendNodeIds":[id]}))?;
247                Ok(json!({"nodeId":id}))
248            }
249            "DOM.pushNodesByBackendIdsToFrontend" => {
250                let requested = p["backendNodeIds"].as_array().ok_or_else(|| Error::params("backendNodeIds must be an array"))?;
251                let ids: Vec<u64> = requested.iter().map(|id| id.as_u64().ok_or_else(||Error::params("Invalid backendNodeId"))).collect::<Result<_>>()?;
252                let nodes = self.inspector.snapshot()?;
253                let map: HashMap<_,_> = nodes.iter().map(|n|(n.id,n)).collect();
254                let result: Vec<_> = ids.iter().map(|id| if *id == 1 || map.contains_key(id) { *id } else { 0 }).collect();
255                let mut parents = HashSet::new();
256                for id in &result {
257                    let mut current = map.get(id).and_then(|n|n.parent);
258                    while let Some(parent) = current { parents.insert(parent); current = map.get(&parent).and_then(|n|n.parent); }
259                }
260                if result.iter().any(|id|*id>1) {
261                    let roots: Vec<_> = nodes.first().map(|n|dom_node(n.id,&map,0)).into_iter().collect();
262                    self.events.push(json!({"method":"DOM.setChildNodes","params":{"parentId":1,"nodes":roots}}));
263                    for n in nodes.iter().filter(|n|parents.contains(&n.id)) {
264                        let children: Vec<_> = n.children.iter().map(|id|dom_node(*id,&map,0)).collect();
265                        self.events.push(json!({"method":"DOM.setChildNodes","params":{"parentId":n.id,"nodes":children}}));
266                    }
267                }
268                Ok(json!({"nodeIds":result}))
269            }
270            "DOM.getAttributes" => {
271                let id = node_id(p)?;
272                let nodes = self.inspector.snapshot()?;
273                Ok(json!({"attributes":nodes.iter().find(|n|n.id == id).ok_or_else(||Error::server("Unknown node"))?.attributes}))
274            }
275            "DOM.querySelector" | "DOM.querySelectorAll" => {
276                let scope = node_id(p)?; let selector = string(p,"selector")?;
277                let ids = self.inspector.query(if scope==1 {None} else {Some(scope)},selector)?;
278                Ok(if method.ends_with("All") { json!({"nodeIds":ids}) } else { json!({"nodeId":ids.first().copied().unwrap_or(0)}) })
279            }
280            "DOM.setAttributeValue" => { self.inspector.set_attribute(node_id(p)?,string(p,"name")?,string(p,"value")?)?; Ok(json!({})) }
281            "DOM.getBoxModel" => {
282                let b = self.inspector.box_model(node_id(p)?)?;
283                Ok(json!({"model":{"content":quad(b.content),"padding":quad(b.padding),"border":quad(b.border),"margin":quad(b.margin),"width":b.border.width.round() as i64,"height":b.border.height.round() as i64}}))
284            }
285            "DOM.getContentQuads" => Ok(json!({"quads":[quad(self.inspector.box_model(node_id(p)?)?.content)]})),
286            "DOM.getNodeForLocation" => {
287                let root = self.inspector.root()?;
288                let visual = root.get_visual_at(Point::new(number(p,"x")?,number(p,"y")?)).ok_or_else(||Error::server("No node at point"))?;
289                let id = self.inspector.id_for(&visual.upcast::<StyledElement>());
290                Ok(json!({"nodeId":id,"backendNodeId":id,"frameId":self.target_id}))
291            }
292            "DOM.focus" => { self.inspector.element(node_id(p)?)?.cast::<InputElement>().ok_or_else(||Error::server("Node cannot receive input"))?.focus(); Ok(json!({})) }
293            "CSS.getComputedStyleForNode" => Ok(json!({"computedStyle":self.inspector.properties(node_id(p)?)?.iter().map(|p|json!({"name":p.name,"value":p.value})).collect::<Vec<_>>()})),
294            "CSS.getInlineStylesForNode" => {
295                let id = node_id(p)?;
296                Ok(json!({"inlineStyle":style(self.inspector.properties(id)?.into_iter().filter(|p|p.local).map(|p|json!({"name":p.name,"value":p.value})).collect())}))
297            }
298            "CSS.getMatchedStylesForNode" => {
299                let id = node_id(p)?;
300                let rules: Vec<_> = self.inspector.matched_styles(id)?.into_iter().map(|s| json!({"rule":{"selectorList":{"selectors":[{"text":s.selector}],"text":s.selector},"origin":"regular","style":style(s.properties.into_iter().map(|(name,value)|json!({"name":name,"value":value})).collect())},"matchingSelectors":[0]})).collect();
301                Ok(json!({"matchedCSSRules":rules,"inherited":[],"pseudoElements":[],"inlineStyle":style(self.inspector.properties(id)?.into_iter().filter(|p|p.local).map(|p|json!({"name":p.name,"value":p.value})).collect())}))
302            }
303            "Overlay.highlightNode" => { self.inspector.highlight(node_id(p)?)?; Ok(json!({})) }
304            "Overlay.hideHighlight" => { self.inspector.hide_highlight(); Ok(json!({})) }
305            "Overlay.setInspectMode" => { self.inspect_mode(string(p,"mode")?)?; Ok(json!({})) }
306            "Page.captureScreenshot" => {
307                if p["format"].as_str().is_some_and(|f|f!="png") || p.get("clip").is_some() { return Err(Error::params("Only full-window PNG screenshots are supported")); }
308                Ok(json!({"data":base64::engine::general_purpose::STANDARD.encode(self.inspector.screenshot()?)}))
309            }
310            "Page.getLayoutMetrics" => {
311                let bounds = self.inspector.bounds()?;
312                let layout = json!({"pageX":0,"pageY":0,"clientWidth":bounds.width,"clientHeight":bounds.height});
313                let visual = json!({"offsetX":0,"offsetY":0,"pageX":0,"pageY":0,"clientWidth":bounds.width,"clientHeight":bounds.height,"scale":1,"zoom":1});
314                Ok(json!({"layoutViewport":layout,"visualViewport":visual,"contentSize":rect(bounds),"cssLayoutViewport":layout,"cssVisualViewport":visual,"cssContentSize":rect(bounds)}))
315            }
316            "Page.getFrameTree" | "Page.getResourceTree" => Ok(json!({"frameTree":{"resources":[],"frame":{"id":self.target_id,"loaderId":self.target_id,"url":self.url(),"domainAndRegistry":"","securityOrigin":"ferroui://","mimeType":"application/xml","secureContextType":"Secure","crossOriginIsolatedContextType":"NotIsolated","gatedAPIFeatures":[]}}})),
317            "Accessibility.getFullAXTree" | "Accessibility.queryAXTree" => self.accessibility(p,method.ends_with("queryAXTree")),
318            "Runtime.evaluate" | "Runtime.callFunctionOn" => Err(Error::server("FerroUI has no JavaScript runtime. Use DOM, CSS and Input commands through a CDP session.")),
319            "Schema.getDomains" => {
320                let schema: Value = serde_json::from_str(include_str!("protocol.json")).expect("embedded protocol");
321                Ok(json!({"domains":schema["domains"].as_array().unwrap().iter().map(|d|json!({"name":d["domain"],"version":"1.3"})).collect::<Vec<_>>()}))
322            }
323            "Runtime.runIfWaitingForDebugger" => Ok(json!({})),
324            // These client setup commands have no effect on a native, non-navigating target.
325            "Page.setLifecycleEventsEnabled" | "Page.setInterceptFileChooserDialog" | "Emulation.setFocusEmulationEnabled" | "Emulation.setEmulatedMedia" | "Network.enable" | "Network.disable" | "Target.setAutoAttach" => Ok(json!({})),
326            "Page.addScriptToEvaluateOnNewDocument" if p["source"].as_str() == Some("") => Ok(json!({"identifier":"empty-script"})),
327            "Page.addScriptToEvaluateOnNewDocument" => Err(Error::server("FerroUI has no document scripts")),
328            method if method.starts_with("Input.") => self.input.dispatch(&self.inspector.root()?,method,p),
329            _ => Err(Error::method(method)),
330        }
331    }
332    fn url(&self) -> String {
333        format!("ferroui://{}", self.target_id)
334    }
335    fn stop_inspecting(&mut self) {
336        for s in self.inspect_subscriptions.drain(..) {
337            s.dispose();
338        }
339        self.picked.borrow_mut().clear();
340    }
341    fn inspect_mode(&mut self, mode: &str) -> Result<()> {
342        if !matches!(mode, "none" | "searchForNode") {
343            return Err(Error::params("mode must be none or searchForNode"));
344        }
345        self.stop_inspecting();
346        self.inspector.hide_highlight();
347        if mode == "none" {
348            return Ok(());
349        }
350        let root = self.inspector.root()?;
351        let queue = self.picked.clone();
352        let weak = root.downgrade();
353        let token = root.add_handler_with(
354            InputElement::pointer_moved_event(),
355            move |_, e| {
356                if let Some(root) = weak.upgrade() {
357                    let mut q = queue.borrow_mut();
358                    if q.len() < 64 {
359                        q.push((e.get_position(Some(&root)), false));
360                    }
361                }
362            },
363            RoutingStrategies::TUNNEL,
364            true,
365        );
366        let weak = root.downgrade();
367        self.inspect_subscriptions.push(Disposable::create(move || {
368            if let Some(root) = weak.upgrade() {
369                root.remove_handler(InputElement::pointer_moved_event(), token);
370            }
371        }));
372        let queue = self.picked.clone();
373        let weak = root.downgrade();
374        let token = root.add_handler_with(
375            InputElement::pointer_pressed_event(),
376            move |_, e| {
377                if let Some(root) = weak.upgrade() {
378                    let mut q = queue.borrow_mut();
379                    if q.len() < 64 {
380                        q.push((e.get_position(Some(&root)), true));
381                    }
382                    e.set_handled(true);
383                }
384            },
385            RoutingStrategies::TUNNEL,
386            true,
387        );
388        let weak = root.downgrade();
389        self.inspect_subscriptions.push(Disposable::create(move || {
390            if let Some(root) = weak.upgrade() {
391                root.remove_handler(InputElement::pointer_pressed_event(), token);
392            }
393        }));
394        Ok(())
395    }
396    /// Poll on the UI thread, typically every 100 ms. Tree changes invalidate the document.
397    pub fn poll(&mut self) -> Vec<Value> {
398        let points = std::mem::take(&mut *self.picked.borrow_mut());
399        for (point, pressed) in points {
400            if let Ok(root) = self.inspector.root() {
401                if let Some(visual) = root.get_visual_at(point) {
402                    let id = self.inspector.id_for(&visual.upcast::<StyledElement>());
403                    let _ = self.inspector.highlight(id);
404                    self.events.push(if pressed { json!({"method":"Overlay.inspectNodeRequested","params":{"backendNodeId":id}}) } else { json!({"method":"Overlay.nodeHighlightRequested","params":{"nodeId":id}}) });
405                }
406            }
407        }
408        if self.enabled.contains("DOM") {
409            if let Ok(nodes) = self.inspector.snapshot() {
410                if let Some(previous) = &self.last_tree {
411                    let topology_changed = previous.len() != nodes.len()
412                        || previous.iter().zip(&nodes).any(|(a, b)| {
413                            a.id != b.id
414                                || a.parent != b.parent
415                                || a.name != b.name
416                                || a.children != b.children
417                        });
418                    if topology_changed {
419                        self.events
420                            .push(json!({"method":"DOM.documentUpdated","params":{}}));
421                    } else {
422                        for (before, after) in previous.iter().zip(&nodes) {
423                            let old: HashMap<_, _> = before
424                                .attributes
425                                .as_chunks::<2>()
426                                .0
427                                .iter()
428                                .map(|p| (&p[0], &p[1]))
429                                .collect();
430                            let new: HashMap<_, _> = after
431                                .attributes
432                                .as_chunks::<2>()
433                                .0
434                                .iter()
435                                .map(|p| (&p[0], &p[1]))
436                                .collect();
437                            for (name, value) in &new {
438                                if old.get(name) != Some(value) {
439                                    self.events.push(json!({"method":"DOM.attributeModified","params":{"nodeId":after.id,"name":name,"value":value}}));
440                                }
441                            }
442                            for name in old.keys().filter(|name| !new.contains_key(*name)) {
443                                self.events.push(json!({"method":"DOM.attributeRemoved","params":{"nodeId":after.id,"name":name}}));
444                            }
445                        }
446                    }
447                }
448                self.last_tree = Some(nodes);
449            }
450        }
451        std::mem::take(&mut self.events)
452    }
453    fn accessibility(&mut self, p: &Value, query: bool) -> Result<Value> {
454        let root: Ref<Control> =
455            if query && (p.get("nodeId").is_some() || p.get("backendNodeId").is_some()) {
456                self.inspector
457                    .element(node_id(p)?)?
458                    .cast::<Control>()
459                    .ok_or_else(|| Error::server("Node has no automation peer"))?
460            } else {
461                self.inspector.root()?.upcast()
462            };
463        let peer = ControlAutomationPeer::create_peer_for_element(&root);
464        let max_depth = if p.get("depth").is_some() {
465            depth(p, 256)?
466        } else {
467            256
468        };
469        let mut pending: Vec<(Ref<AutomationPeer>, Option<String>, i64)> = vec![(peer, None, 0)];
470        let mut nodes = Vec::new();
471        let mut seen = HashSet::new();
472        while let Some((peer, parent, d)) = pending.pop() {
473            let Some(owner) = peer
474                .cast::<ControlAutomationPeer>()
475                .and_then(|p| p.try_owner())
476            else {
477                continue;
478            };
479            let id = self.inspector.id_for(&owner.upcast::<StyledElement>());
480            if !seen.insert(id) {
481                continue;
482            }
483            let children = if d < max_depth {
484                peer.get_children().iter().cloned().collect::<Vec<_>>()
485            } else {
486                vec![]
487            };
488            let child_ids: Vec<_> = children
489                .iter()
490                .filter_map(|c| {
491                    c.cast::<ControlAutomationPeer>()
492                        .and_then(|p| p.try_owner())
493                })
494                .map(|c| {
495                    self.inspector
496                        .id_for(&c.upcast::<StyledElement>())
497                        .to_string()
498                })
499                .collect();
500            let role = format!("{:?}", peer.get_automation_control_type());
501            let name = peer.get_name();
502            if !query
503                || (p["accessibleName"].as_str().is_none_or(|n| n == name)
504                    && p["role"]
505                        .as_str()
506                        .is_none_or(|r| r.eq_ignore_ascii_case(&role)))
507            {
508                let mut node = json!({"nodeId":id.to_string(),"backendDOMNodeId":id,"ignored":false,"role":{"type":"role","value":role},"name":{"type":"computedString","value":name},"description":{"type":"computedString","value":peer.get_help_text()},"childIds":child_ids,"properties":[{"name":"disabled","value":{"type":"boolean","value":!peer.is_enabled()}},{"name":"focusable","value":{"type":"boolean","value":peer.is_keyboard_focusable()}},{"name":"focused","value":{"type":"boolean","value":peer.has_keyboard_focus()}}]});
509                if let Some(parent) = parent {
510                    node["parentId"] = json!(parent);
511                }
512                nodes.push(node);
513            }
514            pending.extend(
515                children
516                    .into_iter()
517                    .rev()
518                    .map(|c| (c, Some(id.to_string()), d + 1)),
519            );
520        }
521        Ok(json!({"nodes":nodes}))
522    }
523}
524impl Drop for CdpSession {
525    fn drop(&mut self) {
526        self.stop_inspecting();
527    }
528}
529pub(crate) fn response(id: Value, result: Result<Value>) -> Value {
530    match result {
531        Ok(result) => json!({"id":id,"result":result}),
532        Err(e) => json!({"id":id,"error":{"code":e.code,"message":e.message}}),
533    }
534}