use tracing::{debug, instrument};
use viewpoint_cdp::protocol::dom::{
BackendNodeId, DescribeNodeParams, DescribeNodeResult, ResolveNodeParams, ResolveNodeResult,
};
use super::Page;
use super::locator::ElementHandle;
use crate::error::{LocatorError, PageError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParsedRef {
pub context_index: usize,
pub page_index: usize,
pub frame_index: usize,
pub element_counter: usize,
}
impl ParsedRef {
pub fn new(
context_index: usize,
page_index: usize,
frame_index: usize,
element_counter: usize,
) -> Self {
Self {
context_index,
page_index,
frame_index,
element_counter,
}
}
}
pub fn parse_ref(ref_str: &str) -> Result<ParsedRef, LocatorError> {
if !ref_str.starts_with('c') {
return Err(LocatorError::EvaluationError(format!(
"Invalid ref format: expected 'c{{ctx}}p{{page}}f{{frame}}e{{counter}}', got '{ref_str}'"
)));
}
parse_ref_format(ref_str)
}
fn parse_ref_format(ref_str: &str) -> Result<ParsedRef, LocatorError> {
let without_c = ref_str.strip_prefix('c').ok_or_else(|| {
LocatorError::EvaluationError(format!(
"Invalid ref format: expected 'c' prefix in '{ref_str}'"
))
})?;
let (context_part, rest) = without_c.split_once('p').ok_or_else(|| {
LocatorError::EvaluationError(format!(
"Invalid ref format: expected 'p' separator in '{ref_str}'"
))
})?;
let (page_part, rest) = rest.split_once('f').ok_or_else(|| {
LocatorError::EvaluationError(format!(
"Invalid ref format: expected 'f' separator in '{ref_str}'"
))
})?;
let (frame_part, element_part) = rest.split_once('e').ok_or_else(|| {
LocatorError::EvaluationError(format!(
"Invalid ref format: expected 'e' separator in '{ref_str}'"
))
})?;
let context_index = context_part.parse::<usize>().map_err(|e| {
LocatorError::EvaluationError(format!("Invalid context index in ref '{ref_str}': {e}"))
})?;
let page_index = page_part.parse::<usize>().map_err(|e| {
LocatorError::EvaluationError(format!("Invalid page index in ref '{ref_str}': {e}"))
})?;
let frame_index = frame_part.parse::<usize>().map_err(|e| {
LocatorError::EvaluationError(format!("Invalid frame index in ref '{ref_str}': {e}"))
})?;
let element_counter = element_part.parse::<usize>().map_err(|e| {
LocatorError::EvaluationError(format!("Invalid element counter in ref '{ref_str}': {e}"))
})?;
Ok(ParsedRef::new(
context_index,
page_index,
frame_index,
element_counter,
))
}
pub fn format_ref(
context_index: usize,
page_index: usize,
frame_index: usize,
element_counter: usize,
) -> String {
format!("c{context_index}p{page_index}f{frame_index}e{element_counter}")
}
impl Page {
#[instrument(level = "debug", skip(self), fields(target_id = %self.target_id, ref_str = %ref_str))]
pub async fn element_from_ref(&self, ref_str: &str) -> Result<ElementHandle<'_>, LocatorError> {
if self.is_closed() {
return Err(LocatorError::PageClosed);
}
let parsed = parse_ref(ref_str)?;
if parsed.context_index != self.context_index {
return Err(LocatorError::EvaluationError(format!(
"Context index mismatch: ref '{ref_str}' is for context {}, but this page is in context {}",
parsed.context_index, self.context_index
)));
}
if parsed.page_index != self.page_index {
return Err(LocatorError::EvaluationError(format!(
"Page index mismatch: ref '{ref_str}' is for page {}, but this is page {}",
parsed.page_index, self.page_index
)));
}
debug!(
context_index = parsed.context_index,
page_index = parsed.page_index,
frame_index = parsed.frame_index,
element_counter = parsed.element_counter,
"Resolving ref to element"
);
let backend_node_id = self.get_backend_node_id_for_ref(ref_str)?;
let result: ResolveNodeResult = self
.connection()
.send_command(
"DOM.resolveNode",
Some(ResolveNodeParams {
node_id: None,
backend_node_id: Some(backend_node_id),
object_group: Some("viewpoint-ref".to_string()),
execution_context_id: None,
}),
Some(self.session_id()),
)
.await
.map_err(|e| {
LocatorError::NotFound(format!("Ref not found. Capture a new snapshot. Error: {e}"))
})?;
let object_id = result.object.object_id.ok_or_else(|| {
LocatorError::NotFound("Ref not found. Capture a new snapshot.".to_string())
})?;
debug!(object_id = %object_id, "Resolved ref to element handle");
Ok(ElementHandle {
object_id,
page: self,
})
}
pub fn locator_from_ref(&self, ref_str: &str) -> super::Locator<'_> {
use super::locator::{Locator, Selector};
let parsed = parse_ref(ref_str)
.expect("Invalid ref format. Refs must be in format 'c{ctx}p{page}f{frame}e{counter}'");
assert!(
parsed.context_index == self.context_index,
"Context index mismatch: ref is for context {}, but this page is in context {}",
parsed.context_index,
self.context_index
);
assert!(
parsed.page_index == self.page_index,
"Page index mismatch: ref is for page {}, but this is page {}",
parsed.page_index,
self.page_index
);
Locator::new(self, Selector::Ref(ref_str.to_string()))
}
pub(crate) async fn get_backend_node_id(
&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)
}
pub fn get_backend_node_id_for_ref(
&self,
ref_str: &str,
) -> Result<BackendNodeId, LocatorError> {
self.ref_map.read().get(ref_str).copied().ok_or_else(|| {
LocatorError::NotFound("Ref not found. Capture a new snapshot.".to_string())
})
}
pub(crate) fn store_ref_mapping(&self, ref_str: String, backend_node_id: BackendNodeId) {
self.ref_map.write().insert(ref_str, backend_node_id);
}
pub(crate) fn clear_ref_map(&self) {
self.ref_map.write().clear();
}
}
#[cfg(test)]
mod tests;