use std::time::Duration;
use serde::Deserialize;
use tracing::{debug, instrument};
use viewpoint_cdp::protocol::dom::{BackendNodeId, ResolveNodeParams, ResolveNodeResult};
use viewpoint_js::js;
use super::Locator;
use super::Selector;
use crate::error::LocatorError;
impl Locator<'_> {
#[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
pub async fn highlight(&self) -> Result<(), LocatorError> {
self.highlight_for(Duration::from_secs(2)).await
}
#[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
pub async fn highlight_for(&self, duration: Duration) -> Result<(), LocatorError> {
self.wait_for_actionable().await?;
debug!(?duration, "Highlighting element");
if let Selector::Ref(ref_str) = &self.selector {
let backend_node_id = self.page.get_backend_node_id_for_ref(ref_str)?;
return self
.highlight_by_backend_id(backend_node_id, duration)
.await;
}
if let Selector::BackendNodeId(backend_node_id) = &self.selector {
return self
.highlight_by_backend_id(*backend_node_id, duration)
.await;
}
let selector_expr = self.selector.to_js_expression();
let highlight_js = js! {
(function() {
const elements = @{selector_expr};
if (elements.length === 0) return { found: false };
const el = elements[0];
const originalOutline = el.style.outline;
const originalOutlineOffset = el.style.outlineOffset;
const originalTransition = el.style.transition;
el.style.transition = "outline 0.2s ease-in-out";
el.style.outline = "3px solid #ff00ff";
el.style.outlineOffset = "2px";
el.__viewpoint_original_outline = originalOutline;
el.__viewpoint_original_outline_offset = originalOutlineOffset;
el.__viewpoint_original_transition = originalTransition;
return { found: true };
})()
};
let result = self.evaluate_js(&highlight_js).await?;
let found = result
.get("found")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
if !found {
return Err(LocatorError::NotFound(format!("{:?}", self.selector)));
}
tokio::time::sleep(duration).await;
let cleanup_js = js! {
(function() {
const elements = @{selector_expr};
if (elements.length === 0) return;
const el = elements[0];
el.style.outline = el.__viewpoint_original_outline || "";
el.style.outlineOffset = el.__viewpoint_original_outline_offset || "";
el.style.transition = el.__viewpoint_original_transition || "";
delete el.__viewpoint_original_outline;
delete el.__viewpoint_original_outline_offset;
delete el.__viewpoint_original_transition;
})()
};
let _ = self.evaluate_js(&cleanup_js).await;
Ok(())
}
async fn highlight_by_backend_id(
&self,
backend_node_id: BackendNodeId,
duration: Duration,
) -> Result<(), LocatorError> {
let result: ResolveNodeResult = self
.page
.connection()
.send_command(
"DOM.resolveNode",
Some(ResolveNodeParams {
node_id: None,
backend_node_id: Some(backend_node_id),
object_group: Some("viewpoint-highlight".to_string()),
execution_context_id: None,
}),
Some(self.page.session_id()),
)
.await
.map_err(|_| {
LocatorError::NotFound(format!(
"Could not resolve backend node ID {backend_node_id}: element may no longer exist"
))
})?;
let object_id = result.object.object_id.ok_or_else(|| {
LocatorError::NotFound(format!(
"No object ID for backend node ID {backend_node_id}"
))
})?;
#[derive(Debug, Deserialize)]
struct CallResult {
result: viewpoint_cdp::protocol::runtime::RemoteObject,
#[serde(rename = "exceptionDetails")]
exception_details: Option<viewpoint_cdp::protocol::runtime::ExceptionDetails>,
}
let js_highlight = js! {
(function() {
const el = this;
const originalOutline = el.style.outline;
const originalOutlineOffset = el.style.outlineOffset;
const originalTransition = el.style.transition;
el.style.transition = "outline 0.2s ease-in-out";
el.style.outline = "3px solid #ff00ff";
el.style.outlineOffset = "2px";
el.__viewpoint_original_outline = originalOutline;
el.__viewpoint_original_outline_offset = originalOutlineOffset;
el.__viewpoint_original_transition = originalTransition;
return { found: true };
})
};
let js_highlight = js_highlight.trim_start_matches('(').trim_end_matches(')');
let call_result: CallResult = self
.page
.connection()
.send_command(
"Runtime.callFunctionOn",
Some(serde_json::json!({
"objectId": object_id,
"functionDeclaration": js_highlight,
"returnByValue": true
})),
Some(self.page.session_id()),
)
.await?;
if let Some(exception) = call_result.exception_details {
let _ = self
.page
.connection()
.send_command::<_, serde_json::Value>(
"Runtime.releaseObject",
Some(serde_json::json!({ "objectId": object_id })),
Some(self.page.session_id()),
)
.await;
return Err(LocatorError::EvaluationError(exception.text));
}
tokio::time::sleep(duration).await;
let js_remove_highlight = js! {
(function() {
const el = this;
el.style.outline = el.__viewpoint_original_outline || "";
el.style.outlineOffset = el.__viewpoint_original_outline_offset || "";
el.style.transition = el.__viewpoint_original_transition || "";
delete el.__viewpoint_original_outline;
delete el.__viewpoint_original_outline_offset;
delete el.__viewpoint_original_transition;
})
};
let js_remove_highlight = js_remove_highlight
.trim_start_matches('(')
.trim_end_matches(')');
let _ = self
.page
.connection()
.send_command::<_, CallResult>(
"Runtime.callFunctionOn",
Some(serde_json::json!({
"objectId": object_id,
"functionDeclaration": js_remove_highlight,
"returnByValue": true
})),
Some(self.page.session_id()),
)
.await;
let _ = self
.page
.connection()
.send_command::<_, serde_json::Value>(
"Runtime.releaseObject",
Some(serde_json::json!({ "objectId": object_id })),
Some(self.page.session_id()),
)
.await;
Ok(())
}
}