use std::time::Duration;
use serde_json::json;
use tokio::time::Instant;
use crate::element::Element;
use crate::error::{Result, ZendriverError};
#[allow(dead_code)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct ActionabilityCheck {
pub visible: bool,
pub stable: bool,
pub enabled: bool,
pub receives_pointer: bool,
}
impl ActionabilityCheck {
#[allow(dead_code)] pub(crate) const FULL: Self = Self {
visible: true,
stable: true,
enabled: true,
receives_pointer: true,
};
#[allow(dead_code)] pub(crate) const VISIBLE_ONLY: Self = Self {
visible: true,
stable: false,
enabled: false,
receives_pointer: false,
};
#[allow(dead_code)] pub(crate) const TEXT_INPUT: Self = Self {
visible: true,
stable: false,
enabled: true,
receives_pointer: false,
};
}
pub(crate) async fn check_visible(el: &Element) -> Result<bool> {
let js = r#"
function(el) {
if (!el || !el.isConnected) return false;
// offsetParent === null catches `display: none` on the element or any ancestor
// (except for fixed-position elements, which are handled by the bbox check below).
if (el.offsetParent === null && getComputedStyle(el).position !== 'fixed') return false;
const rect = el.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) return false;
const style = getComputedStyle(el);
if (style.visibility === 'hidden') return false;
if (style.opacity === '0') return false;
return true;
}
"#;
let res = el.call_on_main(js, json!([])).await?;
Ok(res.get("value").and_then(|v| v.as_bool()).unwrap_or(false))
}
#[allow(dead_code)] pub(crate) async fn check_stable(el: &Element) -> Result<bool> {
let js = r#"
function(el) {
return new Promise(resolve => {
if (!el || !el.isConnected) { resolve(false); return; }
const first = el.getBoundingClientRect();
requestAnimationFrame(() => {
requestAnimationFrame(() => {
const second = el.getBoundingClientRect();
const stable =
Math.abs(first.x - second.x) < 0.5 &&
Math.abs(first.y - second.y) < 0.5 &&
Math.abs(first.width - second.width) < 0.5 &&
Math.abs(first.height - second.height) < 0.5;
resolve(stable);
});
});
});
}
"#;
let res = el.call_on_main(js, json!([])).await?;
Ok(res.get("value").and_then(|v| v.as_bool()).unwrap_or(false))
}
#[allow(dead_code)] pub(crate) async fn check_enabled(el: &Element) -> Result<bool> {
let js = r#"
function(el) {
if (!el) return false;
// `disabled === false` for form controls; `undefined` for non-form elements
// (which we treat as enabled).
if (el.disabled === true) return false;
const ariaDisabled = el.getAttribute && el.getAttribute('aria-disabled');
if (ariaDisabled === 'true') return false;
return true;
}
"#;
let res = el.call_on_main(js, json!([])).await?;
Ok(res.get("value").and_then(|v| v.as_bool()).unwrap_or(false))
}
#[allow(dead_code)] pub(crate) async fn check_receives_pointer(el: &Element) -> Result<bool> {
let js = r#"
function(el) {
if (!el || !el.isConnected) return false;
const rect = el.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) return false;
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
let hit = document.elementFromPoint(cx, cy);
while (hit) {
if (hit === el) return true;
hit = hit.parentElement;
}
return false;
}
"#;
let res = el.call_on_main(js, json!([])).await?;
Ok(res.get("value").and_then(|v| v.as_bool()).unwrap_or(false))
}
#[allow(dead_code)] pub(crate) async fn wait_actionable(
el: &Element,
require: ActionabilityCheck,
timeout: Duration,
) -> Result<()> {
let deadline = Instant::now() + timeout;
let poll_interval = Duration::from_millis(50);
loop {
let mut failed_reason: Option<&'static str> = None;
if require.visible && !check_visible(el).await? {
failed_reason = Some("not visible");
} else if require.enabled && !check_enabled(el).await? {
failed_reason = Some("not enabled");
} else if require.stable && !check_stable(el).await? {
failed_reason = Some("not stable (still animating)");
} else if require.receives_pointer && !check_receives_pointer(el).await? {
failed_reason = Some("occluded by overlay");
}
match failed_reason {
None => return Ok(()),
Some(reason) => {
if Instant::now() >= deadline {
return Err(ZendriverError::NotActionable(timeout, reason.to_owned()));
}
tokio::time::sleep(poll_interval).await;
}
}
}
}