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::{apply_refs_to_snapshot, SnapshotOptions};
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 apply_refs_to_snapshot(&mut snapshot, &ref_map);
176
177 let _ = self.release_object(&elements_object_id).await;
179 }
180 }
181
182 let _ = self.release_object(&result_object_id).await;
184
185 Ok(snapshot)
186 }
187
188 async fn get_all_array_elements(
193 &self,
194 array_object_id: &str,
195 ) -> Result<Vec<(usize, String)>, PageError> {
196 #[derive(Debug, serde::Deserialize)]
197 struct PropertyDescriptor {
198 name: String,
199 value: Option<viewpoint_cdp::protocol::runtime::RemoteObject>,
200 }
201
202 #[derive(Debug, serde::Deserialize)]
203 struct GetPropertiesResult {
204 result: Vec<PropertyDescriptor>,
205 }
206
207 let result: GetPropertiesResult = self
208 .connection
209 .send_command(
210 "Runtime.getProperties",
211 Some(serde_json::json!({
212 "objectId": array_object_id,
213 "ownProperties": true,
214 "generatePreview": false
215 })),
216 Some(&self.session_id),
217 )
218 .await?;
219
220 let mut elements: Vec<(usize, String)> = Vec::new();
222
223 for prop in result.result {
224 if let Ok(index) = prop.name.parse::<usize>() {
226 if let Some(value) = prop.value {
227 if let Some(object_id) = value.object_id {
228 elements.push((index, object_id));
229 }
230 }
231 }
232 }
233
234 elements.sort_by_key(|(index, _)| *index);
236
237 trace!(element_count = elements.len(), "Batch-fetched array elements");
238
239 Ok(elements)
240 }
241
242 async fn resolve_node_ids_parallel(
247 &self,
248 element_object_ids: Vec<(usize, String)>,
249 max_concurrency: usize,
250 ) -> HashMap<usize, BackendNodeId> {
251 let mut ref_map = HashMap::new();
252
253 for chunk in element_object_ids.chunks(max_concurrency) {
255 let futures: FuturesUnordered<_> = chunk
256 .iter()
257 .map(|(index, object_id)| {
258 let index = *index;
259 let object_id = object_id.clone();
260 async move {
261 match self.describe_node(&object_id).await {
262 Ok(backend_node_id) => {
263 trace!(
264 index = index,
265 backend_node_id = backend_node_id,
266 "Resolved element ref"
267 );
268 Some((index, backend_node_id))
269 }
270 Err(e) => {
271 debug!(index = index, error = %e, "Failed to get backendNodeId for element");
272 None
273 }
274 }
275 }
276 })
277 .collect();
278
279 let results: Vec<_> = futures.collect().await;
281 for result in results.into_iter().flatten() {
282 ref_map.insert(result.0, result.1);
283 }
284 }
285
286 ref_map
287 }
288
289 pub(super) async fn get_property_value(
291 &self,
292 object_id: &str,
293 property: &str,
294 ) -> Result<serde_json::Value, PageError> {
295 #[derive(Debug, serde::Deserialize)]
296 struct CallResult {
297 result: viewpoint_cdp::protocol::runtime::RemoteObject,
298 }
299
300 let result: CallResult = self
301 .connection
302 .send_command(
303 "Runtime.callFunctionOn",
304 Some(serde_json::json!({
305 "objectId": object_id,
306 "functionDeclaration": format!("function() {{ return this.{}; }}", property),
307 "returnByValue": true
308 })),
309 Some(&self.session_id),
310 )
311 .await?;
312
313 Ok(result.result.value.unwrap_or(serde_json::Value::Null))
314 }
315
316 pub(super) async fn get_property_object(
318 &self,
319 object_id: &str,
320 property: &str,
321 ) -> Result<Option<String>, PageError> {
322 #[derive(Debug, serde::Deserialize)]
323 struct CallResult {
324 result: viewpoint_cdp::protocol::runtime::RemoteObject,
325 }
326
327 let result: CallResult = self
328 .connection
329 .send_command(
330 "Runtime.callFunctionOn",
331 Some(serde_json::json!({
332 "objectId": object_id,
333 "functionDeclaration": format!("function() {{ return this.{}; }}", property),
334 "returnByValue": false
335 })),
336 Some(&self.session_id),
337 )
338 .await?;
339
340 Ok(result.result.object_id)
341 }
342
343 pub(super) async fn describe_node(&self, object_id: &str) -> Result<BackendNodeId, PageError> {
345 let result: DescribeNodeResult = self
346 .connection
347 .send_command(
348 "DOM.describeNode",
349 Some(DescribeNodeParams {
350 node_id: None,
351 backend_node_id: None,
352 object_id: Some(object_id.to_string()),
353 depth: Some(0),
354 pierce: None,
355 }),
356 Some(&self.session_id),
357 )
358 .await?;
359
360 Ok(result.node.backend_node_id)
361 }
362
363 pub(super) async fn release_object(&self, object_id: &str) -> Result<(), PageError> {
365 let _: serde_json::Value = self
366 .connection
367 .send_command(
368 "Runtime.releaseObject",
369 Some(serde_json::json!({
370 "objectId": object_id
371 })),
372 Some(&self.session_id),
373 )
374 .await?;
375
376 Ok(())
377 }
378}