#![cfg(feature = "cdp-backend")]
use std::collections::BTreeMap;
use crate::render::chrome::page::Page;
use crate::render::chrome_protocol::cdp::browser_protocol::dom::{
BackendNodeId, GetBoxModelParams, ResolveNodeParams,
};
use crate::render::chrome_protocol::cdp::js_protocol::runtime::{
CallFunctionOnParams, RemoteObjectId,
};
use rand::rngs::SmallRng;
use rand::RngExt;
use crate::render::interact::{click_point, dispatch_typing, MousePos, Rect};
use crate::{Error, Result};
pub fn lookup_backend_node_id(
ref_id: &str,
ref_map: &BTreeMap<String, i64>,
) -> Option<BackendNodeId> {
ref_map.get(ref_id).copied().map(BackendNodeId::new)
}
pub async fn backend_node_rect(page: &Page, bnid: BackendNodeId) -> Result<Option<Rect>> {
let params = GetBoxModelParams::builder().backend_node_id(bnid).build();
let model = match page.execute(params).await {
Ok(r) => r.result.model.clone(),
Err(_) => return Ok(None),
};
let q = model.content.inner();
if q.len() < 8 {
return Ok(None);
}
let xs = [q[0], q[2], q[4], q[6]];
let ys = [q[1], q[3], q[5], q[7]];
let x = xs.iter().cloned().fold(f64::INFINITY, f64::min);
let y = ys.iter().cloned().fold(f64::INFINITY, f64::min);
let x_max = xs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let y_max = ys.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let w = (x_max - x).max(1.0);
let h = (y_max - y).max(1.0);
Ok(Some(Rect { x, y, w, h }))
}
pub async fn click_by_backend_node(
page: &Page,
bnid: BackendNodeId,
from: MousePos,
) -> Result<MousePos> {
let rect = backend_node_rect(page, bnid)
.await?
.ok_or_else(|| Error::Render(format!("no box for backend_node_id={}", bnid.inner())))?;
let mut rng = rand::make_rng::<SmallRng>();
let tx = rect.x + rect.w * rng.random_range(0.25..0.75);
let ty = rect.y + rect.h * rng.random_range(0.25..0.75);
let target_width = rect.w.min(rect.h).max(10.0);
click_point(page, from, tx, ty, target_width).await
}
async fn resolve_to_object_id(page: &Page, bnid: BackendNodeId) -> Result<RemoteObjectId> {
let params = ResolveNodeParams::builder().backend_node_id(bnid).build();
let resp = page
.execute(params)
.await
.map_err(|e| Error::Render(format!("DOM.resolveNode: {e}")))?;
resp.result
.object
.object_id
.clone()
.ok_or_else(|| Error::Render("DOM.resolveNode returned no objectId".into()))
}
async fn focus_by_backend_node(page: &Page, bnid: BackendNodeId) -> Result<()> {
let object_id = resolve_to_object_id(page, bnid).await?;
let params = CallFunctionOnParams::builder()
.object_id(object_id)
.function_declaration("function() { this.focus(); }")
.await_promise(true)
.build()
.map_err(|e| Error::Render(format!("callFunctionOn build: {e}")))?;
page.execute(params)
.await
.map_err(|e| Error::Render(format!("callFunctionOn focus: {e}")))?;
Ok(())
}
pub async fn type_by_backend_node(page: &Page, bnid: BackendNodeId, text: &str) -> Result<()> {
focus_by_backend_node(page, bnid).await?;
dispatch_typing(page, text).await
}
#[cfg(test)]
mod tests {
use super::*;
use crate::script::spec::Locator;
#[test]
fn lookup_returns_none_for_missing_ref() {
let mut m = BTreeMap::new();
m.insert("@e1".into(), 42i64);
assert!(lookup_backend_node_id("@e99", &m).is_none());
}
#[test]
fn lookup_returns_backend_id_for_known_ref() {
let mut m = BTreeMap::new();
m.insert("@e1".into(), 42i64);
m.insert("@e2".into(), 1337i64);
let got = lookup_backend_node_id("@e2", &m).expect("should find");
assert_eq!(got.inner(), &1337);
}
#[test]
fn locator_ax_ref_round_trips_through_map() {
let mut m = BTreeMap::new();
m.insert("@e3".into(), 7i64);
let loc = Locator::Raw("@e3".into());
let ref_id = loc.ax_ref().expect("should be an ax ref");
let bnid = lookup_backend_node_id(ref_id, &m).expect("resolve");
assert_eq!(bnid.inner(), &7);
}
#[test]
fn non_ax_selectors_return_none() {
assert!(Locator::Raw("#login".into()).ax_ref().is_none());
assert!(Locator::Raw("role=button".into()).ax_ref().is_none());
assert!(Locator::Raw("@named-selector".into()).ax_ref().is_none());
}
}