use std::collections::HashMap;
use crate::cdp::client::{CdpClient, CdpClientError};
use crate::cdp::types::{AXNode, GetFullAXTreeResult};
use crate::element_ref::ElementRef;
use crate::snapshot_render::format_ax_tree;
pub use crate::snapshot_secret::Redaction;
pub struct Snapshot {
pub text: String,
pub uid_map: HashMap<String, ElementRef>,
pub identity: Option<(String, String)>,
}
pub async fn document_identity(client: &CdpClient) -> Option<(String, String)> {
let tree: serde_json::Value = client.call("Page.getFrameTree", serde_json::json!({})).await.ok()?;
let root = tree.get("frameTree")?;
let wanted = client.frame_context().map(|c| c.frame_id);
find_frame(root, wanted.as_deref())
}
fn find_frame(node: &serde_json::Value, wanted: Option<&str>) -> Option<(String, String)> {
let frame = node.get("frame")?;
let id = frame.get("id")?.as_str()?.to_string();
let loader = frame.get("loaderId")?.as_str()?.to_string();
if wanted.is_none_or(|w| w == id) {
return Some((id, loader));
}
node.get("childFrames")?
.as_array()?
.iter()
.find_map(|child| find_frame(child, wanted))
}
pub async fn settle(client: &CdpClient, quiet_ms: u32, hard_ms: u32) {
let expression = format!(
r"new Promise(resolve => {{
let settled = false, quiet = null, obs = null;
const finish = () => {{
if (settled) return;
settled = true;
clearTimeout(quiet);
clearTimeout(hard);
if (obs) obs.disconnect();
resolve();
}};
quiet = setTimeout(finish, {quiet_ms});
const hard = setTimeout(finish, {hard_ms});
obs = new MutationObserver(() => {{
clearTimeout(quiet);
quiet = setTimeout(finish, {quiet_ms});
}});
obs.observe(document.body || document.documentElement, {{
childList: true, subtree: true, attributes: true, characterData: true
}});
}})"
);
let _ = client
.call::<_, serde_json::Value>(
"Runtime.evaluate",
serde_json::json!({
"expression": expression,
"awaitPromise": true,
"returnByValue": true,
}),
)
.await;
}
pub struct Views {
pub full: Snapshot,
shown: Option<String>,
}
impl Views {
pub fn shown(&self) -> &str {
self.shown.as_deref().unwrap_or(&self.full.text)
}
pub const fn from_parts(full: Snapshot, shown: Option<String>) -> Self {
Self { full, shown }
}
}
pub async fn take_snapshot(
client: &CdpClient,
verbose: bool,
max_depth: Option<usize>,
focus_uid: Option<&str>,
role_filter: Option<&[&str]>,
) -> Result<Snapshot, CdpClientError> {
let (nodes, redaction) = fetch_tree(client).await?;
let (text, uid_map, _) =
format_ax_tree(&nodes, verbose, max_depth, focus_uid, role_filter, &redaction, None);
let identity = document_identity(client).await;
Ok(Snapshot { text, uid_map, identity })
}
pub async fn take_views(
client: &CdpClient,
verbose: bool,
max_depth: Option<usize>,
focus_uid: Option<&str>,
role_filter: Option<&[&str]>,
) -> Result<Views, CdpClientError> {
let (nodes, redaction) = fetch_tree(client).await?;
let (text, uid_map, anon) =
format_ax_tree(&nodes, verbose, None, None, None, &redaction, None);
let identity = document_identity(client).await;
let full = Snapshot { text, uid_map, identity };
let reduced = max_depth.is_some() || focus_uid.is_some() || role_filter.is_some();
let shown = reduced.then(|| {
format_ax_tree(&nodes, verbose, max_depth, focus_uid, role_filter, &redaction, Some(&anon)).0
});
Ok(Views { full, shown })
}
async fn fetch_tree(client: &CdpClient) -> Result<(Vec<AXNode>, Redaction), CdpClientError> {
client
.send("Accessibility.enable", serde_json::json!({}))
.await?;
let mut params = serde_json::json!({});
if let Some(ctx) = client.frame_context() {
params["frameId"] = serde_json::json!(ctx.frame_id);
}
let result: GetFullAXTreeResult = client
.call("Accessibility.getFullAXTree", params)
.await?;
let redaction = crate::snapshot_secret::probe(client, &result.nodes).await;
Ok((result.nodes, redaction))
}
#[cfg(test)]
mod tests {
#[test]
fn bug_content_center_empty_quad() {
use crate::cdp::types::BoxModel;
let model = BoxModel {
content: vec![], border: vec![],
};
let (x, y) = model.content_center();
assert!(x.abs() < f64::EPSILON);
assert!(y.abs() < f64::EPSILON);
}
}