viewpoint_core/page/frame/
aria.rs1use std::collections::HashMap;
4
5use tracing::{debug, instrument, trace};
6use viewpoint_cdp::protocol::dom::{BackendNodeId, DescribeNodeParams, DescribeNodeResult};
7use viewpoint_cdp::protocol::runtime::EvaluateParams;
8use viewpoint_js::js;
9
10use super::Frame;
11use crate::error::PageError;
12use crate::page::aria_snapshot::apply_refs_to_snapshot;
13use crate::page::locator::aria_js::aria_snapshot_with_refs_js;
14
15impl Frame {
16 #[instrument(level = "debug", skip(self), fields(frame_id = %self.id))]
41 pub async fn aria_snapshot(&self) -> Result<crate::page::locator::AriaSnapshot, PageError> {
42 if self.is_detached() {
43 return Err(PageError::EvaluationFailed("Frame is detached".to_string()));
44 }
45
46 self.capture_snapshot_with_refs().await
48 }
49
50 #[instrument(level = "debug", skip(self), fields(frame_id = %self.id))]
56 pub(super) async fn capture_snapshot_with_refs(
57 &self,
58 ) -> Result<crate::page::locator::AriaSnapshot, PageError> {
59 let snapshot_fn = aria_snapshot_with_refs_js();
60
61 let js_code = js! {
64 (function() {
65 const getSnapshotWithRefs = @{snapshot_fn};
66 return getSnapshotWithRefs(document.body);
67 })()
68 };
69
70 let context_id = self.main_world_context_id();
72 trace!(context_id = ?context_id, "Using execution context for aria_snapshot()");
73
74 let result: viewpoint_cdp::protocol::runtime::EvaluateResult = self
77 .connection
78 .send_command(
79 "Runtime.evaluate",
80 Some(EvaluateParams {
81 expression: js_code,
82 object_group: Some("viewpoint-snapshot".to_string()),
83 include_command_line_api: None,
84 silent: Some(true),
85 context_id,
86 return_by_value: Some(false), await_promise: Some(false),
88 }),
89 Some(&self.session_id),
90 )
91 .await?;
92
93 if let Some(exception) = result.exception_details {
94 return Err(PageError::EvaluationFailed(exception.text));
95 }
96
97 let result_object_id = result.result.object_id.ok_or_else(|| {
98 PageError::EvaluationFailed("No object ID from snapshot evaluation".to_string())
99 })?;
100
101 let snapshot_value = self.get_property_value(&result_object_id, "snapshot").await?;
103
104 let mut snapshot: crate::page::locator::AriaSnapshot =
106 serde_json::from_value(snapshot_value).map_err(|e| {
107 PageError::EvaluationFailed(format!("Failed to parse aria snapshot: {e}"))
108 })?;
109
110 let elements_result = self.get_property_object(&result_object_id, "elements").await?;
112
113 if let Some(elements_object_id) = elements_result {
114 let length_value = self
116 .get_property_value(&elements_object_id, "length")
117 .await?;
118 let element_count = length_value.as_u64().unwrap_or(0) as usize;
119
120 debug!(element_count = element_count, "Resolving element refs");
121
122 let mut ref_map: HashMap<usize, BackendNodeId> = HashMap::new();
124
125 for i in 0..element_count {
126 if let Ok(Some(element_object_id)) =
128 self.get_array_element(&elements_object_id, i).await
129 {
130 match self.describe_node(&element_object_id).await {
132 Ok(backend_node_id) => {
133 ref_map.insert(i, backend_node_id);
134 trace!(
135 index = i,
136 backend_node_id = backend_node_id,
137 "Resolved element ref"
138 );
139 }
140 Err(e) => {
141 debug!(index = i, error = %e, "Failed to get backendNodeId for element");
142 }
143 }
144 }
145 }
146
147 apply_refs_to_snapshot(&mut snapshot, &ref_map);
149
150 let _ = self.release_object(&elements_object_id).await;
152 }
153
154 let _ = self.release_object(&result_object_id).await;
156
157 Ok(snapshot)
158 }
159
160 pub(super) async fn get_property_value(
162 &self,
163 object_id: &str,
164 property: &str,
165 ) -> Result<serde_json::Value, PageError> {
166 #[derive(Debug, serde::Deserialize)]
167 struct CallResult {
168 result: viewpoint_cdp::protocol::runtime::RemoteObject,
169 }
170
171 let result: CallResult = self
172 .connection
173 .send_command(
174 "Runtime.callFunctionOn",
175 Some(serde_json::json!({
176 "objectId": object_id,
177 "functionDeclaration": format!("function() {{ return this.{}; }}", property),
178 "returnByValue": true
179 })),
180 Some(&self.session_id),
181 )
182 .await?;
183
184 Ok(result.result.value.unwrap_or(serde_json::Value::Null))
185 }
186
187 pub(super) async fn get_property_object(
189 &self,
190 object_id: &str,
191 property: &str,
192 ) -> Result<Option<String>, PageError> {
193 #[derive(Debug, serde::Deserialize)]
194 struct CallResult {
195 result: viewpoint_cdp::protocol::runtime::RemoteObject,
196 }
197
198 let result: CallResult = self
199 .connection
200 .send_command(
201 "Runtime.callFunctionOn",
202 Some(serde_json::json!({
203 "objectId": object_id,
204 "functionDeclaration": format!("function() {{ return this.{}; }}", property),
205 "returnByValue": false
206 })),
207 Some(&self.session_id),
208 )
209 .await?;
210
211 Ok(result.result.object_id)
212 }
213
214 pub(super) async fn get_array_element(
216 &self,
217 array_object_id: &str,
218 index: usize,
219 ) -> Result<Option<String>, PageError> {
220 #[derive(Debug, serde::Deserialize)]
221 struct CallResult {
222 result: viewpoint_cdp::protocol::runtime::RemoteObject,
223 }
224
225 let result: CallResult = self
226 .connection
227 .send_command(
228 "Runtime.callFunctionOn",
229 Some(serde_json::json!({
230 "objectId": array_object_id,
231 "functionDeclaration": format!("function() {{ return this[{}]; }}", index),
232 "returnByValue": false
233 })),
234 Some(&self.session_id),
235 )
236 .await?;
237
238 Ok(result.result.object_id)
239 }
240
241 pub(super) async fn describe_node(&self, object_id: &str) -> Result<BackendNodeId, PageError> {
243 let result: DescribeNodeResult = self
244 .connection
245 .send_command(
246 "DOM.describeNode",
247 Some(DescribeNodeParams {
248 node_id: None,
249 backend_node_id: None,
250 object_id: Some(object_id.to_string()),
251 depth: Some(0),
252 pierce: None,
253 }),
254 Some(&self.session_id),
255 )
256 .await?;
257
258 Ok(result.node.backend_node_id)
259 }
260
261 pub(super) async fn release_object(&self, object_id: &str) -> Result<(), PageError> {
263 let _: serde_json::Value = self
264 .connection
265 .send_command(
266 "Runtime.releaseObject",
267 Some(serde_json::json!({
268 "objectId": object_id
269 })),
270 Some(&self.session_id),
271 )
272 .await?;
273
274 Ok(())
275 }
276}