use serde::Deserialize;
use tracing::debug;
use viewpoint_cdp::protocol::input::{
DispatchKeyEventParams, DispatchMouseEventParams, InsertTextParams, MouseButton,
};
use viewpoint_js::js;
use super::frame_locator::FrameElementLocator;
use crate::error::LocatorError;
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct FrameElementInfo {
pub found: bool,
pub count: usize,
pub visible: Option<bool>,
pub enabled: Option<bool>,
pub x: Option<f64>,
pub y: Option<f64>,
pub width: Option<f64>,
pub height: Option<f64>,
pub text: Option<String>,
pub error: Option<String>,
}
impl FrameElementLocator<'_> {
#[tracing::instrument(level = "debug", skip(self), fields(selector = ?self.selector()))]
pub async fn click(&self) -> Result<(), LocatorError> {
let info = self.wait_for_actionable().await?;
let x = info.x.expect("visible element has x")
+ info.width.expect("visible element has width") / 2.0;
let y = info.y.expect("visible element has y")
+ info.height.expect("visible element has height") / 2.0;
debug!(x, y, "Clicking element in frame");
self.dispatch_mouse_event(DispatchMouseEventParams::mouse_move(x, y))
.await?;
self.dispatch_mouse_event(DispatchMouseEventParams::mouse_down(
x,
y,
MouseButton::Left,
))
.await?;
self.dispatch_mouse_event(DispatchMouseEventParams::mouse_up(x, y, MouseButton::Left))
.await?;
Ok(())
}
#[tracing::instrument(level = "debug", skip(self), fields(selector = ?self.selector()))]
pub async fn fill(&self, text: &str) -> Result<(), LocatorError> {
let _info = self.wait_for_actionable().await?;
debug!(text, "Filling element in frame");
self.focus_element().await?;
let mut select_all = DispatchKeyEventParams::key_down("a");
select_all.modifiers = Some(viewpoint_cdp::protocol::input::modifiers::CTRL);
self.dispatch_key_event(select_all).await?;
self.dispatch_key_event(DispatchKeyEventParams::key_down("Backspace"))
.await?;
self.insert_text(text).await?;
Ok(())
}
#[tracing::instrument(level = "debug", skip(self), fields(selector = ?self.selector()))]
pub async fn type_text(&self, text: &str) -> Result<(), LocatorError> {
self.wait_for_actionable().await?;
debug!(text, "Typing text in frame element");
self.focus_element().await?;
for ch in text.chars() {
let char_str = ch.to_string();
self.dispatch_key_event(DispatchKeyEventParams::char(&char_str))
.await?;
}
Ok(())
}
#[tracing::instrument(level = "debug", skip(self), fields(selector = ?self.selector()))]
pub async fn hover(&self) -> Result<(), LocatorError> {
let info = self.wait_for_actionable().await?;
let x = info.x.expect("visible element has x")
+ info.width.expect("visible element has width") / 2.0;
let y = info.y.expect("visible element has y")
+ info.height.expect("visible element has height") / 2.0;
debug!(x, y, "Hovering over element in frame");
self.dispatch_mouse_event(DispatchMouseEventParams::mouse_move(x, y))
.await?;
Ok(())
}
pub async fn text_content(&self) -> Result<Option<String>, LocatorError> {
let info = self.query_element_info().await?;
Ok(info.text)
}
pub async fn is_visible(&self) -> Result<bool, LocatorError> {
let info = self.query_element_info().await?;
Ok(info.visible.unwrap_or(false))
}
pub async fn count(&self) -> Result<usize, LocatorError> {
let info = self.query_element_info().await?;
Ok(info.count)
}
pub(crate) async fn wait_for_actionable(&self) -> Result<FrameElementInfo, LocatorError> {
let start = std::time::Instant::now();
let timeout = self.options().timeout;
loop {
let info = self.query_element_info().await?;
if let Some(error) = &info.error {
if start.elapsed() >= timeout {
return Err(LocatorError::NotFound(error.clone()));
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
continue;
}
if !info.found {
if start.elapsed() >= timeout {
return Err(LocatorError::NotFound(format!("{:?}", self.selector())));
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
continue;
}
if !info.visible.unwrap_or(false) {
if start.elapsed() >= timeout {
return Err(LocatorError::NotVisible);
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
continue;
}
return Ok(info);
}
}
pub(crate) async fn query_element_info(&self) -> Result<FrameElementInfo, LocatorError> {
let frame_access = self.frame_locator().to_js_frame_access();
let element_selector = self.selector().to_js_expression();
let js_code = js! {
(function() {
const frameDoc = @{frame_access};
if (!frameDoc) {
return { found: false, count: 0, error: "Frame not found or not accessible" };
}
let elements;
try {
elements = (function() {
const document = frameDoc;
return Array.from(@{element_selector});
})();
} catch (e) {
return { found: false, count: 0, error: e.message };
}
if (elements.length === 0) {
return { found: false, count: 0 };
}
const el = elements[0];
const rect = el.getBoundingClientRect();
let frameRect = { x: 0, y: 0 };
let current = frameDoc.defaultView?.frameElement;
while (current) {
const currentRect = current.getBoundingClientRect();
frameRect.x += currentRect.x;
frameRect.y += currentRect.y;
current = current.ownerDocument?.defaultView?.frameElement;
}
const style = frameDoc.defaultView?.getComputedStyle(el) || window.getComputedStyle(el);
const visible = rect.width > 0 && rect.height > 0 &&
style.visibility !== "hidden" &&
style.display !== "none" &&
parseFloat(style.opacity) > 0;
return {
found: true,
count: elements.length,
visible: visible,
enabled: !el.disabled,
x: frameRect.x + rect.x,
y: frameRect.y + rect.y,
width: rect.width,
height: rect.height,
text: el.textContent
};
})()
};
let result = self.evaluate_js(&js_code).await?;
let info: FrameElementInfo = serde_json::from_value(result)
.map_err(|e| LocatorError::EvaluationError(e.to_string()))?;
Ok(info)
}
pub(crate) async fn focus_element(&self) -> Result<(), LocatorError> {
let frame_access = self.frame_locator().to_js_frame_access();
let element_selector = self.selector().to_js_expression();
let js_code = js! {
(function() {
const frameDoc = @{frame_access};
if (!frameDoc) return false;
const elements = (function() {
const document = frameDoc;
return Array.from(@{element_selector});
})();
if (elements.length > 0) {
elements[0].focus();
return true;
}
return false;
})()
};
self.evaluate_js(&js_code).await?;
Ok(())
}
pub(crate) async fn evaluate_js(
&self,
expression: &str,
) -> Result<serde_json::Value, LocatorError> {
let page = self.frame_locator().page();
if page.is_closed() {
return Err(LocatorError::PageClosed);
}
page.evaluate_js_raw(expression)
.await
.map_err(|e| LocatorError::EvaluationError(e.to_string()))
}
pub(crate) async fn dispatch_mouse_event(
&self,
params: DispatchMouseEventParams,
) -> Result<(), LocatorError> {
let page = self.frame_locator().page();
page.connection()
.send_command::<_, serde_json::Value>(
"Input.dispatchMouseEvent",
Some(params),
Some(page.session_id()),
)
.await?;
Ok(())
}
pub(crate) async fn dispatch_key_event(
&self,
params: DispatchKeyEventParams,
) -> Result<(), LocatorError> {
let page = self.frame_locator().page();
page.connection()
.send_command::<_, serde_json::Value>(
"Input.dispatchKeyEvent",
Some(params),
Some(page.session_id()),
)
.await?;
Ok(())
}
pub(crate) async fn insert_text(&self, text: &str) -> Result<(), LocatorError> {
let page = self.frame_locator().page();
page.connection()
.send_command::<_, serde_json::Value>(
"Input.insertText",
Some(InsertTextParams {
text: text.to_string(),
}),
Some(page.session_id()),
)
.await?;
Ok(())
}
}