use std::collections::HashMap;
use tracing::{debug, instrument, trace, warn};
use viewpoint_cdp::protocol::dom::{BackendNodeId, DescribeNodeParams, DescribeNodeResult};
use viewpoint_js::js;
use super::locator::aria_js::aria_snapshot_with_refs_js;
use super::locator::AriaSnapshot;
use super::ref_resolution::format_ref;
use super::Page;
use crate::error::PageError;
impl Page {
#[instrument(level = "debug", skip(self), fields(target_id = %self.target_id))]
pub async fn aria_snapshot_with_frames(&self) -> Result<AriaSnapshot, PageError> {
if self.closed {
return Err(PageError::Closed);
}
let main_frame = self.main_frame().await?;
let mut root_snapshot = main_frame.aria_snapshot().await?;
let frames = self.frames().await?;
let mut frame_snapshots: HashMap<String, AriaSnapshot> = HashMap::new();
for frame in &frames {
if !frame.is_main() {
match frame.aria_snapshot().await {
Ok(snapshot) => {
let url = frame.url();
if !url.is_empty() && url != "about:blank" {
frame_snapshots.insert(url.clone(), snapshot.clone());
}
let name = frame.name();
if !name.is_empty() {
frame_snapshots.insert(name.clone(), snapshot.clone());
}
frame_snapshots.insert(frame.id().to_string(), snapshot);
}
Err(e) => {
warn!(
error = %e,
frame_id = %frame.id(),
frame_url = %frame.url(),
"Failed to capture frame snapshot, skipping"
);
}
}
}
}
stitch_frame_content(&mut root_snapshot, &frame_snapshots, 0);
Ok(root_snapshot)
}
#[instrument(level = "debug", skip(self), fields(target_id = %self.target_id))]
pub async fn aria_snapshot(&self) -> Result<AriaSnapshot, PageError> {
if self.closed {
return Err(PageError::Closed);
}
self.capture_snapshot_with_refs().await
}
#[instrument(level = "debug", skip(self), fields(target_id = %self.target_id))]
async fn capture_snapshot_with_refs(&self) -> Result<AriaSnapshot, PageError> {
let snapshot_fn = aria_snapshot_with_refs_js();
let js_code = js! {
(function() {
const getSnapshotWithRefs = @{snapshot_fn};
return getSnapshotWithRefs(document.body);
})()
};
let result: viewpoint_cdp::protocol::runtime::EvaluateResult = self
.connection()
.send_command(
"Runtime.evaluate",
Some(viewpoint_cdp::protocol::runtime::EvaluateParams {
expression: js_code,
object_group: Some("viewpoint-snapshot".to_string()),
include_command_line_api: None,
silent: Some(true),
context_id: None,
return_by_value: Some(false), await_promise: Some(false),
}),
Some(self.session_id()),
)
.await?;
if let Some(exception) = result.exception_details {
return Err(PageError::EvaluationFailed(exception.text));
}
let result_object_id = result.result.object_id.ok_or_else(|| {
PageError::EvaluationFailed("No object ID from snapshot evaluation".to_string())
})?;
let snapshot_value = self.get_property_value(&result_object_id, "snapshot").await?;
let mut snapshot: AriaSnapshot = serde_json::from_value(snapshot_value).map_err(|e| {
PageError::EvaluationFailed(format!("Failed to parse aria snapshot: {e}"))
})?;
let elements_result = self.get_property_object(&result_object_id, "elements").await?;
if let Some(elements_object_id) = elements_result {
let length_value = self.get_property_value(&elements_object_id, "length").await?;
let element_count = length_value.as_u64().unwrap_or(0) as usize;
debug!(element_count = element_count, "Resolving element refs");
let mut ref_map: HashMap<usize, BackendNodeId> = HashMap::new();
for i in 0..element_count {
if let Ok(Some(element_object_id)) = self.get_array_element(&elements_object_id, i).await {
match self.describe_node(&element_object_id).await {
Ok(backend_node_id) => {
ref_map.insert(i, backend_node_id);
trace!(index = i, backend_node_id = backend_node_id, "Resolved element ref");
}
Err(e) => {
debug!(index = i, error = %e, "Failed to get backendNodeId for element");
}
}
}
}
apply_refs_to_snapshot(&mut snapshot, &ref_map);
let _ = self.release_object(&elements_object_id).await;
}
let _ = self.release_object(&result_object_id).await;
Ok(snapshot)
}
async fn get_property_value(
&self,
object_id: &str,
property: &str,
) -> Result<serde_json::Value, PageError> {
#[derive(Debug, serde::Deserialize)]
struct CallResult {
result: viewpoint_cdp::protocol::runtime::RemoteObject,
}
let result: CallResult = self
.connection()
.send_command(
"Runtime.callFunctionOn",
Some(serde_json::json!({
"objectId": object_id,
"functionDeclaration": format!("function() {{ return this.{}; }}", property),
"returnByValue": true
})),
Some(self.session_id()),
)
.await?;
Ok(result.result.value.unwrap_or(serde_json::Value::Null))
}
async fn get_property_object(
&self,
object_id: &str,
property: &str,
) -> Result<Option<String>, PageError> {
#[derive(Debug, serde::Deserialize)]
struct CallResult {
result: viewpoint_cdp::protocol::runtime::RemoteObject,
}
let result: CallResult = self
.connection()
.send_command(
"Runtime.callFunctionOn",
Some(serde_json::json!({
"objectId": object_id,
"functionDeclaration": format!("function() {{ return this.{}; }}", property),
"returnByValue": false
})),
Some(self.session_id()),
)
.await?;
Ok(result.result.object_id)
}
async fn get_array_element(
&self,
array_object_id: &str,
index: usize,
) -> Result<Option<String>, PageError> {
#[derive(Debug, serde::Deserialize)]
struct CallResult {
result: viewpoint_cdp::protocol::runtime::RemoteObject,
}
let result: CallResult = self
.connection()
.send_command(
"Runtime.callFunctionOn",
Some(serde_json::json!({
"objectId": array_object_id,
"functionDeclaration": format!("function() {{ return this[{}]; }}", index),
"returnByValue": false
})),
Some(self.session_id()),
)
.await?;
Ok(result.result.object_id)
}
async fn describe_node(&self, object_id: &str) -> Result<BackendNodeId, PageError> {
let result: DescribeNodeResult = self
.connection()
.send_command(
"DOM.describeNode",
Some(DescribeNodeParams {
node_id: None,
backend_node_id: None,
object_id: Some(object_id.to_string()),
depth: Some(0),
pierce: None,
}),
Some(self.session_id()),
)
.await?;
Ok(result.node.backend_node_id)
}
async fn release_object(&self, object_id: &str) -> Result<(), PageError> {
let _: serde_json::Value = self
.connection()
.send_command(
"Runtime.releaseObject",
Some(serde_json::json!({
"objectId": object_id
})),
Some(self.session_id()),
)
.await?;
Ok(())
}
}
pub(crate) fn apply_refs_to_snapshot(snapshot: &mut AriaSnapshot, ref_map: &HashMap<usize, BackendNodeId>) {
if let Some(index) = snapshot.element_index {
if let Some(&backend_node_id) = ref_map.get(&index) {
snapshot.node_ref = Some(format_ref(backend_node_id));
}
snapshot.element_index = None;
}
for child in &mut snapshot.children {
apply_refs_to_snapshot(child, ref_map);
}
}
fn stitch_frame_content(
snapshot: &mut AriaSnapshot,
frame_snapshots: &HashMap<String, AriaSnapshot>,
depth: usize,
) {
const MAX_DEPTH: usize = 10;
if depth > MAX_DEPTH {
warn!(
depth = depth,
"Max frame nesting depth exceeded, stopping recursion"
);
return;
}
if snapshot.is_frame == Some(true) {
let frame_snapshot = snapshot
.frame_url
.as_ref()
.and_then(|url| frame_snapshots.get(url))
.or_else(|| {
snapshot
.frame_name
.as_ref()
.and_then(|name| frame_snapshots.get(name))
});
if let Some(frame_content) = frame_snapshot {
debug!(
frame_url = ?snapshot.frame_url,
frame_name = ?snapshot.frame_name,
depth = depth,
"Stitching frame content into snapshot"
);
snapshot.is_frame = Some(false);
snapshot.children = vec![frame_content.clone()];
} else {
debug!(
frame_url = ?snapshot.frame_url,
frame_name = ?snapshot.frame_name,
"No matching frame snapshot found for iframe boundary"
);
}
}
for child in &mut snapshot.children {
stitch_frame_content(child, frame_snapshots, depth + 1);
}
}