1use crate::error::{CdpError, Result};
2use crate::page::{Page, element::ElementHandle};
3use cdp_protocol::accessibility as ax;
4use cdp_protocol::accessibility::{
5 Disable as AccessibilityDisable, DisableReturnObject as AccessibilityDisableReturnObject,
6 Enable as AccessibilityEnable, EnableReturnObject as AccessibilityEnableReturnObject,
7 GetFullAXTree, GetFullAXTreeReturnObject, GetPartialAXTree, GetPartialAXTreeReturnObject,
8};
9use std::collections::{HashMap, HashSet};
10use std::sync::Arc;
11
12pub struct AccessibilityController {
14 page: Arc<Page>,
15}
16
17impl AccessibilityController {
18 pub(crate) fn new(page: Arc<Page>) -> Self {
19 Self { page }
20 }
21
22 pub async fn snapshot(
35 &self,
36 options: Option<AccessibilitySnapshotOptions>,
37 ) -> Result<AccessibilitySnapshot> {
38 let opts = options.unwrap_or_default();
39 self.enable().await?;
40
41 let (nodes, root_backend_id) = if let Some(element) = opts.root.as_ref() {
42 self.ensure_same_page(element)?;
43 let method = GetPartialAXTree {
44 node_id: element.node_id,
45 backend_node_id: Some(element.backend_node_id),
46 object_id: None,
47 fetch_relatives: Some(true),
48 };
49 let response: GetPartialAXTreeReturnObject =
50 self.page.session.send_command(method, None).await?;
51 (response.nodes, Some(element.backend_node_id))
52 } else {
53 let method = GetFullAXTree {
54 depth: opts.max_depth,
55 frame_id: None,
56 };
57 let response: GetFullAXTreeReturnObject =
58 self.page.session.send_command(method, None).await?;
59 (response.nodes, None)
60 };
61
62 Ok(AccessibilitySnapshot::from_ax_nodes(
63 nodes,
64 root_backend_id,
65 opts.interesting_only,
66 ))
67 }
68
69 pub async fn enable(&self) -> Result<()> {
71 let method = AccessibilityEnable(None);
72 let _: AccessibilityEnableReturnObject =
73 self.page.session.send_command(method, None).await?;
74 Ok(())
75 }
76
77 pub async fn disable(&self) -> Result<()> {
79 let method = AccessibilityDisable(None);
80 let _: AccessibilityDisableReturnObject =
81 self.page.session.send_command(method, None).await?;
82 Ok(())
83 }
84
85 fn ensure_same_page(&self, element: &ElementHandle) -> Result<()> {
86 if !Arc::ptr_eq(&self.page, &element.page) {
87 return Err(CdpError::page(
88 "Accessibility snapshot root element must belong to the same page".to_string(),
89 ));
90 }
91 Ok(())
92 }
93}
94
95#[derive(Clone)]
97pub struct AccessibilitySnapshotOptions {
98 pub root: Option<ElementHandle>,
99 pub interesting_only: bool,
100 pub max_depth: Option<u32>,
101}
102
103impl Default for AccessibilitySnapshotOptions {
104 fn default() -> Self {
105 Self {
106 root: None,
107 interesting_only: true,
108 max_depth: None,
109 }
110 }
111}
112
113impl AccessibilitySnapshotOptions {
114 pub fn with_root(mut self, element: ElementHandle) -> Self {
115 self.root = Some(element);
116 self
117 }
118
119 pub fn with_interesting_only(mut self, interesting_only: bool) -> Self {
120 self.interesting_only = interesting_only;
121 self
122 }
123
124 pub fn with_max_depth(mut self, depth: u32) -> Self {
125 self.max_depth = Some(depth);
126 self
127 }
128}
129
130#[derive(Clone, Debug, Default)]
132pub struct AccessibilitySnapshot {
133 pub roots: Vec<AccessibilityNode>,
134}
135
136impl AccessibilitySnapshot {
137 fn from_ax_nodes(
138 nodes: Vec<ax::AxNode>,
139 root_backend_id: Option<u32>,
140 interesting_only: bool,
141 ) -> Self {
142 let mut map: HashMap<String, ax::AxNode> = HashMap::new();
143 for node in nodes {
144 map.insert(node.node_id.clone(), node);
145 }
146
147 let mut referenced_children: HashSet<String> = HashSet::new();
148 for node in map.values() {
149 if let Some(children) = &node.child_ids {
150 for child in children {
151 referenced_children.insert(child.clone());
152 }
153 }
154 }
155
156 let mut candidate_roots: Vec<String> = Vec::new();
157 if let Some(backend_id) = root_backend_id
158 && let Some(root) = map
159 .values()
160 .find(|node| node.backend_dom_node_id == Some(backend_id))
161 {
162 candidate_roots.push(root.node_id.clone());
163 }
164
165 if candidate_roots.is_empty() {
166 for node in map.values() {
167 let parent_known = node.parent_id.as_ref().and_then(|parent| map.get(parent));
168 if parent_known.is_none() && !referenced_children.contains(&node.node_id) {
169 candidate_roots.push(node.node_id.clone());
170 }
171 }
172 }
173
174 if candidate_roots.is_empty()
175 && let Some(first) = map.keys().next()
176 {
177 candidate_roots.push(first.clone());
178 }
179
180 let mut visited: HashSet<String> = HashSet::new();
181 let mut roots: Vec<AccessibilityNode> = Vec::new();
182 for root_id in candidate_roots {
183 roots.extend(build_subtree(
184 &root_id,
185 &map,
186 interesting_only,
187 &mut visited,
188 ));
189 }
190
191 Self { roots }
192 }
193
194 pub fn is_empty(&self) -> bool {
195 self.roots.is_empty()
196 }
197}
198
199fn build_subtree(
200 node_id: &str,
201 nodes: &HashMap<String, ax::AxNode>,
202 interesting_only: bool,
203 visited: &mut HashSet<String>,
204) -> Vec<AccessibilityNode> {
205 if !visited.insert(node_id.to_string()) {
206 return Vec::new();
207 }
208
209 let node = match nodes.get(node_id) {
210 Some(node) => node.clone(),
211 None => return Vec::new(),
212 };
213
214 let child_ids = node.child_ids.clone().unwrap_or_default();
215 let mut children: Vec<AccessibilityNode> = Vec::new();
216 for child_id in child_ids {
217 children.extend(build_subtree(&child_id, nodes, interesting_only, visited));
218 }
219
220 let interesting = !interesting_only || !node.ignored;
221 if interesting {
222 vec![AccessibilityNode::from_ax_node(node, children)]
223 } else {
224 children
225 }
226}
227
228#[derive(Clone, Debug)]
230pub struct AccessibilityNode {
231 pub node_id: String,
232 pub ignored: bool,
233 pub ignored_reasons: Vec<AccessibilityProperty>,
234 pub role: Option<ax::AxValue>,
235 pub chrome_role: Option<ax::AxValue>,
236 pub name: Option<ax::AxValue>,
237 pub description: Option<ax::AxValue>,
238 pub value: Option<ax::AxValue>,
239 pub properties: Vec<AccessibilityProperty>,
240 pub backend_dom_node_id: Option<u32>,
241 pub frame_id: Option<String>,
242 pub children: Vec<AccessibilityNode>,
243}
244
245impl AccessibilityNode {
246 fn from_ax_node(node: ax::AxNode, children: Vec<AccessibilityNode>) -> Self {
247 Self {
248 node_id: node.node_id,
249 ignored: node.ignored,
250 ignored_reasons: convert_properties(node.ignored_reasons),
251 role: node.role,
252 chrome_role: node.chrome_role,
253 name: node.name,
254 description: node.description,
255 value: node.value,
256 properties: convert_properties(node.properties),
257 backend_dom_node_id: node.backend_dom_node_id,
258 frame_id: node.frame_id,
259 children,
260 }
261 }
262}
263
264#[derive(Clone, Debug)]
266pub struct AccessibilityProperty {
267 pub name: String,
268 pub value: ax::AxValue,
269}
270
271fn convert_properties(input: Option<Vec<ax::AxProperty>>) -> Vec<AccessibilityProperty> {
272 input
273 .unwrap_or_default()
274 .into_iter()
275 .map(|prop| AccessibilityProperty {
276 name: serialize_property_name(&prop.name),
277 value: prop.value,
278 })
279 .collect()
280}
281
282fn serialize_property_name(name: &ax::AxPropertyName) -> String {
283 serde_json::to_value(name)
284 .ok()
285 .and_then(|val| val.as_str().map(|s| s.to_string()))
286 .unwrap_or_else(|| format!("{:?}", name))
287}