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 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!(
238 element_count = elements.len(),
239 "Batch-fetched array elements"
240 );
241
242 Ok(elements)
243 }
244
245 async fn resolve_node_ids_parallel(
250 &self,
251 element_object_ids: Vec<(usize, String)>,
252 max_concurrency: usize,
253 ) -> HashMap<usize, BackendNodeId> {
254 let mut ref_map = HashMap::new();
255
256 for chunk in element_object_ids.chunks(max_concurrency) {
258 let futures: FuturesUnordered<_> = chunk
259 .iter()
260 .map(|(index, object_id)| {
261 let index = *index;
262 let object_id = object_id.clone();
263 async move {
264 match self.describe_node(&object_id).await {
265 Ok(backend_node_id) => {
266 trace!(
267 index = index,
268 backend_node_id = backend_node_id,
269 "Resolved element ref"
270 );
271 Some((index, backend_node_id))
272 }
273 Err(e) => {
274 debug!(index = index, error = %e, "Failed to get backendNodeId for element");
275 None
276 }
277 }
278 }
279 })
280 .collect();
281
282 let results: Vec<_> = futures.collect().await;
284 for result in results.into_iter().flatten() {
285 ref_map.insert(result.0, result.1);
286 }
287 }
288
289 ref_map
290 }
291
292 pub(super) async fn get_property_value(
294 &self,
295 object_id: &str,
296 property: &str,
297 ) -> Result<serde_json::Value, PageError> {
298 #[derive(Debug, serde::Deserialize)]
299 struct CallResult {
300 result: viewpoint_cdp::protocol::runtime::RemoteObject,
301 }
302
303 let result: CallResult = self
304 .connection
305 .send_command(
306 "Runtime.callFunctionOn",
307 Some(serde_json::json!({
308 "objectId": object_id,
309 "functionDeclaration": format!("function() {{ return this.{}; }}", property),
310 "returnByValue": true
311 })),
312 Some(&self.session_id),
313 )
314 .await?;
315
316 Ok(result.result.value.unwrap_or(serde_json::Value::Null))
317 }
318
319 pub(super) async fn get_property_object(
321 &self,
322 object_id: &str,
323 property: &str,
324 ) -> Result<Option<String>, PageError> {
325 #[derive(Debug, serde::Deserialize)]
326 struct CallResult {
327 result: viewpoint_cdp::protocol::runtime::RemoteObject,
328 }
329
330 let result: CallResult = self
331 .connection
332 .send_command(
333 "Runtime.callFunctionOn",
334 Some(serde_json::json!({
335 "objectId": object_id,
336 "functionDeclaration": format!("function() {{ return this.{}; }}", property),
337 "returnByValue": false
338 })),
339 Some(&self.session_id),
340 )
341 .await?;
342
343 Ok(result.result.object_id)
344 }
345
346 pub(super) async fn describe_node(&self, object_id: &str) -> Result<BackendNodeId, PageError> {
348 let result: DescribeNodeResult = self
349 .connection
350 .send_command(
351 "DOM.describeNode",
352 Some(DescribeNodeParams {
353 node_id: None,
354 backend_node_id: None,
355 object_id: Some(object_id.to_string()),
356 depth: Some(0),
357 pierce: None,
358 }),
359 Some(&self.session_id),
360 )
361 .await?;
362
363 Ok(result.node.backend_node_id)
364 }
365
366 pub(super) async fn release_object(&self, object_id: &str) -> Result<(), PageError> {
368 let _: serde_json::Value = self
369 .connection
370 .send_command(
371 "Runtime.releaseObject",
372 Some(serde_json::json!({
373 "objectId": object_id
374 })),
375 Some(&self.session_id),
376 )
377 .await?;
378
379 Ok(())
380 }
381}