use crate::cdp::session::CdpSession;
use crate::error::{Error, Result};
use serde_json::{json, Value};
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Clone)]
pub struct Accessibility {
inner: Arc<AccessibilityInner>,
}
struct AccessibilityInner {
session: CdpSession,
}
impl Accessibility {
pub(crate) fn new(session: CdpSession) -> Self {
Self {
inner: Arc::new(AccessibilityInner { session }),
}
}
pub async fn snapshot(
&self,
options: Option<AccessibilitySnapshotOptions>,
) -> Result<Option<AccessibilityNode>> {
let opts = options.unwrap_or_default();
let interesting_only = opts.interesting_only.unwrap_or(true);
let _ = self.inner.session.send("Accessibility.enable", json!({})).await;
let nodes = if let Some(root_node_id) = &opts.root {
self.partial_tree(root_node_id).await?
} else {
let resp: Value = self
.inner
.session
.send("Accessibility.getFullAXTree", json!({}))
.await?;
resp.get("nodes")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default()
};
if nodes.is_empty() {
return Ok(None);
}
let root_id = match nodes.first().and_then(|n| n.get("nodeId")).and_then(|v| v.as_str()) {
Some(id) => id.to_string(),
None => return Ok(None),
};
let tree = build_tree(&nodes, &root_id, interesting_only)?;
Ok(tree)
}
async fn partial_tree(&self, backend_node_id: &str) -> Result<Vec<Value>> {
let backend: i64 = backend_node_id
.parse()
.map_err(|_| Error::InvalidArgument("root must be a numeric backendNodeId".into()))?;
let resp: Value = self
.inner
.session
.send(
"Accessibility.getPartialAXTree",
json!({ "backendNodeId": backend }),
)
.await?;
Ok(resp
.get("nodes")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default())
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct AccessibilitySnapshotOptions {
pub interesting_only: Option<bool>,
pub root: Option<String>,
}
impl AccessibilitySnapshotOptions {
pub fn interesting_only(mut self, v: bool) -> Self {
self.interesting_only = Some(v);
self
}
pub fn root(mut self, v: impl Into<String>) -> Self {
self.root = Some(v.into());
self
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct AccessibilityNode {
pub role: Option<String>,
pub name: Option<String>,
pub value: Option<Value>,
pub description: Option<String>,
pub checked: Option<String>,
pub disabled: Option<bool>,
pub expanded: Option<String>,
pub focused: Option<bool>,
pub modal: Option<bool>,
pub multiline: Option<bool>,
pub readonly: Option<bool>,
pub required: Option<bool>,
pub selected: Option<bool>,
pub hidden: Option<bool>,
pub children: Vec<AccessibilityNode>,
}
impl AccessibilityNode {
pub fn role(&self) -> Option<&str> {
self.role.as_deref()
}
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
}
fn build_tree(
nodes: &[Value],
root_id: &str,
interesting_only: bool,
) -> Result<Option<AccessibilityNode>> {
let mut by_id: HashMap<String, &Value> = HashMap::with_capacity(nodes.len());
for n in nodes {
if let Some(id) = n.get("nodeId").and_then(|v| v.as_str()) {
by_id.insert(id.to_string(), n);
}
}
let root = by_id.get(root_id).ok_or_else(|| {
Error::ProtocolError(format!(
"accessibility tree root node '{root_id}' not found in response"
))
})?;
let mut node = convert_node(root, &by_id, interesting_only)?;
if interesting_only {
node = prune(node);
}
if node.role.is_none()
&& node.name.is_none()
&& node.value.is_none()
&& node.description.is_none()
&& node.children.is_empty()
{
return Ok(None);
}
Ok(Some(node))
}
fn convert_node(
raw: &Value,
by_id: &HashMap<String, &Value>,
interesting_only: bool,
) -> Result<AccessibilityNode> {
let role = property_value(raw, "role").or_else(|| {
raw.get("role")
.and_then(|r| r.get("value"))
.and_then(|v| v.as_str())
.map(String::from)
});
let name = property_value(raw, "name").or_else(|| {
raw.get("name")
.and_then(|n| n.get("value"))
.and_then(|v| v.as_str())
.map(String::from)
});
let description = property_value(raw, "description").or_else(|| {
raw.get("description")
.and_then(|d| d.get("value"))
.and_then(|v| v.as_str())
.map(String::from)
});
let value = raw
.get("value")
.and_then(|v| v.get("value"))
.cloned()
.filter(|v| !v.is_null());
let checked = property_value(raw, "checked").filter(|s| s != "false");
let expanded = property_value(raw, "expanded").map(|s| match s.as_str() {
"true" => "expanded".to_string(),
"false" => "collapsed".to_string(),
other => other.to_string(),
});
let disabled = boolean_property(raw, "disabled");
let focused = boolean_property(raw, "focused");
let modal = boolean_property(raw, "modal");
let multiline = boolean_property(raw, "multiline");
let readonly = boolean_property(raw, "readonly");
let required = boolean_property(raw, "required");
let selected = boolean_property(raw, "selected");
let hidden = boolean_property(raw, "hidden")
.or_else(|| property_value(raw, "visibility").map(|s| s == "hidden"));
let mut children = Vec::new();
if let Some(child_ids) = raw.get("childIds").and_then(|v| v.as_array()) {
for cid in child_ids {
if let Some(id) = cid.as_str() {
if let Some(child_raw) = by_id.get(id) {
let mut child = convert_node(child_raw, by_id, interesting_only)?;
if interesting_only {
child = prune(child);
}
let empty = child.role.is_none()
&& child.name.is_none()
&& child.value.is_none()
&& child.description.is_none()
&& child.children.is_empty();
if !empty {
children.push(child);
}
}
}
}
}
Ok(AccessibilityNode {
role,
name,
value,
description,
checked,
disabled,
expanded,
focused,
modal,
multiline,
readonly,
required,
selected,
hidden,
children,
})
}
fn property_value(node: &Value, name: &str) -> Option<String> {
let props = node.get("properties").and_then(|v| v.as_array())?;
for p in props {
let matches = p
.get("name")
.and_then(|v| v.as_str())
.map(|n| n == name)
.unwrap_or(false);
if matches {
return p
.get("value")
.and_then(|v| v.get("value"))
.and_then(|v| v.as_str())
.map(String::from);
}
}
None
}
fn boolean_property(node: &Value, name: &str) -> Option<bool> {
let props = node.get("properties").and_then(|v| v.as_array())?;
for p in props {
let matches = p
.get("name")
.and_then(|v| v.as_str())
.map(|n| n == name)
.unwrap_or(false);
if matches {
return p
.get("value")
.and_then(|v| v.get("value"))
.and_then(|v| v.as_bool());
}
}
None
}
fn prune(node: AccessibilityNode) -> AccessibilityNode {
let has_payload = node.role.is_some()
|| node.name.is_some()
|| node.value.is_some()
|| node.description.is_some()
|| node.checked.is_some()
|| node.expanded.is_some()
|| node.disabled == Some(true)
|| node.focused == Some(true)
|| node.selected == Some(true);
if has_payload || !node.children.is_empty() {
node
} else {
AccessibilityNode::default()
}
}