viewpoint_core/page/frame/
aria.rs1use std::collections::HashMap;
4
5use futures::stream::{FuturesUnordered, StreamExt};
6use tracing::{debug, instrument, trace};
7use viewpoint_cdp::protocol::dom::{BackendNodeId, DescribeNodeParams, DescribeNodeResult};
8use viewpoint_cdp::protocol::runtime::EvaluateParams;
9use viewpoint_js::js;
10
11use super::Frame;
12use crate::error::PageError;
13use crate::page::aria_snapshot::{SnapshotOptions, apply_refs_to_snapshot};
14use crate::page::locator::aria_js::aria_snapshot_with_refs_js;
15
16impl Frame {
17 #[instrument(level = "debug", skip(self), fields(frame_id = %self.id))]
42 pub async fn aria_snapshot(&self) -> Result<crate::page::locator::AriaSnapshot, PageError> {
43 self.aria_snapshot_with_options(SnapshotOptions::default())
44 .await
45 }
46
47 #[instrument(level = "debug", skip(self, options), fields(frame_id = %self.id))]
64 pub async fn aria_snapshot_with_options(
65 &self,
66 options: SnapshotOptions,
67 ) -> Result<crate::page::locator::AriaSnapshot, PageError> {
68 if self.is_detached() {
69 return Err(PageError::EvaluationFailed("Frame is detached".to_string()));
70 }
71
72 self.capture_snapshot_with_refs(options).await
74 }
75
76 #[instrument(level = "debug", skip(self, options), fields(frame_id = %self.id))]
88 pub(super) async fn capture_snapshot_with_refs(
89 &self,
90 options: SnapshotOptions,
91 ) -> Result<crate::page::locator::AriaSnapshot, PageError> {
92 let snapshot_fn = aria_snapshot_with_refs_js();
93
94 let js_code = js! {
97 (function() {
98 const getSnapshotWithRefs = @{snapshot_fn};
99 return getSnapshotWithRefs(document.body);
100 })()
101 };
102
103 let context_id = self.main_world_context_id();
105 trace!(context_id = ?context_id, "Using execution context for aria_snapshot()");
106
107 let result: viewpoint_cdp::protocol::runtime::EvaluateResult = self
110 .connection
111 .send_command(
112 "Runtime.evaluate",
113 Some(EvaluateParams {
114 expression: js_code,
115 object_group: Some("viewpoint-snapshot".to_string()),
116 include_command_line_api: None,
117 silent: Some(true),
118 context_id,
119 return_by_value: Some(false), await_promise: Some(false),
121 }),
122 Some(&self.session_id),
123 )
124 .await?;
125
126 if let Some(exception) = result.exception_details {
127 return Err(PageError::EvaluationFailed(exception.text));
128 }
129
130 let result_object_id = result.result.object_id.ok_or_else(|| {
131 PageError::EvaluationFailed("No object ID from snapshot evaluation".to_string())
132 })?;
133
134 let snapshot_value = self
136 .get_property_value(&result_object_id, "snapshot")
137 .await?;
138
139 let mut snapshot: crate::page::locator::AriaSnapshot =
141 serde_json::from_value(snapshot_value).map_err(|e| {
142 PageError::EvaluationFailed(format!("Failed to parse aria snapshot: {e}"))
143 })?;
144
145 if options.get_include_refs() {
147 let elements_result = self
149 .get_property_object(&result_object_id, "elements")
150 .await?;
151
152 if let Some(elements_object_id) = elements_result {
153 let element_object_ids = self.get_all_array_elements(&elements_object_id).await?;
155 let element_count = element_object_ids.len();
156
157 debug!(
158 element_count = element_count,
159 max_concurrency = options.get_max_concurrency(),
160 "Resolving element refs in parallel"
161 );
162
163 let ref_map = self
165 .resolve_node_ids_parallel(element_object_ids, options.get_max_concurrency())
166 .await;
167
168 debug!(
169 resolved_count = ref_map.len(),
170 total_count = element_count,
171 "Completed parallel ref resolution"
172 );
173
174 let _ = apply_refs_to_snapshot(
180 &mut snapshot,
181 &ref_map,
182 self.context_index,
183 self.page_index,
184 self.frame_index,
185 );
186
187 let _ = self.release_object(&elements_object_id).await;
189 }
190 }
191
192 let _ = self.release_object(&result_object_id).await;
194
195 Ok(snapshot)
196 }
197
198 async fn get_all_array_elements(
203 &self,
204 array_object_id: &str,
205 ) -> Result<Vec<(usize, String)>, PageError> {
206 #[derive(Debug, serde::Deserialize)]
207 struct PropertyDescriptor {
208 name: String,
209 value: Option<viewpoint_cdp::protocol::runtime::RemoteObject>,
210 }
211
212 #[derive(Debug, serde::Deserialize)]
213 struct GetPropertiesResult {
214 result: Vec<PropertyDescriptor>,
215 }
216
217 let result: GetPropertiesResult = self
218 .connection
219 .send_command(
220 "Runtime.getProperties",
221 Some(serde_json::json!({
222 "objectId": array_object_id,
223 "ownProperties": true,
224 "generatePreview": false
225 })),
226 Some(&self.session_id),
227 )
228 .await?;
229
230 let mut elements: Vec<(usize, String)> = Vec::new();
232
233 for prop in result.result {
234 if let Ok(index) = prop.name.parse::<usize>() {
236 if let Some(value) = prop.value {
237 if let Some(object_id) = value.object_id {
238 elements.push((index, object_id));
239 }
240 }
241 }
242 }
243
244 elements.sort_by_key(|(index, _)| *index);
246
247 trace!(
248 element_count = elements.len(),
249 "Batch-fetched array elements"
250 );
251
252 Ok(elements)
253 }
254
255 async fn resolve_node_ids_parallel(
260 &self,
261 element_object_ids: Vec<(usize, String)>,
262 max_concurrency: usize,
263 ) -> HashMap<usize, BackendNodeId> {
264 let mut ref_map = HashMap::new();
265
266 for chunk in element_object_ids.chunks(max_concurrency) {
268 let futures: FuturesUnordered<_> = chunk
269 .iter()
270 .map(|(index, object_id)| {
271 let index = *index;
272 let object_id = object_id.clone();
273 async move {
274 match self.describe_node(&object_id).await {
275 Ok(backend_node_id) => {
276 trace!(
277 index = index,
278 backend_node_id = backend_node_id,
279 "Resolved element ref"
280 );
281 Some((index, backend_node_id))
282 }
283 Err(e) => {
284 debug!(index = index, error = %e, "Failed to get backendNodeId for element");
285 None
286 }
287 }
288 }
289 })
290 .collect();
291
292 let results: Vec<_> = futures.collect().await;
294 for result in results.into_iter().flatten() {
295 ref_map.insert(result.0, result.1);
296 }
297 }
298
299 ref_map
300 }
301
302 pub(super) async fn get_property_value(
304 &self,
305 object_id: &str,
306 property: &str,
307 ) -> Result<serde_json::Value, PageError> {
308 #[derive(Debug, serde::Deserialize)]
309 struct CallResult {
310 result: viewpoint_cdp::protocol::runtime::RemoteObject,
311 }
312
313 let js_fn = js! {
314 (function() { return this[#{property}]; })
315 };
316 let function_declaration = js_fn.trim_start_matches('(').trim_end_matches(')');
318
319 let result: CallResult = self
320 .connection
321 .send_command(
322 "Runtime.callFunctionOn",
323 Some(serde_json::json!({
324 "objectId": object_id,
325 "functionDeclaration": function_declaration,
326 "returnByValue": true
327 })),
328 Some(&self.session_id),
329 )
330 .await?;
331
332 Ok(result.result.value.unwrap_or(serde_json::Value::Null))
333 }
334
335 pub(super) async fn get_property_object(
337 &self,
338 object_id: &str,
339 property: &str,
340 ) -> Result<Option<String>, PageError> {
341 #[derive(Debug, serde::Deserialize)]
342 struct CallResult {
343 result: viewpoint_cdp::protocol::runtime::RemoteObject,
344 }
345
346 let js_fn = js! {
347 (function() { return this[#{property}]; })
348 };
349 let function_declaration = js_fn.trim_start_matches('(').trim_end_matches(')');
351
352 let result: CallResult = self
353 .connection
354 .send_command(
355 "Runtime.callFunctionOn",
356 Some(serde_json::json!({
357 "objectId": object_id,
358 "functionDeclaration": function_declaration,
359 "returnByValue": false
360 })),
361 Some(&self.session_id),
362 )
363 .await?;
364
365 Ok(result.result.object_id)
366 }
367
368 pub(super) async fn describe_node(&self, object_id: &str) -> Result<BackendNodeId, PageError> {
370 let result: DescribeNodeResult = self
371 .connection
372 .send_command(
373 "DOM.describeNode",
374 Some(DescribeNodeParams {
375 node_id: None,
376 backend_node_id: None,
377 object_id: Some(object_id.to_string()),
378 depth: Some(0),
379 pierce: None,
380 }),
381 Some(&self.session_id),
382 )
383 .await?;
384
385 Ok(result.node.backend_node_id)
386 }
387
388 pub(super) async fn release_object(&self, object_id: &str) -> Result<(), PageError> {
390 let _: serde_json::Value = self
391 .connection
392 .send_command(
393 "Runtime.releaseObject",
394 Some(serde_json::json!({
395 "objectId": object_id
396 })),
397 Some(&self.session_id),
398 )
399 .await?;
400
401 Ok(())
402 }
403}